fix(ai): preserved unwrapped thinking signatures
Tracked whether Anthropic thinking-envelope normalization actually stripped a wrapper before mutating thinking bytes or clearing signatures. Added coverage for signed thinking with surrounding whitespace to preserve byte-for-byte replay. Fixes #2695
This commit is contained in:
@@ -1467,10 +1467,12 @@ const THINKING_ENVELOPE_CLOSE = "</thinking>";
|
||||
|
||||
function unwrapAnthropicThinkingEnvelope(text: string): string | undefined {
|
||||
let current = text.trim();
|
||||
let stripped = false;
|
||||
while (current.startsWith(THINKING_ENVELOPE_OPEN) && current.endsWith(THINKING_ENVELOPE_CLOSE)) {
|
||||
current = current.slice(THINKING_ENVELOPE_OPEN.length, current.length - THINKING_ENVELOPE_CLOSE.length).trim();
|
||||
stripped = true;
|
||||
}
|
||||
return current === text ? undefined : current;
|
||||
return stripped ? current : undefined;
|
||||
}
|
||||
|
||||
function createEmptyUsage(premiumRequests?: number): Usage {
|
||||
|
||||
@@ -349,6 +349,42 @@ describe("anthropic stream envelope handling", () => {
|
||||
{ type: "text", text: "Check logs before accepting container health." },
|
||||
]);
|
||||
});
|
||||
it("preserves signed thinking bytes when no literal thinking envelope is present", async () => {
|
||||
const signedThinking = "\nCheck logs before accepting container health.\n";
|
||||
vi.spyOn(AnthropicMessages.prototype, "create").mockImplementation(
|
||||
() => createMockRequest(createThinkingSuccessEvents(signedThinking)) as never,
|
||||
);
|
||||
|
||||
const stream = streamAnthropic(model, context, { apiKey: "sk-ant-test" });
|
||||
for await (const _ of stream) {
|
||||
// drain stream
|
||||
}
|
||||
const result = await stream.result();
|
||||
|
||||
expect(result.stopReason).toBe("stop");
|
||||
expect(result.content).toHaveLength(1);
|
||||
const block = result.content[0];
|
||||
expect(block?.type).toBe("thinking");
|
||||
if (block?.type !== "thinking") {
|
||||
throw new Error("Expected signed thinking content");
|
||||
}
|
||||
expect(block.thinking).toBe(signedThinking);
|
||||
expect(block.thinkingSignature).toBe("sig_thinking");
|
||||
|
||||
const replayParams = convertAnthropicMessages(
|
||||
[
|
||||
{ role: "user", content: "Say hi", timestamp: 1 },
|
||||
result,
|
||||
{ role: "user", content: "follow up", timestamp: 2 },
|
||||
],
|
||||
model,
|
||||
false,
|
||||
);
|
||||
const replayAssistant = replayParams.find(param => param.role === "assistant");
|
||||
expect(replayAssistant?.content).toEqual([
|
||||
{ type: "thinking", thinking: signedThinking, signature: "sig_thinking" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops replayed closed blocks after a duplicate message_start instead of duplicating content", async () => {
|
||||
const events: MockAnthropicEvent[] = [
|
||||
|
||||
Reference in New Issue
Block a user