@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
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Proxy Request Tracer
3
+ *
4
+ * Creates and manages OTel spans for the proxy request lifecycle.
5
+ * Provides a clean API for claudeProxyRoutes to trace each phase:
6
+ * receive -> account_selection -> upstream (per retry) -> stream -> end
7
+ *
8
+ * Uses the existing instrumentation infrastructure:
9
+ * - getTracer() from instrumentation.ts for span creation
10
+ * - setLangfuseContext() for Langfuse enrichment
11
+ * - OtelBridge for context propagation to/from upstream
12
+ * - SpanAttributes from spanTypes.ts for attribute naming
13
+ * - calculateCost() from pricing.ts for cost tracking
14
+ * - TelemetryService for metrics recording
15
+ */
16
+ import { SpanStatusCode, context, metrics, trace, } from "@opentelemetry/api";
17
+ import { getTracer, setLangfuseContext, } from "../services/server/ai/observability/instrumentation.js";
18
+ import { OtelBridge } from "../observability/otelBridge.js";
19
+ import { calculateCost } from "../utils/pricing.js";
20
+ import { TelemetryService } from "../telemetry/telemetryService.js";
21
+ import { logger } from "../utils/logger.js";
22
+ const LOG_PREFIX = "[ProxyTracer]";
23
+ let _metrics = null;
24
+ function getProxyMetrics() {
25
+ if (_metrics) {
26
+ return _metrics;
27
+ }
28
+ const meter = metrics.getMeter("neurolink.proxy", "1.0.0");
29
+ const createdMetrics = {
30
+ requestsTotal: meter.createCounter("proxy_requests_total", {
31
+ description: "Total number of proxy requests",
32
+ unit: "{request}",
33
+ }),
34
+ requestDuration: meter.createHistogram("proxy_request_duration_ms", {
35
+ description: "Proxy request duration in milliseconds",
36
+ unit: "ms",
37
+ }),
38
+ tokensInput: meter.createCounter("proxy_tokens_input", {
39
+ description: "Total input tokens consumed via proxy",
40
+ unit: "{token}",
41
+ }),
42
+ tokensOutput: meter.createCounter("proxy_tokens_output", {
43
+ description: "Total output tokens produced via proxy",
44
+ unit: "{token}",
45
+ }),
46
+ tokensCacheRead: meter.createCounter("proxy_tokens_cache_read", {
47
+ description: "Total cache-read tokens via proxy",
48
+ unit: "{token}",
49
+ }),
50
+ tokensCacheCreation: meter.createCounter("proxy_tokens_cache_creation", {
51
+ description: "Total cache-creation tokens via proxy",
52
+ unit: "{token}",
53
+ }),
54
+ tokensReasoning: meter.createCounter("proxy_tokens_reasoning", {
55
+ description: "Total reasoning tokens via proxy",
56
+ unit: "{token}",
57
+ }),
58
+ costTotal: meter.createCounter("proxy_cost_usd_total", {
59
+ description: "Total estimated cost in USD",
60
+ unit: "USD",
61
+ }),
62
+ errorsTotal: meter.createCounter("proxy_errors_total", {
63
+ description: "Total proxy errors",
64
+ unit: "{error}",
65
+ }),
66
+ retriesTotal: meter.createCounter("proxy_retries_total", {
67
+ description: "Total upstream retry attempts",
68
+ unit: "{retry}",
69
+ }),
70
+ modelSubstitutionTotal: meter.createCounter("proxy_model_substitution_total", {
71
+ description: "Total proxy requests where the response model differs from the requested model",
72
+ unit: "{substitution}",
73
+ }),
74
+ requestBodySize: meter.createHistogram("proxy_request_body_bytes", {
75
+ description: "Request body size in bytes sent upstream",
76
+ unit: "By",
77
+ }),
78
+ responseBodySize: meter.createHistogram("proxy_response_body_bytes", {
79
+ description: "Response body size in bytes received from upstream",
80
+ unit: "By",
81
+ }),
82
+ };
83
+ _metrics = createdMetrics;
84
+ return createdMetrics;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Header redaction (mirrors requestLogger.ts patterns)
88
+ // ---------------------------------------------------------------------------
89
+ /** Headers whose values must always be fully redacted. */
90
+ const SENSITIVE_HEADER_NAMES = new Set([
91
+ "authorization",
92
+ "proxy-authorization",
93
+ "x-api-key",
94
+ "cookie",
95
+ "set-cookie",
96
+ ]);
97
+ /** Pattern matching header names likely to contain secrets. */
98
+ const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
99
+ function redactHeaders(headers) {
100
+ const redacted = {};
101
+ for (const [key, value] of Object.entries(headers)) {
102
+ const lower = key.toLowerCase();
103
+ if (SENSITIVE_HEADER_NAMES.has(lower) ||
104
+ SENSITIVE_HEADER_PATTERN.test(lower)) {
105
+ redacted[key] = "[REDACTED]";
106
+ }
107
+ else {
108
+ redacted[key] = value;
109
+ }
110
+ }
111
+ return redacted;
112
+ }
113
+ /** Redact sensitive JSON fields in request/response bodies before logging. */
114
+ const SENSITIVE_BODY_KEYS = /api[_-]?key|token|secret|password|credential|authorization/i;
115
+ const BODY_LOGGING_ENABLED = process.env.NEUROLINK_PROXY_TRACE_BODY_LOGGING === "true";
116
+ const MAX_BODY_LOG_SIZE = Number.parseInt(process.env.NEUROLINK_PROXY_TRACE_BODY_LOG_BYTES ?? "8192", 10);
117
+ const MAX_STREAM_EVENTS_TO_LOG = 200;
118
+ function redactBodyForLogging(body, maxLen = 8192) {
119
+ const truncated = body.length > maxLen ? body.slice(0, maxLen) + "…[truncated]" : body;
120
+ try {
121
+ const parsed = JSON.parse(truncated);
122
+ function walk(obj) {
123
+ if (obj === null || typeof obj !== "object") {
124
+ return obj;
125
+ }
126
+ if (Array.isArray(obj)) {
127
+ return obj.map(walk);
128
+ }
129
+ const out = {};
130
+ for (const [k, v] of Object.entries(obj)) {
131
+ out[k] = SENSITIVE_BODY_KEYS.test(k) ? "[REDACTED]" : walk(v);
132
+ }
133
+ return out;
134
+ }
135
+ return JSON.stringify(walk(parsed));
136
+ }
137
+ catch {
138
+ return truncated;
139
+ }
140
+ }
141
+ function buildBodyEventAttributes(body) {
142
+ const redacted = redactBodyForLogging(body, MAX_BODY_LOG_SIZE);
143
+ return {
144
+ "proxy.body": redacted,
145
+ "proxy.body.size": body.length,
146
+ "proxy.body.logged": true,
147
+ "proxy.body.truncated": body.length > MAX_BODY_LOG_SIZE,
148
+ };
149
+ }
150
+ // ---------------------------------------------------------------------------
151
+ // Client app detection
152
+ // ---------------------------------------------------------------------------
153
+ function detectClientApp(userAgent) {
154
+ if (!userAgent) {
155
+ return "unknown";
156
+ }
157
+ if (userAgent.startsWith("claude-cli/")) {
158
+ return "cli";
159
+ }
160
+ if (userAgent.startsWith("ai/")) {
161
+ return "sdk";
162
+ }
163
+ return "unknown";
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // ProxyTracer
167
+ // ---------------------------------------------------------------------------
168
+ class ProxyTracer {
169
+ rootSpan;
170
+ proxyTracer = getTracer("neurolink.proxy");
171
+ bridge = new OtelBridge();
172
+ requestId;
173
+ model;
174
+ startTime;
175
+ isStream;
176
+ accountEmail;
177
+ usage;
178
+ mode = "full";
179
+ constructor(rootSpan, requestId, model, stream) {
180
+ this.rootSpan = rootSpan;
181
+ this.requestId = requestId;
182
+ this.model = model;
183
+ this.startTime = Date.now();
184
+ this.isStream = stream;
185
+ }
186
+ /**
187
+ * Create a root span for a proxy request and set Langfuse context.
188
+ *
189
+ * If the incoming request carries a `traceparent` header, the root span
190
+ * will be linked to the caller's trace via OtelBridge.extractContext().
191
+ */
192
+ static startRequest(ctx, incomingHeaders) {
193
+ const tracer = getTracer("neurolink.proxy");
194
+ // Extract parent context from incoming headers (Claude Code may send traceparent)
195
+ let parentContext = context.active();
196
+ if (incomingHeaders) {
197
+ const bridge = new OtelBridge();
198
+ const extracted = bridge.extractContext(incomingHeaders);
199
+ if (extracted) {
200
+ // Create a remote span context so the root span becomes a child of the caller
201
+ parentContext = trace.setSpanContext(context.active(), extracted);
202
+ }
203
+ }
204
+ const clientApp = ctx.clientApp ?? detectClientApp(ctx.userAgent);
205
+ const rootSpan = tracer.startSpan("proxy.request", {
206
+ attributes: {
207
+ "proxy.request_id": ctx.requestId,
208
+ "http.method": ctx.method,
209
+ "http.target": ctx.path,
210
+ "gen_ai.request.model": ctx.model,
211
+ "proxy.stream": ctx.stream,
212
+ "proxy.tool_count": ctx.toolCount,
213
+ "proxy.client_app": clientApp,
214
+ },
215
+ }, parentContext);
216
+ if (ctx.sessionId) {
217
+ rootSpan.setAttribute("session.id", ctx.sessionId);
218
+ }
219
+ if (ctx.userAgent) {
220
+ rootSpan.setAttribute("http.user_agent", ctx.userAgent);
221
+ }
222
+ // Read x-neurolink-* context headers from calling SDK (e.g., Curator)
223
+ const nlSessionId = incomingHeaders?.["x-neurolink-session-id"];
224
+ const nlUserId = incomingHeaders?.["x-neurolink-user-id"];
225
+ const nlConversationId = incomingHeaders?.["x-neurolink-conversation-id"];
226
+ if (nlSessionId) {
227
+ rootSpan.setAttribute("neurolink.session_id", nlSessionId);
228
+ }
229
+ if (nlUserId) {
230
+ rootSpan.setAttribute("neurolink.user_id", nlUserId);
231
+ }
232
+ if (nlConversationId) {
233
+ rootSpan.setAttribute("neurolink.conversation_id", nlConversationId);
234
+ }
235
+ const instance = new ProxyTracer(rootSpan, ctx.requestId, ctx.model, ctx.stream);
236
+ // Set Langfuse context (fire-and-forget — non-blocking)
237
+ // Prefer NeuroLink session/user from calling SDK over Claude Code session
238
+ setLangfuseContext({
239
+ sessionId: nlSessionId ?? ctx.sessionId,
240
+ userId: nlUserId,
241
+ conversationId: nlConversationId,
242
+ requestId: ctx.requestId,
243
+ traceName: `proxy:${ctx.model}`,
244
+ operationName: "proxy.request",
245
+ metadata: {
246
+ clientApp,
247
+ stream: ctx.stream,
248
+ toolCount: ctx.toolCount,
249
+ },
250
+ }).catch((err) => {
251
+ logger.debug(`${LOG_PREFIX} Failed to set Langfuse context`, {
252
+ error: err instanceof Error ? err.message : String(err),
253
+ });
254
+ });
255
+ return instance;
256
+ }
257
+ // -------------------------------------------------------------------------
258
+ // Child spans
259
+ // -------------------------------------------------------------------------
260
+ /** Span covering the initial request receive and parse phase. */
261
+ startReceive() {
262
+ return this.proxyTracer.startSpan("proxy.receive", {
263
+ attributes: {
264
+ "proxy.request_id": this.requestId,
265
+ },
266
+ }, trace.setSpan(context.active(), this.rootSpan));
267
+ }
268
+ /** Span covering account selection logic (fill-first / round-robin). */
269
+ startAccountSelection() {
270
+ return this.proxyTracer.startSpan("proxy.account_selection", {
271
+ attributes: {
272
+ "proxy.request_id": this.requestId,
273
+ },
274
+ }, trace.setSpan(context.active(), this.rootSpan));
275
+ }
276
+ /** Span covering a single upstream attempt. One per retry. */
277
+ startUpstreamAttempt(ctx) {
278
+ return this.proxyTracer.startSpan("proxy.upstream", {
279
+ attributes: {
280
+ "proxy.request_id": this.requestId,
281
+ "proxy.upstream.attempt": ctx.attempt,
282
+ "proxy.upstream.account": ctx.account,
283
+ "proxy.upstream.polyfill_headers": ctx.polyfillHeaders,
284
+ "proxy.upstream.polyfill_body": ctx.polyfillBody,
285
+ "http.url": ctx.upstreamUrl,
286
+ },
287
+ }, trace.setSpan(context.active(), this.rootSpan));
288
+ }
289
+ /** Span covering the SSE stream relay phase. */
290
+ startStream() {
291
+ return this.proxyTracer.startSpan("proxy.stream", {
292
+ attributes: {
293
+ "proxy.request_id": this.requestId,
294
+ "gen_ai.request.model": this.model,
295
+ },
296
+ }, trace.setSpan(context.active(), this.rootSpan));
297
+ }
298
+ // -------------------------------------------------------------------------
299
+ // Attribute setters
300
+ // -------------------------------------------------------------------------
301
+ /** Record account selection outcome on the root span. */
302
+ setAccountSelection(ctx) {
303
+ this.accountEmail = ctx.selectedAccount;
304
+ this.rootSpan.setAttributes({
305
+ "proxy.account.strategy": ctx.strategy,
306
+ "proxy.account.total": ctx.accountsTotal,
307
+ "proxy.account.healthy": ctx.accountsHealthy,
308
+ "proxy.account.selected": ctx.selectedAccount,
309
+ "proxy.account.type": ctx.accountType,
310
+ });
311
+ if (ctx.rateLimitBefore5h !== undefined) {
312
+ this.rootSpan.setAttribute("proxy.ratelimit.before.5h", ctx.rateLimitBefore5h);
313
+ }
314
+ if (ctx.rateLimitBefore7d !== undefined) {
315
+ this.rootSpan.setAttribute("proxy.ratelimit.before.7d", ctx.rateLimitBefore7d);
316
+ }
317
+ // Update Langfuse context with account as userId
318
+ setLangfuseContext({ userId: ctx.selectedAccount }).catch(() => {
319
+ // Non-fatal
320
+ });
321
+ }
322
+ /** Record token usage and cost on the root span. */
323
+ setUsage(ctx) {
324
+ this.usage = ctx;
325
+ const totalTokens = ctx.inputTokens +
326
+ ctx.outputTokens +
327
+ ctx.cacheCreationTokens +
328
+ ctx.cacheReadTokens +
329
+ (ctx.reasoningTokens ?? 0);
330
+ // NeuroLink-format token attributes (from SpanAttributes)
331
+ this.rootSpan.setAttributes({
332
+ "ai.tokens.input": ctx.inputTokens,
333
+ "ai.tokens.output": ctx.outputTokens,
334
+ "ai.tokens.total": totalTokens,
335
+ "ai.tokens.cache_creation": ctx.cacheCreationTokens,
336
+ "ai.tokens.cache_read": ctx.cacheReadTokens,
337
+ });
338
+ if (ctx.reasoningTokens !== undefined) {
339
+ this.rootSpan.setAttribute("ai.tokens.reasoning", ctx.reasoningTokens);
340
+ }
341
+ // GenAI semantic convention attributes (for Langfuse compatibility)
342
+ this.rootSpan.setAttributes({
343
+ "gen_ai.usage.input_tokens": ctx.inputTokens,
344
+ "gen_ai.usage.output_tokens": ctx.outputTokens,
345
+ "gen_ai.usage.total_tokens": totalTokens,
346
+ });
347
+ // Cost calculation via pricing.ts
348
+ const cost = calculateCost("anthropic", this.model, {
349
+ input: ctx.inputTokens,
350
+ output: ctx.outputTokens,
351
+ total: totalTokens,
352
+ cacheCreationTokens: ctx.cacheCreationTokens,
353
+ cacheReadTokens: ctx.cacheReadTokens,
354
+ });
355
+ if (cost > 0) {
356
+ this.rootSpan.setAttributes({
357
+ "ai.cost.total": cost,
358
+ "ai.cost.currency": "USD",
359
+ });
360
+ }
361
+ // Rate-limit utilisation after the request
362
+ if (ctx.rateLimitAfter5h !== undefined) {
363
+ this.rootSpan.setAttribute("proxy.ratelimit.after.5h", ctx.rateLimitAfter5h);
364
+ }
365
+ if (ctx.rateLimitAfter7d !== undefined) {
366
+ this.rootSpan.setAttribute("proxy.ratelimit.after.7d", ctx.rateLimitAfter7d);
367
+ }
368
+ }
369
+ /** Record an error on the root span. */
370
+ setError(errorType, errorMessage) {
371
+ this.rootSpan.setAttributes({
372
+ "error.type": errorType,
373
+ "error.message": errorMessage,
374
+ error: true,
375
+ });
376
+ }
377
+ /** Record whether the request was handled in full or passthrough mode. */
378
+ setMode(mode) {
379
+ this.mode = mode;
380
+ this.rootSpan.setAttribute("proxy.mode", mode);
381
+ }
382
+ /**
383
+ * Record that the proxy substituted a different model than was requested.
384
+ * Sets span attributes and increments the substitution metric counter.
385
+ */
386
+ setModelSubstitution(requestedModel, actualModel) {
387
+ this.rootSpan.setAttributes({
388
+ "proxy.model_substituted": true,
389
+ "proxy.original_model": requestedModel,
390
+ "proxy.actual_model": actualModel,
391
+ "gen_ai.response.model": actualModel,
392
+ });
393
+ const m = getProxyMetrics();
394
+ m.modelSubstitutionTotal.add(1, {
395
+ requested_model: requestedModel,
396
+ actual_model: actualModel,
397
+ });
398
+ }
399
+ // -------------------------------------------------------------------------
400
+ // Log payloads as span events
401
+ // -------------------------------------------------------------------------
402
+ /** Log the incoming client request body (redacted). */
403
+ logRequestBody(body) {
404
+ if (!BODY_LOGGING_ENABLED) {
405
+ this.rootSpan.addEvent("proxy.client.request_body", {
406
+ "proxy.body.size": body.length,
407
+ "proxy.body.logged": false,
408
+ });
409
+ return;
410
+ }
411
+ this.rootSpan.addEvent("proxy.client.request_body", {
412
+ ...buildBodyEventAttributes(body),
413
+ });
414
+ }
415
+ /** Log the incoming client request headers (redacted). */
416
+ logRequestHeaders(headers) {
417
+ this.rootSpan.addEvent("proxy.client.request_headers", {
418
+ "proxy.headers": JSON.stringify(redactHeaders(headers)),
419
+ });
420
+ }
421
+ /** Log the upstream request body (redacted, as sent to Anthropic). */
422
+ logUpstreamRequestBody(body) {
423
+ if (!BODY_LOGGING_ENABLED) {
424
+ this.rootSpan.addEvent("proxy.upstream.request_body", {
425
+ "proxy.body.size": body.length,
426
+ "proxy.body.logged": false,
427
+ });
428
+ return;
429
+ }
430
+ this.rootSpan.addEvent("proxy.upstream.request_body", {
431
+ ...buildBodyEventAttributes(body),
432
+ });
433
+ }
434
+ /** Log the upstream request headers (redacted). */
435
+ logUpstreamRequestHeaders(headers) {
436
+ this.rootSpan.addEvent("proxy.upstream.request_headers", {
437
+ "proxy.headers": JSON.stringify(redactHeaders(headers)),
438
+ });
439
+ }
440
+ /** Log the upstream response headers (redacted). */
441
+ logUpstreamResponseHeaders(headers) {
442
+ this.rootSpan.addEvent("proxy.upstream.response_headers", {
443
+ "proxy.headers": JSON.stringify(redactHeaders(headers)),
444
+ });
445
+ }
446
+ /** Log the upstream response body (redacted). */
447
+ logUpstreamResponseBody(body) {
448
+ if (!BODY_LOGGING_ENABLED) {
449
+ this.rootSpan.addEvent("proxy.upstream.response_body", {
450
+ "proxy.body.size": body.length,
451
+ "proxy.body.logged": false,
452
+ });
453
+ return;
454
+ }
455
+ this.rootSpan.addEvent("proxy.upstream.response_body", {
456
+ ...buildBodyEventAttributes(body),
457
+ });
458
+ }
459
+ /** Log SSE stream events (each event has type, timestamp, data). */
460
+ logStreamEvents(events) {
461
+ if (!BODY_LOGGING_ENABLED) {
462
+ this.rootSpan.addEvent("proxy.stream.events", {
463
+ "proxy.stream.event_count": events.length,
464
+ "proxy.body.logged": false,
465
+ });
466
+ return;
467
+ }
468
+ const truncated = events.length > MAX_STREAM_EVENTS_TO_LOG;
469
+ const redactedEvents = events
470
+ .slice(0, MAX_STREAM_EVENTS_TO_LOG)
471
+ .map((event) => ({
472
+ ...event,
473
+ data: event.data
474
+ ? redactBodyForLogging(event.data, MAX_BODY_LOG_SIZE)
475
+ : "",
476
+ }));
477
+ if (truncated) {
478
+ redactedEvents.push({
479
+ type: "truncated",
480
+ timestamp: Date.now(),
481
+ data: "…[truncated]",
482
+ });
483
+ }
484
+ this.rootSpan.addEvent("proxy.stream.events", {
485
+ "proxy.stream.event_count": events.length,
486
+ "proxy.stream.events": JSON.stringify(redactedEvents),
487
+ "proxy.body.logged": true,
488
+ "proxy.body.truncated": truncated,
489
+ });
490
+ }
491
+ // -------------------------------------------------------------------------
492
+ // Metric recording helpers
493
+ // -------------------------------------------------------------------------
494
+ /** Record an upstream retry attempt. */
495
+ recordRetry(account, reason) {
496
+ const m = getProxyMetrics();
497
+ m.retriesTotal.add(1, {
498
+ model: this.model,
499
+ account,
500
+ reason,
501
+ });
502
+ }
503
+ /** Record request and/or response body sizes for bandwidth tracking. */
504
+ recordBodySizes(requestBytes, responseBytes) {
505
+ const m = getProxyMetrics();
506
+ const labels = {
507
+ model: this.model,
508
+ account: this.accountEmail ?? "unknown",
509
+ };
510
+ if (requestBytes !== undefined && requestBytes > 0) {
511
+ m.requestBodySize.record(requestBytes, labels);
512
+ }
513
+ if (responseBytes !== undefined && responseBytes > 0) {
514
+ m.responseBodySize.record(responseBytes, labels);
515
+ }
516
+ }
517
+ // -------------------------------------------------------------------------
518
+ // Context accessors
519
+ // -------------------------------------------------------------------------
520
+ /** Return the OTel trace/span IDs for this request (for log correlation). */
521
+ getTraceContext() {
522
+ const spanCtx = this.rootSpan.spanContext();
523
+ return {
524
+ traceId: spanCtx.traceId,
525
+ spanId: spanCtx.spanId,
526
+ };
527
+ }
528
+ /** Return the captured usage (set by setUsage). */
529
+ getUsage() {
530
+ return this.usage;
531
+ }
532
+ // -------------------------------------------------------------------------
533
+ // Lifecycle
534
+ // -------------------------------------------------------------------------
535
+ /** End the root span with final HTTP status and duration, and emit OTEL metrics. */
536
+ end(responseStatus, durationMs) {
537
+ this.rootSpan.setAttributes({
538
+ "http.status_code": responseStatus,
539
+ "proxy.duration_ms": durationMs,
540
+ "proxy.mode": this.mode,
541
+ ...(this.accountEmail
542
+ ? { "proxy.account": this.accountEmail }
543
+ : undefined),
544
+ });
545
+ if (responseStatus >= 400) {
546
+ this.rootSpan.setStatus({
547
+ code: SpanStatusCode.ERROR,
548
+ message: `HTTP ${responseStatus}`,
549
+ });
550
+ }
551
+ else {
552
+ this.rootSpan.setStatus({ code: SpanStatusCode.OK });
553
+ }
554
+ this.rootSpan.end();
555
+ // ---- Emit OTEL metrics (lazy-init instruments) ----
556
+ const m = getProxyMetrics();
557
+ const labels = {
558
+ model: this.model,
559
+ account: this.accountEmail ?? "unknown",
560
+ status: String(responseStatus),
561
+ stream: String(this.isStream),
562
+ mode: this.mode,
563
+ };
564
+ m.requestsTotal.add(1, labels);
565
+ m.requestDuration.record(durationMs, labels);
566
+ // Token metrics (only if usage was captured)
567
+ if (this.usage) {
568
+ const tokenLabels = {
569
+ model: this.model,
570
+ account: this.accountEmail ?? "unknown",
571
+ };
572
+ m.tokensInput.add(this.usage.inputTokens, tokenLabels);
573
+ m.tokensOutput.add(this.usage.outputTokens, tokenLabels);
574
+ m.tokensCacheRead.add(this.usage.cacheReadTokens, tokenLabels);
575
+ m.tokensCacheCreation.add(this.usage.cacheCreationTokens, tokenLabels);
576
+ if (this.usage.reasoningTokens) {
577
+ m.tokensReasoning.add(this.usage.reasoningTokens, tokenLabels);
578
+ }
579
+ // Cost
580
+ const totalTokens = this.usage.inputTokens +
581
+ this.usage.outputTokens +
582
+ this.usage.cacheCreationTokens +
583
+ this.usage.cacheReadTokens +
584
+ (this.usage.reasoningTokens ?? 0);
585
+ const cost = calculateCost("anthropic", this.model, {
586
+ input: this.usage.inputTokens,
587
+ output: this.usage.outputTokens,
588
+ total: totalTokens,
589
+ cacheCreationTokens: this.usage.cacheCreationTokens,
590
+ cacheReadTokens: this.usage.cacheReadTokens,
591
+ });
592
+ if (cost > 0) {
593
+ m.costTotal.add(cost, tokenLabels);
594
+ }
595
+ }
596
+ // Error metrics
597
+ if (responseStatus >= 400) {
598
+ const errorType = responseStatus === 429
599
+ ? "rate_limit"
600
+ : responseStatus === 401
601
+ ? "auth"
602
+ : responseStatus >= 500
603
+ ? "server"
604
+ : "client";
605
+ m.errorsTotal.add(1, {
606
+ model: this.model,
607
+ account: this.accountEmail ?? "unknown",
608
+ error_type: errorType,
609
+ status: String(responseStatus),
610
+ });
611
+ }
612
+ }
613
+ /** Record metrics via TelemetryService (call after setUsage). */
614
+ recordMetrics() {
615
+ if (!this.usage) {
616
+ return;
617
+ }
618
+ const totalTokens = this.usage.inputTokens +
619
+ this.usage.outputTokens +
620
+ this.usage.cacheCreationTokens +
621
+ this.usage.cacheReadTokens +
622
+ (this.usage.reasoningTokens ?? 0);
623
+ const durationMs = Date.now() - this.startTime;
624
+ const cost = calculateCost("anthropic", this.model, {
625
+ input: this.usage.inputTokens,
626
+ output: this.usage.outputTokens,
627
+ total: totalTokens,
628
+ cacheCreationTokens: this.usage.cacheCreationTokens,
629
+ cacheReadTokens: this.usage.cacheReadTokens,
630
+ });
631
+ TelemetryService.getInstance().recordAIRequest("anthropic", this.model, totalTokens, durationMs, cost > 0 ? cost : undefined);
632
+ }
633
+ // -------------------------------------------------------------------------
634
+ // Context propagation
635
+ // -------------------------------------------------------------------------
636
+ /**
637
+ * Get trace context headers for propagation to the upstream Anthropic request.
638
+ * Injects the current trace's `traceparent` / `tracestate` into a new header map.
639
+ */
640
+ getTraceHeaders() {
641
+ return this.bridge.injectContext({}, trace.setSpan(context.active(), this.rootSpan));
642
+ }
643
+ }
644
+ export { ProxyTracer };
@@ -0,0 +1,10 @@
1
+ export type RawStreamCapture = {
2
+ totalBytes: number;
3
+ text: string;
4
+ truncated: boolean;
5
+ };
6
+ export type RawStreamCaptureResult = {
7
+ stream: TransformStream<Uint8Array, Uint8Array>;
8
+ capture: Promise<RawStreamCapture>;
9
+ };
10
+ export declare function createRawStreamCapture(): RawStreamCaptureResult;