fix(ai): carried thinking start bytes through stream wrappers

This commit is contained in:
can1357
2026-08-16 01:52:07 +02:00
parent 9274c50534
commit 2c530dc4a9
4 changed files with 65 additions and 25 deletions
+1
View File
@@ -63,6 +63,7 @@
### Removed
- Removed legacy local request-cost estimation machinery and database schemas previously used for OpenCode Go estimates.
- Fixed Anthropic-compatible streams dropping thinking bytes supplied by `content_block_start`, which invalidated signed-thinking replay ([#8319](https://github.com/can1357/oh-my-pi/pull/8319) by [@max12525k](https://github.com/max12525k)).
## [17.2.15] - 2026-08-12
+8
View File
@@ -2416,6 +2416,14 @@ const streamAnthropicOnce = (
contentIndex,
partial: output,
});
if (block.thinking) {
stream.push({
type: "thinking_delta",
contentIndex,
delta: block.thinking,
partial: output,
});
}
} else if (event.content_block.type === "redacted_thinking") {
streamedReplayUnsafeContent = true;
const block: Block = {
@@ -902,31 +902,6 @@ describe("anthropic stream envelope handling", () => {
]);
});
it("preserves signed thinking content from content_block_start", async () => {
const events = createThinkingSuccessEvents(" summary tail");
events[1] = {
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "Summary prefix" },
};
vi.spyOn(AnthropicMessages.prototype, "create").mockImplementation(() => createMockRequest(events) 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.content).toHaveLength(1);
const block = result.content[0];
expect(block?.type).toBe("thinking");
if (block?.type !== "thinking") {
throw new Error("Expected thinking content from content_block_start");
}
expect(block.thinking).toBe("Summary prefix summary tail");
expect(block.thinkingSignature).toBe("sig_thinking");
});
it("drops replayed closed blocks after a duplicate message_start instead of duplicating content", async () => {
const events: MockAnthropicEvent[] = [
{
@@ -659,6 +659,43 @@ describe("leaked thinking healing through stream()", () => {
return Object.assign(fn, { preconnect: fetch.preconnect });
}
function anthropicThinkingFetch(): FetchImpl {
const body = [
sseFrame("message_start", {
type: "message_start",
message: { id: "msg_thinking_prefix", usage: { input_tokens: 5, output_tokens: 0 } },
}),
sseFrame("content_block_start", {
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "Summary prefix" },
}),
sseFrame("content_block_delta", {
type: "content_block_delta",
index: 0,
delta: { type: "thinking_delta", thinking: " summary tail" },
}),
sseFrame("content_block_delta", {
type: "content_block_delta",
index: 0,
delta: { type: "signature_delta", signature: "sig_thinking" },
}),
sseFrame("content_block_stop", { type: "content_block_stop", index: 0 }),
sseFrame("message_delta", {
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: { input_tokens: 5, output_tokens: 4 },
}),
sseFrame("message_stop", { type: "message_stop" }),
].join("");
const fn = async (_input: string | URL | Request, _init?: RequestInit): Promise<Response> =>
new Response(body, {
status: 200,
headers: { "content-type": "text/event-stream", "request-id": "req_thinking_prefix" },
});
return Object.assign(fn, { preconnect: fetch.preconnect });
}
function anthropicModel(overrides: Partial<Model<"anthropic-messages">> = {}): Model<"anthropic-messages"> {
return buildModel({
id: "claude-sonnet-4-5",
@@ -711,6 +748,25 @@ describe("leaked thinking healing through stream()", () => {
expect(texts(result).join("").trim()).toBe("Final answer.");
});
it("preserves thinking bytes from content_block_start through a non-official endpoint", async () => {
const result = await stream(
anthropicModel({ provider: "zai", baseUrl: "https://api.z.ai/api/anthropic" }),
context,
{
apiKey: "test",
fetch: anthropicThinkingFetch(),
},
).result();
expect(thinks(result)).toEqual([
{
type: "thinking",
thinking: "Summary prefix summary tail",
thinkingSignature: "sig_thinking",
},
]);
});
it("replays native web-search history on a custom Anthropic continuation", async () => {
const searchResult = {
type: "web_search_tool_result",