diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 2f5be4b8..471f03ce 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -8,6 +8,7 @@ ### Added +- Added Cloudflare AI Gateway as a built-in provider with OpenAI, Anthropic, and Workers AI gateway routing plus `CLOUDFLARE_API_KEY`/`CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_GATEWAY_ID` authentication ([#3856](https://github.com/badlogic/pi-mono/pull/3856) by [@mchenco](https://github.com/mchenco)). - Added Moonshot AI as a built-in OpenAI-compatible provider with model catalog generation and `MOONSHOT_API_KEY` authentication. - Added Mistral Medium 3.5 model metadata and reasoning-mode handling ([#4009](https://github.com/badlogic/pi-mono/pull/4009) by [@technocidal](https://github.com/technocidal)). - Added `AssistantMessage.responseModel` on the openai-completions path: surfaces the concrete `chunk.model` when it differs from the requested id (e.g. OpenRouter `auto` -> `anthropic/...`) ([#3968](https://github.com/badlogic/pi-mono/pull/3968) by [@purrgrammer](https://github.com/purrgrammer)). diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 52c19024..1c38af31 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -4,6 +4,7 @@ import { writeFileSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import { + CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL, CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL, CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL, CLOUDFLARE_WORKERS_AI_BASE_URL, @@ -419,14 +420,18 @@ async function loadModelsDevData(): Promise[]> { const upstream = prefixedId.slice(0, slashIdx); const nativeId = prefixedId.slice(slashIdx + 1); - let api: "openai-completions" | "openai-responses"; + let api: "anthropic-messages" | "openai-completions" | "openai-responses"; let baseUrl: string; let id: string; if (upstream === "openai") { api = "openai-responses"; baseUrl = CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL; id = nativeId; - } else if (upstream === "workers-ai" || upstream === "anthropic") { + } else if (upstream === "anthropic") { + api = "anthropic-messages"; + baseUrl = CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL; + id = nativeId; + } else if (upstream === "workers-ai") { api = "openai-completions"; baseUrl = CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL; id = prefixedId; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 4fe861d1..1f4aaf25 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -2766,6 +2766,607 @@ export const MODELS = { maxTokens: 40000, } satisfies Model<"openai-completions">, }, + "cloudflare-ai-gateway": { + "claude-3-5-haiku": { + id: "claude-3-5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3-haiku": { + id: "claude-3-haiku", + name: "Claude Haiku 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.25, + output: 1.25, + cacheRead: 0.03, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-opus": { + id: "claude-3-opus", + name: "Claude Opus 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3-sonnet": { + id: "claude-3-sonnet", + name: "Claude Sonnet 3", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 0.3, + }, + contextWindow: 200000, + maxTokens: 4096, + } satisfies Model<"anthropic-messages">, + "claude-3.5-haiku": { + id: "claude-3.5-haiku", + name: "Claude Haiku 3.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.8, + output: 4, + cacheRead: 0.08, + cacheWrite: 1, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-3.5-sonnet": { + id: "claude-3.5-sonnet", + name: "Claude Sonnet 3.5 v2", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: false, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 8192, + } satisfies Model<"anthropic-messages">, + "claude-haiku-4-5": { + id: "claude-haiku-4-5", + name: "Claude Haiku 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1, + output: 5, + cacheRead: 0.1, + cacheWrite: 1.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4": { + id: "claude-opus-4", + name: "Claude Opus 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-1": { + id: "claude-opus-4-1", + name: "Claude Opus 4.1 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 75, + cacheRead: 1.5, + cacheWrite: 18.75, + }, + contextWindow: 200000, + maxTokens: 32000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-5": { + id: "claude-opus-4-5", + name: "Claude Opus 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-6": { + id: "claude-opus-4-6", + name: "Claude Opus 4.6 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4": { + id: "claude-sonnet-4", + name: "Claude Sonnet 4 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-5": { + id: "claude-sonnet-4-5", + name: "Claude Sonnet 4.5 (latest)", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 200000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude Sonnet 4.6", + api: "anthropic-messages", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 3, + output: 15, + cacheRead: 0.3, + cacheWrite: 3.75, + }, + contextWindow: 1000000, + maxTokens: 64000, + } satisfies Model<"anthropic-messages">, + "gpt-4": { + id: "gpt-4", + name: "GPT-4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text"], + cost: { + input: 30, + output: 60, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 8192, + maxTokens: 8192, + } satisfies Model<"openai-responses">, + "gpt-4-turbo": { + id: "gpt-4-turbo", + name: "GPT-4 Turbo", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 10, + output: 30, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 4096, + } satisfies Model<"openai-responses">, + "gpt-4o": { + id: "gpt-4o", + name: "GPT-4o", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 2.5, + output: 10, + cacheRead: 1.25, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-4o-mini": { + id: "gpt-4o-mini", + name: "GPT-4o mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: false, + input: ["text", "image"], + cost: { + input: 0.15, + output: 0.6, + cacheRead: 0.08, + cacheWrite: 0, + }, + contextWindow: 128000, + maxTokens: 16384, + } satisfies Model<"openai-responses">, + "gpt-5.1": { + id: "gpt-5.1", + name: "GPT-5.1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.13, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.1-codex": { + id: "gpt-5.1-codex", + name: "GPT-5.1 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.25, + output: 10, + cacheRead: 0.125, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2": { + id: "gpt-5.2", + name: "GPT-5.2", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.2-codex": { + id: "gpt-5.2-codex", + name: "GPT-5.2 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.3-codex": { + id: "gpt-5.3-codex", + name: "GPT-5.3 Codex", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.75, + output: 14, + cacheRead: 0.175, + cacheWrite: 0, + }, + contextWindow: 400000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.4": { + id: "gpt-5.4", + name: "GPT-5.4", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2.5, + output: 15, + cacheRead: 0.25, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "gpt-5.5": { + id: "gpt-5.5", + name: "GPT-5.5", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 30, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 1050000, + maxTokens: 128000, + } satisfies Model<"openai-responses">, + "o1": { + id: "o1", + name: "o1", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 15, + output: 60, + cacheRead: 7.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3": { + id: "o3", + name: "o3", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 2, + output: 8, + cacheRead: 0.5, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-mini": { + id: "o3-mini", + name: "o3-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.55, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o3-pro": { + id: "o3-pro", + name: "o3-pro", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 20, + output: 80, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "o4-mini": { + id: "o4-mini", + name: "o4-mini", + api: "openai-responses", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.1, + output: 4.4, + cacheRead: 0.28, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 100000, + } satisfies Model<"openai-responses">, + "workers-ai/@cf/moonshotai/kimi-k2.5": { + id: "workers-ai/@cf/moonshotai/kimi-k2.5", + name: "Kimi K2.5", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.6, + output: 3, + cacheRead: 0.1, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/moonshotai/kimi-k2.6": { + id: "workers-ai/@cf/moonshotai/kimi-k2.6", + name: "Kimi K2.6", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0.95, + output: 4, + cacheRead: 0.16, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { + id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", + name: "Nemotron 3 Super 120B", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.5, + output: 1.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 256000, + maxTokens: 256000, + } satisfies Model<"openai-completions">, + "workers-ai/@cf/zai-org/glm-4.7-flash": { + id: "workers-ai/@cf/zai-org/glm-4.7-flash", + name: "GLM-4.7-Flash", + api: "openai-completions", + provider: "cloudflare-ai-gateway", + baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", + compat: {"sendSessionAffinityHeaders":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0.06, + output: 0.4, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + }, "cloudflare-workers-ai": { "@cf/google/gemma-4-26b-a4b-it": { id: "@cf/google/gemma-4-26b-a4b-it", @@ -3259,7 +3860,7 @@ export const MODELS = { } satisfies Model<"anthropic-messages">, "accounts/fireworks/routers/kimi-k2p5-turbo": { id: "accounts/fireworks/routers/kimi-k2p5-turbo", - name: "Kimi K2.5 Turbo (firepass)", + name: "Kimi K2.5 Turbo", api: "anthropic-messages", provider: "fireworks", baseUrl: "https://api.fireworks.ai/inference", @@ -5546,6 +6147,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 262144, } satisfies Model<"mistral-conversations">, + "mistral-medium-2604": { + id: "mistral-medium-2604", + name: "Mistral Medium 3.5", + api: "mistral-conversations", + provider: "mistral", + baseUrl: "https://api.mistral.ai", + reasoning: true, + input: ["text", "image"], + cost: { + input: 1.5, + output: 7.5, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 262144, + maxTokens: 262144, + } satisfies Model<"mistral-conversations">, "mistral-medium-3.5": { id: "mistral-medium-3.5", name: "Mistral Medium 3.5", @@ -5569,16 +6187,16 @@ export const MODELS = { api: "mistral-conversations", provider: "mistral", baseUrl: "https://api.mistral.ai", - reasoning: false, + reasoning: true, input: ["text", "image"], cost: { - input: 0.4, - output: 2, + input: 1.5, + output: 7.5, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 128000, - maxTokens: 16384, + contextWindow: 262144, + maxTokens: 262144, } satisfies Model<"mistral-conversations">, "mistral-nemo": { id: "mistral-nemo", @@ -8567,7 +9185,7 @@ export const MODELS = { cacheRead: 0.024999999999999998, cacheWrite: 0.08333333333333334, }, - contextWindow: 1048576, + contextWindow: 1000000, maxTokens: 8192, } satisfies Model<"openai-completions">, "google/gemini-2.0-flash-lite-001": { @@ -8859,6 +9477,23 @@ export const MODELS = { contextWindow: 262144, maxTokens: 32768, } satisfies Model<"openai-completions">, + "ibm-granite/granite-4.1-8b": { + id: "ibm-granite/granite-4.1-8b", + name: "IBM: Granite 4.1 8B", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: false, + input: ["text"], + cost: { + input: 0.049999999999999996, + output: 0.09999999999999999, + cacheRead: 0.049999999999999996, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 131072, + } satisfies Model<"openai-completions">, "inception/mercury-2": { id: "inception/mercury-2", name: "Inception: Mercury 2", @@ -10484,12 +11119,12 @@ export const MODELS = { input: ["text"], cost: { input: 0.039, - output: 0.19, + output: 0.18, cacheRead: 0, cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 131072, + maxTokens: 4096, } satisfies Model<"openai-completions">, "openai/gpt-oss-120b:free": { id: "openai/gpt-oss-120b:free", @@ -15654,605 +16289,4 @@ export const MODELS = { maxTokens: 131072, } satisfies Model<"openai-completions">, }, - "cloudflare-ai-gateway": { - "anthropic/claude-3-5-haiku": { - id: "anthropic/claude-3-5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-haiku": { - id: "anthropic/claude-3-haiku", - name: "Claude Haiku 3", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.25, - output: 1.25, - cacheRead: 0.03, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-opus": { - id: "anthropic/claude-3-opus", - name: "Claude Opus 3", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3-sonnet": { - id: "anthropic/claude-3-sonnet", - name: "Claude Sonnet 3", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 0.3, - }, - contextWindow: 200000, - maxTokens: 4096, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-haiku": { - id: "anthropic/claude-3.5-haiku", - name: "Claude Haiku 3.5 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-3.5-sonnet": { - id: "anthropic/claude-3.5-sonnet", - name: "Claude Sonnet 3.5 v2", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: false, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "anthropic/claude-haiku-4-5": { - id: "anthropic/claude-haiku-4-5", - name: "Claude Haiku 4.5 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1, - output: 5, - cacheRead: 0.1, - cacheWrite: 1.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4": { - id: "anthropic/claude-opus-4", - name: "Claude Opus 4 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4-1": { - id: "anthropic/claude-opus-4-1", - name: "Claude Opus 4.1 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - contextWindow: 200000, - maxTokens: 32000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4-5": { - id: "anthropic/claude-opus-4-5", - name: "Claude Opus 4.5 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4-6": { - id: "anthropic/claude-opus-4-6", - name: "Claude Opus 4.6 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-opus-4-7": { - id: "anthropic/claude-opus-4-7", - name: "Claude Opus 4.7", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 25, - cacheRead: 0.5, - cacheWrite: 6.25, - }, - contextWindow: 1000000, - maxTokens: 128000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4": { - id: "anthropic/claude-sonnet-4", - name: "Claude Sonnet 4 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4-5": { - id: "anthropic/claude-sonnet-4-5", - name: "Claude Sonnet 4.5 (latest)", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 200000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "anthropic/claude-sonnet-4-6": { - id: "anthropic/claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - reasoning: true, - input: ["text", "image"], - cost: { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - contextWindow: 1000000, - maxTokens: 64000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/moonshotai/kimi-k2.5": { - id: "workers-ai/@cf/moonshotai/kimi-k2.5", - name: "Kimi K2.5", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.6, - output: 3, - cacheRead: 0.1, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/moonshotai/kimi-k2.6": { - id: "workers-ai/@cf/moonshotai/kimi-k2.6", - name: "Kimi K2.6", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 0.95, - output: 4, - cacheRead: 0.16, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/nvidia/nemotron-3-120b-a12b": { - id: "workers-ai/@cf/nvidia/nemotron-3-120b-a12b", - name: "Nemotron 3 Super 120B", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.5, - output: 1.5, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 256000, - maxTokens: 256000, - } satisfies Model<"openai-completions">, - "workers-ai/@cf/zai-org/glm-4.7-flash": { - id: "workers-ai/@cf/zai-org/glm-4.7-flash", - name: "GLM-4.7-Flash", - api: "openai-completions", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/compat", - compat: {"sendSessionAffinityHeaders":true}, - reasoning: true, - input: ["text"], - cost: { - input: 0.06, - output: 0.4, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 131072, - } satisfies Model<"openai-completions">, - "gpt-4": { - id: "gpt-4", - name: "GPT-4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text"], - cost: { - input: 30, - output: 60, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 8192, - maxTokens: 8192, - } satisfies Model<"openai-responses">, - "gpt-4-turbo": { - id: "gpt-4-turbo", - name: "GPT-4 Turbo", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 10, - output: 30, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 4096, - } satisfies Model<"openai-responses">, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 2.5, - output: 10, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: false, - input: ["text", "image"], - cost: { - input: 0.15, - output: 0.6, - cacheRead: 0.08, - cacheWrite: 0, - }, - contextWindow: 128000, - maxTokens: 16384, - } satisfies Model<"openai-responses">, - "gpt-5.1": { - id: "gpt-5.1", - name: "GPT-5.1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.13, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.1-codex": { - id: "gpt-5.1-codex", - name: "GPT-5.1 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 10, - cacheRead: 0.125, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2": { - id: "gpt-5.2", - name: "GPT-5.2", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.2-codex": { - id: "gpt-5.2-codex", - name: "GPT-5.2 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.3-codex": { - id: "gpt-5.3-codex", - name: "GPT-5.3 Codex", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.75, - output: 14, - cacheRead: 0.175, - cacheWrite: 0, - }, - contextWindow: 400000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.4": { - id: "gpt-5.4", - name: "GPT-5.4", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2.5, - output: 15, - cacheRead: 0.25, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "gpt-5.5": { - id: "gpt-5.5", - name: "GPT-5.5", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 5, - output: 30, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 1050000, - maxTokens: 128000, - } satisfies Model<"openai-responses">, - "o1": { - id: "o1", - name: "o1", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 15, - output: 60, - cacheRead: 7.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3": { - id: "o3", - name: "o3", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 2, - output: 8, - cacheRead: 0.5, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-mini": { - id: "o3-mini", - name: "o3-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.55, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o3-pro": { - id: "o3-pro", - name: "o3-pro", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 20, - output: 80, - cacheRead: 0, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - "o4-mini": { - id: "o4-mini", - name: "o4-mini", - api: "openai-responses", - provider: "cloudflare-ai-gateway", - baseUrl: "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai", - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.1, - output: 4.4, - cacheRead: 0.28, - cacheWrite: 0, - }, - contextWindow: 200000, - maxTokens: 100000, - } satisfies Model<"openai-responses">, - }, } as const; diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 367e1975..13d82b85 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -32,6 +32,7 @@ import { headersToRecord } from "../utils/headers.js"; import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; +import { resolveCloudflareBaseUrl } from "./cloudflare.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; @@ -215,8 +216,8 @@ export interface AnthropicOptions extends StreamOptions { client?: Anthropic; } -function mergeHeaders(...headerSources: (Record | undefined)[]): Record { - const merged: Record = {}; +function mergeHeaders(...headerSources: (Record | undefined)[]): Record { + const merged: Record = {}; for (const headers of headerSources) { if (headers) { Object.assign(merged, headers); @@ -772,17 +773,39 @@ function createClient( // Adaptive thinking models (Opus 4.6, Sonnet 4.6) have interleaved thinking built-in. // The beta header is deprecated on Opus 4.6 and redundant on Sonnet 4.6, so skip it. const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model.id); + const betaFeatures: string[] = []; + if (useFineGrainedToolStreamingBeta) { + betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA); + } + if (needsInterleavedBeta) { + betaFeatures.push(INTERLEAVED_THINKING_BETA); + } + + if (model.provider === "cloudflare-ai-gateway") { + const client = new Anthropic({ + apiKey: null, + authToken: null, + baseURL: resolveCloudflareBaseUrl(model), + dangerouslyAllowBrowser: true, + defaultHeaders: mergeHeaders( + { + accept: "application/json", + "anthropic-dangerous-direct-browser-access": "true", + "cf-aig-authorization": `Bearer ${apiKey}`, + "x-api-key": null, + Authorization: null, + ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}), + }, + model.headers, + optionsHeaders, + ), + }); + + return { client, isOAuthToken: false }; + } // Copilot: Bearer auth, selective betas. if (model.provider === "github-copilot") { - const betaFeatures: string[] = []; - if (useFineGrainedToolStreamingBeta) { - betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA); - } - if (needsInterleavedBeta) { - betaFeatures.push(INTERLEAVED_THINKING_BETA); - } - const client = new Anthropic({ apiKey: null, authToken: apiKey, @@ -803,14 +826,6 @@ function createClient( return { client, isOAuthToken: false }; } - const betaFeatures: string[] = []; - if (useFineGrainedToolStreamingBeta) { - betaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA); - } - if (needsInterleavedBeta) { - betaFeatures.push(INTERLEAVED_THINKING_BETA); - } - // OAuth: Bearer auth, Claude Code identity headers if (isOAuthToken(apiKey)) { const client = new Anthropic({ diff --git a/packages/ai/src/providers/cloudflare.ts b/packages/ai/src/providers/cloudflare.ts index 4fb234b5..8e9d44cb 100644 --- a/packages/ai/src/providers/cloudflare.ts +++ b/packages/ai/src/providers/cloudflare.ts @@ -12,6 +12,10 @@ export const CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL = export const CLOUDFLARE_AI_GATEWAY_OPENAI_BASE_URL = "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/openai"; +/** AI Gateway → Anthropic passthrough. */ +export const CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL = + "https://gateway.ai.cloudflare.com/v1/{CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}/anthropic"; + export function isCloudflareProvider(provider: string): boolean { return provider === "cloudflare-workers-ai" || provider === "cloudflare-ai-gateway"; } @@ -20,11 +24,12 @@ export function isCloudflareProvider(provider: string): boolean { export function resolveCloudflareBaseUrl(model: Model): string { const url = model.baseUrl; if (!url.includes("{")) return url; - return url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name: string) => { + const baseUrl = url.replace(/\{([A-Z_][A-Z0-9_]*)\}/g, (_match, name: string) => { const value = process.env[name]; if (!value) { throw new Error(`${name} is required for provider ${model.provider} but is not set.`); } return value; }); + return baseUrl; } diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 2860b254..6c7ef0d0 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -458,11 +458,20 @@ function createClient( Object.assign(headers, optionsHeaders); } + const defaultHeaders = + model.provider === "cloudflare-ai-gateway" + ? { + ...headers, + Authorization: headers.Authorization ?? null, + "cf-aig-authorization": `Bearer ${apiKey}`, + } + : headers; + return new OpenAI({ apiKey, baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders: headers, + defaultHeaders, }); } @@ -1029,6 +1038,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet const isZai = provider === "zai" || baseUrl.includes("api.z.ai"); const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com"); + const isCloudflareAiGateway = provider === "cloudflare-ai-gateway" || baseUrl.includes("gateway.ai.cloudflare.com"); const isNonStandard = provider === "cerebras" || @@ -1041,9 +1051,10 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet isMoonshot || provider === "opencode" || baseUrl.includes("opencode.ai") || - isCloudflareWorkersAI; + isCloudflareWorkersAI || + isCloudflareAiGateway; - const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot; + const useMaxTokens = baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway; const isGrok = provider === "xai" || baseUrl.includes("api.x.ai"); const isGroq = provider === "groq" || baseUrl.includes("groq.com"); @@ -1070,7 +1081,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet return { supportsStore: !isNonStandard, supportsDeveloperRole: !isNonStandard, - supportsReasoningEffort: !isGrok && !isZai && !isMoonshot, + supportsReasoningEffort: !isGrok && !isZai && !isMoonshot && !isCloudflareAiGateway, reasoningEffortMap, supportsUsageInStreaming: true, maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens", @@ -1088,10 +1099,10 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet openRouterRouting: {}, vercelGatewayRouting: {}, zaiToolStream: false, - supportsStrictMode: !isMoonshot, + supportsStrictMode: !isMoonshot && !isCloudflareAiGateway, cacheControlFormat, sendSessionAffinityHeaders: false, - supportsLongCacheRetention: true, + supportsLongCacheRetention: !(isCloudflareWorkersAI || isCloudflareAiGateway), }; } diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 80acc65f..7d327468 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -197,11 +197,20 @@ function createClient( Object.assign(headers, optionsHeaders); } + const defaultHeaders = + model.provider === "cloudflare-ai-gateway" + ? { + ...headers, + Authorization: headers.Authorization ?? null, + "cf-aig-authorization": `Bearer ${apiKey}`, + } + : headers; + return new OpenAI({ apiKey, baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl, dangerouslyAllowBrowser: true, - defaultHeaders: headers, + defaultHeaders, }); } diff --git a/packages/ai/test/cloudflare-utils.ts b/packages/ai/test/cloudflare-utils.ts new file mode 100644 index 00000000..40ec74c5 --- /dev/null +++ b/packages/ai/test/cloudflare-utils.ts @@ -0,0 +1,9 @@ +export function hasCloudflareWorkersAICredentials(): boolean { + return !!process.env.CLOUDFLARE_API_KEY && !!process.env.CLOUDFLARE_ACCOUNT_ID; +} + +export function hasCloudflareAiGatewayCredentials(): boolean { + return ( + !!process.env.CLOUDFLARE_API_KEY && !!process.env.CLOUDFLARE_ACCOUNT_ID && !!process.env.CLOUDFLARE_GATEWAY_ID + ); +} diff --git a/packages/ai/test/cross-provider-handoff.test.ts b/packages/ai/test/cross-provider-handoff.test.ts index 2f24b01d..f1c98080 100644 --- a/packages/ai/test/cross-provider-handoff.test.ts +++ b/packages/ai/test/cross-provider-handoff.test.ts @@ -29,6 +29,7 @@ import { getModel } from "../src/models.js"; import { completeSimple, getEnvApiKey } from "../src/stream.js"; import type { Api, AssistantMessage, Message, Model, Tool, ToolResultMessage } from "../src/types.js"; import { hasAzureOpenAICredentials } from "./azure-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Simple tool for testing @@ -48,6 +49,7 @@ interface ProviderModelPair { model: string; label: string; apiOverride?: Api; + upstreamApiKeyEnv?: string; } const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ @@ -83,6 +85,24 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [ { provider: "cerebras", model: "zai-glm-4.7", label: "cerebras-zai-glm-4.7" }, // Cloudflare Workers AI { provider: "cloudflare-workers-ai", model: "@cf/moonshotai/kimi-k2.6", label: "cloudflare-kimi-k2.6" }, + // Cloudflare AI Gateway + { + provider: "cloudflare-ai-gateway", + model: "workers-ai/@cf/moonshotai/kimi-k2.6", + label: "cloudflare-gateway-kimi-k2.6", + }, + { + provider: "cloudflare-ai-gateway", + model: "claude-sonnet-4-5", + label: "cloudflare-gateway-claude-sonnet-4-5", + upstreamApiKeyEnv: "ANTHROPIC_API_KEY", + }, + { + provider: "cloudflare-ai-gateway", + model: "gpt-5.1", + label: "cloudflare-gateway-gpt-5.1", + upstreamApiKeyEnv: "OPENAI_API_KEY", + }, // Groq { provider: "groq", model: "openai/gpt-oss-120b", label: "groq-gpt-oss-120b" }, // Hugging Face @@ -127,18 +147,31 @@ async function getApiKey(provider: string): Promise { /** * Synchronous check for API key availability (env vars only, for skipIf) */ -function hasApiKey(provider: string): boolean { - if (provider === "azure-openai-responses") { +function hasApiKey(pair: ProviderModelPair): boolean { + if (pair.provider === "azure-openai-responses") { return hasAzureOpenAICredentials(); } - return !!getEnvApiKey(provider); + if (pair.provider === "cloudflare-workers-ai") { + return hasCloudflareWorkersAICredentials(); + } + if (pair.provider === "cloudflare-ai-gateway") { + if (!hasCloudflareAiGatewayCredentials()) return false; + return pair.upstreamApiKeyEnv ? !!process.env[pair.upstreamApiKeyEnv] : true; + } + return !!getEnvApiKey(pair.provider); +} + +function getHeaders(pair: ProviderModelPair): Record | undefined { + if (!pair.upstreamApiKeyEnv) return undefined; + const upstreamApiKey = process.env[pair.upstreamApiKeyEnv]; + return upstreamApiKey ? { Authorization: `Bearer ${upstreamApiKey}` } : undefined; } /** * Check if any provider has API keys available (for skipIf at describe level) */ function hasAnyApiKey(): boolean { - return PROVIDER_MODEL_PAIRS.some((pair) => hasApiKey(pair.provider)); + return PROVIDER_MODEL_PAIRS.some((pair) => hasApiKey(pair)); } function dumpFailurePayload(params: { label: string; error: string; payload?: unknown; messages: Message[] }): void { @@ -176,6 +209,7 @@ async function generateContext( }; const supportsReasoning = model.reasoning === true; + const headers = getHeaders(pair); let lastPayload: unknown; let assistantResponse: AssistantMessage; try { @@ -189,6 +223,7 @@ async function generateContext( { apiKey, reasoning: supportsReasoning ? "high" : undefined, + headers, onPayload: (payload) => { lastPayload = payload; }, @@ -250,6 +285,7 @@ async function generateContext( { apiKey, reasoning: supportsReasoning ? "high" : undefined, + headers, onPayload: (payload) => { lastPayload = payload; }, @@ -296,7 +332,7 @@ describe.skipIf(!hasAnyApiKey())("Cross-Provider Handoff", () => { for (const pair of PROVIDER_MODEL_PAIRS) { const apiKey = await getApiKey(pair.provider); - if (!apiKey) { + if (!apiKey || !hasApiKey(pair)) { console.log(`[${pair.label}] Skipping - no auth for ${pair.provider}`); continue; } @@ -344,7 +380,7 @@ describe.skipIf(!hasAnyApiKey())("Cross-Provider Handoff", () => { for (const targetPair of availablePairs) { const apiKey = await getApiKey(targetPair.provider); - if (!apiKey) { + if (!apiKey || !hasApiKey(targetPair)) { console.log(`[Target: ${targetPair.label}] Skipping - no auth`); continue; } @@ -384,6 +420,7 @@ describe.skipIf(!hasAnyApiKey())("Cross-Provider Handoff", () => { ? { ...baseModel, api: targetPair.apiOverride } : baseModel; const supportsReasoning = model.reasoning === true; + const headers = getHeaders(targetPair); console.log( `[Target: ${targetPair.label}] Testing with ${otherMessages.length} messages from other providers...`, @@ -401,6 +438,7 @@ describe.skipIf(!hasAnyApiKey())("Cross-Provider Handoff", () => { { apiKey, reasoning: supportsReasoning ? "high" : undefined, + headers, onPayload: (payload) => { lastPayload = payload; }, diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 230abbb1..c451eddf 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -7,6 +7,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) @@ -306,28 +307,45 @@ describe("AI Providers Empty Message Tests", () => { }); }); - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( - "Cloudflare Workers AI Provider Empty Messages", - () => { - const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI Provider Empty Messages", () => { + const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); - it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { - await testEmptyMessage(llm); - }); + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); - it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { - await testEmptyStringMessage(llm); - }); + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); - it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { - await testWhitespaceOnlyMessage(llm); - }); + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); - it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { - await testEmptyAssistantMessage(llm); - }); - }, - ); + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); + + describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway Provider Empty Messages", () => { + const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + + it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { + await testEmptyMessage(llm); + }); + + it("should handle empty string content", { retry: 3, timeout: 30000 }, async () => { + await testEmptyStringMessage(llm); + }); + + it("should handle whitespace-only content", { retry: 3, timeout: 30000 }, async () => { + await testWhitespaceOnlyMessage(llm); + }); + + it("should handle empty assistant message in conversation", { retry: 3, timeout: 30000 }, async () => { + await testEmptyAssistantMessage(llm); + }); + }); describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider Empty Messages", () => { const llm = getModel("huggingface", "moonshotai/Kimi-K2.5"); diff --git a/packages/ai/test/openai-completions-empty-tools.test.ts b/packages/ai/test/openai-completions-empty-tools.test.ts index 627d1b35..0c120980 100644 --- a/packages/ai/test/openai-completions-empty-tools.test.ts +++ b/packages/ai/test/openai-completions-empty-tools.test.ts @@ -9,10 +9,15 @@ import { streamSimple } from "../src/stream.js"; const mockState = vi.hoisted(() => ({ lastParams: undefined as unknown, + lastClientOptions: undefined as unknown, })); vi.mock("openai", () => { class FakeOpenAI { + constructor(options: unknown) { + mockState.lastClientOptions = options; + } + chat = { completions: { create: (params: unknown) => { @@ -52,6 +57,7 @@ vi.mock("openai", () => { describe("openai-completions empty tools handling", () => { beforeEach(() => { mockState.lastParams = undefined; + mockState.lastClientOptions = undefined; }); it("omits tools field when context.tools is an empty array", async () => { @@ -87,6 +93,79 @@ describe("openai-completions empty tools handling", () => { expect("tools" in (params as object)).toBe(false); }); + it("uses conservative OpenAI-compatible fields for Cloudflare AI Gateway /compat models", async () => { + process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; + process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; + const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; + + await streamSimple( + model, + { + systemPrompt: "You are helpful.", + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { apiKey: "test", reasoning: "high" }, + ).result(); + + const params = mockState.lastParams as { + messages: Array<{ role: string }>; + max_tokens?: number; + max_completion_tokens?: number; + reasoning_effort?: string; + store?: boolean; + }; + expect(params.messages[0].role).toBe("system"); + expect(params.max_tokens).toBeDefined(); + expect(params.max_completion_tokens).toBeUndefined(); + expect(params.reasoning_effort).toBeUndefined(); + expect(params.store).toBeUndefined(); + + const clientOptions = mockState.lastClientOptions as { + baseURL?: string; + defaultHeaders?: Record; + }; + expect(clientOptions.baseURL).toBe("https://gateway.ai.cloudflare.com/v1/account-id/gateway-id/compat"); + expect(clientOptions.defaultHeaders?.Authorization).toBeNull(); + expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer test"); + }); + + it("preserves inline upstream Authorization for Cloudflare AI Gateway BYOK requests", async () => { + process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; + process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; + const model = getModel("cloudflare-ai-gateway", "gpt-5.1")!; + + await streamSimple( + model, + { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { apiKey: "cf-token", headers: { Authorization: "Bearer upstream-token" } }, + ).result(); + + const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; + expect(clientOptions.defaultHeaders?.Authorization).toBe("Bearer upstream-token"); + expect(clientOptions.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer cf-token"); + }); + + it("sends session affinity headers for Workers AI through Cloudflare AI Gateway", async () => { + process.env.CLOUDFLARE_ACCOUNT_ID = "account-id"; + process.env.CLOUDFLARE_GATEWAY_ID = "gateway-id"; + const workersModel = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6")!; + + await streamSimple( + workersModel, + { + messages: [{ role: "user", content: "hi", timestamp: Date.now() }], + }, + { apiKey: "test", sessionId: "session-1" }, + ).result(); + + const clientOptions = mockState.lastClientOptions as { defaultHeaders?: Record }; + expect(clientOptions.defaultHeaders?.session_id).toBe("session-1"); + expect(clientOptions.defaultHeaders?.["x-client-request-id"]).toBe("session-1"); + expect(clientOptions.defaultHeaders?.["x-session-affinity"]).toBe("session-1"); + }); + it("still emits tools: [] for Anthropic/LiteLLM proxy when conversation has tool history", async () => { const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!; const model = { ...baseModel, api: "openai-completions" } as const; diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index df1fdc70..abc91dca 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -13,6 +13,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { StringEnum } from "../src/utils/typebox-helpers.js"; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; const __filename = fileURLToPath(import.meta.url); @@ -614,7 +615,7 @@ describe("Generate E2E Tests", () => { }); }); - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( + describe.skipIf(!hasCloudflareWorkersAICredentials())( "Cloudflare Workers AI Provider (Kimi K2.6 via OpenAI Completions)", () => { const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); @@ -641,59 +642,98 @@ describe("Generate E2E Tests", () => { }, ); - describe.skipIf( - !process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_GATEWAY_ID, - )("Cloudflare AI Gateway → Workers AI (Kimi K2.6 via /compat)", () => { - const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareAiGatewayCredentials())( + "Cloudflare AI Gateway → Workers AI (Kimi K2.6 via /compat)", + () => { + const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); - it("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm); - }); + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm); + }); - it("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm); - }); + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm); + }); - it("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm); - }); - }); + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm); + }); - describe.skipIf( - !process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_GATEWAY_ID, - )("Cloudflare AI Gateway → OpenAI (gpt-5.1 via /openai responses)", () => { - const llm = getModel("cloudflare-ai-gateway", "gpt-5.1"); + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, { reasoningEffort: "medium" }); + }); - it("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm); - }); + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, { reasoningEffort: "medium" }); + }); + }, + ); - it("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm); - }); + describe.skipIf(!hasCloudflareAiGatewayCredentials() || !process.env.OPENAI_API_KEY)( + "Cloudflare AI Gateway → OpenAI BYOK (gpt-5.1 via /openai responses)", + () => { + const llm = getModel("cloudflare-ai-gateway", "gpt-5.1"); + const options = { headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` } }; + const thinkingOptions = { + ...options, + thinkingEnabled: true, + reasoningEffort: "medium", + } satisfies StreamOptionsWithExtras; - it("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm); - }); - }); + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm, options); + }); - describe.skipIf( - !process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_GATEWAY_ID, - )("Cloudflare AI Gateway → Anthropic (claude-sonnet-4-5 via /compat)", () => { - const llm = getModel("cloudflare-ai-gateway", "anthropic/claude-sonnet-4-5"); + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm, options); + }); - it("should complete basic text generation", { retry: 3 }, async () => { - await basicTextGeneration(llm); - }); + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm, options); + }); - it("should handle tool calling", { retry: 3 }, async () => { - await handleToolCall(llm); - }); + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); - it("should handle streaming", { retry: 3 }, async () => { - await handleStreaming(llm); - }); - }); + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }, + ); + + describe.skipIf(!hasCloudflareAiGatewayCredentials() || !process.env.ANTHROPIC_API_KEY)( + "Cloudflare AI Gateway → Anthropic BYOK (claude-sonnet-4-5 via /anthropic messages)", + () => { + const llm = getModel("cloudflare-ai-gateway", "claude-sonnet-4-5"); + const options = { headers: { Authorization: `Bearer ${process.env.ANTHROPIC_API_KEY}` } }; + const thinkingOptions = { + ...options, + thinkingEnabled: true, + reasoningEffort: "high", + } satisfies StreamOptionsWithExtras; + + it("should complete basic text generation", { retry: 3 }, async () => { + await basicTextGeneration(llm, options); + }); + + it("should handle tool calling", { retry: 3 }, async () => { + await handleToolCall(llm, options); + }); + + it("should handle streaming", { retry: 3 }, async () => { + await handleStreaming(llm, options); + }); + + it("should handle thinking mode", { retry: 3 }, async () => { + await handleThinking(llm, thinkingOptions); + }); + + it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { + await multiTurn(llm, thinkingOptions); + }); + }, + ); describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider (Kimi-K2.5 via OpenAI Completions)", () => { const llm = getModel("huggingface", "moonshotai/Kimi-K2.5"); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index bd1ee8c0..ff3fc7a3 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -7,6 +7,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) @@ -155,16 +156,21 @@ describe("Token Statistics on Abort", () => { }); }); - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( - "Cloudflare Workers AI Provider", - () => { - const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI Provider", () => { + const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); - it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { - await testTokensOnAbort(llm); - }); - }, - ); + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); + + describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway Provider", () => { + const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + + it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { + await testTokensOnAbort(llm); + }); + }); describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider", () => { const llm = getModel("huggingface", "moonshotai/Kimi-K2.5"); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 22822e32..514a6fd1 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -8,6 +8,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) @@ -166,20 +167,21 @@ describe("Tool Call Without Result Tests", () => { }); }); - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( - "Cloudflare Workers AI Provider", - () => { - const model = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI Provider", () => { + const model = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); - it( - "should filter out tool calls without corresponding tool results", - { retry: 3, timeout: 30000 }, - async () => { - await testToolCallWithoutResult(model); - }, - ); - }, - ); + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); + + describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway Provider", () => { + const model = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + + it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { + await testToolCallWithoutResult(model); + }); + }); describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider", () => { const model = getModel("huggingface", "moonshotai/Kimi-K2.5"); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index 300b0b0f..c5eda15c 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -21,6 +21,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Resolve OAuth tokens at module level (async, runs before tests) @@ -308,29 +309,51 @@ describe("totalTokens field", () => { // Cloudflare Workers AI // ========================================================================= - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( - "Cloudflare Workers AI", - () => { - it( - "@cf/moonshotai/kimi-k2.6 - should return totalTokens equal to sum of components", - { retry: 3, timeout: 60000 }, - async () => { - const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI", () => { + it( + "@cf/moonshotai/kimi-k2.6 - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); - console.log(`\nCloudflare Workers AI / ${llm.id}:`); - const { first, second } = await testTotalTokensWithCache(llm, { - apiKey: process.env.CLOUDFLARE_API_KEY, - }); + console.log(`\nCloudflare Workers AI / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.CLOUDFLARE_API_KEY, + }); - logUsage("First request", first); - logUsage("Second request", second); + logUsage("First request", first); + logUsage("Second request", second); - assertTotalTokensEqualsComponents(first); - assertTotalTokensEqualsComponents(second); - }, - ); - }, - ); + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); + + // ========================================================================= + // Cloudflare AI Gateway + // ========================================================================= + + describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway", () => { + it( + "workers-ai/@cf/moonshotai/kimi-k2.6 - should return totalTokens equal to sum of components", + { retry: 3, timeout: 60000 }, + async () => { + const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + + console.log(`\nCloudflare AI Gateway / ${llm.id}:`); + const { first, second } = await testTotalTokensWithCache(llm, { + apiKey: process.env.CLOUDFLARE_API_KEY, + }); + + logUsage("First request", first); + logUsage("Second request", second); + + assertTotalTokensEqualsComponents(first); + assertTotalTokensEqualsComponents(second); + }, + ); + }); // ========================================================================= // Hugging Face diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index b32005f5..6b102c39 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -8,6 +8,7 @@ type StreamOptionsWithExtras = StreamOptions & Record; import { hasAzureOpenAICredentials, resolveAzureDeploymentName } from "./azure-utils.js"; import { hasBedrockCredentials } from "./bedrock-utils.js"; +import { hasCloudflareAiGatewayCredentials, hasCloudflareWorkersAICredentials } from "./cloudflare-utils.js"; import { resolveApiKey } from "./oauth.js"; // Empty schema for test tools - must be proper OBJECT type for Cloud Code Assist @@ -497,28 +498,37 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { }); }); - describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)( - "Cloudflare Workers AI Provider Unicode Handling", - () => { - const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); + describe.skipIf(!hasCloudflareWorkersAICredentials())("Cloudflare Workers AI Provider Unicode Handling", () => { + const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6"); - it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { - await testEmojiInToolResults(llm); - }); + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); - it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { - await testRealWorldLinkedInData(llm); - }); + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); - it( - "should handle unpaired high surrogate (0xD83D) in tool results", - { retry: 3, timeout: 30000 }, - async () => { - await testUnpairedHighSurrogate(llm); - }, - ); - }, - ); + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); + + describe.skipIf(!hasCloudflareAiGatewayCredentials())("Cloudflare AI Gateway Provider Unicode Handling", () => { + const llm = getModel("cloudflare-ai-gateway", "workers-ai/@cf/moonshotai/kimi-k2.6"); + + it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { + await testEmojiInToolResults(llm); + }); + + it("should handle real-world LinkedIn comment data with emoji", { retry: 3, timeout: 30000 }, async () => { + await testRealWorldLinkedInData(llm); + }); + + it("should handle unpaired high surrogate (0xD83D) in tool results", { retry: 3, timeout: 30000 }, async () => { + await testUnpairedHighSurrogate(llm); + }); + }); describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider Unicode Handling", () => { const llm = getModel("huggingface", "moonshotai/Kimi-K2.5"); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 20e6fbe0..dabf4cd4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ ### Added +- Added Cloudflare AI Gateway as a built-in provider with `CLOUDFLARE_API_KEY`/`CLOUDFLARE_ACCOUNT_ID`/`CLOUDFLARE_GATEWAY_ID` setup, default model resolution, `/login` display support, and provider documentation ([#3856](https://github.com/badlogic/pi-mono/pull/3856) by [@mchenco](https://github.com/mchenco)). - Added Moonshot AI as a built-in provider with `MOONSHOT_API_KEY` setup, default model resolution, and `/login` display support. - Added Mistral Medium 3.5 built-in model support via `@mariozechner/pi-ai` ([#4009](https://github.com/badlogic/pi-mono/pull/4009) by [@technocidal](https://github.com/technocidal)). - Added routed OpenAI-compatible response model metadata in assistant messages, so providers such as OpenRouter can expose the concrete model used ([#3968](https://github.com/badlogic/pi-mono/pull/3968) by [@purrgrammer](https://github.com/purrgrammer)). diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 81e51b18..41b1c103 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -178,10 +178,21 @@ export AWS_BEDROCK_FORCE_HTTP1=1 export CLOUDFLARE_API_KEY=... # or use /login export CLOUDFLARE_ACCOUNT_ID=... export CLOUDFLARE_GATEWAY_ID=... # create at dash.cloudflare.com → AI → AI Gateway -pi --provider cloudflare-ai-gateway --model "anthropic/claude-sonnet-4-5" +pi --provider cloudflare-ai-gateway --model "claude-sonnet-4-5" ``` -Routes to OpenAI, Anthropic, and Workers AI through Cloudflare's [Unified API](https://developers.cloudflare.com/ai-gateway/usage/unified-api/) (`/compat`). Workers AI and Anthropic models use the prefixed model id (`workers-ai/@cf/...`, `anthropic/...`). OpenAI reasoning models (gpt-5.x, o-series) need `/v1/responses` which `/compat` doesn't yet expose, so they route through the gateway's `/openai` provider-specific subpath with the native id (`gpt-5.1`). Requires BYOK or unified billing on the gateway for non-Workers-AI upstreams. +Routes to OpenAI, Anthropic, and Workers AI through Cloudflare AI Gateway. Workers AI uses the Unified API (`/compat`) and prefixed model IDs (`workers-ai/@cf/...`). OpenAI uses the OpenAI passthrough route (`/openai`) with native OpenAI model IDs such as `gpt-5.1`. Anthropic uses the Anthropic passthrough route (`/anthropic`) with native Anthropic model IDs such as `claude-sonnet-4-5`. + +AI Gateway authentication uses `CLOUDFLARE_API_KEY` as `cf-aig-authorization`. Upstream authentication can be one of: + +| Mode | Request auth | Upstream auth | +|------|--------------|---------------| +| Workers AI | Cloudflare token only | Cloudflare-native | +| Unified billing | Cloudflare token only | Cloudflare handles upstream auth and deducts credits | +| Stored BYOK | Cloudflare token only | Cloudflare injects provider keys stored in the AI Gateway dashboard | +| Inline BYOK | Cloudflare token plus upstream `Authorization` header | The request supplies the upstream provider key | + +For normal pi usage, prefer unified billing or stored BYOK. Inline BYOK requires configuring an additional upstream `Authorization` header for the Cloudflare AI Gateway provider, for example via a `models.json` provider/model override. ### Cloudflare Workers AI diff --git a/packages/coding-agent/src/core/provider-display-names.ts b/packages/coding-agent/src/core/provider-display-names.ts index 42028ffd..63f888ac 100644 --- a/packages/coding-agent/src/core/provider-display-names.ts +++ b/packages/coding-agent/src/core/provider-display-names.ts @@ -3,6 +3,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record = { "amazon-bedrock": "Amazon Bedrock", "azure-openai-responses": "Azure OpenAI Responses", cerebras: "Cerebras", + "cloudflare-ai-gateway": "Cloudflare AI Gateway", "cloudflare-workers-ai": "Cloudflare Workers AI", deepseek: "DeepSeek", fireworks: "Fireworks",