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 -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.",
}),
]),
);
});
});