fix(provider): send Anthropic's dashed native slug through the AI Gateway (#44281)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Charlie Gleason 2026-08-24 16:15:26 -05:00 committed by GitHub
parent 611cc73d84
commit f8b4dd70ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 19 additions and 2 deletions

View file

@ -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

View file

@ -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))
@ -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)