@juspay/neurolink 13.4.10 → 13.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -3
- package/README.md +3 -2
- package/dist/adapters/video/videoAnalyzer.d.ts +1 -0
- package/dist/adapters/video/videoAnalyzer.js +4 -0
- package/dist/autoresearch/index.d.ts +1 -0
- package/dist/autoresearch/index.js +4 -0
- package/dist/autoresearch/worker.js +4 -0
- package/dist/browser/neurolink.min.js +466 -466
- package/dist/cli/commands/decide.js +25 -5
- package/dist/cli/commands/observability.js +11 -4
- package/dist/cli/commands/telemetry.js +36 -6
- package/dist/cli/utils/formatters.d.ts +17 -0
- package/dist/cli/utils/formatters.js +39 -0
- package/dist/constants/langfuse.d.ts +17 -1
- package/dist/constants/langfuse.js +23 -1
- package/dist/constants/telemetry.d.ts +6 -0
- package/dist/constants/telemetry.js +6 -0
- package/dist/core/baseProvider.js +2 -0
- package/dist/core/geminiLoopAdapter.js +19 -0
- package/dist/core/loopEngine.js +10 -7
- package/dist/core/modules/StreamHandler.js +5 -4
- package/dist/core/modules/TelemetryHandler.d.ts +5 -1
- package/dist/core/modules/TelemetryHandler.js +16 -0
- package/dist/core/modules/ToolsManager.js +3 -0
- package/dist/core/nativeGenerateLoop.js +1 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +4 -0
- package/dist/evaluation/ragasEvaluator.js +1 -0
- package/dist/factories/providerDescriptors.d.ts +4 -3
- package/dist/factories/providerDescriptors.js +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/externalServerManager.js +6 -2
- package/dist/mcp/toolRegistry.js +3 -0
- package/dist/neurolink.d.ts +20 -10
- package/dist/neurolink.js +134 -65
- package/dist/observability/FEATURE-STATUS.md +0 -4
- package/dist/observability/exporterRegistry.d.ts +6 -1
- package/dist/observability/exporterRegistry.js +18 -0
- package/dist/observability/exporterSpanProcessor.d.ts +20 -6
- package/dist/observability/exporterSpanProcessor.js +166 -34
- package/dist/observability/exporters/baseExporter.d.ts +22 -3
- package/dist/observability/exporters/baseExporter.js +87 -5
- package/dist/observability/exporters/langfuseExporter.d.ts +29 -3
- package/dist/observability/exporters/langfuseExporter.js +251 -41
- package/dist/observability/langfuseMasking.d.ts +1 -1
- package/dist/observability/langfuseMasking.js +1 -1
- package/dist/observability/otlpEncoding.d.ts +18 -5
- package/dist/observability/otlpEncoding.js +69 -12
- package/dist/observability/utils/spanSerializer.d.ts +3 -2
- package/dist/observability/utils/spanSerializer.js +5 -3
- package/dist/processors/base/BaseFileProcessor.d.ts +1 -0
- package/dist/processors/base/BaseFileProcessor.js +4 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +4 -0
- package/dist/processors/registry/ProcessorRegistry.d.ts +1 -0
- package/dist/processors/registry/ProcessorRegistry.js +5 -0
- package/dist/providers/amazonBedrock/client.js +7 -5
- package/dist/providers/amazonSagemaker.js +32 -12
- package/dist/providers/anthropic/client.js +1 -0
- package/dist/providers/anthropic/loopAdapter.js +7 -3
- package/dist/providers/catalog/provider-catalog.schema.json +64 -3
- package/dist/providers/catalog/schema.js +81 -1
- package/dist/providers/catalog/xor.json +30 -10
- package/dist/providers/configuredDecision.d.ts +8 -1
- package/dist/providers/configuredDecision.js +22 -0
- package/dist/providers/decide/dialects/openaiDecisions.js +8 -0
- package/dist/providers/decide/dialects/systemOne.js +28 -2
- package/dist/providers/decide/guards.d.ts +6 -0
- package/dist/providers/decide/guards.js +22 -0
- package/dist/providers/googleAiStudio/client.js +2 -0
- package/dist/providers/googleVertex/client.d.ts +6 -5
- package/dist/providers/googleVertex/client.js +46 -19
- package/dist/providers/openaiChatCompletionsBase.js +4 -4
- package/dist/providers/systemOneDecision.d.ts +30 -7
- package/dist/providers/systemOneDecision.js +71 -14
- package/dist/proxy/codexUsage.js +59 -2
- package/dist/proxy/proxyTracer.d.ts +25 -1
- package/dist/proxy/proxyTracer.js +146 -22
- package/dist/proxy/proxyTranslationEngine.js +4 -2
- package/dist/proxy/sseInterceptor.js +6 -4
- package/dist/rag/chunkers/BaseChunker.js +1 -0
- package/dist/rag/index.d.ts +1 -0
- package/dist/rag/index.js +4 -0
- package/dist/rag/ragIntegration.js +1 -0
- package/dist/rag/reranker/reranker.js +2 -0
- package/dist/rag/retrieval/vectorQueryTool.js +1 -0
- package/dist/server/routes/codexProxyRoutes.js +3 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +8 -7
- package/dist/services/server/ai/observability/instrumentation.js +217 -47
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +19 -1
- package/dist/services/server/ai/observability/langfuseSetup.js +62 -4
- package/dist/services/server/ai/observability/otlpSetup.d.ts +12 -2
- package/dist/services/server/ai/observability/otlpSetup.js +55 -4
- package/dist/telemetry/aiMetrics.d.ts +56 -0
- package/dist/telemetry/aiMetrics.js +229 -0
- package/dist/telemetry/attributes.d.ts +24 -1
- package/dist/telemetry/attributes.js +94 -9
- package/dist/telemetry/chatMessages.d.ts +6 -0
- package/dist/telemetry/chatMessages.js +37 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +1 -1
- package/dist/telemetry/observation.d.ts +5 -5
- package/dist/telemetry/observation.js +128 -39
- package/dist/telemetry/spanSanitizer.d.ts +2 -2
- package/dist/telemetry/spanSanitizer.js +76 -8
- package/dist/telemetry/telemetryStarter.d.ts +16 -0
- package/dist/telemetry/telemetryStarter.js +28 -0
- package/dist/telemetry/traceContext.d.ts +3 -3
- package/dist/telemetry/traceContext.js +3 -3
- package/dist/telemetry/tracers.d.ts +13 -1
- package/dist/telemetry/tracers.js +49 -10
- package/dist/telemetry/withSpan.d.ts +19 -3
- package/dist/telemetry/withSpan.js +149 -20
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/decision.d.ts +117 -10
- package/dist/types/exporter.d.ts +98 -3
- package/dist/types/livekit.d.ts +16 -1
- package/dist/types/loopEngine.d.ts +16 -1
- package/dist/types/observability.d.ts +38 -5
- package/dist/types/providerCatalog.d.ts +12 -1
- package/dist/types/providers.d.ts +2 -0
- package/dist/types/proxy.d.ts +2 -0
- package/dist/types/span.d.ts +26 -0
- package/dist/utils/decisionAnswers.d.ts +7 -1
- package/dist/utils/decisionAnswers.js +43 -2
- package/dist/utils/decisionLimits.d.ts +13 -1
- package/dist/utils/decisionLimits.js +52 -1
- package/dist/utils/decisionMedia.d.ts +4 -2
- package/dist/utils/decisionMedia.js +216 -14
- package/dist/utils/timeout.js +4 -2
- package/dist/voice/livekit/realtimeMcpTools.js +11 -2
- package/dist/voice/livekit/realtimeMemory.d.ts +20 -2
- package/dist/voice/livekit/realtimeMemory.js +139 -46
- package/dist/voice/livekit/realtimeVoiceAgent.js +20 -8
- package/dist/voice/livekit/turnRecorder.js +22 -9
- package/dist/workflow/core/ensembleExecutor.js +2 -0
- package/dist/workflow/core/judgeScorer.js +1 -0
- package/dist/workflow/core/responseConditioner.js +1 -0
- package/dist/workflow/core/workflowRunner.js +6 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.js +4 -0
- package/docs-site/static/search-index.json +56 -48
- package/package.json +21 -2
|
@@ -2,3 +2,9 @@ export declare const isRecord: (v: unknown) => v is Record<string, unknown>;
|
|
|
2
2
|
export declare const asNumber: (v: unknown) => number | undefined;
|
|
3
3
|
export declare const asNumberMap: (v: unknown) => Record<string, number> | undefined;
|
|
4
4
|
export declare const asStringMap: (v: unknown) => Record<string, string> | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* A map whose values are shown as text: a string as is, anything else a JSON
|
|
7
|
+
* value can hold as its JSON text. A score legend echoes each level as the
|
|
8
|
+
* caller gave it, and a level may be a number or an object.
|
|
9
|
+
*/
|
|
10
|
+
export declare const asTextMap: (v: unknown) => Record<string, string> | undefined;
|
|
@@ -27,3 +27,25 @@ export const asStringMap = (v) => {
|
|
|
27
27
|
}
|
|
28
28
|
return out;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* A map whose values are shown as text: a string as is, anything else a JSON
|
|
32
|
+
* value can hold as its JSON text. A score legend echoes each level as the
|
|
33
|
+
* caller gave it, and a level may be a number or an object.
|
|
34
|
+
*/
|
|
35
|
+
export const asTextMap = (v) => {
|
|
36
|
+
if (!isRecord(v)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
41
|
+
if (typeof raw === "string") {
|
|
42
|
+
out[k] = raw;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (raw === undefined || typeof raw === "function") {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
out[k] = JSON.stringify(raw);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
@@ -771,6 +771,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
771
771
|
name: "provider.stream",
|
|
772
772
|
tracer: tracers.provider,
|
|
773
773
|
detached: true,
|
|
774
|
+
rootWhenOrphaned: true,
|
|
774
775
|
attributes: {
|
|
775
776
|
[ATTR.NL_MODEL]: modelName,
|
|
776
777
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -1474,6 +1475,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1474
1475
|
name: "provider.generate",
|
|
1475
1476
|
tracer: tracers.provider,
|
|
1476
1477
|
detached: true,
|
|
1478
|
+
rootWhenOrphaned: true,
|
|
1477
1479
|
attributes: {
|
|
1478
1480
|
[ATTR.NL_MODEL]: modelName,
|
|
1479
1481
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -520,11 +520,12 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
520
520
|
private preserveStreamResultAccessors;
|
|
521
521
|
private withClaudeGeneration;
|
|
522
522
|
/**
|
|
523
|
-
* Emit `generation:end` so
|
|
524
|
-
* the
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
523
|
+
* Emit `generation:end` so event listeners and the in-process metrics
|
|
524
|
+
* aggregator record the generation. Vertex bypasses the AI SDK (and
|
|
525
|
+
* therefore the experimental_telemetry plumbing), so nothing else emits it;
|
|
526
|
+
* the in-process store is never exported, and the Langfuse generation
|
|
527
|
+
* observations are the spans opened where each model call is made.
|
|
528
|
+
* Mirrors the Bedrock + Ollama pattern.
|
|
528
529
|
*/
|
|
529
530
|
private emitGenerationEnd;
|
|
530
531
|
/**
|
|
@@ -43,7 +43,9 @@ import { createStreamChannel } from "../../core/streamChannel.js";
|
|
|
43
43
|
import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
|
|
44
44
|
import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
|
|
45
45
|
import { resolveLiveTool } from "../../tools/toolDiscovery.js";
|
|
46
|
-
import { ATTR, spanContentAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
|
|
46
|
+
import { ATTR, recordsSpanInputs, spanContentAttribute, spanModelRequestAttribute, tracers, withClientSpan, withClientStreamSpan, withSpan, } from "../../telemetry/index.js";
|
|
47
|
+
import { recordsSpanContent } from "../../telemetry/attributes.js";
|
|
48
|
+
import { inTurnlessTree } from "../../telemetry/tracers.js";
|
|
47
49
|
import { observe, observeGeneration, observeTrace, } from "../../telemetry/observation.js";
|
|
48
50
|
import { withGeneration, withStepSpan } from "../../telemetry/withSpan.js";
|
|
49
51
|
import { context as otelContext, SpanKind, SpanStatusCode, trace as otelTrace, } from "@opentelemetry/api";
|
|
@@ -1004,6 +1006,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1004
1006
|
name: "provider.stream",
|
|
1005
1007
|
tracer: tracers.provider,
|
|
1006
1008
|
detached: true,
|
|
1009
|
+
rootWhenOrphaned: true,
|
|
1007
1010
|
attributes: {
|
|
1008
1011
|
[ATTR.NL_MODEL]: modelName,
|
|
1009
1012
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -3472,7 +3475,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3472
3475
|
// Langfuse/OTel: the native SDK bypasses the Vercel AI SDK's
|
|
3473
3476
|
// experimental_telemetry, so emit spans manually — one turn span, one
|
|
3474
3477
|
// generation span per API call, one tool span per execution — all carrying
|
|
3475
|
-
// langfuse.* attributes
|
|
3478
|
+
// the langfuse.* observation attributes ExporterSpanProcessor exports.
|
|
3476
3479
|
// Usage lives ONLY on the generation spans (Langfuse sums usage across
|
|
3477
3480
|
// observations for trace totals, so repeating it on the turn double-counts).
|
|
3478
3481
|
const offeredToolNames = (tools ?? []).map((anthropicTool) => anthropicTool.name);
|
|
@@ -5515,12 +5518,20 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5515
5518
|
name: "provider.generate",
|
|
5516
5519
|
tracer: tracers.provider,
|
|
5517
5520
|
detached: true,
|
|
5521
|
+
rootWhenOrphaned: true,
|
|
5518
5522
|
attributes: {
|
|
5519
5523
|
[ATTR.NL_MODEL]: modelName,
|
|
5520
5524
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
5521
5525
|
},
|
|
5522
5526
|
}, async (generateSpan) => {
|
|
5523
5527
|
const generateStartTime = Date.now();
|
|
5528
|
+
// Neither this span nor executeGeneration is an observation: the
|
|
5529
|
+
// generation spans below carry the model call. Marked internal so no
|
|
5530
|
+
// exporter of ours sends them as duplicates, except for the root a
|
|
5531
|
+
// direct call opened for itself, which the loop hangs from.
|
|
5532
|
+
if (!inTurnlessTree(generateSpan)) {
|
|
5533
|
+
generateSpan.setAttribute(ATTR.NL_INTERNAL, true);
|
|
5534
|
+
}
|
|
5524
5535
|
// One dispatch decision for the whole override: Vertex's generate()
|
|
5525
5536
|
// bypasses BaseProvider.generate(), so it re-runs the same
|
|
5526
5537
|
// resolveRequestKind() the core call sites use rather than keeping
|
|
@@ -5597,15 +5608,21 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5597
5608
|
const inputPrompt = mergedOptions.input?.text ||
|
|
5598
5609
|
mergedOptions.prompt ||
|
|
5599
5610
|
"";
|
|
5600
|
-
//
|
|
5601
|
-
//
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5611
|
+
// A host exporter on the same provider reads the input and output of
|
|
5612
|
+
// these two spans, so they follow the policy of a generation: masked,
|
|
5613
|
+
// and left out when inputs (or all content) are not recorded. The
|
|
5614
|
+
// input is set before the call so an error path still carries it.
|
|
5615
|
+
const generationInputAttribute = recordsSpanInputs()
|
|
5616
|
+
? spanModelRequestAttribute({
|
|
5617
|
+
...(mergedOptions.systemPrompt
|
|
5618
|
+
? { system: mergedOptions.systemPrompt }
|
|
5619
|
+
: {}),
|
|
5620
|
+
prompt: inputPrompt,
|
|
5621
|
+
})
|
|
5622
|
+
: undefined;
|
|
5623
|
+
if (generationInputAttribute !== undefined) {
|
|
5624
|
+
generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_INPUT, generationInputAttribute);
|
|
5625
|
+
}
|
|
5609
5626
|
try {
|
|
5610
5627
|
let result;
|
|
5611
5628
|
// Wrap the actual native generate call in `neurolink.executeGeneration`
|
|
@@ -5619,10 +5636,15 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5619
5636
|
detached: true,
|
|
5620
5637
|
attributes: {
|
|
5621
5638
|
[ATTR.NL_MODEL]: modelName,
|
|
5639
|
+
[ATTR.NL_INTERNAL]: true,
|
|
5622
5640
|
path: isAnthropicModel(modelName)
|
|
5623
5641
|
? "native.anthropic"
|
|
5624
5642
|
: "native.google-genai",
|
|
5625
|
-
|
|
5643
|
+
...(generationInputAttribute !== undefined
|
|
5644
|
+
? {
|
|
5645
|
+
[LangfuseOtelSpanAttributes.OBSERVATION_INPUT]: generationInputAttribute,
|
|
5646
|
+
}
|
|
5647
|
+
: {}),
|
|
5626
5648
|
},
|
|
5627
5649
|
}, async (executionSpan) => {
|
|
5628
5650
|
let nativeResult;
|
|
@@ -5640,7 +5662,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5640
5662
|
});
|
|
5641
5663
|
nativeResult = await this.runNativeGenerateWithMiddleware(mergedOptions, modelName, (effectiveOptions) => this.executeNativeGemini3GenerateWithSchemaFallback(effectiveOptions, modelName));
|
|
5642
5664
|
}
|
|
5643
|
-
|
|
5665
|
+
if (recordsSpanContent()) {
|
|
5666
|
+
executionSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(nativeResult?.content ?? ""));
|
|
5667
|
+
}
|
|
5644
5668
|
return nativeResult;
|
|
5645
5669
|
});
|
|
5646
5670
|
// Pipe through TTS-of-AI-response when caller asks for it. The
|
|
@@ -5648,7 +5672,9 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5648
5672
|
// enabled / useAiResponse is false, so the cost is zero on
|
|
5649
5673
|
// non-TTS paths.
|
|
5650
5674
|
result = await this.synthesizeAIResponseIfNeeded(result, options);
|
|
5651
|
-
|
|
5675
|
+
if (recordsSpanContent()) {
|
|
5676
|
+
generateSpan.setAttribute(LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, spanContentAttribute(result?.content ?? ""));
|
|
5677
|
+
}
|
|
5652
5678
|
// Fire onFinish lifecycle callback for the native generate path.
|
|
5653
5679
|
// Pipeline A providers get this for free via the AI SDK middleware
|
|
5654
5680
|
// wrapper (LifecycleMiddleware); native @google/genai bypasses
|
|
@@ -6343,11 +6369,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
6343
6369
|
}));
|
|
6344
6370
|
}
|
|
6345
6371
|
/**
|
|
6346
|
-
* Emit `generation:end` so
|
|
6347
|
-
* the
|
|
6348
|
-
*
|
|
6349
|
-
*
|
|
6350
|
-
*
|
|
6372
|
+
* Emit `generation:end` so event listeners and the in-process metrics
|
|
6373
|
+
* aggregator record the generation. Vertex bypasses the AI SDK (and
|
|
6374
|
+
* therefore the experimental_telemetry plumbing), so nothing else emits it;
|
|
6375
|
+
* the in-process store is never exported, and the Langfuse generation
|
|
6376
|
+
* observations are the spans opened where each model call is made.
|
|
6377
|
+
* Mirrors the Bedrock + Ollama pattern.
|
|
6351
6378
|
*/
|
|
6352
6379
|
emitGenerationEnd(modelName, result, startTime, success, error, prompt) {
|
|
6353
6380
|
const emitter = this.neurolink?.getEventEmitter();
|
|
@@ -1946,9 +1946,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
1946
1946
|
// a blanket "stop" for every stream that drained without throwing.
|
|
1947
1947
|
streamMetadata.finishReason = toUnifiedFinishReason(rawFinishReason);
|
|
1948
1948
|
// No-output path: stream completed normally but yielded zero text.
|
|
1949
|
-
// Build an enriched sentinel + stamp the active OTel span
|
|
1950
|
-
//
|
|
1951
|
-
// observation
|
|
1949
|
+
// Build an enriched sentinel + stamp the active OTel span with the
|
|
1950
|
+
// WARNING observation level, which ExporterSpanProcessor exports,
|
|
1951
|
+
// so the observation does not read as a silent success.
|
|
1952
1952
|
if (contentYielded === 0 && toolsUsed.length === 0) {
|
|
1953
1953
|
logger.warn(`${providerName}: Stream produced no output — emitting enriched sentinel`);
|
|
1954
1954
|
const fauxNoOutput = new NoOutputGeneratedError({
|
|
@@ -2078,7 +2078,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
2078
2078
|
...(reasoningTokens > 0 ? { reasoningTokens } : {}),
|
|
2079
2079
|
};
|
|
2080
2080
|
};
|
|
2081
|
-
const stepSpans = new StepSpans();
|
|
2081
|
+
const stepSpans = new StepSpans("stream");
|
|
2082
2082
|
try {
|
|
2083
2083
|
// May grow mid-turn: hydrated tools with wire-unsafe names need
|
|
2084
2084
|
// reverse-mapping even when the initial name set required none.
|
|
@@ -22,6 +22,12 @@ export declare function describeValidationErrors(detail: readonly unknown[]): st
|
|
|
22
22
|
* belongs in an error message or a log, whatever the wording, so the
|
|
23
23
|
* configured key, anything shaped like a LiteLLM key, any long hex run and any
|
|
24
24
|
* embedded `data:` URL are all removed.
|
|
25
|
+
*
|
|
26
|
+
* Its request-validation errors also quote the request back (pydantic's
|
|
27
|
+
* `input_value=…`, the start of the state and questions), and its routing
|
|
28
|
+
* errors append the gateway's fallback table, which names every model group it
|
|
29
|
+
* serves. The first is the caller's data and the second is the gateway's, so
|
|
30
|
+
* both are cut.
|
|
25
31
|
*/
|
|
26
32
|
export declare function redactCredentials(message: string, apiKey: string): string;
|
|
27
33
|
/**
|
|
@@ -85,12 +91,14 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
85
91
|
*/
|
|
86
92
|
protected readRetryAfterMs(_headers: Headers): number | undefined;
|
|
87
93
|
/**
|
|
88
|
-
* The per-attempt timeout when the caller sets none
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
94
|
+
* The per-attempt timeout when the caller sets none: the descriptor's
|
|
95
|
+
* `decideMs`, plus `decideMsPerKStateTokens` for every full 1,000 estimated
|
|
96
|
+
* state tokens when it declares that, since a long state takes longer to read
|
|
97
|
+
* and a flat budget would cut off a request well inside the model's window. A
|
|
98
|
+
* provider whose latency grows another way overrides this. `stateTokens` is
|
|
99
|
+
* the local estimate, 0 when the descriptor sets no local limits.
|
|
92
100
|
*/
|
|
93
|
-
protected defaultTimeoutMs(_questionCount: number,
|
|
101
|
+
protected defaultTimeoutMs(_questionCount: number, stateTokens: number): number | undefined;
|
|
94
102
|
/**
|
|
95
103
|
* The part of a successful response that holds `answers`, `usage` and
|
|
96
104
|
* `model`. The default is the response itself; a vendor that wraps every
|
|
@@ -101,6 +109,12 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
101
109
|
/** Per-question confidence a transport reports outside the answer objects. */
|
|
102
110
|
protected reportedConfidence(_payload: unknown): Record<string, number>;
|
|
103
111
|
protected resolveResponseModel(payload: Record<string, unknown>, requestedModel: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* The model this request goes to and is checked against: the call's own, else
|
|
114
|
+
* the instance's. A provider whose default cannot read some requests may pick
|
|
115
|
+
* another for them here.
|
|
116
|
+
*/
|
|
117
|
+
protected resolveRequestModel(request: DecisionRequest): string;
|
|
104
118
|
/** Extra fields for the credential-rejected warning. */
|
|
105
119
|
protected diagnosticContext(): Record<string, unknown>;
|
|
106
120
|
protected getProviderName(): AIProviderName;
|
|
@@ -126,8 +140,17 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
|
|
|
126
140
|
*/
|
|
127
141
|
decide(request: DecisionRequest): Promise<DecisionResult>;
|
|
128
142
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
143
|
+
* Refuse a question this provider does not read: a type it does not list in
|
|
144
|
+
* `decisionLimits.questionTypes` (absent means boolean, choice and score), or
|
|
145
|
+
* a choice or score past its per-question cap. Checked even when the limits
|
|
146
|
+
* are advisory, since a server that does not know a question type cannot
|
|
147
|
+
* answer it.
|
|
148
|
+
*/
|
|
149
|
+
private assertSupportedQuestions;
|
|
150
|
+
/**
|
|
151
|
+
* Turn the request's images, video and audio into wire form, or refuse it.
|
|
152
|
+
* Refused as a non-retryable `invalid_request`: resending the same media
|
|
153
|
+
* cannot help.
|
|
131
154
|
*/
|
|
132
155
|
private prepareMedia;
|
|
133
156
|
/**
|
|
@@ -6,7 +6,7 @@ import { ProviderError } from "../types/index.js";
|
|
|
6
6
|
import { prepareDecisionMedia } from "../utils/decisionMedia.js";
|
|
7
7
|
import { logger } from "../utils/logger.js";
|
|
8
8
|
import { redactUrlsInText } from "../utils/logSanitize.js";
|
|
9
|
-
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, } from "../utils/decisionLimits.js";
|
|
9
|
+
import { estimateDecisionStateTokens, resolveDecisionLimitsReading, unsupportedDecisionQuestion, } from "../utils/decisionLimits.js";
|
|
10
10
|
import { systemOneDialect } from "./decide/dialects/systemOne.js";
|
|
11
11
|
import { asNumber, isRecord } from "./decide/guards.js";
|
|
12
12
|
/**
|
|
@@ -44,17 +44,31 @@ export function describeValidationErrors(detail) {
|
|
|
44
44
|
* belongs in an error message or a log, whatever the wording, so the
|
|
45
45
|
* configured key, anything shaped like a LiteLLM key, any long hex run and any
|
|
46
46
|
* embedded `data:` URL are all removed.
|
|
47
|
+
*
|
|
48
|
+
* Its request-validation errors also quote the request back (pydantic's
|
|
49
|
+
* `input_value=…`, the start of the state and questions), and its routing
|
|
50
|
+
* errors append the gateway's fallback table, which names every model group it
|
|
51
|
+
* serves. The first is the caller's data and the second is the gateway's, so
|
|
52
|
+
* both are cut.
|
|
47
53
|
*/
|
|
48
54
|
export function redactCredentials(message, apiKey) {
|
|
49
55
|
const withoutKey = apiKey
|
|
50
56
|
? message.split(apiKey).join("[redacted]")
|
|
51
57
|
: message;
|
|
52
|
-
return withoutKey
|
|
58
|
+
return (withoutKey
|
|
53
59
|
.replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
|
|
60
|
+
.replace(/\.?\s*Received Model Group=[\s\S]*$/, "")
|
|
61
|
+
.replace(/\s*No fallback model group found[\s\S]*$/, "")
|
|
62
|
+
// Greedy: the quoted value may itself contain ", input_type=", so only the
|
|
63
|
+
// last one is a delimiter the server wrote.
|
|
64
|
+
.replace(/input_value=[\s\S]*(?=, input_type=)/g, "input_value=[redacted]")
|
|
65
|
+
// Cut short by the server, a quoted value has no input_type after it, and
|
|
66
|
+
// may run over several lines; everything after it is the value.
|
|
67
|
+
.replace(/input_value=(?!\[redacted\])[\s\S]*/g, "input_value=[redacted]")
|
|
54
68
|
.replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
|
|
55
69
|
.replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
|
|
56
70
|
.replace(/data:[a-z]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi, "[media]")
|
|
57
|
-
.trim();
|
|
71
|
+
.trim());
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* Whether a failed `fetch` was one that refused a redirect. undici words every
|
|
@@ -151,13 +165,20 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
151
165
|
return undefined;
|
|
152
166
|
}
|
|
153
167
|
/**
|
|
154
|
-
* The per-attempt timeout when the caller sets none
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
168
|
+
* The per-attempt timeout when the caller sets none: the descriptor's
|
|
169
|
+
* `decideMs`, plus `decideMsPerKStateTokens` for every full 1,000 estimated
|
|
170
|
+
* state tokens when it declares that, since a long state takes longer to read
|
|
171
|
+
* and a flat budget would cut off a request well inside the model's window. A
|
|
172
|
+
* provider whose latency grows another way overrides this. `stateTokens` is
|
|
173
|
+
* the local estimate, 0 when the descriptor sets no local limits.
|
|
158
174
|
*/
|
|
159
|
-
defaultTimeoutMs(_questionCount,
|
|
160
|
-
|
|
175
|
+
defaultTimeoutMs(_questionCount, stateTokens) {
|
|
176
|
+
const base = this.getDescriptorDecideMs();
|
|
177
|
+
const perK = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.timeouts
|
|
178
|
+
?.decideMsPerKStateTokens;
|
|
179
|
+
return base === undefined || perK === undefined
|
|
180
|
+
? base
|
|
181
|
+
: base + Math.floor(stateTokens / 1000) * perK;
|
|
161
182
|
}
|
|
162
183
|
/**
|
|
163
184
|
* The part of a successful response that holds `answers`, `usage` and
|
|
@@ -175,6 +196,14 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
175
196
|
resolveResponseModel(payload, requestedModel) {
|
|
176
197
|
return typeof payload.model === "string" ? payload.model : requestedModel;
|
|
177
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* The model this request goes to and is checked against: the call's own, else
|
|
201
|
+
* the instance's. A provider whose default cannot read some requests may pick
|
|
202
|
+
* another for them here.
|
|
203
|
+
*/
|
|
204
|
+
resolveRequestModel(request) {
|
|
205
|
+
return request.model ?? this.modelName ?? this.getDefaultModel();
|
|
206
|
+
}
|
|
178
207
|
/** Extra fields for the credential-rejected warning. */
|
|
179
208
|
diagnosticContext() {
|
|
180
209
|
return {};
|
|
@@ -250,7 +279,8 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
250
279
|
retryable: false,
|
|
251
280
|
});
|
|
252
281
|
}
|
|
253
|
-
const resolvedModel =
|
|
282
|
+
const resolvedModel = this.resolveRequestModel(request);
|
|
283
|
+
this.assertSupportedQuestions(request, resolvedModel);
|
|
254
284
|
const stateTokens = this.assertWithinDecisionLimits(request, resolvedModel, questionEntries.length);
|
|
255
285
|
const wireQuestions = Object.fromEntries(questionEntries.map(([id, question]) => [
|
|
256
286
|
id,
|
|
@@ -393,12 +423,39 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
393
423
|
});
|
|
394
424
|
}
|
|
395
425
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
426
|
+
* Refuse a question this provider does not read: a type it does not list in
|
|
427
|
+
* `decisionLimits.questionTypes` (absent means boolean, choice and score), or
|
|
428
|
+
* a choice or score past its per-question cap. Checked even when the limits
|
|
429
|
+
* are advisory, since a server that does not know a question type cannot
|
|
430
|
+
* answer it.
|
|
431
|
+
*/
|
|
432
|
+
assertSupportedQuestions(request, model) {
|
|
433
|
+
const descriptor = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName);
|
|
434
|
+
const limits = descriptor
|
|
435
|
+
? resolveDecisionLimitsReading(descriptor, model)
|
|
436
|
+
: null;
|
|
437
|
+
for (const [id, question] of Object.entries(request.questions)) {
|
|
438
|
+
const problem = unsupportedDecisionQuestion(question, limits);
|
|
439
|
+
if (problem !== undefined) {
|
|
440
|
+
throw this.decisionError({
|
|
441
|
+
kind: "invalid_request",
|
|
442
|
+
message: `${this.vendorLabel()} cannot answer question "${id}": ${problem}.`,
|
|
443
|
+
retryable: false,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Turn the request's images, video and audio into wire form, or refuse it.
|
|
450
|
+
* Refused as a non-retryable `invalid_request`: resending the same media
|
|
451
|
+
* cannot help.
|
|
398
452
|
*/
|
|
399
453
|
async prepareMedia(request) {
|
|
400
454
|
const limits = PROVIDER_DESCRIPTORS_BY_NAME.get(this.providerName)?.decisionLimits;
|
|
401
|
-
const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders({
|
|
455
|
+
const prepared = await prepareDecisionMedia(request, limits?.media, this.vendorLabel(), listMediaDecisionProviders({
|
|
456
|
+
video: request.video !== undefined,
|
|
457
|
+
audio: request.audio !== undefined,
|
|
458
|
+
}));
|
|
402
459
|
if (prepared.status === "refused") {
|
|
403
460
|
throw this.decisionError({
|
|
404
461
|
kind: "invalid_request",
|
|
@@ -423,7 +480,7 @@ export class SystemOneDecisionProvider extends BaseProvider {
|
|
|
423
480
|
if (bytes > limit) {
|
|
424
481
|
throw this.decisionError({
|
|
425
482
|
kind: "invalid_request",
|
|
426
|
-
message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images${media?.video ? ", or a shorter video" : ""}.`,
|
|
483
|
+
message: `The request is ${bytes} bytes; ${this.vendorLabel()} accepts at most ${limit}. Send fewer or smaller images${media?.video ? ", or a shorter video" : ""}${media?.audio ? ", or a shorter audio clip" : ""}.`,
|
|
427
484
|
retryable: false,
|
|
428
485
|
});
|
|
429
486
|
}
|
package/dist/proxy/codexUsage.js
CHANGED
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
import { appendFileSync } from "node:fs";
|
|
43
43
|
import { extractSSEEvents } from "./sseInterceptor.js";
|
|
44
44
|
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
45
|
+
/**
|
|
46
|
+
* Characters of `response.output_item.done` payloads kept to stand in for a
|
|
47
|
+
* completed response that carries no output. Bounds what the tap holds for a
|
|
48
|
+
* stream whose items it cannot otherwise discard.
|
|
49
|
+
*/
|
|
50
|
+
const OUTPUT_ITEMS_LIMIT_CHARS = 1024 * 1024;
|
|
51
|
+
/** Type of the item that stands where output items past the cap were left out. */
|
|
52
|
+
const OMITTED_ITEMS_TYPE = "neurolink_output_omitted";
|
|
45
53
|
const nonNegativeInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
|
|
46
54
|
? Math.floor(value)
|
|
47
55
|
: 0;
|
|
@@ -208,6 +216,42 @@ function createCaptureSink() {
|
|
|
208
216
|
export function createCodexUsageTap() {
|
|
209
217
|
const evidence = { completed: false, terminalBytes: 0 };
|
|
210
218
|
let totalBytes = 0;
|
|
219
|
+
// Items announced by response.output_item.done. The terminal response may
|
|
220
|
+
// arrive with an empty or absent output, leaving these as the only record
|
|
221
|
+
// of what the model said.
|
|
222
|
+
const streamedItems = [];
|
|
223
|
+
let streamedItemChars = 0;
|
|
224
|
+
let omittedItems = 0;
|
|
225
|
+
// An item past the cap leaves a marker in its place, so the record reads as
|
|
226
|
+
// incomplete rather than as a shorter reply; adjacent omissions share one.
|
|
227
|
+
const omitItem = () => {
|
|
228
|
+
omittedItems += 1;
|
|
229
|
+
const last = streamedItems[streamedItems.length - 1];
|
|
230
|
+
if (last?.type === OMITTED_ITEMS_TYPE) {
|
|
231
|
+
streamedItems[streamedItems.length - 1] = {
|
|
232
|
+
...last,
|
|
233
|
+
omitted_items: Number(last.omitted_items) + 1,
|
|
234
|
+
};
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const marker = {
|
|
238
|
+
type: OMITTED_ITEMS_TYPE,
|
|
239
|
+
omitted_items: 1,
|
|
240
|
+
note: `…[truncated: output items past ${OUTPUT_ITEMS_LIMIT_CHARS} characters are not recorded]`,
|
|
241
|
+
};
|
|
242
|
+
streamedItems.push(marker);
|
|
243
|
+
streamedItemChars += JSON.stringify(marker).length;
|
|
244
|
+
};
|
|
245
|
+
const withStreamedOutput = (response) => {
|
|
246
|
+
if ((Array.isArray(response.output) && response.output.length > 0) ||
|
|
247
|
+
streamedItems.length === 0) {
|
|
248
|
+
return response;
|
|
249
|
+
}
|
|
250
|
+
if (omittedItems > 0) {
|
|
251
|
+
evidence.outputItemsOmitted = true;
|
|
252
|
+
}
|
|
253
|
+
return { ...response, output: [...streamedItems] };
|
|
254
|
+
};
|
|
211
255
|
const inspectEvidence = (events) => {
|
|
212
256
|
for (const frame of events) {
|
|
213
257
|
if (frame.data.trim() === "[DONE]") {
|
|
@@ -234,6 +278,19 @@ export function createCodexUsageTap() {
|
|
|
234
278
|
evidence.firstUsefulOutputEvent = String(type);
|
|
235
279
|
}
|
|
236
280
|
}
|
|
281
|
+
if (type === "response.output_item.done" &&
|
|
282
|
+
event.item &&
|
|
283
|
+
typeof event.item === "object" &&
|
|
284
|
+
!Array.isArray(event.item)) {
|
|
285
|
+
if (streamedItemChars + frame.data.length <=
|
|
286
|
+
OUTPUT_ITEMS_LIMIT_CHARS) {
|
|
287
|
+
streamedItems.push(event.item);
|
|
288
|
+
streamedItemChars += frame.data.length;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
omitItem();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
237
294
|
// Some clients/providers emit complete output items without deltas.
|
|
238
295
|
// Reasoning/control events are not useful client output. Recognize
|
|
239
296
|
// actual tool calls and text only, without inventing an earlier time.
|
|
@@ -277,8 +334,8 @@ export function createCodexUsageTap() {
|
|
|
277
334
|
type.startsWith("response.") &&
|
|
278
335
|
completedResponse &&
|
|
279
336
|
typeof completedResponse === "object" &&
|
|
280
|
-
"output" in completedResponse) {
|
|
281
|
-
evidence.response = completedResponse;
|
|
337
|
+
("output" in completedResponse || streamedItems.length > 0)) {
|
|
338
|
+
evidence.response = withStreamedOutput(completedResponse);
|
|
282
339
|
}
|
|
283
340
|
if (type === "response.completed") {
|
|
284
341
|
evidence.completed = true;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - calculateCost() from pricing.ts for cost tracking
|
|
13
13
|
*/
|
|
14
14
|
import { type Span } from "@opentelemetry/api";
|
|
15
|
-
import type { AccountSelectionContext, ProxyRequestContext, ProxyRequestOrigin, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
15
|
+
import type { AccountSelectionContext, JsonValue, ProxyRequestContext, ProxyRequestOrigin, ResponseInfoContext, UpstreamAttemptContext, UsageContext, ProxyUsageAttributionOptions, RuntimeRequestMetadata, ProxyLogTraceContext } from "../types/index.js";
|
|
16
16
|
declare class ProxyTracer {
|
|
17
17
|
private readonly rootSpan;
|
|
18
18
|
private readonly traceContext;
|
|
@@ -43,6 +43,12 @@ declare class ProxyTracer {
|
|
|
43
43
|
private readonly requestedModel;
|
|
44
44
|
private input?;
|
|
45
45
|
private output?;
|
|
46
|
+
/** The parsed response outranks the raw upstream body that arrives after it. */
|
|
47
|
+
private outputIsParsed;
|
|
48
|
+
/** A model call made through `runModelCall` records the generation. */
|
|
49
|
+
private modelCallDelegated;
|
|
50
|
+
/** That call's records carry no content; fixed when the call starts. */
|
|
51
|
+
private modelContentWithheld;
|
|
46
52
|
private responseModel?;
|
|
47
53
|
private finishReason?;
|
|
48
54
|
private firstOutputAt?;
|
|
@@ -65,6 +71,20 @@ declare class ProxyTracer {
|
|
|
65
71
|
startUpstreamAttempt(ctx: UpstreamAttemptContext): Span;
|
|
66
72
|
/** Span covering the SSE stream relay phase. */
|
|
67
73
|
startStream(): Span;
|
|
74
|
+
/**
|
|
75
|
+
* Run a model call inside this request's trace context, so the spans it
|
|
76
|
+
* starts nest under the proxy root and carry the request's user and session.
|
|
77
|
+
* That call's generation holds usage and cost, so the root records none of
|
|
78
|
+
* its own. Its prompt and answer are request and response bodies, so its
|
|
79
|
+
* records carry them only with `NEUROLINK_PROXY_TRACE_BODY_LOGGING=true`.
|
|
80
|
+
*/
|
|
81
|
+
runModelCall<T>(fn: () => T): T;
|
|
82
|
+
/**
|
|
83
|
+
* The delegated call's stream, read and closed under the same content rule:
|
|
84
|
+
* the call records its answer while the stream is consumed, which happens
|
|
85
|
+
* after `runModelCall` has returned.
|
|
86
|
+
*/
|
|
87
|
+
modelStream<T>(iterator: AsyncIterator<T>): AsyncIterator<T>;
|
|
68
88
|
/** Record account selection outcome on the root span. */
|
|
69
89
|
setAccountSelection(ctx: AccountSelectionContext): void;
|
|
70
90
|
/** Attribute fallback metrics to the account that actually served them. */
|
|
@@ -99,6 +119,8 @@ declare class ProxyTracer {
|
|
|
99
119
|
attemptCount: number;
|
|
100
120
|
reason: string;
|
|
101
121
|
}): void;
|
|
122
|
+
/** Record the client request body as the input of the root and its generation. */
|
|
123
|
+
setRequestInput(body: JsonValue): void;
|
|
102
124
|
/** Log the incoming client request body (redacted). */
|
|
103
125
|
logRequestBody(body: string): void;
|
|
104
126
|
/** Log the incoming client request headers (redacted). */
|
|
@@ -127,6 +149,8 @@ declare class ProxyTracer {
|
|
|
127
149
|
getUsage(): UsageContext | undefined;
|
|
128
150
|
/** End the root span with final HTTP status and duration, and emit OTEL metrics. */
|
|
129
151
|
end(responseStatus: number, durationMs: number): void;
|
|
152
|
+
/** Bodies are only worth keeping while something is recording the span. */
|
|
153
|
+
private recordsBodies;
|
|
130
154
|
private levelFor;
|
|
131
155
|
private observeOutcome;
|
|
132
156
|
/**
|