remove gemini cli and antigravity support
This commit is contained in:
@@ -10,10 +10,7 @@ import { hasBedrockCredentials } from "./bedrock-utils.js";
|
||||
import { resolveApiKey } from "./oauth.js";
|
||||
|
||||
// Resolve OAuth tokens at module level (async, runs before tests)
|
||||
const [geminiCliToken, openaiCodexToken] = await Promise.all([
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [openaiCodexToken] = await Promise.all([resolveApiKey("openai-codex")]);
|
||||
|
||||
async function testAbortSignal<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
||||
const context: Context = {
|
||||
@@ -217,19 +214,6 @@ describe("AI Providers Abort Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Google Gemini CLI / Antigravity share the same provider, so one test covers both
|
||||
describe("Google Gemini CLI Provider Abort", () => {
|
||||
it.skipIf(!geminiCliToken)("should abort mid-stream", { retry: 3 }, async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testAbortSignal(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle immediate abort", { retry: 3 }, async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testImmediateAbort(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider Abort", () => {
|
||||
it.skipIf(!openaiCodexToken)("should abort mid-stream", { retry: 3 }, async () => {
|
||||
const llm = getModel("openai-codex", "gpt-5.2-codex");
|
||||
|
||||
@@ -23,13 +23,8 @@ import { hasBedrockCredentials } from "./bedrock-utils.js";
|
||||
import { resolveApiKey } from "./oauth.js";
|
||||
|
||||
// Resolve OAuth tokens at module level (async, runs before tests)
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const oauthTokens = await Promise.all([resolveApiKey("github-copilot"), resolveApiKey("openai-codex")]);
|
||||
const [githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
// Lorem ipsum paragraph for realistic token estimation
|
||||
const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. `;
|
||||
@@ -220,64 +215,12 @@ describe("Context overflow error handling", () => {
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Google Gemini CLI (OAuth)
|
||||
// Uses same API as Google, expects same error pattern
|
||||
// =============================================================================
|
||||
|
||||
describe("Google Gemini CLI (OAuth)", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
const result = await testContextOverflow(model, geminiCliToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Google Antigravity (OAuth)
|
||||
// Tests both Gemini and Anthropic models via Antigravity
|
||||
// =============================================================================
|
||||
|
||||
describe("Google Antigravity (OAuth)", () => {
|
||||
// Gemini model
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "gemini-3-flash");
|
||||
const result = await testContextOverflow(model, antigravityToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
expect(result.errorMessage).toMatch(/input token count.*exceeds the maximum/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
|
||||
// Anthropic model via Antigravity
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should detect overflow via isContextOverflow",
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
const result = await testContextOverflow(model, antigravityToken!);
|
||||
logResult(result);
|
||||
|
||||
expect(result.stopReason).toBe("error");
|
||||
// Anthropic models return "prompt is too long" pattern
|
||||
expect(result.errorMessage).toMatch(/prompt is too long/i);
|
||||
expect(isContextOverflow(result.response, model.contextWindow)).toBe(true);
|
||||
},
|
||||
120000,
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// OpenAI Codex (OAuth)
|
||||
// Uses ChatGPT Plus/Pro subscription via OAuth
|
||||
|
||||
@@ -66,9 +66,6 @@ const PROVIDER_MODEL_PAIRS: ProviderModelPair[] = [
|
||||
{ provider: "azure-openai-responses", model: "gpt-4o-mini", label: "azure-openai-responses-gpt-4o-mini" },
|
||||
// OpenAI Codex
|
||||
{ provider: "openai-codex", model: "gpt-5.2-codex", label: "openai-codex-gpt-5.2-codex" },
|
||||
// Google Antigravity
|
||||
{ provider: "google-antigravity", model: "gemini-3-flash", label: "antigravity-gemini-3-flash" },
|
||||
{ provider: "google-antigravity", model: "claude-sonnet-4-5", label: "antigravity-claude-sonnet-4-5" },
|
||||
// GitHub Copilot
|
||||
{ provider: "github-copilot", model: "claude-sonnet-4.5", label: "copilot-claude-sonnet-4.5" },
|
||||
{ provider: "github-copilot", model: "gpt-5.1-codex", label: "copilot-gpt-5.1-codex" },
|
||||
|
||||
@@ -13,11 +13,9 @@ import { resolveApiKey } from "./oauth.js";
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
async function testEmptyMessage<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
||||
// Test with completely empty content array
|
||||
@@ -577,154 +575,6 @@ describe("AI Providers Empty Message Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider Empty Messages", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle empty content array",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testEmptyMessage(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle empty string content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testEmptyStringMessage(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle whitespace-only content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testWhitespaceOnlyMessage(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle empty assistant message in conversation",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testEmptyAssistantMessage(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider Empty Messages", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle empty content array",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testEmptyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle empty string content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle whitespace-only content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle empty assistant message in conversation",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle empty content array",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testEmptyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle empty string content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle whitespace-only content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle empty assistant message in conversation",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle empty content array",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testEmptyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle empty string content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testEmptyStringMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle whitespace-only content",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testWhitespaceOnlyMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle empty assistant message in conversation",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testEmptyAssistantMessage(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider Empty Messages", () => {
|
||||
it.skipIf(!openaiCodexToken)(
|
||||
"gpt-5.2-codex - should handle empty content array",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js";
|
||||
import type { Context, Model } from "../src/types.js";
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
const apiKey = JSON.stringify({ token: "token", projectId: "project" });
|
||||
|
||||
const createSseResponse = () => {
|
||||
const sse = `${[
|
||||
`data: ${JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: { role: "model", parts: [{ text: "Hello" }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
},
|
||||
})}`,
|
||||
].join("\n\n")}\n\n`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(sse));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("google-gemini-cli Claude thinking header", () => {
|
||||
const context: Context = {
|
||||
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
||||
};
|
||||
|
||||
it("adds anthropic-beta for Claude thinking models", async () => {
|
||||
const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
expect(headers.get("anthropic-beta")).toBe("interleaved-thinking-2025-05-14");
|
||||
return createSseResponse();
|
||||
});
|
||||
|
||||
global.fetch = fetchMock as typeof fetch;
|
||||
|
||||
const model: Model<"google-gemini-cli"> = {
|
||||
id: "claude-opus-4-5-thinking",
|
||||
name: "Claude Opus 4.5 Thinking",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
|
||||
const stream = streamGoogleGeminiCli(model, context, { apiKey });
|
||||
for await (const _event of stream) {
|
||||
// exhaust stream
|
||||
}
|
||||
await stream.result();
|
||||
});
|
||||
|
||||
it("does not add anthropic-beta for Gemini models", async () => {
|
||||
const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
expect(headers.has("anthropic-beta")).toBe(false);
|
||||
return createSseResponse();
|
||||
});
|
||||
|
||||
global.fetch = fetchMock as typeof fetch;
|
||||
|
||||
const model: Model<"google-gemini-cli"> = {
|
||||
id: "gemini-2.5-flash",
|
||||
name: "Gemini 2.5 Flash",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
|
||||
const stream = streamGoogleGeminiCli(model, context, { apiKey });
|
||||
for await (const _event of stream) {
|
||||
// exhaust stream
|
||||
}
|
||||
await stream.result();
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js";
|
||||
import type { Context, Model } from "../src/types.js";
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("google-gemini-cli empty stream retry", () => {
|
||||
it("retries empty SSE responses without duplicate start", async () => {
|
||||
const emptyStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const sse = `${[
|
||||
`data: ${JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: { role: "model", parts: [{ text: "Hello" }] },
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 1,
|
||||
candidatesTokenCount: 1,
|
||||
totalTokenCount: 2,
|
||||
},
|
||||
},
|
||||
})}`,
|
||||
].join("\n\n")}\n\n`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const dataStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(sse));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
let callCount = 0;
|
||||
const fetchMock = vi.fn(async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
return new Response(emptyStream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
return new Response(dataStream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
});
|
||||
|
||||
global.fetch = fetchMock as typeof fetch;
|
||||
|
||||
const model: Model<"google-gemini-cli"> = {
|
||||
id: "gemini-2.5-flash",
|
||||
name: "Gemini 2.5 Flash",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
|
||||
const context: Context = {
|
||||
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
||||
};
|
||||
|
||||
const stream = streamGoogleGeminiCli(model, context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "project" }),
|
||||
});
|
||||
|
||||
let startCount = 0;
|
||||
let doneCount = 0;
|
||||
let text = "";
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === "start") {
|
||||
startCount += 1;
|
||||
}
|
||||
if (event.type === "done") {
|
||||
doneCount += 1;
|
||||
}
|
||||
if (event.type === "text_delta") {
|
||||
text += event.delta;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await stream.result();
|
||||
|
||||
expect(text).toBe("Hello");
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(startCount).toBe(1);
|
||||
expect(doneCount).toBe(1);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { extractRetryDelay } from "../src/providers/google-gemini-cli.js";
|
||||
|
||||
describe("extractRetryDelay header parsing", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("prefers Retry-After seconds header", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
||||
|
||||
const response = new Response("", { headers: { "Retry-After": "5" } });
|
||||
const delay = extractRetryDelay("Please retry in 1s", response);
|
||||
|
||||
expect(delay).toBe(6000);
|
||||
});
|
||||
|
||||
it("parses Retry-After HTTP date header", () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2025-01-01T00:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const retryAt = new Date(now.getTime() + 12000).toUTCString();
|
||||
const response = new Response("", { headers: { "Retry-After": retryAt } });
|
||||
const delay = extractRetryDelay("", response);
|
||||
|
||||
expect(delay).toBe(13000);
|
||||
});
|
||||
|
||||
it("parses x-ratelimit-reset header", () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2025-01-01T00:00:00Z");
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const resetAtMs = now.getTime() + 20000;
|
||||
const resetSeconds = Math.floor(resetAtMs / 1000).toString();
|
||||
const response = new Response("", { headers: { "x-ratelimit-reset": resetSeconds } });
|
||||
const delay = extractRetryDelay("", response);
|
||||
|
||||
expect(delay).toBe(21000);
|
||||
});
|
||||
|
||||
it("parses x-ratelimit-reset-after header", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
||||
|
||||
const response = new Response("", { headers: { "x-ratelimit-reset-after": "30" } });
|
||||
const delay = extractRetryDelay("", response);
|
||||
|
||||
expect(delay).toBe(31000);
|
||||
});
|
||||
});
|
||||
@@ -34,11 +34,11 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () =>
|
||||
id: "call_1",
|
||||
name: "bash",
|
||||
arguments: { command: "ls -la" },
|
||||
// No thoughtSignature: simulates Claude via Antigravity.
|
||||
// No thoughtSignature: simulates replay from a provider without thought signatures.
|
||||
},
|
||||
],
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
model: "claude-sonnet-4-6",
|
||||
usage: {
|
||||
input: 0,
|
||||
@@ -144,8 +144,8 @@ describe("google-shared convertMessages — Gemini 3 unsigned tool calls", () =>
|
||||
// No thoughtSignature
|
||||
},
|
||||
],
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-antigravity",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
model: "claude-sonnet-4-6",
|
||||
usage: {
|
||||
input: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { convertMessages } from "../src/providers/google-shared.js";
|
||||
import type { Context, Model } from "../src/types.js";
|
||||
|
||||
function makeModel<TApi extends "google-generative-ai" | "google-gemini-cli">(
|
||||
function makeModel<TApi extends "google-generative-ai">(
|
||||
api: TApi,
|
||||
provider: Model<TApi>["provider"],
|
||||
id: string,
|
||||
@@ -99,26 +99,4 @@ describe("google-shared image tool result routing", () => {
|
||||
expect(imageResponse?.parts).toHaveLength(1);
|
||||
expect(imageResponse?.parts?.[0]?.inlineData).toBeTruthy();
|
||||
});
|
||||
|
||||
it("nests image tool results for non-Gemini models on Antigravity / Cloud Code Assist", () => {
|
||||
const model = makeModel("google-gemini-cli", "google-antigravity", "claude-sonnet-4-6");
|
||||
const contents = convertMessages(model, makeContext(model));
|
||||
|
||||
expect(contents).toHaveLength(3);
|
||||
const toolResultTurn = contents[2];
|
||||
expect(toolResultTurn.parts).toHaveLength(3);
|
||||
const imageResponse = toolResultTurn.parts?.[1]?.functionResponse;
|
||||
expect(imageResponse).toBeTruthy();
|
||||
expect(imageResponse?.parts).toHaveLength(1);
|
||||
expect(imageResponse?.parts?.[0]?.inlineData).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps separate synthetic image turn for Gemini 2.x Cloud Code Assist models", () => {
|
||||
const model = makeModel("google-gemini-cli", "google-gemini-cli", "gemini-2.5-flash");
|
||||
const contents = convertMessages(model, makeContext(model));
|
||||
|
||||
expect(contents).toHaveLength(5);
|
||||
expect(contents[3].parts?.[0]?.text).toBe("Tool result image:");
|
||||
expect(contents[3].parts?.[1]?.inlineData).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest";
|
||||
import { getModel } from "../src/models.js";
|
||||
import { streamSimple } from "../src/stream.js";
|
||||
import type { Api, Context, Model, SimpleStreamOptions } from "../src/types.js";
|
||||
import { resolveApiKey } from "./oauth.js";
|
||||
|
||||
type SimpleOptionsWithExtras = SimpleStreamOptions & Record<string, unknown>;
|
||||
|
||||
@@ -20,9 +19,6 @@ interface DisableExpectations {
|
||||
maxOutputTokens?: number;
|
||||
}
|
||||
|
||||
const oauthTokens = await Promise.all([resolveApiKey("google-gemini-cli"), resolveApiKey("google-antigravity")]);
|
||||
const [geminiCliToken, antigravityToken] = oauthTokens;
|
||||
|
||||
function makeContext(): Context {
|
||||
return {
|
||||
systemPrompt: "You are a precise assistant. Follow the requested output format exactly.",
|
||||
@@ -148,24 +144,6 @@ describe("Google Vertex thinking disable E2E", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI thinking disable E2E", () => {
|
||||
it.skipIf(!geminiCliToken)("disables thinking for Gemini 2.5", { retry: 2, timeout: 30000 }, async () => {
|
||||
await expectThinkingDisabledE2E(getModel("google-gemini-cli", "gemini-2.5-flash"), {
|
||||
requestOptions: { apiKey: geminiCliToken! },
|
||||
maxOutputTokens: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity thinking disable E2E", () => {
|
||||
it.skipIf(!antigravityToken)("disables thinking for Gemini 3.x", { retry: 2, timeout: 30000 }, async () => {
|
||||
await expectThinkingDisabledE2E(getModel("google-antigravity", "gemini-3-flash"), {
|
||||
requestOptions: { apiKey: antigravityToken! },
|
||||
maxOutputTokens: 100,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI thinking disable E2E", () => {
|
||||
it("disables thinking for Responses reasoning models", { retry: 2, timeout: 30000 }, async () => {
|
||||
await expectThinkingDisabledE2E(getModel("openai", "gpt-5.4-mini"), {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { Type } from "typebox";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js";
|
||||
import type { Context, Model, ToolCall } from "../src/types.js";
|
||||
|
||||
const emptySchema = Type.Object({});
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("google providers tool call missing args", () => {
|
||||
it("defaults arguments to empty object when provider omits args field", async () => {
|
||||
// Simulate a tool call response where args is missing (no-arg tool)
|
||||
const sse = `${[
|
||||
`data: ${JSON.stringify({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
name: "get_status",
|
||||
// args intentionally omitted
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finishReason: "STOP",
|
||||
},
|
||||
],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 10,
|
||||
candidatesTokenCount: 5,
|
||||
totalTokenCount: 15,
|
||||
},
|
||||
},
|
||||
})}`,
|
||||
].join("\n\n")}\n\n`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const dataStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(sse));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const fetchMock = vi.fn(async () => {
|
||||
return new Response(dataStream, {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
});
|
||||
});
|
||||
|
||||
global.fetch = fetchMock as typeof fetch;
|
||||
|
||||
const model: Model<"google-gemini-cli"> = {
|
||||
id: "gemini-2.5-flash",
|
||||
name: "Gemini 2.5 Flash",
|
||||
api: "google-gemini-cli",
|
||||
provider: "google-gemini-cli",
|
||||
baseUrl: "https://cloudcode-pa.googleapis.com",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
|
||||
const context: Context = {
|
||||
messages: [{ role: "user", content: "Check status", timestamp: Date.now() }],
|
||||
tools: [
|
||||
{
|
||||
name: "get_status",
|
||||
description: "Get current status",
|
||||
parameters: emptySchema,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const stream = streamGoogleGeminiCli(model, context, {
|
||||
apiKey: JSON.stringify({ token: "token", projectId: "project" }),
|
||||
});
|
||||
|
||||
for await (const _ of stream) {
|
||||
// consume stream
|
||||
}
|
||||
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("toolUse");
|
||||
expect(result.content).toHaveLength(1);
|
||||
|
||||
const toolCall = result.content[0] as ToolCall;
|
||||
expect(toolCall.type).toBe("toolCall");
|
||||
expect(toolCall.name).toBe("get_status");
|
||||
expect(toolCall.arguments).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -16,11 +16,9 @@ import { resolveApiKey } from "./oauth.js";
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
/**
|
||||
* Test that tool results containing only images work correctly across all providers.
|
||||
@@ -398,67 +396,6 @@ describe("Tool Results with Images", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle tool result with only image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await handleToolWithImageResult(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle tool result with text and image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await handleToolWithTextAndImageResult(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle tool result with only image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await handleToolWithImageResult(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle tool result with text and image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await handleToolWithTextAndImageResult(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
/** These two don't work, the model simply won't call the tool, works in pi
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle tool result with only image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await handleToolWithImageResult(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle tool result with text and image",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await handleToolWithTextAndImageResult(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);**/
|
||||
|
||||
// Note: gpt-oss-120b-medium does not support images, so not tested here
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider", () => {
|
||||
it.skipIf(!openaiCodexToken)(
|
||||
"gpt-5.2-codex - should handle tool result with only image",
|
||||
|
||||
@@ -53,7 +53,6 @@ function saveAuthStorage(storage: AuthStorage): void {
|
||||
* For API key credentials, returns the key directly.
|
||||
* For OAuth credentials, returns the access token (refreshing if expired and saving back).
|
||||
*
|
||||
* For google-gemini-cli and google-antigravity, returns JSON-encoded { token, projectId }
|
||||
*/
|
||||
export async function resolveApiKey(provider: string): Promise<string | undefined> {
|
||||
const storage = loadAuthStorage();
|
||||
|
||||
@@ -7,13 +7,8 @@ import { resolveApiKey } from "./oauth.js";
|
||||
|
||||
type StreamOptionsWithExtras = StreamOptions & Record<string, unknown>;
|
||||
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const oauthTokens = await Promise.all([resolveApiKey("github-copilot"), resolveApiKey("openai-codex")]);
|
||||
const [githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
async function expectResponseId<TApi extends Api>(model: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
||||
const context: Context = {
|
||||
@@ -117,25 +112,6 @@ describe("responseId E2E Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider", () => {
|
||||
it.skipIf(!geminiCliToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await expectResponseId(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider", () => {
|
||||
it.skipIf(!antigravityToken)("Gemini path should expose responseId", { retry: 3, timeout: 30000 }, async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3.1-pro-high");
|
||||
await expectResponseId(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("Claude path should expose responseId", { retry: 3, timeout: 30000 }, async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-6");
|
||||
await expectResponseId(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider", () => {
|
||||
it.skipIf(!openaiCodexToken)("should expose responseId", { retry: 3, timeout: 30000 }, async () => {
|
||||
const llm = getModel("openai-codex", "gpt-5.2-codex");
|
||||
|
||||
@@ -22,11 +22,9 @@ const __dirname = dirname(__filename);
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
// Calculator tool definition (same as examples)
|
||||
// Note: Using StringEnum helper because Google's API doesn't support anyOf/const patterns
|
||||
@@ -1028,124 +1026,6 @@ describe("Generate E2E Tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider (gemini-2.5-flash)", () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
|
||||
it.skipIf(!geminiCliToken)("should complete basic text generation", { retry: 3 }, async () => {
|
||||
await basicTextGeneration(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle tool calling", { retry: 3 }, async () => {
|
||||
await handleToolCall(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle streaming", { retry: 3 }, async () => {
|
||||
await handleStreaming(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle thinking", { retry: 3 }, async () => {
|
||||
await handleThinking(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 1024 } });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||
await multiTurn(llm, { apiKey: geminiCliToken, thinking: { enabled: true, budgetTokens: 2048 } });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle image input", { retry: 3 }, async () => {
|
||||
await handleImage(llm, { apiKey: geminiCliToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider (gemini-3-flash-preview with thinkingLevel)", () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-3-flash-preview");
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle thinking with thinkingLevel", { retry: 3 }, async () => {
|
||||
await handleThinking(llm, { apiKey: geminiCliToken, thinking: { enabled: true, level: "LOW" } });
|
||||
});
|
||||
|
||||
it.skipIf(!geminiCliToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||
await multiTurn(llm, { apiKey: geminiCliToken, thinking: { enabled: true, level: "MEDIUM" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider (gemini-3.1-pro-high)", () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3.1-pro-high");
|
||||
|
||||
it.skipIf(!antigravityToken)("should complete basic text generation", { retry: 3 }, async () => {
|
||||
await basicTextGeneration(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle tool calling", { retry: 3 }, async () => {
|
||||
await handleToolCall(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle streaming", { retry: 3 }, async () => {
|
||||
await handleStreaming(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle thinking with thinkingLevel", { retry: 3 }, async () => {
|
||||
// gemini-3-pro only supports LOW/HIGH
|
||||
await handleThinking(llm, {
|
||||
apiKey: antigravityToken,
|
||||
thinking: { enabled: true, level: "LOW" },
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||
await multiTurn(llm, { apiKey: antigravityToken, thinking: { enabled: true, level: "HIGH" } });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle image input", { retry: 3 }, async () => {
|
||||
await handleImage(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider (gemini-3.1-pro-high with thinkingLevel)", () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3.1-pro-high");
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle thinking with thinkingLevel HIGH", { retry: 3 }, async () => {
|
||||
// gemini-3-pro only supports LOW/HIGH
|
||||
await handleThinking(llm, {
|
||||
apiKey: antigravityToken,
|
||||
thinking: { enabled: true, level: "HIGH" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider (claude-sonnet-4-5)", () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
|
||||
it.skipIf(!antigravityToken)("should complete basic text generation", { retry: 3 }, async () => {
|
||||
await basicTextGeneration(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle tool calling", { retry: 3 }, async () => {
|
||||
await handleToolCall(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle streaming", { retry: 3 }, async () => {
|
||||
await handleStreaming(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle thinking", { retry: 3 }, async () => {
|
||||
// claude-sonnet-4-5 has reasoning: false, use claude-sonnet-4-5-thinking
|
||||
const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking");
|
||||
await handleThinking(thinkingModel, {
|
||||
apiKey: antigravityToken,
|
||||
thinking: { enabled: true, budgetTokens: 4096 },
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||
const thinkingModel = getModel("google-antigravity", "claude-sonnet-4-5-thinking");
|
||||
await multiTurn(thinkingModel, { apiKey: antigravityToken, thinking: { enabled: true, budgetTokens: 4096 } });
|
||||
});
|
||||
|
||||
it.skipIf(!antigravityToken)("should handle image input", { retry: 3 }, async () => {
|
||||
await handleImage(llm, { apiKey: antigravityToken });
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider (gpt-5.4)", () => {
|
||||
const llm = getModel("openai-codex", "gpt-5.4");
|
||||
|
||||
|
||||
@@ -13,11 +13,9 @@ import { resolveApiKey } from "./oauth.js";
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
async function testTokensOnAbort<TApi extends Api>(llm: Model<TApi>, options: StreamOptionsWithExtras = {}) {
|
||||
const context: Context = {
|
||||
@@ -50,7 +48,7 @@ async function testTokensOnAbort<TApi extends Api>(llm: Model<TApi>, options: St
|
||||
|
||||
expect(msg.stopReason).toBe("aborted");
|
||||
|
||||
// OpenAI providers, OpenAI Codex, Gemini CLI, zai, Amazon Bedrock, and the GPT-OSS model on Antigravity only send usage in the final chunk,
|
||||
// OpenAI providers, OpenAI Codex, zai, and Amazon Bedrock only send usage in the final chunk,
|
||||
// so when aborted they have no token stats. Anthropic and Google send usage information early in the stream.
|
||||
// MiniMax and Kimi report input tokens but not output tokens differently on aborted requests.
|
||||
if (
|
||||
@@ -59,11 +57,9 @@ async function testTokensOnAbort<TApi extends Api>(llm: Model<TApi>, options: St
|
||||
llm.api === "openai-responses" ||
|
||||
llm.api === "azure-openai-responses" ||
|
||||
llm.api === "openai-codex-responses" ||
|
||||
llm.provider === "google-gemini-cli" ||
|
||||
llm.provider === "zai" ||
|
||||
llm.provider === "amazon-bedrock" ||
|
||||
llm.provider === "vercel-ai-gateway" ||
|
||||
(llm.provider === "google-antigravity" && llm.id.includes("gpt-oss"))
|
||||
llm.provider === "vercel-ai-gateway"
|
||||
) {
|
||||
expect(msg.usage.input).toBe(0);
|
||||
expect(msg.usage.output).toBe(0);
|
||||
@@ -79,7 +75,7 @@ async function testTokensOnAbort<TApi extends Api>(llm: Model<TApi>, options: St
|
||||
expect(msg.usage.input).toBeGreaterThan(0);
|
||||
expect(msg.usage.output).toBeGreaterThan(0);
|
||||
|
||||
// Some providers (Antigravity, Copilot) have zero cost rates
|
||||
// Some providers (Copilot) have zero cost rates
|
||||
if (llm.cost.input > 0) {
|
||||
expect(msg.usage.cost.input).toBeGreaterThan(0);
|
||||
expect(msg.usage.cost.total).toBeGreaterThan(0);
|
||||
@@ -254,46 +250,6 @@ describe("Token Statistics on Abort", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should include token stats when aborted mid-stream",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testTokensOnAbort(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should include token stats when aborted mid-stream",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testTokensOnAbort(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-6 - should include token stats when aborted mid-stream",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-6");
|
||||
await testTokensOnAbort(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should include token stats when aborted mid-stream",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testTokensOnAbort(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider", () => {
|
||||
it.skipIf(!openaiCodexToken)(
|
||||
"gpt-5.2-codex - should include token stats when aborted mid-stream",
|
||||
|
||||
@@ -14,11 +14,9 @@ import { resolveApiKey } from "./oauth.js";
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
// Simple calculate tool
|
||||
const calculateSchema = Type.Object({
|
||||
@@ -275,46 +273,6 @@ describe("Tool Call Without Result Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should filter out tool calls without corresponding tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const model = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testToolCallWithoutResult(model, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should filter out tool calls without corresponding tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testToolCallWithoutResult(model, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should filter out tool calls without corresponding tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testToolCallWithoutResult(model, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should filter out tool calls without corresponding tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const model = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testToolCallWithoutResult(model, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("OpenAI Codex Provider", () => {
|
||||
it.skipIf(!openaiCodexToken)(
|
||||
"gpt-5.2-codex - should filter out tool calls without corresponding tool results",
|
||||
|
||||
@@ -27,11 +27,9 @@ import { resolveApiKey } from "./oauth.js";
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
// Generate a long system prompt to trigger caching (>2k bytes for most providers)
|
||||
const LONG_SYSTEM_PROMPT = `You are a helpful assistant. Be concise in your responses.
|
||||
@@ -600,85 +598,11 @@ describe("totalTokens field", () => {
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Google Gemini CLI (OAuth)
|
||||
// =========================================================================
|
||||
|
||||
describe("Google Gemini CLI (OAuth)", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should return totalTokens equal to sum of components",
|
||||
{ retry: 3, timeout: 60000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
|
||||
console.log(`\nGoogle Gemini CLI / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: geminiCliToken });
|
||||
|
||||
logUsage("First request", first);
|
||||
logUsage("Second request", second);
|
||||
|
||||
assertTotalTokensEqualsComponents(first);
|
||||
assertTotalTokensEqualsComponents(second);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Google Antigravity (OAuth)
|
||||
// =========================================================================
|
||||
|
||||
describe("Google Antigravity (OAuth)", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should return totalTokens equal to sum of components",
|
||||
{ retry: 3, timeout: 60000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
|
||||
console.log(`\nGoogle Antigravity / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken });
|
||||
|
||||
logUsage("First request", first);
|
||||
logUsage("Second request", second);
|
||||
|
||||
assertTotalTokensEqualsComponents(first);
|
||||
assertTotalTokensEqualsComponents(second);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should return totalTokens equal to sum of components",
|
||||
{ retry: 3, timeout: 60000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
|
||||
console.log(`\nGoogle Antigravity / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken });
|
||||
|
||||
logUsage("First request", first);
|
||||
logUsage("Second request", second);
|
||||
|
||||
assertTotalTokensEqualsComponents(first);
|
||||
assertTotalTokensEqualsComponents(second);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should return totalTokens equal to sum of components",
|
||||
{ retry: 3, timeout: 60000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
|
||||
console.log(`\nGoogle Antigravity / ${llm.id}:`);
|
||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: antigravityToken });
|
||||
|
||||
logUsage("First request", first);
|
||||
logUsage("Second request", second);
|
||||
|
||||
assertTotalTokensEqualsComponents(first);
|
||||
assertTotalTokensEqualsComponents(second);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.skipIf(!hasBedrockCredentials())("Amazon Bedrock", () => {
|
||||
it(
|
||||
"claude-sonnet-4-5 - should return totalTokens equal to sum of components",
|
||||
|
||||
@@ -17,11 +17,9 @@ const emptySchema = Type.Object({});
|
||||
const oauthTokens = await Promise.all([
|
||||
resolveApiKey("anthropic"),
|
||||
resolveApiKey("github-copilot"),
|
||||
resolveApiKey("google-gemini-cli"),
|
||||
resolveApiKey("google-antigravity"),
|
||||
resolveApiKey("openai-codex"),
|
||||
]);
|
||||
const [anthropicOAuthToken, githubCopilotToken, geminiCliToken, antigravityToken, openaiCodexToken] = oauthTokens;
|
||||
const [anthropicOAuthToken, githubCopilotToken, openaiCodexToken] = oauthTokens;
|
||||
|
||||
/**
|
||||
* Test for Unicode surrogate pair handling in tool results.
|
||||
@@ -451,118 +449,6 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Gemini CLI Provider Unicode Handling", () => {
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle emoji in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testEmojiInToolResults(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle real-world LinkedIn comment data with emoji",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testRealWorldLinkedInData(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!geminiCliToken)(
|
||||
"gemini-2.5-flash - should handle unpaired high surrogate (0xD83D) in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-gemini-cli", "gemini-2.5-flash");
|
||||
await testUnpairedHighSurrogate(llm, { apiKey: geminiCliToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Google Antigravity Provider Unicode Handling", () => {
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle emoji in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testEmojiInToolResults(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle real-world LinkedIn comment data with emoji",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testRealWorldLinkedInData(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gemini-3-flash - should handle unpaired high surrogate (0xD83D) in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gemini-3-flash");
|
||||
await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle emoji in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testEmojiInToolResults(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle real-world LinkedIn comment data with emoji",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testRealWorldLinkedInData(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"claude-sonnet-4-5 - should handle unpaired high surrogate (0xD83D) in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "claude-sonnet-4-5");
|
||||
await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle emoji in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testEmojiInToolResults(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle real-world LinkedIn comment data with emoji",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testRealWorldLinkedInData(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!antigravityToken)(
|
||||
"gpt-oss-120b-medium - should handle unpaired high surrogate (0xD83D) in tool results",
|
||||
{ retry: 3, timeout: 30000 },
|
||||
async () => {
|
||||
const llm = getModel("google-antigravity", "gpt-oss-120b-medium");
|
||||
await testUnpairedHighSurrogate(llm, { apiKey: antigravityToken });
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Unicode Handling", () => {
|
||||
const llm = getModel("xai", "grok-3");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user