Merge remote-tracking branch 'origin/main' into add-kimi-deferred-tools

This commit is contained in:
David Brailovsky
2026-07-16 17:30:50 +02:00
55 changed files with 1814 additions and 308 deletions
+5
View File
@@ -2,6 +2,8 @@
## [Unreleased]
## [0.80.8] - 2026-07-16
### Breaking Changes
- Changed runtime authentication to provider-scoped `Models.checkAuth()`, `getAuth()`, `login()`, and `logout()` APIs. `checkAuth()` now returns `AuthCheck | undefined`, and API-key auth resolvers no longer receive a model.
@@ -17,6 +19,8 @@
- Added neutral auth-flow information/link events and provider-owned Amazon Bedrock and Google Vertex AI credential selection flows.
- Added `ModelsStore` with an in-memory default for restoring and persisting dynamic provider catalogs.
- Added the dynamic Radius `pi-messages` gateway provider with OAuth and credential-specific catalog refresh.
- Added `Models.refresh({ force: true })` to let providers bypass freshness checks for explicit refreshes.
- Added xAI device-code OAuth login and routed Grok 4.5 through OpenAI Responses, with low, medium, and high thinking support ([#6651](https://github.com/earendil-works/pi-mono/pull/6651) by [@Jaaneek](https://github.com/Jaaneek)).
### Changed
@@ -26,6 +30,7 @@
- Fixed Cloudflare Workers AI and AI Gateway streams to materialize account and gateway endpoint placeholders after auth resolution, including compat streaming with custom model objects.
- Fixed lazy provider streams to preserve their final assistant message when forwarding an inner stream.
- Fixed OpenAI Codex session IDs longer than 64 characters to meet the API limit ([#6630](https://github.com/earendil-works/pi-mono/issues/6630)).
## [0.80.7] - 2026-07-14
+1 -1
View File
@@ -1047,7 +1047,7 @@ if (result.aborted) console.log('refresh cancelled');
for (const [provider, error] of result.errors) console.error(provider, error);
```
Use `models.refresh({ allowNetwork: false })` to restore persisted catalogs without network access. Model reads stay synchronous and return the last restored or refreshed list.
Use `models.refresh({ allowNetwork: false })` to restore persisted catalogs without network access, or `models.refresh({ force: true })` to bypass provider freshness checks. Model reads stay synchronous and return the last restored or refreshed list.
Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags. `Models.getAuth(model)` includes those model headers, and stream methods merge them before explicit request headers and `transformHeaders`. See [OpenAI Compatibility Settings](#openai-compatibility-settings).
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@earendil-works/pi-ai",
"version": "0.80.7",
"version": "0.80.8",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./dist/index.js",
@@ -34,6 +34,10 @@
"./bedrock-provider": {
"types": "./dist/bedrock-provider.d.ts",
"import": "./dist/bedrock-provider.js"
},
"./bun-oauth": {
"types": "./dist/bun-oauth.d.ts",
"import": "./dist/bun-oauth.js"
}
},
"bin": {
@@ -46,6 +50,7 @@
"scripts": {
"clean": "shx rm -rf dist",
"generate-models": "node scripts/generate-models.ts",
"generate-model-catalog": "node scripts/generate-models.ts --strict --json-only --json-output ../../.artifacts/model-catalog",
"generate-image-models": "node scripts/generate-image-models.ts",
"build": "npm run generate-models && npm run generate-image-models && tsgo -p tsconfig.build.json",
"test": "vitest --run",
+167 -70
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { readdirSync, rmSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import {
CLOUDFLARE_AI_GATEWAY_ANTHROPIC_BASE_URL,
@@ -22,6 +22,40 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageRoot = join(__dirname, "..");
function readGeneratorOptions(args: string[]): {
strict: boolean;
jsonOnly: boolean;
jsonOutputDir: string | undefined;
} {
let strict = false;
let jsonOnly = false;
let jsonOutputDir: string | undefined;
for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--strict") {
strict = true;
continue;
}
if (arg === "--json-only") {
jsonOnly = true;
continue;
}
if (arg === "--json-output") {
const value = args[++index];
if (!value) throw new Error("--json-output requires a directory");
jsonOutputDir = resolve(value);
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
if (jsonOnly && !jsonOutputDir) throw new Error("--json-only requires --json-output");
return { strict, jsonOnly, jsonOutputDir };
}
const generatorOptions = readGeneratorOptions(process.argv.slice(2));
interface ModelsDevModel {
id: string;
name: string;
@@ -191,6 +225,16 @@ const DEEPSEEK_V4_THINKING_LEVEL_MAP = {
max: "max",
} as const;
const KIMI_K3_THINKING_LEVEL_MAP = {
off: null,
minimal: null,
low: null,
medium: null,
high: null,
xhigh: null,
max: "max",
} as const;
const ANT_LING_RING_THINKING_LEVEL_MAP = {
off: null,
minimal: null,
@@ -255,6 +299,14 @@ const OPENAI_RESPONSES_NONE_REASONING_MODELS = new Set([
"gpt-5.6-terra",
"gpt-5.6-luna",
]);
const XAI_RESPONSES_MODEL_ID = "grok-4.5";
const XAI_RESPONSES_EFFORT_LEVEL_MAP = {
off: null,
minimal: null,
} as const;
const XAI_RESPONSES_COMPAT: OpenAIResponsesCompat = {
supportsLongCacheRetention: false,
};
const OPENCODE_OPENAI_COMPLETIONS_LONG_CACHE_RETENTION_UNSUPPORTED_MODELS = new Set([
"opencode:deepseek-v4-flash",
@@ -539,6 +591,9 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
) {
mergeThinkingLevelMap(model, { off: "none" });
}
if (model.provider === "xai" && model.api === "openai-responses" && model.id === XAI_RESPONSES_MODEL_ID) {
mergeThinkingLevelMap(model, XAI_RESPONSES_EFFORT_LEVEL_MAP);
}
if (supportsOpenAiXhigh(model.id)) {
mergeThinkingLevelMap(model, { xhigh: "xhigh" });
}
@@ -678,6 +733,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
try {
console.log("Fetching models from NVIDIA NIM API...");
const response = await fetch(`${NVIDIA_BASE_URL}/models`);
if (!response.ok) throw new Error(`NVIDIA NIM API returned ${response.status}`);
const data = (await response.json()) as { data?: NvidiaNimModelListItem[] };
const modelIds = new Map<string, string>();
@@ -690,6 +746,7 @@ async function fetchNvidiaNimModelIds(): Promise<Map<string, string>> {
return modelIds;
} catch (error) {
console.error("Failed to fetch NVIDIA NIM models:", error);
if (generatorOptions.strict) throw error;
return new Map();
}
}
@@ -698,6 +755,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
try {
console.log("Fetching models from OpenRouter API...");
const response = await fetch("https://openrouter.ai/api/v1/models");
if (!response.ok) throw new Error(`OpenRouter API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -750,6 +808,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to fetch OpenRouter models:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -758,6 +817,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
try {
console.log("Fetching models from Vercel AI Gateway API...");
const response = await fetch(`${AI_GATEWAY_MODELS_URL}/models`);
if (!response.ok) throw new Error(`Vercel AI Gateway API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -808,6 +868,7 @@ async function fetchAiGatewayModels(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to fetch Vercel AI Gateway models:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -816,6 +877,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
try {
console.log("Fetching models from models.dev API...");
const response = await fetch("https://models.dev/api.json");
if (!response.ok) throw new Error(`models.dev API returned ${response.status}`);
const data = await response.json();
const models: Model<any>[] = [];
@@ -1127,13 +1189,15 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, model] of Object.entries(data.xai.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const useResponsesApi = modelId === XAI_RESPONSES_MODEL_ID;
models.push({
id: modelId,
name: m.name || modelId,
api: "openai-completions",
api: useResponsesApi ? "openai-responses" : "openai-completions",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
...(useResponsesApi ? { compat: { ...XAI_RESPONSES_COMPAT } } : {}),
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
@@ -1610,13 +1674,15 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, m] of Object.entries(moonshotModels[key])) {
if (m.tool_call !== true) continue;
const isKimiK3 = modelId === "kimi-k3";
models.push({
id: modelId,
name: m.name || modelId,
api: "openai-completions",
provider,
baseUrl,
reasoning: m.reasoning === true,
reasoning: isKimiK3 || m.reasoning === true,
...(isKimiK3 ? { thinkingLevelMap: KIMI_K3_THINKING_LEVEL_MAP } : {}),
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
@@ -1626,7 +1692,9 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
compat: moonshotCompat,
compat: isKimiK3
? { ...moonshotCompat, requiresReasoningContentOnAssistantMessages: true }
: moonshotCompat,
});
}
}
@@ -1691,6 +1759,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
return models;
} catch (error) {
console.error("Failed to load models.dev data:", error);
if (generatorOptions.strict) throw error;
return [];
}
}
@@ -2224,85 +2293,110 @@ async function generateModels() {
}
}
// Generate TypeScript files: one catalog per provider plus an aggregator
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
const sortedProviderIds = Object.keys(providers).sort();
if (!generatorOptions.jsonOnly) {
// Generate TypeScript files: one catalog per provider plus an aggregator
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
// Do not edit manually - run 'npm run generate-models' to update
`;
const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
const catalogConstName = (providerId: string) =>
`${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
function emitModel(model: Model<any>, indent: string): string {
let output = `${indent}"${model.id}": {\n`;
output += `${indent}\tid: "${model.id}",\n`;
output += `${indent}\tname: "${model.name}",\n`;
output += `${indent}\tapi: "${model.api}",\n`;
output += `${indent}\tprovider: "${model.provider}",\n`;
if (model.baseUrl !== undefined) {
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
function emitModel(model: Model<any>, indent: string): string {
let output = `${indent}"${model.id}": {\n`;
output += `${indent}\tid: "${model.id}",\n`;
output += `${indent}\tname: "${model.name}",\n`;
output += `${indent}\tapi: "${model.api}",\n`;
output += `${indent}\tprovider: "${model.provider}",\n`;
if (model.baseUrl !== undefined) {
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
}
if (model.headers) {
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
}
if (model.compat) {
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
}
output += `${indent}\treasoning: ${model.reasoning},\n`;
if (model.thinkingLevelMap) {
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
}
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
output += `${indent}\tcost: {\n`;
output += `${indent}\t\tinput: ${model.cost.input},\n`;
output += `${indent}\t\toutput: ${model.cost.output},\n`;
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
if (model.cost.tiers) {
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
}
output += `${indent}\t},\n`;
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
output += `${indent}} satisfies Model<"${model.api}">,\n`;
return output;
}
if (model.headers) {
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
}
if (model.compat) {
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
}
output += `${indent}\treasoning: ${model.reasoning},\n`;
if (model.thinkingLevelMap) {
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
}
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
output += `${indent}\tcost: {\n`;
output += `${indent}\t\tinput: ${model.cost.input},\n`;
output += `${indent}\t\toutput: ${model.cost.output},\n`;
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
if (model.cost.tiers) {
output += `${indent}\t\ttiers: ${JSON.stringify(model.cost.tiers)},\n`;
}
output += `${indent}\t},\n`;
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
output += `${indent}} satisfies Model<"${model.api}">,\n`;
return output;
}
const sortedProviderIds = Object.keys(providers).sort();
const providersDir = join(packageRoot, "src/providers");
const providersDir = join(packageRoot, "src/providers");
// Remove stale per-provider catalogs
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts")) {
rmSync(join(providersDir, entry));
// Remove stale per-provider catalogs
for (const entry of readdirSync(providersDir)) {
if (entry.endsWith(".models.ts")) {
rmSync(join(providersDir, entry));
}
}
}
// Per-provider catalogs (sorted for deterministic output)
for (const providerId of sortedProviderIds) {
const models = providers[providerId];
// Per-provider catalogs (sorted for deterministic output)
for (const providerId of sortedProviderIds) {
const models = providers[providerId];
let output = generatedHeader;
output += `import type { Model } from "../types.ts";\n\n`;
output += `export const ${catalogConstName(providerId)} = {\n`;
const sortedModelIds = Object.keys(models).sort();
for (const modelId of sortedModelIds) {
output += emitModel(models[modelId], "\t");
}
output += `} as const;\n`;
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
}
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
// Aggregator
let output = generatedHeader;
output += `import type { Model } from "../types.ts";\n\n`;
output += `export const ${catalogConstName(providerId)} = {\n`;
const sortedModelIds = Object.keys(models).sort();
for (const modelId of sortedModelIds) {
output += emitModel(models[modelId], "\t");
for (const providerId of sortedProviderIds) {
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
}
output += `\nexport const MODELS = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
}
output += `} as const;\n`;
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
console.log("Generated src/models.generated.ts");
}
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
// Aggregator
let output = generatedHeader;
for (const providerId of sortedProviderIds) {
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
if (generatorOptions.jsonOutputDir) {
const jsonProviders: Record<string, Record<string, Model<any>>> = {};
for (const providerId of sortedProviderIds) {
jsonProviders[providerId] = {};
for (const modelId of Object.keys(providers[providerId]).sort()) {
jsonProviders[providerId][modelId] = providers[providerId][modelId];
}
}
const providerOutputDir = join(generatorOptions.jsonOutputDir, "providers");
rmSync(generatorOptions.jsonOutputDir, { recursive: true, force: true });
mkdirSync(providerOutputDir, { recursive: true });
const writeJson = (path: string, value: unknown) => writeFileSync(path, `${JSON.stringify(value)}\n`);
writeJson(join(generatorOptions.jsonOutputDir, "models.json"), jsonProviders);
writeJson(join(generatorOptions.jsonOutputDir, "providers.json"), sortedProviderIds);
for (const providerId of sortedProviderIds) {
writeJson(join(providerOutputDir, `${providerId}.json`), jsonProviders[providerId]);
}
console.log(`Generated JSON model catalog under ${generatorOptions.jsonOutputDir}`);
}
output += `\nexport const MODELS = {\n`;
for (const providerId of sortedProviderIds) {
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
}
output += `} as const;\n`;
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
console.log("Generated src/models.generated.ts");
// Print statistics
const totalModels = allModels.length;
@@ -2318,4 +2412,7 @@ async function generateModels() {
}
// Run the generator
generateModels().catch(console.error);
generateModels().catch((error) => {
console.error(error);
process.exitCode = 1;
});
+1
View File
@@ -282,6 +282,7 @@ function buildParams(model: Model<"openai-responses">, context: Context, options
effort: (model.thinkingLevelMap?.off ?? "none") as NonNullable<typeof params.reasoning>["effort"],
};
}
if (model.provider === "xai") params.include = ["reasoning.encrypted_content"];
}
return params;
+36 -8
View File
@@ -11,18 +11,46 @@ const importOAuthModule = (specifier: string): Promise<unknown> => {
return import(runtimeSpecifier);
};
export const loadAnthropicOAuth = async (): Promise<OAuthAuth> =>
((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth;
type OAuthFlowLoaders = {
anthropic: () => OAuthAuth | Promise<OAuthAuth>;
openaiCodex: () => OAuthAuth | Promise<OAuthAuth>;
githubCopilot: () => OAuthAuth | Promise<OAuthAuth>;
xai: () => OAuthAuth | Promise<OAuthAuth>;
radius: (options: { name: string; gateway: string }) => OAuthAuth | Promise<OAuthAuth>;
};
export const loadOpenAICodexOAuth = async (): Promise<OAuthAuth> =>
((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth;
let bundledLoaders: OAuthFlowLoaders | undefined;
export const loadGitHubCopilotOAuth = async (): Promise<OAuthAuth> =>
((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth;
/** Registers statically bundled OAuth flows for standalone Bun binaries. */
export function registerBundledOAuthFlowLoaders(loaders: OAuthFlowLoaders): void {
bundledLoaders = loaders;
}
export const loadRadiusOAuth = async (options: { name: string; gateway: string }): Promise<OAuthAuth> =>
(
export const loadAnthropicOAuth = async (): Promise<OAuthAuth> => {
if (bundledLoaders) return bundledLoaders.anthropic();
return ((await importOAuthModule("./anthropic.ts")) as { anthropicOAuth: OAuthAuth }).anthropicOAuth;
};
export const loadOpenAICodexOAuth = async (): Promise<OAuthAuth> => {
if (bundledLoaders) return bundledLoaders.openaiCodex();
return ((await importOAuthModule("./openai-codex.ts")) as { openaiCodexOAuth: OAuthAuth }).openaiCodexOAuth;
};
export const loadGitHubCopilotOAuth = async (): Promise<OAuthAuth> => {
if (bundledLoaders) return bundledLoaders.githubCopilot();
return ((await importOAuthModule("./github-copilot.ts")) as { githubCopilotOAuth: OAuthAuth }).githubCopilotOAuth;
};
export const loadXaiOAuth = async (): Promise<OAuthAuth> => {
if (bundledLoaders) return bundledLoaders.xai();
return ((await importOAuthModule("./xai.ts")) as { xaiOAuth: OAuthAuth }).xaiOAuth;
};
export const loadRadiusOAuth = async (options: { name: string; gateway: string }): Promise<OAuthAuth> => {
if (bundledLoaders) return bundledLoaders.radius(options);
return (
(await importOAuthModule("./radius.ts")) as {
createRadiusOAuth: (input: { name: string; gateway: string }) => OAuthAuth;
}
).createRadiusOAuth(options);
};
+231
View File
@@ -0,0 +1,231 @@
/**
* xAI OAuth device-code flow.
*/
import type { AuthInteraction, OAuthAuth, OAuthCredential } from "../types.ts";
import { pollOAuthDeviceCodeFlow } from "./device-code.ts";
const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
const XAI_SCOPE = "openid profile email offline_access grok-cli:access api:access";
const XAI_DEVICE_CODE_URL = "https://auth.x.ai/oauth2/device/code";
const XAI_TOKEN_URL = "https://auth.x.ai/oauth2/token";
// Refresh slightly before the reported expiry to avoid using a token that dies mid-request.
const REFRESH_SKEW_MS = 5 * 60 * 1000;
const DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
type JsonObject = Record<string, unknown>;
type OAuthHttpResponse = {
ok: boolean;
status: number;
body: JsonObject;
};
type XaiDeviceCode = {
deviceCode: string;
userCode: string;
verificationUri: string;
intervalSeconds?: number;
expiresInSeconds: number;
};
function requiredString(body: JsonObject, field: string): string {
const value = body[field];
if (typeof value !== "string" || value.length === 0) {
throw new Error(`Invalid xAI OAuth response field: ${field}`);
}
return value;
}
function positiveNumber(body: JsonObject, field: string): number {
const value = body[field];
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
throw new Error(`Invalid xAI OAuth response field: ${field}`);
}
return value;
}
// The verification URI is opened in the user's browser; force it to be an https URL
// so a malicious response cannot make `open` launch something else.
function validateVerificationUri(raw: string): string {
let url: URL;
try {
url = new URL(raw);
} catch {
throw new Error("Untrusted verification URI in xAI OAuth response");
}
if (url.protocol !== "https:") {
throw new Error("Untrusted verification URI in xAI OAuth response");
}
return url.href;
}
async function postForm(url: string, fields: Record<string, string>, signal?: AbortSignal): Promise<OAuthHttpResponse> {
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams(fields),
signal,
});
} catch (error) {
if (signal?.aborted) {
throw new Error("Login cancelled");
}
throw error;
}
let body: JsonObject;
try {
const parsed = (await response.json()) as unknown;
body = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as JsonObject) : {};
} catch {
if (signal?.aborted) {
throw new Error("Login cancelled");
}
throw new Error(`xAI OAuth returned invalid JSON (HTTP ${response.status})`);
}
return {
ok: response.ok,
status: response.status,
body,
};
}
function requestFailure(action: string, response: OAuthHttpResponse): Error {
const error = typeof response.body.error === "string" ? response.body.error : undefined;
const description =
typeof response.body.error_description === "string" ? response.body.error_description : undefined;
const detail = [error, description].filter(Boolean).join(": ");
return new Error(`xAI OAuth ${action} failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`);
}
function parseDeviceCode(body: JsonObject): XaiDeviceCode {
// RFC 8628 allows interval 0 (no minimum wait); fall back to the poller's
// default instead of failing on non-positive or malformed values.
const interval = body.interval;
const intervalSeconds =
typeof interval === "number" && Number.isFinite(interval) && interval > 0 ? interval : undefined;
return {
deviceCode: requiredString(body, "device_code"),
userCode: requiredString(body, "user_code"),
verificationUri: validateVerificationUri(requiredString(body, "verification_uri")),
intervalSeconds,
expiresInSeconds: positiveNumber(body, "expires_in"),
};
}
function credentialsFromTokenResponse(body: JsonObject, previousRefreshToken?: string): OAuthCredential {
const access = requiredString(body, "access_token");
// xAI may omit refresh_token on refresh when the token is not rotated.
const refresh =
body.refresh_token === undefined && previousRefreshToken
? previousRefreshToken
: requiredString(body, "refresh_token");
const expiresInSeconds =
body.expires_in === undefined ? DEFAULT_TOKEN_LIFETIME_SECONDS : positiveNumber(body, "expires_in");
return {
type: "oauth",
access,
refresh,
expires: Date.now() + expiresInSeconds * 1000 - REFRESH_SKEW_MS,
};
}
async function requestDeviceCode(signal?: AbortSignal): Promise<XaiDeviceCode> {
const response = await postForm(
XAI_DEVICE_CODE_URL,
{
client_id: XAI_CLIENT_ID,
scope: XAI_SCOPE,
referrer: "pi",
},
signal,
);
if (!response.ok) {
throw requestFailure("device authorization", response);
}
return parseDeviceCode(response.body);
}
async function pollForTokens(device: XaiDeviceCode, signal?: AbortSignal): Promise<OAuthCredential> {
return pollOAuthDeviceCodeFlow<OAuthCredential>({
intervalSeconds: device.intervalSeconds,
expiresInSeconds: device.expiresInSeconds,
waitBeforeFirstPoll: true,
signal,
poll: async () => {
const response = await postForm(
XAI_TOKEN_URL,
{
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
client_id: XAI_CLIENT_ID,
device_code: device.deviceCode,
},
signal,
);
if (response.ok) {
return { status: "complete", value: credentialsFromTokenResponse(response.body) };
}
const error = response.body.error;
if (error === "authorization_pending") {
return { status: "pending" };
}
if (error === "slow_down") {
const interval = response.body.interval;
return { status: "slow_down", intervalSeconds: typeof interval === "number" ? interval : undefined };
}
if (error === "access_denied" || error === "authorization_denied") {
return { status: "failed", message: "xAI device authorization was denied" };
}
if (error === "expired_token") {
return { status: "failed", message: "xAI device code expired" };
}
return { status: "failed", message: requestFailure("device token polling", response).message };
},
});
}
async function loginXai(interaction: AuthInteraction): Promise<OAuthCredential> {
const device = await requestDeviceCode(interaction.signal);
interaction.notify({
type: "device_code",
userCode: device.userCode,
verificationUri: device.verificationUri,
intervalSeconds: device.intervalSeconds,
expiresInSeconds: device.expiresInSeconds,
});
return pollForTokens(device, interaction.signal);
}
async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredential> {
const response = await postForm(
XAI_TOKEN_URL,
{
grant_type: "refresh_token",
client_id: XAI_CLIENT_ID,
refresh_token: refreshToken,
},
signal,
);
if (!response.ok) {
throw requestFailure("token refresh", response);
}
return credentialsFromTokenResponse(response.body, refreshToken);
}
export const xaiOAuth: OAuthAuth = {
name: "xAI (Grok/X subscription)",
login: loginXai,
refresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),
async toAuth(credential) {
return { apiKey: credential.access };
},
};
+17
View File
@@ -0,0 +1,17 @@
import { anthropicOAuth } from "./auth/oauth/anthropic.ts";
import { githubCopilotOAuth } from "./auth/oauth/github-copilot.ts";
import { registerBundledOAuthFlowLoaders } from "./auth/oauth/load.ts";
import { openaiCodexOAuth } from "./auth/oauth/openai-codex.ts";
import { createRadiusOAuth } from "./auth/oauth/radius.ts";
import { xaiOAuth } from "./auth/oauth/xai.ts";
/** Register OAuth flows statically embedded in the standalone Bun binary. */
export function registerBunOAuthFlows(): void {
registerBundledOAuthFlowLoaders({
anthropic: () => anthropicOAuth,
openaiCodex: () => openaiCodexOAuth,
githubCopilot: () => githubCopilotOAuth,
xai: () => xaiOAuth,
radius: createRadiusOAuth,
});
}
+11 -1
View File
@@ -38,11 +38,15 @@ export interface RefreshModelsContext {
store: ProviderModelsStore;
/** False during offline/cache-only initialization. */
allowNetwork: boolean;
/** Bypass provider freshness checks and fetch immediately when network access is allowed. */
force?: boolean;
signal?: AbortSignal;
}
export interface ModelsRefreshOptions {
allowNetwork?: boolean;
/** Bypass provider freshness checks and fetch immediately when network access is allowed. */
force?: boolean;
signal?: AbortSignal;
}
@@ -290,7 +294,13 @@ class ModelsImpl implements MutableModels {
stored = await this.readCredential(provider.id);
const credential = await this.resolveRefreshCredential(provider, stored, allowNetwork, options.signal);
if (!credential) return;
await provider.refreshModels({ credential, store, allowNetwork, signal: options.signal });
await provider.refreshModels({
credential,
store,
allowNetwork,
force: options.force,
signal: options.signal,
});
} catch (error) {
if (!options.signal?.aborted) {
errors.set(
@@ -22,6 +22,24 @@ export const KIMI_CODING_MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
"k3": {
id: "k3",
name: "Kimi K3",
api: "anthropic-messages",
provider: "kimi-coding",
baseUrl: "https://api.kimi.com/coding",
headers: {"User-Agent":"KimiCLI/1.5"},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"anthropic-messages">,
"kimi-for-coding": {
id: "kimi-for-coding",
name: "Kimi For Coding",
@@ -40,6 +58,24 @@ export const KIMI_CODING_MODELS = {
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
"kimi-for-coding-highspeed": {
id: "kimi-for-coding-highspeed",
name: "Kimi For Coding HighSpeed",
api: "anthropic-messages",
provider: "kimi-coding",
baseUrl: "https://api.kimi.com/coding",
headers: {"User-Agent":"KimiCLI/1.5"},
reasoning: true,
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
"kimi-k2-thinking": {
id: "kimi-k2-thinking",
name: "Kimi K2 Thinking",
@@ -168,4 +168,23 @@ export const MOONSHOTAI_CN_MODELS = {
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"kimi-k3": {
id: "kimi-k3",
name: "Kimi K3",
api: "openai-completions",
provider: "moonshotai-cn",
baseUrl: "https://api.moonshot.cn/v1",
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek","requiresReasoningContentOnAssistantMessages":true},
reasoning: true,
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":null,"xhigh":null,"max":"max"},
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
} as const;
@@ -168,4 +168,23 @@ export const MOONSHOTAI_MODELS = {
contextWindow: 262144,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"kimi-k3": {
id: "kimi-k3",
name: "Kimi K3",
api: "openai-completions",
provider: "moonshotai",
baseUrl: "https://api.moonshot.ai/v1",
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","supportsStrictMode":false,"thinkingFormat":"deepseek","requiresReasoningContentOnAssistantMessages":true},
reasoning: true,
thinkingLevelMap: {"off":null,"minimal":null,"low":null,"medium":null,"high":null,"xhigh":null,"max":"max"},
input: ["text", "image"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
} as const;
+153 -135
View File
@@ -385,7 +385,7 @@ export const OPENROUTER_MODELS = {
cacheRead: 0.3,
cacheWrite: 3.75,
},
contextWindow: 1000000,
contextWindow: 200000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"anthropic/claude-sonnet-4.5": {
@@ -652,13 +652,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.24,
output: 0.9,
input: 0.27,
output: 1.12,
cacheRead: 0.135,
cacheWrite: 0,
},
contextWindow: 163840,
maxTokens: 16384,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-chat-v3.1": {
id: "deepseek/deepseek-chat-v3.1",
@@ -725,11 +725,11 @@ export const OPENROUTER_MODELS = {
input: ["text"],
cost: {
input: 0.27,
output: 0.95,
cacheRead: 0.13,
output: 1,
cacheRead: 0.135,
cacheWrite: 0,
},
contextWindow: 163840,
contextWindow: 131072,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.2": {
@@ -742,13 +742,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.2145,
output: 0.32175,
cacheRead: 0.02145,
input: 0.269,
output: 0.4,
cacheRead: 0.1345,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 64000,
contextWindow: 163840,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v3.2-exp": {
id: "deepseek/deepseek-v3.2-exp",
@@ -779,13 +779,13 @@ export const OPENROUTER_MODELS = {
thinkingLevelMap: {"minimal":null,"low":null,"medium":null,"high":"high","max":null,"xhigh":"xhigh"},
input: ["text"],
cost: {
input: 0.09,
output: 0.18,
cacheRead: 0.018,
input: 0.098,
output: 0.196,
cacheRead: 0.02,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 65536,
contextWindow: 1048575,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"deepseek/deepseek-v4-pro": {
id: "deepseek/deepseek-v4-pro",
@@ -1051,12 +1051,12 @@ export const OPENROUTER_MODELS = {
input: ["text", "image"],
cost: {
input: 0.08,
output: 0.16,
cacheRead: 0,
output: 0.45,
cacheRead: 0.04,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"google/gemma-4-26b-a4b-it": {
id: "google/gemma-4-26b-a4b-it",
@@ -1068,13 +1068,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.06,
output: 0.33,
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"openai-completions">,
"google/gemma-4-26b-a4b-it:free": {
id: "google/gemma-4-26b-a4b-it:free",
@@ -1104,13 +1104,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.06,
output: 0.35,
cacheRead: 0,
input: 0.22,
output: 0.55,
cacheRead: 0.12,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 8192,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"google/gemma-4-31b-it:free": {
id: "google/gemma-4-31b-it:free",
@@ -1303,13 +1303,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
cacheRead: 0,
input: 0.05,
output: 0.08,
cacheRead: 0.025,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.3-70b-instruct": {
id: "meta-llama/llama-3.3-70b-instruct",
@@ -1321,13 +1321,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.1,
output: 0.32,
input: 0.13,
output: 0.4,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.3-70b-instruct:free": {
id: "meta-llama/llama-3.3-70b-instruct:free",
@@ -1383,6 +1383,24 @@ export const OPENROUTER_MODELS = {
contextWindow: 327680,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta/muse-spark-1.1": {
id: "meta/muse-spark-1.1",
name: "Meta: Muse Spark 1.1",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"openrouter"},
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.25,
output: 4.25,
cacheRead: 0.15,
cacheWrite: 0,
},
contextWindow: 1048576,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"minimax/minimax-m1": {
id: "minimax/minimax-m1",
name: "MiniMax: MiniMax M1",
@@ -1393,7 +1411,7 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.4,
input: 0.55,
output: 2.2,
cacheRead: 0,
cacheWrite: 0,
@@ -1465,13 +1483,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.24,
output: 0.96,
cacheRead: 0,
input: 0.3,
output: 1.2,
cacheRead: 0.06,
cacheWrite: 0,
},
contextWindow: 196608,
maxTokens: 196608,
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"minimax/minimax-m3": {
id: "minimax/minimax-m3",
@@ -1488,8 +1506,8 @@ export const OPENROUTER_MODELS = {
cacheRead: 0.06,
cacheWrite: 0,
},
contextWindow: 1000000,
maxTokens: 131072,
contextWindow: 524288,
maxTokens: 512000,
} satisfies Model<"openai-completions">,
"mistralai/codestral-2508": {
id: "mistralai/codestral-2508",
@@ -1700,12 +1718,12 @@ export const OPENROUTER_MODELS = {
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mistral-saba": {
id: "mistralai/mistral-saba",
@@ -1753,13 +1771,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.075,
output: 0.2,
cacheRead: 0,
input: 0.1,
output: 0.3,
cacheRead: 0.01,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x22b-instruct": {
id: "mistralai/mixtral-8x22b-instruct",
@@ -1845,11 +1863,11 @@ export const OPENROUTER_MODELS = {
cost: {
input: 0.6,
output: 2.5,
cacheRead: 0.15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 100352,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"moonshotai/kimi-k2.5": {
id: "moonshotai/kimi-k2.5",
@@ -1879,13 +1897,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.66,
output: 3.41,
cacheRead: 0.15,
input: 0.95,
output: 4,
cacheRead: 0.16,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"moonshotai/kimi-k2.7-code": {
id: "moonshotai/kimi-k2.7-code",
@@ -1897,9 +1915,9 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.719,
output: 3.49,
cacheRead: 0.149,
input: 0.75,
output: 3.5,
cacheRead: 0.16,
cacheWrite: 0,
},
contextWindow: 262144,
@@ -2023,12 +2041,12 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.08,
output: 0.45,
cacheRead: 0,
input: 0.21,
output: 0.455,
cacheRead: 0.06,
cacheWrite: 0,
},
contextWindow: 262144,
contextWindow: 1000000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nvidia/nemotron-3-super-120b-a12b:free": {
@@ -2059,13 +2077,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.5,
output: 2.2,
cacheRead: 0.1,
input: 0.6,
output: 3.6,
cacheRead: 0.2,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 16384,
contextWindow: 512288,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nvidia/nemotron-3-ultra-550b-a55b:free": {
id: "nvidia/nemotron-3-ultra-550b-a55b:free",
@@ -2245,7 +2263,7 @@ export const OPENROUTER_MODELS = {
cacheWrite: 0,
},
contextWindow: 1047576,
maxTokens: 4096,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"openai/gpt-4.1-mini": {
id: "openai/gpt-4.1-mini",
@@ -2295,7 +2313,7 @@ export const OPENROUTER_MODELS = {
cost: {
input: 2.5,
output: 10,
cacheRead: 0,
cacheRead: 1.25,
cacheWrite: 0,
},
contextWindow: 128000,
@@ -2511,11 +2529,11 @@ export const OPENROUTER_MODELS = {
cost: {
input: 1.25,
output: 10,
cacheRead: 0.13,
cacheRead: 0.125,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"openai/gpt-5.1-codex": {
id: "openai/gpt-5.1-codex",
@@ -2529,7 +2547,7 @@ export const OPENROUTER_MODELS = {
cost: {
input: 1.25,
output: 10,
cacheRead: 0.13,
cacheRead: 0.125,
cacheWrite: 0,
},
contextWindow: 400000,
@@ -2977,8 +2995,8 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.03,
output: 0.15,
input: 0.037,
output: 0.17,
cacheRead: 0,
cacheWrite: 0,
},
@@ -2995,13 +3013,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.029,
output: 0.14,
cacheRead: 0,
input: 0.03,
output: 0.13,
cacheRead: 0.03,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"openai/gpt-oss-20b:free": {
id: "openai/gpt-oss-20b:free",
@@ -3517,13 +3535,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.04815,
output: 0.19305,
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 32000,
contextWindow: 262144,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"qwen/qwen3-30b-a3b-thinking-2507": {
id: "qwen/qwen3-30b-a3b-thinking-2507",
@@ -3589,9 +3607,9 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.22,
output: 1.8,
cacheRead: 0,
input: 0.3,
output: 1,
cacheRead: 0.1,
cacheWrite: 0,
},
contextWindow: 262144,
@@ -3733,13 +3751,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text"],
cost: {
input: 0.09,
input: 0.1,
output: 1.1,
cacheRead: 0,
cacheRead: 0.07,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 16384,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3-next-80b-a3b-instruct:free": {
id: "qwen/qwen3-next-80b-a3b-instruct:free",
@@ -3787,13 +3805,13 @@ export const OPENROUTER_MODELS = {
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.2,
output: 0.88,
cacheRead: 0.11,
input: 0.21,
output: 1.9,
cacheRead: 0.1,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 16384,
contextWindow: 131072,
maxTokens: 32768,
} satisfies Model<"openai-completions">,
"qwen/qwen3-vl-235b-a22b-thinking": {
id: "qwen/qwen3-vl-235b-a22b-thinking",
@@ -3919,7 +3937,7 @@ export const OPENROUTER_MODELS = {
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-27b": {
id: "qwen/qwen3.5-27b",
@@ -3951,11 +3969,11 @@ export const OPENROUTER_MODELS = {
cost: {
input: 0.14,
output: 1,
cacheRead: 0.05,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 81920,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-397b-a17b": {
id: "qwen/qwen3.5-397b-a17b",
@@ -3967,13 +3985,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.385,
output: 2.45,
cacheRead: 0.111,
input: 0.45,
output: 3,
cacheRead: 0.225,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-9b": {
id: "qwen/qwen3.5-9b",
@@ -4057,13 +4075,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.289,
output: 2.4,
input: 0.45,
output: 2.7,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 131072,
contextWindow: 262144,
maxTokens: 65536,
} satisfies Model<"openai-completions">,
"qwen/qwen3.6-35b-a3b": {
id: "qwen/qwen3.6-35b-a3b",
@@ -4147,10 +4165,10 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 1.25,
output: 3.75,
cacheRead: 0.25,
cacheWrite: 1.5625,
input: 1.475,
output: 4.425,
cacheRead: 0.295,
cacheWrite: 1.84375,
},
contextWindow: 1000000,
maxTokens: 65536,
@@ -4291,13 +4309,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.14,
output: 0.58,
cacheRead: 0.035,
input: 0.2,
output: 0.8,
cacheRead: 0.05,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"tencent/hy3-preview": {
id: "tencent/hy3-preview",
@@ -4453,13 +4471,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.105,
input: 0.14,
output: 0.28,
cacheRead: 0.028,
cacheRead: 0.0028,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"xiaomi/mimo-v2.5-pro": {
id: "xiaomi/mimo-v2.5-pro",
@@ -4543,13 +4561,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.43,
output: 1.75,
cacheRead: 0.08,
input: 0.5,
output: 2,
cacheRead: 0.1,
cacheWrite: 0,
},
contextWindow: 198000,
maxTokens: 16384,
contextWindow: 202752,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"z-ai/glm-4.6v": {
id: "z-ai/glm-4.6v",
@@ -4597,13 +4615,13 @@ export const OPENROUTER_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.06,
input: 0.0605,
output: 0.4,
cacheRead: 0.01,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 16384,
contextWindow: 131072,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"z-ai/glm-5": {
id: "z-ai/glm-5",
@@ -4620,8 +4638,8 @@ export const OPENROUTER_MODELS = {
cacheRead: 0.119,
cacheWrite: 0,
},
contextWindow: 198000,
maxTokens: 128000,
contextWindow: 202752,
maxTokens: 202752,
} satisfies Model<"openai-completions">,
"z-ai/glm-5-turbo": {
id: "z-ai/glm-5-turbo",
@@ -4638,7 +4656,7 @@ export const OPENROUTER_MODELS = {
cacheRead: 0.24,
cacheWrite: 0,
},
contextWindow: 262144,
contextWindow: 202752,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"z-ai/glm-5.1": {
@@ -4670,13 +4688,13 @@ export const OPENROUTER_MODELS = {
thinkingLevelMap: {"xhigh":"xhigh"},
input: ["text"],
cost: {
input: 0.924,
output: 2.904,
cacheRead: 0.1716,
input: 0.9366,
output: 2.9436,
cacheRead: 0.17394,
cacheWrite: 0,
},
contextWindow: 1024000,
maxTokens: 128000,
contextWindow: 1048576,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"z-ai/glm-5v-turbo": {
id: "z-ai/glm-5v-turbo",
@@ -622,6 +622,25 @@ export const VERCEL_AI_GATEWAY_MODELS = {
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"anthropic/claude-opus-4.7-fast": {
id: "anthropic/claude-opus-4.7-fast",
name: "Claude Opus 4.7 (Fast)",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
compat: {"forceAdaptiveThinking":true,"supportsTemperature":false},
reasoning: true,
thinkingLevelMap: {"xhigh":"xhigh","max":"max"},
input: ["text", "image"],
cost: {
input: 30,
output: 150,
cacheRead: 3,
cacheWrite: 37.5,
},
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"anthropic/claude-opus-4.8": {
id: "anthropic/claude-opus-4.8",
name: "Claude Opus 4.8",
@@ -641,6 +660,25 @@ export const VERCEL_AI_GATEWAY_MODELS = {
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"anthropic/claude-opus-4.8-fast": {
id: "anthropic/claude-opus-4.8-fast",
name: "Claude Opus 4.8 (Fast)",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
compat: {"forceAdaptiveThinking":true,"supportsTemperature":false},
reasoning: true,
thinkingLevelMap: {"xhigh":"xhigh","max":"max"},
input: ["text", "image"],
cost: {
input: 10,
output: 50,
cacheRead: 1,
cacheWrite: 12.5,
},
contextWindow: 1000000,
maxTokens: 128000,
} satisfies Model<"anthropic-messages">,
"anthropic/claude-sonnet-4": {
id: "anthropic/claude-sonnet-4",
name: "Claude Sonnet 4",
@@ -841,8 +879,8 @@ export const VERCEL_AI_GATEWAY_MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.21,
output: 0.79,
input: 0.25,
output: 0.95,
cacheRead: 0.13,
cacheWrite: 0,
},
@@ -2717,6 +2755,23 @@ export const VERCEL_AI_GATEWAY_MODELS = {
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"anthropic-messages">,
"thinkingmachines/inkling": {
id: "thinkingmachines/inkling",
name: "Inkling",
api: "anthropic-messages",
provider: "vercel-ai-gateway",
baseUrl: "https://ai-gateway.vercel.sh",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1,
output: 4.05,
cacheRead: 0.17,
cacheWrite: 0,
},
contextWindow: 256000,
maxTokens: 256000,
} satisfies Model<"anthropic-messages">,
"xai/grok-4.1-fast-non-reasoning": {
id: "xai/grok-4.1-fast-non-reasoning",
name: "Grok 4.1 Fast Non-Reasoning",
+4 -3
View File
@@ -97,11 +97,12 @@ export const XAI_MODELS = {
"grok-4.5": {
id: "grok-4.5",
name: "Grok 4.5",
api: "openai-completions",
api: "openai-responses",
provider: "xai",
baseUrl: "https://api.x.ai/v1",
compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false},
compat: {"supportsLongCacheRetention":false},
reasoning: true,
thinkingLevelMap: {"off":null,"minimal":null},
input: ["text", "image"],
cost: {
input: 2,
@@ -111,7 +112,7 @@ export const XAI_MODELS = {
},
contextWindow: 500000,
maxTokens: 500000,
} satisfies Model<"openai-completions">,
} satisfies Model<"openai-responses">,
"grok-build-0.1": {
id: "grok-build-0.1",
name: "Grok Build 0.1",
+12 -4
View File
@@ -1,15 +1,23 @@
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
import { envApiKeyAuth } from "../auth/helpers.ts";
import { openAIResponsesApi } from "../api/openai-responses.lazy.ts";
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
import { loadXaiOAuth } from "../auth/oauth/load.ts";
import { createProvider, type Provider } from "../models.ts";
import { XAI_MODELS } from "./xai.models.ts";
export function xaiProvider(): Provider<"openai-completions"> {
export function xaiProvider(): Provider<"openai-completions" | "openai-responses"> {
return createProvider({
id: "xai",
name: "xAI",
baseUrl: "https://api.x.ai/v1",
auth: { apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]) },
auth: {
apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]),
oauth: lazyOAuth({ name: "xAI (Grok/X subscription)", load: loadXaiOAuth }),
},
models: Object.values(XAI_MODELS),
api: openAICompletionsApi(),
api: {
"openai-completions": openAICompletionsApi(),
"openai-responses": openAIResponsesApi(),
},
});
}
+5 -2
View File
@@ -283,8 +283,9 @@ describe("Models runtime", () => {
expect(offline.getModel("dynamic", "fetched")).toBeDefined();
});
it("passes effective API-key credentials and skips unconfigured providers", async () => {
it("passes effective API-key credentials and refresh options while skipping unconfigured providers", async () => {
let effectiveCredential: unknown;
let forceRefresh: boolean | undefined;
let unconfiguredRefreshes = 0;
const models = createModels();
models.setProvider(
@@ -293,6 +294,7 @@ describe("Models runtime", () => {
auth: { apiKey: envKeyAuth("ambient-key") },
refreshModels: async (context) => {
effectiveCredential = context.credential;
forceRefresh = context.force;
},
}),
);
@@ -306,8 +308,9 @@ describe("Models runtime", () => {
}),
);
await models.refresh();
await models.refresh({ force: true });
expect(effectiveCredential).toEqual({ type: "api_key", key: "ambient-key", env: undefined });
expect(forceRefresh).toBe(true);
expect(unconfiguredRefreshes).toBe(0);
});
+6
View File
@@ -3,6 +3,7 @@ import { InMemoryCredentialStore } from "../src/auth/credential-store.ts";
import { anthropicOAuth } from "../src/auth/oauth/anthropic.ts";
import { githubCopilotOAuth } from "../src/auth/oauth/github-copilot.ts";
import { openaiCodexOAuth } from "../src/auth/oauth/openai-codex.ts";
import { xaiOAuth } from "../src/auth/oauth/xai.ts";
import { createModels } from "../src/models.ts";
import * as extensionOAuthCompatibility from "../src/oauth.ts";
import { anthropicProvider } from "../src/providers/anthropic.ts";
@@ -32,6 +33,11 @@ describe.sequential("OAuthAuth adapters", () => {
expect(auth).toEqual({ apiKey: "token" });
});
it("xAI toAuth derives the api key from the access token", async () => {
const auth = await xaiOAuth.toAuth({ type: "oauth", access: "token", refresh: "r", expires: 0 });
expect(auth).toEqual({ apiKey: "token" });
});
it("github-copilot toAuth derives baseUrl from the token proxy endpoint", async () => {
const access = "tid=abc;exp=123;proxy-ep=proxy.enterprise.example;rest";
const auth = await githubCopilotOAuth.toAuth({ type: "oauth", access, refresh: "r", expires: 0 });
+285
View File
@@ -0,0 +1,285 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { xaiOAuth } from "../src/auth/oauth/xai.ts";
import type { OAuthCredential } from "../src/auth/types.ts";
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
function requestUrl(input: unknown): string {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (input instanceof Request) return input.url;
throw new Error(`Unsupported request input: ${String(input)}`);
}
function requestForm(init: RequestInit | undefined): URLSearchParams {
return new URLSearchParams(String(init?.body));
}
function deviceCodeResponse(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
device_code: "device-code",
user_code: "ABCD-1234",
verification_uri: "https://accounts.x.ai/oauth2/device",
expires_in: 900,
interval: 5,
...overrides,
};
}
function tokenResponse(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
access_token: "access-token",
refresh_token: "refresh-token",
expires_in: 21_600,
token_type: "Bearer",
...overrides,
};
}
type DeviceCodeInfo = {
userCode: string;
verificationUri: string;
intervalSeconds?: number;
expiresInSeconds?: number;
};
function loginXaiForTest(options: {
onDeviceCode: (info: DeviceCodeInfo) => void;
signal?: AbortSignal;
}): Promise<OAuthCredential> {
return xaiOAuth.login({
signal: options.signal,
prompt: () => {
throw new Error("Unexpected prompt");
},
notify: (event) => {
if (event.type === "device_code") {
const { type: _, ...info } = event;
options.onDeviceCode(info);
}
},
});
}
function refreshXaiForTest(refreshToken: string): Promise<OAuthCredential> {
return xaiOAuth.refresh({ type: "oauth", access: "old-access", refresh: refreshToken, expires: 0 });
}
describe("xAI OAuth device flow", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
vi.useRealTimers();
});
it("uses the device grant, delays polling, and handles pending and slow_down", async () => {
vi.useFakeTimers();
const startTime = new Date("2026-07-09T20:00:00Z");
vi.setSystemTime(startTime);
const pollTimes: number[] = [];
const tokenReplies = [
jsonResponse({ error: "authorization_pending" }, 400),
jsonResponse({ error: "slow_down", interval: 10 }, 400),
jsonResponse(tokenResponse()),
];
const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
const url = requestUrl(input);
if (url === "https://auth.x.ai/oauth2/device/code") {
const form = requestForm(init);
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
expect(form.get("scope")).toBe("openid profile email offline_access grok-cli:access api:access");
expect(form.get("referrer")).toBe("pi");
return jsonResponse(deviceCodeResponse());
}
if (url === "https://auth.x.ai/oauth2/token") {
pollTimes.push(Date.now());
const form = requestForm(init);
expect(form.get("grant_type")).toBe("urn:ietf:params:oauth:grant-type:device_code");
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
expect(form.get("device_code")).toBe("device-code");
const reply = tokenReplies.shift();
if (!reply) throw new Error("Unexpected token poll");
return reply;
}
throw new Error(`Unexpected request: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const deviceCodes: DeviceCodeInfo[] = [];
const loginPromise = loginXaiForTest({ onDeviceCode: (info) => deviceCodes.push(info) });
await vi.advanceTimersByTimeAsync(0);
expect(deviceCodes).toEqual([
{
userCode: "ABCD-1234",
verificationUri: "https://accounts.x.ai/oauth2/device",
intervalSeconds: 5,
expiresInSeconds: 900,
},
]);
expect(pollTimes).toEqual([]);
await vi.advanceTimersByTimeAsync(5000);
expect(pollTimes).toEqual([startTime.getTime() + 5000]);
// slow_down raised the interval to 10 seconds
await vi.advanceTimersByTimeAsync(5000);
expect(pollTimes).toEqual([startTime.getTime() + 5000, startTime.getTime() + 10_000]);
await vi.advanceTimersByTimeAsync(10_000);
const credentials = await loginPromise;
expect(pollTimes).toEqual([
startTime.getTime() + 5000,
startTime.getTime() + 10_000,
startTime.getTime() + 20_000,
]);
expect(credentials).toEqual({
type: "oauth",
access: "access-token",
refresh: "refresh-token",
expires: startTime.getTime() + 20_000 + 21_600_000 - 300_000,
});
});
it("falls back to the default poll interval when the response reports interval 0", async () => {
vi.useFakeTimers();
const startTime = new Date("2026-07-09T20:00:00Z");
vi.setSystemTime(startTime);
const pollTimes: number[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (input: unknown) => {
if (requestUrl(input) === "https://auth.x.ai/oauth2/device/code") {
return jsonResponse(deviceCodeResponse({ interval: 0 }));
}
pollTimes.push(Date.now());
return jsonResponse(tokenResponse());
}),
);
const loginPromise = loginXaiForTest({ onDeviceCode: () => {} });
// RFC 8628 default interval is 5 seconds when the server does not require a wait.
await vi.advanceTimersByTimeAsync(5000);
await loginPromise;
expect(pollTimes).toEqual([startTime.getTime() + 5000]);
});
it.each(["http://accounts.x.ai/oauth2/device", "file:///etc/passwd", "not a url"])(
"rejects a non-https verification URI: %s",
async (verificationUri) => {
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse(deviceCodeResponse({ verification_uri: verificationUri }))),
);
await expect(loginXaiForTest({ onDeviceCode: () => {} })).rejects.toThrow("Untrusted verification URI");
},
);
it.each(["access_denied", "authorization_denied"])(
"fails when device authorization is denied: %s",
async (error) => {
vi.useFakeTimers();
let requestCount = 0;
vi.stubGlobal(
"fetch",
vi.fn(async () => {
requestCount += 1;
return requestCount === 1
? jsonResponse(deviceCodeResponse({ interval: 1 }))
: jsonResponse({ error }, 400);
}),
);
const loginPromise = loginXaiForTest({ onDeviceCode: () => {} });
const assertion = expect(loginPromise).rejects.toThrow("xAI device authorization was denied");
await vi.advanceTimersByTimeAsync(1000);
await assertion;
},
);
it("cancels while waiting for the first token poll", async () => {
vi.useFakeTimers();
const controller = new AbortController();
const fetchMock = vi.fn(async () => jsonResponse(deviceCodeResponse()));
vi.stubGlobal("fetch", fetchMock);
const loginPromise = loginXaiForTest({
onDeviceCode: () => controller.abort(),
signal: controller.signal,
});
await expect(loginPromise).rejects.toThrow("Login cancelled");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("refreshes tokens and preserves an unrotated refresh token", async () => {
let requestCount = 0;
const fetchMock = vi.fn(async (input: unknown, init?: RequestInit) => {
expect(requestUrl(input)).toBe("https://auth.x.ai/oauth2/token");
const form = requestForm(init);
expect(form.get("grant_type")).toBe("refresh_token");
expect(form.get("client_id")).toBe("b1a00492-073a-47ea-816f-4c329264a828");
requestCount += 1;
if (requestCount === 1) {
expect(form.get("refresh_token")).toBe("old-refresh");
return jsonResponse(tokenResponse({ access_token: "new-access", refresh_token: "new-refresh" }));
}
expect(form.get("refresh_token")).toBe("keep-refresh");
return jsonResponse(tokenResponse({ access_token: "newer-access", refresh_token: undefined }));
});
vi.stubGlobal("fetch", fetchMock);
const rotated = await refreshXaiForTest("old-refresh");
const preserved = await refreshXaiForTest("keep-refresh");
expect(rotated.type).toBe("oauth");
expect(rotated.refresh).toBe("new-refresh");
expect(rotated.access).toBe("new-access");
expect(preserved.refresh).toBe("keep-refresh");
expect(preserved.access).toBe("newer-access");
expect(xaiOAuth.name).toBe("xAI (Grok/X subscription)");
await expect(xaiOAuth.toAuth(preserved)).resolves.toEqual({ apiKey: "newer-access" });
});
it("assumes a one-hour lifetime when expires_in is missing", async () => {
vi.useFakeTimers();
const startTime = new Date("2026-07-09T20:00:00Z");
vi.setSystemTime(startTime);
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse(tokenResponse({ expires_in: undefined }))),
);
const credentials = await refreshXaiForTest("old-refresh");
expect(credentials.expires).toBe(startTime.getTime() + 3_600_000 - 300_000);
});
it("rejects token responses with missing fields", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse(tokenResponse({ access_token: undefined }))),
);
await expect(refreshXaiForTest("old-refresh")).rejects.toThrow("Invalid xAI OAuth response field: access_token");
});
it("surfaces the upstream error code and description on refresh failure", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse({ error: "invalid_grant", error_description: "refresh token revoked" }, 400)),
);
await expect(refreshXaiForTest("old-refresh")).rejects.toThrow(
"xAI OAuth token refresh failed (HTTP 400): invalid_grant: refresh token revoked",
);
});
});
+105
View File
@@ -0,0 +1,105 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenAIResponsesOptions } from "../src/api/openai-responses.ts";
import { getSupportedThinkingLevels } from "../src/models.ts";
import { XAI_MODELS } from "../src/providers/xai.models.ts";
import { xaiProvider } from "../src/providers/xai.ts";
import type { Context, Model } from "../src/types.ts";
type CapturedRequest = {
url: string;
headers: Headers;
body: Record<string, unknown>;
};
function completedResponse(): Response {
const event = {
type: "response.completed",
sequence_number: 0,
response: {
id: "resp_xai_test",
status: "completed",
output: [],
usage: {
input_tokens: 1,
output_tokens: 1,
total_tokens: 2,
input_tokens_details: { cached_tokens: 0 },
},
},
};
return new Response(`data: ${JSON.stringify(event)}\n\ndata: [DONE]\n\n`, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
async function captureRequest(
model: Model<"openai-responses">,
context: Context,
options: OpenAIResponsesOptions,
): Promise<CapturedRequest> {
let captured: CapturedRequest | undefined;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
const request = new Request(input, init);
captured = {
url: request.url,
headers: request.headers,
body: JSON.parse(await request.clone().text()) as Record<string, unknown>,
};
return completedResponse();
});
const result = await xaiProvider().stream(model, context, options).result();
expect(result.stopReason, result.errorMessage).toBe("stop");
expect(captured).toBeDefined();
return captured!;
}
describe("xAI Responses provider", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("uses Responses with low/medium/high efforts only for Grok 4.5", () => {
expect(XAI_MODELS["grok-4.5"].api).toBe("openai-responses");
expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.5"])).toEqual(["low", "medium", "high"]);
expect(XAI_MODELS["grok-4.3"].api).toBe("openai-completions");
});
it("uses /responses with bearer auth and xAI-compatible request fields", async () => {
const captured = await captureRequest(
XAI_MODELS["grok-4.5"],
{
systemPrompt: "You are a careful coding assistant.",
messages: [{ role: "user", content: "hello", timestamp: 1 }],
},
{
apiKey: "xai-test-token",
sessionId: "pi-session-123",
cacheRetention: "long",
reasoningEffort: "medium",
},
);
expect(captured.url).toBe("https://api.x.ai/v1/responses");
expect(captured.headers.get("authorization")).toBe("Bearer xai-test-token");
expect(captured.headers.get("session_id")).toBe("pi-session-123");
expect(captured.body).toMatchObject({
model: "grok-4.5",
store: false,
stream: true,
prompt_cache_key: "pi-session-123",
reasoning: { effort: "medium" },
include: ["reasoning.encrypted_content"],
});
expect(captured.body).not.toHaveProperty("prompt_cache_retention");
expect(captured.body.input).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "developer",
content: "You are a careful coding assistant.",
}),
]),
);
});
});