Skip to content

Commit a66e6cd

Browse files
authored
feat(core): discover Bedrock credentials in the provider plugin (#47548)
1 parent 93b3725 commit a66e6cd

6 files changed

Lines changed: 181 additions & 589 deletions

File tree

‎bun.lock‎

Lines changed: 5 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/core/package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
},
101101
"dependencies": {
102102
"@ai-sdk/alibaba": "1.0.17",
103-
"@ai-sdk/amazon-bedrock": "4.0.112",
104103
"@ai-sdk/anthropic": "3.0.82",
105104
"@ai-sdk/azure": "3.0.88",
106105
"@ai-sdk/cohere": "3.0.27",
@@ -112,7 +111,6 @@
112111
"@ai-sdk/provider": "3.0.8",
113112
"@ai-sdk/provider-utils": "4.0.23",
114113
"@ai-sdk/vercel": "2.0.39",
115-
"@aws-sdk/credential-providers": "3.1057.0",
116114
"@lydell/node-pty": "catalog:",
117115
"@modelcontextprotocol/sdk": "1.29.0",
118116
"@ff-labs/fff-bun": "0.10.5",

‎packages/core/src/aisdk-native.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,9 @@ function mapBedrockSettings(
195195
? { baseURL: settings.endpoint }
196196
: {}),
197197
...(apiKey === undefined ? {} : { apiKey }),
198+
...(settings.auth === "bearer" || settings.auth === "sigv4" ? { auth: settings.auth } : {}),
198199
...(credentials === undefined ? {} : { credentials }),
200+
...(typeof settings.profile === "string" ? { profile: settings.profile } : {}),
199201
...(typeof settings.region === "string" ? { region: settings.region } : {}),
200202
...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
201203
}
Lines changed: 41 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,59 @@
11
import { Effect } from "effect"
2-
import type { LanguageModelV3 } from "@ai-sdk/provider"
32
import { define } from "@opencode-ai/plugin/effect/plugin"
43
import { Provider } from "../../provider.js"
54

