Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,39 @@ export namespace Provider {
})
export type Info = z.infer<typeof Info>

const BEDROCK_1M_MODELS = ["claude-opus-4-6", "claude-sonnet-4-5", "claude-sonnet-4-6"]
const BEDROCK_1M_BETA = "context-1m-2025-08-07"

function splitBedrock1m(providerID: string, models: Record<string, Model>) {
if (providerID !== "amazon-bedrock") return

for (const [id, model] of Object.entries(models)) {
if (!BEDROCK_1M_MODELS.some((m) => model.api.id.includes(m))) 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 existing = (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, 200_000) }

models[`${id}-1m`] = {
...model,
id: `${id}-1m`,
name: `${name} (1M Experimental)`,
status: "beta",
limit: { ...model.limit, context: 1_000_000 },
options: { ...model.options, anthropicBeta: [...new Set([...existing, BEDROCK_1M_BETA])] },
}
}
}

function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
const m: Model = {
id: model.id,
Expand Down Expand Up @@ -744,13 +777,16 @@ export namespace Provider {
}

export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
const models = mapValues(provider.models, (model) => fromModelsDevModel(provider, model))
splitBedrock1m(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,
}
}

Expand Down
205 changes: 205 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,211 @@ 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: "2026-02-05",
limit: { context: 1_000_000, output: 128_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("fromModelsDevProvider splits bedrock sonnet 4.5 into 200K and 1M variants", () => {
const provider = Provider.fromModelsDevProvider({
id: "amazon-bedrock",
name: "Amazon Bedrock",
env: [],
models: {
"us.anthropic.claude-sonnet-4-5-20250929-v1:0": {
id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
name: "Claude Sonnet 4.5",
attachment: true,
reasoning: true,
tool_call: true,
temperature: true,
release_date: "2025-09-29",
limit: { context: 1_000_000, output: 64_000 },
options: {},
},
},
})

const base = provider.models["us.anthropic.claude-sonnet-4-5-20250929-v1:0"]
const extended = provider.models["us.anthropic.claude-sonnet-4-5-20250929-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(extended.options.anthropicBeta).toEqual(["context-1m-2025-08-07"])
expect(extended.status).toBe("beta")
})

test("fromModelsDevProvider splits bedrock sonnet 4.6 into 200K and 1M variants", () => {
const provider = Provider.fromModelsDevProvider({
id: "amazon-bedrock",
name: "Amazon Bedrock",
env: [],
models: {
"us.anthropic.claude-sonnet-4-6-v1:0": {
id: "us.anthropic.claude-sonnet-4-6-v1:0",
name: "Claude Sonnet 4.6",
attachment: true,
reasoning: true,
tool_call: true,
temperature: true,
release_date: "2026-02-17",
limit: { context: 1_000_000, output: 64_000 },
options: {},
},
},
})

const base = provider.models["us.anthropic.claude-sonnet-4-6-v1:0"]
const extended = provider.models["us.anthropic.claude-sonnet-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(extended.options.anthropicBeta).toEqual(["context-1m-2025-08-07"])
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("fromModelsDevProvider does not split non-1m bedrock models", () => {
const provider = Provider.fromModelsDevProvider({
id: "amazon-bedrock",
name: "Amazon Bedrock",
env: [],
models: {
"us.anthropic.claude-haiku-4-20250414-v1:0": {
id: "us.anthropic.claude-haiku-4-20250414-v1:0",
name: "Claude Haiku 4",
attachment: true,
reasoning: false,
tool_call: true,
temperature: true,
release_date: "2025-04-14",
limit: { context: 200_000, output: 64_000 },
options: {},
},
},
})

const models = Object.keys(provider.models)
expect(models).toEqual(["us.anthropic.claude-haiku-4-20250414-v1:0"])
expect(provider.models["us.anthropic.claude-haiku-4-20250414-v1:0-1m"]).toBeUndefined()
})

test("fromModelsDevProvider does not split non-bedrock providers", () => {
const provider = Provider.fromModelsDevProvider({
id: "anthropic",
name: "Anthropic",
env: ["ANTHROPIC_API_KEY"],
models: {
"claude-opus-4-6": {
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
attachment: true,
reasoning: true,
tool_call: true,
temperature: true,
release_date: "2026-02-05",
limit: { context: 200_000, output: 128_000 },
options: {},
},
},
})

const models = Object.keys(provider.models)
expect(models).toEqual(["claude-opus-4-6"])
expect(provider.models["claude-opus-4-6-1m"]).toBeUndefined()
})

test("provider env fallback - second env var used if first missing", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
Expand Down
Loading