@juspay/neurolink 9.42.0 → 9.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/auth/anthropicOAuth.js +12 -0
  3. package/dist/browser/neurolink.min.js +335 -334
  4. package/dist/cli/commands/mcp.d.ts +6 -0
  5. package/dist/cli/commands/mcp.js +200 -184
  6. package/dist/cli/commands/proxy.js +560 -518
  7. package/dist/core/baseProvider.d.ts +6 -1
  8. package/dist/core/baseProvider.js +219 -232
  9. package/dist/core/factory.d.ts +3 -0
  10. package/dist/core/factory.js +140 -190
  11. package/dist/core/modules/ToolsManager.d.ts +1 -0
  12. package/dist/core/modules/ToolsManager.js +40 -42
  13. package/dist/core/toolEvents.d.ts +3 -0
  14. package/dist/core/toolEvents.js +7 -0
  15. package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
  16. package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
  17. package/dist/evaluation/scorers/scorerRegistry.js +356 -284
  18. package/dist/lib/auth/anthropicOAuth.js +12 -0
  19. package/dist/lib/core/baseProvider.d.ts +6 -1
  20. package/dist/lib/core/baseProvider.js +219 -232
  21. package/dist/lib/core/factory.d.ts +3 -0
  22. package/dist/lib/core/factory.js +140 -190
  23. package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
  24. package/dist/lib/core/modules/ToolsManager.js +40 -42
  25. package/dist/lib/core/toolEvents.d.ts +3 -0
  26. package/dist/lib/core/toolEvents.js +8 -0
  27. package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
  28. package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
  29. package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
  30. package/dist/lib/mcp/toolRegistry.d.ts +2 -0
  31. package/dist/lib/mcp/toolRegistry.js +32 -31
  32. package/dist/lib/neurolink.d.ts +38 -0
  33. package/dist/lib/neurolink.js +1890 -1707
  34. package/dist/lib/providers/googleAiStudio.js +0 -5
  35. package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
  36. package/dist/lib/providers/googleNativeGemini3.js +39 -1
  37. package/dist/lib/providers/googleVertex.d.ts +10 -0
  38. package/dist/lib/providers/googleVertex.js +445 -445
  39. package/dist/lib/providers/litellm.d.ts +1 -0
  40. package/dist/lib/providers/litellm.js +73 -64
  41. package/dist/lib/providers/ollama.js +17 -4
  42. package/dist/lib/providers/openAI.d.ts +2 -0
  43. package/dist/lib/providers/openAI.js +139 -140
  44. package/dist/lib/proxy/claudeFormat.js +14 -5
  45. package/dist/lib/proxy/oauthFetch.js +298 -318
  46. package/dist/lib/proxy/proxyConfig.js +3 -1
  47. package/dist/lib/proxy/proxyFetch.js +250 -222
  48. package/dist/lib/proxy/proxyHealth.d.ts +17 -0
  49. package/dist/lib/proxy/proxyHealth.js +55 -0
  50. package/dist/lib/proxy/requestLogger.js +140 -48
  51. package/dist/lib/proxy/routingPolicy.d.ts +33 -0
  52. package/dist/lib/proxy/routingPolicy.js +255 -0
  53. package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
  54. package/dist/lib/proxy/snapshotPersistence.js +41 -0
  55. package/dist/lib/proxy/sseInterceptor.js +36 -11
  56. package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
  57. package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
  58. package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
  59. package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
  60. package/dist/lib/tasks/store/redisTaskStore.js +42 -17
  61. package/dist/lib/tasks/taskManager.d.ts +2 -0
  62. package/dist/lib/tasks/taskManager.js +100 -5
  63. package/dist/lib/telemetry/telemetryService.js +9 -5
  64. package/dist/lib/types/cli.d.ts +4 -0
  65. package/dist/lib/types/proxyTypes.d.ts +211 -1
  66. package/dist/lib/types/tools.d.ts +18 -0
  67. package/dist/lib/utils/providerHealth.d.ts +1 -0
  68. package/dist/lib/utils/providerHealth.js +46 -31
  69. package/dist/lib/utils/providerUtils.js +11 -22
  70. package/dist/lib/utils/schemaConversion.d.ts +1 -0
  71. package/dist/lib/utils/schemaConversion.js +3 -0
  72. package/dist/mcp/toolRegistry.d.ts +2 -0
  73. package/dist/mcp/toolRegistry.js +32 -31
  74. package/dist/neurolink.d.ts +38 -0
  75. package/dist/neurolink.js +1890 -1707
  76. package/dist/providers/googleAiStudio.js +0 -5
  77. package/dist/providers/googleNativeGemini3.d.ts +4 -0
  78. package/dist/providers/googleNativeGemini3.js +39 -1
  79. package/dist/providers/googleVertex.d.ts +10 -0
  80. package/dist/providers/googleVertex.js +445 -445
  81. package/dist/providers/litellm.d.ts +1 -0
  82. package/dist/providers/litellm.js +73 -64
  83. package/dist/providers/ollama.js +17 -4
  84. package/dist/providers/openAI.d.ts +2 -0
  85. package/dist/providers/openAI.js +139 -140
  86. package/dist/proxy/claudeFormat.js +14 -5
  87. package/dist/proxy/oauthFetch.js +298 -318
  88. package/dist/proxy/proxyConfig.js +3 -1
  89. package/dist/proxy/proxyFetch.js +250 -222
  90. package/dist/proxy/proxyHealth.d.ts +17 -0
  91. package/dist/proxy/proxyHealth.js +54 -0
  92. package/dist/proxy/requestLogger.js +140 -48
  93. package/dist/proxy/routingPolicy.d.ts +33 -0
  94. package/dist/proxy/routingPolicy.js +254 -0
  95. package/dist/proxy/snapshotPersistence.d.ts +2 -0
  96. package/dist/proxy/snapshotPersistence.js +40 -0
  97. package/dist/proxy/sseInterceptor.js +36 -11
  98. package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
  99. package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
  100. package/dist/services/server/ai/observability/instrumentation.js +194 -218
  101. package/dist/tasks/backends/bullmqBackend.js +24 -18
  102. package/dist/tasks/store/redisTaskStore.js +42 -17
  103. package/dist/tasks/taskManager.d.ts +2 -0
  104. package/dist/tasks/taskManager.js +100 -5
  105. package/dist/telemetry/telemetryService.js +9 -5
  106. package/dist/types/cli.d.ts +4 -0
  107. package/dist/types/proxyTypes.d.ts +211 -1
  108. package/dist/types/tools.d.ts +18 -0
  109. package/dist/utils/providerHealth.d.ts +1 -0
  110. package/dist/utils/providerHealth.js +46 -31
  111. package/dist/utils/providerUtils.js +12 -22
  112. package/dist/utils/schemaConversion.d.ts +1 -0
  113. package/dist/utils/schemaConversion.js +3 -0
  114. package/package.json +3 -2
  115. package/scripts/observability/check-proxy-telemetry.mjs +1 -1
  116. package/scripts/observability/manage-local-openobserve.sh +36 -5
