diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index 2803cb7a8e8f..0cf10dfe6824 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -33,7 +33,7 @@ export const CloudflareAIGatewayPlugin = define({ // gateway's stored/BYOK keys instead. const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/") const unified = createUnified(isWorkersAi ? { apiKey: config.apiKey } : {}) - return gateway(unified(modelID)) + return gateway(unified(nativeModelId(modelID))) }, } }), @@ -85,3 +85,15 @@ function gatewayOptions(options: Record, metadata: unknown) { function stringOption(options: Record, key: string) { return typeof options[key] === "string" ? options[key] : undefined } + +// models.dev publishes Anthropic ids with dot-separated version numbers (e.g. +// "claude-haiku-4.5") to match its site-wide display convention, but Anthropic's +// real API model slugs use dashes ("claude-haiku-4-5"). The gateway's Unified API +// forwards the model id straight through to the upstream provider, so an +// unmodified dotted id 404s against Anthropic with a "model not found" error. +// Every other proxied provider in the catalog (OpenAI, xAI, Google, ...) already +// uses dots natively, so this translation is scoped to the "anthropic/" prefix. +function nativeModelId(modelID: string): string { + if (!modelID.startsWith("anthropic/")) return modelID + return modelID.replaceAll(".", "-") +} diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index 008b9a46a3e0..7b2f21099ba1 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -401,6 +401,61 @@ describe("CloudflareAIGatewayPlugin", () => { ), ) + it.effect("translates models.dev's dotted Anthropic ids to Anthropic's native dashed slugs", () => + withEnv(cloudflareEnv(), () => + Effect.gen(function* () { + resetCalls() + const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service + yield* addPlugin() + + const result = yield* aisdk.runSDK({ + model: ModelV2.Info.make({ + ...ModelV2.Info.empty( + ProviderV2.ID.make("cloudflare-ai-gateway"), + ModelV2.ID.make("anthropic/claude-haiku-4.5"), + ), + api: { + id: ModelV2.ID.make("anthropic/claude-haiku-4.5"), + type: "aisdk", + package: "test-provider", + }, + }), + package: "ai-gateway-provider", + options: { name: "cloudflare-ai-gateway" }, + }) + + result.sdk.languageModel("anthropic/claude-haiku-4.5") + + expect(unifiedCalls).toEqual(["anthropic/claude-haiku-4-5"]) + }), + ), + ) + + it.effect("leaves non-Anthropic ids with dots untouched (e.g. OpenAI's gpt-4.1)", () => + withEnv(cloudflareEnv(), () => + Effect.gen(function* () { + resetCalls() + const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service + yield* addPlugin() + + const result = yield* aisdk.runSDK({ + model: ModelV2.Info.make({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-4.1")), + api: { id: ModelV2.ID.make("openai/gpt-4.1"), type: "aisdk", package: "test-provider" }, + }), + package: "ai-gateway-provider", + options: { name: "cloudflare-ai-gateway" }, + }) + + result.sdk.languageModel("openai/gpt-4.1") + + expect(unifiedCalls).toEqual(["openai/gpt-4.1"]) + }), + ), + ) + it.effect("ignores non Cloudflare AI Gateway packages", () => withEnv(cloudflareEnv(), () => Effect.gen(function* () {