Skip to content

Commit ab2b895

Browse files
committed
feat(opencode): add multi-region support
1 parent f4f28bf commit ab2b895

7 files changed

Lines changed: 98 additions & 56 deletions

File tree

packages/opencode/src/plugin/kiro.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { setTimeout as sleep } from "node:timers/promises"
44
import path from "path"
55
import os from "os"
66

7-
const OIDC_ENDPOINT = "https://oidc.us-east-1.amazonaws.com"
7+
const oidc = (region: string) => `https://oidc.${region}.amazonaws.com`
88
const BUILDER_ID_URL = "https://view.awsapps.com/start"
99
const SCOPES = [
1010
"codewhisperer:completions",
@@ -86,16 +86,27 @@ export async function KiroAuthPlugin(_input: PluginInput): Promise<Hooks> {
8686
{
8787
type: "text" as const,
8888
key: "startUrl",
89-
message: "Enter your SSO start URL",
89+
message: "Enter your SSO start URL (defaults to $AWS_SSO_START_URL if set)",
9090
placeholder: "https://d-xxxxxxxxxx.awsapps.com/start",
9191
when: { key: "authType", op: "eq" as const, value: "idc" },
9292
},
93+
{
94+
type: "text" as const,
95+
key: "region",
96+
message: "Enter your AWS SSO region (defaults to $AWS_SSO_REGION if set)",
97+
placeholder: "us-east-1",
98+
when: { key: "authType", op: "eq" as const, value: "idc" },
99+
},
93100
],
94101
async authorize(inputs = {} as Record<string, string>) {
95102
const url =
96-
inputs.authType === "idc" ? inputs.startUrl : BUILDER_ID_URL
103+
inputs.authType === "idc" ? (inputs.startUrl || process.env.AWS_SSO_START_URL) : BUILDER_ID_URL
104+
const region =
105+
inputs.authType === "idc"
106+
? (inputs.region || process.env.AWS_SSO_REGION || "us-east-1")
107+
: "us-east-1"
97108

98-
const registration = await fetch(`${OIDC_ENDPOINT}/client/register`, {
109+
const registration = await fetch(`${oidc(region)}/client/register`, {
99110
method: "POST",
100111
headers: {
101112
"Content-Type": "application/json",
@@ -121,7 +132,7 @@ export async function KiroAuthPlugin(_input: PluginInput): Promise<Hooks> {
121132
clientSecretExpiresAt: number
122133
}
123134

124-
const device = await fetch(`${OIDC_ENDPOINT}/device_authorization`, {
135+
const device = await fetch(`${oidc(region)}/device_authorization`, {
125136
method: "POST",
126137
headers: {
127138
"Content-Type": "application/json",
@@ -155,7 +166,7 @@ export async function KiroAuthPlugin(_input: PluginInput): Promise<Hooks> {
155166
const delay = { ms: auth.interval }
156167

157168
while (true) {
158-
const response = await fetch(`${OIDC_ENDPOINT}/token`, {
169+
const response = await fetch(`${oidc(region)}/token`, {
159170
method: "POST",
160171
headers: {
161172
"Content-Type": "application/json",
@@ -185,7 +196,7 @@ export async function KiroAuthPlugin(_input: PluginInput): Promise<Hooks> {
185196
accessToken: tokens.accessToken,
186197
refreshToken: tokens.refreshToken,
187198
expiresAt: expires.toISOString(),
188-
region: "us-east-1",
199+
region,
189200
clientId: client.clientId,
190201
clientSecret: client.clientSecret,
191202
})

packages/opencode/src/provider/provider.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import {
5252
isWorkflowModel,
5353
discoverWorkflowModels,
5454
} from "gitlab-ai-provider"
55-
import { hasToken } from "./sdk/kiro/kiro-auth"
55+
import { hasToken, getApiRegion } from "./sdk/kiro/kiro-auth"
5656
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
5757
import { GoogleAuth } from "google-auth-library"
5858
import { ProviderTransform } from "./transform"
@@ -822,12 +822,13 @@ export namespace Provider {
822822
Effect.promise(async () => {
823823
const found = await hasToken()
824824
if (!found) return { autoload: false }
825+
const apiRegion = await getApiRegion()
825826
return {
826827
autoload: true,
827828
async getModel(sdk: ReturnType<typeof createKiro>, modelID: string, options?: Record<string, any>) {
828829
const ctx = options?.["context"] as number | undefined
829-
if (!ctx) return sdk.languageModel(modelID)
830-
return createKiro({ context: ctx }).languageModel(modelID)
830+
if (!ctx) return createKiro({ region: apiRegion }).languageModel(modelID)
831+
return createKiro({ context: ctx, region: apiRegion }).languageModel(modelID)
831832
},
832833
}
833834
}),
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { createKiro } from "./kiro-provider"
22
export type { KiroProvider, KiroProviderSettings } from "./kiro-provider"
33
export { getQuota } from "./kiro-quota"
4+
export { getApiRegion } from "./kiro-auth"

packages/opencode/src/provider/sdk/kiro/kiro-auth.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,30 @@ export function getToken(): Promise<string | undefined> {
114114
export function hasToken(): Promise<boolean> {
115115
return Bun.file(TOKEN_PATH).exists()
116116
}
117+
118+
const region: { api: string } = { api: "" }
119+
120+
export function getApiRegion(): Promise<string> {
121+
if (region.api) return Promise.resolve(region.api)
122+
return getToken()
123+
.then((token) => {
124+
if (!token) return "us-east-1"
125+
return fetch("https://q.us-east-1.amazonaws.com/ListAvailableModels?origin=AI_EDITOR", {
126+
method: "GET",
127+
headers: {
128+
Authorization: `Bearer ${token}`,
129+
"Content-Type": "application/json",
130+
"User-Agent": "aws-sdk-js/1.0.27 ua/2.1 os/darwin lang/js api/codewhispererstreaming#1.0.27 m/E Kiro-opencode",
131+
"x-amz-user-agent": "aws-sdk-js/1.0.27 Kiro-opencode",
132+
"x-amzn-codewhisperer-optout": "true",
133+
},
134+
})
135+
.then((r) => (r.ok ? "us-east-1" : "eu-central-1"))
136+
.catch(() => "eu-central-1")
137+
})
138+
.catch(() => "us-east-1")
139+
.then((result) => {
140+
region.api = result
141+
return result
142+
})
143+
}

packages/opencode/src/provider/sdk/kiro/kiro-language-model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import { decodeEventStream } from "./kiro-eventstream"
1212
import { KiroAuthError, KiroApiError } from "./kiro-error"
1313
import type { KiroStreamEvent, KiroToolSpec } from "./kiro-api-types"
1414

15-
const ENDPOINT = "https://q.us-east-1.amazonaws.com"
16-
1715
const THINKING_TOOL: KiroToolSpec = {
1816
toolSpecification: {
1917
name: "thinking",
@@ -177,6 +175,7 @@ export class KiroLanguageModel implements LanguageModelV3 {
177175
readonly provider: string
178176
readonly fetch?: typeof globalThis.fetch
179177
readonly context?: number
178+
readonly region?: string
180179
},
181180
) {
182181
this.provider = config.provider
@@ -187,8 +186,9 @@ export class KiroLanguageModel implements LanguageModelV3 {
187186
token: string,
188187
state: ReturnType<typeof translate>,
189188
): Promise<Response> {
189+
const endpoint = `https://q.${this.config.region ?? "us-east-1"}.amazonaws.com`
190190
return (this.config.fetch ?? globalThis.fetch)(
191-
`${ENDPOINT}/generateAssistantResponse`,
191+
`${endpoint}/generateAssistantResponse`,
192192
{
193193
method: "POST",
194194
headers: {

packages/opencode/src/provider/sdk/kiro/kiro-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { KiroLanguageModel } from "./kiro-language-model"
44
export interface KiroProviderSettings {
55
readonly fetch?: typeof globalThis.fetch
66
readonly context?: number
7+
readonly region?: string
78
}
89

910
export interface KiroProvider {
@@ -17,6 +18,7 @@ export function createKiro(settings: KiroProviderSettings = {}): KiroProvider {
1718
provider: "kiro",
1819
fetch: settings.fetch,
1920
context: settings.context,
21+
region: settings.region,
2022
})
2123

2224
provider.languageModel = provider
Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
1-
import { getToken } from "./kiro-auth"
2-
3-
const ENDPOINT = "https://q.us-east-1.amazonaws.com"
1+
import { getToken, getApiRegion } from "./kiro-auth"
42

53
export function getQuota(): Promise<
64
{ currentUsage: number; usageLimit: number; subscriptionTitle: string } | undefined
75
> {
86
return getToken().then((token) => {
97
if (!token) return undefined
10-
return fetch(
11-
`${ENDPOINT}/getUsageLimits?origin=AI_EDITOR&resourceType=AGENTIC_REQUEST`,
12-
{
13-
method: "GET",
14-
headers: {
15-
Authorization: `Bearer ${token}`,
16-
"Content-Type": "application/json",
17-
"User-Agent":
18-
"aws-sdk-js/1.0.27 ua/2.1 os/darwin lang/js api/codewhispererstreaming#1.0.27 m/E Kiro-opencode",
19-
"x-amz-user-agent": "aws-sdk-js/1.0.27 Kiro-opencode",
20-
"x-amzn-codewhisperer-optout": "true",
21-
"x-amzn-kiro-agent-mode": "vibe",
8+
return getApiRegion().then((region) =>
9+
fetch(
10+
`https://q.${region}.amazonaws.com/getUsageLimits?origin=AI_EDITOR&resourceType=AGENTIC_REQUEST`,
11+
{
12+
method: "GET",
13+
headers: {
14+
Authorization: `Bearer ${token}`,
15+
"Content-Type": "application/json",
16+
"User-Agent":
17+
"aws-sdk-js/1.0.27 ua/2.1 os/darwin lang/js api/codewhispererstreaming#1.0.27 m/E Kiro-opencode",
18+
"x-amz-user-agent": "aws-sdk-js/1.0.27 Kiro-opencode",
19+
"x-amzn-codewhisperer-optout": "true",
20+
"x-amzn-kiro-agent-mode": "vibe",
21+
},
2222
},
23-
},
24-
)
25-
.then((response) => {
26-
if (!response.ok) return undefined
27-
return response.json() as Promise<{
28-
subscriptionInfo: {
29-
subscriptionTitle: string
30-
}
31-
usageBreakdownList: Array<{
32-
currentUsage: number
33-
currentUsageWithPrecision: number
34-
usageLimit: number
35-
usageLimitWithPrecision: number
23+
)
24+
.then((response) => {
25+
if (!response.ok) return undefined
26+
return response.json() as Promise<{
27+
subscriptionInfo: {
28+
subscriptionTitle: string
29+
}
30+
usageBreakdownList: Array<{
31+
currentUsage: number
32+
currentUsageWithPrecision: number
33+
usageLimit: number
34+
usageLimitWithPrecision: number
35+
}>
3636
}>
37-
}>
38-
})
39-
.then((body) => {
40-
if (!body) return undefined
41-
const item = body.usageBreakdownList[0]
42-
if (!item) return undefined
43-
return {
44-
currentUsage: item.currentUsageWithPrecision ?? item.currentUsage,
45-
usageLimit: item.usageLimitWithPrecision ?? item.usageLimit,
46-
subscriptionTitle: body.subscriptionInfo.subscriptionTitle
47-
.toLowerCase()
48-
.replace(/\b\w/g, (c) => c.toUpperCase()),
49-
}
50-
})
51-
.catch(() => undefined)
37+
})
38+
.then((body) => {
39+
if (!body) return undefined
40+
const item = body.usageBreakdownList[0]
41+
if (!item) return undefined
42+
return {
43+
currentUsage: item.currentUsageWithPrecision ?? item.currentUsage,
44+
usageLimit: item.usageLimitWithPrecision ?? item.usageLimit,
45+
subscriptionTitle: body.subscriptionInfo.subscriptionTitle
46+
.toLowerCase()
47+
.replace(/\b\w/g, (c) => c.toUpperCase()),
48+
}
49+
})
50+
.catch(() => undefined),
51+
)
5252
})
5353
}

0 commit comments

Comments
 (0)