@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.
Files changed (43) 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.d.ts +33 -0
  13. package/dist/lib/providers/googleVertex/client.js +110 -11
  14. package/dist/lib/proxy/codexUsage.d.ts +68 -0
  15. package/dist/lib/proxy/codexUsage.js +247 -0
  16. package/dist/lib/proxy/proxyAnalysis.js +87 -3
  17. package/dist/lib/proxy/proxyFetch.d.ts +1 -0
  18. package/dist/lib/proxy/proxyFetch.js +29 -0
  19. package/dist/lib/proxy/proxyTracer.d.ts +13 -2
  20. package/dist/lib/proxy/proxyTracer.js +29 -7
  21. package/dist/lib/proxy/proxyTranslationEngine.js +22 -5
  22. package/dist/lib/server/routes/codexProxyRoutes.js +29 -1
  23. package/dist/lib/server/routes/openaiProxyRoutes.js +5 -0
  24. package/dist/lib/types/proxy.d.ts +65 -0
  25. package/dist/lib/utils/pricing.d.ts +9 -0
  26. package/dist/lib/utils/pricing.js +136 -1
  27. package/dist/providers/googleAiStudio/client.js +6 -3
  28. package/dist/providers/googleVertex/client.d.ts +33 -0
  29. package/dist/providers/googleVertex/client.js +110 -11
  30. package/dist/proxy/codexUsage.d.ts +68 -0
  31. package/dist/proxy/codexUsage.js +246 -0
  32. package/dist/proxy/proxyAnalysis.js +87 -3
  33. package/dist/proxy/proxyFetch.d.ts +1 -0
  34. package/dist/proxy/proxyFetch.js +29 -0
  35. package/dist/proxy/proxyTracer.d.ts +13 -2
  36. package/dist/proxy/proxyTracer.js +29 -7
  37. package/dist/proxy/proxyTranslationEngine.js +22 -5
  38. package/dist/server/routes/codexProxyRoutes.js +29 -1
  39. package/dist/server/routes/openaiProxyRoutes.js +5 -0
  40. package/dist/types/proxy.d.ts +65 -0
  41. package/dist/utils/pricing.d.ts +9 -0
  42. package/dist/utils/pricing.js +136 -1
  43. package/package.json +2 -1
