Skip to content

Commit b3cbdfa

Browse files
authored
fix(provider): dedupe streaming usage reports (#145)
1 parent 6809989 commit b3cbdfa

2 files changed

Lines changed: 28 additions & 11 deletions

File tree

src/client/core.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
DeepSeekRequest,
66
DeepSeekStreamChunk,
77
DeepSeekToolCall,
8+
DeepSeekUsage,
89
StreamCallbacks,
910
} from '../types';
1011
import { createHttpError, formatRequestError, normalizeRequestError } from './error';
@@ -64,6 +65,7 @@ export class DeepSeekClient {
6465
const reader = response.body.getReader();
6566
const decoder = new TextDecoder();
6667
let buffer = '';
68+
let latestUsage: DeepSeekUsage | undefined;
6769

6870
// Accumulate tool call deltas by index, then emit on finish_reason=stop/tool_calls
6971
const pendingToolCalls = new Map<number, DeepSeekToolCall>();
@@ -97,6 +99,7 @@ export class DeepSeekClient {
9799
callbacks.onToolCall(tc);
98100
}
99101
pendingToolCalls.clear();
102+
reportFinalUsage(callbacks, latestUsage);
100103
callbacks.onDone();
101104
return;
102105
}
@@ -110,9 +113,10 @@ export class DeepSeekClient {
110113
const chunk: DeepSeekStreamChunk = JSON.parse(jsonStr);
111114
const choice = chunk.choices?.[0];
112115

113-
// Capture usage stats from the API for token-count calibration.
114-
if (chunk.usage && callbacks.onUsage) {
115-
callbacks.onUsage(chunk.usage);
116+
// Some OpenAI-compatible providers emit usage on every streaming chunk.
117+
// Keep only the latest value and report it once when the stream completes.
118+
if (chunk.usage) {
119+
latestUsage = chunk.usage;
116120
}
117121

118122
if (!choice) {
@@ -166,6 +170,7 @@ export class DeepSeekClient {
166170
}
167171
}
168172

173+
reportFinalUsage(callbacks, latestUsage);
169174
callbacks.onDone();
170175
} catch (error) {
171176
if (isAbortError(error) && cancellationToken?.isCancellationRequested) {
@@ -180,6 +185,13 @@ export class DeepSeekClient {
180185
}
181186
}
182187

188+
function reportFinalUsage(callbacks: StreamCallbacks, usage: DeepSeekUsage | undefined): void {
189+
if (!usage || !callbacks.onUsage) {
190+
return;
191+
}
192+
callbacks.onUsage(usage);
193+
}
194+
183195
function isAbortError(error: unknown): boolean {
184196
return error instanceof Error && error.name === 'AbortError';
185197
}

src/provider/stream.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type CacheDiagnosticsRun,
88
type ReplayMarkerReportTrigger,
99
} from './debug';
10-
import { formatRequestLogLine } from './routing';
10+
import { formatRequestLogLine, type RequestKind } from './routing';
1111
import {
1212
createReplayMarkerPart,
1313
hasReplayMarkerMetadata,
@@ -89,7 +89,7 @@ export function streamChatCompletion({
8989
);
9090
setCharsPerToken(charsPerToken);
9191
prepared.cacheDiagnostics.onUsage(usage, charsPerToken);
92-
reportCopilotContextUsage(progress, usage);
92+
reportCopilotContextUsage(progress, usage, prepared.requestKind);
9393
},
9494
},
9595
token,
@@ -267,6 +267,7 @@ function updateCharsPerToken(
267267
function reportCopilotContextUsage(
268268
progress: vscode.Progress<vscode.LanguageModelResponsePart>,
269269
usage: DeepSeekUsage,
270+
requestKind: RequestKind,
270271
): void {
271272
const data = {
272273
prompt_tokens: usage.prompt_tokens,
@@ -277,10 +278,14 @@ function reportCopilotContextUsage(
277278
},
278279
};
279280

280-
progress.report(
281-
new vscode.LanguageModelDataPart(
282-
new TextEncoder().encode(JSON.stringify(data)),
283-
COPILOT_USAGE_DATA_PART_MIME,
284-
),
285-
);
281+
try {
282+
progress.report(
283+
new vscode.LanguageModelDataPart(
284+
new TextEncoder().encode(JSON.stringify(data)),
285+
COPILOT_USAGE_DATA_PART_MIME,
286+
),
287+
);
288+
} catch (error) {
289+
logger.warn(formatRequestLogLine(requestKind, 'Failed to report usage data'), error);
290+
}
286291
}

0 commit comments

Comments
 (0)