@juspay/neurolink 9.42.0 → 9.43.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 (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +335 -334
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +200 -184
  6. package/dist/cli/commands/proxy.js +560 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +219 -232
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +140 -190
  11. package/dist/core/modules/ToolsManager.d.ts +1 -0
  12. package/dist/core/modules/ToolsManager.js +40 -42
  13. package/dist/core/toolEvents.d.ts +3 -0
  14. package/dist/core/toolEvents.js +7 -0
  15. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  16. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  17. package/dist/evaluation/scorers/scorerRegistry.js +356 -284
  18. package/dist/lib/auth/anthropicOAuth.js +12 -0
  19. package/dist/lib/core/baseProvider.d.ts +6 -1
  20. package/dist/lib/core/baseProvider.js +219 -232
  21. package/dist/lib/core/factory.d.ts +3 -0
  22. package/dist/lib/core/factory.js +140 -190
  23. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  24. package/dist/lib/core/modules/ToolsManager.js +40 -42
  25. package/dist/lib/core/toolEvents.d.ts +3 -0
  26. package/dist/lib/core/toolEvents.js +8 -0
  27. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  28. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  29. package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
  30. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  31. package/dist/lib/mcp/toolRegistry.js +32 -31
  32. package/dist/lib/neurolink.d.ts +38 -0
  33. package/dist/lib/neurolink.js +1890 -1707
  34. package/dist/lib/providers/googleAiStudio.js +0 -5
  35. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  36. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  37. package/dist/lib/providers/googleVertex.d.ts +10 -0
  38. package/dist/lib/providers/googleVertex.js +445 -445
  39. package/dist/lib/providers/litellm.d.ts +1 -0
  40. package/dist/lib/providers/litellm.js +73 -64
  41. package/dist/lib/providers/ollama.js +17 -4
  42. package/dist/lib/providers/openAI.d.ts +2 -0
  43. package/dist/lib/providers/openAI.js +139 -140
  44. package/dist/lib/proxy/claudeFormat.js +14 -5
  45. package/dist/lib/proxy/oauthFetch.js +298 -318
  46. package/dist/lib/proxy/proxyConfig.js +3 -1
  47. package/dist/lib/proxy/proxyFetch.js +250 -222
  48. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  49. package/dist/lib/proxy/proxyHealth.js +55 -0
  50. package/dist/lib/proxy/requestLogger.js +140 -48
  51. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  52. package/dist/lib/proxy/routingPolicy.js +255 -0
  53. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  54. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  55. package/dist/lib/proxy/sseInterceptor.js +36 -11
  56. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  57. package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
  58. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  59. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  60. package/dist/lib/tasks/store/redisTaskStore.js +42 -17
  61. package/dist/lib/tasks/taskManager.d.ts +2 -0
  62. package/dist/lib/tasks/taskManager.js +100 -5
  63. package/dist/lib/telemetry/telemetryService.js +9 -5
  64. package/dist/lib/types/cli.d.ts +4 -0
  65. package/dist/lib/types/proxyTypes.d.ts +211 -1
  66. package/dist/lib/types/tools.d.ts +18 -0
  67. package/dist/lib/utils/providerHealth.d.ts +1 -0
  68. package/dist/lib/utils/providerHealth.js +46 -31
  69. package/dist/lib/utils/providerUtils.js +11 -22
  70. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  71. package/dist/lib/utils/schemaConversion.js +3 -0
  72. package/dist/mcp/toolRegistry.d.ts +2 -0
  73. package/dist/mcp/toolRegistry.js +32 -31
  74. package/dist/neurolink.d.ts +38 -0
  75. package/dist/neurolink.js +1890 -1707
  76. package/dist/providers/googleAiStudio.js +0 -5
  77. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  78. package/dist/providers/googleNativeGemini3.js +39 -1
  79. package/dist/providers/googleVertex.d.ts +10 -0
  80. package/dist/providers/googleVertex.js +445 -445
  81. package/dist/providers/litellm.d.ts +1 -0
  82. package/dist/providers/litellm.js +73 -64
  83. package/dist/providers/ollama.js +17 -4
  84. package/dist/providers/openAI.d.ts +2 -0
  85. package/dist/providers/openAI.js +139 -140
  86. package/dist/proxy/claudeFormat.js +14 -5
  87. package/dist/proxy/oauthFetch.js +298 -318
  88. package/dist/proxy/proxyConfig.js +3 -1
  89. package/dist/proxy/proxyFetch.js +250 -222
  90. package/dist/proxy/proxyHealth.d.ts +17 -0
  91. package/dist/proxy/proxyHealth.js +54 -0
  92. package/dist/proxy/requestLogger.js +140 -48
  93. package/dist/proxy/routingPolicy.d.ts +33 -0
  94. package/dist/proxy/routingPolicy.js +254 -0
  95. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  96. package/dist/proxy/snapshotPersistence.js +40 -0
  97. package/dist/proxy/sseInterceptor.js +36 -11
  98. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  99. package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
  100. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  101. package/dist/tasks/backends/bullmqBackend.js +24 -18
  102. package/dist/tasks/store/redisTaskStore.js +42 -17
  103. package/dist/tasks/taskManager.d.ts +2 -0
  104. package/dist/tasks/taskManager.js +100 -5
  105. package/dist/telemetry/telemetryService.js +9 -5
  106. package/dist/types/cli.d.ts +4 -0
  107. package/dist/types/proxyTypes.d.ts +211 -1
  108. package/dist/types/tools.d.ts +18 -0
  109. package/dist/utils/providerHealth.d.ts +1 -0
  110. package/dist/utils/providerHealth.js +46 -31
  111. package/dist/utils/providerUtils.js +12 -22
  112. package/dist/utils/schemaConversion.d.ts +1 -0
  113. package/dist/utils/schemaConversion.js +3 -0
  114. package/package.json +3 -2
  115. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  116. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -10,11 +10,307 @@
10
10
  *
11
11
  * @module proxy/oauthFetch
12
12
  */
13
- import { CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, MCP_TOOL_PREFIX, buildStableClaudeCodeBillingHeader, getOrCreateClaudeCodeIdentity, } from "../auth/anthropicOAuth.js";
13
+ import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, MCP_TOOL_PREFIX, } from "../auth/anthropicOAuth.js";
14
14
  import { logger } from "../utils/logger.js";