@@ -1,4 +1,4 @@
1
- import type { ModelMessage, LanguageModel, Tool } from "ai";
1
+ import type { LanguageModel, ModelMessage, Tool } from "ai";
2
2
  import type { AIProviderName } from "../constants/enums.js";
3
3
  import type { EvaluationData } from "../index.js";
4
4
  import type { NeuroLink } from "../neurolink.js";
@@ -136,6 +136,11 @@ export declare abstract class BaseProvider implements AIProvider {
136
136
  * Alias for generate method - implements AIProvider interface
137
137
  */
138
138
  gen(optionsOrPrompt: TextGenerationOptions | string, analysisSchema?: ValidationSchema): Promise<EnhancedGenerateResult | null>;
139
+ private runGenerateInActiveContext;
140
+ private handleDirectTTSSynthesis;
141
+ private handleVideoFrameGeneration;
142
+ private executeStandardGenerateFlow;
143
+ private synthesizeAIResponseIfNeeded;
139
144
  /**
140
145
  * BACKWARD COMPATIBILITY: Legacy generateText method
141
146
  * Converts EnhancedGenerateResult to TextGenerationResult format
@@ -6,9 +6,9 @@ import { MiddlewareFactory } from "../middleware/factory.js";
6
6
  import { SpanStatus, SpanType } from "../observability/types/spanTypes.js";
7
7
  import { SpanSerializer } from "../observability/utils/spanSerializer.js";
8
8
  import { ATTR, tracers } from "../telemetry/index.js";
9
- import { calculateCost } from "../utils/pricing.js";
10
9
  import { isAbortError } from "../utils/errorHandling.js";
11
10
  import { logger } from "../utils/logger.js";
11
+ import { calculateCost } from "../utils/pricing.js";
12
12
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
13
13
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
14
14
  import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
@@ -131,9 +131,15 @@ export class BaseProvider {
131
131
  }
132
132
  }
133
133
  // CRITICAL: Image generation models don't support real streaming
134
- // Force fake streaming for image models to ensure image output is yielded
134
+ // Force fake streaming for image models to ensure image output is yielded.
135
+ // Skip this path when the caller explicitly requests non-image output (e.g.
136
+ // JSON analysis) so dual-mode models like gemini-3.1-flash-image-preview
137
+ // can still perform text/structured generation.
135
138
  const isImageModel = IMAGE_GENERATION_MODELS.some((m) => this.modelName.includes(m));
136
- if (isImageModel) {
139
+ const requestsNonImageOutput = options.output?.format === "json" ||
140
+ options.output?.format === "structured" ||
141
+ options.output?.format === "text";
142
+ if (isImageModel && !requestsNonImageOutput) {
137
143
  logger.info(`Image model detected, forcing fake streaming`, {
138
144
  provider: this.providerName,
139
145
  model: this.modelName,
@@ -522,242 +528,223 @@ export class BaseProvider {
522
528
  });
523
529
  // Set this span as the active context so child spans (GenerationHandler, etc.) become descendants
524
530
  const activeCtx = trace.setSpan(context.active(), otelSpan);
525
- let otelSpanEnded = false;
526
- return await context.with(activeCtx, async () => {
531
+ const otelSpanState = { ended: false };
532
+ return await context.with(activeCtx, async () => this.runGenerateInActiveContext(options, startTime, metricsSpan, otelSpan, otelSpanState));
533
+ }
534
+ /**
535
+ * Alias for generate method - implements AIProvider interface
536
+ */
537
+ async gen(optionsOrPrompt, analysisSchema) {
538
+ return this.generate(optionsOrPrompt, analysisSchema);
539
+ }
540
+ async runGenerateInActiveContext(options, startTime, metricsSpan, otelSpan, otelSpanState) {
541
+ try {
542
+ if (options.output?.mode === "video") {
543
+ return await this.handleVideoGeneration(options, startTime);
544
+ }
545
+ const isImageModel = IMAGE_GENERATION_MODELS.some((m) => this.modelName.includes(m));
546
+ const requestsNonImageOutput = options.output?.format === "json" ||
547
+ options.output?.format === "structured" ||
548
+ options.output?.format === "text";
549
+ if (isImageModel && !requestsNonImageOutput) {
550
+ logger.info(`Image generation model detected, routing to executeImageGeneration`, {
551
+ provider: this.providerName,
552
+ model: this.modelName,
553
+ });
554
+ const imageResult = await this.executeImageGeneration(options);
555
+ return await this.enhanceResult(imageResult, options, startTime);
556
+ }
557
+ if (options.tts?.enabled && !options.tts?.useAiResponse) {
558
+ return this.handleDirectTTSSynthesis(options, startTime);
559
+ }
560
+ const { tools, model } = await this.prepareGenerationContext(options);
561
+ const messages = await this.buildMessages(options);
562
+ const videoFrameResult = await this.handleVideoFrameGeneration(options, messages, model, startTime);
563
+ if (videoFrameResult) {
564
+ return videoFrameResult;
565
+ }
566
+ return await this.executeStandardGenerateFlow(options, startTime, metricsSpan, model, messages, tools);
567
+ }
568
+ catch (error) {
569
+ SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
570
+ otelSpan.setStatus({
571
+ code: SpanStatusCode.ERROR,
572
+ message: error instanceof Error ? error.message : String(error),
573
+ });
574
+ otelSpan.end();
575
+ otelSpanState.ended = true;
576
+ if (isAbortError(error)) {
577
+ logger.info(`Generate aborted for ${this.providerName}`, {
578
+ error: error instanceof Error ? error.message : String(error),
579
+ });
580
+ }
581
+ else {
582
+ logger.error(`Generate failed for ${this.providerName}:`, error);
583
+ }
584
+ throw this.handleProviderError(error);
585
+ }
586
+ finally {
587
+ if (!otelSpanState.ended) {
588
+ otelSpan.setStatus({ code: SpanStatusCode.OK });
589
+ otelSpan.end();
590
+ }
591
+ }
592
+ }
593
+ async handleDirectTTSSynthesis(options, startTime) {
594
+ const textToSynthesize = options.prompt ?? options.input?.text ?? "";
595
+ const baseResult = {
596
+ content: textToSynthesize,
597
+ provider: options.provider ?? this.providerName,
598
+ model: this.modelName,
599
+ usage: { input: 0, output: 0, total: 0 },
600
+ };
601
+ try {
602
+ if (!options.tts) {
603
+ return this.enhanceResult(baseResult, options, startTime);
604
+ }
605
+ baseResult.audio = await TTSProcessor.synthesize(textToSynthesize, options.provider ?? this.providerName, options.tts);
606
+ }
607
+ catch (ttsError) {
608
+ logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
609
+ }
610
+ return this.enhanceResult(baseResult, options, startTime);
611
+ }
612
+ async handleVideoFrameGeneration(options, messages, model, startTime) {
613
+ if (!hasVideoFrames(messages)) {
614
+ return null;
615
+ }
616
+ const videoAnalysisResult = await executeVideoAnalysis(messages, {
617
+ provider: options.provider,
618
+ providerName: this.providerName,
619
+ region: options.region,
620
+ });
621
+ const userText = messages
622
+ .filter((m) => m.role === "user")
623
+ .flatMap((m) => Array.isArray(m.content)
624
+ ? m.content
625
+ .filter((p) => p.type === "text")
626
+ .map((p) => p.text)
627
+ : [typeof m.content === "string" ? m.content : ""])
628
+ .filter(Boolean)
629
+ .join("\n")
630
+ .trim();
631
+ let formattedContent = videoAnalysisResult;
632
+ let usage = { input: 0, output: 0, total: 0 };
633
+ if (options.systemPrompt) {
527
634
  try {
528
- // ===== VIDEO GENERATION MODE =====
529
- // Generate video from image + prompt using Veo 3.1
530
- if (options.output?.mode === "video") {
531
- return await this.handleVideoGeneration(options, startTime);
532
- }
533
- // ===== IMAGE GENERATION MODE =====
534
- // Route to executeImageGeneration for image generation models
535
- const isImageModel = IMAGE_GENERATION_MODELS.some((m) => this.modelName.includes(m));
536
- if (isImageModel) {
537
- logger.info(`Image generation model detected, routing to executeImageGeneration`, {
538
- provider: this.providerName,
539
- model: this.modelName,
540
- });
541
- const imageResult = await this.executeImageGeneration(options);
542
- return await this.enhanceResult(imageResult, options, startTime);
543
- }
544
- // ===== TTS MODE 1: Direct Input Synthesis (useAiResponse=false) =====
545
- // Synthesize input text directly without AI generation
546
- // This is optimal for simple read-aloud scenarios
547
- if (options.tts?.enabled && !options.tts?.useAiResponse) {
548
- const textToSynthesize = options.prompt ?? options.input?.text ?? "";
549
- // Build base result structure - common to both paths
550
- const baseResult = {
551
- content: textToSynthesize,
552
- provider: options.provider ?? this.providerName,
553
- model: this.modelName,
554
- usage: { input: 0, output: 0, total: 0 },
555
- };
556
- try {
557
- const ttsResult = await TTSProcessor.synthesize(textToSynthesize, options.provider ?? this.providerName, options.tts);
558
- baseResult.audio = ttsResult;
559
- }
560
- catch (ttsError) {
561
- logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
562
- // baseResult remains without audio - graceful degradation
563
- }
564
- // Call enhanceResult for consistency - enables analytics/evaluation for TTS-only requests
565
- return await this.enhanceResult(baseResult, options, startTime);
566
- }
567
- // ===== Normal AI Generation Flow =====
568
- const { tools, model } = await this.prepareGenerationContext(options);
569
- const messages = await this.buildMessages(options);
570
- // ===== VIDEO ANALYSIS FROM MESSAGES CONTENT =====
571
- // Check if video files are present in messages content array
572
- // If video analysis is needed, perform it via Gemini, then pass through Claude for formatting
573
- if (hasVideoFrames(messages)) {
574
- const videoAnalysisResult = await executeVideoAnalysis(messages, {
575
- provider: options.provider,
576
- providerName: this.providerName,
577
- region: options.region,
578
- // Don't pass the main conversation model — video analysis uses
579
- // Google's Gemini API (generateContent) which only supports Gemini models.
580
- // Let videoAnalysisProcessor use its own default (gemini-2.5-flash).
581
- });
582
- // Extract user's original text from messages (excluding image parts)
583
- const userTextParts = messages
584
- .filter((m) => m.role === "user")
585
- .flatMap((m) => Array.isArray(m.content)
586
- ? m.content
587
- .filter((p) => p.type === "text")
588
- .map((p) => p.text)
589
- : [typeof m.content === "string" ? m.content : ""])
590
- .filter(Boolean);
591
- const userText = userTextParts.join("\n").trim();
592
- // Pass Gemini's analysis through Claude for structured JSON formatting
593
- // The system prompt (from Curator) includes JSON_REPORT_PROMPT_SUFFIX
594
- // which instructs Claude to output {"summary": "...", "details": "..."}
595
- let formattedContent = videoAnalysisResult;
596
- let usage = { input: 0, output: 0, total: 0 };
597
- if (options.systemPrompt) {
598
- try {
599
- const formattingPrompt = userText
600
- ? `The user asked: "${userText}"\n\nHere is the video/image analysis result from the visual analysis system:\n\n${videoAnalysisResult}\n\nBased on this analysis, provide your response.`
601
- : `Here is a video/image analysis result from the visual analysis system:\n\n${videoAnalysisResult}\n\nBased on this analysis, provide your response.`;
602
- logger.debug("[VideoAnalysis] Formatting via Claude", {
603
- userTextLength: userText.length,
604
- analysisLength: videoAnalysisResult.length,
605
- });
606
- const formattedResult = await generateText({
607
- model,
608
- system: options.systemPrompt,
609
- messages: [
610
- { role: "user", content: formattingPrompt },
611
- ],
612
- maxOutputTokens: options.maxTokens || 8192,
613
- temperature: 0.3,
614
- abortSignal: options.abortSignal,
615
- experimental_telemetry: this.telemetryHandler?.getTelemetryConfig(options, "generate"),
616
- });
617
- formattedContent = formattedResult.text;
618
- usage = {
619
- input: formattedResult.usage?.inputTokens || 0,
620
- output: formattedResult.usage?.outputTokens || 0,
621
- total: (formattedResult.usage?.inputTokens || 0) +
622
- (formattedResult.usage?.outputTokens || 0),
623
- };
624
- logger.debug("[VideoAnalysis] Claude formatting complete", {
625
- formattedLength: formattedContent.length,
626
- usage,
627
- });
628
- }
629
- catch (error) {
630
- logger.warn("[VideoAnalysis] Claude formatting failed, using raw Gemini output", {
631
- error: error instanceof Error ? error.message : String(error),
632
- });
633
- // formattedContent remains as raw videoAnalysisResult (graceful degradation)
634
- }
635
- }
636
- const videoResult = {
637
- content: formattedContent,
638
- provider: options.provider ?? this.providerName,
639
- model: this.modelName,
640
- usage,
641
- };
642
- return await this.enhanceResult(videoResult, options, startTime);
643
- }
644
- // Compose timeout signal with user-provided abort signal (mirrors stream path)
645
- const timeoutController = createTimeoutController(options.timeout, this.providerName, "generate");
646
- const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
647
- const composedOptions = composedSignal
648
- ? { ...options, abortSignal: composedSignal }
649
- : options;
650
- let generateResult;
651
- try {
652
- generateResult = await this.executeGeneration(model, messages, tools, composedOptions);
653
- }
654
- finally {
655
- timeoutController?.cleanup();
656
- }
657
- this.analyzeAIResponse(generateResult);
658
- this.logGenerationComplete(generateResult);
659
- const responseTime = Date.now() - startTime;
660
- await this.recordPerformanceMetrics(generateResult.usage, responseTime);
661
- const { toolsUsed, toolExecutions } = this.extractToolInformation(generateResult);
662
- let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutions, options);
663
- // ===== TTS MODE 2: AI Response Synthesis (useAiResponse=true) =====
664
- // Synthesize AI-generated response after generation completes
665
- if (options.tts?.enabled && options.tts?.useAiResponse) {
666
- const aiResponse = enhancedResult.content;
667
- const provider = options.provider ?? this.providerName;
668
- // Validate AI response and provider before synthesis
669
- if (aiResponse && provider) {
670
- try {
671
- const ttsResult = await TTSProcessor.synthesize(aiResponse, provider, options.tts);
672
- // Add audio to enhanced result (TTSProcessor already includes latency in metadata)
673
- enhancedResult = {
674
- ...enhancedResult,
675
- audio: ttsResult,
676
- };
677
- }
678
- catch (ttsError) {
679
- // Log TTS error but continue with text-only result
680
- logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
681
- // enhancedResult remains unchanged (no audio field added)
682
- }
683
- }
684
- else {
685
- logger.warn(`TTS synthesis skipped despite being enabled`, {
686
- provider: this.providerName,
687
- hasAiResponse: !!aiResponse,
688
- aiResponseLength: aiResponse?.length ?? 0,
689
- hasProvider: !!provider,
690
- ttsConfig: {
691
- enabled: options.tts?.enabled,
692
- useAiResponse: options.tts?.useAiResponse,
693
- },
694
- reason: !aiResponse
695
- ? "AI response is empty or undefined"
696
- : "Provider is missing",
697
- });
698
- }
699
- }
700
- // Observability: record successful generate span with token/cost data
701
- let enrichedGenerateSpan = { ...metricsSpan };
702
- if (enhancedResult?.usage) {
703
- enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
704
- promptTokens: enhancedResult.usage.input || 0,
705
- completionTokens: enhancedResult.usage.output || 0,
706
- totalTokens: enhancedResult.usage.total || 0,
707
- });
708
- const cost = calculateCost(this.providerName, this.modelName, {
709
- input: enhancedResult.usage.input || 0,
710
- output: enhancedResult.usage.output || 0,
711
- total: enhancedResult.usage.total || 0,
712
- });
713
- if (cost && cost > 0) {
714
- enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, {
715
- totalCost: cost,
716
- });
717
- }
718
- }
719
- const _endedGenerateSpan = SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
720
- // Note: Do NOT record to getMetricsAggregator() here — the neurolink.ts
721
- // generation:end listener creates an authoritative span with richer context
722
- // (provider name, model, input/output) and records to both aggregators.
723
- // Recording here would double-count cost and token metrics.
724
- return await this.enhanceResult(enhancedResult, options, startTime);
635
+ const formattingPrompt = userText
636
+ ? `The user asked: "${userText}"\n\nHere is the video/image analysis result from the visual analysis system:\n\n${videoAnalysisResult}\n\nBased on this analysis, provide your response.`
637
+ : `Here is a video/image analysis result from the visual analysis system:\n\n${videoAnalysisResult}\n\nBased on this analysis, provide your response.`;
638
+ logger.debug("[VideoAnalysis] Formatting via Claude", {
639
+ userTextLength: userText.length,
640
+ analysisLength: videoAnalysisResult.length,
641
+ });
642
+ const formattedResult = await generateText({
643
+ model,
644
+ system: options.systemPrompt,
645
+ messages: [{ role: "user", content: formattingPrompt }],
646
+ maxOutputTokens: options.maxTokens || 8192,
647
+ temperature: 0.3,
648
+ abortSignal: options.abortSignal,
649
+ experimental_telemetry: this.telemetryHandler?.getTelemetryConfig(options, "generate"),
650
+ });
651
+ formattedContent = formattedResult.text;
652
+ usage = {
653
+ input: formattedResult.usage?.inputTokens || 0,
654
+ output: formattedResult.usage?.outputTokens || 0,
655
+ total: (formattedResult.usage?.inputTokens || 0) +
656
+ (formattedResult.usage?.outputTokens || 0),
657
+ };
658
+ logger.debug("[VideoAnalysis] Claude formatting complete", {
659
+ formattedLength: formattedContent.length,
660
+ usage,
661
+ });
725
662
  }