@@ -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";
@@ -578,6 +578,27 @@ function reclaimVertexAnthropicContext(messages, modelName, observedPromptTokens
578
578
  export class GoogleVertexProvider extends BaseProvider {
579
579
  projectId;
580
580
  location;
581
+ /**
582
+ * Vertex AI Express Mode credentials.
583
+ *
584
+ * Vertex supports two authentication modes. The long-standing one pairs a
585
+ * project and location with Application Default Credentials, which makes
586
+ * the SDK mint an OAuth token through google-auth-library before every
587
+ * request. Express Mode instead authenticates with an API key alone.
588
+ *
589
+ * Express is used only when an apiKey is supplied WITHOUT an explicit
590
+ * project or location, so existing ADC callers — including those already
591
+ * passing an apiKey alongside a project — keep exactly the behaviour they
592
+ * have today.
593
+ */
594
+ expressApiKey;
595
+ /**
596
+ * Optional endpoint override, mirroring AI Studio's
597
+ * `credentials.googleAiStudio.baseURL`: per-request credential first, then
598
+ * the environment, then unset so the SDK applies its own default. Blank
599
+ * values count as unset so an empty override cannot clobber that default.
600
+ */
601
+ baseURL;
581
602
  registeredTools = new Map();
582
603
  toolContext = {};
583
604
  // Memory-managed cache for model configuration lookups to avoid repeated calls
@@ -601,14 +622,33 @@ export class GoogleVertexProvider extends BaseProvider {
601
622
  }
602
623
  if (credentials.apiKey) {
603
624
  process.env.GOOGLE_API_KEY = String(credentials.apiKey);
625
+ // Express Mode only when the caller gave a key and NOTHING else to
626
+ // authenticate with. An apiKey passed next to a project is the
627
+ // pre-existing combination and must keep resolving through ADC.
628
+ if (!credentials.projectId && !credentials.location) {
629
+ this.expressApiKey = String(credentials.apiKey);
630
+ }
631
+ }
632
+ if (credentials.baseURL) {
633
+ this.baseURL = String(credentials.baseURL);
604
634
  }
605
635
  }
606
- // Validate Google Cloud credentials - now using consolidated utility
607
- if (!hasGoogleCredentials()) {
608
- validateApiKey(createGoogleAuthConfig());
636
+ // Express Mode authenticates with the key alone, so neither the ADC
637
+ // credential check nor project resolution applies. Both THROW when
638
+ // nothing is configured, which would fail an apiKey-only request before
639
+ // the Express client is ever built — and would do so only on machines
640
+ // without an ambient project, which is exactly where Express is the point.
641
+ const usingExpress = Boolean(this.resolveExpressApiKey());
642
+ if (!usingExpress) {
643
+ // Validate Google Cloud credentials - now using consolidated utility
644
+ if (!hasGoogleCredentials()) {
645
+ validateApiKey(createGoogleAuthConfig());
646
+ }
609
647
  }
610
648
  // Initialize Google Cloud configuration
611
- this.projectId = credentials?.projectId || getVertexProjectId();
649
+ this.projectId =
650
+ credentials?.projectId ||
651
+ (usingExpress ? "" : getVertexProjectId());
612
652
  this.location =
613
653
  region || credentials?.location || getVertexLocation();
614
654
  logger.debug("[GoogleVertexProvider] Constructor initialized", {
@@ -803,7 +843,11 @@ export class GoogleVertexProvider extends BaseProvider {
803
843
  * Create @google/genai client configured for Vertex AI
804
844
  */
805
845
  async createVertexGenAIClient(regionOverride) {
806
- const project = getVertexProjectId();
846
+ warnGoogleSdkIgnoresProxy("GoogleVertex");
847
+ const expressApiKey = this.resolveExpressApiKey();
848
+ // Resolved only on the ADC path: getVertexProjectId() throws when no
849
+ // project is configured, which an Express request legitimately has none of.
850
+ const project = expressApiKey ? "" : getVertexProjectId();
807
851
  const location = regionOverride || this.location || getVertexLocation();
808
852
  const mod = await import("@google/genai");
809
853
  const ctor = mod.GoogleGenAI;
@@ -818,17 +862,72 @@ export class GoogleVertexProvider extends BaseProvider {
818
862
  });
819
863
  }
820
864
  const Ctor = ctor;
821
- // Use vertexai mode with project and location
822
- // Include httpOptions with proxy fetch for corporate network support
865
+ const baseUrl = this.resolveBaseURL();
866
+ const httpOptions = {
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
+ //
871
+ // Only set when resolved: the SDK falls back to its own default
872
+ // whenever httpOptions.baseUrl is undefined, so omitting the key and
873
+ // passing undefined behave identically.
874
+ ...(baseUrl ? { baseUrl } : {}),
875
+ };
876
+ if (expressApiKey) {
877
+ // Express Mode: an API key replaces project/location entirely. Passing
878
+ // them alongside the key would defeat it — the SDK prefers
879
+ // project/location and falls back to ADC, which is the very thing
880
+ // Express exists to avoid.
881
+ return new Ctor({
882
+ vertexai: true,
883
+ apiKey: expressApiKey,
884
+ httpOptions,
885
+ });
886
+ }
887
+ // Project/location mode, authenticating through ADC.
823
888
  return new Ctor({
824
889
  vertexai: true,
825
890
  project,
826
891
  location,
827
- httpOptions: {
828
- fetch: createProxyFetch(),
829
- },
892
+ httpOptions,
830
893
  });
831
894
  }
895
+ /** Endpoint override: credential, then environment, then unset. */
896
+ resolveBaseURL() {
897
+ const resolved = this.baseURL?.trim() || process.env.GOOGLE_VERTEX_BASE_URL?.trim();
898
+ return resolved && resolved.length > 0 ? resolved : undefined;
899
+ }
900
+ /**
901
+ * Express Mode key, if this provider should use it.
902
+ *
903
+ * Deliberately NOT read from GOOGLE_API_KEY: that variable is already set
904
+ * by callers who also configure a project, and treating it as an Express
905
+ * opt-in would silently switch their authentication mode. Express is opted
906
+ * into per request, or through GOOGLE_VERTEX_API_KEY which exists only for
907
+ * this purpose.
908
+ */
909
+ resolveExpressApiKey() {
910
+ // A per-request key already carries its own guard: it is only recorded
911
+ // when the caller supplied no project and no location.
912
+ const fromCredentials = this.expressApiKey?.trim();
913
+ if (fromCredentials) {
914
+ return fromCredentials;
915
+ }
916
+ const fromEnv = process.env.GOOGLE_VERTEX_API_KEY?.trim();
917
+ if (!fromEnv) {
918
+ return undefined;
919
+ }
920
+ // The environment key gets the same guard. A process that configures a
921
+ // project has an ADC setup, and letting an ambient key silently switch it
922
+ // to Express would change how that process authenticates.
923
+ const projectConfigured = [
924
+ "GOOGLE_CLOUD_PROJECT_ID",
925
+ "VERTEX_PROJECT_ID",
926
+ "GOOGLE_VERTEX_PROJECT",
927
+ "GOOGLE_CLOUD_PROJECT",
928
+ ].some((name) => (process.env[name] ?? "").trim().length > 0);
929
+ return projectConfigured ? undefined : fromEnv;
930
+ }
832
931
  /**
833
932
  * Convert one AI-SDK tool into a Vertex Gemini function declaration.
834
933
  * Single source for the pre-loop snapshot AND the mid-turn discovery
@@ -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
+ };
@@ -0,0 +1,247 @@
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 { appendFileSync } from "node:fs";
43
+ const nonNegativeInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
44
+ ? Math.floor(value)
45
+ : 0;
46
+ /**
47
+ * Pull usage out of one parsed SSE `data:` payload.
48
+ *
49
+ * Returns null when the payload carries no recognisable usage object, so the
50
+ * caller can keep the last non-null result rather than overwriting it with a
51
+ * later event that happens not to carry usage.
52
+ */
53
+ export function extractCodexUsage(payload) {
54
+ if (payload === null || typeof payload !== "object") {
55
+ return null;
56
+ }
57
+ const root = payload;
58
+ const response = root.response;
59
+ const usage = (response && typeof response === "object" && response.usage
60
+ ? response.usage
61
+ : root.usage);
62
+ if (!usage || typeof usage !== "object") {
63
+ return null;
64
+ }
65
+ const input = usage.input_tokens ?? usage.prompt_tokens;
66
+ const output = usage.output_tokens ?? usage.completion_tokens;
67
+ if (input === undefined && output === undefined) {
68
+ return null;
69
+ }
70
+ const inputDetails = usage.input_tokens_details;
71
+ const outputDetails = usage.output_tokens_details;
72
+ return {
73
+ inputTokens: nonNegativeInt(input),
74
+ outputTokens: nonNegativeInt(output),
75
+ cacheReadTokens: nonNegativeInt(inputDetails?.cached_tokens),
76
+ cacheCreationTokens: nonNegativeInt(inputDetails?.cache_write_tokens),
77
+ reasoningTokens: nonNegativeInt(outputDetails?.reasoning_tokens),
78
+ };
79
+ }
80
+ /**
81
+ * Scan a slice of SSE text for usage, returning the last one found.
82
+ *
83
+ * Exported for tests: it is the whole parsing decision, and driving it through
84
+ * a real Codex stream would need a live ChatGPT subscription.
85
+ */
86
+ export function scanCodexSSEForUsage(text) {
87
+ let found = null;
88
+ for (const line of text.split("\n")) {
89
+ if (!line.startsWith("data:")) {
90
+ continue;
91
+ }
92
+ const raw = line.slice(5).trim();
93
+ if (!raw || raw === "[DONE]") {
94
+ continue;
95
+ }
96
+ try {
97
+ const usage = extractCodexUsage(JSON.parse(raw));
98
+ if (usage) {
99
+ found = usage;
100
+ }
101
+ }
102
+ catch {
103
+ // Partial or non-JSON payload — the next chunk may complete it. Never
104
+ // let a malformed line escape into the relay.
105
+ }
106
+ }
107
+ return found;
108
+ }
109
+ /**
110
+ * Maximum bytes written by the opt-in raw capture. One `response.completed`
111
+ * event is a few hundred bytes; 256 KiB is generous and bounds a runaway file.
112
+ */
113
+ const CAPTURE_LIMIT_BYTES = 256 * 1024;
114
+ /**
115
+ * Opt-in raw capture of one Codex SSE stream, for confirming the `usage` wire
116
+ * shape against real traffic.
117
+ *
118
+ * Off unless `NEUROLINK_PROXY_CODEX_CAPTURE` names a file. It is deliberately
119
+ * env-gated and undocumented in the CLI: the captured bytes are the assistant's
120
+ * actual response, so this is a debugging tool the operator turns on
121
+ * deliberately, not something that runs by default. Capture stops at the first
122
+ * completed stream and is capped.
123
+ */
124
+ function createCaptureSink() {
125
+ const target = process.env.NEUROLINK_PROXY_CODEX_CAPTURE;
126
+ if (!target) {
127
+ return null;
128
+ }
129
+ let written = 0;
130
+ let started = false;
131
+ return (chunk) => {
132
+ if (written >= CAPTURE_LIMIT_BYTES) {
133
+ return;
134
+ }
135
+ try {
136
+ // Append only the new bytes. Rewriting the accumulated buffer on every
137
+ // chunk is quadratic in stream length and runs in a live relay's
138
+ // transform(), so a long response would do hundreds of growing
139
+ // synchronous writes.
140
+ // Slice to the remaining capacity rather than writing the whole chunk.
141
+ // The guard above only says the cap was not ALREADY reached, so a single
142
+ // large chunk arriving at 255 KiB would otherwise land in full and the
143
+ // file would end up far past its bound — the cap has to hold per write,
144
+ // not per stream.
145
+ const remaining = CAPTURE_LIMIT_BYTES - written;
146
+ const slice = chunk.byteLength > remaining ? chunk.subarray(0, remaining) : chunk;
147
+ appendFileSync(target, slice, { flag: started ? "a" : "w" });
148
+ started = true;
149
+ written += slice.byteLength;
150
+ }
151
+ catch {
152
+ // Capture is best-effort telemetry; never let it touch the relay.
153
+ }
154
+ };
155
+ }
156
+ /**
157
+ * A pass-through TransformStream that reports the usage seen on a Codex SSE
158
+ * stream.
159
+ *
160
+ * `usage` resolves when the stream ends: to the last usage observed, or null if
161
+ * none was. It never rejects.
162
+ */
163
+ export function createCodexUsageTap() {
164
+ let settleUsage = () => { };
165
+ const usage = new Promise((resolve) => {
166
+ settleUsage = resolve;
167
+ });
168
+ // flush() and cancel() are mutually exclusive in principle, but a
169
+ // double-settle must be harmless rather than relied upon.
170
+ let settled = false;
171
+ const settle = (value) => {
172
+ if (settled) {
173
+ return;
174
+ }
175
+ settled = true;
176
+ settleUsage(value);
177
+ };
178
+ const decoder = new TextDecoder();
179
+ const capture = createCaptureSink();
180
+ let carry = "";
181
+ let latest = null;
182
+ /**
183
+ * Ceiling on the unterminated tail we are willing to hold.
184
+ *
185
+ * `carry` normally holds a fraction of one SSE line, because every newline
186
+ * flushes it. A stream that never sends one — a hung upstream, a
187
+ * non-SSE body relayed by mistake — would otherwise grow it without bound
188
+ * for the life of the request. One `response.completed` event is a few
189
+ * hundred bytes, so a megabyte is far past any real event, and dropping the
190
+ * tail costs at most the usage reading this tap is allowed to miss anyway.
191
+ */
192
+ const CARRY_LIMIT_CHARS = 1024 * 1024;
193
+ const transformer = {
194
+ transform(chunk, controller) {
195
+ // Bytes go out first and unconditionally: nothing below can delay or
196
+ // alter what the client receives.
197
+ controller.enqueue(chunk);
198
+ try {
199
+ capture?.(chunk);
200
+ carry += decoder.decode(chunk, { stream: true });
201
+ // Keep only the trailing partial line; events are newline-delimited.
202
+ const lastBreak = carry.lastIndexOf("\n");
203
+ if (lastBreak === -1) {
204
+ if (carry.length > CARRY_LIMIT_CHARS) {
205
+ // No line break in a megabyte: this is not the SSE stream we can
206
+ // read. Give up on the tail rather than grow forever.
207
+ carry = "";
208
+ }
209
+ return;
210
+ }
211
+ const complete = carry.slice(0, lastBreak);
212
+ carry = carry.slice(lastBreak + 1);
213
+ const seen = scanCodexSSEForUsage(complete);
214
+ if (seen) {
215
+ latest = seen;
216
+ }
217
+ }
218
+ catch {
219
+ // Telemetry must never break the relay.
220
+ }
221
+ },
222
+ flush() {
223
+ try {
224
+ const seen = scanCodexSSEForUsage(carry);
225
+ if (seen) {
226
+ latest = seen;
227
+ }
228
+ }
229
+ catch {
230
+ // ignored — see above
231
+ }
232
+ settle(latest);
233
+ },
234
+ /**
235
+ * A client hanging up mid-response, or an upstream error, aborts the
236
+ * stream rather than closing it — so flush() never runs. Without this the
237
+ * usage promise would never settle and every aborted request would leak a
238
+ * pending handler. Report whatever was seen before the abort.
239
+ */
240
+ cancel() {
241
+ settle(latest);
242
+ },
243
+ };
244
+ const stream = new TransformStream(transformer);
245
+ return { stream, usage };
246
+ }
247
+ //# sourceMappingURL=codexUsage.js.map
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { createInterface } from "node:readline";
5
5
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES, } from "./routingEvidence.js";
7
+ import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
7
8
  const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
8
9
  const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
9
10
  const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
@@ -358,6 +359,13 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
358
359
  let cacheReadTokens = 0;
359
360
  let cacheCreationTokens = 0;
360
361
  let inputTokens = 0;
362
+ let outputTokens = 0;
363
+ let estimatedCostUsd = 0;
364
+ let requestsPriced = 0;
365
+ let requestsPricedByPrefix = 0;
366
+ let requestsUnpriced = 0;
367
+ const modelsPricedByPrefix = new Set();
368
+ const unpricedModels = new Set();
361
369
  const finalRequestLatency = [];
362
370
  const singleAttemptDelta = [];
363
371
  const errorTypes = {};
@@ -394,13 +402,50 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
394
402
  singleAttemptDelta.push(request.durationMs - requestAttempts.totalDurationMs);
395
403
  }
396
404
  if (request.inputTokens !== null ||
405
+ request.outputTokens !== null ||
397
406
  request.cacheReadTokens !== null ||
398
407
  request.cacheCreationTokens !== null) {
399
408
  requestsWithUsage += 1;
400
409
  inputTokens += request.inputTokens ?? 0;
410
+ outputTokens += request.outputTokens ?? 0;
401
411
  cacheReadTokens += request.cacheReadTokens ?? 0;
402
412
  cacheCreationTokens += request.cacheCreationTokens ?? 0;
403
413
  requestsWithCacheRead += (request.cacheReadTokens ?? 0) > 0 ? 1 : 0;
414
+ if (request.model) {
415
+ // Records written before `provider` existed carry only a model name.
416
+ // "openai-compatible" resolves to the cross-provider table search in
417
+ // pricing.ts, which finds the model wherever it lives — a far better
418
+ // guess than assuming Anthropic and pricing a GPT model at $0.
419
+ const cost = calculateCost(request.provider ?? "openai-compatible", request.model, {
420
+ input: request.inputTokens ?? 0,
421
+ output: request.outputTokens ?? 0,
422
+ total: (request.inputTokens ?? 0) +
423
+ (request.outputTokens ?? 0) +
424
+ (request.cacheCreationTokens ?? 0) +
425
+ (request.cacheReadTokens ?? 0),
426
+ cacheCreationTokens: request.cacheCreationTokens ?? 0,
427
+ cacheReadTokens: request.cacheReadTokens ?? 0,
428
+ });
429
+ // Ask the table directly rather than inferring from cost > 0: a real
430
+ // request with trivial usage can round to $0.000000 and is priced, not
431
+ // unpriced.
432
+ const priced = hasPricing(request.provider ?? "openai-compatible", request.model);
433
+ if (priced) {
434
+ estimatedCostUsd += cost;
435
+ requestsPriced += 1;
436
+ // A prefix fallback means the rate was inherited from a
437
+ // similarly-named model, not quoted for this one. Surface it rather
438
+ // than presenting a guess as a figure.
439
+ if (!isExactPricingMatch(request.provider ?? "openai-compatible", request.model)) {
440
+ requestsPricedByPrefix += 1;
441
+ modelsPricedByPrefix.add(request.model);
442
+ }
443
+ }
444
+ else {
445
+ requestsUnpriced += 1;
446
+ unpricedModels.add(request.model);
447
+ }
448
+ }
404
449
  }
405
450
  }
