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
7 changes: 6 additions & 1 deletion packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,12 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
// The passthrough wrappers inject a CF_TEMP_TOKEN sentinel that the gateway strips before
// dispatch, so upstream billing stays on the gateway (Unified Billing / stored BYOK).
if (modelID.startsWith("openai/")) return aigateway(createOpenAI()(modelID.slice("openai/".length)))
if (modelID.startsWith("anthropic/")) return aigateway(createAnthropic()(modelID.slice("anthropic/".length)))
// models.dev lists Anthropic ids with dotted versions (claude-haiku-4.5); Anthropic's
// Messages API expects dashed native slugs (claude-haiku-4-5), so translate before passing.
// No native Anthropic slug contains a dot, so the blanket replacement is lossless here -
// unlike OpenAI above, whose native ids (e.g. gpt-4.1) keep their dots and must not be touched.
if (modelID.startsWith("anthropic/"))
return aigateway(createAnthropic()(modelID.slice("anthropic/".length).replaceAll(".", "-")))
// Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is
// the only one that should receive the Cloudflare token as its upstream Authorization header.
// The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as
Expand Down
14 changes: 13 additions & 1 deletion packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ function extractUpstreamHeaders(body: unknown): Record<string, unknown> | undefi
function gatewayModel(apiId: string, gatewayToken = "test") {
const aigateway = createAiGateway({ accountId: "test", gateway: "test", apiKey: gatewayToken })
if (apiId.startsWith("openai/")) return aigateway(createOpenAI()(apiId.slice("openai/".length)))
if (apiId.startsWith("anthropic/")) return aigateway(createAnthropic()(apiId.slice("anthropic/".length)))
if (apiId.startsWith("anthropic/"))
return aigateway(createAnthropic()(apiId.slice("anthropic/".length).replaceAll(".", "-")))
const isWorkersAi = apiId.startsWith("workers-ai/") || apiId.startsWith("@cf/")
const unified = createUnified(isWorkersAi ? { apiKey: gatewayToken } : {})
return aigateway(unified(apiId))
Expand Down Expand Up @@ -195,6 +196,17 @@ describe("cf-ai-gateway routing", () => {
expect(upstream?.model).toBe("claude-sonnet-4-6")
})

test("anthropic/* with a dotted models.dev id reaches Anthropic as a dashed native slug", async () => {
// models.dev ids are dotted (claude-haiku-4.5); Anthropic's Messages API 404s unless the
// version is dashed (claude-haiku-4-5). Regression guard for the dotted-id translation.
await callThroughGateway("anthropic/claude-haiku-4.5", {})
const step = firstStep(captured?.outerBody)
expect(step?.provider).toBe("anthropic")
expect(step?.endpoint).toBe("v1/messages")
const upstream = extractUpstreamQuery(captured?.outerBody)
expect(upstream?.model).toBe("claude-haiku-4-5")
})

test("workers-ai models stay on the unified /compat route", async () => {
await callThroughGateway("workers-ai/@cf/moonshotai/kimi-k2.6", {})
const step = firstStep(captured?.outerBody)
Expand Down
Loading