@juspay/neurolink 9.41.0 → 9.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +7 -1
  3. package/dist/auth/anthropicOAuth.d.ts +18 -3
  4. package/dist/auth/anthropicOAuth.js +149 -4
  5. package/dist/auth/providers/firebase.js +5 -1
  6. package/dist/auth/providers/jwt.js +5 -1
  7. package/dist/auth/providers/workos.js +5 -1
  8. package/dist/auth/sessionManager.d.ts +1 -1
  9. package/dist/auth/sessionManager.js +58 -27
  10. package/dist/browser/neurolink.min.js +354 -334
  11. package/dist/cli/commands/mcp.d.ts +6 -0
  12. package/dist/cli/commands/mcp.js +188 -181
  13. package/dist/cli/commands/proxy.d.ts +2 -1
  14. package/dist/cli/commands/proxy.js +713 -431
  15. package/dist/cli/commands/task.js +3 -0
  16. package/dist/cli/factories/commandFactory.d.ts +2 -0
  17. package/dist/cli/factories/commandFactory.js +38 -0
  18. package/dist/cli/parser.js +4 -3
  19. package/dist/client/aiSdkAdapter.js +3 -0
  20. package/dist/client/streamingClient.js +30 -10
  21. package/dist/core/baseProvider.d.ts +6 -1
  22. package/dist/core/baseProvider.js +208 -230
  23. package/dist/core/factory.d.ts +3 -0
  24. package/dist/core/factory.js +138 -188
  25. package/dist/core/modules/GenerationHandler.js +3 -2
  26. package/dist/core/redisConversationMemoryManager.js +7 -3
  27. package/dist/evaluation/BatchEvaluator.js +4 -1
  28. package/dist/evaluation/hooks/observabilityHooks.js +5 -3
  29. package/dist/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  30. package/dist/evaluation/pipeline/evaluationPipeline.js +24 -9
  31. package/dist/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  32. package/dist/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  33. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  34. package/dist/evaluation/scorers/scorerRegistry.js +353 -282
  35. package/dist/lib/auth/anthropicOAuth.d.ts +18 -3
  36. package/dist/lib/auth/anthropicOAuth.js +149 -4
  37. package/dist/lib/auth/providers/firebase.js +5 -1
  38. package/dist/lib/auth/providers/jwt.js +5 -1
  39. package/dist/lib/auth/providers/workos.js +5 -1
  40. package/dist/lib/auth/sessionManager.d.ts +1 -1
  41. package/dist/lib/auth/sessionManager.js +58 -27
  42. package/dist/lib/client/aiSdkAdapter.js +3 -0
  43. package/dist/lib/client/streamingClient.js +30 -10
  44. package/dist/lib/core/baseProvider.d.ts +6 -1
  45. package/dist/lib/core/baseProvider.js +208 -230
  46. package/dist/lib/core/factory.d.ts +3 -0
  47. package/dist/lib/core/factory.js +138 -188
  48. package/dist/lib/core/modules/GenerationHandler.js +3 -2
  49. package/dist/lib/core/redisConversationMemoryManager.js +7 -3
  50. package/dist/lib/evaluation/BatchEvaluator.js +4 -1
  51. package/dist/lib/evaluation/hooks/observabilityHooks.js +5 -3
  52. package/dist/lib/evaluation/pipeline/evaluationPipeline.d.ts +3 -2
  53. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +24 -9
  54. package/dist/lib/evaluation/pipeline/strategies/batchStrategy.js +6 -3
  55. package/dist/lib/evaluation/pipeline/strategies/samplingStrategy.js +18 -10
  56. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  57. package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
  58. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  59. package/dist/lib/mcp/toolRegistry.js +32 -31
  60. package/dist/lib/neurolink.d.ts +41 -2
  61. package/dist/lib/neurolink.js +1616 -1681
  62. package/dist/lib/observability/otelBridge.d.ts +2 -2
  63. package/dist/lib/observability/otelBridge.js +12 -3
  64. package/dist/lib/providers/amazonBedrock.js +2 -4
  65. package/dist/lib/providers/anthropic.d.ts +9 -5
  66. package/dist/lib/providers/anthropic.js +19 -14
  67. package/dist/lib/providers/anthropicBaseProvider.d.ts +3 -3
  68. package/dist/lib/providers/anthropicBaseProvider.js +5 -4
  69. package/dist/lib/providers/azureOpenai.d.ts +1 -1
  70. package/dist/lib/providers/azureOpenai.js +5 -4
  71. package/dist/lib/providers/googleAiStudio.js +30 -6
  72. package/dist/lib/providers/googleVertex.d.ts +10 -0
  73. package/dist/lib/providers/googleVertex.js +437 -423
  74. package/dist/lib/providers/huggingFace.d.ts +3 -3
  75. package/dist/lib/providers/huggingFace.js +6 -8
  76. package/dist/lib/providers/litellm.d.ts +1 -0
  77. package/dist/lib/providers/litellm.js +76 -55
  78. package/dist/lib/providers/mistral.js +2 -1
  79. package/dist/lib/providers/ollama.js +93 -23
  80. package/dist/lib/providers/openAI.d.ts +2 -0
  81. package/dist/lib/providers/openAI.js +141 -141
  82. package/dist/lib/providers/openRouter.js +2 -1
  83. package/dist/lib/providers/openaiCompatible.d.ts +4 -4
  84. package/dist/lib/providers/openaiCompatible.js +4 -4
  85. package/dist/lib/proxy/claudeFormat.d.ts +3 -2
  86. package/dist/lib/proxy/claudeFormat.js +27 -14
  87. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  88. package/dist/lib/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  89. package/dist/lib/proxy/modelRouter.js +3 -0
  90. package/dist/lib/proxy/oauthFetch.d.ts +1 -1
  91. package/dist/lib/proxy/oauthFetch.js +289 -316
  92. package/dist/lib/proxy/proxyConfig.js +46 -24
  93. package/dist/lib/proxy/proxyEnv.d.ts +19 -0
  94. package/dist/lib/proxy/proxyEnv.js +73 -0
  95. package/dist/lib/proxy/proxyFetch.js +291 -217
  96. package/dist/lib/proxy/proxyTracer.d.ts +133 -0
  97. package/dist/lib/proxy/proxyTracer.js +645 -0
  98. package/dist/lib/proxy/rawStreamCapture.d.ts +10 -0
  99. package/dist/lib/proxy/rawStreamCapture.js +83 -0
  100. package/dist/lib/proxy/requestLogger.d.ts +32 -5
  101. package/dist/lib/proxy/requestLogger.js +503 -47
  102. package/dist/lib/proxy/sseInterceptor.d.ts +97 -0
  103. package/dist/lib/proxy/sseInterceptor.js +427 -0
  104. package/dist/lib/proxy/usageStats.d.ts +4 -3
  105. package/dist/lib/proxy/usageStats.js +25 -12
  106. package/dist/lib/rag/chunkers/MarkdownChunker.js +13 -5
  107. package/dist/lib/rag/chunking/markdownChunker.js +15 -6
  108. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +17 -3
  109. package/dist/lib/server/routes/claudeProxyRoutes.js +3032 -1349
  110. package/dist/lib/services/server/ai/observability/instrumentation.d.ts +7 -1
  111. package/dist/lib/services/server/ai/observability/instrumentation.js +337 -161
  112. package/dist/lib/tasks/backends/bullmqBackend.d.ts +1 -0
  113. package/dist/lib/tasks/backends/bullmqBackend.js +35 -22
  114. package/dist/lib/tasks/store/redisTaskStore.d.ts +1 -0
  115. package/dist/lib/tasks/store/redisTaskStore.js +54 -39
  116. package/dist/lib/tasks/taskManager.d.ts +5 -0
  117. package/dist/lib/tasks/taskManager.js +158 -30
  118. package/dist/lib/telemetry/index.d.ts +2 -1
  119. package/dist/lib/telemetry/index.js +2 -1
  120. package/dist/lib/telemetry/telemetryService.d.ts +3 -0
  121. package/dist/lib/telemetry/telemetryService.js +69 -5
  122. package/dist/lib/types/cli.d.ts +10 -0
  123. package/dist/lib/types/proxyTypes.d.ts +160 -5
  124. package/dist/lib/types/streamTypes.d.ts +25 -3
  125. package/dist/lib/utils/messageBuilder.js +3 -2
  126. package/dist/lib/utils/providerHealth.d.ts +19 -0
  127. package/dist/lib/utils/providerHealth.js +279 -33
  128. package/dist/lib/utils/providerUtils.js +17 -22
  129. package/dist/lib/utils/toolChoice.d.ts +4 -0
  130. package/dist/lib/utils/toolChoice.js +7 -0
  131. package/dist/mcp/toolRegistry.d.ts +2 -0
  132. package/dist/mcp/toolRegistry.js +32 -31
  133. package/dist/neurolink.d.ts +41 -2
  134. package/dist/neurolink.js +1616 -1681
  135. package/dist/observability/otelBridge.d.ts +2 -2
  136. package/dist/observability/otelBridge.js +12 -3
  137. package/dist/providers/amazonBedrock.js +2 -4
  138. package/dist/providers/anthropic.d.ts +9 -5
  139. package/dist/providers/anthropic.js +19 -14
  140. package/dist/providers/anthropicBaseProvider.d.ts +3 -3
  141. package/dist/providers/anthropicBaseProvider.js +5 -4
  142. package/dist/providers/azureOpenai.d.ts +1 -1
  143. package/dist/providers/azureOpenai.js +5 -4
  144. package/dist/providers/googleAiStudio.js +30 -6
  145. package/dist/providers/googleVertex.d.ts +10 -0
  146. package/dist/providers/googleVertex.js +437 -423
  147. package/dist/providers/huggingFace.d.ts +3 -3
  148. package/dist/providers/huggingFace.js +6 -7
  149. package/dist/providers/litellm.d.ts +1 -0
  150. package/dist/providers/litellm.js +76 -55
  151. package/dist/providers/mistral.js +2 -1
  152. package/dist/providers/ollama.js +93 -23
  153. package/dist/providers/openAI.d.ts +2 -0
  154. package/dist/providers/openAI.js +141 -141
  155. package/dist/providers/openRouter.js +2 -1
  156. package/dist/providers/openaiCompatible.d.ts +4 -4
  157. package/dist/providers/openaiCompatible.js +4 -3
  158. package/dist/proxy/claudeFormat.d.ts +3 -2
  159. package/dist/proxy/claudeFormat.js +27 -14
  160. package/dist/proxy/cloaking/plugins/sessionIdentity.d.ts +2 -6
  161. package/dist/proxy/cloaking/plugins/sessionIdentity.js +9 -33
  162. package/dist/proxy/modelRouter.js +3 -0
  163. package/dist/proxy/oauthFetch.d.ts +1 -1
  164. package/dist/proxy/oauthFetch.js +289 -316
  165. package/dist/proxy/proxyConfig.js +46 -24
  166. package/dist/proxy/proxyEnv.d.ts +19 -0
  167. package/dist/proxy/proxyEnv.js +72 -0
  168. package/dist/proxy/proxyFetch.js +291 -217
  169. package/dist/proxy/proxyTracer.d.ts +133 -0
  170. package/dist/proxy/proxyTracer.js +644 -0
  171. package/dist/proxy/rawStreamCapture.d.ts +10 -0
  172. package/dist/proxy/rawStreamCapture.js +82 -0
  173. package/dist/proxy/requestLogger.d.ts +32 -5
  174. package/dist/proxy/requestLogger.js +503 -47
  175. package/dist/proxy/sseInterceptor.d.ts +97 -0
  176. package/dist/proxy/sseInterceptor.js +426 -0
  177. package/dist/proxy/usageStats.d.ts +4 -3
  178. package/dist/proxy/usageStats.js +25 -12
  179. package/dist/rag/chunkers/MarkdownChunker.js +13 -5
  180. package/dist/rag/chunking/markdownChunker.js +15 -6
  181. package/dist/server/routes/claudeProxyRoutes.d.ts +17 -3
  182. package/dist/server/routes/claudeProxyRoutes.js +3032 -1349
  183. package/dist/services/server/ai/observability/instrumentation.d.ts +7 -1
  184. package/dist/services/server/ai/observability/instrumentation.js +337 -161
  185. package/dist/tasks/backends/bullmqBackend.d.ts +1 -0
  186. package/dist/tasks/backends/bullmqBackend.js +35 -22
  187. package/dist/tasks/store/redisTaskStore.d.ts +1 -0
  188. package/dist/tasks/store/redisTaskStore.js +54 -39
  189. package/dist/tasks/taskManager.d.ts +5 -0
  190. package/dist/tasks/taskManager.js +158 -30
  191. package/dist/telemetry/index.d.ts +2 -1
  192. package/dist/telemetry/index.js +2 -1
  193. package/dist/telemetry/telemetryService.d.ts +3 -0
  194. package/dist/telemetry/telemetryService.js +69 -5
  195. package/dist/types/cli.d.ts +10 -0
  196. package/dist/types/proxyTypes.d.ts +160 -5
  197. package/dist/types/streamTypes.d.ts +25 -3
  198. package/dist/utils/messageBuilder.js +3 -2
  199. package/dist/utils/providerHealth.d.ts +19 -0
  200. package/dist/utils/providerHealth.js +279 -33
  201. package/dist/utils/providerUtils.js +18 -22
  202. package/dist/utils/toolChoice.d.ts +4 -0
  203. package/dist/utils/toolChoice.js +6 -0
  204. package/docs/assets/dashboards/neurolink-proxy-observability-dashboard.json +6609 -0
  205. package/docs/changelog.md +252 -0
  206. package/package.json +19 -2
  207. package/scripts/observability/check-proxy-telemetry.mjs +235 -0
  208. package/scripts/observability/docker-compose.proxy-observability.yaml +55 -0
  209. package/scripts/observability/import-openobserve-dashboard.mjs +240 -0
  210. package/scripts/observability/manage-local-openobserve.sh +215 -0
  211. package/scripts/observability/otel-collector.proxy-observability.yaml +78 -0
  212. package/scripts/observability/proxy-observability.env.example +23 -0
