feat(ai): add Cloudflare Workers AI as a provider (#3851)
* feat(ai): add Cloudflare Workers AI as a provider
Cloudflare Workers AI hosts open-weight LLMs (Kimi K2.6, GPT-OSS,
GLM-4.7, Llama 4, Gemma 4, Nemotron 3) on Cloudflare's GPU network with
an OpenAI-compatible endpoint. Reuses the openai-completions API
protocol; the per-account URL contains a {CLOUDFLARE_ACCOUNT_ID}
placeholder resolved at request time by a small helper.
Pi automatically sets x-session-affinity for prefix caching:
https://developers.cloudflare.com/workers-ai/features/prompt-caching/
Auth: CLOUDFLARE_API_KEY (matches pi's *_API_KEY convention) +
CLOUDFLARE_ACCOUNT_ID. The User-Agent identifies traffic as
'pi-coding-agent' in Cloudflare analytics.
Verified end-to-end against a real Cloudflare account: 17 e2e tests
pass across stream/empty/tokens/unicode/tool-call-without-result/
total-tokens against @cf/moonshotai/kimi-k2.6.
Cloudflare AI Gateway is a separate, larger change (it requires routing
through provider-specific subpaths with the matching API protocol per
upstream) and will land in a follow-up PR.
* refactor(ai): move Cloudflare User-Agent and session-affinity flag to per-model metadata
Instead of conditionally setting them in openai-completions.ts based on
provider detection, declare them as model-level fields in the catalog
(headers + compat). This is consistent with how the github-copilot and
kimi-coding entries already declare their static headers.
packages/ai/scripts/generate-models.ts: emit headers and compat fields
on each cloudflare-workers-ai entry (CLOUDFLARE_STATIC_HEADERS).
packages/ai/src/providers/openai-completions.ts: drop the
isCloudflareProvider conditional that injected User-Agent and the
isCloudflareWorkersAI override of sendSessionAffinityHeaders.
packages/ai/src/models.generated.ts: re-spliced 8 cloudflare-workers-ai
entries with headers + compat.
Behavior is unchanged - verified via fetch interceptor that User-Agent
and x-session-affinity / session_id / x-client-request-id are still sent
on outbound requests. 5/5 e2e tests pass.
This commit is contained in:
@@ -57,6 +57,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
|||||||
- **Mistral**
|
- **Mistral**
|
||||||
- **Groq**
|
- **Groq**
|
||||||
- **Cerebras**
|
- **Cerebras**
|
||||||
|
- **Cloudflare Workers AI**
|
||||||
- **xAI**
|
- **xAI**
|
||||||
- **OpenRouter**
|
- **OpenRouter**
|
||||||
- **Vercel AI Gateway**
|
- **Vercel AI Gateway**
|
||||||
@@ -844,7 +845,7 @@ const ollamaReasoningModel: Model<'openai-completions'> = {
|
|||||||
|
|
||||||
### OpenAI Compatibility Settings
|
### OpenAI Compatibility Settings
|
||||||
|
|
||||||
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, zAi, OpenCode, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field only supports Responses-specific flags.
|
The `openai-completions` API is implemented by many providers with minor differences. By default, the library auto-detects compatibility settings based on `baseUrl` for a small set of known OpenAI-compatible providers (Cerebras, xAI, Chutes, DeepSeek, zAi, OpenCode, Cloudflare Workers AI, etc.). For custom proxies or unknown endpoints, you can override these settings via the `compat` field. For `openai-responses` models, the compat field only supports Responses-specific flags.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface OpenAICompletionsCompat {
|
interface OpenAICompletionsCompat {
|
||||||
@@ -1028,6 +1029,7 @@ In Node.js environments, you can set environment variables to avoid passing API
|
|||||||
| Mistral | `MISTRAL_API_KEY` |
|
| Mistral | `MISTRAL_API_KEY` |
|
||||||
| Groq | `GROQ_API_KEY` |
|
| Groq | `GROQ_API_KEY` |
|
||||||
| Cerebras | `CEREBRAS_API_KEY` |
|
| Cerebras | `CEREBRAS_API_KEY` |
|
||||||
|
| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID` |
|
||||||
| xAI | `XAI_API_KEY` |
|
| xAI | `XAI_API_KEY` |
|
||||||
| Fireworks | `FIREWORKS_API_KEY` |
|
| Fireworks | `FIREWORKS_API_KEY` |
|
||||||
| OpenRouter | `OPENROUTER_API_KEY` |
|
| OpenRouter | `OPENROUTER_API_KEY` |
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { writeFileSync } from "fs";
|
import { writeFileSync } from "fs";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import { CLOUDFLARE_WORKERS_AI_BASE_URL } from "../src/providers/cloudflare.js";
|
||||||
import {
|
import {
|
||||||
Api,
|
Api,
|
||||||
type AnthropicMessagesCompat,
|
type AnthropicMessagesCompat,
|
||||||
@@ -63,6 +64,10 @@ const KIMI_STATIC_HEADERS = {
|
|||||||
"User-Agent": "KimiCLI/1.5",
|
"User-Agent": "KimiCLI/1.5",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const CLOUDFLARE_STATIC_HEADERS = {
|
||||||
|
"User-Agent": "pi-coding-agent",
|
||||||
|
} as const;
|
||||||
|
|
||||||
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
const AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1";
|
||||||
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
const AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
||||||
const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
|
const ZAI_TOOL_STREAM_UNSUPPORTED_MODELS = new Set(["glm-4.5", "glm-4.5-air", "glm-4.5-flash", "glm-4.5v"]);
|
||||||
@@ -376,6 +381,34 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process Cloudflare Workers AI models
|
||||||
|
if (data["cloudflare-workers-ai"]?.models) {
|
||||||
|
for (const [modelId, model] of Object.entries(data["cloudflare-workers-ai"].models)) {
|
||||||
|
const m = model as ModelsDevModel;
|
||||||
|
if (m.tool_call !== true) continue;
|
||||||
|
|
||||||
|
models.push({
|
||||||
|
id: modelId,
|
||||||
|
name: m.name || modelId,
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: CLOUDFLARE_WORKERS_AI_BASE_URL,
|
||||||
|
reasoning: m.reasoning === true,
|
||||||
|
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||||
|
cost: {
|
||||||
|
input: m.cost?.input || 0,
|
||||||
|
output: m.cost?.output || 0,
|
||||||
|
cacheRead: m.cost?.cache_read || 0,
|
||||||
|
cacheWrite: m.cost?.cache_write || 0,
|
||||||
|
},
|
||||||
|
contextWindow: m.limit?.context || 4096,
|
||||||
|
maxTokens: m.limit?.output || 4096,
|
||||||
|
headers: { ...CLOUDFLARE_STATIC_HEADERS },
|
||||||
|
compat: { sendSessionAffinityHeaders: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process xAi models
|
// Process xAi models
|
||||||
if (data.xai?.models) {
|
if (data.xai?.models) {
|
||||||
for (const [modelId, model] of Object.entries(data.xai.models)) {
|
for (const [modelId, model] of Object.entries(data.xai.models)) {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined {
|
|||||||
opencode: "OPENCODE_API_KEY",
|
opencode: "OPENCODE_API_KEY",
|
||||||
"opencode-go": "OPENCODE_API_KEY",
|
"opencode-go": "OPENCODE_API_KEY",
|
||||||
"kimi-coding": "KIMI_API_KEY",
|
"kimi-coding": "KIMI_API_KEY",
|
||||||
|
"cloudflare-workers-ai": "CLOUDFLARE_API_KEY",
|
||||||
};
|
};
|
||||||
|
|
||||||
const envVar = envMap[provider];
|
const envVar = envMap[provider];
|
||||||
|
|||||||
@@ -15374,4 +15374,158 @@ export const MODELS = {
|
|||||||
maxTokens: 131072,
|
maxTokens: 131072,
|
||||||
} satisfies Model<"openai-completions">,
|
} satisfies Model<"openai-completions">,
|
||||||
},
|
},
|
||||||
|
"cloudflare-workers-ai": {
|
||||||
|
"@cf/google/gemma-4-26b-a4b-it": {
|
||||||
|
id: "@cf/google/gemma-4-26b-a4b-it",
|
||||||
|
name: "Gemma 4 26B A4B IT",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
compat: {"sendSessionAffinityHeaders":true},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0.1,
|
||||||
|
output: 0.3,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 256000,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"@cf/meta/llama-4-scout-17b-16e-instruct": {
|
||||||
|
id: "@cf/meta/llama-4-scout-17b-16e-instruct",
|
||||||
|
name: "Llama 4 Scout 17B 16E Instruct",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
compat: {"sendSessionAffinityHeaders":true},
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text", "image"],
|
||||||
|
cost: {
|
||||||
|
input: 0.27,
|
||||||
|
output: 0.85,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"@cf/moonshotai/kimi-k2.5": {
|
||||||
|
id: "@cf/moonshotai/kimi-k2.5",
|
||||||
|
name: "Kimi K2.5",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
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">,
|
||||||
|
"@cf/moonshotai/kimi-k2.6": {
|
||||||
|
id: "@cf/moonshotai/kimi-k2.6",
|
||||||
|
name: "Kimi K2.6",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
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">,
|
||||||
|
"@cf/nvidia/nemotron-3-120b-a12b": {
|
||||||
|
id: "@cf/nvidia/nemotron-3-120b-a12b",
|
||||||
|
name: "Nemotron 3 Super 120B",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
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">,
|
||||||
|
"@cf/openai/gpt-oss-120b": {
|
||||||
|
id: "@cf/openai/gpt-oss-120b",
|
||||||
|
name: "GPT OSS 120B",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
compat: {"sendSessionAffinityHeaders":true},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.35,
|
||||||
|
output: 0.75,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"@cf/openai/gpt-oss-20b": {
|
||||||
|
id: "@cf/openai/gpt-oss-20b",
|
||||||
|
name: "GPT OSS 20B",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
compat: {"sendSessionAffinityHeaders":true},
|
||||||
|
reasoning: true,
|
||||||
|
input: ["text"],
|
||||||
|
cost: {
|
||||||
|
input: 0.2,
|
||||||
|
output: 0.3,
|
||||||
|
cacheRead: 0,
|
||||||
|
cacheWrite: 0,
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
|
maxTokens: 16384,
|
||||||
|
} satisfies Model<"openai-completions">,
|
||||||
|
"@cf/zai-org/glm-4.7-flash": {
|
||||||
|
id: "@cf/zai-org/glm-4.7-flash",
|
||||||
|
name: "GLM-4.7-Flash",
|
||||||
|
api: "openai-completions",
|
||||||
|
provider: "cloudflare-workers-ai",
|
||||||
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||||
|
headers: {"User-Agent":"pi-coding-agent"},
|
||||||
|
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">,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
22
packages/ai/src/providers/cloudflare.ts
Normal file
22
packages/ai/src/providers/cloudflare.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Model } from "../types.js";
|
||||||
|
|
||||||
|
/** Workers AI endpoint. `{CLOUDFLARE_ACCOUNT_ID}` is substituted at request time. */
|
||||||
|
export const CLOUDFLARE_WORKERS_AI_BASE_URL =
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/{CLOUDFLARE_ACCOUNT_ID}/ai/v1";
|
||||||
|
|
||||||
|
export function isCloudflareProvider(provider: string): boolean {
|
||||||
|
return provider === "cloudflare-workers-ai";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Substitute `{VAR}` placeholders in a Cloudflare baseUrl from process.env. */
|
||||||
|
export function resolveCloudflareBaseUrl(model: Model<"openai-completions">): string {
|
||||||
|
const url = model.baseUrl;
|
||||||
|
if (!url.includes("{")) return url;
|
||||||
|
return 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|||||||
import { headersToRecord } from "../utils/headers.js";
|
import { headersToRecord } from "../utils/headers.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
|
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
||||||
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
||||||
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
import { buildBaseOptions, clampReasoning } from "./simple-options.js";
|
||||||
import { transformMessages } from "./transform-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
@@ -456,7 +457,7 @@ function createClient(
|
|||||||
|
|
||||||
return new OpenAI({
|
return new OpenAI({
|
||||||
apiKey,
|
apiKey,
|
||||||
baseURL: model.baseUrl,
|
baseURL: isCloudflareProvider(model.provider) ? resolveCloudflareBaseUrl(model) : model.baseUrl,
|
||||||
dangerouslyAllowBrowser: true,
|
dangerouslyAllowBrowser: true,
|
||||||
defaultHeaders: headers,
|
defaultHeaders: headers,
|
||||||
});
|
});
|
||||||
@@ -1022,6 +1023,7 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
const baseUrl = model.baseUrl;
|
const baseUrl = model.baseUrl;
|
||||||
|
|
||||||
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
|
const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
|
||||||
|
const isCloudflareWorkersAI = provider === "cloudflare-workers-ai" || baseUrl.includes("api.cloudflare.com");
|
||||||
|
|
||||||
const isNonStandard =
|
const isNonStandard =
|
||||||
provider === "cerebras" ||
|
provider === "cerebras" ||
|
||||||
@@ -1032,7 +1034,8 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
|
|||||||
baseUrl.includes("deepseek.com") ||
|
baseUrl.includes("deepseek.com") ||
|
||||||
isZai ||
|
isZai ||
|
||||||
provider === "opencode" ||
|
provider === "opencode" ||
|
||||||
baseUrl.includes("opencode.ai");
|
baseUrl.includes("opencode.ai") ||
|
||||||
|
isCloudflareWorkersAI;
|
||||||
|
|
||||||
const useMaxTokens = baseUrl.includes("chutes.ai");
|
const useMaxTokens = baseUrl.includes("chutes.ai");
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export type KnownProvider =
|
|||||||
| "fireworks"
|
| "fireworks"
|
||||||
| "opencode"
|
| "opencode"
|
||||||
| "opencode-go"
|
| "opencode-go"
|
||||||
| "kimi-coding";
|
| "kimi-coding"
|
||||||
|
| "cloudflare-workers-ai";
|
||||||
export type Provider = KnownProvider | string;
|
export type Provider = KnownProvider | string;
|
||||||
|
|
||||||
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
|
export type ThinkingLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [
|
|||||||
{ provider: "xai", model: "grok-code-fast-1", label: "xai-grok-code-fast-1" },
|
{ provider: "xai", model: "grok-code-fast-1", label: "xai-grok-code-fast-1" },
|
||||||
// Cerebras
|
// Cerebras
|
||||||
{ provider: "cerebras", model: "zai-glm-4.7", label: "cerebras-zai-glm-4.7" },
|
{ 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" },
|
||||||
// Groq
|
// Groq
|
||||||
{ provider: "groq", model: "openai/gpt-oss-120b", label: "groq-gpt-oss-120b" },
|
{ provider: "groq", model: "openai/gpt-oss-120b", label: "groq-gpt-oss-120b" },
|
||||||
// Hugging Face
|
// Hugging Face
|
||||||
|
|||||||
@@ -308,6 +308,29 @@ 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");
|
||||||
|
|
||||||
|
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", () => {
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider Empty Messages", () => {
|
||||||
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
||||||
|
|
||||||
|
|||||||
@@ -616,6 +616,33 @@ describe("Generate E2E Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe.skipIf(!process.env.CLOUDFLARE_API_KEY || !process.env.CLOUDFLARE_ACCOUNT_ID)(
|
||||||
|
"Cloudflare Workers AI Provider (Kimi K2.6 via OpenAI Completions)",
|
||||||
|
() => {
|
||||||
|
const llm = getModel("cloudflare-workers-ai", "@cf/moonshotai/kimi-k2.6");
|
||||||
|
|
||||||
|
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 streaming", { retry: 3 }, async () => {
|
||||||
|
await handleStreaming(llm);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle thinking mode", { retry: 3 }, async () => {
|
||||||
|
await handleThinking(llm, { reasoningEffort: "medium" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||||
|
await multiTurn(llm, { reasoningEffort: "medium" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider (Kimi-K2.5 via OpenAI Completions)", () => {
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider (Kimi-K2.5 via OpenAI Completions)", () => {
|
||||||
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,17 @@ 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");
|
||||||
|
|
||||||
|
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", () => {
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider", () => {
|
||||||
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,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");
|
||||||
|
|
||||||
|
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", () => {
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider", () => {
|
||||||
const model = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
const model = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,34 @@ 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");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
assertTotalTokensEqualsComponents(first);
|
||||||
|
assertTotalTokensEqualsComponents(second);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Hugging Face
|
// Hugging Face
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -611,6 +611,29 @@ 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");
|
||||||
|
|
||||||
|
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", () => {
|
describe.skipIf(!process.env.HF_TOKEN)("Hugging Face Provider Unicode Handling", () => {
|
||||||
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
const llm = getModel("huggingface", "moonshotai/Kimi-K2.5");
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated
|
|||||||
- Mistral
|
- Mistral
|
||||||
- Groq
|
- Groq
|
||||||
- Cerebras
|
- Cerebras
|
||||||
|
- Cloudflare Workers AI
|
||||||
- xAI
|
- xAI
|
||||||
- OpenRouter
|
- OpenRouter
|
||||||
- Vercel AI Gateway
|
- Vercel AI Gateway
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pi
|
|||||||
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
||||||
| Groq | `GROQ_API_KEY` | `groq` |
|
| Groq | `GROQ_API_KEY` | `groq` |
|
||||||
| Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
|
| Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
|
||||||
|
| Cloudflare Workers AI | `CLOUDFLARE_API_KEY` (+ `CLOUDFLARE_ACCOUNT_ID`) | `cloudflare-workers-ai` |
|
||||||
| xAI | `XAI_API_KEY` | `xai` |
|
| xAI | `XAI_API_KEY` | `xai` |
|
||||||
| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
|
| OpenRouter | `OPENROUTER_API_KEY` | `openrouter` |
|
||||||
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |
|
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` |
|
||||||
@@ -172,6 +173,18 @@ export AWS_BEDROCK_SKIP_AUTH=1
|
|||||||
export AWS_BEDROCK_FORCE_HTTP1=1
|
export AWS_BEDROCK_FORCE_HTTP1=1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Cloudflare Workers AI
|
||||||
|
|
||||||
|
`CLOUDFLARE_API_KEY` can be set via `/login`. `CLOUDFLARE_ACCOUNT_ID` must be set as an environment variable.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CLOUDFLARE_API_KEY=... # or use /login
|
||||||
|
export CLOUDFLARE_ACCOUNT_ID=...
|
||||||
|
pi --provider cloudflare-workers-ai --model "@cf/moonshotai/kimi-k2.6"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pi automatically sets `x-session-affinity` for [prefix caching](https://developers.cloudflare.com/workers-ai/features/prompt-caching/) discounts.
|
||||||
|
|
||||||
### Google Vertex AI
|
### Google Vertex AI
|
||||||
|
|
||||||
Uses Application Default Credentials:
|
Uses Application Default Credentials:
|
||||||
|
|||||||
@@ -316,6 +316,8 @@ ${chalk.bold("Environment Variables:")}
|
|||||||
MINIMAX_API_KEY - MiniMax API key
|
MINIMAX_API_KEY - MiniMax API key
|
||||||
OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key
|
OPENCODE_API_KEY - OpenCode Zen/OpenCode Go API key
|
||||||
KIMI_API_KEY - Kimi For Coding API key
|
KIMI_API_KEY - Kimi For Coding API key
|
||||||
|
CLOUDFLARE_API_KEY - Cloudflare API token (Workers AI)
|
||||||
|
CLOUDFLARE_ACCOUNT_ID - Cloudflare account id (required for Workers AI)
|
||||||
AWS_PROFILE - AWS profile for Amazon Bedrock
|
AWS_PROFILE - AWS profile for Amazon Bedrock
|
||||||
AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
|
AWS_ACCESS_KEY_ID - AWS access key for Amazon Bedrock
|
||||||
AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
|
AWS_SECRET_ACCESS_KEY - AWS secret key for Amazon Bedrock
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|||||||
opencode: "kimi-k2.6",
|
opencode: "kimi-k2.6",
|
||||||
"opencode-go": "kimi-k2.6",
|
"opencode-go": "kimi-k2.6",
|
||||||
"kimi-coding": "kimi-for-coding",
|
"kimi-coding": "kimi-for-coding",
|
||||||
|
"cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ScopedModel {
|
export interface ScopedModel {
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ const API_KEY_LOGIN_PROVIDERS: Record<string, string> = {
|
|||||||
[BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
|
[BEDROCK_PROVIDER_ID]: "Amazon Bedrock",
|
||||||
"azure-openai-responses": "Azure OpenAI Responses",
|
"azure-openai-responses": "Azure OpenAI Responses",
|
||||||
cerebras: "Cerebras",
|
cerebras: "Cerebras",
|
||||||
|
"cloudflare-workers-ai": "Cloudflare Workers AI",
|
||||||
deepseek: "DeepSeek",
|
deepseek: "DeepSeek",
|
||||||
fireworks: "Fireworks",
|
fireworks: "Fireworks",
|
||||||
google: "Google Gemini",
|
google: "Google Gemini",
|
||||||
|
|||||||
Reference in New Issue
Block a user