fix(ai): finalize cloudflare gateway provider support
This commit is contained in:
9
packages/ai/test/cloudflare-utils.ts
Normal file
9
packages/ai/test/cloudflare-utils.ts
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
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");
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
};
|
||||
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<string, unknown> };
|
||||
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<string, string> };
|
||||
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;
|
||||
|
||||
@@ -13,6 +13,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
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");
|
||||
|
||||
@@ -7,6 +7,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
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");
|
||||
|
||||
@@ -8,6 +8,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
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");
|
||||
|
||||
@@ -21,6 +21,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user