@@ -10,61 +10,306 @@
10
10
  *
11
11
  * @module proxy/oauthFetch
12
12
  */
13
- import { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX, } 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 { randomBytes, randomUUID } from "crypto";
15
+ import { createProxyFetch } from "./proxyFetch.js";
16
16
  // Re-export constants for consumers that previously imported them alongside
17
17
  // the function from `providers/anthropic.ts`.
18
18
  export { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX };
19
- // ---------------------------------------------------------------------------
20
- // Helpers
21
- // ---------------------------------------------------------------------------
22
- /** Cache fake user IDs per token prefix (1-hour TTL, max 1000 entries). */
23
- const userIdCache = new Map();
24
- const USER_ID_CACHE_TTL_MS = 3_600_000; // 1 hour
25
- const USER_ID_CACHE_MAX_SIZE = 1_000;
26
- /** Evict expired entries and enforce max size (LRU-approximation via insertion order). */
27
- function evictUserIdCache() {
28
- const now = Date.now();
29
- for (const [key, entry] of userIdCache) {
30
- if (entry.expires <= now) {
31
- userIdCache.delete(key);
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);
32
26
  }
33
27
  }
34
- // If still over capacity, remove oldest entries (Map preserves insertion order)
35
- if (userIdCache.size > USER_ID_CACHE_MAX_SIZE) {
36
- const excess = userIdCache.size - USER_ID_CACHE_MAX_SIZE;
37
- let removed = 0;
38
- for (const key of userIdCache.keys()) {
39
- if (removed >= excess) {
40
- break;
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));
41
53
  }
