codex-copilot-dx 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,14 @@ import {
15
15
  upstreamChatStreamError,
16
16
  withChatStreamUsage,
17
17
  } from "./stream-contract.mjs";
18
- import { isChatOutputDelta, markFirstOutput, markOutputTokens } from "./stream-performance.mjs";
18
+ import {
19
+ isChatOutputDelta,
20
+ markFirstOutput,
21
+ markOutputTokens,
22
+ markResponseErrorOrigin,
23
+ markResponseTerminal,
24
+ markStreamFailure,
25
+ } from "./stream-performance.mjs";
19
26
 
20
27
  function cloneJson(value) {
21
28
  return value === undefined ? undefined : structuredClone(value);
@@ -66,18 +73,20 @@ export function responsesToChat(body) {
66
73
 
67
74
  const messageContent = (content) => {
68
75
  if (typeof content === "string") return content;
69
- if (!Array.isArray(content)) return JSON.stringify(content);
76
+ if (content === null) return null;
77
+ if (!Array.isArray(content)) throw chatCompatibilityError("message content", typeof content);
70
78
  const parts = [];
71
79
  for (const part of content) {
72
- if (!part || typeof part !== "object") continue;
80
+ if (!part || typeof part !== "object") throw chatCompatibilityError("message content", part?.type);
73
81
  if (["input_text", "output_text", "text"].includes(part.type)) {
74
- parts.push({ type: "text", text: String(part.text || "") });
82
+ if (typeof part.text !== "string") throw chatCompatibilityError("message content", part.type);
83
+ parts.push({ type: "text", text: part.text });
75
84
  } else if (["input_image", "image_url", "image"].includes(part.type)) {
76
85
  const imagePart = chatImagePart(part);
77
86
  if (imagePart) parts.push(imagePart);
78
- else parts.push({ type: "text", text: JSON.stringify(part) });
87
+ else throw chatCompatibilityError("message content", part.type);
79
88
  } else {
80
- parts.push({ type: "text", text: JSON.stringify(part) });
89
+ throw chatCompatibilityError("message content", part.type);
81
90
  }
82
91
  }
83
92
  if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
@@ -176,11 +185,20 @@ export function responsesToChat(body) {
176
185
 
177
186
  function uid() { return randomUUID().replace(/-/g, ""); }
178
187
 
188
+ function chatTerminal(finishReason) {
189
+ const reason = finishReason === "length" ? "max_output_tokens"
190
+ : finishReason === "content_filter" ? "content_filter" : null;
191
+ return reason
192
+ ? { status: "incomplete", incomplete_details: { reason } }
193
+ : { status: "completed" };
194
+ }
195
+
179
196
  export function chatToResponses(chatResp, model) {
180
197
  const id = `resp_${uid()}`, choice = chatResp.choices?.[0], msg = choice?.message, output = [];
181
- if (msg?.content) output.push({ type: "message", id: `msg_${uid()}`, role: "assistant", status: "completed", content: [{ type: "output_text", text: msg.content }] });
182
- if (msg?.tool_calls) for (const tc of msg.tool_calls) output.push({ type: "function_call", id: tc.id, call_id: tc.id, name: tc.function.name, arguments: tc.function.arguments, status: "completed" });
183
- return { id, object: "response", status: "completed", model: chatResp.model || model, output, usage: chatResp.usage ? { input_tokens: chatResp.usage.prompt_tokens || 0, output_tokens: chatResp.usage.completion_tokens || 0, total_tokens: chatResp.usage.total_tokens || 0 } : undefined };
198
+ const terminal = chatTerminal(choice?.finish_reason);
199
+ if (msg?.content) output.push({ type: "message", id: `msg_${uid()}`, role: "assistant", status: terminal.status, content: [{ type: "output_text", text: msg.content }] });
200
+ if (msg?.tool_calls) for (const tc of msg.tool_calls) output.push({ type: "function_call", id: tc.id, call_id: tc.id, name: tc.function.name, arguments: tc.function.arguments, status: terminal.status });
201
+ return { id, object: "response", ...terminal, model: chatResp.model || model, output, usage: chatResp.usage ? { input_tokens: chatResp.usage.prompt_tokens || 0, output_tokens: chatResp.usage.completion_tokens || 0, total_tokens: chatResp.usage.total_tokens || 0 } : undefined };
184
202
  }
185
203
 
186
204
  export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
@@ -216,11 +234,15 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
216
234
  releaseRequest?.();
217
235
  }
218
236
  } catch (e) {
237
+ markResponseErrorOrigin("transport");
238
+ markStreamFailure();
219
239
  const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : 502;
220
240
  await onError(statusCode, e.message);
221
241
  return false;
222
242
  }
223
243
  if (!resp.ok) {
244
+ markResponseErrorOrigin("upstream_http");
245
+ markStreamFailure();
224
246
  await onError(resp.status, await readBoundedResponseText(resp, {
225
247
  maxBytes: MAX_UPSTREAM_ERROR_BODY_BYTES,
226
248
  label: "Copilot Chat error body",
@@ -230,6 +252,8 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
230
252
  try {
231
253
  await requireUpstreamEventStream(resp);
232
254
  } catch (error) {
255
+ markResponseErrorOrigin("transport");
256
+ markStreamFailure();
233
257
  await onError(error.statusCode, error.message, error);
234
258
  return false;
235
259
  }
@@ -237,6 +261,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
237
261
  abort?.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
238
262
  const respId = `resp_${uid()}`;
239
263
  let actualModel = requestModel;
264
+ let finishReason = null;
240
265
  let fullText = "";
241
266
  let messageItem = null;
242
267
  let nextOutputIndex = 0;
@@ -298,7 +323,8 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
298
323
  return { tool, created: true };
299
324
  };
300
325
 
301
- const emitCompleted = async () => {
326
+ const emitTerminal = async () => {
327
+ const terminal = chatTerminal(finishReason);
302
328
  if (!messageItem && toolCalls.size === 0) await ensureMessageItem();
303
329
  const output = [];
304
330
  if (messageItem) {
@@ -306,7 +332,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
306
332
  type: "message",
307
333
  id: messageItem.id,
308
334
  role: "assistant",
309
- status: "completed",
335
+ status: terminal.status,
310
336
  content: [{ type: "output_text", text: fullText }],
311
337
  };
312
338
  await emitEvent("response.output_text.done", { output_index: messageItem.outputIndex, content_index: 0, text: fullText });
@@ -321,7 +347,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
321
347
  call_id: tool.callId,
322
348
  name: tool.name,
323
349
  arguments: tool.arguments,
324
- status: "completed",
350
+ status: terminal.status,
325
351
  };
326
352
  await emitEvent("response.function_call_arguments.done", {
327
353
  output_index: tool.outputIndex,
@@ -331,16 +357,21 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
331
357
  await emitEvent("response.output_item.done", { output_index: tool.outputIndex, item });
332
358
  output[tool.outputIndex] = item;
333
359
  }
334
- await emitEvent("response.completed", {
360
+ if (terminal.status === "incomplete") markStreamFailure();
361
+ await emitEvent(`response.${terminal.status}`, {
335
362
  response: {
336
363
  id: respId,
337
364
  object: "response",
338
- status: "completed",
365
+ ...terminal,
339
366
  model: actualModel,
340
367
  output: output.filter(Boolean),
341
368
  usage,
342
369
  },
343
370
  });
371
+ markResponseTerminal(terminal.status, {
372
+ model: actualModel, origin: "chat_completion", reason: terminal.incomplete_details?.reason,
373
+ });
374
+ return terminal.status === "completed";
344
375
  };
345
376
 
346
377
  try {
@@ -350,7 +381,7 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
350
381
  })) {
351
382
  if (!line.startsWith("data: ")) continue;
352
383
  const data = line.slice(6).trim();
353
- if (data === "[DONE]") { await emitCompleted(); onDone(); return true; }
384
+ if (data === "[DONE]") { const completed = await emitTerminal(); onDone(); return completed; }
354
385
  let parsed;
355
386
  try {
356
387
  parsed = JSON.parse(data);
@@ -373,7 +404,9 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
373
404
  if (cached) usage.input_tokens_details = { cached_tokens: cached };
374
405
  markOutputTokens(usage.output_tokens);
375
406
  }
376
- const delta = parsed.choices?.[0]?.delta;
407
+ const choice = parsed.choices?.[0];
408
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
409
+ const delta = choice?.delta;
377
410
  if (!delta) continue;
378
411
  if (isChatOutputDelta(delta)) markPerformanceOutput();
379
412
  if (delta.content) {
@@ -401,14 +434,19 @@ export async function forwardToChat(chatReq, emitEvent, onDone, onError, options
401
434
  }
402
435
  } catch (e) {
403
436
  if (e === downstreamClosed) {
437
+ markResponseTerminal("cancelled", { model: actualModel, origin: "client_disconnect" });
404
438
  await resp.body?.cancel?.().catch?.(() => {});
405
439
  return false;
406
440
  }
441
+ markResponseErrorOrigin("transport");
442
+ markStreamFailure();
407
443
  const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(abort?.reason) : (e?.statusCode || 502);
408
444
  await onError(statusCode, e?.message || "upstream stream error", e);
409
445
  return false;
410
446
  }
411
447
  const error = incompleteUpstreamStream("[DONE]");
448
+ markResponseErrorOrigin("transport");
449
+ markStreamFailure();
412
450
  await onError(error.statusCode, error.message, error);
413
451
  return false;
414
452
  }
@@ -37,7 +37,13 @@ import { status } from "./status.mjs";
37
37
  import { endStreamWithError } from "./stream-errors.mjs";
38
38
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
39
39
  import { recordResponsesUsage } from "./usage.mjs";
40
- import { measureRequestStage, measureRequestStageAsync } from "./stream-performance.mjs";
40
+ import {
41
+ markResponseErrorOrigin,
42
+ markResponseModel,
43
+ markResponseTerminal,
44
+ measureRequestStage,
45
+ measureRequestStageAsync,
46
+ } from "./stream-performance.mjs";
41
47
 
42
48
  const RESPONSES_ONLY_FALLBACK = new Set([
43
49
  "gpt-5.6-luna",
@@ -267,6 +273,7 @@ export function createResponsesHandler(options) {
267
273
  stripUnsupportedGpt6ServiceTier(prepared.body, requestedModel, upstreamModel, priorityTierModel);
268
274
  if (requestedModel === CODEX_AUTO_REVIEW_MODEL) delete prepared.body.service_tier;
269
275
  if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
276
+ markResponseModel(upstreamModel);
270
277
  const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
271
278
  console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${streaming}`));
272
279
  const usesCustomTools = responsesBodyUsesCustomTools(prepared.body);
@@ -322,19 +329,21 @@ export function createResponsesHandler(options) {
322
329
  });
323
330
  const written = await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`, { signal: abort.signal });
324
331
  if (!written) return false;
325
- if (event === "response.completed") {
326
- rememberResponseHistory(prepared, data.response);
332
+ if (event === "response.completed" || event === "response.incomplete") {
333
+ if (event === "response.completed") rememberResponseHistory(prepared, data.response);
327
334
  recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
328
335
  }
329
336
  return true;
330
337
  }, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg, error, upstreamResponse) => {
331
338
  if (!res.headersSent) {
332
339
  if (upstreamResponse) {
340
+ markResponseErrorOrigin("upstream_http");
333
341
  markAdaptiveHttpTimeout(upstreamResponse.status);
334
342
  sendUpstreamError(res, upstreamResponse, errMsg);
335
343
  return;
336
344
  }
337
345
  const responseError = applyAdaptiveTimeout(error || Object.assign(new Error(errMsg), { statusCode }));
346
+ markResponseErrorOrigin("transport");
338
347
  sendJsonError(res, responseError, statusCode || 500);
339
348
  return;
340
349
  }
@@ -374,13 +383,17 @@ export function createResponsesHandler(options) {
374
383
  label: "Copilot Chat error body",
375
384
  });
376
385
  if (!upstream.ok) {
386
+ markResponseErrorOrigin("upstream_http");
377
387
  markAdaptiveHttpTimeout(upstream.status);
378
388
  sendUpstreamError(res, upstream, data);
379
389
  return;
380
390
  }
381
391
  const response = chatToResponses(JSON.parse(data), model);
392
+ markResponseTerminal(response.status, {
393
+ model: response.model || model, origin: "chat_completion", reason: response.incomplete_details?.reason,
394
+ });
382
395
  rememberResponseHistory(prepared, response);
383
- imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
396
+ if (response.status === "completed") imagePressure?.markSuccess?.(responseHistoryPressureRootId(prepared));
384
397
  recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
385
398
  res.writeHead(200, safeUpstreamResponseHeaders(upstream.headers, {
386
399
  contentType: "application/json",
@@ -388,6 +401,8 @@ export function createResponsesHandler(options) {
388
401
  res.end(JSON.stringify(response));
389
402
  } catch (error) {
390
403
  const responseError = applyAdaptiveTimeout(error);
404
+ markResponseErrorOrigin(responseError?.statusCode >= 400 && responseError.statusCode < 500
405
+ ? "client_validation" : "transport");
391
406
  logRequestFailure("Responses", responseError, abort);
392
407
  sendJsonError(res, responseError, 502);
393
408
  }
@@ -397,6 +412,8 @@ export function createResponsesHandler(options) {
397
412
  }
398
413
  } catch (error) {
399
414
  const responseError = applyAdaptiveTimeout(error);
415
+ markResponseErrorOrigin(responseError?.statusCode >= 400 && responseError.statusCode < 500
416
+ ? "client_validation" : "transport");
400
417
  logRequestFailure("Responses", responseError, abort);
401
418
  sendJsonError(res, responseError, 502);
402
419
  } finally {
@@ -40,6 +40,8 @@ import {
40
40
  isResponsesOutputEvent,
41
41
  markFirstOutput,
42
42
  markOutputTokens,
43
+ markResponseErrorOrigin,
44
+ markResponseTerminal,
43
45
  markStreamFailure,
44
46
  } from "./stream-performance.mjs";
45
47
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
@@ -146,6 +148,8 @@ function inspectResponseSseEvent(state, eventName, data) {
146
148
  if (eventType === "response.completed") {
147
149
  const response = event.response;
148
150
  state.completed = { response, event };
151
+ } else if (eventType === "response.incomplete") {
152
+ state.incomplete = { response: event.response, event };
149
153
  }
150
154
  return changed ? event : null;
151
155
  }
@@ -370,6 +374,7 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
370
374
  usedRetryPolicies = new Set(),
371
375
  } = opened;
372
376
  if (errorText !== undefined) {
377
+ markResponseErrorOrigin("upstream_http");
373
378
  sendUpstreamError(res, resp, errorText);
374
379
  return { successful: false, compacted: false, upstreamStatus: resp.status };
375
380
  }
@@ -386,6 +391,7 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
386
391
  const reader = resp.body.getReader();
387
392
  const streamState = {
388
393
  completed: null,
394
+ incomplete: null,
389
395
  failure: null,
390
396
  sawTerminal: false,
391
397
  sawOutput: false,
@@ -467,12 +473,28 @@ async function proxyStreamingResponses(opened, res, upstream, options) {
467
473
  }
468
474
  continue;
469
475
  }
470
- if (streamState.failure) markStreamFailure();
471
476
  if (holdPrelude && !await flushPrelude()) return { successful: false, compacted: false };
477
+ if (streamState.completed) {
478
+ markResponseTerminal("completed", {
479
+ model: streamState.completed.response.model || reqContext.body?.model,
480
+ origin: "upstream_response",
481
+ });
482
+ } else if (streamState.incomplete) {
483
+ markResponseTerminal("incomplete", {
484
+ model: streamState.incomplete.response.model || reqContext.body?.model,
485
+ origin: "upstream_response",
486
+ reason: streamState.incomplete.response.incomplete_details?.reason,
487
+ });
488
+ } else if (streamState.failure) {
489
+ markStreamFailure();
490
+ markResponseTerminal("failed", { model: reqContext.body?.model, origin: "upstream_event" });
491
+ }
472
492
  storeCompletedResponse(reqContext, streamState.completed);
473
493
  if (!res.writableEnded) res.end();
474
494
  return { successful: Boolean(streamState.completed), compacted: false };
475
495
  } catch (error) {
496
+ markResponseErrorOrigin("transport");
497
+ markStreamFailure();
476
498
  logRequestFailure("Responses", error, options.abort);
477
499
  writeHeaders();
478
500
  await endStreamWithError(res, error, options.abort);
@@ -502,6 +524,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
502
524
  const { resp, errorText, usedRetryPolicies = new Set() } = opened;
503
525
  reqContext = opened.reqContext;
504
526
  if (errorText !== undefined) {
527
+ markResponseErrorOrigin("upstream_http");
505
528
  sendUpstreamError(res, resp, errorText);
506
529
  return { successful: false, compacted: false, upstreamStatus: resp.status };
507
530
  }
@@ -528,6 +551,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
528
551
  response,
529
552
  event: response,
530
553
  });
554
+ markResponseTerminal("completed", { model: response.model || reqContext.body?.model, origin: "upstream_response" });
531
555
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
532
556
  contentType: "application/json",
533
557
  }));
@@ -572,6 +596,13 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
572
596
  response,
573
597
  event: response,
574
598
  });
599
+ if (["completed", "incomplete", "failed"].includes(response.status)) {
600
+ markResponseTerminal(response.status, {
601
+ model: response.model || reqContext.body?.model,
602
+ origin: "upstream_response",
603
+ reason: response.incomplete_details?.reason,
604
+ });
605
+ }
575
606
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
576
607
  contentType: "application/json",
577
608
  }));
