@juspay/neurolink 12.15.0 → 12.15.1

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.
@@ -16,6 +16,7 @@
16
16
  import { SpanStatusCode, SpanKind, ROOT_CONTEXT, isSpanContextValid, propagation, context, metrics, trace, } from "@opentelemetry/api";
17
17
  import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
18
18
  import { OtelBridge } from "../observability/otelBridge.js";
19
+ import { proxyTokenUsage } from "./proxyTokenUsage.js";
19
20
  import { calculateCost } from "../utils/pricing.js";
20
21
  import { TelemetryService } from "../telemetry/telemetryService.js";
21
22
  import { logger } from "../utils/logger.js";
@@ -360,17 +361,22 @@ class ProxyTracer {
360
361
  // Non-fatal
361
362
  });
362
363
  }
364
+ /** Attribute fallback metrics to the account that actually served them. */
365
+ setServedAccount(account, accountType) {
366
+ this.accountEmail = account;
367
+ this.rootSpan.setAttributes({
368
+ "proxy.account.served": account,
369
+ "proxy.account.served_type": accountType,
370
+ });
371
+ }
363
372
  /** Record token usage and cost on the root span. */
364
373
  setUsage(ctx) {
365
374
  this.usage = ctx;
366
- const totalTokens = ctx.inputTokens +
367
- ctx.outputTokens +
368
- ctx.cacheCreationTokens +
369
- ctx.cacheReadTokens +
370
- (ctx.reasoningTokens ?? 0);
371
- // NeuroLink-format token attributes (from SpanAttributes)
375
+ const billingUsage = proxyTokenUsage(ctx);
376
+ const totalTokens = billingUsage.total;
377
+ // Custom token buckets are disjoint; standard GenAI input includes cache.
372
378
  this.rootSpan.setAttributes({
373
- "ai.tokens.input": ctx.inputTokens,
379
+ "ai.tokens.input": billingUsage.input,
374
380
  "ai.tokens.output": ctx.outputTokens,
375
381
  "ai.tokens.total": totalTokens,
376
382
  "ai.tokens.cache_creation": ctx.cacheCreationTokens,
@@ -381,18 +387,12 @@ class ProxyTracer {
381
387
  }
382
388
  // GenAI semantic convention attributes (for Langfuse compatibility)
383
389
  this.rootSpan.setAttributes({
384
- "gen_ai.usage.input_tokens": ctx.inputTokens,
390
+ "gen_ai.usage.input_tokens": totalTokens - ctx.outputTokens,
385
391
  "gen_ai.usage.output_tokens": ctx.outputTokens,
386
392
  "gen_ai.usage.total_tokens": totalTokens,
387
393
  });
388
394
  // Cost calculation via pricing.ts
389
- const cost = calculateCost(this.billingProvider, this.model, {
390
- input: ctx.inputTokens,
391
- output: ctx.outputTokens,
392
- total: totalTokens,
393
- cacheCreationTokens: ctx.cacheCreationTokens,
394
- cacheReadTokens: ctx.cacheReadTokens,
395
- });
395
+ const cost = calculateCost(this.billingProvider, this.model, billingUsage);
396
396
  if (cost > 0) {
397
397
  this.rootSpan.setAttributes({
398
398
  "ai.cost.total": cost,
@@ -665,7 +665,7 @@ class ProxyTracer {
665
665
  model: this.model,
666
666
  account: this.accountEmail ?? "unknown",
667
667
  };
668
- m.tokensInput.add(this.usage.inputTokens, tokenLabels);
668
+ m.tokensInput.add(proxyTokenUsage(this.usage).input, tokenLabels);
669
669
  m.tokensOutput.add(this.usage.outputTokens, tokenLabels);
670
670
  m.tokensCacheRead.add(this.usage.cacheReadTokens, tokenLabels);
671
671
  m.tokensCacheCreation.add(this.usage.cacheCreationTokens, tokenLabels);
@@ -673,18 +673,8 @@ class ProxyTracer {
673
673
  m.tokensReasoning.add(this.usage.reasoningTokens, tokenLabels);
674
674
  }
675
675
  // Cost
676
- const totalTokens = this.usage.inputTokens +
677
- this.usage.outputTokens +
678
- this.usage.cacheCreationTokens +
679
- this.usage.cacheReadTokens +
680
- (this.usage.reasoningTokens ?? 0);
681
- const cost = calculateCost(this.billingProvider, this.model, {
682
- input: this.usage.inputTokens,
683
- output: this.usage.outputTokens,
684
- total: totalTokens,
685
- cacheCreationTokens: this.usage.cacheCreationTokens,
686
- cacheReadTokens: this.usage.cacheReadTokens,
687
- });
676
+ const billingUsage = proxyTokenUsage(this.usage);
677
+ const cost = calculateCost(this.billingProvider, this.model, billingUsage);
688
678
  if (cost > 0) {
689
679
  m.costTotal.add(cost, tokenLabels);
690
680
  }
@@ -711,19 +701,10 @@ class ProxyTracer {
711
701
  if (!this.usage) {
712
702
  return;
713
703
  }
714
- const totalTokens = this.usage.inputTokens +
715
- this.usage.outputTokens +
716
- this.usage.cacheCreationTokens +
717
- this.usage.cacheReadTokens +
718
- (this.usage.reasoningTokens ?? 0);
704
+ const billingUsage = proxyTokenUsage(this.usage);
705
+ const totalTokens = billingUsage.total;
719
706
  const durationMs = Date.now() - this.startTime;
720
- const cost = calculateCost(this.billingProvider, this.model, {
721
- input: this.usage.inputTokens,
722
- output: this.usage.outputTokens,
723
- total: totalTokens,
724
- cacheCreationTokens: this.usage.cacheCreationTokens,
725
- cacheReadTokens: this.usage.cacheReadTokens,
726
- });
707
+ const cost = calculateCost(this.billingProvider, this.model, billingUsage);
727
708
  TelemetryService.getInstance().recordAIRequest(this.billingProvider, this.model, totalTokens, durationMs, cost > 0 ? cost : undefined);
728
709
  }
729
710
  // -------------------------------------------------------------------------
@@ -133,10 +133,14 @@ export function buildQuotaResponseHeaders(context, now = Date.now()) {
133
133
  }
134
134
  const quota = context.quota;
135
135
  if (quota && context.source !== "none") {
136
- set("x-neurolink-quota-session-left-pct", utilizationToLeftPct(quota.sessionUsed));
136
+ set("x-neurolink-quota-session-left-pct", quota.sessionStatus === "unknown"
137
+ ? undefined
138
+ : utilizationToLeftPct(quota.sessionUsed));
137
139
  set("x-neurolink-quota-session-status", quota.sessionStatus);
138
140
  set("x-neurolink-quota-session-resets-in", secondsUntil(quota.sessionResetAt, now));
139
- set("x-neurolink-quota-weekly-left-pct", utilizationToLeftPct(quota.weeklyUsed));
141
+ set("x-neurolink-quota-weekly-left-pct", quota.weeklyStatus === "unknown"
142
+ ? undefined
143
+ : utilizationToLeftPct(quota.weeklyUsed));
140
144
  set("x-neurolink-quota-weekly-status", quota.weeklyStatus);
141
145
  set("x-neurolink-quota-weekly-resets-in", secondsUntil(quota.weeklyResetAt, now));
142
146
  set("x-neurolink-quota-unified-status", quota.unifiedStatus);
@@ -173,7 +177,7 @@ export function summarizePoolHeadroom(entries, now = Date.now()) {
173
177
  continue;
174
178
  }
175
179
  available += 1;
176
- if (entry.quota) {
180
+ if (entry.quota && entry.quota.sessionStatus !== "unknown") {
177
181
  const left = utilizationToLeftPct(entry.quota.sessionUsed);
178
182
  if (bestSessionLeftPct === undefined || left > bestSessionLeftPct) {
179
183
  bestSessionLeftPct = left;
@@ -3693,6 +3693,25 @@ async function executeClaudeCodexFallback(args) {
3693
3693
  registerProxyResponseObserver(ctx.metadata, {
3694
3694
  onTerminal: () => childAccounting,
3695
3695
  });
3696
+ const recordCodexUsage = (result) => {
3697
+ ctx.metadata.codexFallbackModel = model;
3698
+ try {
3699
+ tracer?.setModelSubstitution(body.model, model, "openai");
3700
+ tracer?.setServedAccount(codexHeaders["x-neurolink-account"] ?? "unknown", "codex-oauth");
3701
+ if (result?.usage) {
3702
+ tracer?.setUsage({
3703
+ inputTokens: result.usage.input,
3704
+ outputTokens: result.usage.output,
3705
+ cacheReadTokens: result.usage.cacheReadTokens ?? 0,
3706
+ cacheCreationTokens: result.usage.cacheCreationTokens ?? 0,
3707
+ reasoningTokens: result.usage.reasoning,
3708
+ });
3709
+ }
3710
+ }
3711
+ catch {
3712
+ // Instrumentation must never turn completed output into a failed stream.
3713
+ }
3714
+ };
3696
3715
  const codexHeaders = { ...(codexCtx.responseHeaders ?? {}) };
3697
3716
  if (body.stream) {
3698
3717
  const bridge = await createCodexFallbackStream(codexResponse, body.model);
@@ -3712,12 +3731,14 @@ async function executeClaudeCodexFallback(args) {
3712
3731
  if (status >= 400) {
3713
3732
  ctx.metadata.terminalErrorType = errorType;
3714
3733
  }
3734
+ recordCodexUsage(result);
3715
3735
  tracer?.end(status, Date.now() - requestStartTime);
3716
3736
  logFinalRequest(status, account, accountType, errorType, message, {
3717
3737
  inputTokens: result?.usage?.input,
3718
3738
  outputTokens: result?.usage?.output,
3719
3739
  cacheCreationTokens: result?.usage?.cacheCreationTokens,
3720
3740
  cacheReadTokens: result?.usage?.cacheReadTokens,
3741
+ reasoningTokens: result?.usage?.reasoning,
3721
3742
  });
3722
3743
  recordFallbackAttempt({
3723
3744
  provider: "codex",
@@ -3862,6 +3883,7 @@ async function executeClaudeCodexFallback(args) {
3862
3883
  ...(parsed.usage ? { usage: parsed.usage } : {}),
3863
3884
  toolCalls: parsed.toolCalls,
3864
3885
  };
3886
+ recordCodexUsage(parsed);
3865
3887
  tracer?.end(200, Date.now() - requestStartTime);
3866
3888
  const clientResponse = serializeClaudeResponse(internal, body.model);
3867
3889
  observeClaudeJsonOutput(ctx.metadata, clientResponse);
@@ -3870,6 +3892,7 @@ async function executeClaudeCodexFallback(args) {
3870
3892
  outputTokens: parsed.usage?.output,
3871
3893
  cacheCreationTokens: parsed.usage?.cacheCreationTokens,
3872
3894
  cacheReadTokens: parsed.usage?.cacheReadTokens,
3895
+ reasoningTokens: parsed.usage?.reasoning,
3873
3896
  });
3874
3897
  const clientResponseText = JSON.stringify(clientResponse);
3875
3898
  logProxyBody({
@@ -6009,7 +6032,15 @@ function createClaudeRequestRuntimeContext(args) {
6009
6032
  requestId: ctx.requestId,
6010
6033
  method: ctx.method,
6011
6034
  path: ctx.path,
6012
- model: body.model,
6035
+ model: typeof ctx.metadata.codexFallbackModel === "string"
6036
+ ? ctx.metadata.codexFallbackModel
6037
+ : body.model,
6038
+ ...(typeof ctx.metadata.codexFallbackModel === "string"
6039
+ ? {
6040
+ requestedModel: body.model,
6041
+ inputIncludesCachedTokens: false,
6042
+ }
6043
+ : {}),
6013
6044
  stream: !!body.stream,
6014
6045
  toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
6015
6046
  account: finalAccountLabel ?? "",
@@ -6042,6 +6073,9 @@ function createClaudeRequestRuntimeContext(args) {
6042
6073
  ...(extra?.cacheReadTokens !== undefined
6043
6074
  ? { cacheReadTokens: extra.cacheReadTokens }
6044
6075
  : {}),
6076
+ ...(extra?.reasoningTokens !== undefined
6077
+ ? { reasoningTokens: extra.reasoningTokens }
6078
+ : {}),
6045
6079
  ...(traceCtx
6046
6080
  ? { traceId: traceCtx.traceId, spanId: traceCtx.spanId }
6047
6081
  : {}),
@@ -35,19 +35,15 @@ declare function refreshCodexTokenOnce(key: string, refreshToken: string): Promi
35
35
  */
36
36
  declare function loadCodexProxyAccounts(): Promise<CodexRuntimeAccount[]>;
37
37
  /**
38
- * Order accounts fill-first by quota: eligible (not cooling) first, then least
39
- * session utilization, treating unknown quota as "probe first" so a fresh
40
- * account gets observed rather than starved.
38
+ * Order non-cooling accounts ahead of cooling ones, then defer rejected quota.
39
+ * Within each group, probe unknown quota first and otherwise fill the least-used
40
+ * session so fresh accounts get observed rather than starved.
41
41
  */
42
42
  declare function orderCodexAccounts(accounts: CodexRuntimeAccount[], now: number): CodexRuntimeAccount[];
43
43
  /** Build the upstream request headers, replacing client auth with the account's. */
44
44
  declare function buildCodexUpstreamHeaders(clientHeaders: Record<string, string>, account: CodexRuntimeAccount): Record<string, string>;
45
- /**
46
- * Classify a 429 into a cooldown plan. Codex reports primary (session) and
47
- * secondary (weekly) windows; a rejected window cools until its reset, a plain
48
- * burst limit cools briefly and clamps to 15 min.
49
- */
50
- declare function planCodexCooldown(quota: AccountQuota | null, retryAfterMs: number, now: number): {
45
+ /** Honor known quota resets while bounding unscoped or transient account cooldowns. */
46
+ declare function planCodexCooldown(quota: AccountQuota | null, retryAfterMs: number, now: number, errorText?: string): {
51
47
  coolingUntil: number;
52
48
  reason: RateLimitCoolingReason;
53
49
  };
@@ -188,9 +188,9 @@ async function loadCodexProxyAccounts() {
188
188
  return accounts;
189
189
  }
190
190
  /**
191
- * Order accounts fill-first by quota: eligible (not cooling) first, then least
192
- * session utilization, treating unknown quota as "probe first" so a fresh
193
- * account gets observed rather than starved.
191
+ * Order non-cooling accounts ahead of cooling ones, then defer rejected quota.
192
+ * Within each group, probe unknown quota first and otherwise fill the least-used
193
+ * session so fresh accounts get observed rather than starved.
194
194
  */
195
195
  function orderCodexAccounts(accounts, now) {
196
196
  return [...accounts].sort((a, b) => {
@@ -199,8 +199,13 @@ function orderCodexAccounts(accounts, now) {
199
199
  if (aCooling !== bCooling) {
200
200
  return aCooling ? 1 : -1;
201
201
  }
202
- const aUsed = a.quota ? a.quota.sessionUsed : -1;
203
- const bUsed = b.quota ? b.quota.sessionUsed : -1;
202
+ const aRejected = a.quota?.unifiedStatus === "rejected";
203
+ const bRejected = b.quota?.unifiedStatus === "rejected";
204
+ if (aRejected !== bRejected) {
205
+ return aRejected ? 1 : -1;
206
+ }
207
+ const aUsed = a.quota && a.quota.sessionStatus !== "unknown" ? a.quota.sessionUsed : -1;
208
+ const bUsed = b.quota && b.quota.sessionStatus !== "unknown" ? b.quota.sessionUsed : -1;
204
209
  return aUsed - bUsed;
205
210
  });
206
211
  }
@@ -231,12 +236,51 @@ function buildCodexUpstreamHeaders(clientHeaders, account) {
231
236
  }
232
237
  return headers;
233
238
  }
234
- /**
235
- * Classify a 429 into a cooldown plan. Codex reports primary (session) and
236
- * secondary (weekly) windows; a rejected window cools until its reset, a plain
237
- * burst limit cools briefly and clamps to 15 min.
238
- */
239
- function planCodexCooldown(quota, retryAfterMs, now) {
239
+ /** Extract explicit plan-exhaustion evidence without guessing account-wide scope. */
240
+ function parseCodexQuotaError(errorText, now) {
241
+ try {
242
+ const payload = JSON.parse(errorText);
243
+ const error = payload?.error;
244
+ if (!error ||
245
+ typeof error !== "object" ||
246
+ (error.type !== "usage_limit_reached" &&
247
+ error.code !== "usage_limit_reached")) {
248
+ return null;
249
+ }
250
+ const absolute = error.resets_at ?? error.reset_at;
251
+ const relative = error.resets_in_seconds ?? error.reset_after_seconds;
252
+ const resetAt = typeof absolute === "number" && Number.isFinite(absolute) && absolute > 0
253
+ ? absolute > 4_102_444_800
254
+ ? absolute
255
+ : absolute * 1000
256
+ : typeof relative === "number" &&
257
+ Number.isFinite(relative) &&
258
+ relative > 0
259
+ ? now + relative * 1000
260
+ : 0;
261
+ // Only explicit account-wide window evidence permits a long cooldown.
262
+ // A model/surface-scoped or unscoped rejection must not park the entire pool.
263
+ const window = error.limit_type;
264
+ const scoped = error.model || (error.scope !== undefined && error.scope !== "account");
265
+ const scope = scoped
266
+ ? "unknown"
267
+ : window === "primary" || window === "session"
268
+ ? "session"
269
+ : window === "secondary" || window === "weekly"
270
+ ? "weekly"
271
+ : "unknown";
272
+ return {
273
+ errorCode: "usage_limit_reached",
274
+ resetAt: Number.isFinite(resetAt) ? resetAt : 0,
275
+ scope,
276
+ };
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ /** Honor known quota resets while bounding unscoped or transient account cooldowns. */
283
+ function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
240
284
  // A reported reset can already be in the past — a stale header, or a clock
241
285
  // skew. Taken literally the account is eligible again immediately and the
242
286
  // pool re-sends to something the provider just rejected.
@@ -255,8 +299,25 @@ function planCodexCooldown(quota, retryAfterMs, now) {
255
299
  };
256
300
  }
257
301
  }
302
+ const bodyQuota = parseCodexQuotaError(errorText, now);
303
+ if (bodyQuota) {
304
+ if (bodyQuota.scope !== "unknown" && bodyQuota.resetAt > 0) {
305
+ return {
306
+ coolingUntil: Math.max(bodyQuota.resetAt, floor),
307
+ reason: bodyQuota.scope,
308
+ };
309
+ }
310
+ return {
311
+ coolingUntil: now +
312
+ Math.min(MAX_TRANSIENT_COOLDOWN_MS, Math.max(bodyQuota.resetAt - now, retryAfterMs, DEFAULT_TRANSIENT_COOLDOWN_MS)),
313
+ reason: "unified",
314
+ };
315
+ }
258
316
  const delay = Math.min(MAX_TRANSIENT_COOLDOWN_MS, Math.max(retryAfterMs, DEFAULT_TRANSIENT_COOLDOWN_MS));
259
- return { coolingUntil: now + delay, reason: "transient" };
317
+ return {
318
+ coolingUntil: now + delay,
319
+ reason: quota?.unifiedStatus === "rejected" ? "unified" : "transient",
320
+ };
260
321
  }
261
322
  /** Set the x-neurolink-* attribution headers on the context. */
262
323
  function publishCodexHeaders(ctx, account, attempt, quota) {
@@ -268,9 +329,14 @@ function publishCodexHeaders(ctx, account, attempt, quota) {
268
329
  ctx.responseHeaders["x-neurolink-served-by"] = "codex";
269
330
  ctx.responseHeaders["x-neurolink-attempt"] = String(attempt);
270
331
  ctx.responseHeaders["x-neurolink-quota-source"] = quota ? "live" : "none";
271
- if (quota) {
332
+ if (quota && quota.sessionStatus !== "unknown") {
272
333
  ctx.responseHeaders["x-neurolink-quota-session-left-pct"] = String(Math.round((1 - quota.sessionUsed) * 100));
273
- ctx.responseHeaders["x-neurolink-weekly-left-pct"] = String(Math.round((1 - quota.weeklyUsed) * 100));
334
+ }
335
+ if (quota && quota.weeklyStatus !== "unknown") {
336
+ const remaining = String(Math.round((1 - quota.weeklyUsed) * 100));
337
+ ctx.responseHeaders["x-neurolink-quota-weekly-left-pct"] = remaining;
338
+ // Preserve the original Codex header for existing clients.
339
+ ctx.responseHeaders["x-neurolink-weekly-left-pct"] = remaining;
274
340
  }
275
341
  }
276
342
  /** Core pooled handler for POST /backend-api/codex/responses. */
@@ -389,6 +455,7 @@ async function executeCodexResponsesRequest(ctx) {
389
455
  // This is the cost provider. accountKey and the response header identify
390
456
  // the actual Codex pool that supplied the credential.
391
457
  provider: "openai",
458
+ inputIncludesCachedTokens: true,
392
459
  ...tracer?.getTraceContext(),
393
460
  ...(reasoningEffort ? { reasoningEffort } : {}),
394
461
  firstUsefulOutputStatus: extra.firstUsefulOutputMs !== undefined ? "observed" : "not_observed",
@@ -679,11 +746,20 @@ async function executeCodexResponsesRequest(ctx) {
679
746
  outputTokens: usage.outputTokens,
680
747
  cacheReadTokens: usage.cacheReadTokens,
681
748
  cacheCreationTokens: usage.cacheCreationTokens,
749
+ reasoningTokens: usage.reasoningTokensObserved
750
+ ? usage.reasoningTokens
751
+ : undefined,
682
752
  }
683
753
  : {};
684
754
  if (usage) {
685
755
  try {
686
- tracer?.setUsage(usage);
756
+ tracer?.setUsage({
757
+ ...usage,
758
+ reasoningTokens: usage.reasoningTokensObserved
759
+ ? usage.reasoningTokens
760
+ : undefined,
761
+ inputIncludesCachedTokens: true,
762
+ });
687
763
  }
688
764
  catch {
689
765
  // Pricing/metrics must never change the stream outcome.
@@ -825,7 +901,9 @@ async function executeCodexResponsesRequest(ctx) {
825
901
  saveAccountQuota(account.key, quota).catch(() => undefined);
826
902
  }
827
903
  const retryAfterMs = parseRetryAfterMs(upstream.headers.get("retry-after"));
828
- const plan = planCodexCooldown(quota, retryAfterMs, Date.now());
904
+ const now = Date.now();
905
+ const bodyQuota = parseCodexQuotaError(errText, now);
906
+ const plan = planCodexCooldown(quota, retryAfterMs, now, errText);
829
907
  await saveAccountCooldown(account.key, plan.coolingUntil, plan.reason).catch(() => undefined);
830
908
  const rateLimitKind = plan.reason === "transient" ? "transient" : "quota";
831
909
  const errorMessage = summarizeCodexUpstreamError(errText, "Codex account rate-limited");
@@ -833,11 +911,21 @@ async function executeCodexResponsesRequest(ctx) {
833
911
  writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
834
912
  errorType: "rate_limit_error",
835
913
  errorMessage,
836
- retryable: true,
914
+ retryable: rateLimitKind === "transient",
837
915
  rateLimitKind,
838
916
  cooldownReason: plan.reason,
917
+ ...(bodyQuota
918
+ ? {
919
+ errorCode: bodyQuota.errorCode,
920
+ quotaResetAt: bodyQuota.resetAt || undefined,
921
+ quotaScope: bodyQuota.scope,
922
+ }
923
+ : {}),
839
924
  });
840
- lastFailure = { errorType: "rate_limit_error" };
925
+ lastFailure = {
926
+ errorType: "rate_limit_error",
927
+ errorCode: bodyQuota?.errorCode,
928
+ };
841
929
  lastErrorStatus = 429;
842
930
  lastErrorMessage = "Codex account rate-limited";
843
931
  break; // rotate
@@ -64,6 +64,8 @@ export type CodexRateLimitWindow = {
64
64
  };
65
65
  /** Codex rate-limit block: a primary (short) and secondary (long) window. */
66
66
  export type CodexRateLimits = {
67
+ allowed?: boolean;
68
+ limit_reached?: boolean;
67
69
  primary?: CodexRateLimitWindow | null;
68
70
  secondary?: CodexRateLimitWindow | null;
69
71
  };
@@ -73,6 +75,8 @@ export type CodexUsageResponse = {
73
75
  rate_limits?: CodexRateLimits | null;
74
76
  /** Current ChatGPT WHAM account-usage payload. */
75
77
  rate_limit?: {
78
+ allowed?: boolean;
79
+ limit_reached?: boolean;
76
80
  primary_window?: CodexRateLimitWindow | null;
77
81
  secondary_window?: CodexRateLimitWindow | null;
78
82
  } | null;
@@ -86,6 +90,12 @@ export type CodexUsageFetchResult = {
86
90
  ok: false;
87
91
  reason: "not_oauth" | "auth" | "rate_limited" | "http" | "network" | "parse";
88
92
  };
93
+ /** Explicit plan-exhaustion evidence from a Codex error response. */
94
+ export type CodexQuotaError = {
95
+ errorCode: string;
96
+ resetAt: number;
97
+ scope: "session" | "weekly" | "unknown";
98
+ };
89
99
  /** A Codex account with its runtime cooldown/quota state hydrated from disk. */
90
100
  export type CodexRuntimeAccount = {
91
101
  key: string;
@@ -221,6 +221,7 @@ export type InternalResult = {
221
221
  total: number;
222
222
  cacheCreationTokens?: number;
223
223
  cacheReadTokens?: number;
224
+ reasoning?: number;
224
225
  };
225
226
  toolCalls?: Array<{
226
227
  toolCallId: string;
@@ -576,6 +577,8 @@ export type RequestLogEntry = {
576
577
  method: string;
577
578
  path: string;
578
579
  model: string;
580
+ /** Client model when a different upstream model served the request. */
581
+ requestedModel?: string;
579
582
  stream: boolean;
580
583
  toolCount: number;
581
584
  account: string;
@@ -590,10 +593,14 @@ export type RequestLogEntry = {
590
593
  errorCode?: string;
591
594
  /** Whether changing credentials can affect this transport failure. */
592
595
  transportScope?: ProxyTransportScope;
596
+ /** True when input includes the cache breakdown (native Codex wire usage). */
597
+ inputIncludesCachedTokens?: boolean;
593
598
  inputTokens?: number;
594
599
  outputTokens?: number;
595
600
  cacheCreationTokens?: number;
596
601
  cacheReadTokens?: number;
602
+ /** Reasoning tokens are a subset of output, never additional usage. */
603
+ reasoningTokens?: number;
597
604
  /**
598
605
  * Provider that actually served the request, for costing. Absent on records
599
606
  * written before this field existed; `proxyAnalysis` then falls back to a
@@ -814,6 +821,10 @@ export type RequestAttemptLogEntry = {
814
821
  rateLimitKind?: "transient" | "quota";
815
822
  /** Reset-aware cooldown reason selected for a rate-limited attempt. */
816
823
  cooldownReason?: "transient" | "session" | "weekly" | "unified";
824
+ /** Reset reported by a structured quota rejection, epoch milliseconds. */
825
+ quotaResetAt?: number;
826
+ /** Unknown scope never implies an account-wide long-window rejection. */
827
+ quotaScope?: "session" | "weekly" | "unknown";
817
828
  inputTokens?: number;
818
829
  outputTokens?: number;
819
830
  cacheCreationTokens?: number;
@@ -828,9 +839,9 @@ export type RequestAttemptLogEntry = {
828
839
  traceFlags?: number;
829
840
  };
830
841
  /** Additional fields recorded when a Codex response becomes client-final. */
831
- export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent">>;
842
+ export type CodexFinalLogExtra = Partial<Pick<RequestLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "inputTokens" | "outputTokens" | "cacheReadTokens" | "cacheCreationTokens" | "reasoningTokens" | "terminalOutcome" | "firstUsefulOutputMs" | "firstUsefulOutputStatus" | "firstUsefulOutputEvent">>;
832
843
  /** Additional fields recorded for each upstream Codex account attempt. */
833
- export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason">>;
844
+ export type CodexAttemptLogExtra = Partial<Pick<RequestAttemptLogEntry, "errorType" | "errorMessage" | "errorCode" | "transportScope" | "retryable" | "rateLimitKind" | "cooldownReason" | "quotaResetAt" | "quotaScope">>;
834
845
  /** Minimal persistence contract needed by Codex rotating-token refreshes. */
835
846
  export type CodexRefreshTokenStore = {
836
847
  peekTokens(provider: string): Promise<StoredOAuthTokens | null>;
@@ -863,6 +874,7 @@ export type ClaudeFinalRequestLogger = (status: number, accountLabel: string, ac
863
874
  outputTokens?: number;
864
875
  cacheCreationTokens?: number;
865
876
  cacheReadTokens?: number;
877
+ reasoningTokens?: number;
866
878
  errorCode?: string;
867
879
  transportScope?: ProxyTransportScope;
868
880
  }) => void;
@@ -1246,15 +1258,15 @@ export type AccountQuota = {
1246
1258
  /** Top-level unified status. A rejected value can be authoritative even
1247
1259
  * while both 5h and 7d sub-window statuses still report allowed. */
1248
1260
  unifiedStatus?: string;
1249
- /** 0.0-1.0 (from unified-5h-utilization) */
1261
+ /** 0.0-1.0 utilization; ignore the numeric placeholder when status is unknown. */
1250
1262
  sessionUsed: number;
1251
- /** "allowed" | "throttled" | "rejected" */
1263
+ /** "allowed" | "throttled" | "rejected" | "unknown" */
1252
1264
  sessionStatus: string;
1253
1265
  /** Unix timestamp (seconds) when the 5h window resets */
1254
1266
  sessionResetAt: number;
1255
- /** 0.0-1.0 (from unified-7d-utilization) */
1267
+ /** 0.0-1.0 utilization; ignore the numeric placeholder when status is unknown. */
1256
1268
  weeklyUsed: number;
1257
- /** "allowed" | "throttled" | "rejected" */
1269
+ /** "allowed" | "throttled" | "rejected" | "unknown" */
1258
1270
  weeklyStatus: string;
1259
1271
  /** Unix timestamp (seconds) when the 7d window resets */
1260
1272
  weeklyResetAt: number;
@@ -1700,6 +1712,8 @@ export type UpstreamAttemptContext = {
1700
1712
  };
1701
1713
  /** Token usage and rate-limit utilisation recorded at end of request. */
1702
1714
  export type UsageContext = {
1715
+ /** True when input includes the cache breakdown (native Codex wire usage). */
1716
+ inputIncludesCachedTokens?: boolean;
1703
1717
  inputTokens: number;
1704
1718
  outputTokens: number;
1705
1719
  cacheCreationTokens: number;
@@ -2008,6 +2022,7 @@ export type ProxyAnalysisReport = {
2008
2022
  requestsWithCacheRead: number;
2009
2023
  cacheReadTokens: number;
2010
2024
  cacheCreationTokens: number;
2025
+ /** Ordinary input, excluding the separately reported cache read/write buckets. */
2011
2026
  inputTokens: number;
2012
2027
  outputTokens: number;
2013
2028
  requestHitRate: number | null;
@@ -2068,6 +2083,7 @@ export type ProxyAnalysisFinalRequestRecord = {
2068
2083
  accountType: string;
2069
2084
  model: string | null;
2070
2085
  provider: string | null;
2086
+ inputIncludesCachedTokens?: boolean;
2071
2087
  inputTokens: number | null;
2072
2088
  outputTokens: number | null;
2073
2089
  cacheReadTokens: number | null;
@@ -2104,6 +2120,8 @@ export type CodexStreamUsage = {
2104
2120
  /** Cache writes, which bill at a premium over both reads and plain input. */
2105
2121
  cacheCreationTokens: number;
2106
2122
  reasoningTokens: number;
2123
+ /** Whether the provider supplied a valid reasoning breakdown. */
2124
+ reasoningTokensObserved?: boolean;
2107
2125
  };
2108
2126
  /** Semantic completion evidence observed in native Codex SSE bytes. */
2109
2127
  export type CodexStreamEvidence = {
@@ -899,8 +899,10 @@ export function calculateCost(provider, model, usage) {
899
899
  // priced providers still get the cheaper cacheRead rate.
900
900
  cost += usage.cacheReadTokens * (rates.cacheRead ?? rates.input);
901
901
  }
902
- if (usage.cacheCreationTokens && rates.cacheCreation) {
903
- cost += usage.cacheCreationTokens * rates.cacheCreation;
902
+ if (usage.cacheCreationTokens) {
903
+ // Like cache reads, disjoint cache writes must not silently become free
904
+ // when a model has no separate creation rate. Preserve explicit zero rates.
905
+ cost += usage.cacheCreationTokens * (rates.cacheCreation ?? rates.input);
904
906
  }
905
907
  return Math.round(cost * 1_000_000) / 1_000_000; // Round to 6 decimal places
906
908
  }