@juspay/neurolink 12.25.2 → 12.25.4

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.
@@ -1,6 +1,28 @@
1
1
  import type { NativeGenerateGuardConfig } from "../types/index.js";
2
2
  /** V3-shape adapter for the reclaim policy shared with streaming guards. */
3
3
  export declare function createNativeGenerateGuard(config: NativeGenerateGuardConfig): {
4
+ /**
5
+ * Record the prompt-token count the vendor reported for the step just
6
+ * sent.
7
+ *
8
+ * Paired with the estimate this guard made for that same step it yields a
9
+ * calibration ratio — how far the local estimator undercounts what the
10
+ * vendor actually charges — and that ratio scales the reclaim threshold,
11
+ * so the guard fires against real token accounting rather than its own.
12
+ * A non-finite or non-positive figure is ignored, leaving calibration at
13
+ * 1 rather than skewing it toward a number the vendor never reported.
14
+ */
4
15
  observeUsage(usage: unknown): void;
16
+ /**
17
+ * Reclaim context from `conversation` when it has crossed the guard's
18
+ * threshold, or return `undefined` to send it unchanged.
19
+ *
20
+ * The initial task prefix — every message up to and including the first
21
+ * user turn — is protected as a unit, so leading system messages cannot
22
+ * displace the task itself from entry zero. Only the entries strictly
23
+ * between that prefix and the newest `DEFAULT_LOOP_GUARD_PROTECTED_TAIL`
24
+ * are eligible for truncation or dropping under the shared reclaim
25
+ * policy; that protected tail — the live working set — is left intact.
26
+ */
5
27
  guardConversation(conversation: Array<Record<string, unknown>>): Array<Record<string, unknown>> | undefined;
6
28
  };
