@juspay/neurolink 12.40.3 → 12.41.0

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/dist/browser/neurolink.min.js +447 -443
  3. package/dist/cli/commands/proxy.js +5 -1
  4. package/dist/proxy/accountLedger.js +3 -0
  5. package/dist/proxy/codexOutboundCache.d.ts +52 -0
  6. package/dist/proxy/codexOutboundCache.js +147 -0
  7. package/dist/proxy/codexOutboundFallback.d.ts +27 -2
  8. package/dist/proxy/codexOutboundFallback.js +147 -11
  9. package/dist/proxy/codexResponsesFormat.js +7 -1
  10. package/dist/proxy/codexToAnthropicFallback.js +6 -0
  11. package/dist/proxy/proxyAnalysis.js +4 -0
  12. package/dist/proxy/proxyConfig.js +120 -0
  13. package/dist/proxy/proxyTokenUsage.d.ts +15 -0
  14. package/dist/proxy/proxyTokenUsage.js +33 -0
  15. package/dist/proxy/proxyTracer.d.ts +16 -1
  16. package/dist/proxy/proxyTracer.js +26 -5
  17. package/dist/proxy/requestLogger.js +9 -0
  18. package/dist/proxy/runtimeConfig.js +20 -0
  19. package/dist/proxy/sseInterceptor.js +9 -0
  20. package/dist/proxy/vertexAnthropicFallback.js +9 -0
  21. package/dist/server/routes/claudeProxyRoutes.js +65 -1
  22. package/dist/server/routes/codexProxyRoutes.d.ts +39 -3
  23. package/dist/server/routes/codexProxyRoutes.js +792 -8
  24. package/dist/server/routes/index.d.ts +8 -0
  25. package/dist/server/routes/index.js +9 -1
  26. package/dist/types/analytics.d.ts +25 -0
  27. package/dist/types/codex.d.ts +45 -1
  28. package/dist/types/proxy.d.ts +40 -2
  29. package/dist/types/proxyClient.d.ts +2 -0
  30. package/dist/types/server.d.ts +15 -0
  31. package/dist/types/subscription.d.ts +22 -0
  32. package/dist/utils/anthropicCacheBreakpoints.d.ts +2 -2
  33. package/dist/utils/anthropicCacheBreakpoints.js +14 -3
  34. package/dist/utils/pricing.js +101 -24
  35. package/docs-site/static/search-index.json +17 -6
  36. package/package.json +1 -1
@@ -217,6 +217,10 @@ const MIN_SESSION_AFFINITY_IDLE_TTL_MS = 60_000;
217
217
  const MAX_SESSION_AFFINITY_IDLE_TTL_MS = 86_400_000;
218
218
  const MIN_SPILL_INFLIGHT = 0;
219
219
  const MAX_SPILL_INFLIGHT = 100;
220
+ /** Prior design left this uncapped and flagged it as an open risk (unbounded
221
+ * chain degrades worst-case exhausted-turn latency linearly). 8 is deliberate
222
+ * headroom (2 providers x up to 4 models each). */
223
+ const MAX_CODEX_OUTBOUND_FALLBACK_TARGETS = 8;
220
224
  function kebabToCamelRoutingKey(kebabKey) {
221
225
  return kebabKey.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
222
226
  }
@@ -419,6 +423,68 @@ export function validateProxyConfig(config) {
419
423
  rawSpillInflight > MAX_SPILL_INFLIGHT)) {
420
424
  errors.push("routing.spill-inflight must be an integer between 0 and 100");
421
425
  }
