Skip to content

Commit d735fe2

Browse files
committed
fix: decouple stream metadata fetches to prevent partial failures
Each promise (totalUsage, contextUsage, contextProviderMetadata) now has independent timeout + error handling. If providerMetadata rejects or times out, totalUsage is still returned for cost calculation.
1 parent 51e533d commit d735fe2

File tree

2 files changed

+18
-26
lines changed

2 files changed

+18
-26
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ lint: node_modules/.installed ## Run ESLint (typecheck runs in separate target)
224224
lint-fix: node_modules/.installed ## Run linter with --fix
225225
@./scripts/lint.sh --fix
226226

227-
ifeq ($(OS),Windows_NT)
227+
ifeq ($(OS),Windows_NT)
228228
typecheck: node_modules/.installed src/version.ts ## Run TypeScript type checking (uses tsgo for 10x speedup)
229229
@# On Windows, use npm run because bun x doesn't correctly pass arguments
230230
@npmx concurrently -g \

src/node/services/streamManager.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -340,31 +340,23 @@ export class StreamManager extends EventEmitter {
340340
contextProviderMetadata?: Record<string, unknown>;
341341
duration: number;
342342
}> {
343-
let totalUsage: LanguageModelV2Usage | undefined;
344-
let contextUsage: LanguageModelV2Usage | undefined;
345-
let contextProviderMetadata: Record<string, unknown> | undefined;
346-
347-
try {
348-
// Fetch all metadata in parallel with timeout
349-
// - totalUsage: sum of all steps (for cost calculation)
350-
// - usage: last step only (for context window display)
351-
// - providerMetadata: last step (for context window cache display)
352-
const [total, context, contextMeta] = await Promise.race([
353-
Promise.all([
354-
streamInfo.streamResult.totalUsage,
355-
streamInfo.streamResult.usage,
356-
streamInfo.streamResult.providerMetadata,
357-
]),
358-
new Promise<[undefined, undefined, undefined]>((resolve) =>
359-
setTimeout(() => resolve([undefined, undefined, undefined]), timeoutMs)
360-
),
361-
]);
362-
totalUsage = total;
363-
contextUsage = context;
364-
contextProviderMetadata = contextMeta;
365-
} catch (error) {
366-
log.debug("Could not retrieve stream metadata:", error);
367-
}
343+
// Helper: wrap promise with independent timeout + error handling
344+
// Each promise resolves independently - one failure doesn't mask others
345+
const withTimeout = <T>(promise: Promise<T>): Promise<T | undefined> =>
346+
Promise.race([
347+
promise,
348+
new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), timeoutMs)),
349+
]).catch(() => undefined);
350+
351+
// Fetch all metadata in parallel with independent timeouts
352+
// - totalUsage: sum of all steps (for cost calculation)
353+
// - contextUsage: last step only (for context window display)
354+
// - contextProviderMetadata: last step (for context window cache display)
355+
const [totalUsage, contextUsage, contextProviderMetadata] = await Promise.all([
356+
withTimeout(streamInfo.streamResult.totalUsage),
357+
withTimeout(streamInfo.streamResult.usage),
358+
withTimeout(streamInfo.streamResult.providerMetadata),
359+
]);
368360

369361
return {
370362
totalUsage,

0 commit comments

Comments
 (0)