@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
@@ -1,6 +1,6 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
2
3
  import { embed, embedMany, NoOutputGeneratedError, stepCountIs, streamText, } from "ai";
3
- import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";
4
4
  import { AIProviderName } from "../constants/enums.js";
5
5
  import { BaseProvider } from "../core/baseProvider.js";
6
6
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
@@ -12,6 +12,7 @@ import { calculateCost } from "../utils/pricing.js";
12
12
  import { createOpenAIConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js";
13
13
  import { isZodSchema } from "../utils/schemaConversion.js";
14
14
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
15
+ import { resolveToolChoice } from "../utils/toolChoice.js";
15
16
  import { getModelId } from "./providerTypeUtils.js";
16
17
  /**
17
18
  * Retrieve a tool's schema, handling both AI SDK v6 (`inputSchema`) and
@@ -278,6 +279,16 @@ export class OpenAIProvider extends BaseProvider {
278
279
  // Build message array from options with multimodal support
279
280
  // Using protected helper from BaseProvider to eliminate code duplication
280
281
  const messages = await this.buildMessagesForStream(options);
282
+ let resolvedToolChoice = resolveToolChoice(options, tools, shouldUseTools);
283
+ // Guard: if toolChoice names a specific tool that was filtered out, fall back to "auto"
284
+ if (resolvedToolChoice !== null &&
285
+ typeof resolvedToolChoice === "object" &&
286
+ "toolName" in resolvedToolChoice &&
287
+ typeof resolvedToolChoice.toolName === "string" &&
288
+ !tools[resolvedToolChoice.toolName]) {
289
+ logger.warn(`OpenAI: toolChoice references tool "${resolvedToolChoice.toolName}" which was removed during filtering; falling back to "auto"`);
290
+ resolvedToolChoice = "auto";
291
+ }
281
292
  // Debug the actual request being sent to OpenAI
282
293
  logger.debug(`OpenAI: streamText request parameters:`, {
283
294
  modelName: this.modelName,
@@ -285,7 +296,7 @@ export class OpenAIProvider extends BaseProvider {
285
296
  temperature: options.temperature,
286
297
  maxTokens: options.maxTokens,
287
298
  toolsCount: Object.keys(tools).length,
288
- toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
299
+ toolChoice: resolvedToolChoice,
289
300
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
290
301
  firstToolExample: Object.keys(tools).length > 0
291
302
  ? {
@@ -314,7 +325,7 @@ export class OpenAIProvider extends BaseProvider {
314
325
  maxRetries: 0, // NL11: Disable AI SDK's invisible internal retries; we handle retries with OTel instrumentation
315
326
  tools,
316
327
  stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
317
- toolChoice: shouldUseTools && Object.keys(tools).length > 0 ? "auto" : "none",
328
+ toolChoice: resolvedToolChoice,
318
329
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
319
330
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
320
331
  onStepFinish: ({ toolCalls, toolResults }) => {
@@ -381,150 +392,14 @@ export class OpenAIProvider extends BaseProvider {
381
392
  hasToolResults: !!result.toolResults,
382
393
  resultType: typeof result,
383
394
  });
384
- // Transform string stream to content object stream using fullStream
385
- const transformedStream = async function* () {
386
- try {
387
- logger.debug(`OpenAI: Starting stream transformation`, {
388
- hasTextStream: !!result.textStream,
389
- hasFullStream: !!result.fullStream,
390
- resultKeys: Object.keys(result),
391
- toolsEnabled: shouldUseTools,
392
- toolsCount: Object.keys(tools).length,
393
- });
394
- let chunkCount = 0;
395
- let contentYielded = 0;
396
- // Try fullStream first (handles both text and tool calls), fallback to textStream
397
- const streamToUse = result.fullStream || result.textStream;
398
- if (!streamToUse) {
399
- logger.error("OpenAI: No stream available in result", {
400
- resultKeys: Object.keys(result),
401
- });
402
- return;
403
- }
404
- logger.debug(`OpenAI: Stream source selected:`, {
405
- usingFullStream: !!result.fullStream,
406
- usingTextStream: !!result.textStream && !result.fullStream,
407
- streamSourceType: result.fullStream ? "fullStream" : "textStream",
408
- });
409
- for await (const chunk of streamToUse) {
410
- chunkCount++;
411
- logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
412
- chunkType: typeof chunk,
413
- chunkValue: typeof chunk === "string"
414
- ? chunk.substring(0, 50)
415
- : "not-string",
416
- chunkKeys: chunk && typeof chunk === "object"
417
- ? Object.keys(chunk)
418
- : "not-object",
419
- hasText: chunk && typeof chunk === "object" && "text" in chunk,
420
- hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
421
- hasType: chunk && typeof chunk === "object" && "type" in chunk,
422
- chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
423
- ? chunk.type
424
- : "no-type",
425
- });
426
- let contentToYield = null;
427
- // Handle different chunk types from fullStream
428
- if (chunk && typeof chunk === "object") {
429
- // Log the full chunk structure for debugging (debug mode only)
430
- if (process.env.NEUROLINK_DEBUG === "true") {
431
- logger.debug(`OpenAI: Full chunk structure:`, {
432
- chunkKeys: Object.keys(chunk),
433
- fullChunk: JSON.stringify(chunk).substring(0, 500),
434
- });
435
- }
436
- if ("type" in chunk && chunk.type === "error") {
437
- // Handle error chunks when tools are enabled
438
- const errorChunk = chunk;
439
- logger.error(`OpenAI: Error chunk received:`, {
440
- errorType: errorChunk.type,
441
- errorDetails: errorChunk.error,
442
- fullChunk: JSON.stringify(chunk),
443
- });
444
- // Throw a more descriptive error for tool-related issues
445
- const errorMessage = errorChunk.error &&
446
- typeof errorChunk.error === "object" &&
447
- "message" in errorChunk.error
448
- ? String(errorChunk.error.message)
449
- : "OpenAI API error when tools are enabled";
450
- throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
451
- }
452
- else if ("type" in chunk &&
453
- chunk.type === "text-delta" &&
454
- "textDelta" in chunk) {
455
- // Text delta from fullStream
456
- contentToYield = chunk.textDelta;
457
- logger.debug(`OpenAI: Found text-delta:`, {
458
- textDelta: contentToYield,
459
- });
460
- }
461
- else if ("text" in chunk) {
462
- // Direct text chunk
463
- contentToYield = chunk.text;
464
- logger.debug(`OpenAI: Found direct text:`, {
465
- text: contentToYield,
466
- });
467
- }
468
- else {
469
- // Log unhandled chunks in debug mode only
470
- if (process.env.NEUROLINK_DEBUG === "true") {
471
- logger.debug(`OpenAI: Unhandled object chunk:`, {
472
- chunkKeys: Object.keys(chunk),
473
- chunkType: chunk.type || "no-type",
474
- fullChunk: JSON.stringify(chunk).substring(0, 500),
475
- });
476
- }
477
- }
478
- }
479
- else if (typeof chunk === "string") {
480
- // Direct string chunk from textStream
481
- contentToYield = chunk;
482
- logger.debug(`OpenAI: Found string chunk:`, {
483
- content: contentToYield,
484
- });
485
- }
486
- else {
487
- logger.warn(`OpenAI: Unhandled chunk type:`, {
488
- type: typeof chunk,
489
- value: String(chunk).substring(0, 100),
490
- });
491
- }
492
- if (contentToYield) {
493
- contentYielded++;
494
- logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
495
- content: contentToYield.substring(0, 50),
496
- length: contentToYield.length,
497
- });
498
- yield { content: contentToYield };
499
- }
500
- }
501
- logger.debug(`OpenAI: Stream transformation completed`, {
502
- totalChunks: chunkCount,
503
- contentYielded,
504
- success: contentYielded > 0,
505
- });
506
- if (contentYielded === 0) {
507
- logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
508
- }
509
- }
510
- catch (streamError) {
511
- // AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
512
- // Treat as an empty stream rather than crashing with an unhandled rejection.
513
- if (NoOutputGeneratedError.isInstance(streamError)) {
514
- logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
515
- return;
516
- }
517
- logger.error(`OpenAI: Stream transformation error:`, streamError);
518
- throw streamError;
519
- }
520
- };
395
+ const transformedStream = this.createOpenAITransformedStream(result, shouldUseTools, tools);
521
396
  // Create analytics promise that resolves after stream completion
522
397
  const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
523
398
  requestId: `openai-stream-${Date.now()}`,
524
399
  streamingMode: true,
525
400
  });
526
401
  return {
527
- stream: transformedStream(),
402
+ stream: transformedStream,
528
403
  provider: this.providerName,
529
404
  model: this.modelName,
530
405
  analytics: analyticsPromise,
@@ -539,6 +414,131 @@ export class OpenAIProvider extends BaseProvider {
539
414
  throw this.handleProviderError(error);
540
415
  }
541
416
  }
417
+ async *createOpenAITransformedStream(result, shouldUseTools, tools) {
418
+ try {
419
+ logger.debug(`OpenAI: Starting stream transformation`, {
420
+ hasTextStream: !!result.textStream,
421
+ hasFullStream: !!result.fullStream,
422
+ resultKeys: Object.keys(result),
423
+ toolsEnabled: shouldUseTools,
424
+ toolsCount: Object.keys(tools).length,
425
+ });
426
+ let chunkCount = 0;
427
+ let contentYielded = 0;
428
+ const streamToUse = result.fullStream || result.textStream;
429
+ if (!streamToUse) {
430
+ logger.error("OpenAI: No stream available in result", {
431
+ resultKeys: Object.keys(result),
432
+ });
433
+ return;
434
+ }
435
+ logger.debug(`OpenAI: Stream source selected:`, {
436
+ usingFullStream: !!result.fullStream,
437
+ usingTextStream: !!result.textStream && !result.fullStream,
438
+ streamSourceType: result.fullStream ? "fullStream" : "textStream",
439
+ });
440
+ for await (const chunk of streamToUse) {
441
+ chunkCount++;
442
+ logger.debug(`OpenAI: Processing chunk ${chunkCount}:`, {
443
+ chunkType: typeof chunk,
444
+ chunkValue: typeof chunk === "string"
445
+ ? chunk.substring(0, 50)
446
+ : "not-string",
447
+ chunkKeys: chunk && typeof chunk === "object"
448
+ ? Object.keys(chunk)
449
+ : "not-object",
450
+ hasText: chunk && typeof chunk === "object" && "text" in chunk,
451
+ hasTextDelta: chunk && typeof chunk === "object" && "textDelta" in chunk,
452
+ hasType: chunk && typeof chunk === "object" && "type" in chunk,
453
+ chunkTypeValue: chunk && typeof chunk === "object" && "type" in chunk
454
+ ? chunk.type
455
+ : "no-type",
456
+ });
457
+ const contentToYield = this.extractOpenAIChunkContent(chunk);
458
+ if (contentToYield) {
459
+ contentYielded++;
460
+ logger.debug(`OpenAI: Yielding content ${contentYielded}:`, {
461
+ content: contentToYield.substring(0, 50),
462
+ length: contentToYield.length,
463
+ });
464
+ yield { content: contentToYield };
465
+ }
466
+ }
467
+ logger.debug(`OpenAI: Stream transformation completed`, {
468
+ totalChunks: chunkCount,
469
+ contentYielded,
470
+ success: contentYielded > 0,
471
+ });
472
+ if (contentYielded === 0) {
473
+ logger.warn(`OpenAI: No content was yielded from stream despite processing ${chunkCount} chunks`);
474
+ }
475
+ }
476
+ catch (streamError) {
477
+ if (NoOutputGeneratedError.isInstance(streamError)) {
478
+ logger.warn("OpenAI: Stream produced no output (NoOutputGeneratedError)");
479
+ return;
480
+ }
481
+ logger.error(`OpenAI: Stream transformation error:`, streamError);
482
+ throw streamError;
483
+ }
484
+ }
485
+ extractOpenAIChunkContent(chunk) {
486
+ if (chunk && typeof chunk === "object") {
487
+ if (process.env.NEUROLINK_DEBUG === "true") {
488
+ logger.debug(`OpenAI: Full chunk structure:`, {
489
+ chunkKeys: Object.keys(chunk),
490
+ fullChunk: JSON.stringify(chunk).substring(0, 500),
491
+ });
492
+ }
493
+ if ("type" in chunk && chunk.type === "error") {
494
+ const errorChunk = chunk;
495
+ logger.error(`OpenAI: Error chunk received:`, {
496
+ errorType: errorChunk.type,
497
+ errorDetails: errorChunk.error,
498
+ fullChunk: JSON.stringify(chunk),
499
+ });
500
+ const errorMessage = errorChunk.error &&
501
+ typeof errorChunk.error === "object" &&
502
+ "message" in errorChunk.error
503
+ ? String(errorChunk.error.message)
504
+ : "OpenAI API error when tools are enabled";
505
+ throw new Error(`OpenAI streaming error with tools: ${errorMessage}. Try disabling tools with --disableTools`);
506
+ }
507
+ if ("type" in chunk &&
508
+ chunk.type === "text-delta" &&
509
+ "textDelta" in chunk) {
510
+ const textDelta = chunk.textDelta;
511
+ logger.debug(`OpenAI: Found text-delta:`, { textDelta });
512
+ return textDelta;
513
+ }
514
+ if ("text" in chunk) {
515
+ const text = chunk.text;
516
+ logger.debug(`OpenAI: Found direct text:`, { text });
517
+ return text;
518
+ }
519
+ if (process.env.NEUROLINK_DEBUG === "true") {
520
+ logger.debug(`OpenAI: Unhandled object chunk:`, {
521
+ chunkKeys: Object.keys(chunk),
522
+ chunkType: "type" in chunk
523
+ ? String(chunk.type)
524
+ : "no-type",
525
+ fullChunk: JSON.stringify(chunk).substring(0, 500),
526
+ });
527
+ }
528
+ return null;
529
+ }
530
+ if (typeof chunk === "string") {
531
+ logger.debug(`OpenAI: Found string chunk:`, {
532
+ content: chunk,
533
+ });
534
+ return chunk;
535
+ }
536
+ logger.warn(`OpenAI: Unhandled chunk type:`, {
537
+ type: typeof chunk,
538
+ value: String(chunk).substring(0, 100),
539
+ });
540
+ return null;
541
+ }
542
542
  /**
543
543
  * Generate embeddings for text using OpenAI text-embedding models
544
544
  * @param text - The text to embed
@@ -9,6 +9,7 @@ import { isAbortError } from "../utils/errorHandling.js";
9
9
  import { logger } from "../utils/logger.js";
10
10
  import { getProviderModel } from "../utils/providerConfig.js";
11
11
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
12
+ import { resolveToolChoice } from "../utils/toolChoice.js";
12
13
  // Constants
13
14
  const MODELS_DISCOVERY_TIMEOUT_MS = 5000; // 5 seconds for model discovery
14
15
  // Configuration helpers
@@ -234,7 +235,7 @@ export class OpenRouterProvider extends BaseProvider {
234
235
  ...(shouldUseTools &&
235
236
  Object.keys(tools).length > 0 && {
236
237
  tools,
237
- toolChoice: "auto",
238
+ toolChoice: resolveToolChoice(options, tools, shouldUseTools),
238
239
  maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
239
240
  }),
240
241
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
@@ -1,8 +1,8 @@
1
- import { type Schema, type LanguageModel } from "ai";
2
- import type { ZodUnknownSchema } from "../types/typeAliases.js";
3
- import { AIProviderName } from "../constants/enums.js";
4
- import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
1
+ import { type LanguageModel, type Schema } from "ai";
2
+ import type { AIProviderName } from "../constants/enums.js";
5
3
  import { BaseProvider } from "../core/baseProvider.js";
4
+ import type { StreamOptions, StreamResult } from "../types/streamTypes.js";
5
+ import type { ZodUnknownSchema } from "../types/typeAliases.js";
6
6
  /**
7
7
  * OpenAI Compatible Provider - BaseProvider Implementation
8
8
  * Provides access to one of the OpenAI-compatible endpoint (OpenRouter, vLLM, LiteLLM, etc.)
@@ -1,11 +1,11 @@
1
1
  import { createOpenAI } from "@ai-sdk/openai";
2
2
  import { NoOutputGeneratedError, streamText, } from "ai";
3
- import { AIProviderName } from "../constants/enums.js";
4
3
  import { BaseProvider } from "../core/baseProvider.js";
5
- import { logger } from "../utils/logger.js";
6
- import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
7
4
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
8
5
  import { createProxyFetch } from "../proxy/proxyFetch.js";
6
+ import { logger } from "../utils/logger.js";
7
+ import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
8
+ import { resolveToolChoice } from "../utils/toolChoice.js";
9
9
  import { toAnalyticsStreamResult } from "./providerTypeUtils.js";
10
10
  // Constants
11
11
  const FALLBACK_OPENAI_COMPATIBLE_MODEL = "gpt-3.5-turbo";
@@ -178,7 +178,7 @@ export class OpenAICompatibleProvider extends BaseProvider {
178
178
  ? { temperature: options.temperature }
179
179
  : {}),
180
180
  tools,
181
- toolChoice: shouldUseTools ? "auto" : "none",
181
+ toolChoice: resolveToolChoice(options, tools, shouldUseTools),
182
182
  abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
183
183
  experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
184
184
  onStepFinish: (event) => {
@@ -74,6 +74,7 @@ export declare function formatSSE(eventType: string, data: unknown): string;
74
74
  export declare class ClaudeStreamSerializer {
75
75
  private state;
76
76
  private currentBlockType;
77
+ private sawToolUseBlock;
77
78
  private blockIndex;
78
79
  private hasOpenedBlock;
79
80
  private outputTokens;
@@ -107,8 +108,8 @@ export declare class ClaudeStreamSerializer {
107
108
  */
108
109
  private openBlock;
109
110
  /**
110
- * Emit the opening frames: message_start, ping, content_block_start (text).
111
- * Automatically called on the first pushDelta if not called manually.
111
+ * Emit the opening frames: message_start and ping.
112
+ * The first actual content decides which content block opens next.
112
113
  */
113
114
  start(): Generator<string>;
114
115
  /**
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * Reference: https://docs.anthropic.com/en/api/messages
10
10
  */
11
+ import { jsonSchema, tool } from "ai";
11
12
  import { randomBytes } from "crypto";
12
- import { jsonSchema } from "ai";
13
13
  // ---------------------------------------------------------------------------
14
14
  // Helpers
15
15
  // ---------------------------------------------------------------------------
@@ -148,13 +148,13 @@ export function parseClaudeRequest(body) {
148
148
  const tools = {};
149
149
  if (body.tools) {
150
150
  for (const t of body.tools) {
151
- tools[t.name] = {
151
+ tools[t.name] = tool({
152
152
  description: t.description ?? "",
153
- // Wrap raw JSON schema with AI SDK's jsonSchema() so the SDK
154
- // recognizes it (it checks for Symbol.for("vercel.ai.schema")).
155
- // Without this, the SDK tries zodSchema() on raw JSON and crashes.
156
- parameters: jsonSchema(t.input_schema ?? { type: "object" }),
157
- };
153
+ // Fallback providers consume AI SDK-style tools, not Claude wire-format
154
+ // tool descriptors. Wrap the raw JSON schema once here so every
155
+ // downstream provider sees a canonical `inputSchema` shape.
156
+ inputSchema: jsonSchema(t.input_schema ?? { type: "object" }),
157
+ });
158
158
  }
159
159
  }
160
160
  // --- tool_choice ---
@@ -239,6 +239,11 @@ function mapStopReason(finishReason) {
239
239
  */
240
240
  export function serializeClaudeResponse(result, requestModel) {
241
241
  const content = [];
242
+ const inferredFinishReason = result.toolCalls &&
243
+ result.toolCalls.length > 0 &&
244
+ (!result.finishReason || result.finishReason === "stop")
245
+ ? "tool_use"
246
+ : result.finishReason;
242
247
  // Thinking/reasoning content block (if present)
243
248
  if (result.reasoning) {
244
249
  content.push({ type: "thinking", thinking: result.reasoning });
@@ -250,11 +255,15 @@ export function serializeClaudeResponse(result, requestModel) {
250
255
  // Tool use blocks — normalize IDs to Claude `toolu_` format
251
256
  if (result.toolCalls && result.toolCalls.length > 0) {
252
257
  for (const tc of result.toolCalls) {
258
+ const toolInput = tc.args ??
259
+ tc.parameters ??
260
+ tc.input ??
261
+ {};
253
262
  content.push({
254
263
  type: "tool_use",
255
264
  id: generateToolUseId(),
256
265
  name: tc.toolName,
257
- input: tc.args,
266
+ input: toolInput,
258
267
  });
259
268
  }
260
269
  }
@@ -268,7 +277,7 @@ export function serializeClaudeResponse(result, requestModel) {
268
277
  role: "assistant",
269
278
  content,
270
279
  model: result.model ?? requestModel,
271
- stop_reason: mapStopReason(result.finishReason),
280
+ stop_reason: mapStopReason(inferredFinishReason),
272
281
  stop_sequence: null,
273
282
  usage: {
274
283
  input_tokens: result.usage?.input ?? 0,
@@ -363,6 +372,7 @@ export function formatSSE(eventType, data) {
363
372
  export class ClaudeStreamSerializer {
364
373
  state = "idle";
365
374
  currentBlockType = null;
375
+ sawToolUseBlock = false;
366
376
  blockIndex = 0;
367
377
  hasOpenedBlock = false;
368
378
  outputTokens = 0;
@@ -465,15 +475,14 @@ export class ClaudeStreamSerializer {
465
475
  // Public API
466
476
  // -----------------------------------------------------------------------
467
477
  /**
468
- * Emit the opening frames: message_start, ping, content_block_start (text).
469
- * Automatically called on the first pushDelta if not called manually.
478
+ * Emit the opening frames: message_start and ping.
479
+ * The first actual content decides which content block opens next.
470
480
  */
471
481
  *start() {
472
482
  if (this.state !== "idle") {
473
483
  return;
474
484
  }
475
485
  yield* this.ensureMessageStarted();
476
- yield* this.openBlock({ type: "text", text: "" });
477
486
  }
478
487
  /**
479
488
  * Push a text delta. Returns zero or more SSE frames.
@@ -529,6 +538,7 @@ export class ClaudeStreamSerializer {
529
538
  if (this.state === "done" || this.state === "error") {
530
539
  return;
531
540
  }
541
+ this.sawToolUseBlock = true;
532
542
  yield* this.ensureMessageStarted();
533
543
  // Open a tool_use block (closes any current block)
534
544
  yield* this.openBlock({ type: "tool_use", id, name, input: "" });
@@ -562,19 +572,22 @@ export class ClaudeStreamSerializer {
562
572
  *finish(outputTokens, finishReason) {
563
573
  // If we never started (empty response), start first
564
574
  if (this.state === "idle") {
565
- yield* this.start();
575
+ yield* this.ensureMessageStarted();
566
576
  }
567
577
  if (this.state === "done" || this.state === "error") {
568
578
  return;
569
579
  }
570
580
  this.outputTokens = outputTokens ?? this.outputTokens;
581
+ const resolvedFinishReason = this.sawToolUseBlock && (!finishReason || finishReason === "stop")
582
+ ? "tool_use"
583
+ : finishReason;
571
584
  // Close any open content block
572
585
  yield* this.closeCurrentBlock();
573
586
  // message_delta
574
587
  const messageDelta = {
575
588
  type: "message_delta",
576
589
  delta: {
577
- stop_reason: mapStopReason(finishReason),
590
+ stop_reason: mapStopReason(resolvedFinishReason),
578
591
  stop_sequence: null,
579
592
  },
580
593
  usage: { output_tokens: this.outputTokens },
@@ -3,13 +3,9 @@
3
3
  * so that Anthropic sees consistent "user" fingerprints even when requests are
4
4
  * spread across multiple accounts.
5
5
  *
6
- * Session IDs follow the format:
7
- * user_[32 hex chars]_account_[UUIDv4]_session_[UUIDv4]
8
- *
9
- * IDs are cached with a 1-hour TTL and reused for subsequent requests from
10
- * the same account within that window.
6
+ * The generated metadata matches Claude Code's shape:
7
+ * {"device_id":"<64 hex>","account_uuid":"<uuid>","session_id":"<uuid>"}
11
8
  */
12
9
  import type { CloakingPlugin } from "../../../types/index.js";
13
- /** Purge all expired sessions from the cache. Exported for external timer use. */
14
10
  export declare function purgeExpiredSessions(): void;
15
11
  export declare function createSessionIdentity(): CloakingPlugin;
@@ -3,29 +3,12 @@
3
3
  * so that Anthropic sees consistent "user" fingerprints even when requests are
4
4
  * spread across multiple accounts.
5
5
  *
6
- * Session IDs follow the format:
7
- * user_[32 hex chars]_account_[UUIDv4]_session_[UUIDv4]
8
- *
9
- * IDs are cached with a 1-hour TTL and reused for subsequent requests from
10
- * the same account within that window.
6
+ * The generated metadata matches Claude Code's shape:
7
+ * {"device_id":"<64 hex>","account_uuid":"<uuid>","session_id":"<uuid>"}
11
8
  */
12
- import { randomBytes, randomUUID } from "crypto";
13
- // ── Session cache with TTL ───────────────────────────────────────────────────
14
- const TTL_MS = 3_600_000; // 1 hour
15
- const sessionCache = new Map();
16
- /** Generate a new session user ID in the required format. */
17
- function generateUserId() {
18
- const hex = randomBytes(32).toString("hex"); // 64 hex chars, take first 32
19
- return `user_${hex.slice(0, 32)}_account_${randomUUID()}_session_${randomUUID()}`;
20
- }
21
- /** Purge all expired sessions from the cache. Exported for external timer use. */
9
+ import { getOrCreateClaudeCodeIdentity, purgeExpiredClaudeCodeIdentities, } from "../../../auth/anthropicOAuth.js";
22
10
  export function purgeExpiredSessions() {
23
- const now = Date.now();
24
- for (const [key, entry] of sessionCache) {
25
- if (entry.expiresAt <= now) {
26
- sessionCache.delete(key);
27
- }
28
- }
11
+ purgeExpiredClaudeCodeIdentities();
29
12
  }
30
13
  export function createSessionIdentity() {
31
14
  return {
@@ -34,23 +17,16 @@ export function createSessionIdentity() {
34
17
  enabled: true,
35
18
  async transformRequest(ctx) {
36
19
  const accountId = ctx.account.id;
37
- const now = Date.now();
38
- // Check cache first — reuse if still valid
39
- let cached = sessionCache.get(accountId);
40
- if (!cached || cached.expiresAt <= now) {
41
- cached = {
42
- userId: generateUserId(),
43
- expiresAt: now + TTL_MS,
44
- };
45
- sessionCache.set(accountId, cached);
46
- }
20
+ const identity = getOrCreateClaudeCodeIdentity(accountId, {
21
+ existingUserId: ctx.request.body.metadata?.user_id,
22
+ });
47
23
  const body = { ...ctx.request.body };
48
24
  // Only set user_id if not already present — in passthrough mode,
49
- // oauthFetch.ts owns this field and sets it from its own session cache.
25
+ // oauthFetch.ts owns this field and sets it from the shared helper.
50
26
  if (!body.metadata?.user_id) {
51
27
  body.metadata = {
52
28
  ...body.metadata,
53
- user_id: cached.userId,
29
+ user_id: identity.metadataUserId,
54
30
  };
55
31
  }
56
32
  return {
@@ -15,6 +15,9 @@ export class ModelRouter {
15
15
  if (this.passthrough.has(requestedModel)) {
16
16
  return { provider: "anthropic", model: requestedModel };
17
17
  }
18
+ if (requestedModel.startsWith("gemini-")) {
19
+ return { provider: "vertex", model: requestedModel };
20
+ }
18
21
  if (requestedModel.startsWith("claude-")) {
19
22
  return { provider: "anthropic", model: requestedModel };
20
23
  }
@@ -20,7 +20,7 @@ export { CLAUDE_CLI_USER_AGENT, MCP_TOOL_PREFIX };
20
20
  * - Sets User-Agent to Claude CLI
21
21
  * - Adds ?beta=true query parameter to /v1/messages
22
22
  * - Injects billing header & agent block into system prompt
23
- * - Injects fake user ID into metadata
23
+ * - Injects Claude-Code-shaped user ID into metadata
24
24
  * - Adds Stainless SDK headers for fingerprint matching
25
25
  * - Disables thinking when tool_choice is forced
26
26
  *