@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.
- package/CHANGELOG.md +8 -0
- package/dist/auth/anthropicOAuth.js +12 -0
- package/dist/browser/neurolink.min.js +335 -334
- package/dist/cli/commands/mcp.d.ts +6 -0
- package/dist/cli/commands/mcp.js +200 -184
- package/dist/cli/commands/proxy.js +560 -518
- package/dist/core/baseProvider.d.ts +6 -1
- package/dist/core/baseProvider.js +219 -232
- package/dist/core/factory.d.ts +3 -0
- package/dist/core/factory.js +140 -190
- package/dist/core/modules/ToolsManager.d.ts +1 -0
- package/dist/core/modules/ToolsManager.js +40 -42
- package/dist/core/toolEvents.d.ts +3 -0
- package/dist/core/toolEvents.js +7 -0
- package/dist/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/auth/anthropicOAuth.js +12 -0
- package/dist/lib/core/baseProvider.d.ts +6 -1
- package/dist/lib/core/baseProvider.js +219 -232
- package/dist/lib/core/factory.d.ts +3 -0
- package/dist/lib/core/factory.js +140 -190
- package/dist/lib/core/modules/ToolsManager.d.ts +1 -0
- package/dist/lib/core/modules/ToolsManager.js +40 -42
- package/dist/lib/core/toolEvents.d.ts +3 -0
- package/dist/lib/core/toolEvents.js +8 -0
- package/dist/lib/evaluation/pipeline/evaluationPipeline.js +5 -2
- package/dist/lib/evaluation/scorers/scorerRegistry.d.ts +3 -0
- package/dist/lib/evaluation/scorers/scorerRegistry.js +356 -284
- package/dist/lib/mcp/toolRegistry.d.ts +2 -0
- package/dist/lib/mcp/toolRegistry.js +32 -31
- package/dist/lib/neurolink.d.ts +38 -0
- package/dist/lib/neurolink.js +1890 -1707
- package/dist/lib/providers/googleAiStudio.js +0 -5
- package/dist/lib/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/lib/providers/googleNativeGemini3.js +39 -1
- package/dist/lib/providers/googleVertex.d.ts +10 -0
- package/dist/lib/providers/googleVertex.js +445 -445
- package/dist/lib/providers/litellm.d.ts +1 -0
- package/dist/lib/providers/litellm.js +73 -64
- package/dist/lib/providers/ollama.js +17 -4
- package/dist/lib/providers/openAI.d.ts +2 -0
- package/dist/lib/providers/openAI.js +139 -140
- package/dist/lib/proxy/claudeFormat.js +14 -5
- package/dist/lib/proxy/oauthFetch.js +298 -318
- package/dist/lib/proxy/proxyConfig.js +3 -1
- package/dist/lib/proxy/proxyFetch.js +250 -222
- package/dist/lib/proxy/proxyHealth.d.ts +17 -0
- package/dist/lib/proxy/proxyHealth.js +55 -0
- package/dist/lib/proxy/requestLogger.js +140 -48
- package/dist/lib/proxy/routingPolicy.d.ts +33 -0
- package/dist/lib/proxy/routingPolicy.js +255 -0
- package/dist/lib/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/lib/proxy/snapshotPersistence.js +41 -0
- package/dist/lib/proxy/sseInterceptor.js +36 -11
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/lib/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/lib/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/lib/tasks/backends/bullmqBackend.js +24 -18
- package/dist/lib/tasks/store/redisTaskStore.js +42 -17
- package/dist/lib/tasks/taskManager.d.ts +2 -0
- package/dist/lib/tasks/taskManager.js +100 -5
- package/dist/lib/telemetry/telemetryService.js +9 -5
- package/dist/lib/types/cli.d.ts +4 -0
- package/dist/lib/types/proxyTypes.d.ts +211 -1
- package/dist/lib/types/tools.d.ts +18 -0
- package/dist/lib/utils/providerHealth.d.ts +1 -0
- package/dist/lib/utils/providerHealth.js +46 -31
- package/dist/lib/utils/providerUtils.js +11 -22
- package/dist/lib/utils/schemaConversion.d.ts +1 -0
- package/dist/lib/utils/schemaConversion.js +3 -0
- package/dist/mcp/toolRegistry.d.ts +2 -0
- package/dist/mcp/toolRegistry.js +32 -31
- package/dist/neurolink.d.ts +38 -0
- package/dist/neurolink.js +1890 -1707
- package/dist/providers/googleAiStudio.js +0 -5
- package/dist/providers/googleNativeGemini3.d.ts +4 -0
- package/dist/providers/googleNativeGemini3.js +39 -1
- package/dist/providers/googleVertex.d.ts +10 -0
- package/dist/providers/googleVertex.js +445 -445
- package/dist/providers/litellm.d.ts +1 -0
- package/dist/providers/litellm.js +73 -64
- package/dist/providers/ollama.js +17 -4
- package/dist/providers/openAI.d.ts +2 -0
- package/dist/providers/openAI.js +139 -140
- package/dist/proxy/claudeFormat.js +14 -5
- package/dist/proxy/oauthFetch.js +298 -318
- package/dist/proxy/proxyConfig.js +3 -1
- package/dist/proxy/proxyFetch.js +250 -222
- package/dist/proxy/proxyHealth.d.ts +17 -0
- package/dist/proxy/proxyHealth.js +54 -0
- package/dist/proxy/requestLogger.js +140 -48
- package/dist/proxy/routingPolicy.d.ts +33 -0
- package/dist/proxy/routingPolicy.js +254 -0
- package/dist/proxy/snapshotPersistence.d.ts +2 -0
- package/dist/proxy/snapshotPersistence.js +40 -0
- package/dist/proxy/sseInterceptor.js +36 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +2916 -2377
- package/dist/services/server/ai/observability/instrumentation.js +194 -218
- package/dist/tasks/backends/bullmqBackend.js +24 -18
- package/dist/tasks/store/redisTaskStore.js +42 -17
- package/dist/tasks/taskManager.d.ts +2 -0
- package/dist/tasks/taskManager.js +100 -5
- package/dist/telemetry/telemetryService.js +9 -5
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/proxyTypes.d.ts +211 -1
- package/dist/types/tools.d.ts +18 -0
- package/dist/utils/providerHealth.d.ts +1 -0
- package/dist/utils/providerHealth.js +46 -31
- package/dist/utils/providerUtils.js +12 -22
- package/dist/utils/schemaConversion.d.ts +1 -0
- package/dist/utils/schemaConversion.js +3 -0
- package/package.json +3 -2
- package/scripts/observability/check-proxy-telemetry.mjs +1 -1
- package/scripts/observability/manage-local-openobserve.sh +36 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
728
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
}
|
|
711
|
+
}
|
|
712
|
+
SpanSerializer.endSpan(enrichedGenerateSpan, SpanStatus.OK);
|
|
713
|
+
return this.enhanceResult(enhancedResult, options, startTime);
|
|
755
714
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
package/dist/core/factory.d.ts
CHANGED
|
@@ -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')
|