|
1 | 1 | import { Effect } from "effect" |
2 | | -import type { LanguageModelV3 } from "@ai-sdk/provider" |
3 | 2 | import { define } from "@opencode-ai/plugin/effect/plugin" |
4 | 3 | import { Provider } from "../../provider.js" |
5 | 4 |
|
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") |
59 | 19 | } |
60 | 20 |
|
61 | 21 | export const AmazonBedrockPlugin = define({ |
62 | 22 | id: "opencode.provider.amazon.bedrock", |
63 | 23 | 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 | + }) |
64 | 33 | yield* ctx.catalog.transform((evt) => { |
65 | 34 | 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 |
68 | 36 | 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 | + } |
73 | 54 | delete provider.settings.endpoint |
74 | 55 | }) |
75 | 56 | } |
76 | 57 | }) |
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 | | - ) |
122 | 58 | }), |
123 | 59 | }) |
0 commit comments