@@ -581,6 +612,7 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
581
612
  res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
582
613
  contentType: "application/json",
583
614
  }));
615
+ markResponseErrorOrigin("upstream_http");
584
616
  res.end(data);
585
617
  return { successful: false, compacted: false };
586
618
  }
@@ -3,6 +3,13 @@ import { currentRequestContext } from "./request-context.mjs";
3
3
 
4
4
  const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact"]);
5
5
  const PREPARATION_STAGES = Object.freeze(["admission", "body", "history", "images", "serialization"]);
6
+ const TERMINAL_OUTCOMES = Object.freeze(["completed", "incomplete", "failed", "cancelled", "unknown"]);
7
+ const INCOMPLETE_REASONS = new Set(["max_output_tokens", "max_messages", "content_filter", "steered"]);
8
+ const TERMINAL_ORIGINS = new Set([
9
+ "upstream_response", "upstream_event", "chat_completion", "client_validation",
10
+ "upstream_http", "transport", "client_disconnect", "unclassified",
11
+ ]);
12
+ const MAX_MODEL_OUTCOME_LABELS = 32;
6
13
  const PREPARATION_EDGES_MS = Object.freeze([1, 2, 5, 10, 20, 50, 100, 250, 500, 1_000, 5_000, 30_000, 120_000]);
