fix: SSE streaming review fixes

- Fix OpenAI path using accumulatedText instead of round-specific text
  in assistant messages for multi-round tool conversations
- Guard JSON.parse in both SSE parsers against corrupted events
- Extract cacheReadTokens from OpenAI prompt_tokens_details when available
- Add tests for JSON parse resilience and cache token extraction (7 new tests)
This commit is contained in:
2026-03-01 10:25:54 +01:00
parent 78c2cb7bb7
commit 781cedade5
3 changed files with 163 additions and 20 deletions

View File

@@ -761,6 +761,8 @@ export class OpenCodeManager {
let promptTokens = 0;
let completionTokens = 0;
let totalTokens = 0;
let cacheReadTokens = 0;
let roundText = ''; // Text produced in this round only
const { events } = await withRetry(() => httpRequestStream(ZEN_OPENAI_URL, {
method: 'POST',
@@ -778,6 +780,7 @@ export class OpenCodeManager {
// Emit text deltas immediately for real-time streaming
if (result.textDelta) {
accumulatedText += result.textDelta;
roundText += result.textDelta;
if (callbacks.onDelta) {
callbacks.onDelta(result.textDelta);
}
@@ -788,6 +791,7 @@ export class OpenCodeManager {
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) {
@@ -799,8 +803,7 @@ export class OpenCodeManager {
// Emit token usage after stream completes
if (callbacks.onTokenUsage) {
const cacheReadTokens = 0; // OpenAI doesn't provide cache info in streaming
const inputTokens = promptTokens;
const inputTokens = promptTokens - cacheReadTokens;
const outputTokens = completionTokens;
const prev = this.conversationUsage.get(conversationId) || {
@@ -846,7 +849,7 @@ export class OpenCodeManager {
// Build the assistant message with tool_calls for conversation history
const assistantMessage: Record<string, unknown> = {
role: 'assistant',
content: accumulatedText || null,
content: roundText || null,
tool_calls: parsedToolCalls.map((tc) => ({
id: tc.id,
type: 'function',