fix(ai): carried thinking start bytes through stream wrappers
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user