@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.
- package/CHANGELOG.md +3 -3
- package/dist/browser/neurolink.min.js +447 -443
- package/dist/cli/commands/proxy.js +5 -1
- package/dist/proxy/accountLedger.js +3 -0
- package/dist/proxy/codexOutboundCache.d.ts +52 -0
- package/dist/proxy/codexOutboundCache.js +147 -0
- package/dist/proxy/codexOutboundFallback.d.ts +27 -2
- package/dist/proxy/codexOutboundFallback.js +147 -11
- package/dist/proxy/codexResponsesFormat.js +7 -1
- package/dist/proxy/codexToAnthropicFallback.js +6 -0
- package/dist/proxy/proxyAnalysis.js +4 -0
- package/dist/proxy/proxyConfig.js +120 -0
- package/dist/proxy/proxyTokenUsage.d.ts +15 -0
- package/dist/proxy/proxyTokenUsage.js +33 -0
- package/dist/proxy/proxyTracer.d.ts +16 -1
- package/dist/proxy/proxyTracer.js +26 -5
- package/dist/proxy/requestLogger.js +9 -0
- package/dist/proxy/runtimeConfig.js +20 -0
- package/dist/proxy/sseInterceptor.js +9 -0
- package/dist/proxy/vertexAnthropicFallback.js +9 -0
- package/dist/server/routes/claudeProxyRoutes.js +65 -1
- package/dist/server/routes/codexProxyRoutes.d.ts +39 -3
- package/dist/server/routes/codexProxyRoutes.js +792 -8
- package/dist/server/routes/index.d.ts +8 -0
- package/dist/server/routes/index.js +9 -1
- package/dist/types/analytics.d.ts +25 -0
- package/dist/types/codex.d.ts +45 -1
- package/dist/types/proxy.d.ts +40 -2
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/server.d.ts +15 -0
- package/dist/types/subscription.d.ts +22 -0
- package/dist/utils/anthropicCacheBreakpoints.d.ts +2 -2
- package/dist/utils/anthropicCacheBreakpoints.js +14 -3
- package/dist/utils/pricing.js +101 -24
- package/docs-site/static/search-index.json +17 -6
- 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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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(
|
|
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
|
-
|
|
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));
|