426
+ const rawCodexOutboundEnabled = readRoutingPolicyKey(routing, "codex-outbound-fallback-enabled");
427
+ const normalizedCodexOutboundEnabled = typeof rawCodexOutboundEnabled === "string"
428
+ ? rawCodexOutboundEnabled.trim().toLowerCase()
429
+ : undefined;
430
+ if (rawCodexOutboundEnabled !== undefined &&
431
+ typeof rawCodexOutboundEnabled !== "boolean" &&
432
+ normalizedCodexOutboundEnabled !== "true" &&
433
+ normalizedCodexOutboundEnabled !== "false") {
434
+ errors.push("routing.codex-outbound-fallback-enabled must be a boolean");
435
+ }
436
+ const rawCodexOutboundTargets = readRoutingPolicyKey(routing, "codex-outbound-fallback-targets");
437
+ if (rawCodexOutboundTargets !== undefined) {
438
+ if (!Array.isArray(rawCodexOutboundTargets)) {
439
+ errors.push("routing.codex-outbound-fallback-targets must be an array");
440
+ }
441
+ else if (rawCodexOutboundTargets.length > MAX_CODEX_OUTBOUND_FALLBACK_TARGETS) {
442
+ errors.push(`routing.codex-outbound-fallback-targets must have at most ${MAX_CODEX_OUTBOUND_FALLBACK_TARGETS} entries`);
443
+ }
444
+ else {
445
+ rawCodexOutboundTargets.forEach((entry, index) => {
446
+ const field = `routing.codex-outbound-fallback-targets[${index}]`;
447
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
448
+ errors.push(`${field} must be an object`);
449
+ return;
450
+ }
451
+ const target = entry;
452
+ if (target.provider !== "anthropic" && target.provider !== "vertex") {
453
+ errors.push(`${field}.provider must be anthropic or vertex`);
454
+ }
455
+ if (typeof target.model !== "string" || target.model.trim() === "") {
456
+ errors.push(`${field}.model must be a non-empty string`);
457
+ }
458
+ });
459
+ }
460
+ }
461
+ const rawCodexOutboundModelMappings = readRoutingPolicyKey(routing, "codex-outbound-fallback-model-mappings");
462
+ if (rawCodexOutboundModelMappings !== undefined) {
463
+ if (!Array.isArray(rawCodexOutboundModelMappings)) {
464
+ errors.push("routing.codex-outbound-fallback-model-mappings must be an array");
465
+ }
466
+ else {
467
+ rawCodexOutboundModelMappings.forEach((entry, index) => {
468
+ const field = `routing.codex-outbound-fallback-model-mappings[${index}]`;
469
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
470
+ errors.push(`${field} must be an object`);
471
+ return;
472
+ }
473
+ const mapping = entry;
474
+ if (typeof mapping.from !== "string" || mapping.from.trim() === "") {
475
+ errors.push(`${field}.from must be a non-empty string`);
476
+ }
477
+ if (typeof mapping.to !== "string" || mapping.to.trim() === "") {
478
+ errors.push(`${field}.to must be a non-empty string`);
479
+ }
480
+ if (mapping.provider !== undefined &&
481
+ (typeof mapping.provider !== "string" ||
482
+ mapping.provider.trim() === "")) {
483
+ errors.push(`${field}.provider must be a non-empty string`);
484
+ }
485
+ });
486
+ }
487
+ }
422
488
  }
423
489
  if (!hasAccounts && !hasRouting) {
424
490
  errors.push('Config must contain at least one of "accounts" or "routing"');
@@ -754,6 +820,60 @@ export function parseRoutingConfig(raw) {
754
820
  logger.warn(`[proxy-config] Ignoring routing.spillInflight: expected integer between 0 and 100, got ${String(rawSpillInflight)}`);
755
821
  }
756
822
  }
823
+ const rawCodexOutboundEnabled = readRoutingPolicyKey(raw, "codex-outbound-fallback-enabled");
824
+ if (rawCodexOutboundEnabled !== undefined) {
825
+ if (typeof rawCodexOutboundEnabled === "boolean") {
826
+ result.codexOutboundFallbackEnabled = rawCodexOutboundEnabled;
827
+ }
828
+ else if (typeof rawCodexOutboundEnabled === "string" &&
829
+ ["true", "false"].includes(rawCodexOutboundEnabled.trim().toLowerCase())) {
830
+ result.codexOutboundFallbackEnabled =
831
+ rawCodexOutboundEnabled.trim().toLowerCase() === "true";
832
+ }
833
+ else {
834
+ logger.warn(`[proxy-config] Ignoring routing.codexOutboundFallbackEnabled: expected boolean, got ${typeof rawCodexOutboundEnabled}`);
835
+ }
836
+ }
837
+ const rawCodexOutboundTargets = readRoutingPolicyKey(raw, "codex-outbound-fallback-targets");
838
+ if (Array.isArray(rawCodexOutboundTargets)) {
839
+ result.codexOutboundFallbackTargets = rawCodexOutboundTargets
840
+ .filter((t) => t !== null && typeof t === "object")
841
+ .map((t) => {
842
+ const provider = t.provider === "anthropic" || t.provider === "vertex"
843
+ ? t.provider
844
+ : undefined;
845
+ const model = String(t.model ?? "").trim();
846
+ if (!provider || !model) {
847
+ logger.warn(`[proxy-config] Skipping codex-outbound-fallback-targets entry with invalid provider/model: ${JSON.stringify(t)}`);
848
+ return null;
849
+ }
850
+ return { provider, model };
851
+ })
852
+ .filter((t) => t !== null)
853
+ .slice(0, MAX_CODEX_OUTBOUND_FALLBACK_TARGETS);
854
+ }
855
+ // codex-outbound-fallback-model-mappings: identical shape to the existing
856
+ // model-mappings block above — same from/to/provider extraction and drop rule.
857
+ const rawCodexOutboundModelMappings = readRoutingPolicyKey(raw, "codex-outbound-fallback-model-mappings");
858
+ if (Array.isArray(rawCodexOutboundModelMappings)) {
859
+ result.codexOutboundFallbackModelMappings = rawCodexOutboundModelMappings
860
+ .filter((m) => m !== null && typeof m === "object")
861
+ .map((m) => {
862
+ const from = String(m.from ?? "").trim();
863
+ const to = String(m.to ?? "").trim();
864
+ const provider = String(m.provider ?? "anthropic").trim() || "anthropic";
865
+ if (!from || !to) {
866
+ logger.warn(`[proxy-config] Skipping codex-outbound-fallback-model-mappings entry with empty "from" or "to": ${JSON.stringify(m)}`);
867
+ return null;
868
+ }
869
+ return {
870
+ from,
871
+ to,
872
+ provider,
873
+ };
874
+ })
875
+ .filter((m) => m !== null);
876
+ }
757
877
  return result;
