diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a02a017e77c..21f03a2720a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -621,6 +621,72 @@ export namespace Provider { }) export type Info = z.infer + const BEDROCK_OPUS_4_6 = "claude-opus-4-6" + const BEDROCK_1M_BETA = "context-1m-2025-08-07" + const BEDROCK_CONTEXT_200K = 200_000 + const BEDROCK_CONTEXT_1M = 1_000_000 + + function splitBedrockOpus46(providerID: string, models: Record) { + if (providerID !== "amazon-bedrock") return + + for (const [id, model] of Object.entries(models)) { + if (!model.api.id.includes(BEDROCK_OPUS_4_6)) continue + if (id.endsWith("-1m")) continue + + const name = model.name.replace(/\s+\((200K|1M Experimental)\)$/i, "") + const opts = { ...model.options } + const raw = opts["anthropicBeta"] + const base = (Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []).filter( + (item): item is string => typeof item === "string", + ) + + delete opts["anthropicBeta"] + + model.name = `${name} (200K)` + model.options = opts + model.limit = { + ...model.limit, + context: Math.min(model.limit.context, BEDROCK_CONTEXT_200K), + } + + const nextID = `${id}-1m` + const next = models[nextID] + + if (next) { + const list = (Array.isArray(next.options["anthropicBeta"]) ? next.options["anthropicBeta"] : []).filter( + (item): item is string => typeof item === "string", + ) + + next.name = `${name} (1M Experimental)` + next.status = "beta" + next.limit = { + ...next.limit, + context: Math.max(next.limit.context, BEDROCK_CONTEXT_1M), + } + next.options = { + ...next.options, + anthropicBeta: [...new Set([...list, ...base, BEDROCK_1M_BETA])], + } + continue + } + + models[nextID] = { + ...model, + id: nextID, + name: `${name} (1M Experimental)`, + status: "beta", + limit: { + ...model.limit, + context: Math.max(model.limit.context, BEDROCK_CONTEXT_1M), + }, + options: { + ...model.options, + anthropicBeta: [...new Set([...base, BEDROCK_1M_BETA])], + }, + } + } + } + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const m: Model = { id: model.id, @@ -689,13 +755,16 @@ export namespace Provider { } export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { + const models = mapValues(provider.models, (model) => fromModelsDevModel(provider, model)) + splitBedrockOpus46(provider.id, models) + return { id: provider.id, source: "custom", name: provider.name, env: provider.env ?? [], options: {}, - models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)), + models, } } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 98cd49c02fd..7a002b4a45e 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1407,6 +1407,89 @@ test("model headers are preserved", async () => { }) }) +test("fromModelsDevProvider splits bedrock opus 4.6 into 200K and 1M variants", () => { + const provider = Provider.fromModelsDevProvider({ + id: "amazon-bedrock", + name: "Amazon Bedrock", + env: [], + models: { + "us.anthropic.claude-opus-4-6-v1:0": { + id: "us.anthropic.claude-opus-4-6-v1:0", + name: "Claude Opus 4.6", + attachment: true, + reasoning: true, + tool_call: true, + temperature: true, + release_date: "2025-12-01", + limit: { context: 1_000_000, output: 64_000 }, + options: { + anthropicBeta: ["existing-beta"], + }, + }, + }, + }) + + const base = provider.models["us.anthropic.claude-opus-4-6-v1:0"] + const extended = provider.models["us.anthropic.claude-opus-4-6-v1:0-1m"] + + expect(base).toBeDefined() + expect(extended).toBeDefined() + + expect(base.name).toContain("(200K)") + expect(extended.name).toContain("(1M Experimental)") + + expect(base.limit.context).toBe(200_000) + expect(extended.limit.context).toBe(1_000_000) + + expect(base.options.anthropicBeta).toBeUndefined() + expect(extended.options.anthropicBeta).toEqual(expect.arrayContaining(["existing-beta", "context-1m-2025-08-07"])) + + expect(extended.api.id).toBe(base.api.id) + expect(extended.status).toBe("beta") +}) + +test("bedrock custom opus 4.6 model is not auto-split", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + models: { + "anthropic.claude-opus-4-6-v1:0": { + name: "Claude Opus 4.6", + attachment: true, + reasoning: true, + tool_call: true, + temperature: true, + limit: { context: 1_000_000, output: 64_000 }, + }, + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const providers = await Provider.list() + const base = providers["amazon-bedrock"].models["anthropic.claude-opus-4-6-v1:0"] + const extended = providers["amazon-bedrock"].models["anthropic.claude-opus-4-6-v1:0-1m"] + + expect(base).toBeDefined() + expect(extended).toBeUndefined() + expect(base.name).toBe("Claude Opus 4.6") + expect(base.limit.context).toBe(1_000_000) + expect(base.options.anthropicBeta).toBeUndefined() + }, + }) +}) + test("provider env fallback - second env var used if first missing", async () => { await using tmp = await tmpdir({ init: async (dir) => {