From f8b4dd70ac26996436e259fc386917944c05f481 Mon Sep 17 00:00:00 2001 From: Charlie Gleason Date: Mon, 24 Aug 2026 16:15:26 -0500 Subject: [PATCH] fix(provider): send Anthropic's dashed native slug through the AI Gateway (#44281) Co-authored-by: Claude Sonnet 5 --- packages/opencode/src/provider/provider.ts | 7 ++++++- .../test/provider/cf-ai-gateway-e2e.test.ts | 14 +++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index dba9cbece55..32e01512fbe 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -844,7 +844,12 @@ 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. + // 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 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 cb1654006e6..97f9d4f909a 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)