fix(ai): finalize cloudflare gateway provider support

This commit is contained in:
Mario Zechner
2026-05-01 00:56:05 +02:00
parent 24fb6b833b
commit a45577bd00
19 changed files with 1086 additions and 768 deletions

View File

@@ -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<string | undefined> {
/**
* 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<string, string> | 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;
},