42
- userIdCache.delete(key);
43
- removed++;
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));
44
60
  }
45
61
  }
62
+ return requestHeaders;
46
63
  }
47
- function getOrCreateUserId(tokenPrefix) {
48
- evictUserIdCache();
49
- const cached = userIdCache.get(tokenPrefix);
50
- if (cached && cached.expires > Date.now()) {
51
- return cached.id;
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
+ }
52
144
  }
53
- const hex = randomBytes(32).toString("hex");
54
- const id = `user_${hex}_account_${randomUUID()}_session_${randomUUID()}`;
55
- userIdCache.set(tokenPrefix, {
56
- id,
57
- expires: Date.now() + USER_ID_CACHE_TTL_MS,
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,
58
271
  });
59
- return id;
60
272
  }
61
- /** Compute a short hex hash of a string using Web Crypto (SHA-256). */
62
- async function shortSha256(data) {
63
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
64
- return Array.from(new Uint8Array(buf))
65
- .map((b) => b.toString(16).padStart(2, "0"))
66
- .join("")
67
- .substring(0, 5);
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;
68
313
  }
69
314
  // ---------------------------------------------------------------------------
70
315
  // Main factory
@@ -77,7 +322,7 @@ async function shortSha256(data) {
77
322
  * - Sets User-Agent to Claude CLI
78
323
  * - Adds ?beta=true query parameter to /v1/messages
79
324
  * - Injects billing header & agent block into system prompt
80
- * - Injects fake user ID into metadata
325
+ * - Injects Claude-Code-shaped user ID into metadata
81
326
  * - Adds Stainless SDK headers for fingerprint matching
82
327
  * - Disables thinking when tool_choice is forced
83
328
  *
@@ -91,277 +336,5 @@ async function shortSha256(data) {
91
336
  * Used for proxy passthrough where the request body must be forwarded as-is.
92
337
  */
93
338
  export function createOAuthFetch(getToken, includeOptionalBetas = true, enableMcpPrefix = false, skipBodyTransform = false) {
94
- return async (input, init) => {
95
- // Build the URL
96
- let requestUrl = null;
97
- try {
98
- if (typeof input === "string" || input instanceof URL) {
99
- requestUrl = new URL(input.toString());
100
- }
101
- else if (input instanceof Request) {
102
- requestUrl = new URL(input.url);
103
- }
104
- }
105
- catch {
106
- requestUrl = null;
107
- }
108
- // Add ?beta=true to /v1/messages endpoint
109
- if (requestUrl &&
110
- requestUrl.pathname === "/v1/messages" &&
111
- !requestUrl.searchParams.has("beta")) {
112
- requestUrl.searchParams.set("beta", "true");
113
- }
114
- // Build new headers
115
- const requestHeaders = new Headers();
116
- // Copy headers from Request object if present
117
- if (input instanceof Request) {
118
- input.headers.forEach((value, key) => {
119
- requestHeaders.set(key, value);
120
- });
121
- }
122
- // Copy headers from init if present
123
- if (init?.headers) {
124
- if (init.headers instanceof Headers) {
125
- init.headers.forEach((value, key) => {
126
- requestHeaders.set(key, value);
127
- });
128
- }
129
- else if (Array.isArray(init.headers)) {
130
- for (const [key, value] of init.headers) {
131
- if (typeof value !== "undefined") {
132
- requestHeaders.set(key, String(value));
133
- }
134
- }
135
- }
136
- else {
137
- for (const [key, value] of Object.entries(init.headers)) {
138
- if (typeof value !== "undefined") {
139
- requestHeaders.set(key, String(value));
140
- }
141
- }
142
- }
143
- }
144
- // ------------------------------------------------------------------
145
- // Beta headers — preserve existing client betas and merge in required ones.
146
- // In passthrough mode, Claude Code sends its own betas that MUST be kept
147
- // (e.g., context-management-2025-06-27). We only add oauth if missing.
148
- // ------------------------------------------------------------------
149
- const existingBetas = (requestHeaders.get("anthropic-beta") ?? "")
150
- .split(",")
151
- .map((s) => s.trim())
152
- .filter(Boolean);
153
- const requiredBetas = ["oauth-2025-04-20"];
154
- // Only add our betas if not already present in client's headers
155
- if (!skipBodyTransform) {
156
- // Direct NeuroLink usage — set full beta list
157
- requiredBetas.push("claude-code-20250219", ...(includeOptionalBetas ? ["interleaved-thinking-2025-05-14"] : []), "prompt-caching-scope-2026-01-05");
158
- }
159
- const allBetas = [...new Set([...existingBetas, ...requiredBetas])];
160
- const mergedBetas = allBetas.join(",");
161
- // Set OAuth authorization (Bearer token, NOT x-api-key)
162
- // Call getToken() each time so refreshed tokens are used automatically.
163
- requestHeaders.set("authorization", `Bearer ${getToken()}`);
164
- requestHeaders.set("anthropic-beta", mergedBetas);
165
- if (!skipBodyTransform) {
166
- // Only override user-agent for direct NeuroLink usage
167
- requestHeaders.set("user-agent", CLAUDE_CLI_USER_AGENT);
168
- }
169
- requestHeaders.delete("x-api-key");
170
- // Identity / fingerprint headers (skip in passthrough — client sends its own)
171
- if (!skipBodyTransform) {
172
- requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
173
- requestHeaders.set("x-app", "cli");
174
- requestHeaders.set("connection", "keep-alive");
175
- // Stainless SDK headers
176
- requestHeaders.set("x-stainless-retry-count", "0");
177
- requestHeaders.set("x-stainless-runtime-version", "v24.3.0");
178
- requestHeaders.set("x-stainless-package-version", "0.74.0");
179
- requestHeaders.set("x-stainless-runtime", "node");
180
- requestHeaders.set("x-stainless-lang", "js");
181
- requestHeaders.set("x-stainless-arch", process.arch === "x64" ? "x64" : process.arch);
182
- requestHeaders.set("x-stainless-os", process.platform === "darwin"
183
- ? "MacOS"
184
- : process.platform === "win32"
185
- ? "Windows"
186
- : "Linux");
187
- requestHeaders.set("x-stainless-timeout", "600");
188
- }
189
- logger.debug("[createOAuthFetch] Making OAuth request:", {
190
- url: requestUrl?.toString() || input.toString(),
191
- hasAuthorization: requestHeaders.has("authorization"),
192
- authType: "Bearer",
193
- anthropicBeta: requestHeaders.get("anthropic-beta"),
194
- userAgent: requestHeaders.get("user-agent"),
195
- });
196
- // ------------------------------------------------------------------
197
- // Body transformations (skipped in passthrough/proxy mode)
198
- // ------------------------------------------------------------------
199
- const sourceRequest = input instanceof Request ? input : undefined;
200
- const method = init?.method ?? sourceRequest?.method;
201
- let body = init?.body;
202
- if (body === undefined &&
203
- sourceRequest &&
204
- method !== "GET" &&
205
- method !== "HEAD") {
206
- // Read the body as text (not ReadableStream) so that the JSON transforms
207
- // below can parse and modify it. A ReadableStream would bypass the
208
- // `typeof body === "string"` branch and skip all cloaking transforms.
209
- const contentType = sourceRequest.headers.get("content-type") ?? "";
210
- if (contentType.includes("application/json")) {
211
- body = (await sourceRequest.clone().text()) || undefined;
212
- }
213
- else {
214
- body = sourceRequest.clone().body ?? undefined;
215
- }
216
- }
217
- if (body && typeof body === "string" && !skipBodyTransform) {
218
- try {
219
- const parsed = JSON.parse(body);
220
- // --- mcp_ prefix (only when explicitly enabled) ----------------
221
- if (enableMcpPrefix) {
222
- if (parsed.tools && Array.isArray(parsed.tools)) {
223
- parsed.tools = parsed.tools.map((tool) => ({
224
- ...tool,
225
- name: tool.name ? `${MCP_TOOL_PREFIX}${tool.name}` : tool.name,
226
- }));
227
- }
228
- if (parsed.messages && Array.isArray(parsed.messages)) {
229
- parsed.messages = parsed.messages.map((msg) => {
230
- if (msg.content && Array.isArray(msg.content)) {
231
- msg.content = msg.content.map((block) => {
232
- const b = block;
233
- if (b.type === "tool_use" && b.name) {
234
- return {
235
- ...b,
236
- name: `${MCP_TOOL_PREFIX}${b.name}`,
237
- };
238
- }
239
- return block;
240
- });
241
- }
242
- return msg;
243
- });
244
- }
245
- }
246
- // --- Disable thinking when tool_choice is forced ---------------
247
- if (parsed.tool_choice?.type === "any" ||
248
- parsed.tool_choice?.type === "tool") {
249
- delete parsed.thinking;
250
- }
251
- // --- Inject billing header + agent block into system prompt ----
252
- const version = "2.1.63";
253
- const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(2)))
254
- .map((b) => b.toString(16).padStart(2, "0"))
255
- .join("")
256
- .substring(0, 3);
257
- const bodyString = JSON.stringify(parsed);
258
- const cch = await shortSha256(bodyString);
259
- const billingBlock = {
260
- type: "text",
261
- text: `x-anthropic-billing-header: cc_version=${version}.${randomHex}; cc_entrypoint=cli; cch=${cch};`,
262
- };
263
- const agentBlock = {
264
- type: "text",
265
- text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
266
- };
267
- // Normalise `system` to an array and prepend billing + agent blocks
268
- if (parsed.system) {
269
- if (typeof parsed.system === "string") {
270
- parsed.system = [{ type: "text", text: parsed.system }];
271
- }
272
- if (Array.isArray(parsed.system)) {
273
- parsed.system = [billingBlock, agentBlock, ...parsed.system];
274
- }
275
- }
276
- else {
277
- parsed.system = [billingBlock, agentBlock];
278
- }
279
- // --- Inject fake user ID into metadata -------------------------
280
- const token = getToken();
281
- const tokenPrefix = token.substring(0, Math.min(20, token.length));
282
- parsed.metadata = {
283
- ...parsed.metadata,
284
- user_id: getOrCreateUserId(tokenPrefix),
285
- };
286
- body = JSON.stringify(parsed);
287
- }
288
- catch {
289
- // Ignore JSON parse errors — pass body through unchanged
290
- }
291
- }
292
- // Remove any inherited content-length — the body may have been transformed
293
- // above, so the original length is stale. Let fetch/undici recalculate it.
294
- requestHeaders.delete("content-length");
295
- // Make the request
296
- const response = await fetch(requestUrl?.toString() ||
297
- (input instanceof Request ? input.url : input.toString()), {
298
- ...init,
299
- method,
300
- body,
301
- signal: init?.signal ?? sourceRequest?.signal,
302
- headers: requestHeaders,
303
- });
304
- // Transform streaming response to rename tools back (remove mcp_ prefix).
305
- // Uses a dynamically-sized carry buffer that holds any incomplete JSON
306
- // token spanning a chunk boundary (e.g. a partial `"name": "mcp_..."`).
307
- if (enableMcpPrefix && response.body) {
308
- const reader = response.body.getReader();
309
- const decoder = new TextDecoder();
310
- const encoder = new TextEncoder();
311
- const responseHeaders = new Headers(response.headers);
312
- responseHeaders.delete("content-length");
313
- let carry = "";
314
- const stream = new ReadableStream({
315
- async pull(controller) {
316
- const { done, value } = await reader.read();
317
- if (done) {
318
- // Flush any remaining carry
319
- if (carry) {
320
- const flushed = carry.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
321
- controller.enqueue(encoder.encode(flushed));
322
- carry = "";
323
- }
324
- controller.close();
325
- return;
326
- }
327
- const chunkText = decoder.decode(value, { stream: true });
328
- const combined = carry + chunkText;
329
- // Detect a trailing partial `"name":\s*"mcp_...` that hasn't closed
330
- // yet (no closing quote for the value). We must keep the entire
331
- // partial field in carry so the regex can match it once the next
332
- // chunk completes it.
333
- const partialMatch = combined.match(/"name"\s*:\s*"mcp_[^"]*$/);
334
- let safeText;
335
- if (partialMatch && partialMatch.index !== undefined) {
336
- // Partial mcp_ name field at end — carry the entire partial field
337
- safeText = combined.slice(0, partialMatch.index);
338
- carry = combined.slice(partialMatch.index);
339
- }
340
- else {
341
- // No partial mcp_ field — safe to process everything.
342
- // Still carry trailing content after the last quote to avoid
343
- // splitting other JSON tokens.
344
- const lastQuote = combined.lastIndexOf('"');
345
- const safeLen = lastQuote >= 0 ? lastQuote + 1 : combined.length;
346
- safeText = combined.slice(0, safeLen);
347
- carry = combined.slice(safeLen);
348
- }
349
- // Apply the mcp_ stripping regex on the safe portion
350
- const replaced = safeText.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
351
- if (replaced) {
352
- controller.enqueue(encoder.encode(replaced));
353
- }
354
- },
355
- async cancel(reason) {
356
- await reader.cancel(reason);
357
- },
358
- });
359
- return new Response(stream, {
360
- status: response.status,
361
- statusText: response.statusText,
362
- headers: responseHeaders,
363
- });
364
- }
365
- return response;
366
- };
339
+ return async (input, init) => executeOAuthFetch(input, init, getToken, includeOptionalBetas, enableMcpPrefix, skipBodyTransform);
367
340
  }