726
663
  catch (error) {
727
- // Observability: record failed generate span
728
- const _endedGenerateSpan = SpanSerializer.endSpan(metricsSpan, SpanStatus.ERROR, error instanceof Error ? error.message : String(error));
729
- // Note: Do NOT record to getMetricsAggregator() here — neurolink.ts
730
- // handles authoritative metrics recording to avoid double-counting.
731
- otelSpan.setStatus({
732
- code: SpanStatusCode.ERROR,
733
- message: error instanceof Error ? error.message : String(error),
664
+ logger.warn("[VideoAnalysis] Claude formatting failed, using raw Gemini output", {
665
+ error: error instanceof Error ? error.message : String(error),
734
666
  });
735
- otelSpan.end();
736
- otelSpanEnded = true;
737
- // Abort errors are expected when a generation is cancelled — log at info, not error
738
- if (isAbortError(error)) {
739
- logger.info(`Generate aborted for ${this.providerName}`, {
740
- error: error instanceof Error ? error.message : String(error),
741
- });
742
- }
743
- else {
744
- logger.error(`Generate failed for ${this.providerName}:`, error);
745
- }
746
- throw this.handleProviderError(error);
747
667
  }
748
- finally {
749
- if (!otelSpanEnded) {
750
- otelSpan.setStatus({ code: SpanStatusCode.OK });
751
- otelSpan.end();
752
- }
668
+ }
669
+ return this.enhanceResult({
670
+ content: formattedContent,
671
+ provider: options.provider ?? this.providerName,
672
+ model: this.modelName,
673
+ usage,
674
+ }, options, startTime);
675
+ }
676
+ async executeStandardGenerateFlow(options, startTime, metricsSpan, model, messages, tools) {
677
+ const timeoutController = createTimeoutController(options.timeout, this.providerName, "generate");
678
+ const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
679
+ const composedOptions = composedSignal
680
+ ? { ...options, abortSignal: composedSignal }
681
+ : options;
682
+ let generateResult;
683
+ try {
684
+ generateResult = await this.executeGeneration(model, messages, tools, composedOptions);
685
+ }
686
+ finally {
687
+ timeoutController?.cleanup();
688
+ }
689
+ this.analyzeAIResponse(generateResult);
690
+ this.logGenerationComplete(generateResult);
691
+ const responseTime = Date.now() - startTime;
692
+ await this.recordPerformanceMetrics(generateResult.usage, responseTime);
693
+ const { toolsUsed, toolExecutions } = this.extractToolInformation(generateResult);
694
+ let enhancedResult = this.formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutions, options);
695
+ enhancedResult = await this.synthesizeAIResponseIfNeeded(enhancedResult, options);
696
+ let enrichedGenerateSpan = { ...metricsSpan };
697
+ if (enhancedResult?.usage) {
698
+ enrichedGenerateSpan = SpanSerializer.enrichWithTokenUsage(enrichedGenerateSpan, {
699
+ promptTokens: enhancedResult.usage.input || 0,
700
+ completionTokens: enhancedResult.usage.output || 0,
701
+ totalTokens: enhancedResult.usage.total || 0,
702
+ });
703
+ const cost = calculateCost(this.providerName, this.modelName, {
704
+ input: enhancedResult.usage.input || 0,
705
+ output: enhancedResult.usage.output || 0,
706
+ total: enhancedResult.usage.total || 0,
707
+ });
708
+ if (cost && cost > 0) {
709
+ enrichedGenerateSpan = SpanSerializer.enrichWithCost(enrichedGenerateSpan, { totalCost: cost });
753
710
  }
754
- }); // end context.with
711
+ }
712
+ SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
713
+ return this.enhanceResult(enhancedResult, options, startTime);
755
714
  }
