mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 19:00:44 +00:00
Avoid cross-provider thought signatures (#654)
* Avoid cross-provider thought signatures * Fix Google thought signature replay Filter thought signatures to same provider with base64 validation and rename the transform helper for clarity.
This commit is contained in:
parent
6f3ba88733
commit
934e7e470b
6 changed files with 28 additions and 9 deletions
|
|
@ -26,7 +26,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
|
|
||||||
import { transformMessages } from "./transorm-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
// Stealth mode: Mimic Claude Code's tool naming exactly
|
// Stealth mode: Mimic Claude Code's tool naming exactly
|
||||||
const claudeCodeVersion = "2.1.2";
|
const claudeCodeVersion = "2.1.2";
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { type Content, FinishReason, FunctionCallingConfigMode, type Part, type Schema } from "@google/genai";
|
import { type Content, FinishReason, FunctionCallingConfigMode, type Part, type Schema } from "@google/genai";
|
||||||
import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.js";
|
import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
import { transformMessages } from "./transorm-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
|
type GoogleApiType = "google-generative-ai" | "google-gemini-cli" | "google-vertex";
|
||||||
|
|
||||||
|
|
@ -42,6 +42,22 @@ export function retainThoughtSignature(existing: string | undefined, incoming: s
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thought signatures must be base64 for Google APIs (TYPE_BYTES).
|
||||||
|
const base64SignaturePattern = /^[A-Za-z0-9+/]+={0,2}$/;
|
||||||
|
|
||||||
|
function isValidThoughtSignature(signature: string | undefined): boolean {
|
||||||
|
if (!signature) return false;
|
||||||
|
if (signature.length % 4 !== 0) return false;
|
||||||
|
return base64SignaturePattern.test(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only keep signatures from the same provider/model and with valid base64.
|
||||||
|
*/
|
||||||
|
function resolveThoughtSignature(isSameProviderAndModel: boolean, signature: string | undefined): string | undefined {
|
||||||
|
return isSameProviderAndModel && isValidThoughtSignature(signature) ? signature : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert internal messages to Gemini Content[] format.
|
* Convert internal messages to Gemini Content[] format.
|
||||||
*/
|
*/
|
||||||
|
|
@ -85,9 +101,10 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
// Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity)
|
// Skip empty text blocks - they can cause issues with some models (e.g. Claude via Antigravity)
|
||||||
if (!block.text || block.text.trim() === "") continue;
|
if (!block.text || block.text.trim() === "") continue;
|
||||||
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.textSignature);
|
||||||
parts.push({
|
parts.push({
|
||||||
text: sanitizeSurrogates(block.text),
|
text: sanitizeSurrogates(block.text),
|
||||||
...(block.textSignature && { thoughtSignature: block.textSignature }),
|
...(thoughtSignature && { thoughtSignature }),
|
||||||
});
|
});
|
||||||
} else if (block.type === "thinking") {
|
} else if (block.type === "thinking") {
|
||||||
// Skip empty thinking blocks
|
// Skip empty thinking blocks
|
||||||
|
|
@ -95,10 +112,11 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||||
// Only keep as thinking block if same provider AND same model
|
// Only keep as thinking block if same provider AND same model
|
||||||
// Otherwise convert to plain text (no tags to avoid model mimicking them)
|
// Otherwise convert to plain text (no tags to avoid model mimicking them)
|
||||||
if (isSameProviderAndModel) {
|
if (isSameProviderAndModel) {
|
||||||
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thinkingSignature);
|
||||||
parts.push({
|
parts.push({
|
||||||
thought: true,
|
thought: true,
|
||||||
text: sanitizeSurrogates(block.thinking),
|
text: sanitizeSurrogates(block.thinking),
|
||||||
...(block.thinkingSignature && { thoughtSignature: block.thinkingSignature }),
|
...(thoughtSignature && { thoughtSignature }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
parts.push({
|
parts.push({
|
||||||
|
|
@ -112,8 +130,9 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
||||||
args: block.arguments,
|
args: block.arguments,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (block.thoughtSignature) {
|
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
|
||||||
part.thoughtSignature = block.thoughtSignature;
|
if (thoughtSignature) {
|
||||||
|
part.thoughtSignature = thoughtSignature;
|
||||||
}
|
}
|
||||||
parts.push(part);
|
parts.push(part);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import {
|
||||||
transformRequestBody,
|
transformRequestBody,
|
||||||
} from "./openai-codex/request-transformer.js";
|
} from "./openai-codex/request-transformer.js";
|
||||||
import { parseCodexError, parseCodexSseStream } from "./openai-codex/response-handler.js";
|
import { parseCodexError, parseCodexSseStream } from "./openai-codex/response-handler.js";
|
||||||
import { transformMessages } from "./transorm-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
||||||
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import type {
|
||||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
import { transformMessages } from "./transorm-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize tool call ID for Mistral.
|
* Normalize tool call ID for Mistral.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import type {
|
||||||
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
||||||
import { parseStreamingJson } from "../utils/json-parse.js";
|
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||||
import { transformMessages } from "./transorm-messages.js";
|
import { transformMessages } from "./transform-messages.js";
|
||||||
|
|
||||||
/** Fast deterministic hash to shorten long strings */
|
/** Fast deterministic hash to shorten long strings */
|
||||||
function shortHash(str: string): string {
|
function shortHash(str: string): string {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue