mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-21 04:44:55 +00:00
799 lines
24 KiB
TypeScript
799 lines
24 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
isThinkingCapableModel,
|
|
extractThinkingConfig,
|
|
resolveThinkingConfig,
|
|
filterUnsignedThinkingBlocks,
|
|
filterMessagesThinkingBlocks,
|
|
transformThinkingParts,
|
|
normalizeThinkingConfig,
|
|
parseAntigravityApiBody,
|
|
extractUsageMetadata,
|
|
extractUsageFromSsePayload,
|
|
rewriteAntigravityPreviewAccessError,
|
|
DEFAULT_THINKING_BUDGET,
|
|
} from "./request-helpers";
|
|
|
|
describe("sanitizeThinkingPart (covered via filtering)", () => {
|
|
it("extracts wrapped text and strips SDK fields for Gemini-style thought blocks", () => {
|
|
const validSignature = "s".repeat(60);
|
|
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{
|
|
thought: true,
|
|
text: {
|
|
text: "wrapped thought",
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
},
|
|
thoughtSignature: validSignature,
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterUnsignedThinkingBlocks(contents) as any;
|
|
expect(result[0].parts).toHaveLength(1);
|
|
expect(result[0].parts[0]).toEqual({
|
|
thought: true,
|
|
text: "wrapped thought",
|
|
thoughtSignature: validSignature,
|
|
});
|
|
|
|
// Ensure injected fields are removed
|
|
expect(result[0].parts[0].cache_control).toBeUndefined();
|
|
expect(result[0].parts[0].providerOptions).toBeUndefined();
|
|
});
|
|
|
|
it("extracts wrapped thinking text and strips SDK fields for Anthropic-style thinking blocks", () => {
|
|
const validSignature = "a".repeat(60);
|
|
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{
|
|
type: "thinking",
|
|
thinking: {
|
|
text: "wrapped thinking",
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
},
|
|
signature: validSignature,
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterUnsignedThinkingBlocks(contents) as any;
|
|
expect(result[0].parts).toHaveLength(1);
|
|
expect(result[0].parts[0]).toEqual({
|
|
type: "thinking",
|
|
thinking: "wrapped thinking",
|
|
signature: validSignature,
|
|
});
|
|
});
|
|
|
|
it("preserves signatures while dropping cache_control/providerOptions during signature restoration", () => {
|
|
const cachedSignature = "c".repeat(60);
|
|
const getCachedSignatureFn = (_sessionId: string, _text: string) => cachedSignature;
|
|
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "thinking",
|
|
thinking: {
|
|
thinking: "restore me",
|
|
cache_control: { type: "ephemeral" },
|
|
},
|
|
// no signature present (forces restore)
|
|
providerOptions: { injected: true },
|
|
},
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages, "session-1", getCachedSignatureFn) as any;
|
|
expect(result[0].content[0]).toEqual({
|
|
type: "thinking",
|
|
thinking: "restore me",
|
|
signature: cachedSignature,
|
|
});
|
|
});
|
|
|
|
it("falls back to recursive stripping for signed reasoning blocks and removes nested SDK fields", () => {
|
|
const validSignature = "z".repeat(60);
|
|
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{
|
|
type: "reasoning",
|
|
signature: validSignature,
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
meta: {
|
|
keep: true,
|
|
cache_control: { nested: true },
|
|
arr: [
|
|
{ providerOptions: { nested: true }, keep: 1 },
|
|
{ cache_control: { nested: true }, keep: 2 },
|
|
],
|
|
},
|
|
},
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterUnsignedThinkingBlocks(contents) as any;
|
|
expect(result[0].parts[0]).toEqual({
|
|
type: "reasoning",
|
|
signature: validSignature,
|
|
meta: {
|
|
keep: true,
|
|
arr: [
|
|
{ keep: 1 },
|
|
{ keep: 2 },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isThinkingCapableModel", () => {
|
|
it("returns true for models with 'thinking' in name", () => {
|
|
expect(isThinkingCapableModel("claude-thinking")).toBe(true);
|
|
expect(isThinkingCapableModel("CLAUDE-THINKING-4")).toBe(true);
|
|
expect(isThinkingCapableModel("model-thinking-v1")).toBe(true);
|
|
});
|
|
|
|
it("returns true for models with 'gemini-3' in name", () => {
|
|
expect(isThinkingCapableModel("gemini-3-pro")).toBe(true);
|
|
expect(isThinkingCapableModel("GEMINI-3-flash")).toBe(true);
|
|
expect(isThinkingCapableModel("gemini-3")).toBe(true);
|
|
});
|
|
|
|
it("returns true for models with 'opus' in name", () => {
|
|
expect(isThinkingCapableModel("claude-opus")).toBe(true);
|
|
expect(isThinkingCapableModel("claude-4-opus")).toBe(true);
|
|
expect(isThinkingCapableModel("OPUS")).toBe(true);
|
|
});
|
|
|
|
it("returns false for non-thinking models", () => {
|
|
expect(isThinkingCapableModel("claude-sonnet")).toBe(false);
|
|
expect(isThinkingCapableModel("gemini-2-pro")).toBe(false);
|
|
expect(isThinkingCapableModel("gpt-4")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("extractThinkingConfig", () => {
|
|
it("extracts thinkingConfig from generationConfig", () => {
|
|
const result = extractThinkingConfig(
|
|
{},
|
|
{ thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
|
undefined,
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: 8000 });
|
|
});
|
|
|
|
it("extracts thinkingConfig from extra_body", () => {
|
|
const result = extractThinkingConfig(
|
|
{},
|
|
undefined,
|
|
{ thinkingConfig: { includeThoughts: true, thinkingBudget: 4000 } },
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: 4000 });
|
|
});
|
|
|
|
it("extracts thinkingConfig from requestPayload directly", () => {
|
|
const result = extractThinkingConfig(
|
|
{ thinkingConfig: { includeThoughts: false, thinkingBudget: 2000 } },
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(result).toEqual({ includeThoughts: false, thinkingBudget: 2000 });
|
|
});
|
|
|
|
it("prioritizes generationConfig over extra_body", () => {
|
|
const result = extractThinkingConfig(
|
|
{},
|
|
{ thinkingConfig: { includeThoughts: true, thinkingBudget: 8000 } },
|
|
{ thinkingConfig: { includeThoughts: false, thinkingBudget: 4000 } },
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: 8000 });
|
|
});
|
|
|
|
it("converts Anthropic-style thinking config", () => {
|
|
const result = extractThinkingConfig(
|
|
{ thinking: { type: "enabled", budgetTokens: 10000 } },
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: 10000 });
|
|
});
|
|
|
|
it("uses default budget for Anthropic-style without budgetTokens", () => {
|
|
const result = extractThinkingConfig(
|
|
{ thinking: { type: "enabled" } },
|
|
undefined,
|
|
undefined,
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: DEFAULT_THINKING_BUDGET });
|
|
});
|
|
|
|
it("returns undefined when no config found", () => {
|
|
expect(extractThinkingConfig({}, undefined, undefined)).toBeUndefined();
|
|
});
|
|
|
|
it("uses default budget when thinkingBudget not specified", () => {
|
|
const result = extractThinkingConfig(
|
|
{},
|
|
{ thinkingConfig: { includeThoughts: true } },
|
|
undefined,
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: DEFAULT_THINKING_BUDGET });
|
|
});
|
|
});
|
|
|
|
describe("resolveThinkingConfig", () => {
|
|
it("keeps thinking enabled for Claude models with assistant history", () => {
|
|
const result = resolveThinkingConfig(
|
|
{ includeThoughts: true, thinkingBudget: 8000 },
|
|
true, // isThinkingModel
|
|
true, // isClaudeModel
|
|
true, // hasAssistantHistory
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: 8000 });
|
|
});
|
|
|
|
it("enables thinking for thinking-capable models without user config", () => {
|
|
const result = resolveThinkingConfig(
|
|
undefined,
|
|
true, // isThinkingModel
|
|
false, // isClaudeModel
|
|
false, // hasAssistantHistory
|
|
);
|
|
expect(result).toEqual({ includeThoughts: true, thinkingBudget: DEFAULT_THINKING_BUDGET });
|
|
});
|
|
|
|
it("respects user config for non-Claude models", () => {
|
|
const userConfig = { includeThoughts: false, thinkingBudget: 5000 };
|
|
const result = resolveThinkingConfig(
|
|
userConfig,
|
|
true,
|
|
false,
|
|
false,
|
|
);
|
|
expect(result).toEqual(userConfig);
|
|
});
|
|
|
|
it("returns user config for Claude without history", () => {
|
|
const userConfig = { includeThoughts: true, thinkingBudget: 8000 };
|
|
const result = resolveThinkingConfig(
|
|
userConfig,
|
|
true,
|
|
true, // isClaudeModel
|
|
false, // no history
|
|
);
|
|
expect(result).toEqual(userConfig);
|
|
});
|
|
|
|
it("returns undefined for non-thinking model without user config", () => {
|
|
const result = resolveThinkingConfig(
|
|
undefined,
|
|
false, // not thinking model
|
|
false,
|
|
false,
|
|
);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("filterUnsignedThinkingBlocks", () => {
|
|
it("filters out unsigned thinking parts", () => {
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ type: "thinking", text: "thinking without signature" },
|
|
{ type: "text", text: "visible text" },
|
|
],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toHaveLength(1);
|
|
expect(result[0].parts[0].type).toBe("text");
|
|
});
|
|
|
|
it("keeps signed thinking parts with valid signatures", () => {
|
|
const validSignature = "a".repeat(60);
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ type: "thinking", text: "thinking with signature", signature: validSignature },
|
|
{ type: "text", text: "visible text" },
|
|
],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toHaveLength(2);
|
|
expect(result[0].parts[0].signature).toBe(validSignature);
|
|
});
|
|
|
|
it("filters thinking parts with short signatures", () => {
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ type: "thinking", text: "thinking with short signature", signature: "sig123" },
|
|
{ type: "text", text: "visible text" },
|
|
],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toHaveLength(1);
|
|
expect(result[0].parts[0].type).toBe("text");
|
|
});
|
|
|
|
it("handles Gemini-style thought parts with valid signatures", () => {
|
|
const validSignature = "b".repeat(55);
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ thought: true, text: "no signature" },
|
|
{ thought: true, text: "has signature", thoughtSignature: validSignature },
|
|
],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toHaveLength(1);
|
|
expect(result[0].parts[0].thoughtSignature).toBe(validSignature);
|
|
});
|
|
|
|
it("filters Gemini-style thought parts with short signatures", () => {
|
|
const contents = [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ thought: true, text: "has short signature", thoughtSignature: "sig" },
|
|
],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toHaveLength(0);
|
|
});
|
|
|
|
it("preserves non-thinking parts", () => {
|
|
const contents = [
|
|
{
|
|
role: "user",
|
|
parts: [{ text: "hello" }],
|
|
},
|
|
];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result).toEqual(contents);
|
|
});
|
|
|
|
it("handles empty parts array", () => {
|
|
const contents = [{ role: "model", parts: [] }];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result[0].parts).toEqual([]);
|
|
});
|
|
|
|
it("handles missing parts", () => {
|
|
const contents = [{ role: "model" }];
|
|
const result = filterUnsignedThinkingBlocks(contents);
|
|
expect(result).toEqual(contents);
|
|
});
|
|
});
|
|
|
|
describe("filterMessagesThinkingBlocks", () => {
|
|
it("filters out unsigned thinking blocks in messages[].content", () => {
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "no signature" },
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result[0].content).toHaveLength(1);
|
|
expect(result[0].content[0].type).toBe("text");
|
|
});
|
|
|
|
it("keeps signed thinking blocks with valid signatures and sanitizes injected fields", () => {
|
|
const validSignature = "a".repeat(60);
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "thinking",
|
|
thinking: { text: "wrapped", cache_control: { type: "ephemeral" } },
|
|
signature: validSignature,
|
|
cache_control: { type: "ephemeral" },
|
|
providerOptions: { injected: true },
|
|
},
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result[0].content[0]).toEqual({
|
|
type: "thinking",
|
|
thinking: "wrapped",
|
|
signature: validSignature,
|
|
});
|
|
});
|
|
|
|
it("filters thinking blocks with short signatures", () => {
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "short sig", signature: "sig123" },
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result[0].content).toEqual([{ type: "text", text: "visible" }]);
|
|
});
|
|
|
|
it("restores a missing signature from cache and preserves it after sanitization", () => {
|
|
const cachedSignature = "c".repeat(60);
|
|
const getCachedSignatureFn = (_sessionId: string, _text: string) => cachedSignature;
|
|
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
type: "thinking",
|
|
thinking: { thinking: "restore me", providerOptions: { injected: true } },
|
|
// no signature present (forces restore)
|
|
cache_control: { type: "ephemeral" },
|
|
},
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages, "session-1", getCachedSignatureFn) as any;
|
|
expect(result[0].content[0]).toEqual({
|
|
type: "thinking",
|
|
thinking: "restore me",
|
|
signature: cachedSignature,
|
|
});
|
|
});
|
|
|
|
it("handles Gemini-style thought blocks inside messages content", () => {
|
|
const validSignature = "b".repeat(60);
|
|
const messages = [
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{
|
|
thought: true,
|
|
text: { text: "wrapped thought", cache_control: { type: "ephemeral" } },
|
|
thoughtSignature: validSignature,
|
|
providerOptions: { injected: true },
|
|
},
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result[0].content[0]).toEqual({
|
|
thought: true,
|
|
text: "wrapped thought",
|
|
thoughtSignature: validSignature,
|
|
});
|
|
});
|
|
|
|
it("preserves non-thinking blocks and returns message unchanged when content is missing", () => {
|
|
const messages: any[] = [
|
|
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
{ role: "assistant" },
|
|
];
|
|
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result[0]).toEqual(messages[0]);
|
|
expect(result[1]).toEqual(messages[1]);
|
|
});
|
|
|
|
it("handles non-object messages gracefully", () => {
|
|
const messages: any[] = [null, "string", 123, { role: "assistant", content: [] }];
|
|
const result = filterMessagesThinkingBlocks(messages) as any;
|
|
expect(result).toEqual(messages);
|
|
});
|
|
});
|
|
|
|
describe("transformThinkingParts", () => {
|
|
it("transforms Anthropic-style thinking blocks to reasoning", () => {
|
|
const response = {
|
|
content: [
|
|
{ type: "thinking", thinking: "my thoughts" },
|
|
{ type: "text", text: "visible" },
|
|
],
|
|
};
|
|
const result = transformThinkingParts(response) as any;
|
|
expect(result.content[0].type).toBe("reasoning");
|
|
expect(result.content[0].thought).toBe(true);
|
|
expect(result.reasoning_content).toBe("my thoughts");
|
|
});
|
|
|
|
it("transforms Gemini-style candidates", () => {
|
|
const response = {
|
|
candidates: [
|
|
{
|
|
content: {
|
|
parts: [
|
|
{ thought: true, text: "thinking here" },
|
|
{ text: "output" },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const result = transformThinkingParts(response) as any;
|
|
expect(result.candidates[0].content.parts[0].type).toBe("reasoning");
|
|
expect(result.candidates[0].reasoning_content).toBe("thinking here");
|
|
});
|
|
|
|
it("handles non-object input", () => {
|
|
expect(transformThinkingParts(null)).toBeNull();
|
|
expect(transformThinkingParts(undefined)).toBeUndefined();
|
|
expect(transformThinkingParts("string")).toBe("string");
|
|
});
|
|
|
|
it("preserves other response properties", () => {
|
|
const response = {
|
|
content: [],
|
|
id: "resp-123",
|
|
model: "claude-4",
|
|
};
|
|
const result = transformThinkingParts(response) as any;
|
|
expect(result.id).toBe("resp-123");
|
|
expect(result.model).toBe("claude-4");
|
|
});
|
|
});
|
|
|
|
describe("normalizeThinkingConfig", () => {
|
|
it("returns undefined for non-object input", () => {
|
|
expect(normalizeThinkingConfig(null)).toBeUndefined();
|
|
expect(normalizeThinkingConfig(undefined)).toBeUndefined();
|
|
expect(normalizeThinkingConfig("string")).toBeUndefined();
|
|
});
|
|
|
|
it("normalizes valid config", () => {
|
|
const result = normalizeThinkingConfig({
|
|
thinkingBudget: 8000,
|
|
includeThoughts: true,
|
|
});
|
|
expect(result).toEqual({
|
|
thinkingBudget: 8000,
|
|
includeThoughts: true,
|
|
});
|
|
});
|
|
|
|
it("handles snake_case property names", () => {
|
|
const result = normalizeThinkingConfig({
|
|
thinking_budget: 4000,
|
|
include_thoughts: true,
|
|
});
|
|
expect(result).toEqual({
|
|
thinkingBudget: 4000,
|
|
includeThoughts: true,
|
|
});
|
|
});
|
|
|
|
it("disables includeThoughts when budget is 0", () => {
|
|
const result = normalizeThinkingConfig({
|
|
thinkingBudget: 0,
|
|
includeThoughts: true,
|
|
});
|
|
expect(result?.includeThoughts).toBe(false);
|
|
});
|
|
|
|
it("returns undefined when both values are absent/undefined", () => {
|
|
const result = normalizeThinkingConfig({});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("handles non-finite budget values", () => {
|
|
const result = normalizeThinkingConfig({
|
|
thinkingBudget: Infinity,
|
|
includeThoughts: true,
|
|
});
|
|
// When budget is non-finite (undefined), includeThoughts is forced to false
|
|
expect(result).toEqual({ includeThoughts: false });
|
|
});
|
|
});
|
|
|
|
describe("parseAntigravityApiBody", () => {
|
|
it("parses valid JSON object", () => {
|
|
const result = parseAntigravityApiBody('{"response": {"text": "hello"}}');
|
|
expect(result).toEqual({ response: { text: "hello" } });
|
|
});
|
|
|
|
it("extracts first object from array", () => {
|
|
const result = parseAntigravityApiBody('[{"response": "first"}, {"response": "second"}]');
|
|
expect(result).toEqual({ response: "first" });
|
|
});
|
|
|
|
it("returns null for invalid JSON", () => {
|
|
expect(parseAntigravityApiBody("not json")).toBeNull();
|
|
});
|
|
|
|
it("returns null for empty array", () => {
|
|
expect(parseAntigravityApiBody("[]")).toBeNull();
|
|
});
|
|
|
|
it("returns null for primitive values", () => {
|
|
expect(parseAntigravityApiBody('"string"')).toBeNull();
|
|
expect(parseAntigravityApiBody("123")).toBeNull();
|
|
});
|
|
|
|
it("handles array with null values", () => {
|
|
const result = parseAntigravityApiBody('[null, {"valid": true}]');
|
|
expect(result).toEqual({ valid: true });
|
|
});
|
|
});
|
|
|
|
describe("extractUsageMetadata", () => {
|
|
it("extracts usage from response.usageMetadata", () => {
|
|
const body = {
|
|
response: {
|
|
usageMetadata: {
|
|
totalTokenCount: 1000,
|
|
promptTokenCount: 500,
|
|
candidatesTokenCount: 500,
|
|
cachedContentTokenCount: 100,
|
|
},
|
|
},
|
|
};
|
|
const result = extractUsageMetadata(body);
|
|
expect(result).toEqual({
|
|
totalTokenCount: 1000,
|
|
promptTokenCount: 500,
|
|
candidatesTokenCount: 500,
|
|
cachedContentTokenCount: 100,
|
|
});
|
|
});
|
|
|
|
it("returns null when no usageMetadata", () => {
|
|
expect(extractUsageMetadata({ response: {} })).toBeNull();
|
|
expect(extractUsageMetadata({})).toBeNull();
|
|
});
|
|
|
|
it("handles partial usage data", () => {
|
|
const body = {
|
|
response: {
|
|
usageMetadata: {
|
|
totalTokenCount: 1000,
|
|
},
|
|
},
|
|
};
|
|
const result = extractUsageMetadata(body);
|
|
expect(result).toEqual({
|
|
totalTokenCount: 1000,
|
|
promptTokenCount: undefined,
|
|
candidatesTokenCount: undefined,
|
|
cachedContentTokenCount: undefined,
|
|
});
|
|
});
|
|
|
|
it("filters non-finite numbers", () => {
|
|
const body = {
|
|
response: {
|
|
usageMetadata: {
|
|
totalTokenCount: Infinity,
|
|
promptTokenCount: NaN,
|
|
candidatesTokenCount: 100,
|
|
},
|
|
},
|
|
};
|
|
const result = extractUsageMetadata(body);
|
|
expect(result?.totalTokenCount).toBeUndefined();
|
|
expect(result?.promptTokenCount).toBeUndefined();
|
|
expect(result?.candidatesTokenCount).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe("extractUsageFromSsePayload", () => {
|
|
it("extracts usage from SSE data line", () => {
|
|
const payload = `data: {"response": {"usageMetadata": {"totalTokenCount": 500}}}`;
|
|
const result = extractUsageFromSsePayload(payload);
|
|
expect(result?.totalTokenCount).toBe(500);
|
|
});
|
|
|
|
it("handles multiple SSE lines", () => {
|
|
const payload = `data: {"response": {}}
|
|
data: {"response": {"usageMetadata": {"totalTokenCount": 1000}}}`;
|
|
const result = extractUsageFromSsePayload(payload);
|
|
expect(result?.totalTokenCount).toBe(1000);
|
|
});
|
|
|
|
it("returns null when no usage found", () => {
|
|
const payload = `data: {"response": {"text": "hello"}}`;
|
|
const result = extractUsageFromSsePayload(payload);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("ignores non-data lines", () => {
|
|
const payload = `: keepalive
|
|
event: message
|
|
data: {"response": {"usageMetadata": {"totalTokenCount": 200}}}`;
|
|
const result = extractUsageFromSsePayload(payload);
|
|
expect(result?.totalTokenCount).toBe(200);
|
|
});
|
|
|
|
it("handles malformed JSON gracefully", () => {
|
|
const payload = `data: not json
|
|
data: {"response": {"usageMetadata": {"totalTokenCount": 300}}}`;
|
|
const result = extractUsageFromSsePayload(payload);
|
|
expect(result?.totalTokenCount).toBe(300);
|
|
});
|
|
});
|
|
|
|
describe("rewriteAntigravityPreviewAccessError", () => {
|
|
it("returns null for non-404 status", () => {
|
|
const body = { error: { message: "Not found" } };
|
|
expect(rewriteAntigravityPreviewAccessError(body, 400)).toBeNull();
|
|
expect(rewriteAntigravityPreviewAccessError(body, 500)).toBeNull();
|
|
});
|
|
|
|
it("rewrites error for Antigravity model on 404", () => {
|
|
const body = { error: { message: "Model not found" } };
|
|
const result = rewriteAntigravityPreviewAccessError(body, 404, "claude-opus");
|
|
expect(result?.error?.message).toContain("Model not found");
|
|
expect(result?.error?.message).toContain("preview access");
|
|
});
|
|
|
|
it("rewrites error when error message contains antigravity", () => {
|
|
const body = { error: { message: "antigravity model unavailable" } };
|
|
const result = rewriteAntigravityPreviewAccessError(body, 404);
|
|
expect(result?.error?.message).toContain("preview access");
|
|
});
|
|
|
|
it("returns null for 404 with non-antigravity model", () => {
|
|
const body = { error: { message: "Model not found" } };
|
|
const result = rewriteAntigravityPreviewAccessError(body, 404, "gemini-pro");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("provides default message when error message is empty", () => {
|
|
const body = { error: { message: "" } };
|
|
const result = rewriteAntigravityPreviewAccessError(body, 404, "opus-model");
|
|
expect(result?.error?.message).toContain("Antigravity preview features are not enabled");
|
|
});
|
|
|
|
it("detects Claude models in requested model name", () => {
|
|
const body = { error: {} };
|
|
const result = rewriteAntigravityPreviewAccessError(body, 404, "claude-3-sonnet");
|
|
expect(result?.error?.message).toContain("preview access");
|
|
});
|
|
});
|