@juspay/neurolink 11.12.0 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/dist/browser/neurolink.min.js +396 -396
  3. package/dist/cli/commands/proxy.js +42 -0
  4. package/dist/cli/commands/proxyAnalyze.js +10 -1
  5. package/dist/cli/proxy-clients/claudeCode.js +42 -10
  6. package/dist/cli/proxy-clients/openCode.js +37 -15
  7. package/dist/cli/proxy-clients/qwenCode.js +33 -9
  8. package/dist/cli/proxy-clients/registry.js +10 -2
  9. package/dist/cli/proxy-clients/snapshot.d.ts +52 -0
  10. package/dist/cli/proxy-clients/snapshot.js +98 -0
  11. package/dist/lib/providers/googleAiStudio/client.js +6 -3
  12. package/dist/lib/providers/googleVertex/client.js +6 -3
  13. package/dist/lib/proxy/codexUsage.d.ts +68 -0
  14. package/dist/lib/proxy/codexUsage.js +247 -0
  15. package/dist/lib/proxy/proxyAnalysis.js +87 -3
  16. package/dist/lib/proxy/proxyFetch.d.ts +1 -0
  17. package/dist/lib/proxy/proxyFetch.js +29 -0
  18. package/dist/lib/proxy/proxyTracer.d.ts +13 -2
  19. package/dist/lib/proxy/proxyTracer.js +29 -7
  20. package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
  21. package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
  22. package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
  23. package/dist/lib/types/proxy.d.ts +65 -0
  24. package/dist/lib/utils/pricing.d.ts +9 -0
  25. package/dist/lib/utils/pricing.js +136 -1
  26. package/dist/providers/googleAiStudio/client.js +6 -3
  27. package/dist/providers/googleVertex/client.js +6 -3
  28. package/dist/proxy/codexUsage.d.ts +68 -0
  29. package/dist/proxy/codexUsage.js +246 -0
  30. package/dist/proxy/proxyAnalysis.js +87 -3
  31. package/dist/proxy/proxyFetch.d.ts +1 -0
  32. package/dist/proxy/proxyFetch.js +29 -0
  33. package/dist/proxy/proxyTracer.d.ts +13 -2
  34. package/dist/proxy/proxyTracer.js +29 -7
  35. package/dist/proxy/proxyTranslationEngine.js +22 -5
  36. package/dist/server/routes/codexProxyRoutes.js +29 -1
  37. package/dist/server/routes/openaiProxyRoutes.js +5 -0
  38. package/dist/types/proxy.d.ts +65 -0
  39. package/dist/utils/pricing.d.ts +9 -0
  40. package/dist/utils/pricing.js +136 -1
  41. package/package.json +1 -1
@@ -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
- return new Response(upstream.body, {
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
  }
@@ -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;
@@ -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;
@@ -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
- function findRates(provider, model) {
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) {
@@ -20,7 +20,7 @@ import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js
20
20
  import { buildGeminiResponseSchema, buildNativeConfig, computeMaxSteps, createContextGuard, buildUserPartsWithMultimodal, extractThoughtSignature, handleMaxStepsTermination, prependConversationMessages, } from "../googleNativeGemini3/index.js";
21
21
  import { createStreamChannel } from "../../core/streamChannel.js";
22
22
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
23
- import { createProxyFetch } from "../../proxy/proxyFetch.js";
23
+ import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
24
24
  // Google AI Live API types now imported from ../types/providerSpecific.js
25
25
  // Import proper types for multimodal message handling
26
26
  // Create Google GenAI client
@@ -38,16 +38,19 @@ async function createGoogleGenAIClient(apiKey, baseURL) {
38
38
  });
39
39
  }
40
40
  const Ctor = ctor;
41
- // Include httpOptions with proxy fetch for corporate network support.
41
+ // httpOptions carries the endpoint override and nothing else. It used to
42
+ // also pass a proxy fetch, which the SDK silently ignored — see
43
+ // warnGoogleSdkIgnoresProxy for why that is not fixable here.
44
+ //
42
45
  // baseUrl is only included when resolved — verified against
43
46
  // @google/genai's ApiClient (dist/node/index.cjs) that it falls back to
44
47
  // its own default whenever httpOptions.baseUrl is undefined, so omitting
45
48
  // the key and passing `baseUrl: undefined` behave identically; the key is
46
49
  // still omitted outright for a cleaner outbound config object.
50
+ warnGoogleSdkIgnoresProxy("GoogleAIStudio");
47
51
  return new Ctor({
48
52
  apiKey,
49
53
  httpOptions: {
50
- fetch: createProxyFetch(),
51
54
  ...(baseURL ? { baseUrl: baseURL } : {}),
52
55
  },
53
56
  });