406
451
  return {
@@ -419,6 +464,13 @@ function summarizeFinalRequests(finalRequests, terminalStreamErrors, attemptsByR
419
464
  cacheReadTokens,
420
465
  cacheCreationTokens,
421
466
  inputTokens,
467
+ outputTokens,
468
+ estimatedCostUsd: Number(estimatedCostUsd.toFixed(6)),
469
+ requestsPriced,
470
+ requestsPricedByPrefix,
471
+ modelsPricedByPrefix: [...modelsPricedByPrefix].sort(),
472
+ requestsUnpriced,
473
+ unpricedModels: [...unpricedModels].sort(),
422
474
  requestHitRate: requestsWithUsage > 0
423
475
  ? Number((requestsWithCacheRead / requestsWithUsage).toFixed(4))
424
476
  : null,
@@ -695,13 +747,26 @@ export async function analyzeProxyLogs(options) {
695
747
  for (const filePath of requestFiles) {
696
748
  linesRead += await readJsonLines(filePath, (record) => {
697
749
  const timestamp = observeTimestamp("requests", record);
698
- if (timestamp === null || timestamp < sinceMs || timestamp > untilMs) {
750
+ if (timestamp === null) {
699
751
  return;
700
752
  }
701
753
  const requestId = stringValue(record.requestId);
702
754
  if (!requestId) {
703
755
  return;
704
756
  }
757
+ // A streamed request is logged twice — once when the response headers
758
+ // are known, again when the body finishes and its token counts arrive
759
+ // — and those two writes can straddle the window edge. A Codex turn
760
+ // whose headers land at 23:59:50 and whose stream ends at 00:00:05 has
761
+ // every token it spent in the second record. Filtering that record out
762
+ // by its own timestamp would leave the request counted as completed
763
+ // but contributing nothing to tokens or cost, with nothing in the
764
+ // report to say so. A request the window already admitted therefore
765
+ // keeps accepting its own later records.
766
+ const alreadyAdmitted = finalRequests.has(requestId) || terminalStreamErrors.has(requestId);
767
+ if (!alreadyAdmitted && (timestamp < sinceMs || timestamp > untilMs)) {
768
+ return;
769
+ }
705
770
  if (finiteNumber(record.terminalStatus) !== null) {
706
771
  terminalStreamErrors.add(requestId);
707
772
  return;
@@ -723,19 +788,38 @@ export async function analyzeProxyLogs(options) {
723
788
  else {
724
789
  absentRoutingDecisions += 1;
725
790
  }
726
- finalRequests.set(requestId, {
791
+ const parsed = {
727
792
  timestamp: new Date(timestamp).toISOString(),
728
793
  status,
729
794
  durationMs: finiteNumber(record.responseTimeMs),
730
795
  account: stringValue(record.account) ?? "unknown",
731
796
  accountType: stringValue(record.accountType) ?? "unknown",
797
+ model: stringValue(record.model),
798
+ provider: stringValue(record.provider),
732
799
  inputTokens: finiteNumber(record.inputTokens),
800
+ outputTokens: finiteNumber(record.outputTokens),
733
801
  cacheReadTokens: finiteNumber(record.cacheReadTokens),
734
802
  cacheCreationTokens: finiteNumber(record.cacheCreationTokens),
735
803
  errorType: stringValue(record.errorType),
736
804
  errorCode: stringValue(record.errorCode),
737
805
  routingDecision,
738
- });
806
+ };
807
+ // A request may be logged twice: once when the response headers are
808
+ // known, and again when a streamed body finishes and its token counts
809
+ // become available (the Codex engine does this). Merge rather than
810
+ // replace, so the later usage-bearing record cannot drop an errorType
811
+ // the first one carried, and vice versa.
812
+ const previous = finalRequests.get(requestId);
813
+ finalRequests.set(requestId, previous
814
+ ? {
815
+ ...previous,
816
+ ...Object.fromEntries(Object.entries(parsed).filter(([, value]) => value !== null && value !== undefined)),
817
+ // Attribute the request to when it was first seen. A late
818
+ // completion record must not move it out of the window that
819
+ // admitted it.
820
+ timestamp: previous.timestamp,
821
+ }
822
+ : parsed);
739
823
  }, () => {
740
824
  malformedLines += 1;
741
825
  });
@@ -47,3 +47,4 @@ export declare function getProxyStatus(): {
47
47
  method: string;
48
48
  capabilities: string[];
49
49
  };
50
+ export declare function warnGoogleSdkIgnoresProxy(providerLabel: string): void;
@@ -697,4 +697,33 @@ export function getProxyStatus() {
697
697
  ],
698
698
  };
699
699
  }
700
+ /**
701
+ * One-time warning that the @google/genai SDK cannot honour a configured
702
+ * proxy.
703
+ *
704
+ * Both Google providers passed `httpOptions: { fetch: createProxyFetch() }`,
705
+ * which does nothing. `HttpOptions` in @google/genai 1.46.0 declares only
706
+ * baseUrl, baseUrlResourceScope, apiVersion, headers, timeout, extraBody and
707
+ * retryOptions — there is no `fetch` on it. That property belongs to a
708
+ * different interface (`ClientOptions`), which `GoogleGenAIOptions` does not
709
+ * accept, and the SDK's request path calls global `fetch`. The option was
710
+ * silently dropped and requests went direct.
711
+ *
712
+ * It type-checked only because those constructors are reached through a
713
+ * loosely-typed local alias, which turns off excess-property checking.
714
+ *
715
+ * This SDK version offers no supported injection point, so rather than keep a
716
+ * line that reads like working proxy support, the situation is reported once
717
+ * per process — and only to someone who actually configured a proxy. Silent
718
+ * bypass is the worst outcome available: a corporate user believes their
719
+ * traffic is proxied when it is not.
720
+ */
721
+ let proxyUnsupportedWarned = false;
722
+ export function warnGoogleSdkIgnoresProxy(providerLabel) {
723
+ if (proxyUnsupportedWarned || !getProxyStatus().enabled) {
724
+ return;
725
+ }
726
+ proxyUnsupportedWarned = true;
727
+ logger.warn(`[${providerLabel}] A proxy is configured, but the @google/genai SDK provides no way to route its requests through it (HttpOptions has no 'fetch', and GoogleGenAIOptions accepts none). Requests from this provider go direct.`);
728
+ }
700
729
  //# sourceMappingURL=proxyFetch.js.map