From 29dec99e6784f9277be14074b1f0c2b527c31126 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Sat, 22 Aug 2026 19:34:37 -0500 Subject: [PATCH 1/2] fix(provider): send Anthropic's dashed native slug through the AI Gateway The Cloudflare AI Gateway plugin routes anthropic/* models through the native Anthropic Messages passthrough (createAnthropic), which forwards the model id to Anthropic's API unchanged. models.dev lists Cloudflare's canonical dotted ids (anthropic/claude-haiku-4.5), but Anthropic's Messages API expects dashed native slugs (claude-haiku-4-5), so every Anthropic model 404s with "model: claude-haiku-4.5". Translate dots to dashes when slicing the anthropic/ prefix. OpenAI is unaffected because its native slugs already use dots (gpt-4.1). Cloudflare's own catalog-aware REST endpoints (/ai/v1/messages, /ai/run) do this same translation internally; the native passthrough route opencode uses does not, so the plugin has to. Verified end-to-end against a live gateway: anthropic/claude-haiku-4.5 goes from "Error: model: claude-haiku-4.5" to a normal completion, with OpenAI and Workers AI models unaffected. Co-Authored-By: Claude Sonnet 5 --- packages/opencode/src/provider/provider.ts | 5 ++++- .../test/provider/cf-ai-gateway-e2e.test.ts | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index dba9cbece552..b7ed92314946 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -844,7 +844,10 @@ function custom(dep: CustomDep): Record { // 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. + 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 diff --git a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts index cb1654006e66..97f9d4f909a8 100644 --- a/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts +++ b/packages/opencode/test/provider/cf-ai-gateway-e2e.test.ts @@ -165,7 +165,8 @@ function extractUpstreamHeaders(body: unknown): Record | 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)) @@ -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) From 70ae4617a2edbe94c0e5450a019cf39aeaf58708 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Sun, 23 Aug 2026 21:59:54 -0500 Subject: [PATCH 2/2] docs(provider): note why Anthropic dot->dash replacement is lossless Co-Authored-By: Claude Opus 4.8 --- packages/opencode/src/provider/provider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index b7ed92314946..32e01512fbea 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -846,6 +846,8 @@ function custom(dep: CustomDep): Record { if (modelID.startsWith("openai/")) return aigateway(createOpenAI()(modelID.slice("openai/".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