@@ -11,7 +11,7 @@ import { DEFAULT_GEMINI_STREAM_TIMEOUT_MS, DEFAULT_MAX_STEPS, DEFAULT_TOOL_EXECU
11
11
  import { ModelConfigurationManager } from "../../core/modelConfiguration.js";
12
12
  import { isSchemaComplexityError } from "../../core/modules/structuredOutputPolicy.js";
13
13
  import { redactUrlForError, stringifyContentSafe, } from "../../utils/logSanitize.js";
14
- import { createProxyFetch } from "../../proxy/proxyFetch.js";
14
+ import { warnGoogleSdkIgnoresProxy } from "../../proxy/proxyFetch.js";
15
15
  import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../../types/index.js";
16
16
  import { classifyProviderError } from "../../utils/errorClassifier.js";
17
17
  import { ERROR_CODES, NeuroLinkError } from "../../utils/errorHandling.js";
@@ -843,6 +843,7 @@ export class GoogleVertexProvider extends BaseProvider {
843
843
  * Create @google/genai client configured for Vertex AI
844
844
  */
845
845
  async createVertexGenAIClient(regionOverride) {
846
+ warnGoogleSdkIgnoresProxy("GoogleVertex");
846
847
  const expressApiKey = this.resolveExpressApiKey();
847
848
  // Resolved only on the ADC path: getVertexProjectId() throws when no
848
849
  // project is configured, which an Express request legitimately has none of.
@@ -863,8 +864,10 @@ export class GoogleVertexProvider extends BaseProvider {
863
864
  const Ctor = ctor;
864
865
  const baseUrl = this.resolveBaseURL();
865
866
  const httpOptions = {
866
- // Proxy fetch for corporate network support.
867
- fetch: createProxyFetch(),
867
+ // The endpoint override and nothing else. This object used to also pass
868
+ // a proxy fetch, which the SDK silently ignored — see
869
+ // warnGoogleSdkIgnoresProxy for why that is not fixable here.
870
+ //
868
871
  // Only set when resolved: the SDK falls back to its own default
869
872
  // whenever httpOptions.baseUrl is undefined, so omitting the key and
870
873
  // passing undefined behave identically.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Codex (OpenAI Responses) SSE usage tap.
3
+ *
4
+ * The Codex proxy engine relays `upstream.body` to the client untouched and
5
+ * logs before a single byte is read, so no Codex request has ever carried token
6
+ * counts. This module adds a pass-through tap that scrapes `usage` out of the
7
+ * stream without holding back or altering any bytes.
8
+ *
9
+ * ## Safety contract
10
+ *
11
+ * This sits in the hot path of a live proxy, so it is built to be incapable of
12
+ * breaking a stream:
13
+ *
14
+ * - every chunk is enqueued **before** it is inspected;
15
+ * - all parsing runs inside try/catch, and a throw is swallowed;
16
+ * - a stream whose shape is unrecognised resolves `usage` to `null`, which is
17
+ * exactly today's behaviour (a log with no token fields).
18
+ *
19
+ * The worst case is therefore "no tokens recorded", never a truncated or
20
+ * corrupted response.
21
+ *
22
+ * ## Wire shape
23
+ *
24
+ * **Verified against real traffic.** Captured from a live `codex exec` run
25
+ * through the proxy on 2026-08-21; the trimmed sample is at
26
+ * `test/fixtures/codex-response-usage.sse` and is asserted against in the
27
+ * codex suite. The real shape is
28
+ *
29
+ * event: response.completed
30
+ * data: {"type":"response.completed","response":{"usage":{
31
+ * "input_tokens":N,"output_tokens":M,
32
+ * "input_tokens_details":{"cached_tokens":K,"cache_write_tokens":W},
33
+ * "output_tokens_details":{"reasoning_tokens":R}}}}
34
+ *
35
+ * Note that `response.created` arrives first carrying `usage: null`, which is
36
+ * why the scanner keeps the last non-null result rather than the first.
37
+ *
38
+ * It also accepts a `usage` object at the top level of any event and the
39
+ * `prompt_tokens`/`completion_tokens` spellings. A `null` result means "not
40
+ * observed", never "zero tokens".
41
+ */
42
+ import type { CodexStreamUsage } from "../types/index.js";
43
+ /**
44
+ * Pull usage out of one parsed SSE `data:` payload.
45
+ *
46
+ * Returns null when the payload carries no recognisable usage object, so the
47
+ * caller can keep the last non-null result rather than overwriting it with a
48
+ * later event that happens not to carry usage.
49
+ */
50
+ export declare function extractCodexUsage(payload: unknown): CodexStreamUsage | null;
51
+ /**
52
+ * Scan a slice of SSE text for usage, returning the last one found.
53
+ *
54
+ * Exported for tests: it is the whole parsing decision, and driving it through
55
+ * a real Codex stream would need a live ChatGPT subscription.
56
+ */
57
+ export declare function scanCodexSSEForUsage(text: string): CodexStreamUsage | null;
58
+ /**
59
+ * A pass-through TransformStream that reports the usage seen on a Codex SSE
60
+ * stream.
61
+ *
62
+ * `usage` resolves when the stream ends: to the last usage observed, or null if
63
+ * none was. It never rejects.
64
+ */
65
+ export declare function createCodexUsageTap(): {
66
+ stream: TransformStream<Uint8Array, Uint8Array>;
67
+ usage: Promise<CodexStreamUsage | null>;
68
+ };