758
878
  }
759
879
  // ---------------------------------------------------------------------------
@@ -5,3 +5,18 @@ import type { TokenUsage, UsageContext } from "../types/index.js";
5
5
  * https://developers.openai.com/api/docs/guides/prompt-caching#monitor-cache-performance
6
6
  */
7
7
  export declare function proxyTokenUsage(usage: UsageContext): TokenUsage;
8
+ /**
9
+ * Read Anthropic's `usage.cache_creation.ephemeral_1h_input_tokens` — the
10
+ * 1-hour-TTL share of `cache_creation_input_tokens` — from an untyped usage
11
+ * object. Returns undefined when the breakdown is absent (older replies, or a
12
+ * request that wrote no 1h breakpoint), so callers leave the field unset and
13
+ * price exactly as before.
14
+ */
15
+ export declare function readCacheCreation1hTokens(usage: unknown): number | undefined;
16
+ /**
17
+ * Spread target for a usage literal: the key exists only when a 1h count was
18
+ * observed, so every payload without one keeps its exact old shape.
19
+ */
20
+ export declare function oneHourCacheWriteFields(count: number | undefined): {
21
+ cacheCreation1hTokens?: number;
22
+ };
@@ -16,6 +16,39 @@ export function proxyTokenUsage(usage) {
16
16
  : input + cached + usage.outputTokens,
17
17
  cacheReadTokens: usage.cacheReadTokens,
18
18
  cacheCreationTokens: usage.cacheCreationTokens,
19
+ // Spread only when present so every existing caller's result keeps its
20
+ // exact old shape (no new `undefined`-valued key to trip deep-equal checks).
21
+ ...(usage.cacheCreation1hTokens !== undefined
22
+ ? { cacheCreation1hTokens: usage.cacheCreation1hTokens }
23
+ : {}),
19
24
  reasoning: usage.reasoningTokens,
20
25
  };
21
26
  }
27
+ /**
28
+ * Read Anthropic's `usage.cache_creation.ephemeral_1h_input_tokens` — the
29
+ * 1-hour-TTL share of `cache_creation_input_tokens` — from an untyped usage
30
+ * object. Returns undefined when the breakdown is absent (older replies, or a
31
+ * request that wrote no 1h breakpoint), so callers leave the field unset and
32
+ * price exactly as before.
33
+ */
34
+ export function readCacheCreation1hTokens(usage) {
35
+ if (typeof usage !== "object" || usage === null) {
36
+ return undefined;
37
+ }
38
+ const breakdown = usage.cache_creation;
39
+ if (typeof breakdown !== "object" || breakdown === null) {
40
+ return undefined;
41
+ }
42
+ const raw = breakdown
43
+ .ephemeral_1h_input_tokens;
44
+ return typeof raw === "number" && Number.isFinite(raw) && raw >= 0
45
+ ? raw
46
+ : undefined;
47
+ }
48
+ /**
49
+ * Spread target for a usage literal: the key exists only when a 1h count was
50
+ * observed, so every payload without one keeps its exact old shape.
51
+ */
52
+ export function oneHourCacheWriteFields(count) {
53
+ return count === undefined ? {} : { cacheCreation1hTokens: count };
54
+ }
@@ -14,7 +14,7 @@
14
14
  * - TelemetryService for metrics recording
15
15
  */
16
16
  import { type Span } from "@opentelemetry/api";
