mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 23:28:27 +00:00
refactor!: remove google-antigravity provider support
This commit is contained in:
@@ -55,7 +55,7 @@ function isProfileConfigCompatible(params: {
|
||||
}
|
||||
|
||||
function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string {
|
||||
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||
const needsProjectId = provider === "google-gemini-cli";
|
||||
return needsProjectId
|
||||
? JSON.stringify({
|
||||
token: credentials.access,
|
||||
|
||||
@@ -56,10 +56,6 @@ export function isModernModelRef(ref: ModelRef): boolean {
|
||||
return matchesPrefix(id, GOOGLE_PREFIXES);
|
||||
}
|
||||
|
||||
if (provider === "google-antigravity") {
|
||||
return matchesPrefix(id, GOOGLE_PREFIXES) || matchesPrefix(id, ANTHROPIC_PREFIXES);
|
||||
}
|
||||
|
||||
if (provider === "zai") {
|
||||
return matchesPrefix(id, ZAI_PREFIXES);
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveForwardCompatModel } from "./model-forward-compat.js";
|
||||
import type { ModelRegistry } from "./pi-model-discovery.js";
|
||||
|
||||
function makeRegistry(): ModelRegistry {
|
||||
const templates = new Map<string, Model<Api>>();
|
||||
templates.set("google-antigravity/gemini-3-pro-high", {
|
||||
id: "gemini-3-pro-high",
|
||||
name: "Gemini 3 Pro High",
|
||||
provider: "google-antigravity",
|
||||
api: "google-antigravity",
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
reasoning: true,
|
||||
} as Model<Api>);
|
||||
templates.set("google-antigravity/gemini-3-pro-low", {
|
||||
id: "gemini-3-pro-low",
|
||||
name: "Gemini 3 Pro Low",
|
||||
provider: "google-antigravity",
|
||||
api: "google-antigravity",
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
reasoning: true,
|
||||
} as Model<Api>);
|
||||
|
||||
const registry = {
|
||||
find: (provider: string, modelId: string) => templates.get(`${provider}/${modelId}`) ?? null,
|
||||
} as unknown as ModelRegistry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
describe("resolveForwardCompatModel (google-antigravity Gemini 3.1)", () => {
|
||||
it("resolves gemini-3-1-pro-high from gemini-3-pro-high template", () => {
|
||||
const model = resolveForwardCompatModel(
|
||||
"google-antigravity",
|
||||
"gemini-3-1-pro-high",
|
||||
makeRegistry(),
|
||||
);
|
||||
expect(model?.provider).toBe("google-antigravity");
|
||||
expect(model?.id).toBe("gemini-3-1-pro-high");
|
||||
});
|
||||
|
||||
it("resolves gemini-3-1-pro-low from gemini-3-pro-low template", () => {
|
||||
const model = resolveForwardCompatModel(
|
||||
"google-antigravity",
|
||||
"gemini-3-1-pro-low",
|
||||
makeRegistry(),
|
||||
);
|
||||
expect(model?.provider).toBe("google-antigravity");
|
||||
expect(model?.id).toBe("gemini-3-1-pro-low");
|
||||
});
|
||||
|
||||
it("supports dot-notation model ids", () => {
|
||||
const high = resolveForwardCompatModel(
|
||||
"google-antigravity",
|
||||
"gemini-3.1-pro-high",
|
||||
makeRegistry(),
|
||||
);
|
||||
const low = resolveForwardCompatModel(
|
||||
"google-antigravity",
|
||||
"gemini-3.1-pro-low",
|
||||
makeRegistry(),
|
||||
);
|
||||
expect(high?.id).toBe("gemini-3.1-pro-high");
|
||||
expect(low?.id).toBe("gemini-3.1-pro-low");
|
||||
});
|
||||
});
|
||||
@@ -17,51 +17,6 @@ const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet
|
||||
const ZAI_GLM5_MODEL_ID = "glm-5";
|
||||
const ZAI_GLM5_TEMPLATE_MODEL_IDS = ["glm-4.7"] as const;
|
||||
|
||||
const ANTIGRAVITY_OPUS_46_MODEL_ID = "claude-opus-4-6";
|
||||
const ANTIGRAVITY_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
|
||||
const ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const;
|
||||
const ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID = "claude-opus-4-6-thinking";
|
||||
const ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID = "claude-opus-4.6-thinking";
|
||||
const ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS = [
|
||||
"claude-opus-4-5-thinking",
|
||||
"claude-opus-4.5-thinking",
|
||||
] as const;
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID = "gemini-3-1-pro-high";
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID = "gemini-3.1-pro-high";
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID = "gemini-3-1-pro-low";
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID = "gemini-3.1-pro-low";
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_HIGH_TEMPLATE_MODEL_IDS = ["gemini-3-pro-high"] as const;
|
||||
const ANTIGRAVITY_GEMINI_31_PRO_LOW_TEMPLATE_MODEL_IDS = ["gemini-3-pro-low"] as const;
|
||||
|
||||
export const ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES = [
|
||||
{
|
||||
id: ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID,
|
||||
templatePrefixes: [
|
||||
"google-antigravity/claude-opus-4-5-thinking",
|
||||
"google-antigravity/claude-opus-4.5-thinking",
|
||||
],
|
||||
availabilityAliasIds: [] as const,
|
||||
},
|
||||
{
|
||||
id: ANTIGRAVITY_OPUS_46_MODEL_ID,
|
||||
templatePrefixes: ["google-antigravity/claude-opus-4-5", "google-antigravity/claude-opus-4.5"],
|
||||
availabilityAliasIds: [] as const,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const ANTIGRAVITY_GEMINI_31_FORWARD_COMPAT_CANDIDATES = [
|
||||
{
|
||||
id: ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID,
|
||||
templatePrefixes: ["google-antigravity/gemini-3-pro-high"],
|
||||
availabilityAliasIds: [ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID],
|
||||
},
|
||||
{
|
||||
id: ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID,
|
||||
templatePrefixes: ["google-antigravity/gemini-3-pro-low"],
|
||||
availabilityAliasIds: [ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID],
|
||||
},
|
||||
] as const;
|
||||
|
||||
function cloneFirstTemplateModel(params: {
|
||||
normalizedProvider: string;
|
||||
trimmedModelId: string;
|
||||
@@ -245,94 +200,6 @@ function resolveZaiGlm5ForwardCompatModel(
|
||||
} as Model<Api>);
|
||||
}
|
||||
|
||||
function resolveAntigravityOpus46ForwardCompatModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
modelRegistry: ModelRegistry,
|
||||
): Model<Api> | undefined {
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
if (normalizedProvider !== "google-antigravity") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmedModelId = modelId.trim();
|
||||
const lower = trimmedModelId.toLowerCase();
|
||||
const isOpus46 =
|
||||
lower === ANTIGRAVITY_OPUS_46_MODEL_ID ||
|
||||
lower === ANTIGRAVITY_OPUS_46_DOT_MODEL_ID ||
|
||||
lower.startsWith(`${ANTIGRAVITY_OPUS_46_MODEL_ID}-`) ||
|
||||
lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_MODEL_ID}-`);
|
||||
const isOpus46Thinking =
|
||||
lower === ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID ||
|
||||
lower === ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID ||
|
||||
lower.startsWith(`${ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID}-`) ||
|
||||
lower.startsWith(`${ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID}-`);
|
||||
if (!isOpus46 && !isOpus46Thinking) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const templateIds: string[] = [];
|
||||
if (lower.startsWith(ANTIGRAVITY_OPUS_46_MODEL_ID)) {
|
||||
templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_MODEL_ID, "claude-opus-4-5"));
|
||||
}
|
||||
if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID)) {
|
||||
templateIds.push(lower.replace(ANTIGRAVITY_OPUS_46_DOT_MODEL_ID, "claude-opus-4.5"));
|
||||
}
|
||||
if (lower.startsWith(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID)) {
|
||||
templateIds.push(
|
||||
lower.replace(ANTIGRAVITY_OPUS_46_THINKING_MODEL_ID, "claude-opus-4-5-thinking"),
|
||||
);
|
||||
}
|
||||
if (lower.startsWith(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID)) {
|
||||
templateIds.push(
|
||||
lower.replace(ANTIGRAVITY_OPUS_46_DOT_THINKING_MODEL_ID, "claude-opus-4.5-thinking"),
|
||||
);
|
||||
}
|
||||
templateIds.push(...ANTIGRAVITY_OPUS_TEMPLATE_MODEL_IDS);
|
||||
templateIds.push(...ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS);
|
||||
|
||||
return cloneFirstTemplateModel({
|
||||
normalizedProvider,
|
||||
trimmedModelId,
|
||||
templateIds,
|
||||
modelRegistry,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAntigravityGemini31ForwardCompatModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
modelRegistry: ModelRegistry,
|
||||
): Model<Api> | undefined {
|
||||
const normalizedProvider = normalizeProviderId(provider);
|
||||
if (normalizedProvider !== "google-antigravity") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmedModelId = modelId.trim();
|
||||
const lower = trimmedModelId.toLowerCase();
|
||||
const isGemini31High =
|
||||
lower === ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID ||
|
||||
lower === ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID;
|
||||
const isGemini31Low =
|
||||
lower === ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID ||
|
||||
lower === ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID;
|
||||
if (!isGemini31High && !isGemini31Low) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const templateIds = isGemini31High
|
||||
? [...ANTIGRAVITY_GEMINI_31_PRO_HIGH_TEMPLATE_MODEL_IDS]
|
||||
: [...ANTIGRAVITY_GEMINI_31_PRO_LOW_TEMPLATE_MODEL_IDS];
|
||||
|
||||
return cloneFirstTemplateModel({
|
||||
normalizedProvider,
|
||||
trimmedModelId,
|
||||
templateIds,
|
||||
modelRegistry,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveForwardCompatModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
@@ -342,8 +209,6 @@ export function resolveForwardCompatModel(
|
||||
resolveOpenAICodexGpt53FallbackModel(provider, modelId, modelRegistry) ??
|
||||
resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ??
|
||||
resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ??
|
||||
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ??
|
||||
resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) ??
|
||||
resolveAntigravityGemini31ForwardCompatModel(provider, modelId, modelRegistry)
|
||||
resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
import { sanitizeGoogleTurnOrdering } from "./bootstrap.js";
|
||||
|
||||
export function isGoogleModelApi(api?: string | null): boolean {
|
||||
return (
|
||||
api === "google-gemini-cli" || api === "google-generative-ai" || api === "google-antigravity"
|
||||
);
|
||||
}
|
||||
|
||||
export function isAntigravityClaude(params: {
|
||||
api?: string | null;
|
||||
provider?: string | null;
|
||||
modelId?: string;
|
||||
}): boolean {
|
||||
const provider = params.provider?.toLowerCase();
|
||||
const api = params.api?.toLowerCase();
|
||||
if (provider !== "google-antigravity" && api !== "google-antigravity") {
|
||||
return false;
|
||||
}
|
||||
return params.modelId?.toLowerCase().includes("claude") ?? false;
|
||||
return api === "google-gemini-cli" || api === "google-generative-ai";
|
||||
}
|
||||
|
||||
export { sanitizeGoogleTurnOrdering };
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { sanitizeSessionHistory } from "./pi-embedded-runner/google.js";
|
||||
|
||||
type AssistantContentBlock = {
|
||||
type?: string;
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
thinkingSignature?: string;
|
||||
thought_signature?: string;
|
||||
thoughtSignature?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: unknown;
|
||||
};
|
||||
|
||||
function getAssistantMessage(out: AgentMessage[]) {
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as
|
||||
| { content?: AssistantContentBlock[] }
|
||||
| undefined;
|
||||
if (!assistant) {
|
||||
throw new Error("Expected assistant message in sanitized history");
|
||||
}
|
||||
return assistant;
|
||||
}
|
||||
|
||||
async function sanitizeGoogleAssistantWithContent(content: unknown[]) {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const input = [
|
||||
{
|
||||
role: "user",
|
||||
content: "hi",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content,
|
||||
},
|
||||
] as unknown as AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: "google-antigravity",
|
||||
sessionManager,
|
||||
sessionId: "session:google",
|
||||
});
|
||||
|
||||
return getAssistantMessage(out);
|
||||
}
|
||||
|
||||
async function sanitizeSimpleSession(params: {
|
||||
modelApi: string;
|
||||
sessionId: string;
|
||||
content: unknown[];
|
||||
modelId?: string;
|
||||
provider?: string;
|
||||
}) {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const input = [
|
||||
{
|
||||
role: "user",
|
||||
content: "hi",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: params.content,
|
||||
},
|
||||
] as unknown as AgentMessage[];
|
||||
|
||||
return sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: params.modelApi,
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
sessionManager,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
function geminiThoughtSignatureInput() {
|
||||
return [
|
||||
{ type: "text", text: "hello", thought_signature: "msg_abc123" },
|
||||
{ type: "thinking", thinking: "ok", thought_signature: "c2ln" },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_1",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/foo" },
|
||||
thoughtSignature: '{"id":1}',
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_2",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/bar" },
|
||||
thoughtSignature: "c2ln",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
it("keeps thinking blocks without signatures for Google models", async () => {
|
||||
const assistant = await sanitizeGoogleAssistantWithContent([
|
||||
{ type: "thinking", thinking: "reasoning" },
|
||||
]);
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
});
|
||||
|
||||
it("keeps thinking blocks with signatures for Google models", async () => {
|
||||
const assistant = await sanitizeGoogleAssistantWithContent([
|
||||
{ type: "thinking", thinking: "reasoning", thinkingSignature: "sig" },
|
||||
]);
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
expect(assistant.content?.[0]?.thinkingSignature).toBe("sig");
|
||||
});
|
||||
|
||||
it("keeps thinking blocks with Anthropic-style signatures for Google models", async () => {
|
||||
const assistant = await sanitizeGoogleAssistantWithContent([
|
||||
{ type: "thinking", thinking: "reasoning", signature: "sig" },
|
||||
]);
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
});
|
||||
|
||||
it("converts unsigned thinking blocks to text for Antigravity Claude", async () => {
|
||||
const out = await sanitizeSimpleSession({
|
||||
modelApi: "google-antigravity",
|
||||
modelId: "anthropic/claude-3.5-sonnet",
|
||||
sessionId: "session:antigravity-claude",
|
||||
content: [{ type: "thinking", thinking: "reasoning" }],
|
||||
});
|
||||
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; text?: string }>;
|
||||
};
|
||||
expect(assistant.content).toEqual([{ type: "text", text: "reasoning" }]);
|
||||
});
|
||||
|
||||
it("maps base64 signatures to thinkingSignature for Antigravity Claude", async () => {
|
||||
const out = await sanitizeSimpleSession({
|
||||
modelApi: "google-antigravity",
|
||||
modelId: "anthropic/claude-3.5-sonnet",
|
||||
sessionId: "session:antigravity-claude",
|
||||
content: [{ type: "thinking", thinking: "reasoning", signature: "c2ln" }],
|
||||
});
|
||||
|
||||
const assistant = getAssistantMessage(out);
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
expect(assistant.content?.[0]?.thinkingSignature).toBe("c2ln");
|
||||
});
|
||||
|
||||
it("preserves order for mixed assistant content", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const input = [
|
||||
{
|
||||
role: "user",
|
||||
content: "hi",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "hello" },
|
||||
{ type: "thinking", thinking: "internal note" },
|
||||
{ type: "text", text: "world" },
|
||||
],
|
||||
},
|
||||
] as unknown as AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: "google-antigravity",
|
||||
sessionManager,
|
||||
sessionId: "session:google-mixed",
|
||||
});
|
||||
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; text?: string; thinking?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["text", "thinking", "text"]);
|
||||
expect(assistant.content?.[1]?.thinking).toBe("internal note");
|
||||
});
|
||||
|
||||
it("strips non-base64 thought signatures for OpenRouter Gemini", async () => {
|
||||
const out = await sanitizeSimpleSession({
|
||||
modelApi: "openrouter",
|
||||
provider: "openrouter",
|
||||
modelId: "google/gemini-1.5-pro",
|
||||
sessionId: "session:openrouter-gemini",
|
||||
content: geminiThoughtSignatureInput(),
|
||||
});
|
||||
|
||||
const assistant = getAssistantMessage(out);
|
||||
expect(assistant.content).toEqual([
|
||||
{ type: "text", text: "hello" },
|
||||
{ type: "thinking", thinking: "ok", thought_signature: "c2ln" },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_1",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/foo" },
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_2",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/bar" },
|
||||
thoughtSignature: "c2ln",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips non-base64 thought signatures for native Google Gemini", async () => {
|
||||
const out = await sanitizeSimpleSession({
|
||||
modelApi: "google-generative-ai",
|
||||
provider: "google",
|
||||
modelId: "gemini-2.0-flash",
|
||||
sessionId: "session:google-gemini",
|
||||
content: geminiThoughtSignatureInput(),
|
||||
});
|
||||
|
||||
const assistant = getAssistantMessage(out);
|
||||
expect(assistant.content).toEqual([
|
||||
{ type: "text", text: "hello" },
|
||||
{ type: "thinking", thinking: "ok", thought_signature: "c2ln" },
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call1",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/foo" },
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call2",
|
||||
name: "read",
|
||||
arguments: { path: "/tmp/bar" },
|
||||
thoughtSignature: "c2ln",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps mixed signed/unsigned thinking blocks for Google models", async () => {
|
||||
const assistant = await sanitizeGoogleAssistantWithContent([
|
||||
{ type: "thinking", thinking: "signed", thinkingSignature: "sig" },
|
||||
{ type: "thinking", thinking: "unsigned" },
|
||||
]);
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking", "thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("signed");
|
||||
expect(assistant.content?.[1]?.thinking).toBe("unsigned");
|
||||
});
|
||||
|
||||
it("keeps empty thinking blocks for Google models", async () => {
|
||||
const assistant = await sanitizeGoogleAssistantWithContent([
|
||||
{ type: "thinking", thinking: " " },
|
||||
]);
|
||||
expect(assistant?.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
});
|
||||
|
||||
it("keeps thinking blocks for non-Google models", async () => {
|
||||
const out = await sanitizeSimpleSession({
|
||||
modelApi: "openai",
|
||||
sessionId: "session:openai",
|
||||
content: [{ type: "thinking", thinking: "reasoning" }],
|
||||
});
|
||||
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
});
|
||||
|
||||
it("sanitizes tool call ids for Google APIs", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const longId = `call_${"a".repeat(60)}`;
|
||||
const input = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: longId, name: "read", arguments: {} }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: longId,
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
},
|
||||
] as unknown as AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: "google-antigravity",
|
||||
sessionManager,
|
||||
sessionId: "session:google",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: unknown }).role === "assistant",
|
||||
) as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const toolCall = assistant.content?.[0] as { id?: string };
|
||||
expect(toolCall.id).toBeDefined();
|
||||
expect(toolCall.id?.length).toBeLessThanOrEqual(40);
|
||||
|
||||
const toolResult = out.find(
|
||||
(msg) => (msg as { role?: unknown }).role === "toolResult",
|
||||
) as Extract<AgentMessage, { role: "toolResult" }>;
|
||||
expect(toolResult.toolCallId).toBe(toolCall.id);
|
||||
});
|
||||
});
|
||||
@@ -42,26 +42,6 @@ describe("sanitizeToolsForGoogle", () => {
|
||||
expectFormatRemoved(sanitized, "additionalProperties");
|
||||
});
|
||||
|
||||
it("strips unsupported schema keywords for google-antigravity", () => {
|
||||
const tool = createTool({
|
||||
type: "object",
|
||||
patternProperties: {
|
||||
"^x-": { type: "string" },
|
||||
},
|
||||
properties: {
|
||||
foo: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
},
|
||||
},
|
||||
});
|
||||
const [sanitized] = sanitizeToolsForGoogle({
|
||||
tools: [tool],
|
||||
provider: "google-antigravity",
|
||||
});
|
||||
expectFormatRemoved(sanitized, "patternProperties");
|
||||
});
|
||||
|
||||
it("returns original tools for non-google providers", () => {
|
||||
const tool = createTool({
|
||||
type: "object",
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import type { TranscriptPolicy } from "../transcript-policy.js";
|
||||
import { resolveTranscriptPolicy } from "../transcript-policy.js";
|
||||
import { log } from "./logger.js";
|
||||
import { dropThinkingBlocks, isAssistantMessageWithContent } from "./thinking.js";
|
||||
import { dropThinkingBlocks } from "./thinking.js";
|
||||
import { describeUnknownError } from "./utils.js";
|
||||
|
||||
const GOOGLE_TURN_ORDERING_CUSTOM_TYPE = "google-turn-ordering-bootstrap";
|
||||
@@ -52,85 +52,8 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
|
||||
"maxProperties",
|
||||
]);
|
||||
|
||||
const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||
const INTER_SESSION_PREFIX_BASE = "[Inter-session message]";
|
||||
|
||||
function isValidAntigravitySignature(value: unknown): value is string {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (trimmed.length % 4 !== 0) {
|
||||
return false;
|
||||
}
|
||||
return ANTIGRAVITY_SIGNATURE_RE.test(trimmed);
|
||||
}
|
||||
|
||||
export function sanitizeAntigravityThinkingBlocks(messages: AgentMessage[]): AgentMessage[] {
|
||||
let touched = false;
|
||||
const out: AgentMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!isAssistantMessageWithContent(msg)) {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
const assistant = msg;
|
||||
type AssistantContentBlock = Extract<AgentMessage, { role: "assistant" }>["content"][number];
|
||||
const nextContent: AssistantContentBlock[] = [];
|
||||
let contentChanged = false;
|
||||
for (const block of assistant.content) {
|
||||
if (
|
||||
!block ||
|
||||
typeof block !== "object" ||
|
||||
(block as { type?: unknown }).type !== "thinking"
|
||||
) {
|
||||
nextContent.push(block);
|
||||
continue;
|
||||
}
|
||||
const rec = block as {
|
||||
thinkingSignature?: unknown;
|
||||
signature?: unknown;
|
||||
thought_signature?: unknown;
|
||||
thoughtSignature?: unknown;
|
||||
};
|
||||
const candidate =
|
||||
rec.thinkingSignature ?? rec.signature ?? rec.thought_signature ?? rec.thoughtSignature;
|
||||
if (!isValidAntigravitySignature(candidate)) {
|
||||
// Preserve reasoning content as plain text when signatures are invalid/missing.
|
||||
// Antigravity Claude rejects unsigned thinking blocks, but dropping them loses context.
|
||||
const thinkingText = (block as { thinking?: unknown }).thinking;
|
||||
if (typeof thinkingText === "string" && thinkingText.trim()) {
|
||||
nextContent.push({ type: "text", text: thinkingText } as AssistantContentBlock);
|
||||
}
|
||||
contentChanged = true;
|
||||
continue;
|
||||
}
|
||||
if (rec.thinkingSignature !== candidate) {
|
||||
const nextBlock = {
|
||||
...(block as unknown as Record<string, unknown>),
|
||||
thinkingSignature: candidate,
|
||||
} as AssistantContentBlock;
|
||||
nextContent.push(nextBlock);
|
||||
contentChanged = true;
|
||||
} else {
|
||||
nextContent.push(block);
|
||||
}
|
||||
}
|
||||
if (contentChanged) {
|
||||
touched = true;
|
||||
}
|
||||
if (nextContent.length === 0) {
|
||||
touched = true;
|
||||
continue;
|
||||
}
|
||||
out.push(contentChanged ? { ...assistant, content: nextContent } : msg);
|
||||
}
|
||||
return touched ? out : messages;
|
||||
}
|
||||
|
||||
function buildInterSessionPrefix(message: AgentMessage): string {
|
||||
const provenance = normalizeInputProvenance((message as { provenance?: unknown }).provenance);
|
||||
if (!provenance) {
|
||||
@@ -284,7 +207,7 @@ export function sanitizeToolsForGoogle<
|
||||
// AND Claude models. This field does not support JSON Schema keywords such as
|
||||
// patternProperties, additionalProperties, $ref, etc. We must clean schemas
|
||||
// for every provider that routes through this path.
|
||||
if (params.provider !== "google-gemini-cli" && params.provider !== "google-antigravity") {
|
||||
if (params.provider !== "google-gemini-cli") {
|
||||
return params.tools;
|
||||
}
|
||||
return params.tools.map((tool) => {
|
||||
@@ -301,7 +224,7 @@ export function sanitizeToolsForGoogle<
|
||||
}
|
||||
|
||||
export function logToolSchemasForGoogle(params: { tools: AgentTool[]; provider: string }) {
|
||||
if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") {
|
||||
if (params.provider !== "google-gemini-cli") {
|
||||
return;
|
||||
}
|
||||
const toolNames = params.tools.map((tool, index) => `${index}:${tool.name}`);
|
||||
@@ -481,10 +404,7 @@ export async function sanitizeSessionHistory(params: {
|
||||
const droppedThinking = policy.dropThinkingBlocks
|
||||
? dropThinkingBlocks(sanitizedImages)
|
||||
: sanitizedImages;
|
||||
const sanitizedThinking = policy.sanitizeThinkingSignatures
|
||||
? sanitizeAntigravityThinkingBlocks(droppedThinking)
|
||||
: droppedThinking;
|
||||
const sanitizedToolCalls = sanitizeToolCallInputs(sanitizedThinking, {
|
||||
const sanitizedToolCalls = sanitizeToolCallInputs(droppedThinking, {
|
||||
allowedToolNames: params.allowedToolNames,
|
||||
});
|
||||
const repairedTools = policy.repairToolUseResultPairing
|
||||
|
||||
@@ -232,62 +232,6 @@ describe("resolveModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an antigravity forward-compat fallback for claude-opus-4-6-thinking", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "google-antigravity",
|
||||
modelId: "claude-opus-4-5-thinking",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "claude-opus-4-5-thinking",
|
||||
name: "Claude Opus 4.5 Thinking",
|
||||
provider: "google-antigravity",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
}),
|
||||
});
|
||||
|
||||
expectResolvedForwardCompatFallback({
|
||||
provider: "google-antigravity",
|
||||
id: "claude-opus-4-6-thinking",
|
||||
expectedModel: {
|
||||
provider: "google-antigravity",
|
||||
id: "claude-opus-4-6-thinking",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an antigravity forward-compat fallback for claude-opus-4-6", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "google-antigravity",
|
||||
modelId: "claude-opus-4-5",
|
||||
templateModel: buildForwardCompatTemplate({
|
||||
id: "claude-opus-4-5",
|
||||
name: "Claude Opus 4.5",
|
||||
provider: "google-antigravity",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
}),
|
||||
});
|
||||
|
||||
expectResolvedForwardCompatFallback({
|
||||
provider: "google-antigravity",
|
||||
id: "claude-opus-4-6",
|
||||
expectedModel: {
|
||||
provider: "google-antigravity",
|
||||
id: "claude-opus-4-6",
|
||||
api: "google-gemini-cli",
|
||||
baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
reasoning: true,
|
||||
contextWindow: 200000,
|
||||
maxTokens: 64000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("builds a zai forward-compat fallback for glm-5", () => {
|
||||
mockDiscoveredModel({
|
||||
provider: "zai",
|
||||
|
||||
@@ -82,7 +82,6 @@ import { buildEmbeddedExtensionFactories } from "../extensions.js";
|
||||
import { applyExtraParamsToAgent } from "../extra-params.js";
|
||||
import {
|
||||
logToolSchemasForGoogle,
|
||||
sanitizeAntigravityThinkingBlocks,
|
||||
sanitizeSessionHistory,
|
||||
sanitizeToolsForGoogle,
|
||||
} from "../google.js";
|
||||
@@ -1062,10 +1061,7 @@ export async function runEmbeddedAttempt(
|
||||
sessionManager.resetLeaf();
|
||||
}
|
||||
const sessionContext = sessionManager.buildSessionContext();
|
||||
const sanitizedOrphan = transcriptPolicy.sanitizeThinkingSignatures
|
||||
? sanitizeAntigravityThinkingBlocks(sessionContext.messages)
|
||||
: sessionContext.messages;
|
||||
activeSession.agent.replaceMessages(sanitizedOrphan);
|
||||
activeSession.agent.replaceMessages(sessionContext.messages);
|
||||
log.warn(
|
||||
`Removed orphaned user message to prevent consecutive user turns. ` +
|
||||
`runId=${params.runId} sessionId=${params.sessionId}`,
|
||||
|
||||
@@ -78,16 +78,14 @@ export function normalizeToolParameters(
|
||||
// - Gemini rejects several JSON Schema keywords, so we scrub those.
|
||||
// - OpenAI rejects function tool schemas unless the *top-level* is `type: "object"`.
|
||||
// (TypeBox root unions compile to `{ anyOf: [...] }` without `type`).
|
||||
// - Anthropic (google-antigravity) expects full JSON Schema draft 2020-12 compliance.
|
||||
// - Anthropic expects full JSON Schema draft 2020-12 compliance.
|
||||
//
|
||||
// Normalize once here so callers can always pass `tools` through unchanged.
|
||||
|
||||
const isGeminiProvider =
|
||||
options?.modelProvider?.toLowerCase().includes("google") ||
|
||||
options?.modelProvider?.toLowerCase().includes("gemini");
|
||||
const isAnthropicProvider =
|
||||
options?.modelProvider?.toLowerCase().includes("anthropic") ||
|
||||
options?.modelProvider?.toLowerCase().includes("google-antigravity");
|
||||
const isAnthropicProvider = options?.modelProvider?.toLowerCase().includes("anthropic");
|
||||
|
||||
// If schema already has type + properties (no top-level anyOf to merge),
|
||||
// clean it for Gemini compatibility (but only if using Gemini, not Anthropic)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
import { isAntigravityClaude, isGoogleModelApi } from "./pi-embedded-helpers/google.js";
|
||||
import { isGoogleModelApi } from "./pi-embedded-helpers/google.js";
|
||||
import type { ToolCallIdMode } from "./tool-call-id.js";
|
||||
|
||||
export type TranscriptSanitizeMode = "full" | "images-only";
|
||||
@@ -88,12 +88,6 @@ export function resolveTranscriptPolicy(params: {
|
||||
const isOpenRouterGemini =
|
||||
(provider === "openrouter" || provider === "opencode") &&
|
||||
modelId.toLowerCase().includes("gemini");
|
||||
const isAntigravityClaudeModel = isAntigravityClaude({
|
||||
api: params.modelApi,
|
||||
provider,
|
||||
modelId,
|
||||
});
|
||||
|
||||
const isCopilotClaude = provider === "github-copilot" && modelId.toLowerCase().includes("claude");
|
||||
|
||||
// GitHub Copilot's Claude endpoints can reject persisted `thinking` blocks with
|
||||
@@ -112,16 +106,15 @@ export function resolveTranscriptPolicy(params: {
|
||||
const repairToolUseResultPairing = isGoogle || isAnthropic;
|
||||
const sanitizeThoughtSignatures =
|
||||
isOpenRouterGemini || isGoogle ? { allowBase64Only: true, includeCamelCase: true } : undefined;
|
||||
const sanitizeThinkingSignatures = isAntigravityClaudeModel;
|
||||
|
||||
return {
|
||||
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
||||
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
|
||||
toolCallIdMode,
|
||||
repairToolUseResultPairing: !isOpenAi && repairToolUseResultPairing,
|
||||
preserveSignatures: isAntigravityClaudeModel,
|
||||
preserveSignatures: false,
|
||||
sanitizeThoughtSignatures: isOpenAi ? undefined : sanitizeThoughtSignatures,
|
||||
sanitizeThinkingSignatures,
|
||||
sanitizeThinkingSignatures: false,
|
||||
dropThinkingBlocks,
|
||||
applyGoogleTurnOrdering: !isOpenAi && isGoogle,
|
||||
validateGeminiTurns: !isOpenAi && isGoogle,
|
||||
|
||||
Reference in New Issue
Block a user