fix: thinking block support, roundText preservation, abort listener leak, typo
This commit is contained in:
@@ -509,32 +509,37 @@ export class OpenCodeManager {
|
||||
let cacheWriteTokens = 0;
|
||||
let roundText = '';
|
||||
|
||||
for await (const event of events) {
|
||||
const result = parseAnthropicStreamEvent(event, streamAccumulator);
|
||||
try {
|
||||
for await (const event of events) {
|
||||
const result = parseAnthropicStreamEvent(event, streamAccumulator);
|
||||
|
||||
if (result.textDelta) {
|
||||
roundText += result.textDelta;
|
||||
if (callbacks.onDelta) {
|
||||
callbacks.onDelta(result.textDelta);
|
||||
if (result.textDelta) {
|
||||
roundText += result.textDelta;
|
||||
if (callbacks.onDelta) {
|
||||
callbacks.onDelta(result.textDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.usage) {
|
||||
if (result.usage.inputTokens !== undefined) inputTokens = result.usage.inputTokens;
|
||||
if (result.usage.cacheReadTokens !== undefined) cacheReadTokens = result.usage.cacheReadTokens;
|
||||
if (result.usage.cacheWriteTokens !== undefined) cacheWriteTokens = result.usage.cacheWriteTokens;
|
||||
if (result.usage.outputTokens !== undefined) outputTokens = result.usage.outputTokens;
|
||||
}
|
||||
if (result.usage) {
|
||||
if (result.usage.inputTokens !== undefined) inputTokens = result.usage.inputTokens;
|
||||
if (result.usage.cacheReadTokens !== undefined) cacheReadTokens = result.usage.cacheReadTokens;
|
||||
if (result.usage.cacheWriteTokens !== undefined) cacheWriteTokens = result.usage.cacheWriteTokens;
|
||||
if (result.usage.outputTokens !== undefined) outputTokens = result.usage.outputTokens;
|
||||
}
|
||||
|
||||
if (result.finishReason) {
|
||||
stopReason = result.finishReason;
|
||||
}
|
||||
if (result.finishReason) {
|
||||
stopReason = result.finishReason;
|
||||
}
|
||||
|
||||
if (result.done) break;
|
||||
if (result.done) break;
|
||||
}
|
||||
} finally {
|
||||
// Preserve text already emitted via onDelta even if the stream errors mid-round
|
||||
accumulatedText += roundText;
|
||||
}
|
||||
|
||||
const streamToolCalls = streamAccumulator.toolCalls;
|
||||
accumulatedText += roundText;
|
||||
const streamThinkingBlocks = streamAccumulator.thinkingBlocks;
|
||||
|
||||
// Emit token usage after stream completes (only when usage data was received)
|
||||
const hasUsageData = inputTokens > 0 || outputTokens > 0;
|
||||
@@ -587,6 +592,13 @@ export class OpenCodeManager {
|
||||
// Build assistant content blocks for the next message round
|
||||
const assistantContentBlocks: AnthropicContentBlock[] = [];
|
||||
|
||||
// Add thinking blocks first (Anthropic requires thinking before text when extended thinking is enabled)
|
||||
for (const [, tb] of streamThinkingBlocks) {
|
||||
if (tb.text) {
|
||||
assistantContentBlocks.push({ type: 'thinking', text: tb.text });
|
||||
}
|
||||
}
|
||||
|
||||
// Add text block with text from this round
|
||||
if (roundText) {
|
||||
assistantContentBlocks.push({ type: 'text', text: roundText });
|
||||
@@ -805,32 +817,36 @@ export class OpenCodeManager {
|
||||
let cacheReadTokens = 0;
|
||||
let roundText = '';
|
||||
|
||||
for await (const event of events) {
|
||||
const result = parseOpenAIStreamEvent(event, streamAccumulator);
|
||||
try {
|
||||
for await (const event of events) {
|
||||
const result = parseOpenAIStreamEvent(event, streamAccumulator);
|
||||
|
||||
if (result.textDelta) {
|
||||
roundText += result.textDelta;
|
||||
if (callbacks.onDelta) {
|
||||
callbacks.onDelta(result.textDelta);
|
||||
if (result.textDelta) {
|
||||
roundText += result.textDelta;
|
||||
if (callbacks.onDelta) {
|
||||
callbacks.onDelta(result.textDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.usage) {
|
||||
if (result.usage.promptTokens !== undefined) promptTokens = result.usage.promptTokens;
|
||||
if (result.usage.completionTokens !== undefined) completionTokens = result.usage.completionTokens;
|
||||
if (result.usage.totalTokens !== undefined) totalTokens = result.usage.totalTokens;
|
||||
if (result.usage.cacheReadTokens !== undefined) cacheReadTokens = result.usage.cacheReadTokens;
|
||||
}
|
||||
if (result.usage) {
|
||||
if (result.usage.promptTokens !== undefined) promptTokens = result.usage.promptTokens;
|
||||
if (result.usage.completionTokens !== undefined) completionTokens = result.usage.completionTokens;
|
||||
if (result.usage.totalTokens !== undefined) totalTokens = result.usage.totalTokens;
|
||||
if (result.usage.cacheReadTokens !== undefined) cacheReadTokens = result.usage.cacheReadTokens;
|
||||
}
|
||||
|
||||
if (result.finishReason) {
|
||||
finishReason = result.finishReason;
|
||||
}
|
||||
if (result.finishReason) {
|
||||
finishReason = result.finishReason;
|
||||
}
|
||||
|
||||
if (result.done) break;
|
||||
if (result.done) break;
|
||||
}
|
||||
} finally {
|
||||
// Preserve text already emitted via onDelta even if the stream errors mid-round
|
||||
accumulatedText += roundText;
|
||||
}
|
||||
|
||||
const streamToolCalls = streamAccumulator.toolCalls;
|
||||
accumulatedText += roundText;
|
||||
|
||||
// Emit token usage after stream completes (only when usage data was received)
|
||||
const hasUsageData = promptTokens > 0 || completionTokens > 0;
|
||||
@@ -2347,7 +2363,7 @@ Respond with JSON only: {"title": "...", "alt": "...", "caption": "..."}`;
|
||||
options.signal.addEventListener('abort', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request cancelled'));
|
||||
});
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface OpenAIStreamAccumulator {
|
||||
|
||||
export interface AnthropicStreamAccumulator {
|
||||
toolCalls: Map<number, ToolCallAccumulator>;
|
||||
thinkingBlocks: Map<number, { text: string }>;
|
||||
}
|
||||
|
||||
export interface HttpStreamError extends Error {
|
||||
@@ -119,7 +120,7 @@ export function createOpenAIStreamAccumulator(): OpenAIStreamAccumulator {
|
||||
}
|
||||
|
||||
export function createAnthropicStreamAccumulator(): AnthropicStreamAccumulator {
|
||||
return { toolCalls: new Map() };
|
||||
return { toolCalls: new Map(), thinkingBlocks: new Map() };
|
||||
}
|
||||
|
||||
// ── OpenAI/Mistral SSE Parser ──
|
||||
@@ -217,8 +218,8 @@ export function parseOpenAIStreamEvent(
|
||||
*
|
||||
* Anthropic streaming format uses named event types:
|
||||
* - message_start: input token usage
|
||||
* - content_block_start: text or tool_use block begins
|
||||
* - content_block_delta: text_delta or input_json_delta
|
||||
* - content_block_start: text, tool_use, or thinking block begins
|
||||
* - content_block_delta: text_delta, input_json_delta, or thinking_delta
|
||||
* - content_block_stop: block ends
|
||||
* - message_delta: output tokens + stop_reason
|
||||
* - message_stop: stream complete
|
||||
@@ -260,6 +261,8 @@ export function parseAnthropicStreamEvent(
|
||||
name: block.name,
|
||||
arguments: '',
|
||||
});
|
||||
} else if (block?.type === 'thinking') {
|
||||
accumulator.thinkingBlocks.set(data.index as number, { text: '' });
|
||||
}
|
||||
// text block start is a no-op (empty initial text)
|
||||
break;
|
||||
@@ -274,6 +277,11 @@ export function parseAnthropicStreamEvent(
|
||||
if (tc) {
|
||||
tc.arguments += delta.partial_json;
|
||||
}
|
||||
} else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
||||
const tb = accumulator.thinkingBlocks.get(data.index as number);
|
||||
if (tb) {
|
||||
tb.text += delta.thinking;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user