@@ -52,6 +52,17 @@ export function createNativeGenerateGuard(config) {
52
52
  let observedPromptTokens;
53
53
  const cost = (message) => estimateTokens(textOf(message), config.provider) + TOKENS_PER_MESSAGE;
54
54
  return {
55
+ /**
56
+ * Record the prompt-token count the vendor reported for the step just
57
+ * sent.
58
+ *
59
+ * Paired with the estimate this guard made for that same step it yields a
60
+ * calibration ratio — how far the local estimator undercounts what the
61
+ * vendor actually charges — and that ratio scales the reclaim threshold,
62
+ * so the guard fires against real token accounting rather than its own.
63
+ * A non-finite or non-positive figure is ignored, leaving calibration at
64
+ * 1 rather than skewing it toward a number the vendor never reported.
65
+ */
55
66
  observeUsage(usage) {
56
67
  const input = isRecord(usage) ? usage.inputTokens : undefined;
57
68
  const total = isRecord(input) ? input.total : input;
@@ -60,6 +71,17 @@ export function createNativeGenerateGuard(config) {
60
71
  ? total
61
72
  : undefined;
62
73
  },
74
+ /**
75
+ * Reclaim context from `conversation` when it has crossed the guard's
76
+ * threshold, or return `undefined` to send it unchanged.
77
+ *
78
+ * The initial task prefix — every message up to and including the first
79
+ * user turn — is protected as a unit, so leading system messages cannot
80
+ * displace the task itself from entry zero. Only the entries strictly
81
+ * between that prefix and the newest `DEFAULT_LOOP_GUARD_PROTECTED_TAIL`
82
+ * are eligible for truncation or dropping under the shared reclaim
83
+ * policy; that protected tail — the live working set — is left intact.
84
+ */
63
85
  guardConversation(conversation) {
64
86
  // Policy entry zero is protected. Fold the entire initial task prefix
65
87
  // into it so leading system messages cannot displace task protection.
@@ -322,17 +322,29 @@ export class ToolsManager {
322
322
  for (let pass = 0; pass < MAX_ENVELOPE_TIGHTENING_PASSES; pass++) {
323
323
  const allocations = waterFillTextBudget(sizes, textBudget);
324
324
  const replacements = new Map();
325
+ // Record the sizes here, log them only if this pass is the one that
326
+ // survives. A rejected pass never happened as far as the caller is
327
+ // concerned, and up to MAX_ENVELOPE_TIGHTENING_PASSES of them can be
328
+ // rejected on a single call — building their template strings, and the
329
+ // byteLength each one interpolates, is work paid whether or not debug
330
+ // logging is even enabled.
331
+ const truncatedItems = [];
325
332
  textIndexes.forEach((index, k) => {
326
333
  const item = content[index];
327
334
  const { preview, truncated, originalSize } = generateToolOutputPreview(item.text, { maxBytes: allocations[k], notice });
328
335
  if (truncated) {
329
- logger.debug(`[ToolsManager] Truncated '${toolName}' MCP content text item ${index}: ${originalSize} bytes → ${Buffer.byteLength(preview, "utf-8")} bytes (envelope budget ${maxBytes})`);
336
+ truncatedItems.push({ index, originalSize });
330
337
  replacements.set(index, preview);
331
338
  }
332
339
  });
333
340
  const candidate = rebuild(replacements);
334
341
  const candidateSize = serializedByteLength(candidate);
335
342
  if (candidateSize !== undefined && candidateSize <= maxBytes) {
343
+ if (logger.shouldLog("debug")) {
344
+ for (const { index, originalSize } of truncatedItems) {
345
+ logger.debug(`[ToolsManager] Truncated '${toolName}' MCP content text item ${index}: ${originalSize} bytes → ${Buffer.byteLength(replacements.get(index) ?? "", "utf-8")} bytes (envelope budget ${maxBytes})`);
346
+ }
347
+ }
336
348
  return candidate;
337
349
  }
338
350
  if (candidateSize === undefined || replacements.size === 0) {
@@ -66,6 +66,8 @@ export class CodexFallbackStreamError extends Error {
66
66
  }),
67
67
  inputTokensObserved: observed.inputTokensObserved,
68
68
  outputTokensObserved: observed.outputTokensObserved,
69
+ cacheReadTokensObserved: observed.cacheReadTokensObserved,
70
+ cacheCreationTokensObserved: observed.cacheCreationTokensObserved,
69
71
  }
70
72
  : usage;
71
73
  }
@@ -566,6 +568,8 @@ export function parseCodexFallbackSSE(sse) {
566
568
  }),
567
569
  inputTokensObserved: parsedUsage.inputTokensObserved,
568
570
  outputTokensObserved: parsedUsage.outputTokensObserved,
571
+ cacheReadTokensObserved: parsedUsage.cacheReadTokensObserved,
572
+ cacheCreationTokensObserved: parsedUsage.cacheCreationTokensObserved,
569
573
  };
570
574
  }
571
575
  textFromResponse = outputTextFromResponse(payload);
@@ -725,6 +729,8 @@ export async function createCodexFallbackStream(response, model) {
725
729
  }),
726
730
  inputTokensObserved: parsedUsage.inputTokensObserved,
727
731
  outputTokensObserved: parsedUsage.outputTokensObserved,
732
+ cacheReadTokensObserved: parsedUsage.cacheReadTokensObserved,
733
+ cacheCreationTokensObserved: parsedUsage.cacheCreationTokensObserved,
728
734
  };
729
735
  }
730
736
  const responseBody = payload.response;
@@ -789,10 +795,18 @@ export async function createCodexFallbackStream(response, model) {
789
795
  }, "empty_response", usage);
790
796
  }
791
797
  const finishReason = toolCalls.size > 0 ? "tool_use" : "end_turn";
798
+ // A count Codex never reported is omitted rather than sent as 0: this is
799
+ // what the Claude client reads, and a zero there is indistinguishable
800
+ // from an observed cache miss. `proxyTokenUsage` floors an absent count
801
+ // to 0, so the observation flags are the only thing that still knows.
792
802
  yield* serializer.finish(usage?.output, finishReason, {
793
803
  input_tokens: usage?.input,
794
- cache_read_input_tokens: usage?.cacheReadTokens,
795
- cache_creation_input_tokens: usage?.cacheCreationTokens,
804
+ cache_read_input_tokens: usage?.cacheReadTokensObserved
805
+ ? usage.cacheReadTokens
806
+ : undefined,
807
+ cache_creation_input_tokens: usage?.cacheCreationTokensObserved
808
+ ? usage.cacheCreationTokens
809
+ : undefined,
796
810
  });
