@juspay/neurolink 11.11.7 → 11.13.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 +6 -2
- package/dist/browser/neurolink.min.js +396 -396
- package/dist/cli/commands/proxy.js +42 -0
- package/dist/cli/commands/proxyAnalyze.js +10 -1
- package/dist/cli/proxy-clients/claudeCode.js +42 -10
- package/dist/cli/proxy-clients/openCode.js +37 -15
- package/dist/cli/proxy-clients/qwenCode.js +33 -9
- package/dist/cli/proxy-clients/registry.js +10 -2
- package/dist/cli/proxy-clients/snapshot.d.ts +52 -0
- package/dist/cli/proxy-clients/snapshot.js +98 -0
- package/dist/lib/providers/googleAiStudio/client.js +6 -3
- package/dist/lib/providers/googleVertex/client.d.ts +33 -0
- package/dist/lib/providers/googleVertex/client.js +110 -11
- package/dist/lib/proxy/codexUsage.d.ts +68 -0
- package/dist/lib/proxy/codexUsage.js +247 -0
- package/dist/lib/proxy/proxyAnalysis.js +87 -3
- package/dist/lib/proxy/proxyFetch.d.ts +1 -0
- package/dist/lib/proxy/proxyFetch.js +29 -0
- package/dist/lib/proxy/proxyTracer.d.ts +13 -2
- package/dist/lib/proxy/proxyTracer.js +29 -7
- package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
- package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
- package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
- package/dist/lib/types/proxy.d.ts +65 -0
- package/dist/lib/utils/pricing.d.ts +9 -0
- package/dist/lib/utils/pricing.js +136 -1
- package/dist/providers/googleAiStudio/client.js +6 -3
- package/dist/providers/googleVertex/client.d.ts +33 -0
- package/dist/providers/googleVertex/client.js +110 -11
- package/dist/proxy/codexUsage.d.ts +68 -0
- package/dist/proxy/codexUsage.js +246 -0
- package/dist/proxy/proxyAnalysis.js +87 -3
- package/dist/proxy/proxyFetch.d.ts +1 -0
- package/dist/proxy/proxyFetch.js +29 -0
- package/dist/proxy/proxyTracer.d.ts +13 -2
- package/dist/proxy/proxyTracer.js +29 -7
- package/dist/proxy/proxyTranslationEngine.js +22 -5
- package/dist/server/routes/codexProxyRoutes.js +29 -1
- package/dist/server/routes/openaiProxyRoutes.js +5 -0
- package/dist/types/proxy.d.ts +65 -0
- package/dist/utils/pricing.d.ts +9 -0
- package/dist/utils/pricing.js +136 -1
- package/package.json +2 -1
|
@@ -20,7 +20,18 @@ declare class ProxyTracer {
|
|
|
20
20
|
private readonly proxyTracer;
|
|
21
21
|
private readonly bridge;
|
|
22
22
|
private readonly requestId;
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Model used for costing. Starts as the model the client asked for and is
|
|
25
|
+
* updated by setModelSubstitution(), so cost follows the model that actually
|
|
26
|
+
* served the request rather than the one that was requested.
|
|
27
|
+
*/
|
|
28
|
+
private model;
|
|
29
|
+
/**
|
|
30
|
+
* Provider used for costing. See ProxyRequestContext.provider. Mutable for
|
|
31
|
+
* the same reason `model` is: a fallback can serve the request from a
|
|
32
|
+
* different provider, and pricing has to follow it.
|
|
33
|
+
*/
|
|
34
|
+
private billingProvider;
|
|
24
35
|
private readonly startTime;
|
|
25
36
|
private readonly isStream;
|
|
26
37
|
private accountEmail?;
|
|
@@ -60,7 +71,7 @@ declare class ProxyTracer {
|
|
|
60
71
|
* Record that the proxy substituted a different model than was requested.
|
|
61
72
|
* Sets span attributes and increments the substitution metric counter.
|
|
62
73
|
*/
|
|
63
|
-
setModelSubstitution(requestedModel: string, actualModel: string): void;
|
|
74
|
+
setModelSubstitution(requestedModel: string, actualModel: string, actualProvider?: string): void;
|
|
64
75
|
setFallbackInfo(info: {
|
|
65
76
|
triggered: boolean;
|
|
66
77
|
provider?: string;
|
|
@@ -191,16 +191,28 @@ class ProxyTracer {
|
|
|
191
191
|
proxyTracer = getTracer("neurolink.proxy");
|
|
192
192
|
bridge = new OtelBridge();
|
|
193
193
|
requestId;
|
|
194
|
+
/**
|
|
195
|
+
* Model used for costing. Starts as the model the client asked for and is
|
|
196
|
+
* updated by setModelSubstitution(), so cost follows the model that actually
|
|
197
|
+
* served the request rather than the one that was requested.
|
|
198
|
+
*/
|
|
194
199
|
model;
|
|
200
|
+
/**
|
|
201
|
+
* Provider used for costing. See ProxyRequestContext.provider. Mutable for
|
|
202
|
+
* the same reason `model` is: a fallback can serve the request from a
|
|
203
|
+
* different provider, and pricing has to follow it.
|
|
204
|
+
*/
|
|
205
|
+
billingProvider;
|
|
195
206
|
startTime;
|
|
196
207
|
isStream;
|
|
197
208
|
accountEmail;
|
|
198
209
|
usage;
|
|
199
210
|
mode = "full";
|
|
200
|
-
constructor(rootSpan, requestId, model, stream) {
|
|
211
|
+
constructor(rootSpan, requestId, model, stream, billingProvider) {
|
|
201
212
|
this.rootSpan = rootSpan;
|
|
202
213
|
this.requestId = requestId;
|
|
203
214
|
this.model = model;
|
|
215
|
+
this.billingProvider = billingProvider;
|
|
204
216
|
this.startTime = Date.now();
|
|
205
217
|
this.isStream = stream;
|
|
206
218
|
}
|
|
@@ -257,7 +269,7 @@ class ProxyTracer {
|
|
|
257
269
|
if (nlConversationId) {
|
|
258
270
|
rootSpan.setAttribute("neurolink.conversation_id", nlConversationId);
|
|
259
271
|
}
|
|
260
|
-
const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream);
|
|
272
|
+
const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream, ctx.provider ?? "anthropic");
|
|
261
273
|
// Set Langfuse context (fire-and-forget — non-blocking)
|
|
262
274
|
// Prefer NeuroLink session/user from calling SDK over Claude Code session
|
|
263
275
|
setLangfuseContext({
|
|
@@ -370,7 +382,7 @@ class ProxyTracer {
|
|
|
370
382
|
"gen_ai.usage.total_tokens": totalTokens,
|
|
371
383
|
});
|
|
372
384
|
// Cost calculation via pricing.ts
|
|
373
|
-
const cost = calculateCost(
|
|
385
|
+
const cost = calculateCost(this.billingProvider, this.model, {
|
|
374
386
|
input: ctx.inputTokens,
|
|
375
387
|
output: ctx.outputTokens,
|
|
376
388
|
total: totalTokens,
|
|
@@ -433,12 +445,22 @@ class ProxyTracer {
|
|
|
433
445
|
* Record that the proxy substituted a different model than was requested.
|
|
434
446
|
* Sets span attributes and increments the substitution metric counter.
|
|
435
447
|
*/
|
|
436
|
-
setModelSubstitution(requestedModel, actualModel) {
|
|
448
|
+
setModelSubstitution(requestedModel, actualModel, actualProvider) {
|
|
449
|
+
// Cost must follow the model that served the request, not the alias the
|
|
450
|
+
// client typed — otherwise a claude-* alias routed to another provider is
|
|
451
|
+
// billed at Claude rates. The provider has to move with it: leaving it at
|
|
452
|
+
// the tracer's default means the substituted model is looked up in the
|
|
453
|
+
// wrong pricing table, which yields no rate and so no charge at all.
|
|
454
|
+
this.model = actualModel;
|
|
455
|
+
if (actualProvider) {
|
|
456
|
+
this.billingProvider = actualProvider;
|
|
457
|
+
}
|
|
437
458
|
this.rootSpan.setAttributes({
|
|
438
459
|
"proxy.model_substituted": true,
|
|
439
460
|
"proxy.original_model": requestedModel,
|
|
440
461
|
"proxy.actual_model": actualModel,
|
|
441
462
|
"gen_ai.response.model": actualModel,
|
|
463
|
+
...(actualProvider ? { "proxy.actual_provider": actualProvider } : {}),
|
|
442
464
|
});
|
|
443
465
|
const m = getProxyMetrics();
|
|
444
466
|
m.modelSubstitutionTotal.add(1, {
|
|
@@ -644,7 +666,7 @@ class ProxyTracer {
|
|
|
644
666
|
this.usage.cacheCreationTokens +
|
|
645
667
|
this.usage.cacheReadTokens +
|
|
646
668
|
(this.usage.reasoningTokens ?? 0);
|
|
647
|
-
const cost = calculateCost(
|
|
669
|
+
const cost = calculateCost(this.billingProvider, this.model, {
|
|
648
670
|
input: this.usage.inputTokens,
|
|
649
671
|
output: this.usage.outputTokens,
|
|
650
672
|
total: totalTokens,
|
|
@@ -683,14 +705,14 @@ class ProxyTracer {
|
|
|
683
705
|
this.usage.cacheReadTokens +
|
|
684
706
|
(this.usage.reasoningTokens ?? 0);
|
|
685
707
|
const durationMs = Date.now() - this.startTime;
|
|
686
|
-
const cost = calculateCost(
|
|
708
|
+
const cost = calculateCost(this.billingProvider, this.model, {
|
|
687
709
|
input: this.usage.inputTokens,
|
|
688
710
|
output: this.usage.outputTokens,
|
|
689
711
|
total: totalTokens,
|
|
690
712
|
cacheCreationTokens: this.usage.cacheCreationTokens,
|
|
691
713
|
cacheReadTokens: this.usage.cacheReadTokens,
|
|
692
714
|
});
|
|
693
|
-
TelemetryService.getInstance().recordAIRequest(
|
|
715
|
+
TelemetryService.getInstance().recordAIRequest(this.billingProvider, this.model, totalTokens, durationMs, cost > 0 ? cost : undefined);
|
|
694
716
|
}
|
|
695
717
|
// -------------------------------------------------------------------------
|
|
696
718
|
// Context propagation
|
|
@@ -249,6 +249,8 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
249
249
|
let succeeded = false;
|
|
250
250
|
let streamInterruptedAfterOutput = false;
|
|
251
251
|
let translatedModel;
|
|
252
|
+
/** Provider that actually served the successful attempt, for costing. */
|
|
253
|
+
let translatedProvider;
|
|
252
254
|
let finalStreamError = "No translation providers succeeded";
|
|
253
255
|
let upstreamIterator;
|
|
254
256
|
let lastAttemptLabel = "translation";
|
|
@@ -328,6 +330,19 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
328
330
|
controller.enqueue(encoder.encode(frame));
|
|
329
331
|
}
|
|
330
332
|
}
|
|
333
|
+
translatedModel = streamResult.model;
|
|
334
|
+
translatedProvider = attempt.provider;
|
|
335
|
+
// Substitution BEFORE usage: setUsage() and recordMetrics() price
|
|
336
|
+
// the request immediately, against the tracer's current model and
|
|
337
|
+
// billing provider. Recording first and substituting afterwards
|
|
338
|
+
// bills the model the client ASKED for — which is exactly what
|
|
339
|
+
// ProxyTracer.setModelSubstitution() documents as wrong, since a
|
|
340
|
+
// claude-* alias served by another provider would be charged at
|
|
341
|
+
// Claude rates. The finally block below still calls it, harmlessly,
|
|
342
|
+
// for paths that never reach here.
|
|
343
|
+
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
344
|
+
tracer.setModelSubstitution(requestModel, translatedModel, translatedProvider);
|
|
345
|
+
}
|
|
331
346
|
// Track usage and metrics
|
|
332
347
|
const resolvedUsageForTracer = extractUsageFromStreamResult(streamResult.usage);
|
|
333
348
|
tracer?.setUsage({
|
|
@@ -337,7 +352,6 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
337
352
|
cacheReadTokens: 0,
|
|
338
353
|
});
|
|
339
354
|
tracer?.recordMetrics();
|
|
340
|
-
translatedModel = streamResult.model;
|
|
341
355
|
succeeded = true;
|
|
342
356
|
return;
|
|
343
357
|
}
|
|
@@ -378,7 +392,7 @@ export async function handleTranslatedStreamRequest(args) {
|
|
|
378
392
|
controller.close();
|
|
379
393
|
}
|
|
380
394
|
if (tracer && translatedModel && translatedModel !== requestModel) {
|
|
381
|
-
tracer.setModelSubstitution(requestModel, translatedModel);
|
|
395
|
+
tracer.setModelSubstitution(requestModel, translatedModel, translatedProvider);
|
|
382
396
|
}
|
|
383
397
|
const terminalStatus = cancelled
|
|
384
398
|
? 499
|
|
@@ -499,6 +513,12 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
499
513
|
: undefined,
|
|
500
514
|
toolCalls: streamResult.toolCalls,
|
|
501
515
|
};
|
|
516
|
+
// Substitution BEFORE usage — see the streaming path for why: pricing
|
|
517
|
+
// happens inside setUsage()/recordMetrics(), so substituting afterwards
|
|
518
|
+
// bills the requested model instead of the one that served.
|
|
519
|
+
if (tracer && streamResult.model && streamResult.model !== requestModel) {
|
|
520
|
+
tracer.setModelSubstitution(requestModel, streamResult.model, attempt.provider);
|
|
521
|
+
}
|
|
502
522
|
// Track usage and metrics
|
|
503
523
|
const resolvedUsage = extractUsageFromStreamResult(streamResult.usage);
|
|
504
524
|
tracer?.setUsage({
|
|
@@ -508,9 +528,6 @@ export async function handleTranslatedJsonRequest(args) {
|
|
|
508
528
|
cacheReadTokens: 0,
|
|
509
529
|
});
|
|
510
530
|
tracer?.recordMetrics();
|
|
511
|
-
if (tracer && streamResult.model && streamResult.model !== requestModel) {
|
|
512
|
-
tracer.setModelSubstitution(requestModel, streamResult.model);
|
|
513
|
-
}
|
|
514
531
|
tracer?.end(200, Date.now() - requestStartTime);
|
|
515
532
|
recordFinalSuccess(lastAttemptLabel, "translation");
|
|
516
533
|
const traceCtx = tracer?.getTraceContext();
|
|
@@ -21,6 +21,7 @@ import { tokenStore } from "../../auth/tokenStore.js";
|
|
|
21
21
|
import { CODEX_ORIGINATOR, CODEX_RESPONSES_URL, CODEX_USER_AGENT, codexTokenNeedsRefresh, isPermanentCodexRefreshFailure, refreshCodexToken, resolveCodexAccountId, } from "../../auth/codexOAuth.js";
|
|
22
22
|
import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from "../../proxy/accountCooldown.js";
|
|
23
23
|
import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
24
|
+
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
24
25
|
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
|
|
25
26
|
import { logRequest } from "../../proxy/requestLogger.js";
|
|
26
27
|
import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
|
|
@@ -340,7 +341,34 @@ async function handleCodexResponsesRequest(ctx) {
|
|
|
340
341
|
connection: "keep-alive",
|
|
341
342
|
...(ctx.responseHeaders ?? {}),
|
|
342
343
|
};
|
|
343
|
-
|
|
344
|
+
// Tap the relay for token usage. The log above is written first and
|
|
345
|
+
// unconditionally so a request is never lost when a client hangs up
|
|
346
|
+
// mid-stream; this emits a second record for the same requestId
|
|
347
|
+
// carrying the counts, which proxyAnalysis merges. If the stream shape
|
|
348
|
+
// is not recognised, usage resolves null and nothing extra is written —
|
|
349
|
+
// i.e. exactly the previous behaviour.
|
|
350
|
+
if (!upstream.body) {
|
|
351
|
+
return new Response(upstream.body, {
|
|
352
|
+
status: upstream.status,
|
|
353
|
+
headers,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const { stream: usageTap, usage: usageSeen } = createCodexUsageTap();
|
|
357
|
+
usageSeen
|
|
358
|
+
.then((usage) => {
|
|
359
|
+
if (!usage) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
return writeLog(account.label, upstream.status, {
|
|
363
|
+
provider: "openai",
|
|
364
|
+
inputTokens: usage.inputTokens,
|
|
365
|
+
outputTokens: usage.outputTokens,
|
|
366
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
367
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
368
|
+
});
|
|
369
|
+
})
|
|
370
|
+
.catch(() => undefined);
|
|
371
|
+
return new Response(upstream.body.pipeThrough(usageTap), {
|
|
344
372
|
status: upstream.status,
|
|
345
373
|
headers,
|
|
346
374
|
});
|
|
@@ -335,6 +335,11 @@ export function createOpenAIProxyRoutes(modelRouter, basePath = "", loopbackPort
|
|
|
335
335
|
toolCount: Object.keys(parsed.tools).length,
|
|
336
336
|
clientApp: "openai-compat",
|
|
337
337
|
userAgent: ctx.headers["user-agent"] ?? "",
|
|
338
|
+
// Without this the tracer defaults to "anthropic" and every
|
|
339
|
+
// non-Anthropic model prices to $0 (the anthropic table has no
|
|
340
|
+
// _default), while a claude-* alias routed elsewhere prices at
|
|
341
|
+
// Claude rates. Both are wrong in opposite directions.
|
|
342
|
+
provider: targetProvider ?? "openai-compatible",
|
|
338
343
|
}, ctx.headers);
|
|
339
344
|
tracer.setMode("full");
|
|
340
345
|
}
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -570,6 +570,12 @@ export type RequestLogEntry = {
|
|
|
570
570
|
outputTokens?: number;
|
|
571
571
|
cacheCreationTokens?: number;
|
|
572
572
|
cacheReadTokens?: number;
|
|
573
|
+
/**
|
|
574
|
+
* Provider that actually served the request, for costing. Absent on records
|
|
575
|
+
* written before this field existed; `proxyAnalysis` then falls back to a
|
|
576
|
+
* cross-provider model lookup rather than assuming Anthropic.
|
|
577
|
+
*/
|
|
578
|
+
provider?: string;
|
|
573
579
|
/** OTel trace ID for correlation with distributed traces */
|
|
574
580
|
traceId?: string;
|
|
575
581
|
/** OTel span ID for correlation with distributed traces */
|
|
@@ -1384,6 +1390,13 @@ export type ProxyRequestContext = {
|
|
|
1384
1390
|
sessionId?: string;
|
|
1385
1391
|
userAgent?: string;
|
|
1386
1392
|
clientApp?: string;
|
|
1393
|
+
/**
|
|
1394
|
+
* Provider that will serve the request, used for costing. Defaults to
|
|
1395
|
+
* "anthropic" when omitted, which is correct for the /v1/messages engine;
|
|
1396
|
+
* the OpenAI-compatible engine must pass whatever ModelRouter resolved, or
|
|
1397
|
+
* every non-Anthropic model prices to $0.
|
|
1398
|
+
*/
|
|
1399
|
+
provider?: string;
|
|
1387
1400
|
};
|
|
1388
1401
|
/** Response-side details parsed from the upstream reply (model, finish, tools). */
|
|
1389
1402
|
export type ResponseInfoContext = {
|
|
@@ -1662,7 +1675,27 @@ export type ProxyAnalysisReport = {
|
|
|
1662
1675
|
cacheReadTokens: number;
|
|
1663
1676
|
cacheCreationTokens: number;
|
|
1664
1677
|
inputTokens: number;
|
|
1678
|
+
outputTokens: number;
|
|
1665
1679
|
requestHitRate: number | null;
|
|
1680
|
+
/**
|
|
1681
|
+
* Summed per-request cost in USD. Records that carry no model, or whose
|
|
1682
|
+
* model matches no pricing table, contribute 0 — so this is a floor, not
|
|
1683
|
+
* an exact bill. `requestsPriced` says how many records actually priced.
|
|
1684
|
+
*/
|
|
1685
|
+
estimatedCostUsd: number;
|
|
1686
|
+
requestsPriced: number;
|
|
1687
|
+
/**
|
|
1688
|
+
* Requests whose cost came from a longest-prefix fallback rather than an
|
|
1689
|
+
* exact pricing row — the rate is inherited from a similarly-named model
|
|
1690
|
+
* and may be wrong. Adding the real row makes these exact.
|
|
1691
|
+
*/
|
|
1692
|
+
requestsPricedByPrefix: number;
|
|
1693
|
+
/** Distinct models priced by prefix fallback, for the operator to chase. */
|
|
1694
|
+
modelsPricedByPrefix: string[];
|
|
1695
|
+
/** Requests carrying usage whose model matched no pricing row at all. */
|
|
1696
|
+
requestsUnpriced: number;
|
|
1697
|
+
/** Distinct models with no pricing row at all. */
|
|
1698
|
+
unpricedModels: string[];
|
|
1666
1699
|
};
|
|
1667
1700
|
routing: {
|
|
1668
1701
|
modes: Record<string, number>;
|
|
@@ -1698,13 +1731,45 @@ export type ProxyAnalysisFinalRequestRecord = {
|
|
|
1698
1731
|
durationMs: number | null;
|
|
1699
1732
|
account: string;
|
|
1700
1733
|
accountType: string;
|
|
1734
|
+
model: string | null;
|
|
1735
|
+
provider: string | null;
|
|
1701
1736
|
inputTokens: number | null;
|
|
1737
|
+
outputTokens: number | null;
|
|
1702
1738
|
cacheReadTokens: number | null;
|
|
1703
1739
|
cacheCreationTokens: number | null;
|
|
1704
1740
|
errorType: string | null;
|
|
1705
1741
|
errorCode: string | null;
|
|
1706
1742
|
routingDecision: ProxyAccountRoutingDecision | null;
|
|
1707
1743
|
};
|
|
1744
|
+
/**
|
|
1745
|
+
* A stream transformer that also handles cancellation.
|
|
1746
|
+
*
|
|
1747
|
+
* The Streams standard gives `Transformer` a `cancel()` callback — invoked when
|
|
1748
|
+
* the stream is aborted rather than closed cleanly — and Node implements it,
|
|
1749
|
+
* but TypeScript's bundled lib does not declare it yet. Without it there is no
|
|
1750
|
+
* way to observe a client hanging up mid-response.
|
|
1751
|
+
*/
|
|
1752
|
+
export type ProxyCancellableTransformer<I, O> = Transformer<I, O> & {
|
|
1753
|
+
cancel?: (reason?: unknown) => void;
|
|
1754
|
+
};
|
|
1755
|
+
/**
|
|
1756
|
+
* Token usage scraped from a Codex (OpenAI Responses) SSE stream.
|
|
1757
|
+
*
|
|
1758
|
+
* Verified against real traffic: captured from a live `codex exec` run through
|
|
1759
|
+
* the proxy on 2026-08-21 (`test/fixtures/codex-response-usage.sse`). The
|
|
1760
|
+
* shape is `response.completed` → `response.usage`, carrying `input_tokens`,
|
|
1761
|
+
* `output_tokens`, and an `input_tokens_details` object with `cached_tokens`
|
|
1762
|
+
* and `cache_write_tokens`. The parser also accepts the common variants. Treat
|
|
1763
|
+
* a null result as "not observed", never as "zero tokens".
|
|
1764
|
+
*/
|
|
1765
|
+
export type CodexStreamUsage = {
|
|
1766
|
+
inputTokens: number;
|
|
1767
|
+
outputTokens: number;
|
|
1768
|
+
cacheReadTokens: number;
|
|
1769
|
+
/** Cache writes, which bill at a premium over both reads and plain input. */
|
|
1770
|
+
cacheCreationTokens: number;
|
|
1771
|
+
reasoningTokens: number;
|
|
1772
|
+
};
|
|
1708
1773
|
/** Validated account-routing evidence joined to a final request log. */
|
|
1709
1774
|
export type ProxyAnalysisRoutingRecord = {
|
|
1710
1775
|
requestId: string;
|
package/dist/utils/pricing.d.ts
CHANGED
|
@@ -14,4 +14,13 @@ export declare function calculateCost(provider: string, model: string, usage: To
|
|
|
14
14
|
* USD price, and any caller gated by `hasPricing()` should treat them as
|
|
15
15
|
* non-billable rather than zero-cost-billable.
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Whether a model's rates came from an exact table entry or were inferred.
|
|
19
|
+
*
|
|
20
|
+
* `findRates` falls back to a longest-prefix match, so an unlisted model can
|
|
21
|
+
* silently inherit a listed one's rates — e.g. "gpt-5.6-sol" matches the
|
|
22
|
+
* "gpt-5" entry and is billed at its price. That is a guess, not a quote, and
|
|
23
|
+
* a caller reporting spend needs to be able to say which it had.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isExactPricingMatch(provider: string, model: string): boolean;
|
|
17
26
|
export declare function hasPricing(provider: string, model: string): boolean;
|
package/dist/utils/pricing.js
CHANGED
|
@@ -11,6 +11,47 @@
|
|
|
11
11
|
const PRICING = {
|
|
12
12
|
// Anthropic (direct API) — updated March 2026
|
|
13
13
|
anthropic: {
|
|
14
|
+
// Claude 5 family. Rates from platform.claude.com/docs/en/about-claude/pricing
|
|
15
|
+
// (checked 2026-08-21). Cache multipliers are the documented ones: a 5-minute
|
|
16
|
+
// cache write is 1.25x base input, a cache hit 0.1x.
|
|
17
|
+
"claude-fable-5": {
|
|
18
|
+
input: 10.0 / 1_000_000,
|
|
19
|
+
output: 50.0 / 1_000_000,
|
|
20
|
+
cacheRead: 1.0 / 1_000_000,
|
|
21
|
+
cacheCreation: 12.5 / 1_000_000,
|
|
22
|
+
},
|
|
23
|
+
"claude-mythos-5": {
|
|
24
|
+
input: 10.0 / 1_000_000,
|
|
25
|
+
output: 50.0 / 1_000_000,
|
|
26
|
+
cacheRead: 1.0 / 1_000_000,
|
|
27
|
+
cacheCreation: 12.5 / 1_000_000,
|
|
28
|
+
},
|
|
29
|
+
"claude-opus-5": {
|
|
30
|
+
input: 5.0 / 1_000_000,
|
|
31
|
+
output: 25.0 / 1_000_000,
|
|
32
|
+
cacheRead: 0.5 / 1_000_000,
|
|
33
|
+
cacheCreation: 6.25 / 1_000_000,
|
|
34
|
+
},
|
|
35
|
+
"claude-opus-4-8": {
|
|
36
|
+
input: 5.0 / 1_000_000,
|
|
37
|
+
output: 25.0 / 1_000_000,
|
|
38
|
+
cacheRead: 0.5 / 1_000_000,
|
|
39
|
+
cacheCreation: 6.25 / 1_000_000,
|
|
40
|
+
},
|
|
41
|
+
"claude-opus-4-7": {
|
|
42
|
+
input: 5.0 / 1_000_000,
|
|
43
|
+
output: 25.0 / 1_000_000,
|
|
44
|
+
cacheRead: 0.5 / 1_000_000,
|
|
45
|
+
cacheCreation: 6.25 / 1_000_000,
|
|
46
|
+
},
|
|
47
|
+
// Sonnet 5's $2/$10 launch pricing became the standard price; the
|
|
48
|
+
// previously scheduled 2026-09-01 rise to $3/$15 was cancelled.
|
|
49
|
+
"claude-sonnet-5": {
|
|
50
|
+
input: 2.0 / 1_000_000,
|
|
51
|
+
output: 10.0 / 1_000_000,
|
|
52
|
+
cacheRead: 0.2 / 1_000_000,
|
|
53
|
+
cacheCreation: 2.5 / 1_000_000,
|
|
54
|
+
},
|
|
14
55
|
// Claude 4.6 family
|
|
15
56
|
"claude-opus-4-6": {
|
|
16
57
|
input: 5.0 / 1_000_000,
|
|
@@ -31,6 +72,17 @@ const PRICING = {
|
|
|
31
72
|
cacheRead: 0.3 / 1_000_000,
|
|
32
73
|
cacheCreation: 3.75 / 1_000_000,
|
|
33
74
|
},
|
|
75
|
+
// Undated aliases for the same models. Clients report the bare name far
|
|
76
|
+
// more often than the dated one, and without these the longest-prefix
|
|
77
|
+
// match lands on the previous generation ("claude-sonnet-4"), which both
|
|
78
|
+
// reports as an inferred rate and would silently drift if the two
|
|
79
|
+
// generations ever diverge in price.
|
|
80
|
+
"claude-sonnet-4-5": {
|
|
81
|
+
input: 3.0 / 1_000_000,
|
|
82
|
+
output: 15.0 / 1_000_000,
|
|
83
|
+
cacheRead: 0.3 / 1_000_000,
|
|
84
|
+
cacheCreation: 3.75 / 1_000_000,
|
|
85
|
+
},
|
|
34
86
|
"claude-opus-4-5": {
|
|
35
87
|
input: 5.0 / 1_000_000,
|
|
36
88
|
output: 25.0 / 1_000_000,
|
|
@@ -43,6 +95,12 @@ const PRICING = {
|
|
|
43
95
|
cacheRead: 0.1 / 1_000_000,
|
|
44
96
|
cacheCreation: 1.25 / 1_000_000,
|
|
45
97
|
},
|
|
98
|
+
"claude-haiku-4-5": {
|
|
99
|
+
input: 1.0 / 1_000_000,
|
|
100
|
+
output: 5.0 / 1_000_000,
|
|
101
|
+
cacheRead: 0.1 / 1_000_000,
|
|
102
|
+
cacheCreation: 1.25 / 1_000_000,
|
|
103
|
+
},
|
|
46
104
|
// Claude 4.0/4.1 family
|
|
47
105
|
"claude-opus-4-1": {
|
|
48
106
|
input: 15.0 / 1_000_000,
|
|
@@ -147,6 +205,35 @@ const PRICING = {
|
|
|
147
205
|
},
|
|
148
206
|
// OpenAI — updated March 2026
|
|
149
207
|
openai: {
|
|
208
|
+
// GPT-5.6 family (Sol/Terra/Luna). Rates reflect OpenAI's 2026-07-30 cut,
|
|
209
|
+
// which reduced Luna by 80% and Terra by 20%; Sol was unchanged. Many
|
|
210
|
+
// third-party tables still carry the pre-cut numbers.
|
|
211
|
+
// List rates. Note that some resellers advertise sol at 50% off
|
|
212
|
+
// ($2.50/$15.00/$0.25); those are promotional and expire, so the table
|
|
213
|
+
// carries list price and cache read stays the documented 0.1x of input.
|
|
214
|
+
"gpt-5.6-sol": {
|
|
215
|
+
input: 5.0 / 1_000_000,
|
|
216
|
+
output: 30.0 / 1_000_000,
|
|
217
|
+
cacheRead: 0.5 / 1_000_000,
|
|
218
|
+
cacheCreation: 6.25 / 1_000_000,
|
|
219
|
+
},
|
|
220
|
+
"gpt-5.6-terra": {
|
|
221
|
+
input: 2.0 / 1_000_000,
|
|
222
|
+
output: 12.0 / 1_000_000,
|
|
223
|
+
cacheRead: 0.2 / 1_000_000,
|
|
224
|
+
cacheCreation: 2.5 / 1_000_000,
|
|
225
|
+
},
|
|
226
|
+
"gpt-5.6-luna": {
|
|
227
|
+
input: 0.2 / 1_000_000,
|
|
228
|
+
output: 1.2 / 1_000_000,
|
|
229
|
+
cacheRead: 0.02 / 1_000_000,
|
|
230
|
+
cacheCreation: 0.25 / 1_000_000,
|
|
231
|
+
},
|
|
232
|
+
"gpt-5.5": {
|
|
233
|
+
input: 5.0 / 1_000_000,
|
|
234
|
+
output: 30.0 / 1_000_000,
|
|
235
|
+
cacheRead: 0.5 / 1_000_000,
|
|
236
|
+
},
|
|
150
237
|
// GPT-5.x family
|
|
151
238
|
// cacheRead = 0.25x input (cached input tokens; no separate cacheCreation).
|
|
152
239
|
"gpt-5.4": {
|
|
@@ -625,7 +712,30 @@ const PROVIDER_ALIASES = {
|
|
|
625
712
|
*
|
|
626
713
|
* @returns The rate entry, or undefined when the combination is unknown.
|
|
627
714
|
*/
|
|
628
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Whether the tail left over after a longest-prefix match is only a version or
|
|
717
|
+
* date stamp — e.g. "claude-sonnet-4-5-20250929-v1:0" against the table key
|
|
718
|
+
* "claude-sonnet-4-5". That is the *same* model carrying a release suffix, so
|
|
719
|
+
* its rate is quoted, not inferred.
|
|
720
|
+
*
|
|
721
|
+
* Deliberately narrow: "gpt-5.6-sol" against "gpt-5" leaves ".6-sol", which is
|
|
722
|
+
* a different model generation and stays flagged as inferred.
|
|
723
|
+
*/
|
|
724
|
+
const VERSION_SUFFIX_RE = /^[-@](\d{8}|v\d+(:\d+)?|latest)([-@:](\d{8}|v\d+(:\d+)?))*$/;
|
|
725
|
+
function isVersionOnlySuffix(model, key) {
|
|
726
|
+
if (!model.startsWith(key) || model === key) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
return VERSION_SUFFIX_RE.test(model.slice(key.length));
|
|
730
|
+
}
|
|
731
|
+
function findRates(provider, model,
|
|
732
|
+
/**
|
|
733
|
+
* Set to true when the rates came from a literal table key rather than a
|
|
734
|
+
* prefix/fallback match. Threaded as an out-param so there is exactly one
|
|
735
|
+
* lookup implementation — a second hand-written copy drifted from this one
|
|
736
|
+
* and mislabelled every Bedrock and Vertex-Gemini hit.
|
|
737
|
+
*/
|
|
738
|
+
matchKind) {
|
|
629
739
|
const stripped = provider.toLowerCase().replace(/[^a-z]/g, "");
|
|
630
740
|
const normalizedProvider = PROVIDER_ALIASES[stripped] ?? stripped;
|
|
631
741
|
// Proxy providers (LiteLLM, OpenRouter): search all known providers for a model match
|
|
@@ -633,6 +743,9 @@ function findRates(provider, model) {
|
|
|
633
743
|
for (const providerPricing of Object.values(PRICING)) {
|
|
634
744
|
// Exact match
|
|
635
745
|
if (providerPricing[model]) {
|
|
746
|
+
if (matchKind) {
|
|
747
|
+
matchKind.exact = true;
|
|
748
|
+
}
|
|
636
749
|
return providerPricing[model];
|
|
637
750
|
}
|
|
638
751
|
const sortedKeys = Object.keys(providerPricing).sort((a, b) => b.length - a.length);
|
|
@@ -667,6 +780,9 @@ function findRates(provider, model) {
|
|
|
667
780
|
: model;
|
|
668
781
|
// Exact match
|
|
669
782
|
if (providerPricing[modelKey]) {
|
|
783
|
+
if (matchKind) {
|
|
784
|
+
matchKind.exact = true;
|
|
785
|
+
}
|
|
670
786
|
return providerPricing[modelKey];
|
|
671
787
|
}
|
|
672
788
|
// Longest-prefix match (skip the synthetic "_default" sentinel below)
|
|
@@ -675,6 +791,9 @@ function findRates(provider, model) {
|
|
|
675
791
|
.sort((a, b) => b.length - a.length);
|
|
676
792
|
const key = sortedKeys.find((k) => modelKey.startsWith(k));
|
|
677
793
|
if (key) {
|
|
794
|
+
if (matchKind && isVersionOnlySuffix(modelKey, key)) {
|
|
795
|
+
matchKind.exact = true;
|
|
796
|
+
}
|
|
678
797
|
return providerPricing[key];
|
|
679
798
|
}
|
|
680
799
|
// Fallback: Vertex hosts both Claude and Gemini models.
|
|
@@ -686,6 +805,9 @@ function findRates(provider, model) {
|
|
|
686
805
|
const googlePricing = PRICING["google"];
|
|
687
806
|
if (googlePricing) {
|
|
688
807
|
if (googlePricing[model]) {
|
|
808
|
+
if (matchKind) {
|
|
809
|
+
matchKind.exact = true;
|
|
810
|
+
}
|
|
689
811
|
return googlePricing[model];
|
|
690
812
|
}
|
|
691
813
|
const googleKeys = Object.keys(googlePricing).sort((a, b) => b.length - a.length);
|
|
@@ -742,6 +864,19 @@ export function calculateCost(provider, model, usage) {
|
|
|
742
864
|
* USD price, and any caller gated by `hasPricing()` should treat them as
|
|
743
865
|
* non-billable rather than zero-cost-billable.
|
|
744
866
|
*/
|
|
867
|
+
/**
|
|
868
|
+
* Whether a model's rates came from an exact table entry or were inferred.
|
|
869
|
+
*
|
|
870
|
+
* `findRates` falls back to a longest-prefix match, so an unlisted model can
|
|
871
|
+
* silently inherit a listed one's rates — e.g. "gpt-5.6-sol" matches the
|
|
872
|
+
* "gpt-5" entry and is billed at its price. That is a guess, not a quote, and
|
|
873
|
+
* a caller reporting spend needs to be able to say which it had.
|
|
874
|
+
*/
|
|
875
|
+
export function isExactPricingMatch(provider, model) {
|
|
876
|
+
const matchKind = { exact: false };
|
|
877
|
+
const rates = findRates(provider, model, matchKind);
|
|
878
|
+
return rates !== undefined && matchKind.exact;
|
|
879
|
+
}
|
|
745
880
|
export function hasPricing(provider, model) {
|
|
746
881
|
const rates = findRates(provider, model);
|
|
747
882
|
if (!rates) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.13.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"test:middleware": "npx tsx test/continuous-test-suite-middleware.ts",
|
|
92
92
|
"test:observability": "npx tsx test/continuous-test-suite-observability.ts",
|
|
93
93
|
"test:ppt": "npx tsx test/continuous-test-suite-ppt.ts",
|
|
94
|
+
"test:vertex-loop-characterization": "tsx test/continuous-test-suite-vertex-loop-characterization.ts",
|
|
94
95
|
"test:providers": "npx tsx test/continuous-test-suite-providers.ts",
|
|
95
96
|
"test:new-providers": "npx tsx test/continuous-test-suite-new-providers.ts",
|
|
96
97
|
"test:matrix": "npx tsx test/continuous-test-suite-provider-matrix.ts",
|