mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-13 23:53:18 +00:00
Merge pull request #369 from ndycode/fix/issue-368-sentinel-thinking
Fix Claude thinking signature reuse (issue #368)
This commit is contained in:
@@ -1085,6 +1085,31 @@ function sanitizeThinkingPart(part: Record<string, unknown>): Record<string, unk
|
||||
return stripCacheControlRecursively(part) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function applySentinelSignature(part: Record<string, unknown>): Record<string, unknown> {
|
||||
const updated = { ...part } as Record<string, unknown>;
|
||||
|
||||
if ("thoughtSignature" in updated) {
|
||||
(updated as any).thoughtSignature = SKIP_THOUGHT_SIGNATURE;
|
||||
}
|
||||
if ("signature" in updated) {
|
||||
(updated as any).signature = SKIP_THOUGHT_SIGNATURE;
|
||||
}
|
||||
|
||||
if ((updated as any).thought === true && (updated as any).thoughtSignature === undefined) {
|
||||
(updated as any).thoughtSignature = SKIP_THOUGHT_SIGNATURE;
|
||||
}
|
||||
if (
|
||||
((updated as any).type === "thinking"
|
||||
|| (updated as any).type === "redacted_thinking"
|
||||
|| (updated as any).type === "reasoning")
|
||||
&& (updated as any).signature === undefined
|
||||
) {
|
||||
(updated as any).signature = SKIP_THOUGHT_SIGNATURE;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
function findLastAssistantIndex(contents: any[], roleValue: "model" | "assistant"): number {
|
||||
for (let i = contents.length - 1; i >= 0; i--) {
|
||||
const content = contents[i];
|
||||
@@ -1108,6 +1133,37 @@ function filterContentArray(
|
||||
return stripAllThinkingBlocks(contentArray);
|
||||
}
|
||||
|
||||
if (isClaudeModel) {
|
||||
const filtered: any[] = [];
|
||||
|
||||
for (const item of contentArray) {
|
||||
if (!item || typeof item !== "object") {
|
||||
filtered.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isToolBlock(item)) {
|
||||
filtered.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const isThinking = isThinkingPart(item);
|
||||
const hasSignature = hasSignatureField(item);
|
||||
|
||||
if (!isThinking && !hasSignature) {
|
||||
filtered.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sanitized = sanitizeThinkingPart(item);
|
||||
if (sanitized) {
|
||||
filtered.push(applySentinelSignature(sanitized));
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
const filtered: any[] = [];
|
||||
|
||||
for (const item of contentArray) {
|
||||
@@ -2814,4 +2870,3 @@ data: ${JSON.stringify({ type: "message_stop" })}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -236,11 +236,11 @@ describe("request.ts", () => {
|
||||
expect(result.thoughtSignature).toBe("skip_thought_signature_validator");
|
||||
});
|
||||
|
||||
it("preserves existing thoughtSignature", () => {
|
||||
it("replaces existing thoughtSignature with sentinel", () => {
|
||||
const existingSignature = "a".repeat(MIN_SIGNATURE_LENGTH + 10);
|
||||
const part = { thought: true, text: "thinking...", thoughtSignature: existingSignature };
|
||||
const result = ensureThoughtSignature(part, "session-key");
|
||||
expect(result.thoughtSignature).toBe(existingSignature);
|
||||
expect(result.thoughtSignature).toBe("skip_thought_signature_validator");
|
||||
});
|
||||
|
||||
it("does not modify non-thinking parts", () => {
|
||||
@@ -276,6 +276,13 @@ describe("request.ts", () => {
|
||||
expect(hasSignedThinkingPart(part)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for sentinel signatures", () => {
|
||||
const thoughtPart = { thought: true, thoughtSignature: "skip_thought_signature_validator" };
|
||||
const thinkingPart = { type: "thinking", signature: "skip_thought_signature_validator" };
|
||||
expect(hasSignedThinkingPart(thoughtPart)).toBe(true);
|
||||
expect(hasSignedThinkingPart(thinkingPart)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for part with short signature", () => {
|
||||
const part = { thought: true, thoughtSignature: "short" };
|
||||
expect(hasSignedThinkingPart(part)).toBe(false);
|
||||
|
||||
@@ -355,35 +355,24 @@ function isGeminiThinkingPart(part: any): boolean {
|
||||
// Reference: LLM-API-Key-Proxy uses this pattern for Gemini 3 tool calls.
|
||||
const SENTINEL_SIGNATURE = "skip_thought_signature_validator";
|
||||
|
||||
function ensureThoughtSignature(part: any, sessionId: string): any {
|
||||
function ensureThoughtSignature(part: any, _sessionId: string): any {
|
||||
if (!part || typeof part !== "object") {
|
||||
return part;
|
||||
}
|
||||
|
||||
const text = typeof part.text === "string" ? part.text : typeof part.thinking === "string" ? part.thinking : "";
|
||||
if (!text) {
|
||||
return part;
|
||||
}
|
||||
|
||||
if (part.thought === true) {
|
||||
if (!part.thoughtSignature) {
|
||||
const cached = getCachedSignature(sessionId, text);
|
||||
if (cached) {
|
||||
return { ...part, thoughtSignature: cached };
|
||||
}
|
||||
// Fallback: use sentinel signature to prevent API rejection
|
||||
// This allows Claude to redact the thinking block instead of failing
|
||||
return { ...part, thoughtSignature: SENTINEL_SIGNATURE };
|
||||
if (part.thoughtSignature === SENTINEL_SIGNATURE) {
|
||||
return part;
|
||||
}
|
||||
return part;
|
||||
// Always use sentinel to avoid invalid signature reuse in history.
|
||||
return { ...part, thoughtSignature: SENTINEL_SIGNATURE };
|
||||
}
|
||||
|
||||
if ((part.type === "thinking" || part.type === "reasoning") && !part.signature) {
|
||||
const cached = getCachedSignature(sessionId, text);
|
||||
if (cached) {
|
||||
return { ...part, signature: cached };
|
||||
if (part.type === "thinking" || part.type === "reasoning") {
|
||||
if (part.signature === SENTINEL_SIGNATURE) {
|
||||
return part;
|
||||
}
|
||||
// Fallback: use sentinel signature to prevent API rejection
|
||||
// Always use sentinel to avoid invalid signature reuse in history.
|
||||
return { ...part, signature: SENTINEL_SIGNATURE };
|
||||
}
|
||||
|
||||
@@ -396,11 +385,13 @@ function hasSignedThinkingPart(part: any): boolean {
|
||||
}
|
||||
|
||||
if (part.thought === true) {
|
||||
return typeof part.thoughtSignature === "string" && part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH;
|
||||
return typeof part.thoughtSignature === "string"
|
||||
&& (part.thoughtSignature === SENTINEL_SIGNATURE || part.thoughtSignature.length >= MIN_SIGNATURE_LENGTH);
|
||||
}
|
||||
|
||||
if (part.type === "thinking" || part.type === "reasoning") {
|
||||
return typeof part.signature === "string" && part.signature.length >= MIN_SIGNATURE_LENGTH;
|
||||
return typeof part.signature === "string"
|
||||
&& (part.signature === SENTINEL_SIGNATURE || part.signature.length >= MIN_SIGNATURE_LENGTH);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -433,24 +424,23 @@ function ensureThinkingBeforeToolUseInContents(contents: any[], signatureSession
|
||||
|
||||
const lastThinking = defaultSignatureStore.get(signatureSessionKey);
|
||||
if (!lastThinking) {
|
||||
// No cached signature available - strip thinking blocks entirely
|
||||
// Claude requires valid signatures, and we can't fake them
|
||||
// Return only tool_use parts without any thinking to avoid signature validation errors
|
||||
log.debug("Stripping thinking from tool_use content (no valid cached signature)", { signatureSessionKey });
|
||||
// No cached thinking available - strip thinking blocks entirely
|
||||
// Return only tool_use parts without any thinking to avoid invalid signature reuse
|
||||
log.debug("Stripping thinking from tool_use content (no cached thinking)", { signatureSessionKey });
|
||||
return { ...content, parts: otherParts };
|
||||
}
|
||||
|
||||
const injected = {
|
||||
thought: true,
|
||||
text: lastThinking.text,
|
||||
thoughtSignature: lastThinking.signature,
|
||||
thoughtSignature: SKIP_THOUGHT_SIGNATURE,
|
||||
};
|
||||
|
||||
return { ...content, parts: [injected, ...otherParts] };
|
||||
});
|
||||
}
|
||||
|
||||
function ensureMessageThinkingSignature(block: any, sessionId: string): any {
|
||||
function ensureMessageThinkingSignature(block: any, _sessionId: string): any {
|
||||
if (!block || typeof block !== "object") {
|
||||
return block;
|
||||
}
|
||||
@@ -459,21 +449,11 @@ function ensureMessageThinkingSignature(block: any, sessionId: string): any {
|
||||
return block;
|
||||
}
|
||||
|
||||
if (typeof block.signature === "string" && block.signature.length >= MIN_SIGNATURE_LENGTH) {
|
||||
if (block.signature === SENTINEL_SIGNATURE) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const text = typeof block.thinking === "string" ? block.thinking : typeof block.text === "string" ? block.text : "";
|
||||
if (!text) {
|
||||
return block;
|
||||
}
|
||||
|
||||
const cached = getCachedSignature(sessionId, text);
|
||||
if (cached) {
|
||||
return { ...block, signature: cached };
|
||||
}
|
||||
|
||||
return block;
|
||||
return { ...block, signature: SENTINEL_SIGNATURE };
|
||||
}
|
||||
|
||||
function hasToolUseInContents(contents: any[]): boolean {
|
||||
@@ -516,7 +496,7 @@ function hasSignedThinkingInMessages(messages: any[]): boolean {
|
||||
typeof block === "object" &&
|
||||
(block.type === "thinking" || block.type === "redacted_thinking") &&
|
||||
typeof block.signature === "string" &&
|
||||
block.signature.length >= MIN_SIGNATURE_LENGTH,
|
||||
(block.signature === SKIP_THOUGHT_SIGNATURE || block.signature.length >= MIN_SIGNATURE_LENGTH),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -542,7 +522,11 @@ function ensureThinkingBeforeToolUseInMessages(messages: any[], signatureSession
|
||||
.map((b) => ensureMessageThinkingSignature(b, signatureSessionKey));
|
||||
|
||||
const otherBlocks = blocks.filter((b) => !(b && typeof b === "object" && (b.type === "thinking" || b.type === "redacted_thinking")));
|
||||
const hasSignedThinking = thinkingBlocks.some((b) => typeof b.signature === "string" && b.signature.length >= MIN_SIGNATURE_LENGTH);
|
||||
const hasSignedThinking = thinkingBlocks.some(
|
||||
(b) =>
|
||||
typeof b.signature === "string"
|
||||
&& (b.signature === SKIP_THOUGHT_SIGNATURE || b.signature.length >= MIN_SIGNATURE_LENGTH),
|
||||
);
|
||||
|
||||
if (hasSignedThinking) {
|
||||
return { ...message, content: [...thinkingBlocks, ...otherBlocks] };
|
||||
@@ -566,7 +550,7 @@ function ensureThinkingBeforeToolUseInMessages(messages: any[], signatureSession
|
||||
const injected = {
|
||||
type: "thinking",
|
||||
thinking: lastThinking.text,
|
||||
signature: lastThinking.signature,
|
||||
signature: SKIP_THOUGHT_SIGNATURE,
|
||||
};
|
||||
|
||||
return { ...message, content: [injected, ...otherBlocks] };
|
||||
@@ -1565,7 +1549,8 @@ export async function transformAntigravityResponse(
|
||||
|
||||
try {
|
||||
const headers = new Headers(response.headers);
|
||||
const text = await response.text();
|
||||
// Read from a clone so the original body remains available for callers on failure paths.
|
||||
const text = await response.clone().text();
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody;
|
||||
@@ -1736,4 +1721,3 @@ export const __testExports = {
|
||||
transformStreamingPayload,
|
||||
createStreamingTransformer,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user