7
14
  const TTFT_EDGES_MS = Object.freeze([
8
15
  100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
@@ -78,6 +85,16 @@ function histogramSnapshot(histogram, unit) {
78
85
  };
79
86
  }
80
87
 
88
+ function emptyOutcomes() {
89
+ return Object.fromEntries(TERMINAL_OUTCOMES.map((outcome) => [outcome, 0]));
90
+ }
91
+
92
+ function modelLabel(value) {
93
+ const model = String(value || "").trim();
94
+ return model === "codex-auto-review" || /^gpt-[A-Za-z0-9._-]{1,72}$/.test(model)
95
+ ? model : "other";
96
+ }
97
+
81
98
  function createRoutePerformance() {
82
99
  return {
83
100
  ttft: createHistogram(TTFT_EDGES_MS),
@@ -88,6 +105,10 @@ function createRoutePerformance() {
88
105
  errors_with_output: 0,
89
106
  zero_output_errors: 0,
90
107
  neutral: 0,
108
+ terminalTotals: emptyOutcomes(),
109
+ incompleteReasons: Object.create(null),
110
+ terminalByModel: new Map(),
111
+ terminalByOrigin: Object.create(null),
91
112
  };
92
113
  }
93
114
 
@@ -97,6 +118,12 @@ function routeSnapshot(route) {
97
118
  errors_with_output: route.errors_with_output,
98
119
  zero_output_errors: route.zero_output_errors,
99
120
  neutral: route.neutral,
121
+ terminal_outcomes: {
122
+ totals: { ...route.terminalTotals },
123
+ incomplete_reasons: { ...route.incompleteReasons },
124
+ by_model: Object.fromEntries([...route.terminalByModel].map(([model, counts]) => [model, { ...counts }])),
125
+ by_origin: Object.fromEntries(Object.entries(route.terminalByOrigin).map(([origin, counts]) => [origin, { ...counts }])),
126
+ },
100
127
  ttft_ms: histogramSnapshot(route.ttft, "ms"),
101
128
  tpot_us: histogramSnapshot(route.tpot, "us"),
102
129
  request_ttft_ms: histogramSnapshot(route.requestTtft, "ms"),
@@ -117,6 +144,11 @@ export function createStreamPerformanceMetrics({ now = () => performance.now() }
117
144
  let outputTokens = null;
118
145
  let failed = false;
119
146
  let finished = false;
147
+ let model = "other";
148
+ let terminalOutcome = null;
149
+ let terminalOrigin = null;
150
+ let incompleteReason = null;
151
+ let errorOrigin = null;
120
152
 
121
153
  return {
122
154
  beginStage(stage) {
@@ -140,13 +172,37 @@ export function createStreamPerformanceMetrics({ now = () => performance.now() }
140
172
  const tokens = Number(value);
141
173
  if (Number.isFinite(tokens) && tokens >= 0) outputTokens = tokens;
142
174
  },
175
+ setModel(value) {
176
+ model = modelLabel(value);
177
+ },
178
+ setErrorOrigin(value) {
179
+ if (TERMINAL_ORIGINS.has(value)) errorOrigin = value;
180
+ },
181
+ terminal(outcome, { model: terminalModel, origin, reason } = {}) {
182
+ if (finished || terminalOutcome || !TERMINAL_OUTCOMES.includes(outcome)) return;
183
+ terminalOutcome = outcome;
184
+ if (terminalModel !== undefined) model = modelLabel(terminalModel);
185
+ if (TERMINAL_ORIGINS.has(origin)) terminalOrigin = origin;
186
+ if (outcome === "incomplete") incompleteReason = INCOMPLETE_REASONS.has(reason) ? reason : "other";
187
+ if (outcome !== "completed") failed = true;
188
+ },
143
189
  fail() {
144
190
  failed = true;
145
191
  },
146
- finish({ failed: finishFailed = false } = {}) {
192
+ finish({ failed: finishFailed = false, aborted = false } = {}) {
147
193
  if (finished) return;
148
194
  finished = true;
149
195
  failed ||= finishFailed;
196
+ const outcome = terminalOutcome || (aborted ? "cancelled" : "unknown");
197
+ const origin = terminalOrigin || errorOrigin || (aborted ? "client_disconnect" : "unclassified");
198
+ route.terminalTotals[outcome] += 1;
199
+ if (incompleteReason) route.incompleteReasons[incompleteReason] = (route.incompleteReasons[incompleteReason] || 0) + 1;
200
+ const label = route.terminalByModel.has(model) || route.terminalByModel.size < MAX_MODEL_OUTCOME_LABELS
201
+ ? model : "other";
202
+ if (!route.terminalByModel.has(label)) route.terminalByModel.set(label, emptyOutcomes());
203
+ route.terminalByModel.get(label)[outcome] += 1;
204
+ route.terminalByOrigin[origin] ||= emptyOutcomes();
205
+ route.terminalByOrigin[origin][outcome] += 1;
150
206
  const finishedAt = now();
151
207
  if (firstOutputAt !== null) observe(route.requestTtft, Math.max(0, firstOutputAt - requestStartedAt));
152
208
  if (upstreamStartedAt === null) {
@@ -212,3 +268,15 @@ export function markOutputTokens(value) {
212
268
  export function markStreamFailure() {
213
269
  tracker()?.fail();
214
270
  }
271
+
272
+ export function markResponseModel(model) {
273
+ tracker()?.setModel?.(model);
274
+ }
275
+
276
+ export function markResponseErrorOrigin(origin) {
277
+ tracker()?.setErrorOrigin?.(origin);
278
+ }
279
+
280
+ export function markResponseTerminal(outcome, options) {
281
+ tracker()?.terminal?.(outcome, options);
282
+ }