@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.
@@ -248,8 +248,12 @@ function formatQuotaColumns(quota) {
248
248
  return chalk.green(text);
249
249
  };
250
250
  return {
251
- sessionText: colorize(sessionRemaining, `${sessionRemaining}% left`),
252
- weeklyText: colorize(weeklyRemaining, `${weeklyRemaining}% left`),
251
+ sessionText: quota.sessionStatus === "unknown"
252
+ ? chalk.gray("unknown")
253
+ : colorize(sessionRemaining, `${sessionRemaining}% left`),
254
+ weeklyText: quota.weeklyStatus === "unknown"
255
+ ? chalk.gray("unknown")
256
+ : colorize(weeklyRemaining, `${weeklyRemaining}% left`),
253
257
  sessionReset: quota.sessionResetAt > 0
254
258
  ? chalk.gray(`resets ${formatTimeUntil(quota.sessionResetAt)}`)
255
259
  : "",
@@ -82,6 +82,10 @@ export declare function createProxyStartApp(params: {
82
82
  app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
83
83
  readiness: ProxyReadinessState;
84
84
  }>;
85
+ /** Reload only the ready serving worker. A candidate still starting must not
86
+ * receive SIGHUP before it has installed its handlers. It loads fresh config
87
+ * during startup; draining workers already hold their request snapshots. */
88
+ export declare function registerProxySupervisorReload(activeWorkerPid: () => number | undefined): () => void;
85
89
  export declare const proxyStartCommand: CommandModule<object, ProxyStartArgs>;
86
90
  export declare function sanitizeProxyStatusTerminalErrorMessage(message: string): string;
87
91
  export declare const proxyStatusCommand: CommandModule<object, ProxyStatusArgs>;
@@ -1,4 +1,5 @@
1
1
  import { initializeProxyOtelLogs, routeProxyConsoleToOtel, flushProxyOtelLogs, shutdownProxyOtelLogs, isProxyOtelOnly, withProxyOtelLogShutdown, } from "../../proxy/otelLogSink.js";
2
+ import { registerCliShutdownOwner } from "../lifecycle.js";
2
3
  import { writeFileAtomic } from "../proxy-clients/snapshot.js";
3
4
  import { spawn } from "node:child_process";
4
5
  import { homedir } from "node:os";
@@ -60,6 +61,9 @@ const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/obse
60
61
  */
61
62
  const gatedShareRequests = new WeakSet();
62
63
  const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
64
+ // Allow the normal 30-second worker drain plus startup settlement, leaving
65
+ // telemetry and scheduling headroom inside launchd's 45-second stop budget.
66
+ const PROXY_SUPERVISOR_DRAIN_TIMEOUT_MS = 35_000;
63
67
  /** How long shutdown waits on the share listener before moving on. */
64
68
  const SHARE_LISTENER_CLOSE_TIMEOUT_MS = 10_000;
65
69
  const PROXY_STATUS_RECONCILE_TIMEOUT_MS = 750;
@@ -2504,10 +2508,12 @@ function registerProxyShutdownHandlers(params) {
2504
2508
  }
2505
2509
  try {
2506
2510
  const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../services/server/ai/observability/instrumentation.js");
2507
- await flushProxyOtelLogs();
2508
- await shutdownProxyOtelLogs();
2509
- await flushOpenTelemetry();
2510
- await shutdownOpenTelemetry();
2511
+ await withShutdownDeadline((async () => {
2512
+ await flushProxyOtelLogs();
2513
+ await shutdownProxyOtelLogs();
2514
+ await flushOpenTelemetry();
2515
+ await shutdownOpenTelemetry();
2516
+ })(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "telemetry shutdown");
2511
2517
  }
2512
2518
  catch {
2513
2519
  // non-fatal — proxy shutdown must not block on OTel
@@ -2535,12 +2541,7 @@ function registerProxyShutdownHandlers(params) {
2535
2541
  process.exit(1);
2536
2542
  };
2537
2543
  if (params.registerSignals ?? true) {
2538
- process.on("SIGTERM", () => {
2539
- void shutdown("SIGTERM").catch(forceExitAfterShutdownFailure);
2540
- });
2541
- process.on("SIGINT", () => {
2542
- void shutdown("SIGINT").catch(forceExitAfterShutdownFailure);
2543
- });
2544
+ registerCliShutdownOwner((signal) => shutdown(signal).catch(forceExitAfterShutdownFailure));
2544
2545
  }
2545
2546
  return shutdown;
2546
2547
  }
@@ -2866,10 +2867,81 @@ async function startProxyRuntime(params) {
2866
2867
  * serving-process exits.
2867
2868
  */
2868
2869
  async function runLaunchdProxySupervisor(argv, spinner) {
2870
+ let stopping = false;
2871
+ let activeWorkerPid;
2872
+ let rollingServerTask;
2873
+ let restartControlTask;
2874
+ let updaterSupervisor;
2875
+ let activatePendingUpdate;
2876
+ let otelInitialized = false;
2877
+ /** Own startup as well as serving: await resources already being created,
2878
+ * then drain each independently so one cleanup failure cannot skip the rest. */
2879
+ const shutdown = async (signal) => {
2880
+ if (stopping) {
2881
+ return;
2882
+ }
2883
+ stopping = true;
2884
+ logger.always(`[proxy-supervisor] shutting down (${signal})`);
2885
+ if (activatePendingUpdate) {
2886
+ process.off("SIGUSR2", activatePendingUpdate);
2887
+ }
2888
+ let exitCode = process.exitCode ?? 0;
2889
+ const reportCleanupFailure = (error) => {
2890
+ exitCode = 1;
2891
+ logger.warn(`[proxy-supervisor] cleanup failed: ${String(error)}`);
2892
+ };
2893
+ const cleanup = async (operation, timeoutMs = PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, timeoutMessage = "supervisor resource shutdown deadline exceeded") => {
2894
+ try {
2895
+ await withTimeout(Promise.resolve().then(operation), timeoutMs, timeoutMessage);
2896
+ }
2897
+ catch (error) {
2898
+ reportCleanupFailure(error);
2899
+ }
2900
+ };
2901
+ // Close independently: a stalled control must not delay the worker drain.
2902
+ // The operations retain their continuations after a deadline, so a resource
2903
+ // returned during telemetry cleanup is still closed without extending exit.
2904
+ await Promise.all([
2905
+ cleanup(async () => {
2906
+ // Setup errors are already reported by the control-plane startup path.
2907
+ const control = await restartControlTask?.catch(() => undefined);
2908
+ await control?.close();
2909
+ }),
2910
+ cleanup(() => updaterSupervisor?.stop()),
2911
+ cleanup(async () => {
2912
+ const server = await rollingServerTask;
2913
+ await server?.close();
2914
+ }, PROXY_SUPERVISOR_DRAIN_TIMEOUT_MS),
2915
+ ]);
2916
+ if (otelInitialized) {
2917
+ await cleanup(async () => {
2918
+ await flushProxyLifecycleEvents().catch(reportCleanupFailure);
2919
+ await flushProxyOtelLogs().catch(reportCleanupFailure);
2920
+ await shutdownProxyOtelLogs();
2921
+ }, PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "supervisor telemetry shutdown deadline exceeded");
2922
+ }
2923
+ await cleanup(() => {
2924
+ const supervisorState = loadProxySupervisorState();
2925
+ if (supervisorState?.pid === process.pid) {
2926
+ clearProxySupervisorState();
2927
+ }
2928
+ });
2929
+ process.exit(exitCode);
2930
+ };
2931
+ registerCliShutdownOwner(shutdown);
2932
+ // Protect every startup await; a worker loads fresh config before readiness.
2933
+ registerProxySupervisorReload(() => stopping ? undefined : activeWorkerPid?.());
2869
2934
  await loadProxyStartEnv(argv, spinner);
2935
+ if (stopping) {
2936
+ return;
2937
+ }
2870
2938
  initializeProxyOtelLogs("supervisor");
2939
+ otelInitialized = true;
2871
2940
  routeProxyConsoleToOtel();
2872
2941
  await ensureProxyStartAllowed(spinner);
2942
+ if (stopping) {
2943
+ return;
2944
+ }
2873
2945
  const entryScript = process.argv[1];
2874
2946
  if (!entryScript) {
2875
2947
  throw new Error("proxy supervisor cannot resolve the CLI entry script");
@@ -2885,7 +2957,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2885
2957
  });
2886
2958
  let currentUpdaterPid;
2887
2959
  let restartControl;
2888
- const rollingServer = await startRollingProxyServer({
2960
+ rollingServerTask = startRollingProxyServer({
2889
2961
  onEvent: (event) => logProxyLifecycleEvent({
2890
2962
  event: "supervisor_event",
2891
2963
  requestId: "-",
@@ -2920,9 +2992,20 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2920
2992
  },
2921
2993
  log: (message) => logger.always(message),
2922
2994
  });
2995
+ // Retain the creation promise before awaiting it so shutdown owns late results.
2996
+ const rollingServer = await rollingServerTask.catch((error) => {
2997
+ if (!stopping) {
2998
+ throw error;
2999
+ }
3000
+ return undefined;
3001
+ });
3002
+ if (stopping || !rollingServer) {
3003
+ return;
3004
+ }
3005
+ activeWorkerPid = () => rollingServer.snapshot().active?.pid;
2923
3006
  let rollingReplacement = null;
2924
- const activatePendingUpdate = () => {
2925
- if (rollingReplacement) {
3007
+ activatePendingUpdate = () => {
3008
+ if (stopping || rollingReplacement) {
2926
3009
  return;
2927
3010
  }
2928
3011
  let pendingVersion;
@@ -2953,7 +3036,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2953
3036
  process.on("SIGUSR2", activatePendingUpdate);
2954
3037
  const readinessHost = host === "0.0.0.0" ? "127.0.0.1" : host;
2955
3038
  try {
2956
- restartControl = await startProxyRestartControl({
3039
+ restartControlTask = startProxyRestartControl({
2957
3040
  stateDir: join(homedir(), ".neurolink"),
2958
3041
  server: rollingServer,
2959
3042
  getTelemetry: getProxyProcessTelemetry,
@@ -2977,11 +3060,15 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2977
3060
  return response.json();
2978
3061
  },
2979
3062
  });
3063
+ restartControl = await restartControlTask;
2980
3064
  }
2981
3065
  catch (error) {
2982
3066
  // A control-plane setup failure must not stop an otherwise serving proxy.
2983
3067
  logger.warn(`[proxy-supervisor] safe restart control unavailable: ${error instanceof Error ? error.message : String(error)}`);
2984
3068
  }
3069
+ if (stopping) {
3070
+ return;
3071
+ }
2985
3072
  const updatePersistedUpdaterPid = (updaterPid) => {
2986
3073
  currentUpdaterPid = updaterPid;
2987
3074
  const state = loadProxySupervisorState();
@@ -2994,7 +3081,7 @@ async function runLaunchdProxySupervisor(argv, spinner) {
2994
3081
  restartControl: restartControl?.identity,
2995
3082
  });
2996
3083
  };
2997
- const updaterSupervisor = startUpdaterWorkerSupervisor({
3084
+ updaterSupervisor = startUpdaterWorkerSupervisor({
2998
3085
  spawnWorker: () => spawnProxyUpdater(readinessHost, rollingServer.address.port, process.pid, true),
2999
3086
  isProcessRunning,
3000
3087
  stopWorker: (pid) => process.kill(pid, "SIGTERM"),
@@ -3006,34 +3093,27 @@ async function runLaunchdProxySupervisor(argv, spinner) {
3006
3093
  spinner.succeed(chalk.green("Claude proxy supervisor started successfully"));
3007
3094
  }
3008
3095
  logger.always(`[proxy-supervisor] listening on ${host}:${rollingServer.address.port} workerPid=${rollingServer.snapshot().active?.pid ?? "unknown"} version=${PROXY_VERSION}`);
3009
- let stopping = false;
3010
- /** Drain the rolling workers and flush the supervisor journal before clearing ownership. */
3011
- const shutdown = async (signal) => {
3012
- if (stopping) {
3096
+ await new Promise(() => undefined);
3097
+ }
3098
+ /** Reload only the ready serving worker. A candidate still starting must not
3099
+ * receive SIGHUP before it has installed its handlers. It loads fresh config
3100
+ * during startup; draining workers already hold their request snapshots. */
3101
+ export function registerProxySupervisorReload(activeWorkerPid) {
3102
+ const reload = () => {
3103
+ const pid = activeWorkerPid();
3104
+ if (!pid) {
3013
3105
  return;
3014
3106
  }
3015
- stopping = true;
3016
- logger.always(`[proxy-supervisor] shutting down (${signal})`);
3017
- process.off("SIGUSR2", activatePendingUpdate);
3018
- await restartControl?.close();
3019
- updaterSupervisor.stop();
3020
- await rollingServer.close();
3021
- await flushProxyLifecycleEvents().catch((error) => logger.warn(String(error)));
3022
- await flushProxyOtelLogs().catch(() => undefined);
3023
- await shutdownProxyOtelLogs().catch(() => undefined);
3024
- const supervisorState = loadProxySupervisorState();
3025
- if (supervisorState?.pid === process.pid) {
3026
- clearProxySupervisorState();
3107
+ try {
3108
+ process.kill(pid, "SIGHUP");
3109
+ logger.always(`[proxy-supervisor] forwarded configuration reload to worker pid=${pid}`);
3110
+ }
3111
+ catch (error) {
3112
+ logger.warn(`[proxy-supervisor] configuration reload failed: ${error instanceof Error ? error.message : String(error)}`);
3027
3113
  }
3028
- process.exit(0);
3029
3114
  };
3030
- process.once("SIGTERM", () => {
3031
- void shutdown("SIGTERM");
3032
- });
3033
- process.once("SIGINT", () => {
3034
- void shutdown("SIGINT");
3035
- });
3036
- await new Promise(() => undefined);
3115
+ process.on("SIGHUP", reload);
3116
+ return () => process.off("SIGHUP", reload);
3037
3117
  }
3038
3118
  async function startProxyCommandHandler(argv) {
3039
3119
  const spinner = argv.quiet ? null : ora("Starting Claude proxy...").start();
package/dist/cli/index.js CHANGED
@@ -9,6 +9,7 @@ process.env.DOTENV_CONFIG_QUIET = "true";
9
9
  */
10
10
  import { initializeCliParser } from "./parser.js";
11
11
  import chalk from "chalk";
12
+ import { initializeCliLifecycle, cleanupCliLifecycle } from "./lifecycle.js";
12
13
  // Clean up pnpm-specific environment variables that cause npm warnings
13
14
  // These variables are set by pnpm but cause "Unknown env config" warnings in npm
14
15
  if (process.env.npm_config_verify_deps_before_run) {
@@ -30,34 +31,23 @@ catch {
30
31
  }
31
32
  // Enhanced CLI with Professional UX
32
33
  const cli = initializeCliParser();
34
+ initializeCliLifecycle(async () => {
35
+ const { flushOpenTelemetry } = await import("../services/server/ai/observability/instrumentation.js");
36
+ await flushOpenTelemetry();
37
+ });
33
38
  // Execute CLI
34
39
  (async () => {
35
40
  try {
36
41
  // Parse and execute commands
37
42
  await cli.parse();
38
- await cleanup();
43
+ await cleanupCliLifecycle();
39
44
  }
40
45
  catch (error) {
41
46
  // Global error handler - should not reach here due to fail() handler
42
47
  process.stderr.write(chalk.red(`Unexpected CLI _error: ${error.message}\n`));
43
- await cleanup();
48
+ process.exitCode = 1;
49
+ await cleanupCliLifecycle();
44
50
  process.exit(1);
45
51
  }
46
52
  })();
47
- // Cleanup on exit
48
- process.on("SIGINT", async () => {
49
- await cleanup();
50
- process.exit(0);
51
- });
52
- process.on("SIGTERM", async () => {
53
- await cleanup();
54
- process.exit(0);
55
- });
56
- process.on("beforeExit", async () => {
57
- await cleanup();
58
- });
59
- async function cleanup() {
60
- const { flushOpenTelemetry } = await import("../services/server/ai/observability/instrumentation.js");
61
- await flushOpenTelemetry();
62
- }
63
53
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,8 @@
1
+ /** Install ordinary-command cleanup and its maximum flush duration. */
2
+ export declare function initializeCliLifecycle(cleanup: () => Promise<void>, timeoutMs?: number): void;
3
+ /** Delegate signals to a long-lived command that owns draining and exit. */
4
+ export declare function registerCliShutdownOwner(handler: (signal: NodeJS.Signals) => Promise<void>): void;
5
+ /** Flush ordinary commands once; an active shutdown owner manages its own cleanup. */
6
+ export declare function cleanupCliLifecycle(): Promise<void>;
7
+ /** Bound best-effort cleanup without allowing rejection to escape a signal handler. */
8
+ export declare function runBoundedCliCleanup(cleanup: () => Promise<unknown>, timeoutMs?: number, onTimeout?: () => void): Promise<void>;
@@ -0,0 +1,90 @@
1
+ /** One signal owner per CLI process. Proxy lifecycles own their drain and exit;
2
+ * ordinary commands only need a bounded, best-effort telemetry flush. */
3
+ let owner;
4
+ let cleanupTask;
5
+ let cleanupPromise;
6
+ let shutdownPromise;
7
+ let installed = false;
8
+ let cleanupTimeoutMs = 5_000;
9
+ /** Install ordinary-command cleanup and its maximum flush duration. */
10
+ export function initializeCliLifecycle(cleanup, timeoutMs = 5_000) {
11
+ cleanupTask = cleanup;
12
+ cleanupTimeoutMs = timeoutMs;
13
+ installSignals();
14
+ }
15
+ /** Delegate signals to a long-lived command that owns draining and exit. */
16
+ export function registerCliShutdownOwner(handler) {
17
+ owner = handler;
18
+ installSignals();
19
+ }
20
+ /** Flush ordinary commands once; an active shutdown owner manages its own cleanup. */
21
+ export function cleanupCliLifecycle() {
22
+ if (owner) {
23
+ return Promise.resolve();
24
+ }
25
+ cleanupPromise ??= runBoundedCliCleanup(() => cleanupTask?.() ?? Promise.resolve(), cleanupTimeoutMs, () => {
26
+ // A stalled exporter may retain handles after Promise.race settles.
27
+ // A newly registered owner still retains control of draining and exit.
28
+ if (!owner) {
29
+ process.exit(process.exitCode ?? 0);
30
+ }
31
+ });
32
+ return cleanupPromise;
33
+ }
34
+ /** Bound best-effort cleanup without allowing rejection to escape a signal handler. */
35
+ export async function runBoundedCliCleanup(cleanup, timeoutMs = 5_000, onTimeout) {
36
+ let timer;
37
+ let timedOut = false;
38
+ try {
39
+ await Promise.race([
40
+ Promise.resolve().then(cleanup),
41
+ new Promise((_, reject) => {
42
+ timer = setTimeout(() => {
43
+ timedOut = true;
44
+ reject(new Error("telemetry flush deadline exceeded"));
45
+ }, timeoutMs);
46
+ }),
47
+ ]);
48
+ }
49
+ catch (error) {
50
+ process.stderr.write(`[cli] cleanup failed: ${error instanceof Error ? error.message : String(error)}\n`);
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ if (timedOut) {
56
+ onTimeout?.();
57
+ }
58
+ }
59
+ /** Install one process-wide signal dispatcher and reuse the first shutdown promise. */
60
+ function installSignals() {
61
+ if (installed) {
62
+ return;
63
+ }
64
+ installed = true;
65
+ for (const signal of ["SIGINT", "SIGTERM"]) {
66
+ process.on(signal, () => {
67
+ shutdownPromise ??= Promise.resolve()
68
+ .then(async () => {
69
+ if (!owner) {
70
+ await cleanupCliLifecycle();
71
+ }
72
+ // Startup may register the proxy owner while the flush is pending.
73
+ if (owner) {
74
+ await owner(signal);
75
+ }
76
+ else {
77
+ process.exit(process.exitCode ?? 0);
78
+ }
79
+ })
80
+ .catch((error) => {
81
+ process.stderr.write(`[cli] shutdown failed: ${error instanceof Error ? error.message : String(error)}\n`);
82
+ process.exit(1);
83
+ });
84
+ });
85
+ }
86
+ process.once("beforeExit", () => {
87
+ void cleanupCliLifecycle();
88
+ });
89
+ }
90
+ //# sourceMappingURL=lifecycle.js.map
@@ -103,9 +103,14 @@ function windowResetEpochSeconds(window, nowSeconds) {
103
103
  }
104
104
  return 0;
105
105
  }
106
- function deriveWindowStatus(usedFraction) {
107
- return usedFraction >= 1 ? "rejected" : "allowed";
106
+ /** Classify only finite, nonnegative utilization; invalid measurements stay unknown. */
107
+ function deriveWindowStatus(percent) {
108
+ if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0) {
109
+ return "unknown";
110
+ }
111
+ return percent >= 100 ? "rejected" : "allowed";
108
112
  }
113
+ /** Normalize a reported window while retaining uncertainty in its status. */
109
114
  function toQuotaWindow(kind, group, window, nowSeconds) {
110
115
  if (!window) {
111
116
  return null;
@@ -115,7 +120,7 @@ function toQuotaWindow(kind, group, window, nowSeconds) {
115
120
  kind,
116
121
  group,
117
122
  used,
118
- status: deriveWindowStatus(used),
123
+ status: deriveWindowStatus(window.used_percent),
119
124
  resetsAt: windowResetEpochSeconds(window, nowSeconds),
120
125
  isActive: true,
121
126
  };
@@ -137,11 +142,16 @@ export function codexRateLimitsToQuota(rateLimits, now = Date.now()) {
137
142
  windows.push(secondaryWindow);
138
143
  }
139
144
  return {
145
+ unifiedStatus: rateLimits?.limit_reached === true || rateLimits?.allowed === false
146
+ ? "rejected"
147
+ : rateLimits?.allowed === true
148
+ ? "allowed"
149
+ : "unknown",
140
150
  sessionUsed,
141
- sessionStatus: deriveWindowStatus(sessionUsed),
151
+ sessionStatus: deriveWindowStatus(primary?.used_percent),
142
152
  sessionResetAt: windowResetEpochSeconds(primary, nowSeconds),
143
153
  weeklyUsed,
144
- weeklyStatus: deriveWindowStatus(weeklyUsed),
154
+ weeklyStatus: deriveWindowStatus(secondary?.used_percent),
145
155
  weeklyResetAt: windowResetEpochSeconds(secondary, nowSeconds),
146
156
  // Codex has no overage/fallback concept; keep neutral defaults.
147
157
  fallbackPercentage: 0,
@@ -169,7 +179,7 @@ export function parseCodexRateLimitHeaders(headers, now = Date.now()) {
169
179
  const rateLimits = "rate_limits" in parsed && parsed.rate_limits
170
180
  ? parsed.rate_limits
171
181
  : parsed;
172
- return codexRateLimitsToQuota(rateLimits, now);
182
+ return { ...codexRateLimitsToQuota(rateLimits, now), source: "headers" };
173
183
  }
174
184
  catch {
175
185
  return null;
@@ -204,6 +214,8 @@ export async function fetchCodexAccountUsage(account, options = {}) {
204
214
  const usage = (await response.json());
205
215
  const rateLimits = usage.rate_limit
206
216
  ? {
217
+ allowed: usage.rate_limit.allowed,
218
+ limit_reached: usage.rate_limit.limit_reached,
207
219
  primary: usage.rate_limit.primary_window,
208
220
  secondary: usage.rate_limit.secondary_window,
209
221
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { ClaudeStreamSerializer, generateToolUseId } from "./claudeFormat.js";
10
10
  import { extractCodexUsage } from "./codexUsage.js";
11
+ import { proxyTokenUsage } from "./proxyTokenUsage.js";
11
12
  export class CodexFallbackResponseError extends Error {
12
13
  status;
13
14
  responseBody;
@@ -367,13 +368,16 @@ export function parseCodexFallbackSSE(sse) {
367
368
  }
368
369
  const parsedUsage = extractCodexUsage(payload);
369
370
  if (parsedUsage) {
370
- usage = {
371
- input: parsedUsage.inputTokens,
372
- output: parsedUsage.outputTokens,
373
- total: parsedUsage.inputTokens + parsedUsage.outputTokens,
371
+ usage = proxyTokenUsage({
372
+ inputTokens: parsedUsage.inputTokens,
373
+ outputTokens: parsedUsage.outputTokens,
374
+ reasoningTokens: parsedUsage.reasoningTokensObserved
375
+ ? parsedUsage.reasoningTokens
376
+ : undefined,
374
377
  cacheReadTokens: parsedUsage.cacheReadTokens,
375
378
  cacheCreationTokens: parsedUsage.cacheCreationTokens,
376
- };
379
+ inputIncludesCachedTokens: true,
380
+ });
377
381
  }
378
382
  textFromResponse = outputTextFromResponse(payload);
379
383
  }
@@ -516,13 +520,16 @@ export async function createCodexFallbackStream(response, model) {
516
520
  completed = true;
517
521
  const parsedUsage = extractCodexUsage(payload);
518
522
  if (parsedUsage) {
519
- usage = {
520
- input: parsedUsage.inputTokens,
521
- output: parsedUsage.outputTokens,
522
- total: parsedUsage.inputTokens + parsedUsage.outputTokens,
523
+ usage = proxyTokenUsage({
524
+ inputTokens: parsedUsage.inputTokens,
525
+ outputTokens: parsedUsage.outputTokens,
526
+ reasoningTokens: parsedUsage.reasoningTokensObserved
527
+ ? parsedUsage.reasoningTokens
528
+ : undefined,
523
529
  cacheReadTokens: parsedUsage.cacheReadTokens,
524
530
  cacheCreationTokens: parsedUsage.cacheCreationTokens,
525
- };
531
+ inputIncludesCachedTokens: true,
532
+ });
526
533
  }
527
534
  const responseBody = payload.response;
528
535
  if (isRecord(responseBody) && Array.isArray(responseBody.output)) {
@@ -107,6 +107,9 @@ export function extractCodexUsage(payload) {
107
107
  cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
108
108
  cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
109
109
  reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
110
+ reasoningTokensObserved: typeof outputDetails?.reasoning_tokens === "number" &&
111
+ Number.isFinite(outputDetails.reasoning_tokens) &&
112
+ outputDetails.reasoning_tokens >= 0,
110
113
  };
111
114
  }
112
115
  /**
@@ -1,3 +1,4 @@
1
+ import { proxyTokenUsage } from "./proxyTokenUsage.js";
1
2
  import { createReadStream } from "node:fs";
2
3
  import { isDeepStrictEqual } from "node:util";
3
4
  import { isProxyAuxiliaryRequest } from "./proxyRequestKind.js";
@@ -411,8 +412,15 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
411
412
  request.outputTokens !== null ||
412
413
  request.cacheReadTokens !== null ||
413
414
  request.cacheCreationTokens !== null) {
415
+ const billingUsage = proxyTokenUsage({
416
+ inputIncludesCachedTokens: request.inputIncludesCachedTokens,
417
+ inputTokens: request.inputTokens ?? 0,
418
+ outputTokens: request.outputTokens ?? 0,
419
+ cacheCreationTokens: request.cacheCreationTokens ?? 0,
420
+ cacheReadTokens: request.cacheReadTokens ?? 0,
421
+ });
414
422
  requestsWithUsage += 1;
415
- inputTokens += request.inputTokens ?? 0;
423
+ inputTokens += billingUsage.input;
416
424
  outputTokens += request.outputTokens ?? 0;
417
425
  cacheReadTokens += request.cacheReadTokens ?? 0;
418
426
  cacheCreationTokens += request.cacheCreationTokens ?? 0;
@@ -422,16 +430,7 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
422
430
  // "openai-compatible" resolves to the cross-provider table search in
423
431
  // pricing.ts, which finds the model wherever it lives — a far better
424
432
  // guess than assuming Anthropic and pricing a GPT model at $0.
425
- const cost = calculateCost(request.provider ?? "openai-compatible", request.model, {
426
- input: request.inputTokens ?? 0,
427
- output: request.outputTokens ?? 0,
428
- total: (request.inputTokens ?? 0) +
429
- (request.outputTokens ?? 0) +
430
- (request.cacheCreationTokens ?? 0) +
431
- (request.cacheReadTokens ?? 0),
432
- cacheCreationTokens: request.cacheCreationTokens ?? 0,
433
- cacheReadTokens: request.cacheReadTokens ?? 0,
434
- });
433
+ const cost = calculateCost(request.provider ?? "openai-compatible", request.model, billingUsage);
435
434
  // Ask the table directly rather than inferring from cost > 0: a real
436
435
  // request with trivial usage can round to $0.000000 and is priced, not
437
436
  // unpriced.
@@ -982,6 +981,10 @@ export async function analyzeProxyLogs(options) {
982
981
  accountType: stringValue(record.accountType) ?? "unknown",
983
982
  model: stringValue(record.model),
984
983
  provider: stringValue(record.provider),
984
+ inputIncludesCachedTokens: typeof record.inputIncludesCachedTokens === "boolean"
985
+ ? record.inputIncludesCachedTokens
986
+ : typeof record.path === "string" &&
987
+ record.path.includes("/backend-api/codex/"),
985
988
  inputTokens: finiteNumber(record.inputTokens),
986
989
  outputTokens: finiteNumber(record.outputTokens),
987
990
  cacheReadTokens: finiteNumber(record.cacheReadTokens),
@@ -0,0 +1,7 @@
1
+ import type { TokenUsage, UsageContext } from "../types/index.js";
2
+ /** Convert proxy wire usage into disjoint billing buckets. Codex input already
3
+ * includes cache tokens; reasoning is included in output for both providers.
4
+ * OpenAI's documented formula subtracts both cached_tokens and cache_write_tokens:
5
+ * https://developers.openai.com/api/docs/guides/prompt-caching#monitor-cache-performance
6
+ */
7
+ export declare function proxyTokenUsage(usage: UsageContext): TokenUsage;
@@ -0,0 +1,21 @@
1
+ /** Convert proxy wire usage into disjoint billing buckets. Codex input already
2
+ * includes cache tokens; reasoning is included in output for both providers.
3
+ * OpenAI's documented formula subtracts both cached_tokens and cache_write_tokens:
4
+ * https://developers.openai.com/api/docs/guides/prompt-caching#monitor-cache-performance
5
+ */
6
+ export function proxyTokenUsage(usage) {
7
+ const cached = usage.cacheReadTokens + usage.cacheCreationTokens;
8
+ const input = usage.inputIncludesCachedTokens
9
+ ? Math.max(0, usage.inputTokens - cached)
10
+ : usage.inputTokens;
11
+ return {
12
+ input,
13
+ output: usage.outputTokens,
14
+ total: usage.inputIncludesCachedTokens
15
+ ? usage.inputTokens + usage.outputTokens
16
+ : input + cached + usage.outputTokens,
17
+ cacheReadTokens: usage.cacheReadTokens,
18
+ cacheCreationTokens: usage.cacheCreationTokens,
19
+ reasoning: usage.reasoningTokens,
20
+ };
21
+ }
@@ -57,6 +57,8 @@ declare class ProxyTracer {
57
57
  startStream(): Span;
58
58
  /** Record account selection outcome on the root span. */
59
59
  setAccountSelection(ctx: AccountSelectionContext): void;
60
+ /** Attribute fallback metrics to the account that actually served them. */
61
+ setServedAccount(account: string, accountType: string): void;
60
62
  /** Record token usage and cost on the root span. */
61
63
  setUsage(ctx: UsageContext): void;
62
64
  /**