@juspay/neurolink 9.42.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.
- package/CHANGELOG.md +2 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +337 -336
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +188 -184
- package/dist/cli/commands/proxy.js +537 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +208 -230
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +138 -188
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +208 -230
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +138 -188
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +353 -282
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +38 -0
- package/dist/lib/neurolink.js +1858 -1689
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +436 -444
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +73 -64
- package/dist/lib/providers/ollama.js +17 -4
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +139 -140
- package/dist/lib/proxy/claudeFormat.js +12 -4
- package/dist/lib/proxy/oauthFetch.js +298 -318
- package/dist/lib/proxy/proxyConfig.js +3 -1
- package/dist/lib/proxy/proxyFetch.js +250 -222
- package/dist/lib/proxy/requestLogger.js +132 -45
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +10 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2726 -2272
- package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
- package/dist/lib/tasks/store/redisTaskStore.js +23 -16
- package/dist/lib/tasks/taskManager.d.ts +2 -0
- package/dist/lib/tasks/taskManager.js +100 -5
- package/dist/lib/telemetry/telemetryService.js +9 -5
- package/dist/lib/types/proxyTypes.d.ts +124 -1
- package/dist/lib/utils/providerHealth.d.ts +1 -0
- package/dist/lib/utils/providerHealth.js +46 -31
- package/dist/lib/utils/providerUtils.js +11 -22
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +38 -0
- package/dist/neurolink.js +1858 -1689
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +436 -444
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +73 -64
- package/dist/providers/ollama.js +17 -4
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +139 -140
- package/dist/proxy/claudeFormat.js +12 -4
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/requestLogger.js +132 -45
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +10 -1
- package/dist/server/routes/claudeProxyRoutes.js +2726 -2272
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +23 -16
- package/dist/tasks/taskManager.d.ts +2 -0
- package/dist/tasks/taskManager.js +100 -5
- package/dist/telemetry/telemetryService.js +9 -5
- package/dist/types/proxyTypes.d.ts +124 -1
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/package.json +3 -2
- package/scripts/observability/check-proxy-telemetry.mjs +1 -1
- 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,
|
|
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,322 +336,6 @@ 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
|
}
|
|
361
341
|
//# sourceMappingURL=oauthFetch.js.map
|
|
@@ -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 &&
|
|
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
|
}
|