17
- import type { AccountSelectionContext, ProxyRequestContext, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
17
+ import type { AccountSelectionContext, ProxyRequestContext, ProxyRequestOrigin, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
18
18
  declare class ProxyTracer {
19
19
  private readonly rootSpan;
20
20
  private readonly proxyTracer;
@@ -40,6 +40,8 @@ declare class ProxyTracer {
40
40
  private accountEmail?;
41
41
  private usage?;
42
42
  private mode;
43
+ /** Where the Claude-shaped request originated, for cache/cost attribution. */
44
+ private requestOrigin;
43
45
  private constructor();
44
46
  /**
45
47
  * Create a root span for a proxy request and set Langfuse context.
@@ -60,6 +62,12 @@ declare class ProxyTracer {
60
62
  setAccountSelection(ctx: AccountSelectionContext): void;
61
63
  /** Attribute fallback metrics to the account that actually served them. */
62
64
  setServedAccount(account: string, accountType: string): void;
65
+ /**
66
+ * Record where the Claude-shaped request this span covers originated, so
67
+ * the token/cost counters below can be broken down by origin (e.g. to
68
+ * measure Codex-fallback cache-hit rate separately from native traffic).
69
+ */
70
+ setRequestOrigin(origin: ProxyRequestOrigin): void;
63
71
  /** Record token usage and cost on the root span. */
64
72
  setUsage(ctx: UsageContext, options?: ProxyUsageAttributionOptions): void;
65
73
  /**
@@ -126,6 +134,13 @@ export declare function recordFallbackAttempt(attrs: {
126
134
  status: "success" | "failure";
127
135
  errorMessage?: string;
128
136
  durationMs: number;
137
+ /** Namespaces the leg-health key for a fallback direction other than the
138
+ * existing Claude-engine chain (e.g. `"codex-outbound"`), so a dead-leg
139
+ * alert for `anthropic/claude-...` reached via Codex-outbound fallback
140
+ * never merges its failure streak with the same provider/model reached
141
+ * through the Claude engine's own configured chain. Omitted entirely by
142
+ * every existing call site, which keeps their leg key exactly as before. */
143
+ direction?: string;
129
144
  }): void;
130
145
  /**
131
146
  * Records one Codex-outbound tool-declaration schema degradation (a
@@ -221,6 +221,8 @@ class ProxyTracer {
221
221
  accountEmail;
222
222
  usage;
223
223
  mode = "full";
224
+ /** Where the Claude-shaped request originated, for cache/cost attribution. */
225
+ requestOrigin = "native";
224
226
  constructor(rootSpan, requestId, model, stream, billingProvider) {
225
227
  this.rootSpan = rootSpan;
226
228
  this.requestId = requestId;
@@ -380,6 +382,15 @@ class ProxyTracer {
380
382
  "proxy.account.served_type": accountType,
381
383
  });
382
384
  }
385
+ /**
386
+ * Record where the Claude-shaped request this span covers originated, so
387
+ * the token/cost counters below can be broken down by origin (e.g. to
388
+ * measure Codex-fallback cache-hit rate separately from native traffic).
389
+ */
390
+ setRequestOrigin(origin) {
391
+ this.requestOrigin = origin;
392
+ this.rootSpan.setAttribute("proxy.request_origin", origin);
393
+ }
383
394
  /** Record token usage and cost on the root span. */
384
395
  setUsage(ctx, options = {}) {
385
396
  if (options.recordMetrics !== undefined) {
@@ -695,10 +706,18 @@ class ProxyTracer {
695
706
  }
696
707
  // Token metrics (only if usage was captured)
697
708
  if (this.recordUsageMetrics && this.usage) {
698
- const tokenLabels = {
699
- model: this.model,
700
- account: this.accountEmail ?? "unknown",
701
- };
709
+ // `origin` is added to the label set only when it is not the default
710
+ // "native" value, so every pre-existing series keeps its exact old
711
+ // label set {model, account} when the Codex-outbound-fallback flag is
712
+ // off (or simply unused) — Ruling 1's byte-for-byte requirement — and
713
+ // no metric series is restarted for traffic this PR does not touch.
714
+ const tokenLabels = this.requestOrigin === "native"
715
+ ? { model: this.model, account: this.accountEmail ?? "unknown" }
716
+ : {
717
+ model: this.model,
718
+ account: this.accountEmail ?? "unknown",
719
+ origin: this.requestOrigin,
720
+ };
702
721
  m.tokensInput.add(proxyTokenUsage(this.usage).input, tokenLabels);
703
722
  m.tokensOutput.add(this.usage.outputTokens, tokenLabels);
704
723
  // Skipped, not zeroed, when the provider reported no breakdown: a zero
@@ -850,7 +869,9 @@ export function recordFallbackAttempt(attrs) {
850
869
  // Leg health is tracked outside the metrics try/catch: a telemetry failure
851
870
  // must not be the reason an operator never hears that a leg is dead.
852
871
  try {
853
- trackFallbackLegHealth(`${attrs.provider}/${attrs.model}`, attrs.status, attrs.errorMessage);
872
+ trackFallbackLegHealth(attrs.direction
873
+ ? `${attrs.direction}:${attrs.provider}/${attrs.model}`
874
+ : `${attrs.provider}/${attrs.model}`, attrs.status, attrs.errorMessage);
854
875
  }
855
876
  catch {
856
877
  // Never let health reporting break a request.
@@ -239,6 +239,7 @@ function annotateRequestPricing(entry) {
239
239
  const output = entry.outputTokens;
240
240
  const cacheRead = entry.cacheReadTokens ?? 0;
241
241
  const cacheCreation = entry.cacheCreationTokens ?? 0;
242
+ const cacheCreation1h = entry.cacheCreation1hTokens;
242
243
  if (input === undefined ||
243
244
  output === undefined ||
244
245
  !Number.isFinite(input) ||
@@ -249,6 +250,11 @@ function annotateRequestPricing(entry) {
249
250
  cacheRead < 0 ||
250
251
  !Number.isFinite(cacheCreation) ||
251
252
  cacheCreation < 0 ||
253
+ // The 1h count is a subset of the cache-write total.
254
+ (cacheCreation1h !== undefined &&
255
+ (!Number.isFinite(cacheCreation1h) ||
256
+ cacheCreation1h < 0 ||
257
+ cacheCreation1h > cacheCreation)) ||
252
258
  (entry.inputIncludesCachedTokens === true &&
253
259
  cacheRead + cacheCreation > input) ||
254
260
  // When input includes the cached portion but the provider never reported
@@ -275,6 +281,9 @@ function annotateRequestPricing(entry) {
275
281
  cacheReadTokens: cacheRead,
276
282
  cacheCreationTokens: cacheCreation,
277
283
  inputIncludesCachedTokens: entry.inputIncludesCachedTokens,
284
+ ...(entry.cacheCreation1hTokens !== undefined
285
+ ? { cacheCreation1hTokens: entry.cacheCreation1hTokens }
286
+ : {}),
278
287
  });
279
288
  entry.pricingStatus = "exact";
280
289
  entry.apiEquivalentCostUsd = calculateCost(entry.pricingProvider, entry.model, usage);
@@ -39,6 +39,17 @@ function resolveQuotaRoutingEnabled(envValue, configured) {
39
39
  const normalized = envValue.trim().toLowerCase();
40
40
  return normalized !== "off" && normalized !== "false" && normalized !== "0";
41
41
  }
42
+ function resolveCodexOutboundFallbackEnabled(envValue, configured) {
43
+ if (envValue === undefined || envValue.trim() === "") {
44
+ // Unlike resolveQuotaRoutingEnabled's `configured ?? true`, this defaults
45
+ // false — required so today's behavior is byte-identical when unconfigured.
46
+ // An empty/whitespace-only value (e.g. `FOO=` in a .env file) is treated
47
+ // the same as unset, not as an explicit opt-in.
48
+ return configured ?? false;
49
+ }
50
+ const normalized = envValue.trim().toLowerCase();
51
+ return normalized !== "off" && normalized !== "false" && normalized !== "0";
52
+ }
42
53
  function resolveSessionSoftLimit(envValue, configured, rejectInvalid) {
43
54
  if (envValue !== undefined) {
44
55
  const parsed = Number(envValue);
@@ -234,6 +245,9 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
234
245
  const sessionAffinity = routing?.sessionAffinity ?? false;
235
246
  const sessionAffinityIdleTtlMs = routing?.sessionAffinityIdleTtlMs ?? 3_600_000;
236
247
  const spillInflight = routing?.spillInflight ?? 0;
248
+ const codexOutboundFallbackEnabled = resolveCodexOutboundFallbackEnabled(effectiveEnv.NEUROLINK_PROXY_CODEX_OUTBOUND_FALLBACK, routing?.codexOutboundFallbackEnabled);
249
+ const codexOutboundFallbackTargets = routing?.codexOutboundFallbackTargets ?? [];
250
+ const codexOutboundFallbackModelMappings = routing?.codexOutboundFallbackModelMappings ?? [];
237
251
  const fingerprintSource = JSON.stringify({
238
252
  strategy,
239
253
  passthrough: options.passthrough,
@@ -249,6 +263,9 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
249
263
  sessionAffinity,
250
264
  sessionAffinityIdleTtlMs,
251
265
  spillInflight,
266
+ codexOutboundFallbackEnabled,
267
+ codexOutboundFallbackTargets,
268
+ codexOutboundFallbackModelMappings,
252
269
  });
253
270
  const configHash = createHash("sha256")
254
271
  .update(fingerprintSource)
@@ -274,6 +291,9 @@ async function buildCandidate(options, generation, allowMissingConfig, allowMiss
274
291
  sessionAffinity,
275
292
  sessionAffinityIdleTtlMs,
276
293
  spillInflight,
294
+ codexOutboundFallbackEnabled,
295
+ codexOutboundFallbackTargets,
296
+ codexOutboundFallbackModelMappings,
277
297
  }),
278
298
  configFilePresent,
279
299
  envFilePresent,
@@ -16,6 +16,7 @@
16
16
  * const data = await telemetry; // resolves on stream end
17
17
  */
18
18
  import { hasUsefulClaudeContent } from "./claudeOutputObservation.js";
19
+ import { readCacheCreation1hTokens } from "./proxyTokenUsage.js";
19
20
  // ---------------------------------------------------------------------------
20
21
  // Constants
21
22
  // ---------------------------------------------------------------------------
@@ -180,6 +181,9 @@ function finalize(acc) {
180
181
  inputTokens: acc.inputTokens,
181
182
  outputTokens: acc.outputTokens,
182
183
  cacheCreationInputTokens: acc.cacheCreationInputTokens,
184
+ ...(acc.cacheCreation1hInputTokens !== undefined
185
+ ? { cacheCreation1hInputTokens: acc.cacheCreation1hInputTokens }
186
+ : {}),
183
187
  cacheReadInputTokens: acc.cacheReadInputTokens,
184
188
  totalTokens,
185
189
  },
@@ -213,6 +217,11 @@ function processMessageStart(acc, parsed) {
213
217
  acc.inputTokens += usage.input_tokens ?? 0;
214
218
  acc.outputTokens += usage.output_tokens ?? 0;
215
219
  acc.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0;
220
+ const oneHour = readCacheCreation1hTokens(usage);
221
+ if (oneHour !== undefined) {
222
+ acc.cacheCreation1hInputTokens =
223
+ (acc.cacheCreation1hInputTokens ?? 0) + oneHour;
224
+ }
216
225
  acc.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0;
217
226
  }
218
227
  }
@@ -15,6 +15,7 @@
15
15
  import { logger } from "../utils/logger.js";
16
16
  import { claudeDisabledThinkingReplacement, modelSupportsForcedToolChoice, } from "../models/modelRegistry.js";
17
17
  import { isTransientNetworkError } from "./proxyFetch.js";
18
+ import { readCacheCreation1hTokens } from "./proxyTokenUsage.js";
18
19
  /** Vertex pins the Anthropic API version in the body, not a header. */
19
20
  const VERTEX_ANTHROPIC_VERSION = "vertex-2023-10-16";
20
21
  const CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
@@ -640,6 +641,14 @@ function readUsageEvent(event, into) {
640
641
  into.inputTokens = num(usage.input_tokens);
641
642
  into.cacheCreationTokens = num(usage.cache_creation_input_tokens);
642
643
  into.cacheReadTokens = num(usage.cache_read_input_tokens);
644
+ // The 1h-TTL share of the cache-write total, reported by Anthropic as
645
+ // `cache_creation.ephemeral_1h_input_tokens`. Set only when the breakdown
646
+ // holds a valid count, so any other reply leaves the field unset and
647
+ // prices exactly as it did before.
648
+ const oneHour = readCacheCreation1hTokens(usage);
649
+ if (oneHour !== undefined) {
650
+ into.cacheCreation1hTokens = oneHour;
651
+ }
643
652
  }
644
653
  // Every message_delta restates the running total, so the last one wins.
645
654
  const output = num(usage.output_tokens);
@@ -169,6 +169,7 @@ const UPSTREAM_FETCH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
169
169
  import { prepareProxyRequestContext, ProxyContextPreflightError, } from "../../proxy/proxyContextPreflight.js";
170
170
  import { reserveProxyTokenBudget, getProxyTokenBudgetError, getProxyTokenBudgetSessionKey, } from "../../proxy/proxyTokenBudget.js";
171
171
  import { observeAnthropicBudgetResponse } from "../../proxy/anthropicBudgetResponse.js";
172
+ import { readCacheCreation1hTokens, oneHourCacheWriteFields, } from "../../proxy/proxyTokenUsage.js";
172
173
  function prepareAnthropicWire(ctx, bodyStr) {
173
174
  const body = JSON.parse(bodyStr);
174
175
  const prepared = prepareProxyRequestContext({
@@ -2777,6 +2778,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2777
2778
  inputTokens: data.usage.inputTokens,
2778
2779
  outputTokens: data.usage.outputTokens,
2779
2780
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
2781
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
2780
2782
  cacheReadTokens: data.usage.cacheReadInputTokens,
2781
2783
  });
2782
2784
  capturedTracer.logStreamEvents(data.events);
@@ -2787,6 +2789,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2787
2789
  inputTokens: data.usage.inputTokens,
2788
2790
  outputTokens: data.usage.outputTokens,
2789
2791
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
2792
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
2790
2793
  cacheReadTokens: data.usage.cacheReadInputTokens,
2791
2794
  };
2792
2795
  if (!isNaN(rateLimit5h)) {
@@ -2810,6 +2813,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2810
2813
  inputTokens: data.usage.inputTokens,
2811
2814
  outputTokens: data.usage.outputTokens,
2812
2815
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
2816
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
2813
2817
  cacheReadTokens: data.usage.cacheReadInputTokens,
2814
2818
  });
2815
2819
  logProxyBody({
@@ -2881,6 +2885,7 @@ async function handleClaudePassthroughStreamResponse(args) {
2881
2885
  inputTokens: data.usage.inputTokens,
2882
2886
  outputTokens: data.usage.outputTokens,
2883
2887
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
2888
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
2884
2889
  cacheReadTokens: data.usage.cacheReadInputTokens,
2885
2890
  });
2886
2891
  logProxyBody({
@@ -2977,6 +2982,7 @@ async function handleClaudePassthroughJsonResponse(args) {
2977
2982
  inputTokens: usage.input_tokens ?? 0,
2978
2983
  outputTokens: usage.output_tokens ?? 0,
2979
2984
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2985
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
2980
2986
  cacheReadTokens: usage.cache_read_input_tokens ?? 0,
2981
2987
  });
2982
2988
  const rateLimit5h = parseFloat(response.headers.get("anthropic-ratelimit-unified-5h-utilization") ??
@@ -2988,6 +2994,7 @@ async function handleClaudePassthroughJsonResponse(args) {
2988
2994
  inputTokens: usage.input_tokens ?? 0,
2989
2995
  outputTokens: usage.output_tokens ?? 0,
2990
2996
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
2997
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
2991
2998
  cacheReadTokens: usage.cache_read_input_tokens ?? 0,
2992
2999
  };
2993
3000
  if (!isNaN(rateLimit5h)) {
@@ -3009,6 +3016,7 @@ async function handleClaudePassthroughJsonResponse(args) {
3009
3016
  inputTokens: usage?.input_tokens,
3010
3017
  outputTokens: usage?.output_tokens,
3011
3018
  cacheCreationTokens: usage?.cache_creation_input_tokens,
3019
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
3012
3020
  cacheReadTokens: usage?.cache_read_input_tokens,
3013
3021
  });
3014
3022
  }
@@ -5248,6 +5256,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
5248
5256
  inputTokens: data.usage.inputTokens,
5249
5257
  outputTokens: data.usage.outputTokens,
5250
5258
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
5259
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
5251
5260
  cacheReadTokens: data.usage.cacheReadInputTokens,
5252
5261
  });
5253
5262
  // Bill the borrowing grant from the same totals. A stream's usage is
@@ -5272,6 +5281,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
5272
5281
  inputTokens: data.usage.inputTokens,
5273
5282
  outputTokens: data.usage.outputTokens,
5274
5283
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
5284
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
5275
5285
  cacheReadTokens: data.usage.cacheReadInputTokens,
5276
5286
  };
5277
5287
  if (!isNaN(rateLimit5h)) {
@@ -5292,6 +5302,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
5292
5302
  inputTokens: data.usage.inputTokens,
5293
5303
  outputTokens: data.usage.outputTokens,
5294
5304
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
5305
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
5295
5306
  cacheReadTokens: data.usage.cacheReadInputTokens,
5296
5307
  };
5297
5308
  if (failure) {
@@ -5381,6 +5392,7 @@ function attachAnthropicSuccessStreamTelemetry(args) {
5381
5392
  inputTokens: data.usage.inputTokens,
5382
5393
  outputTokens: data.usage.outputTokens,
5383
5394
  cacheCreationTokens: data.usage.cacheCreationInputTokens,
5395
+ ...oneHourCacheWriteFields(data.usage.cacheCreation1hInputTokens),
5384
5396
  cacheReadTokens: data.usage.cacheReadInputTokens,
5385
5397
  };
5386
5398
  // Settled on the untraced path too: whether telemetry is exported has
@@ -5527,6 +5539,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5527
5539
  inputTokens: usage.input_tokens ?? 0,
5528
5540
  outputTokens: usage.output_tokens ?? 0,
5529
5541
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
5542
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
5530
5543
  cacheReadTokens: usage.cache_read_input_tokens ?? 0,
5531
5544
  });
5532
5545
  const rateLimit5h = parseFloat(response.headers.get("anthropic-ratelimit-unified-5h-utilization") ??
@@ -5538,6 +5551,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5538
5551
  inputTokens: usage.input_tokens ?? 0,
5539
5552
  outputTokens: usage.output_tokens ?? 0,
5540
5553
  cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
5554
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
5541
5555
  cacheReadTokens: usage.cache_read_input_tokens ?? 0,
5542
5556
  };
5543
5557
  if (!isNaN(rateLimit5h)) {
@@ -5559,6 +5573,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5559
5573
  inputTokens: usage?.input_tokens,
5560
5574
  outputTokens: usage?.output_tokens,
5561
5575
  cacheCreationTokens: usage?.cache_creation_input_tokens,
5576
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(usage)),
5562
5577
  cacheReadTokens: usage?.cache_read_input_tokens,
5563
5578
  });
5564
5579
  }
@@ -5571,6 +5586,7 @@ async function handleAnthropicJsonSuccessResponse(args) {
5571
5586
  inputTokens: noTracerUsage?.input_tokens,
5572
5587
  outputTokens: noTracerUsage?.output_tokens,
5573
5588
  cacheCreationTokens: noTracerUsage?.cache_creation_input_tokens,
5589
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(noTracerUsage)),
5574
5590
  cacheReadTokens: noTracerUsage?.cache_read_input_tokens,
5575
5591
  });