797
811
  return {
798
812
  text: textParts.join(""),
@@ -45,6 +45,12 @@ import { sanitizeForLog } from "../utils/logSanitize.js";
45
45
  const nonNegativeInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
46
46
  ? Math.floor(value)
47
47
  : 0;
48
+ /**
49
+ * Whether a count was actually reported, matching what `nonNegativeInt` will
50
+ * keep. A negative or non-finite value is floored to zero there, so treating
51
+ * it as observed would record it as a real zero.
52
+ */
53
+ const observedCount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
48
54
  function usefulOutputItem(item) {
49
55
  if (!item || typeof item !== "object") {
50
56
  return false;
@@ -108,6 +114,8 @@ export function extractCodexUsage(payload) {
108
114
  outputTokensObserved: typeof output === "number" && Number.isFinite(output) && output >= 0,
109
115
  cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
110
116
  cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
117
+ cacheReadTokensObserved: observedCount(inputDetails?.cached_tokens),
118
+ cacheCreationTokensObserved: observedCount(inputDetails?.cache_write_tokens),
111
119
  reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
112
120
  reasoningTokensObserved: typeof outputDetails?.reasoning_tokens === "number" &&
113
121
  Number.isFinite(outputDetails.reasoning_tokens) &&
@@ -439,6 +439,10 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
439
439
  let recoveredAfterRetry = 0;
440
440
  let requestsWithUsage = 0;
441
441
  let requestsWithCacheRead = 0;
442
+ // Turns whose cache breakdown the provider actually reported. A turn that
443
+ // reported none cannot count as a miss, so it is excluded from the hit-rate
444
+ // denominator rather than folded in as a zero.
445
+ let requestsWithCacheObservation = 0;
442
446
  let cacheReadTokens = 0;
443
447
  let cacheCreationTokens = 0;
444
448
  let inputTokens = 0;
@@ -514,7 +518,10 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
514
518
  outputTokens += request.outputTokens ?? 0;
515
519
  cacheReadTokens += request.cacheReadTokens ?? 0;
516
520
  cacheCreationTokens += request.cacheCreationTokens ?? 0;
517
- requestsWithCacheRead += (request.cacheReadTokens ?? 0) > 0 ? 1 : 0;
521
+ if (request.cacheReadTokensObserved !== false) {
522
+ requestsWithCacheObservation += 1;
523
+ requestsWithCacheRead += (request.cacheReadTokens ?? 0) > 0 ? 1 : 0;
524
+ }
518
525
  if (request.model) {
519
526
  // Records written before `provider` existed carry only a model name.
520
527
  // "openai-compatible" resolves to the cross-provider table search in
@@ -557,6 +564,7 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
557
564
  cache: {
558
565
  requestsWithUsage,
559
566
  requestsWithCacheRead,
567
+ requestsWithCacheObservation,
560
568
  cacheReadTokens,
561
569
  cacheCreationTokens,
562
570
  inputTokens,
@@ -567,8 +575,8 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
567
575
  modelsPricedByPrefix: [...modelsPricedByPrefix].sort(),
568
576
  requestsUnpriced,
569
577
  unpricedModels: [...unpricedModels].sort(),
570
- requestHitRate: requestsWithUsage > 0
571
- ? Number((requestsWithCacheRead / requestsWithUsage).toFixed(4))
578
+ requestHitRate: requestsWithCacheObservation > 0
579
+ ? Number((requestsWithCacheRead / requestsWithCacheObservation).toFixed(4))
572
580
  : null,
573
581
  },
574
582
  finalRequestLatency,
@@ -1080,6 +1088,16 @@ export async function analyzeProxyLogs(options) {
1080
1088
  outputTokens: finiteNumber(record.outputTokens),
1081
1089
  cacheReadTokens: finiteNumber(record.cacheReadTokens),
1082
1090
  cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
1091
+ // Read as a boolean only. An absent flag means observed, which keeps
1092
+ // every record written before the flag existed counting as it did.
1093
+ // The merge below drops undefined from the later record, so a
1094
+ // headers-only first record cannot erase a flag the body record sets.
1095
+ cacheReadTokensObserved: typeof record.cacheReadTokensObserved === "boolean"
1096
+ ? record.cacheReadTokensObserved
1097
+ : undefined,
1098
+ cacheCreationTokensObserved: typeof record.cacheCreationTokensObserved === "boolean"
1099
+ ? record.cacheCreationTokensObserved
1100
+ : undefined,
1083
1101
  errorType: stringValue(record.errorType),
1084
1102
  errorCode: stringValue(record.errorCode),
1085
1103
  routingDecision,
@@ -385,8 +385,14 @@ class ProxyTracer {
385
385
  "ai.tokens.input": billingUsage.input,
386
386
  "ai.tokens.output": ctx.outputTokens,
387
387
  "ai.tokens.total": totalTokens,
388
- "ai.tokens.cache_creation": ctx.cacheCreationTokens,
389
- "ai.tokens.cache_read": ctx.cacheReadTokens,
388
+ // An unreported cache count is left off the span rather than written as
389
+ // zero, which would read back as an observed cache miss.
390
+ ...(ctx.cacheCreationTokensObserved === false
391
+ ? {}
392
+ : { "ai.tokens.cache_creation": ctx.cacheCreationTokens }),
393
+ ...(ctx.cacheReadTokensObserved === false
394
+ ? {}
395
+ : { "ai.tokens.cache_read": ctx.cacheReadTokens }),
390
396
  });
391
397
  if (ctx.reasoningTokens !== undefined) {
392
398
  this.rootSpan.setAttribute("ai.tokens.reasoning", ctx.reasoningTokens);
@@ -687,8 +693,15 @@ class ProxyTracer {
687
693
  };
688
694
  m.tokensInput.add(proxyTokenUsage(this.usage).input, tokenLabels);
689
695
  m.tokensOutput.add(this.usage.outputTokens, tokenLabels);
690
- m.tokensCacheRead.add(this.usage.cacheReadTokens, tokenLabels);
691
- m.tokensCacheCreation.add(this.usage.cacheCreationTokens, tokenLabels);
696
+ // Skipped, not zeroed, when the provider reported no breakdown: a zero
697
+ // here is read back as an observed cache miss and drags the measured
698
+ // cache rate down by however many such replies arrive.
699
+ if (this.usage.cacheReadTokensObserved !== false) {
700
+ m.tokensCacheRead.add(this.usage.cacheReadTokens, tokenLabels);
701
+ }
702
+ if (this.usage.cacheCreationTokensObserved !== false) {
703
+ m.tokensCacheCreation.add(this.usage.cacheCreationTokens, tokenLabels);
704
+ }
692
705
  if (this.usage.reasoningTokens) {
693
706
  m.tokensReasoning.add(this.usage.reasoningTokens, tokenLabels);
694
707
  }
@@ -144,6 +144,13 @@ export const __requestLoggerTestHooks = {
144
144
  defaultFlushTimeoutMs: REQUEST_LOG_FLUSH_TIMEOUT_MS,
145
145
  pendingOperationCount: () => pendingLogOperations.size,
146
146
  trackLogOperation,
147
+ /**
148
+ * Pricing annotation, exposed because it is otherwise reachable only through
149
+ * a full log-write cycle. What it refuses to price matters as much as what it
150
+ * prices: a turn whose cached portion is unknown must not be charged as if
151
+ * the cache were empty.
152
+ */
153
+ annotateRequestPricing,
147
154
  setAppendFileForTests: (writer) => {
148
155
  appendMetadataFile = writer;
149
156
  },
@@ -226,7 +233,14 @@ function annotateRequestPricing(entry) {
226
233
  !Number.isFinite(cacheCreation) ||
227
234
  cacheCreation < 0 ||
228
235
  (entry.inputIncludesCachedTokens === true &&
229
- cacheRead + cacheCreation > input)) {
236
+ cacheRead + cacheCreation > input) ||
237
+ // When input includes the cached portion but the provider never reported
238
+ // it, subtracting a defaulted 0 prices the cached tokens at full input
239
+ // rate and reports zero cache savings. That is a guess, not a price, so
240
+ // the turn is incomplete rather than silently mispriced.
241
+ (entry.inputIncludesCachedTokens === true &&
242
+ (entry.cacheReadTokensObserved === false ||
243
+ entry.cacheCreationTokensObserved === false))) {
230
244
  entry.pricingStatus = "usage_incomplete";
231
245
  return;
232
246
  }
@@ -3909,8 +3909,14 @@ async function executeClaudeCodexFallback(args) {
3909
3909
  tracer?.setUsage({
3910
3910
  inputTokens: result.usage.input,
3911
3911
  outputTokens: result.usage.output,
3912
+ // `?? 0` alone would record a Codex reply that carried no
3913
+ // `input_tokens_details` as an observed cache miss. This is the
3914
+ // leg most fallback traffic actually takes, so it is the one that
3915
+ // skews the measured Codex cache rate.
3912
3916
  cacheReadTokens: result.usage.cacheReadTokens ?? 0,
3913
3917
  cacheCreationTokens: result.usage.cacheCreationTokens ?? 0,
3918
+ cacheReadTokensObserved: result.usage.cacheReadTokensObserved === true,
3919
+ cacheCreationTokensObserved: result.usage.cacheCreationTokensObserved === true,
3914
3920
  reasoningTokens: result.usage.reasoning,
3915
3921
  }, { recordMetrics: false });
3916
3922
  }
@@ -3951,8 +3957,14 @@ async function executeClaudeCodexFallback(args) {
3951
3957
  outputTokens: result?.usage?.outputTokensObserved
3952
3958
  ? result.usage.output
3953
3959
  : undefined,
3954
- cacheCreationTokens: result?.usage?.cacheCreationTokens,
3955
- cacheReadTokens: result?.usage?.cacheReadTokens,
3960
+ cacheCreationTokens: result?.usage?.cacheCreationTokensObserved
3961
+ ? result.usage.cacheCreationTokens
3962
+ : undefined,
3963
+ cacheReadTokens: result?.usage?.cacheReadTokensObserved
3964
+ ? result.usage.cacheReadTokens
3965
+ : undefined,
3966
+ cacheReadTokensObserved: result?.usage?.cacheReadTokensObserved === true,
3967
+ cacheCreationTokensObserved: result?.usage?.cacheCreationTokensObserved === true,
3956
3968
  reasoningTokens: result?.usage?.reasoning,
3957
3969
  errorCode: failure?.code,
3958
3970
  retryable: failure?.retryable,
@@ -6394,8 +6406,14 @@ function createClaudeRequestRuntimeContext(args) {
6394
6406
  outputTokens: fallbackFailureUsage.outputTokensObserved
6395
6407
  ? fallbackFailureUsage.output
6396
6408
  : undefined,
6397
- cacheReadTokens: fallbackFailureUsage.cacheReadTokens,
6398
- cacheCreationTokens: fallbackFailureUsage.cacheCreationTokens,
6409
+ cacheReadTokens: fallbackFailureUsage.cacheReadTokensObserved
6410
+ ? fallbackFailureUsage.cacheReadTokens
6411
+ : undefined,
6412
+ cacheCreationTokens: fallbackFailureUsage.cacheCreationTokensObserved
6413
+ ? fallbackFailureUsage.cacheCreationTokens
6414
+ : undefined,
6415
+ cacheReadTokensObserved: fallbackFailureUsage.cacheReadTokensObserved === true,
6416
+ cacheCreationTokensObserved: fallbackFailureUsage.cacheCreationTokensObserved === true,
6399
6417
  reasoningTokens: fallbackFailureUsage.reasoning,
6400
6418
  ...extra,
6401
6419
  };
@@ -6420,6 +6438,8 @@ function createClaudeRequestRuntimeContext(args) {
6420
6438
  outputTokens: fallbackFailureUsage.output,
6421
6439
  cacheReadTokens: fallbackFailureUsage.cacheReadTokens ?? 0,
6422
6440
  cacheCreationTokens: fallbackFailureUsage.cacheCreationTokens ?? 0,
6441
+ cacheReadTokensObserved: fallbackFailureUsage.cacheReadTokensObserved === true,
6442
+ cacheCreationTokensObserved: fallbackFailureUsage.cacheCreationTokensObserved === true,
6423
6443
  reasoningTokens: fallbackFailureUsage.reasoning,
6424
6444
  }, { recordMetrics: false });
6425
6445
  }
@@ -440,6 +440,15 @@ function captureCodexResponse(ctx, response, phase, account, attempt) {
440
440
  headers: response.headers,
441
441
  });
442
442
  }
443
+ /**
444
+ * Serve one native `/backend-api/codex/responses` turn.
445
+ *
446
+ * Owns the whole hop: account selection and rotation, the upstream request,
447
+ * the SSE relay back to the caller, and the terminal accounting that turns the
448
+ * observed wire usage into request-log and tracer records. Usage counts the
449
+ * provider did not report are carried as unobserved rather than zero, so a
450
+ * silent cache breakdown is never recorded as a cache miss.
451
+ */
443
452
  async function executeCodexResponsesRequest(ctx) {
444
453
  const requestStartTime = Date.now();
445
454
  let body = (ctx.body ?? {});
@@ -889,8 +898,12 @@ async function executeCodexResponsesRequest(ctx) {
889
898
  outputTokens: usage.outputTokensObserved
890
899
  ? usage.outputTokens
891
900
  : undefined,
892
- cacheReadTokens: usage.cacheReadTokens,
893
- cacheCreationTokens: usage.cacheCreationTokens,
901
+ cacheReadTokens: usage.cacheReadTokensObserved
902
+ ? usage.cacheReadTokens
903
+ : undefined,
904
+ cacheCreationTokens: usage.cacheCreationTokensObserved
905
+ ? usage.cacheCreationTokens
906
+ : undefined,
894
907
  reasoningTokens: usage.reasoningTokensObserved
895
908
  ? usage.reasoningTokens
896
909
  : undefined,
@@ -904,6 +917,8 @@ async function executeCodexResponsesRequest(ctx) {
904
917
  reasoningTokens: usage.reasoningTokensObserved
905
918
  ? usage.reasoningTokens
906
919
  : undefined,
920
+ cacheReadTokensObserved: usage.cacheReadTokensObserved === true,
921
+ cacheCreationTokensObserved: usage.cacheCreationTokensObserved === true,
907
922
  inputIncludesCachedTokens: true,
908
923
  });
909
924
  }
@@ -184,6 +184,8 @@ export type CodexFallbackResult = {
184
184
  /** Numeric serializer compatibility must not imply provider observation. */
185
185
  inputTokensObserved?: boolean;
186
186
  outputTokensObserved?: boolean;
187
+ cacheReadTokensObserved?: boolean;
188
+ cacheCreationTokensObserved?: boolean;
187
189
  };
188
190
  finishReason: "end_turn" | "tool_use";
189
191
  };
@@ -1685,7 +1685,13 @@ export type NativeGenerateLoopArgs = {
1685
1685
  };
1686
1686
  export type NativeGenerateLoopResult = {
1687
1687
  text: string;
1688
- /** Joined reasoning content parts from the final step, when the vendor sent any. */
1688
+ /**
1689
+ * Joined reasoning content parts from EVERY step, when the vendor sent any.
1690
+ *
1691
+ * Was final-step-only until the native generate loop began accumulating
1692
+ * across steps; a multi-step turn would otherwise report only the reasoning
1693
+ * that happened after its last tool call.
1694
+ */
1689
1695
  reasoning?: string;
1690
1696
  finishReason: string;
1691
1697
  rawFinishReason?: string;
@@ -676,6 +676,15 @@ export type RequestLogEntry = {
676
676
  outputTokens?: number;
677
677
  cacheCreationTokens?: number;
678
678
  cacheReadTokens?: number;
679
+ /**
680
+ * Whether the provider reported each cache count. Omitted means observed,
681
+ * so every path that genuinely reports a breakdown is unchanged. When
682
+ * false the count is absent rather than zero, and the reporting and
683
+ * pricing paths must not read it as a cache miss: a zero folded into a
684
+ * hit-rate denominator biases every rate built on these records down.
685
+ */
686
+ cacheReadTokensObserved?: boolean;
687
+ cacheCreationTokensObserved?: boolean;
679
688
  /** Reasoning tokens are a subset of output, never additional usage. */
680
689
  reasoningTokens?: number;
681
690
  /**
@@ -1032,6 +1041,9 @@ export type ClaudeFinalRequestLogger = (status: number, accountLabel: string, ac
1032
1041
  outputTokens?: number;
1033
1042
  cacheCreationTokens?: number;
1034
1043
  cacheReadTokens?: number;
1044
+ /** False when the provider reported no cache breakdown; see RequestLogEntry. */
1045
+ cacheReadTokensObserved?: boolean;
1046
+ cacheCreationTokensObserved?: boolean;
1035
1047
  reasoningTokens?: number;
1036
1048
  errorCode?: string;
1037
1049
  retryable?: boolean;
@@ -1899,6 +1911,15 @@ export type UsageContext = {
1899
1911
  outputTokens: number;
1900
1912
  cacheCreationTokens: number;
1901
1913
  cacheReadTokens: number;
1914
+ /**
1915
+ * Whether the provider actually reported each cache count. Omitted means
1916
+ * observed, so every path that genuinely reports a breakdown is unchanged.
1917
+ * When false the count is not recorded at all, because a zero written into
1918
+ * `proxy_tokens_cache_read` is indistinguishable from a real cache miss and
1919
+ * biases every rate built on that counter downward.
1920
+ */
1921
+ cacheReadTokensObserved?: boolean;
1922
+ cacheCreationTokensObserved?: boolean;
1902
1923
  reasoningTokens?: number;
1903
1924
  rateLimitAfter5h?: number;
1904
1925
  rateLimitAfter7d?: number;
@@ -2223,6 +2244,12 @@ export type ProxyAnalysisReport = {
2223
2244
  cache: {
2224
2245
  requestsWithUsage: number;
2225
2246
  requestsWithCacheRead: number;
2247
+ /**
2248
+ * Turns whose cache breakdown the provider actually reported, and the
2249
+ * denominator of `requestHitRate`. A turn that reported none is not a
2250
+ * miss, so counting it would bias the rate down.
2251
+ */
2252
+ requestsWithCacheObservation: number;
2226
2253
  cacheReadTokens: number;
2227
2254
  cacheCreationTokens: number;
2228
2255
  /** Ordinary input, excluding the separately reported cache read/write buckets. */
@@ -2294,6 +2321,13 @@ export type ProxyAnalysisFinalRequestRecord = {
2294
2321
  outputTokens: number | null;
2295
2322
  cacheReadTokens: number | null;
2296
2323
  cacheCreationTokens: number | null;
2324
+ /**
2325
+ * False when the provider reported no cache breakdown. Such a turn is not a
2326
+ * cache miss, so it is excluded from the hit-rate denominator rather than
2327
+ * counted as a zero.
2328
+ */
2329
+ cacheReadTokensObserved?: boolean;
2330
+ cacheCreationTokensObserved?: boolean;
2297
2331
  errorType: string | null;
2298
2332
  errorCode: string | null;
2299
2333
  routingDecision: ProxyAccountRoutingDecision | null;
@@ -2328,6 +2362,22 @@ export type CodexStreamUsage = {
2328
2362
  cacheReadTokens: number;
2329
2363
  /** Cache writes, which bill at a premium over both reads and plain input. */
2330
2364
  cacheCreationTokens: number;
2365
+ /**
2366
+ * Whether the provider supplied each cache count, tracked per field.
2367
+ *
2368
+ * Without this a reply that omits `input_tokens_details` is recorded as a
2369
+ * total cache miss rather than as unknown, which biases every measured cache
2370
+ * rate downward. Input, output and reasoning already carry this distinction.
2371
+ *
2372
+ * The two fields are tracked separately because a reply can carry one and
2373
+ * not the other: a single flag covering both would let a missing
2374
+ * `cache_write_tokens` ride in on an observed `cached_tokens` and be
2375
+ * recorded as a real zero. A count that is present but not a finite,
2376
+ * non-negative number is not observed either — `nonNegativeInt` floors it to
2377
+ * zero, which is indistinguishable from a genuine zero once recorded.
2378
+ */
2379
+ cacheReadTokensObserved?: boolean;
2380
+ cacheCreationTokensObserved?: boolean;
2331
2381
  reasoningTokens: number;
2332
2382
  /** Whether the provider supplied a valid reasoning breakdown. */
2333
2383
  reasoningTokensObserved?: boolean;
@@ -1374,7 +1374,8 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1374
1374
  // ordinary "transcribe this recording" request — took the text-only branch
1375
1375
  // below, where the collected bytes have nowhere to go and only the metadata
1376
1376
  // summary survives.
1377
- const hasNativeAudio = (inp.nativeAudioFiles?.length ?? 0) > 0 && supportsNativeAudio(provider);
1377
+ const hasNativeAudio = (inp.nativeAudioFiles?.length ?? 0) > 0 &&
1378
+ supportsNativeAudio(provider, model);
1378
1379
  // If no images, PDFs or audio, use standard message building and convert to MultimodalChatMessage[]
1379
1380
  if (!hasImages && !hasPDFs && !hasNativeAudio) {
1380
1381
  // #289: CSV content[] items don't need vision, so they never reach the
@@ -1574,7 +1575,7 @@ async function appendCsvContentToText(csvItems, baseText) {
1574
1575
  /**
1575
1576
  * Convert advanced content format to provider-specific format
1576
1577
  */
1577
- async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = [], outputFormat) {
1578
+ async function convertContentToProviderFormat(content, provider, model, pdfOptions, audioFiles = [], outputFormat) {
1578
1579
  const textContent = content.find((c) => c.type === "text");
1579
1580
  const imageContent = content.filter((c) => c.type === "image");
1580
1581
  const pdfContent = content.filter((c) => c.type === "pdf");
@@ -1596,7 +1597,7 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
1596
1597
  // change exists to stop, reached through a different branch. Gated on the
1597
1598
  // provider accepting audio so one that cannot keeps the cheaper plain-text
1598
1599
  // shape rather than an array carrying a part it will ignore.
1599
- const deliversAudio = audioFiles.length > 0 && supportsNativeAudio(provider);
1600
+ const deliversAudio = audioFiles.length > 0 && supportsNativeAudio(provider, model);
1600
1601
  const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0 || deliversAudio;
1601
1602
  // Validate that we have at least some content
1602
1603
  if (!hasMultimodal && !text) {
@@ -1627,7 +1628,7 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
1627
1628
  await enforceAggregatePdfLimits(pdfFiles, provider, {
1628
1629
  trustSuppliedPageCounts: false,
1629
1630
  });
1630
- return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model, {
1631
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, model, {
1631
1632
  audioFiles,
1632
1633
  outputFormat,
1633
1634
  });
@@ -2225,7 +2226,7 @@ mediaOptions = {}) {
2225
2226
  // itself rather than replacing the description — a model asked "how long is
2226
2227
  // this?" keeps the exact answer, and one asked "what is said?" can now
2227
2228
  // answer at all.
2228
- if (audioFiles.length > 0 && supportsNativeAudio(provider)) {
2229
+ if (audioFiles.length > 0 && supportsNativeAudio(provider, model)) {
2229
2230
  for (const audio of audioFiles) {
2230
2231
  // Derived from the trimmed basename so a directory containing a dot
2231
2232
  // (`/srv/v1.2/recording`) cannot be mistaken for the file's extension.