15
+ import { createProxyFetch } from "./proxyFetch.js";
15
16
  // Re-export constants for consumers that previously imported them alongside
16
17
  // the function from `providers/anthropic.ts`.
17
18
  export { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX };
19
+ function resolveOAuthRequestUrl(input) {
20
+ try {
21
+ if (typeof input === "string" || input instanceof URL) {
22
+ return new URL(input.toString());
23
+ }
24
+ if (input instanceof Request) {
25
+ return new URL(input.url);
26
+ }
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ return null;
32
+ }
33
+ function mergeRequestHeaders(input, init) {
34
+ const requestHeaders = new Headers();
35
+ if (input instanceof Request) {
36
+ input.headers.forEach((value, key) => {
37
+ requestHeaders.set(key, value);
38
+ });
39
+ }
40
+ if (!init?.headers) {
41
+ return requestHeaders;
42
+ }
43
+ if (init.headers instanceof Headers) {
44
+ init.headers.forEach((value, key) => {
45
+ requestHeaders.set(key, value);
46
+ });
47
+ return requestHeaders;
48
+ }
49
+ if (Array.isArray(init.headers)) {
50
+ for (const [key, value] of init.headers) {
51
+ if (typeof value !== "undefined") {
52
+ requestHeaders.set(key, String(value));
53
+ }
54
+ }
55
+ return requestHeaders;
56
+ }
57
+ for (const [key, value] of Object.entries(init.headers)) {
58
+ if (typeof value !== "undefined") {
59
+ requestHeaders.set(key, String(value));
60
+ }
61
+ }
62
+ return requestHeaders;
63
+ }
64
+ function applyOAuthHeaders(requestHeaders, getToken, includeOptionalBetas, skipBodyTransform) {
65
+ const existingBetas = (requestHeaders.get("anthropic-beta") ?? "")
66
+ .split(",")
67
+ .map((s) => s.trim())
68
+ .filter(Boolean);
69
+ const requiredBetas = ["oauth-2025-04-20"];
70
+ if (!skipBodyTransform) {
71
+ requiredBetas.push(...(includeOptionalBetas
72
+ ? CLAUDE_CODE_OAUTH_BETAS.filter((beta) => beta !== "oauth-2025-04-20")
73
+ : []));
74
+ }
75
+ requestHeaders.set("authorization", `Bearer ${getToken()}`);
76
+ requestHeaders.set("anthropic-beta", [...new Set([...existingBetas, ...requiredBetas])].join(","));
77
+ requestHeaders.delete("x-api-key");
78
+ if (skipBodyTransform) {
79
+ return;
80
+ }
81
+ requestHeaders.set("user-agent", CLAUDE_CLI_USER_AGENT);
82
+ requestHeaders.set("anthropic-version", "2023-06-01");
83
+ requestHeaders.set("accept", "application/json");
84
+ requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
85
+ requestHeaders.set("x-app", "cli");
86
+ requestHeaders.set("connection", "keep-alive");
87
+ requestHeaders.set("x-stainless-retry-count", "0");
88
+ requestHeaders.set("x-stainless-runtime-version", "v24.3.0");
89
+ requestHeaders.set("x-stainless-package-version", "0.74.0");
90
+ requestHeaders.set("x-stainless-runtime", "node");
91
+ requestHeaders.set("x-stainless-lang", "js");
92
+ requestHeaders.set("x-stainless-arch", process.arch === "x64" ? "x64" : process.arch);
93
+ requestHeaders.set("x-stainless-os", process.platform === "darwin"
94
+ ? "MacOS"
95
+ : process.platform === "win32"
96
+ ? "Windows"
97
+ : "Linux");
98
+ requestHeaders.set("x-stainless-timeout", "600");
99
+ }
100
+ async function resolveOAuthRequestBody(input, init) {
101
+ const sourceRequest = input instanceof Request ? input : undefined;
102
+ const method = init?.method ?? sourceRequest?.method;
103
+ let body = init?.body;
104
+ if (body === undefined &&
105
+ sourceRequest &&
106
+ method !== "GET" &&
107
+ method !== "HEAD") {
108
+ const contentType = sourceRequest.headers.get("content-type") ?? "";
109
+ if (contentType.includes("application/json")) {
110
+ body = (await sourceRequest.clone().text()) || undefined;
111
+ }
112
+ else {
113
+ body = sourceRequest.clone().body ?? undefined;
114
+ }
115
+ }
116
+ return { sourceRequest, method, body: body ?? undefined };
117
+ }
118
+ function transformOAuthJsonBody(body, requestHeaders, getToken, enableMcpPrefix) {
119
+ const parsed = JSON.parse(body);
120
+ if (enableMcpPrefix) {
121
+ if (parsed.tools && Array.isArray(parsed.tools)) {
122
+ parsed.tools = parsed.tools.map((tool) => ({
123
+ ...tool,
124
+ name: tool.name ? `${MCP_TOOL_PREFIX}${tool.name}` : tool.name,
125
+ }));
126
+ }
127
+ if (parsed.messages && Array.isArray(parsed.messages)) {
128
+ parsed.messages = parsed.messages.map((msg) => {
129
+ if (msg.content && Array.isArray(msg.content)) {
130
+ msg.content = msg.content.map((block) => {
131
+ const b = block;
132
+ if (b.type === "tool_use" && b.name) {
133
+ return {
134
+ ...b,
135
+ name: `${MCP_TOOL_PREFIX}${b.name}`,
136
+ };
137
+ }
138
+ return block;
139
+ });
140
+ }
141
+ return msg;
142
+ });
143
+ }
144
+ }
145
+ if (parsed.tool_choice?.type === "any" ||
146
+ parsed.tool_choice?.type === "tool") {
147
+ delete parsed.thinking;
148
+ }
149
+ const agentBlock = {
150
+ type: "text",
151
+ text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
152
+ };
153
+ // Normalise `system` to an array and APPEND billing + agent blocks.
154
+ // IMPORTANT: We append (not prepend) to preserve the client's cache
155
+ // prefix chain. Anthropic's prompt caching uses prefix matching — if
156
+ // we insert anything before the client's system blocks, we invalidate
157
+ // all cached content (tools, system prompt, message history).
158
+ //
159
+ // Claude Code sends a billing block with a `cch=<hash>` value that
160
+ // changes on every request. We remove any existing billing/agent
161
+ // blocks from their positions and always append our stable
162
+ // Claude-Code-shaped versions at the end.
163
+ if (parsed.system) {
164
+ if (typeof parsed.system === "string") {
165
+ parsed.system = [{ type: "text", text: parsed.system }];
166
+ }
167
+ if (Array.isArray(parsed.system)) {
168
+ // Find and remove existing billing/agent blocks from wherever
169
+ // the client placed them (typically at system[0])
170
+ const billingIdx = parsed.system.findIndex((b) => typeof b.text === "string" &&
171
+ b.text.includes("x-anthropic-billing-header"));
172
+ const agentIdx = parsed.system.findIndex((b) => typeof b.text === "string" && b.text.includes("Claude Agent SDK"));
173
+ const billingBlock = {
174
+ type: "text",
175
+ text: buildStableClaudeCodeBillingHeader(parsed.system[billingIdx]?.text),
176
+ };
177
+ // Remove in reverse index order so indices stay valid
178
+ const indicesToRemove = [billingIdx, agentIdx]
179
+ .filter((i) => i >= 0)
180
+ .sort((a, b) => b - a);
181
+ for (const idx of indicesToRemove) {
182
+ parsed.system.splice(idx, 1);
183
+ }
184
+ // Always append deterministic billing + agent blocks at the end
185
+ parsed.system = [...parsed.system, billingBlock, agentBlock];
186
+ }
187
+ }
188
+ else {
189
+ const billingBlock = {
190
+ type: "text",
191
+ text: buildStableClaudeCodeBillingHeader(),
192
+ };
193
+ parsed.system = [billingBlock, agentBlock];
194
+ }
195
+ const token = getToken();
196
+ const stableId = parsed.metadata?.user_id ?? token.substring(0, Math.min(20, token.length));
197
+ const identity = getOrCreateClaudeCodeIdentity(stableId, {
198
+ existingUserId: parsed.metadata?.user_id,
199
+ preferredSessionId: requestHeaders.get("x-claude-code-session-id") ?? undefined,
200
+ });
201
+ parsed.metadata = {
202
+ ...parsed.metadata,
203
+ user_id: identity.metadataUserId,
204
+ };
205
+ requestHeaders.set("x-claude-code-session-id", identity.sessionId);
206
+ return JSON.stringify(parsed);
207
+ }
208
+ async function injectOtelHeaders(requestHeaders) {
209
+ try {
210
+ const { propagation: otelPropagation, context: otelContext } = await import("@opentelemetry/api");
211
+ const carrier = {};
212
+ otelPropagation.inject(otelContext.active(), carrier);
213
+ for (const [key, value] of Object.entries(carrier)) {
214
+ if (!requestHeaders.has(key)) {
215
+ requestHeaders.set(key, value);
216
+ }
217
+ }
218
+ }
219
+ catch {
220
+ // OTel not available — skip silently
221
+ }
222
+ }
223
+ function rewriteMcpPrefixedStreamingResponse(response) {
224
+ if (!response.body) {
225
+ return response;
226
+ }
227
+ const reader = response.body.getReader();
228
+ const decoder = new TextDecoder();
229
+ const encoder = new TextEncoder();
230
+ const responseHeaders = new Headers(response.headers);
231
+ responseHeaders.delete("content-length");
232
+ let carry = "";
233
+ const stream = new ReadableStream({
234
+ async pull(controller) {
235
+ const { done, value } = await reader.read();
236
+ if (done) {
237
+ if (carry) {
238
+ controller.enqueue(encoder.encode(carry.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"')));
239
+ carry = "";
240
+ }
241
+ controller.close();
242
+ return;
243
+ }
244
+ const chunkText = decoder.decode(value, { stream: true });
245
+ const combined = carry + chunkText;
246
+ const partialMatch = combined.match(/"name"\s*:\s*"mcp_[^"]*$/);
247
+ let safeText;
248
+ if (partialMatch && partialMatch.index !== undefined) {
249
+ safeText = combined.slice(0, partialMatch.index);
250
+ carry = combined.slice(partialMatch.index);
251
+ }
252
+ else {
253
+ const lastQuote = combined.lastIndexOf('"');
254
+ const safeLen = lastQuote >= 0 ? lastQuote + 1 : combined.length;
255
+ safeText = combined.slice(0, safeLen);
256
+ carry = combined.slice(safeLen);
257
+ }
258
+ const replaced = safeText.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
259
+ if (replaced) {
260
+ controller.enqueue(encoder.encode(replaced));
261
+ }
262
+ },
263
+ async cancel(reason) {
264
+ await reader.cancel(reason);
265
+ },
266
+ });
267
+ return new Response(stream, {
268
+ status: response.status,
269
+ statusText: response.statusText,
270
+ headers: responseHeaders,
271
+ });
272
+ }
273
+ async function executeOAuthFetch(input, init, getToken, includeOptionalBetas, enableMcpPrefix, skipBodyTransform) {
274
+ const requestUrl = resolveOAuthRequestUrl(input);
275
+ if (requestUrl &&
276
+ requestUrl.pathname === "/v1/messages" &&
277
+ !requestUrl.searchParams.has("beta")) {
278
+ requestUrl.searchParams.set("beta", "true");
279
+ }
280
+ const requestHeaders = mergeRequestHeaders(input, init);
281
+ applyOAuthHeaders(requestHeaders, getToken, includeOptionalBetas, skipBodyTransform);
282
+ logger.debug("[createOAuthFetch] Making OAuth request:", {
283
+ url: requestUrl?.toString() || input.toString(),
284
+ hasAuthorization: requestHeaders.has("authorization"),
285
+ authType: "Bearer",
286
+ anthropicBeta: requestHeaders.get("anthropic-beta"),
287
+ userAgent: requestHeaders.get("user-agent"),
288
+ });
289
+ const { sourceRequest, method, body: initialBody, } = await resolveOAuthRequestBody(input, init);
290
+ let body = initialBody;
291
+ if (body && typeof body === "string" && !skipBodyTransform) {
292
+ try {
293
+ body = transformOAuthJsonBody(body, requestHeaders, getToken, enableMcpPrefix);
294
+ }
295
+ catch {
296
+ // Ignore JSON parse errors — pass body through unchanged
297
+ }
298
+ }
299
+ requestHeaders.delete("content-length");
300
+ await injectOtelHeaders(requestHeaders);
301
+ const proxyFetch = createProxyFetch();
302
+ const response = await proxyFetch(requestUrl?.toString() ||
303
+ (input instanceof Request ? input.url : input.toString()), {
304
+ ...init,
305
+ method,
306
+ body,
307
+ signal: init?.signal ?? sourceRequest?.signal,
308
+ headers: requestHeaders,
309
+ });
310
+ return enableMcpPrefix
311
+ ? rewriteMcpPrefixedStreamingResponse(response)
312
+ : response;
313
+ }
18
314
  // ---------------------------------------------------------------------------
19
315
  // Main factory
20
316
  // ---------------------------------------------------------------------------
@@ -40,321 +336,5 @@ export { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX };
40
336
  * Used for proxy passthrough where the request body must be forwarded as-is.
41
337
  */
42
338
  export function createOAuthFetch(getToken, includeOptionalBetas = true, enableMcpPrefix = false, skipBodyTransform = false) {
43
- return async (input, init) => {
44
- // Build the URL
45
- let requestUrl = null;
46
- try {
47
- if (typeof input === "string" || input instanceof URL) {
48
- requestUrl = new URL(input.toString());
49
- }
50
- else if (input instanceof Request) {
51
- requestUrl = new URL(input.url);
52
- }
53
- }
54
- catch {
55
- requestUrl = null;
56
- }
57
- // Add ?beta=true to /v1/messages endpoint
58
- if (requestUrl &&
59
- requestUrl.pathname === "/v1/messages" &&
60
- !requestUrl.searchParams.has("beta")) {
61
- requestUrl.searchParams.set("beta", "true");
62
- }
63
- // Build new headers
64
- const requestHeaders = new Headers();
65
- // Copy headers from Request object if present
66
- if (input instanceof Request) {
67
- input.headers.forEach((value, key) => {
68
- requestHeaders.set(key, value);
69
- });
70
- }
71
- // Copy headers from init if present
72
- if (init?.headers) {
73
- if (init.headers instanceof Headers) {
74
- init.headers.forEach((value, key) => {
75
- requestHeaders.set(key, value);
76
- });
77
- }
78
- else if (Array.isArray(init.headers)) {
79
- for (const [key, value] of init.headers) {
80
- if (typeof value !== "undefined") {
81
- requestHeaders.set(key, String(value));
82
- }
83
- }
84
- }
85
- else {
86
- for (const [key, value] of Object.entries(init.headers)) {
87
- if (typeof value !== "undefined") {
88
- requestHeaders.set(key, String(value));
89
- }
90
- }
91
- }
92
- }
93
- // ------------------------------------------------------------------
94
- // Beta headers — preserve existing client betas and merge in required ones.
95
- // In passthrough mode, Claude Code sends its own betas that MUST be kept
96
- // (e.g., context-management-2025-06-27). We only add oauth if missing.
97
- // ------------------------------------------------------------------
98
- const existingBetas = (requestHeaders.get("anthropic-beta") ?? "")
99
- .split(",")
100
- .map((s) => s.trim())
101
- .filter(Boolean);
102
- const requiredBetas = ["oauth-2025-04-20"];
103
- // Only add our betas if not already present in client's headers
104
- if (!skipBodyTransform) {
105
- // Direct NeuroLink usage — set full beta list
106
- requiredBetas.push(...(includeOptionalBetas
107
- ? CLAUDE_CODE_OAUTH_BETAS.filter((beta) => beta !== "oauth-2025-04-20")
108
- : []));
109
- }
110
- const allBetas = [...new Set([...existingBetas, ...requiredBetas])];
111
- const mergedBetas = allBetas.join(",");
112
- // Set OAuth authorization (Bearer token, NOT x-api-key)
113
- // Call getToken() each time so refreshed tokens are used automatically.
114
- requestHeaders.set("authorization", `Bearer ${getToken()}`);
115
- requestHeaders.set("anthropic-beta", mergedBetas);
116
- if (!skipBodyTransform) {
117
- // Only override user-agent for direct NeuroLink usage
118
- requestHeaders.set("user-agent", CLAUDE_CLI_USER_AGENT);
119
- requestHeaders.set("anthropic-version", "2023-06-01");
120
- requestHeaders.set("accept", "application/json");
121
- }
122
- requestHeaders.delete("x-api-key");
123
- // Identity / fingerprint headers (skip in passthrough — client sends its own)
124
- if (!skipBodyTransform) {
125
- requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
126
- requestHeaders.set("x-app", "cli");
127
- requestHeaders.set("connection", "keep-alive");
128
- // Stainless SDK headers
129
- requestHeaders.set("x-stainless-retry-count", "0");
130
- requestHeaders.set("x-stainless-runtime-version", "v24.3.0");
131
- requestHeaders.set("x-stainless-package-version", "0.74.0");
132
- requestHeaders.set("x-stainless-runtime", "node");
133
- requestHeaders.set("x-stainless-lang", "js");
134
- requestHeaders.set("x-stainless-arch", process.arch === "x64" ? "x64" : process.arch);
135
- requestHeaders.set("x-stainless-os", process.platform === "darwin"
136
- ? "MacOS"
137
- : process.platform === "win32"
138
- ? "Windows"
139
- : "Linux");
140
- requestHeaders.set("x-stainless-timeout", "600");
141
- }
142
- logger.debug("[createOAuthFetch] Making OAuth request:", {
143
- url: requestUrl?.toString() || input.toString(),
144
- hasAuthorization: requestHeaders.has("authorization"),
145
- authType: "Bearer",
146
- anthropicBeta: requestHeaders.get("anthropic-beta"),
147
- userAgent: requestHeaders.get("user-agent"),
148
- });
149
- // ------------------------------------------------------------------
150
- // Body transformations (skipped in passthrough/proxy mode)
151
- // ------------------------------------------------------------------
152
- const sourceRequest = input instanceof Request ? input : undefined;
153
- const method = init?.method ?? sourceRequest?.method;
154
- let body = init?.body;
155
- if (body === undefined &&
156
- sourceRequest &&
157
- method !== "GET" &&
158
- method !== "HEAD") {
159
- // Read the body as text (not ReadableStream) so that the JSON transforms
160
- // below can parse and modify it. A ReadableStream would bypass the
161
- // `typeof body === "string"` branch and skip all cloaking transforms.
162
- const contentType = sourceRequest.headers.get("content-type") ?? "";
163
- if (contentType.includes("application/json")) {
164
- body = (await sourceRequest.clone().text()) || undefined;
165
- }
166
- else {
167
- body = sourceRequest.clone().body ?? undefined;
168
- }
169
- }
170
- if (body && typeof body === "string" && !skipBodyTransform) {
171
- try {
172
- const parsed = JSON.parse(body);
173
- // --- mcp_ prefix (only when explicitly enabled) ----------------
174
- if (enableMcpPrefix) {
175
- if (parsed.tools && Array.isArray(parsed.tools)) {
176
- parsed.tools = parsed.tools.map((tool) => ({
177
- ...tool,
178
- name: tool.name ? `${MCP_TOOL_PREFIX}${tool.name}` : tool.name,
179
- }));
180
- }
181
- if (parsed.messages && Array.isArray(parsed.messages)) {
182
- parsed.messages = parsed.messages.map((msg) => {
183
- if (msg.content && Array.isArray(msg.content)) {
184
- msg.content = msg.content.map((block) => {
185
- const b = block;
186
- if (b.type === "tool_use" && b.name) {
187
- return {
188
- ...b,
189
- name: `${MCP_TOOL_PREFIX}${b.name}`,
190
- };
191
- }
192
- return block;
193
- });
194
- }
195
- return msg;
196
- });
197
- }
198
- }
199
- // --- Disable thinking when tool_choice is forced ---------------
200
- if (parsed.tool_choice?.type === "any" ||
201
- parsed.tool_choice?.type === "tool") {
202
- delete parsed.thinking;
203
- }
204
- const agentBlock = {
205
- type: "text",
206
- text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
207
- };
208
- // Normalise `system` to an array and APPEND billing + agent blocks.
209
- // IMPORTANT: We append (not prepend) to preserve the client's cache
210
- // prefix chain. Anthropic's prompt caching uses prefix matching — if
211
- // we insert anything before the client's system blocks, we invalidate
212
- // all cached content (tools, system prompt, message history).
213
- //
214
- // Claude Code sends a billing block with a `cch=<hash>` value that
215
- // changes on every request. We remove any existing billing/agent
216
- // blocks from their positions and always append our stable
217
- // Claude-Code-shaped versions at the end.
218
- if (parsed.system) {
219
- if (typeof parsed.system === "string") {
220
- parsed.system = [{ type: "text", text: parsed.system }];
221
- }
222
- if (Array.isArray(parsed.system)) {
223
- // Find and remove existing billing/agent blocks from wherever
224
- // the client placed them (typically at system[0])
225
- const billingIdx = parsed.system.findIndex((b) => typeof b.text === "string" &&
226
- b.text.includes("x-anthropic-billing-header"));
227
- const agentIdx = parsed.system.findIndex((b) => typeof b.text === "string" &&
228
- b.text.includes("Claude Agent SDK"));
229
- const billingBlock = {
230
- type: "text",
231
- text: buildStableClaudeCodeBillingHeader(parsed.system[billingIdx]?.text),
232
- };
233
- // Remove in reverse index order so indices stay valid
234
- const indicesToRemove = [billingIdx, agentIdx]
235
- .filter((i) => i >= 0)
236
- .sort((a, b) => b - a);
237
- for (const idx of indicesToRemove) {
238
- parsed.system.splice(idx, 1);
239
- }
240
- // Always append deterministic billing + agent blocks at the end
241
- parsed.system = [...parsed.system, billingBlock, agentBlock];
242
- }
243
- }
244
- else {
245
- const billingBlock = {
246
- type: "text",
247
- text: buildStableClaudeCodeBillingHeader(),
248
- };
249
- parsed.system = [billingBlock, agentBlock];
250
- }
251
- // --- Inject Claude-Code-shaped identity into metadata ----------
252
- // Prefer existing metadata.user_id (refresh-stable) over the access
253
- // token prefix, which changes on every token rotation.
254
- const stableId = parsed.metadata?.user_id ??
255
- getToken().substring(0, Math.min(20, getToken().length));
256
- const identity = getOrCreateClaudeCodeIdentity(stableId, {
257
- existingUserId: parsed.metadata?.user_id,
258
- preferredSessionId: requestHeaders.get("x-claude-code-session-id") ?? undefined,
259
- });
260
- parsed.metadata = {
261
- ...parsed.metadata,
262
- user_id: identity.metadataUserId,
263
- };
264
- requestHeaders.set("x-claude-code-session-id", identity.sessionId);
265
- body = JSON.stringify(parsed);
266
- }
267
- catch {
268
- // Ignore JSON parse errors — pass body through unchanged
269
- }
270
- }
271
- // Remove any inherited content-length — the body may have been transformed
272
- // above, so the original length is stale. Let fetch/undici recalculate it.
273
- requestHeaders.delete("content-length");
274
- // Inject OTel traceparent so the proxy can link to this trace
275
- try {
276
- const { propagation: otelPropagation, context: otelContext } = await import("@opentelemetry/api");
277
- const carrier = {};
278
- otelPropagation.inject(otelContext.active(), carrier);
279
- for (const [key, value] of Object.entries(carrier)) {
280
- if (!requestHeaders.has(key)) {
281
- requestHeaders.set(key, value);
282
- }
283
- }
284
- }
285
- catch {
286
- // OTel not available — skip silently
287
- }
288
- // Make the request
289
- const response = await fetch(requestUrl?.toString() ||
290
- (input instanceof Request ? input.url : input.toString()), {
291
- ...init,
292
- method,
293
- body,
294
- signal: init?.signal ?? sourceRequest?.signal,
295
- headers: requestHeaders,
296
- });
297
- // Transform streaming response to rename tools back (remove mcp_ prefix).
298
- // Uses a dynamically-sized carry buffer that holds any incomplete JSON
299
- // token spanning a chunk boundary (e.g. a partial `"name": "mcp_..."`).
300
- if (enableMcpPrefix && response.body) {
301
- const reader = response.body.getReader();
302
- const decoder = new TextDecoder();
303
- const encoder = new TextEncoder();
304
- const responseHeaders = new Headers(response.headers);
305
- responseHeaders.delete("content-length");
306
- let carry = "";
307
- const stream = new ReadableStream({
308
- async pull(controller) {
309
- const { done, value } = await reader.read();
310
- if (done) {
311
- // Flush any remaining carry
312
- if (carry) {
313
- const flushed = carry.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
314
- controller.enqueue(encoder.encode(flushed));
315
- carry = "";
316
- }
317
- controller.close();
318
- return;
319
- }
320
- const chunkText = decoder.decode(value, { stream: true });
321
- const combined = carry + chunkText;
322
- // Detect a trailing partial `"name":\s*"mcp_...` that hasn't closed
323
- // yet (no closing quote for the value). We must keep the entire
324
- // partial field in carry so the regex can match it once the next
325
- // chunk completes it.
326
- const partialMatch = combined.match(/"name"\s*:\s*"mcp_[^"]*$/);
327
- let safeText;
328
- if (partialMatch && partialMatch.index !== undefined) {
329
- // Partial mcp_ name field at end — carry the entire partial field
330
- safeText = combined.slice(0, partialMatch.index);
331
- carry = combined.slice(partialMatch.index);
332
- }
333
- else {
334
- // No partial mcp_ field — safe to process everything.
335
- // Still carry trailing content after the last quote to avoid
336
- // splitting other JSON tokens.
337
- const lastQuote = combined.lastIndexOf('"');
338
- const safeLen = lastQuote >= 0 ? lastQuote + 1 : combined.length;
339
- safeText = combined.slice(0, safeLen);
340
- carry = combined.slice(safeLen);
341
- }
342
- // Apply the mcp_ stripping regex on the safe portion
343
- const replaced = safeText.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
344
- if (replaced) {
345
- controller.enqueue(encoder.encode(replaced));
346
- }
347
- },
348
- async cancel(reason) {
349
- await reader.cancel(reason);
350
- },
351
- });
352
- return new Response(stream, {
353
- status: response.status,
354
- statusText: response.statusText,
355
- headers: responseHeaders,
356
- });
357
- }
358
- return response;
359
- };
339
+ return async (input, init) => executeOAuthFetch(input, init, getToken, includeOptionalBetas, enableMcpPrefix, skipBodyTransform);
360
340
  }
@@ -436,7 +436,9 @@ export async function loadProxyConfig(filePath, options = {}) {
436
436
  const raw = parsed;
437
437
  const accounts = {};
438
438
  const rawAccounts = raw.accounts;
439
- if (rawAccounts && typeof rawAccounts === "object") {
439
+ if (rawAccounts &&
440
+ typeof rawAccounts === "object" &&
441
+ !Array.isArray(rawAccounts)) {
440
442
  for (const [provider, list] of Object.entries(rawAccounts)) {
441
443
  accounts[provider] = list.map((item) => applyAccountDefaults(item));
442
444
  }