5576
5592
  }
@@ -5646,6 +5662,7 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
5646
5662
  inputTokens: retryUsage.input_tokens ?? 0,
5647
5663
  outputTokens: retryUsage.output_tokens ?? 0,
5648
5664
  cacheCreationTokens: retryUsage.cache_creation_input_tokens ?? 0,
5665
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(retryUsage)),
5649
5666
  cacheReadTokens: retryUsage.cache_read_input_tokens ?? 0,
5650
5667
  });
5651
5668
  }
@@ -5659,6 +5676,7 @@ async function handleAnthropicSuccessfulNonStreamRetryResponse(args) {
5659
5676
  inputTokens: retryUsage?.input_tokens,
5660
5677
  outputTokens: retryUsage?.output_tokens,
5661
5678
  cacheCreationTokens: retryUsage?.cache_creation_input_tokens,
5679
+ ...oneHourCacheWriteFields(readCacheCreation1hTokens(retryUsage)),
5662
5680
  cacheReadTokens: retryUsage?.cache_read_input_tokens,
5663
5681
  });
5664
5682
  }
@@ -6637,6 +6655,9 @@ function createClaudeRequestRuntimeContext(args) {
6637
6655
  ...(extra?.cacheCreationTokens !== undefined
6638
6656
  ? { cacheCreationTokens: extra.cacheCreationTokens }
6639
6657
  : {}),
6658
+ ...(extra?.cacheCreation1hTokens !== undefined
6659
+ ? { cacheCreation1hTokens: extra.cacheCreation1hTokens }
6660
+ : {}),
6640
6661
  ...(extra?.cacheReadTokens !== undefined
6641
6662
  ? { cacheReadTokens: extra.cacheReadTokens }
6642
6663
  : {}),
@@ -6736,6 +6757,9 @@ function createAnthropicAttemptLogger(args) {
6736
6757
  ...(extra?.cacheCreationTokens !== undefined
6737
6758
  ? { cacheCreationTokens: extra.cacheCreationTokens }
6738
6759
  : {}),
6760
+ ...(extra?.cacheCreation1hTokens !== undefined
6761
+ ? { cacheCreation1hTokens: extra.cacheCreation1hTokens }
6762
+ : {}),
6739
6763
  ...(extra?.cacheReadTokens !== undefined
6740
6764
  ? { cacheReadTokens: extra.cacheReadTokens }
6741
6765
  : {}),
@@ -7106,7 +7130,47 @@ function buildDeferredClaudeAccountFailureResponse(args) {
7106
7130
  async function handleAnthropicRoutedClaudeRequest(args) {
7107
7131
  const { ctx, body, modelRouter, configGeneration, tracer, requestStartTime, accountStrategy, primaryAccountKey, accountAllowlist, quotaRoutingEnabled = isQuotaRoutingEnabled(), sessionSoftLimit = getSessionSoftLimit(), sessionResetToleranceMs = getSessionResetToleranceMs(), ranking = "expiry-first", preferPrimary = false, sessionAffinityEnabled = false, sessionAffinityIdleTtlMs = 3_600_000, spillInflight = 0, buildLoggedClaudeError, logProxyBody, logFinalRequest, setRoutingDecision, } = args;
7108
7132
  const parsedRequest = parseClaudeRequest(body);
7109
- const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, modelRouter?.getFallbackChain() ?? [], body.model, parsedRequest);
7133
+ // Loop prevention for the Codex-outbound-fallback leg (stage-c-trigger.md
7134
+ // §3): a request that reached this handler AS a Codex-outbound fallback
7135
+ // loopback call must never be offered a fallback chain that routes back
7136
+ // into Codex, or Codex(exhausted) -> Anthropic -> Codex could cycle.
7137
+ //
7138
+ // The `x-neurolink-internal-origin` header alone is NOT the security
7139
+ // boundary — any external client can send an arbitrary header. The real
7140
+ // boundary is `consumeInternalProxyRequest`: it looks the caller-supplied
7141
+ // `x-neurolink-internal-request` token up in a server-only, in-memory map
7142
+ // keyed by a `crypto.randomUUID()` this same process minted (in
7143
+ // `registerInternalProxyRequest`, codexProxyRoutes.ts) immediately before
7144
+ // issuing its own loopback fetch, and returns a value only when that
7145
+ // exact token is present. An external client cannot guess or observe
7146
+ // that token — it never appears in any response the proxy sends a
7147
+ // client — so it cannot forge a match.
7148
+ //
7149
+ // That call is single-use (deleted on read) and already made exactly
7150
+ // once per request, earlier, by this worker's request-tracking
7151
+ // middleware (`registerProxyRequestTracking` in proxy.ts) — which needs
7152
+ // the same answer to set `RuntimeRequestMetadata.accountingScope` before
7153
+ // any route handler runs. Calling `consumeInternalProxyRequest` a SECOND
7154
+ // time here, with the same token, always finds it already deleted and
7155
+ // returns `undefined` — silently leaving this filter permanently
7156
+ // disengaged regardless of whether the request was genuinely internal.
7157
+ // Read the middleware's already-authoritative result instead, via
7158
+ // `getProxyRequestAccounting(ctx.requestId)` — the same lookup this file
7159
+ // already uses elsewhere (~9322, ~9510) to distinguish an internal leg
7160
+ // from a client request, and which is populated only when the
7161
+ // middleware's own `consumeInternalProxyRequest` call found a genuine
7162
+ // token match. This preserves the same unforgeable boundary without
7163
+ // consuming the token twice.
7164
+ const isCodexOutboundFallbackLeg = ctx.headers["x-neurolink-internal-origin"] === "codex-outbound-fallback" &&
7165
+ getProxyRequestAccounting(ctx.requestId)?.accountingScope === "internal";
7166
+ // The loopback leg owns the turn's usage metrics (its Codex parent records
7167
+ // none), so the fallback origin label has to be stamped here.
7168
+ if (isCodexOutboundFallbackLeg) {
7169
+ tracer?.setRequestOrigin("codex-fallback");
7170
+ }
7171
+ const configuredFallbackPlan = buildProxyTranslationPlan({ provider: "anthropic", model: body.model }, isCodexOutboundFallbackLeg
7172
+ ? (modelRouter?.getFallbackChain() ?? []).filter((entry) => entry.provider !== "codex")
7173
+ : (modelRouter?.getFallbackChain() ?? []), body.model, parsedRequest);
7110
7174
  const hasConfiguredFallback = configuredFallbackPlan.attempts
7111
7175
  .slice(1)
7112
7176
  .some((attempt) => Boolean(attempt.provider && attempt.model));