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