6-
type MantleSDK = {
7-
chat: (modelID: string) => LanguageModelV3
8-
responses: (modelID: string) => LanguageModelV3
9-
}
10-
11-
// Bedrock cross-region inference profiles require regional prefixes only for
12-
// specific model/region combinations. Keep the mapping narrow and avoid
13-
// double-prefixing model IDs that models.dev already marks as global/us/eu/etc.
14-
function resolveModelID(modelID: string, region: string | undefined) {
15-
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
16-
if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID
17-
18-
const resolvedRegion = region ?? "us-east-1"
19-
const regionPrefix = resolvedRegion.split("-")[0]
20-
if (regionPrefix === "us") {
21-
const requiresPrefix = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some(
22-
(item) => modelID.includes(item),
23-
)
24-
if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}`
25-
return modelID
26-
}
27-
if (regionPrefix === "eu") {
28-
const regionRequiresPrefix = [
29-
"eu-west-1",
30-
"eu-west-2",
31-
"eu-west-3",
32-
"eu-north-1",
33-
"eu-central-1",
34-
"eu-south-1",
35-
"eu-south-2",
36-
].some((item) => resolvedRegion.includes(item))
37-
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) =>
38-
modelID.includes(item),
39-
)
40-
return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID
41-
}
42-
if (regionPrefix !== "ap") return modelID
43-
44-
const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion)
45-
if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) {
46-
return `au.${modelID}`
47-
}
48-
49-
const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac"
50-
return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item))
51-
? `${prefix}.${modelID}`
52-
: modelID
53-
}
54-
55-
function selectMantleModel(sdk: MantleSDK, modelID: string) {
56-
if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b")
57-
return sdk.chat(modelID)
58-
return sdk.responses(modelID)
5+
// Ambient inputs the AWS default credential chain can turn into credentials
6+
// without any key stored in opencode. Mirrors the presence checks the AWS CLI
7+
// and SDK use before consulting shared config.
8+
const CHAIN_ENV = [
9+
"AWS_PROFILE",
10+
"AWS_ACCESS_KEY_ID",
11+
"AWS_WEB_IDENTITY_TOKEN_FILE",
12+
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
13+
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
14+
]
15+
16+
const isBedrock = (item: { readonly package: string }) => {
17+
const name = Provider.packageName(item.package)
18+
return name.startsWith("@ai-sdk/amazon-bedrock") || name.startsWith("@opencode-ai/ai/providers/amazon-bedrock")
5919
}
6020

6121
export const AmazonBedrockPlugin = define({
6222
id: "opencode.provider.amazon.bedrock",
6323
effect: Effect.fn(function* (ctx) {
24+
yield* ctx.integration.transform((editor) => {
25+
// models.dev advertises AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and
26+
// AWS_REGION alongside the bearer token. Only the bearer token is a key;
27+
// the rest feed the SigV4 credential chain and must not become one.
28+
editor.method.update({
29+
integrationID: Provider.ID.amazonBedrock,
30+
method: { type: "env", names: ["AWS_BEARER_TOKEN_BEDROCK"] },
31+
})
32+
})
6433
yield* ctx.catalog.transform((evt) => {
6534
for (const item of evt.provider.list()) {
66-
if (!Provider.isAISDK(item.provider.package)) continue
67-
if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue
35+
if (!isBedrock(item.provider)) continue
6836
evt.provider.update(item.provider.id, (provider) => {
69-
if (typeof provider.settings?.endpoint !== "string") return
70-
// The AI SDK expects a base URL, but users configure Bedrock private/VPC
71-
// endpoints as `endpoint`; move it into the catalog endpoint URL once.
72-
provider.settings.baseURL = provider.settings.endpoint
37+
const settings = provider.settings ?? {}
38+
const chain = typeof settings.profile === "string" || CHAIN_ENV.some((name) => process.env[name])
39+
// SigV4 authenticates through the AWS default chain rather than a key
40+
// credential, so ambient AWS configuration is what makes Bedrock usable.
41+
if (chain && provider.activation === "auto") provider.activation = "enabled"
42+
// Same default the native package uses, made explicit here so catalog
43+
// `${AWS_REGION}` URLs resolve without any region configured.
44+
const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1"
45+
provider.settings = {
46+
...settings,
47+
...(typeof settings.region !== "string" ? { region } : {}),
48+
// Users configure Bedrock private/VPC endpoints as `endpoint`; move it
49+
// into the catalog base URL once.
50+
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
51+
? { baseURL: settings.endpoint }
52+
: {}),
53+
}
7354
delete provider.settings.endpoint
7455
})
7556
}
7657
})
77-
yield* ctx.aisdk.hook(
78-
"sdk",
79-
Effect.fn(function* (evt) {
80-
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
81-
const options = { ...evt.options }
82-
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
83-
const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1")
84-
const bearerToken =
85-
process.env.AWS_BEARER_TOKEN_BEDROCK ??
86-
(typeof options.bearerToken === "string" ? options.bearerToken : undefined)
87-
if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken
88-
options.region = region
89-
if (typeof options.endpoint === "string") options.baseURL = options.endpoint
90-
if (!bearerToken && options.credentialProvider === undefined) {
91-
// Do not gate SDK creation on explicit AWS env vars. The default chain
92-
// also handles ~/.aws/credentials, SSO, process creds, and instance roles.
93-
const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers"))
94-
options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {})
95-
}
96-
97-
if (evt.package === "@ai-sdk/amazon-bedrock/mantle") {
98-
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock/mantle"))
99-
evt.sdk = mod.createBedrockMantle(options)
100-
return
101-
}
102-
103-
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
104-
evt.sdk = mod.createAmazonBedrock(options)
105-
}),
106-
)
107-
yield* ctx.aisdk.hook(
108-
"language",
109-
Effect.fn(function* (evt) {
110-
if (evt.model.providerID !== Provider.ID.amazonBedrock) return
111-
if (
112-
Provider.isAISDK(evt.model.package) &&
113-
Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle"
114-
) {
115-
evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id)
116-
return
117-
}
118-
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
119-
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region))
120-
}),
121-
)
12258
}),
12359
})

‎packages/core/test/aisdk-native.test.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ describe("AISDKNative", () => {
311311
region: "eu-west-1",
312312
},
313313
baseURL: "https://bedrock-mantle.${AWS_REGION}.api.aws/v1",
314-
profile: "ignored",
315314
credentialProvider: "ignored",
316315
fetch: "ignored",
317316
store: false,
@@ -333,6 +332,19 @@ describe("AISDKNative", () => {
333332
})
334333
})
335334

335+
test("forwards Bedrock profile and auth mode for the default credential chain", () => {
336+
expect(
337+
map("@ai-sdk/amazon-bedrock", { profile: "work", auth: "sigv4", region: "eu-west-1" }, "anthropic.claude"),
338+
).toEqual({
339+
package: "@opencode-ai/ai/providers/amazon-bedrock",
340+
settings: { profile: "work", auth: "sigv4", region: "eu-west-1" },
341+
})
342+
expect(map("@ai-sdk/amazon-bedrock", { auth: "bogus" }, "anthropic.claude")).toEqual({
343+
package: "@opencode-ai/ai/providers/amazon-bedrock",
344+
settings: {},
345+
})
346+
})
347+
336348
test("maps the legacy Bedrock endpoint override", () => {
337349
expect(
338350
map(

0 commit comments

Comments
 (0)