756
- /**
757
- * Alias for generate method - implements AIProvider interface
758
- */
759
- async gen(optionsOrPrompt, analysisSchema) {
760
- return this.generate(optionsOrPrompt, analysisSchema);
715
+ async synthesizeAIResponseIfNeeded(enhancedResult, options) {
716
+ if (!options.tts?.enabled || !options.tts?.useAiResponse) {
717
+ return enhancedResult;
718
+ }
719
+ const aiResponse = enhancedResult.content;
720
+ const provider = options.provider ?? this.providerName;
721
+ if (!aiResponse || !provider) {
722
+ logger.warn(`TTS synthesis skipped despite being enabled`, {
723
+ provider: this.providerName,
724
+ hasAiResponse: !!aiResponse,
725
+ aiResponseLength: aiResponse?.length ?? 0,
726
+ hasProvider: !!provider,
727
+ ttsConfig: {
728
+ enabled: options.tts?.enabled,
729
+ useAiResponse: options.tts?.useAiResponse,
730
+ },
731
+ reason: !aiResponse
732
+ ? "AI response is empty or undefined"
733
+ : "Provider is missing",
734
+ });
735
+ return enhancedResult;
736
+ }
737
+ try {
738
+ const ttsResult = await TTSProcessor.synthesize(aiResponse, provider, options.tts);
739
+ return {
740
+ ...enhancedResult,
741
+ audio: ttsResult,
742
+ };
743
+ }
744
+ catch (ttsError) {
745
+ logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
746
+ return enhancedResult;
747
+ }
761
748
  }
762
749
  /**
763
750
  * BACKWARD COMPATIBILITY: Legacy generateText method
@@ -15,6 +15,9 @@ export declare class AIProviderFactory {
15
15
  * Prevents hanging on non-responsive endpoints
16
16
  */
17
17
  private static initializeDynamicProviderWithTimeout;
18
+ private static resolveModelFromEnvironment;
19
+ private static resolveDynamicModelName;
20
+ private static createResolvedProvider;
18
21
  /**
19
22
  * Create a provider instance for the specified provider type
20
23
  * @param providerName - Name of the provider ('vertex', 'bedrock', 'openai')