@juspay/neurolink 12.58.1 → 13.0.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 +14 -3
- package/dist/auth/anthropicOAuth.js +3 -3
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/auth/sessionManager.js +4 -4
- package/dist/auth/tokenStore.js +4 -4
- package/dist/browser/neurolink.min.js +474 -473
- package/dist/cli/commands/observability.d.ts +1 -6
- package/dist/cli/commands/observability.js +0 -106
- package/dist/cli/commands/proxy.js +9 -18
- package/dist/cli/commands/telemetry.d.ts +3 -13
- package/dist/cli/commands/telemetry.js +7 -354
- package/dist/cli/loop/session.js +1 -1
- package/dist/client/httpClient.js +1 -1
- package/dist/client/sseClient.js +1 -1
- package/dist/client/streamingClient.js +2 -2
- package/dist/client/wsClient.js +1 -1
- package/dist/constants/langfuse.d.ts +25 -0
- package/dist/constants/langfuse.js +38 -0
- package/dist/constants/telemetry.d.ts +20 -0
- package/dist/constants/telemetry.js +34 -0
- package/dist/core/baseProvider.d.ts +2 -1
- package/dist/core/baseProvider.js +4 -11
- package/dist/core/conversationMemoryManager.js +5 -8
- package/dist/core/factory.js +1 -1
- package/dist/core/geminiLoopAdapter.d.ts +8 -1
- package/dist/core/geminiLoopAdapter.js +25 -1
- package/dist/core/loopEngine.js +69 -40
- package/dist/core/modules/MessageBuilder.js +2 -2
- package/dist/core/modules/StreamHandler.js +2 -2
- package/dist/core/modules/TelemetryHandler.d.ts +2 -18
- package/dist/core/modules/TelemetryHandler.js +1 -66
- package/dist/core/modules/ToolsManager.js +40 -31
- package/dist/core/nativeGenerateLoop.js +78 -8
- package/dist/core/redisConversationMemoryManager.js +8 -15
- package/dist/core/toolExecutionGuards.js +1 -13
- package/dist/index.d.ts +5 -12
- package/dist/index.js +5 -37
- package/dist/mcp/batching/requestBatcher.js +1 -1
- package/dist/mcp/externalServerManager.js +3 -21
- package/dist/mcp/toolDiscoveryService.js +4 -16
- package/dist/mcp/toolRegistry.js +2 -5
- package/dist/memory/memoryRetrievalTools.js +1 -1
- package/dist/neurolink.d.ts +16 -27
- package/dist/neurolink.js +788 -792
- package/dist/observability/exporterSpanProcessor.d.ts +50 -0
- package/dist/observability/exporterSpanProcessor.js +181 -0
- package/dist/observability/exporters/langfuseExporter.d.ts +13 -34
- package/dist/observability/exporters/langfuseExporter.js +101 -154
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/langfuseMasking.d.ts +8 -0
- package/dist/observability/langfuseMasking.js +47 -0
- package/dist/observability/otlpEncoding.d.ts +20 -0
- package/dist/observability/otlpEncoding.js +85 -0
- package/dist/providers/amazonBedrock/client.js +142 -176
- package/dist/providers/amazonBedrock/loopAdapter.js +14 -1
- package/dist/providers/amazonSagemaker.js +118 -80
- package/dist/providers/anthropic/client.js +3 -52
- package/dist/providers/anthropic/loopAdapter.js +24 -1
- package/dist/providers/anthropic/rateLimitCapture.js +9 -12
- package/dist/providers/googleAiStudio/client.d.ts +4 -5
- package/dist/providers/googleAiStudio/client.js +38 -40
- package/dist/providers/googleVertex/client.d.ts +2 -7
- package/dist/providers/googleVertex/client.js +282 -378
- package/dist/providers/googleVertex/constants.js +2 -2
- package/dist/providers/litellm/client.d.ts +2 -9
- package/dist/providers/litellm/client.js +1 -63
- package/dist/providers/openAI/client.d.ts +1 -8
- package/dist/providers/openAI/client.js +0 -65
- package/dist/providers/openaiChatCompletionsBase.d.ts +1 -0
- package/dist/providers/openaiChatCompletionsBase.js +61 -4
- package/dist/providers/replicate.js +22 -4
- package/dist/proxy/codexUsage.js +7 -0
- package/dist/proxy/proxyFetch.js +19 -25
- package/dist/proxy/proxyTracer.d.ts +14 -7
- package/dist/proxy/proxyTracer.js +129 -83
- package/dist/proxy/proxyTranslationEngine.js +0 -1
- package/dist/proxy/requestLogger.d.ts +1 -1
- package/dist/proxy/requestLogger.js +39 -45
- package/dist/server/middleware/common.js +1 -1
- package/dist/server/routes/agentRoutes.js +4 -4
- package/dist/server/routes/claudeProxyRoutes.js +99 -196
- package/dist/server/routes/codexProxyRoutes.js +621 -591
- package/dist/server/routes/healthRoutes.js +6 -6
- package/dist/server/routes/mcpRoutes.js +7 -7
- package/dist/server/routes/memoryRoutes.js +7 -7
- package/dist/server/routes/openApiRoutes.js +3 -3
- package/dist/server/routes/toolRoutes.js +5 -5
- package/dist/server/routes/transcribeRoutes.js +1 -1
- package/dist/server/utils/validation.d.ts +3 -3
- package/dist/services/server/ai/observability/instrumentation.d.ts +34 -201
- package/dist/services/server/ai/observability/instrumentation.js +220 -1267
- package/dist/services/server/ai/observability/langfuseSetup.d.ts +14 -0
- package/dist/services/server/ai/observability/langfuseSetup.js +55 -0
- package/dist/services/server/ai/observability/otlpSetup.d.ts +16 -0
- package/dist/services/server/ai/observability/otlpSetup.js +58 -0
- package/dist/session/globalSessionState.js +0 -11
- package/dist/telemetry/attributes.d.ts +23 -46
- package/dist/telemetry/attributes.js +76 -162
- package/dist/telemetry/chatMessages.d.ts +13 -0
- package/dist/telemetry/chatMessages.js +59 -0
- package/dist/telemetry/index.d.ts +5 -19
- package/dist/telemetry/index.js +5 -26
- package/dist/telemetry/observation.d.ts +37 -0
- package/dist/telemetry/observation.js +112 -0
- package/dist/telemetry/observationNames.d.ts +18 -0
- package/dist/telemetry/observationNames.js +24 -0
- package/dist/telemetry/spanSanitizer.d.ts +39 -0
- package/dist/telemetry/spanSanitizer.js +92 -0
- package/dist/telemetry/spanSerialization.d.ts +13 -0
- package/dist/telemetry/spanSerialization.js +46 -0
- package/dist/telemetry/tracers.d.ts +37 -18
- package/dist/telemetry/tracers.js +97 -19
- package/dist/telemetry/withSpan.d.ts +47 -25
- package/dist/telemetry/withSpan.js +158 -71
- package/dist/types/cli.d.ts +0 -11
- package/dist/types/decision.d.ts +3 -0
- package/dist/types/exporter.d.ts +54 -0
- package/dist/types/exporter.js +0 -4
- package/dist/types/generate.d.ts +4 -1
- package/dist/types/isolatedAgent.d.ts +5 -4
- package/dist/types/loopEngine.d.ts +18 -28
- package/dist/types/observability.d.ts +127 -160
- package/dist/types/providers.d.ts +15 -0
- package/dist/types/proxy.d.ts +41 -2
- package/dist/utils/anthropicTraceSanitizer.d.ts +2 -2
- package/dist/utils/anthropicTraceSanitizer.js +3 -1
- package/dist/utils/conversationMemory.js +4 -4
- package/dist/utils/fileDetector.js +1 -1
- package/dist/utils/messageBuilder.js +1 -1
- package/dist/utils/noOutputSentinel.d.ts +4 -16
- package/dist/utils/noOutputSentinel.js +12 -28
- package/dist/utils/providerRetry.js +4 -4
- package/dist/voice/livekit/brain.js +1 -1
- package/dist/voice/livekit/realtimeMemory.d.ts +1 -1
- package/dist/voice/livekit/realtimeMemory.js +66 -1
- package/dist/voice/livekit/realtimeVoiceAgent.js +1 -1
- package/docs-site/static/search-index.json +42 -63
- package/package.json +10 -4
- package/dist/telemetry/telemetryService.d.ts +0 -79
- package/dist/telemetry/telemetryService.js +0 -572
- package/dist/utils/observabilityHelpers.d.ts +0 -27
- package/dist/utils/observabilityHelpers.js +0 -47
package/dist/cli/loop/session.js
CHANGED
|
@@ -115,7 +115,7 @@ export class LoopSession {
|
|
|
115
115
|
// The .fail() handler in cli.ts is now session-aware and will
|
|
116
116
|
// handle all parsing and validation errors without exiting the loop.
|
|
117
117
|
// We create a fresh instance for each command to prevent state pollution.
|
|
118
|
-
await tracers.sdk.startActiveSpan("
|
|
118
|
+
await tracers.sdk.startActiveSpan("cli.turn", async (turnSpan) => {
|
|
119
119
|
try {
|
|
120
120
|
turnSpan.setAttribute("cli.command", typeof processedCommand === "string"
|
|
121
121
|
? processedCommand.slice(0, 100)
|
|
@@ -187,7 +187,7 @@ export class NeuroLinkClient {
|
|
|
187
187
|
*/
|
|
188
188
|
async request(method, path, body, options) {
|
|
189
189
|
return withClientSpan({
|
|
190
|
-
name: "
|
|
190
|
+
name: "client.http.request",
|
|
191
191
|
tracer: tracers.http,
|
|
192
192
|
attributes: {
|
|
193
193
|
"http.method": method,
|
package/dist/client/sseClient.js
CHANGED
|
@@ -73,7 +73,7 @@ export class NeuroLinkSSE {
|
|
|
73
73
|
*/
|
|
74
74
|
async stream(path, options = {}, callbacks = {}) {
|
|
75
75
|
return withClientSpan({
|
|
76
|
-
name: "
|
|
76
|
+
name: "client.sse.stream",
|
|
77
77
|
tracer: tracers.http,
|
|
78
78
|
attributes: {
|
|
79
79
|
"http.method": options.body ? "POST" : "GET",
|
|
@@ -64,7 +64,7 @@ export class SSEClient {
|
|
|
64
64
|
*/
|
|
65
65
|
async connect(requestOptions = {}) {
|
|
66
66
|
return withClientSpan({
|
|
67
|
-
name: "
|
|
67
|
+
name: "client.streaming.sse.connect",
|
|
68
68
|
tracer: tracers.http,
|
|
69
69
|
attributes: {
|
|
70
70
|
"http.url": this.url,
|
|
@@ -406,7 +406,7 @@ export class WebSocketStreamingClient {
|
|
|
406
406
|
*/
|
|
407
407
|
async connect() {
|
|
408
408
|
return withClientSpan({
|
|
409
|
-
name: "
|
|
409
|
+
name: "client.streaming.ws.connect",
|
|
410
410
|
tracer: tracers.http,
|
|
411
411
|
attributes: { "http.url": this.options.url },
|
|
412
412
|
}, async () => this._connectWS());
|
package/dist/client/wsClient.js
CHANGED
|
@@ -111,7 +111,7 @@ export class NeuroLinkWebSocket {
|
|
|
111
111
|
// Start an OTel span that tracks the lifetime of this connection attempt.
|
|
112
112
|
// Ended in onclose/onerror/disconnect so metrics capture connection
|
|
113
113
|
// duration and error attribution.
|
|
114
|
-
this.connectionSpan = tracers.http.startSpan("
|
|
114
|
+
this.connectionSpan = tracers.http.startSpan("client.ws.connect", {
|
|
115
115
|
kind: SpanKind.CLIENT,
|
|
116
116
|
attributes: {
|
|
117
117
|
"http.url": this.config.baseUrl,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SpanStatus } from "../types/index.js";
|
|
2
|
+
/** @description Langfuse OTLP ingestion endpoint path. */
|
|
3
|
+
export declare const TRACES_PATH = "/api/public/otel/v1/traces";
|
|
4
|
+
/** @description Langfuse health endpoint path. */
|
|
5
|
+
export declare const HEALTH_PATH = "/api/public/health";
|
|
6
|
+
/** @description Langfuse Cloud base URL used when none is configured. */
|
|
7
|
+
export declare const DEFAULT_BASE_URL = "https://cloud.langfuse.com";
|
|
8
|
+
/** @description Spans buffered before an automatic flush. */
|
|
9
|
+
export declare const DEFAULT_FLUSH_AT = 15;
|
|
10
|
+
/** @description Milliseconds between scheduled flushes. */
|
|
11
|
+
export declare const DEFAULT_FLUSH_INTERVAL_MS = 5000;
|
|
12
|
+
/** @description Milliseconds before an export request is aborted. */
|
|
13
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
14
|
+
/** @description Maximum spans sent in one export request. */
|
|
15
|
+
export declare const MAX_SPANS_PER_REQUEST = 500;
|
|
16
|
+
/** @description Maximum serialized bytes sent in one export request. */
|
|
17
|
+
export declare const MAX_BYTES_PER_REQUEST = 3500000;
|
|
18
|
+
/** @description Placeholder that replaces a value fully removed by the user's mask. */
|
|
19
|
+
export declare const FULLY_MASKED = "<fully masked>";
|
|
20
|
+
/** @description Span attributes holding trace and observation input or output, which the mask applies to. */
|
|
21
|
+
export declare const CONTENT_KEYS: Set<string>;
|
|
22
|
+
/** @description Attribute prefixes whose values are metadata entries. */
|
|
23
|
+
export declare const METADATA_PREFIXES: string[];
|
|
24
|
+
/** @description OTLP numeric status code for each span status. */
|
|
25
|
+
export declare const OTLP_STATUS_CODES: Record<SpanStatus, number>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SpanStatus } from "../types/index.js";
|
|
2
|
+
/** @description Langfuse OTLP ingestion endpoint path. */
|
|
3
|
+
export const TRACES_PATH = "/api/public/otel/v1/traces";
|
|
4
|
+
/** @description Langfuse health endpoint path. */
|
|
5
|
+
export const HEALTH_PATH = "/api/public/health";
|
|
6
|
+
/** @description Langfuse Cloud base URL used when none is configured. */
|
|
7
|
+
export const DEFAULT_BASE_URL = "https://cloud.langfuse.com";
|
|
8
|
+
/** @description Spans buffered before an automatic flush. */
|
|
9
|
+
export const DEFAULT_FLUSH_AT = 15;
|
|
10
|
+
/** @description Milliseconds between scheduled flushes. */
|
|
11
|
+
export const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
|
|
12
|
+
/** @description Milliseconds before an export request is aborted. */
|
|
13
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
14
|
+
/** @description Maximum spans sent in one export request. */
|
|
15
|
+
export const MAX_SPANS_PER_REQUEST = 500;
|
|
16
|
+
/** @description Maximum serialized bytes sent in one export request. */
|
|
17
|
+
export const MAX_BYTES_PER_REQUEST = 3_500_000;
|
|
18
|
+
/** @description Placeholder that replaces a value fully removed by the user's mask. */
|
|
19
|
+
export const FULLY_MASKED = "<fully masked>";
|
|
20
|
+
/** @description Span attributes holding trace and observation input or output, which the mask applies to. */
|
|
21
|
+
export const CONTENT_KEYS = new Set([
|
|
22
|
+
"langfuse.observation.input",
|
|
23
|
+
"langfuse.observation.output",
|
|
24
|
+
"langfuse.trace.input",
|
|
25
|
+
"langfuse.trace.output",
|
|
26
|
+
]);
|
|
27
|
+
/** @description Attribute prefixes whose values are metadata entries. */
|
|
28
|
+
export const METADATA_PREFIXES = [
|
|
29
|
+
"langfuse.observation.metadata.",
|
|
30
|
+
"langfuse.trace.metadata.",
|
|
31
|
+
];
|
|
32
|
+
/** @description OTLP numeric status code for each span status. */
|
|
33
|
+
export const OTLP_STATUS_CODES = {
|
|
34
|
+
[SpanStatus.UNSET]: 0,
|
|
35
|
+
[SpanStatus.WARNING]: 0,
|
|
36
|
+
[SpanStatus.OK]: 1,
|
|
37
|
+
[SpanStatus.ERROR]: 2,
|
|
38
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @description Smallest per-string character budget when trimming span payloads. */
|
|
2
|
+
export declare const MIN_STRING_BUDGET_CHARS = 64;
|
|
3
|
+
/** @description Base64 payload length above which a value is treated as binary and replaced. */
|
|
4
|
+
export declare const BINARY_PAYLOAD_MIN_CHARS = 1024;
|
|
5
|
+
/** @description Placeholder written when a span value cannot be serialized. */
|
|
6
|
+
export declare const UNSERIALIZABLE_VALUE = "[unserializable]";
|
|
7
|
+
/** @description Object keys whose values are treated as binary payloads. */
|
|
8
|
+
export declare const BINARY_KEYS: Set<string>;
|
|
9
|
+
/** @description Placeholder that replaces a masked credential in span content. */
|
|
10
|
+
export declare const REDACTED_VALUE = "[REDACTED]";
|
|
11
|
+
/** @description Finish reasons that mark a completed model call as a Langfuse warning. */
|
|
12
|
+
export declare const WARNING_FINISH_REASONS: Set<string>;
|
|
13
|
+
/** @description Room for the "...[truncated N chars]" note, kept outside the opening a cap preserves. */
|
|
14
|
+
export declare const TRUNCATION_NOTE_ALLOWANCE_CHARS = 48;
|
|
15
|
+
/** @description Name of the OpenTelemetry context key that carries a trace's full metadata. */
|
|
16
|
+
export declare const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
|
|
17
|
+
/** @description Log prefix for NeuroLink's OpenTelemetry setup. */
|
|
18
|
+
export declare const OTEL_LOG_PREFIX = "[OpenTelemetry]";
|
|
19
|
+
/** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
|
|
20
|
+
export declare const MAX_REMEMBERED_SPANS = 5000;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @description Smallest per-string character budget when trimming span payloads. */
|
|
2
|
+
export const MIN_STRING_BUDGET_CHARS = 64;
|
|
3
|
+
/** @description Base64 payload length above which a value is treated as binary and replaced. */
|
|
4
|
+
export const BINARY_PAYLOAD_MIN_CHARS = 1_024;
|
|
5
|
+
/** @description Placeholder written when a span value cannot be serialized. */
|
|
6
|
+
export const UNSERIALIZABLE_VALUE = "[unserializable]";
|
|
7
|
+
/** @description Object keys whose values are treated as binary payloads. */
|
|
8
|
+
export const BINARY_KEYS = new Set([
|
|
9
|
+
"data",
|
|
10
|
+
"image",
|
|
11
|
+
"bytes",
|
|
12
|
+
"base64",
|
|
13
|
+
"b64json",
|
|
14
|
+
]);
|
|
15
|
+
/** @description Placeholder that replaces a masked credential in span content. */
|
|
16
|
+
export const REDACTED_VALUE = "[REDACTED]";
|
|
17
|
+
/** @description Finish reasons that mark a completed model call as a Langfuse warning. */
|
|
18
|
+
export const WARNING_FINISH_REASONS = new Set([
|
|
19
|
+
"length",
|
|
20
|
+
"max_tokens",
|
|
21
|
+
"content-filter",
|
|
22
|
+
"content_filter",
|
|
23
|
+
"abort",
|
|
24
|
+
"aborted",
|
|
25
|
+
"no-output",
|
|
26
|
+
]);
|
|
27
|
+
/** @description Room for the "...[truncated N chars]" note, kept outside the opening a cap preserves. */
|
|
28
|
+
export const TRUNCATION_NOTE_ALLOWANCE_CHARS = 48;
|
|
29
|
+
/** @description Name of the OpenTelemetry context key that carries a trace's full metadata. */
|
|
30
|
+
export const TRACE_METADATA_CONTEXT_KEY = "neurolink.trace.metadata";
|
|
31
|
+
/** @description Log prefix for NeuroLink's OpenTelemetry setup. */
|
|
32
|
+
export const OTEL_LOG_PREFIX = "[OpenTelemetry]";
|
|
33
|
+
/** @description Most open and most ended spans the exporter span processor remembers for re-parenting; the oldest are forgotten first. */
|
|
34
|
+
export const MAX_REMEMBERED_SPANS = 5_000;
|
|
@@ -3,6 +3,7 @@ import type { EvaluationData } from "../index.js";
|
|
|
3
3
|
import type { NeuroLink } from "../neurolink.js";
|
|
4
4
|
import type { FileToolRootPolicy, UnknownRecord, MiddlewareFactoryOptions, StreamOptions, StreamResult, AIProvider, AnalyticsData, EnhancedGenerateResult, TextGenerationOptions, TextGenerationResult, ValidationSchema, EmbedInput, DecisionRequest, DecisionResult, MemoryToolCallRecord, MemoryToolResultRecord, ToolExecutionSummaryInternal } from "../types/index.js";
|
|
5
5
|
import { TelemetryHandler } from "./modules/TelemetryHandler.js";
|
|
6
|
+
import { ToolsManager } from "./modules/ToolsManager.js";
|
|
6
7
|
import type { LanguageModel, ModelMessage, RawUsageObject, Tool, ToolCallRepairFunction, ToolSet } from "../types/index.js";
|
|
7
8
|
export declare abstract class BaseProvider implements AIProvider {
|
|
8
9
|
protected modelName: string;
|
|
@@ -25,7 +26,7 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
25
26
|
private streamHandler;
|
|
26
27
|
protected telemetryHandler: TelemetryHandler;
|
|
27
28
|
private utilities;
|
|
28
|
-
|
|
29
|
+
protected readonly toolsManager: ToolsManager;
|
|
29
30
|
constructor(modelName?: string, providerName?: AIProviderName, neurolink?: NeuroLink, middleware?: MiddlewareFactoryOptions);
|
|
30
31
|
/**
|
|
31
32
|
* Update modelName and rebuild composition handlers with the new value.
|
|
@@ -174,7 +174,7 @@ export class BaseProvider {
|
|
|
174
174
|
this.modelName = model;
|
|
175
175
|
trace
|
|
176
176
|
.getSpan(context.active())
|
|
177
|
-
?.setAttribute(ATTR.
|
|
177
|
+
?.setAttribute(ATTR.NL_MODEL, this.modelName);
|
|
178
178
|
this.messageBuilder = new MessageBuilder(this.providerName, this.modelName);
|
|
179
179
|
this.streamHandler = new StreamHandler(this.providerName, this.modelName);
|
|
180
180
|
this.telemetryHandler = new TelemetryHandler(this.providerName, this.modelName, this.neurolink);
|
|
@@ -1538,20 +1538,15 @@ export class BaseProvider {
|
|
|
1538
1538
|
// model that failed rather than the one that served.
|
|
1539
1539
|
const attempt = async () => {
|
|
1540
1540
|
// OTEL span for provider-level generate tracing
|
|
1541
|
-
|
|
1542
|
-
const otelSpan = tracers.provider.startSpan("neurolink.provider.generate", {
|
|
1541
|
+
const otelSpan = tracers.provider.startSpan("provider.generate", {
|
|
1543
1542
|
kind: SpanKind.CLIENT,
|
|
1544
1543
|
attributes: {
|
|
1545
|
-
[ATTR.
|
|
1546
|
-
[ATTR.GEN_AI_MODEL]: this.modelName || options.model || "unknown",
|
|
1547
|
-
[ATTR.GEN_AI_OPERATION]: "generate",
|
|
1544
|
+
[ATTR.NL_MODEL]: this.modelName || options.model || "unknown",
|
|
1548
1545
|
[ATTR.NL_PROVIDER]: this.providerName || "unknown",
|
|
1549
1546
|
},
|
|
1550
1547
|
});
|
|
1551
|
-
// Set this span as the active context so child spans (provider calls, tool executions) become descendants
|
|
1552
|
-
const activeCtx = trace.setSpan(context.active(), otelSpan);
|
|
1553
1548
|
const otelSpanState = { ended: false };
|
|
1554
|
-
return await
|
|
1549
|
+
return await this.runGenerateInActiveContext(options, startTime, otelSpan, otelSpanState);
|
|
1555
1550
|
};
|
|
1556
1551
|
// Callers that own fallback order (providerFallback / modelChain callers,
|
|
1557
1552
|
// or a router that retries on its own) pass the flag on both paths;
|
|
@@ -1761,7 +1756,6 @@ export class BaseProvider {
|
|
|
1761
1756
|
error,
|
|
1762
1757
|
latency,
|
|
1763
1758
|
};
|
|
1764
|
-
this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
|
|
1765
1759
|
logger.error(`TTS synthesis failed in Mode 1 (direct input synthesis):`, ttsError);
|
|
1766
1760
|
}
|
|
1767
1761
|
return this.enhanceResult(baseResult, options, startTime);
|
|
@@ -2119,7 +2113,6 @@ export class BaseProvider {
|
|
|
2119
2113
|
catch (ttsError) {
|
|
2120
2114
|
const latency = Date.now() - ttsStartTime;
|
|
2121
2115
|
const error = this.getTTSErrorDetails(ttsError);
|
|
2122
|
-
this.telemetryHandler.recordTTSFailure(ttsProvider, error, latency);
|
|
2123
2116
|
logger.error(`TTS synthesis failed in Mode 2 (AI response synthesis):`, ttsError);
|
|
2124
2117
|
return {
|
|
2125
2118
|
...enhancedResult,
|
|
@@ -6,7 +6,7 @@ import { randomUUID } from "crypto";
|
|
|
6
6
|
import { DEFAULT_MAX_SESSIONS, MEMORY_THRESHOLD_PERCENTAGE, MESSAGES_PER_TURN, } from "../config/conversationMemory.js";
|
|
7
7
|
import { TokenUtils } from "../constants/tokens.js";
|
|
8
8
|
import { SummarizationEngine } from "../context/summarizationEngine.js";
|
|
9
|
-
import {
|
|
9
|
+
import { context } from "@opentelemetry/api";
|
|
10
10
|
import { tracers, withSpan } from "../telemetry/index.js";
|
|
11
11
|
import { ConversationMemoryError } from "../types/index.js";
|
|
12
12
|
import { buildContextFromPointer, getEffectiveTokenThreshold, } from "../utils/conversationMemory.js";
|
|
@@ -67,7 +67,7 @@ export class ConversationMemoryManager {
|
|
|
67
67
|
*/
|
|
68
68
|
async storeConversationTurn(options) {
|
|
69
69
|
return withSpan({
|
|
70
|
-
name: "
|
|
70
|
+
name: "memory.storeTurn",
|
|
71
71
|
tracer: tracers.memory,
|
|
72
72
|
attributes: {
|
|
73
73
|
"memory.type": "in-memory",
|
|
@@ -114,10 +114,7 @@ export class ConversationMemoryManager {
|
|
|
114
114
|
if (shouldSummarize) {
|
|
115
115
|
// Only trigger summarization if not already in progress for this session
|
|
116
116
|
if (!this.summarizationInProgress.has(options.sessionId)) {
|
|
117
|
-
|
|
118
|
-
// which breaks automatic AsyncLocalStorage propagation and would
|
|
119
|
-
// otherwise cause orphaned traces in Langfuse.
|
|
120
|
-
const summarizeWithContext = runWithCurrentLangfuseContext(async () => {
|
|
117
|
+
const summarizeWithContext = context.bind(context.active(), async () => {
|
|
121
118
|
try {
|
|
122
119
|
await this.checkAndSummarize(session, tokenThreshold, options.requestId);
|
|
123
120
|
}
|
|
@@ -229,7 +226,7 @@ export class ConversationMemoryManager {
|
|
|
229
226
|
*/
|
|
230
227
|
async buildContextMessages(sessionId, _userId, _enableSummarization, requestId) {
|
|
231
228
|
return withSpan({
|
|
232
|
-
name: "
|
|
229
|
+
name: "memory.buildContext",
|
|
233
230
|
tracer: tracers.memory,
|
|
234
231
|
attributes: {
|
|
235
232
|
"memory.type": "in-memory",
|
|
@@ -345,7 +342,7 @@ export class ConversationMemoryManager {
|
|
|
345
342
|
}
|
|
346
343
|
async clearSession(sessionId) {
|
|
347
344
|
return withSpan({
|
|
348
|
-
name: "
|
|
345
|
+
name: "memory.clear",
|
|
349
346
|
tracer: tracers.memory,
|
|
350
347
|
attributes: {
|
|
351
348
|
"memory.type": "in-memory",
|
package/dist/core/factory.js
CHANGED
|
@@ -214,7 +214,7 @@ export class AIProviderFactory {
|
|
|
214
214
|
static async createProvider(providerName, modelName, enableMCP = true, sdk, region, credentials) {
|
|
215
215
|
const functionTag = "AIProviderFactory.createProvider";
|
|
216
216
|
// Providers are registered via ProviderFactory.initialize() on first use
|
|
217
|
-
return factoryTracer.startActiveSpan("
|
|
217
|
+
return factoryTracer.startActiveSpan("factory.createProvider", {
|
|
218
218
|
kind: SpanKind.INTERNAL,
|
|
219
219
|
attributes: {
|
|
220
220
|
"provider.name": providerName,
|
|
@@ -16,5 +16,12 @@
|
|
|
16
16
|
* and original -> sanitized when writing `functionResponse` parts. Neither the
|
|
17
17
|
* engine nor any other adapter needs to know sanitization happened.
|
|
18
18
|
*/
|
|
19
|
-
import type { AgenticLoopAdapter, GeminiLoopAdapterConfig, GeminiStepRaw, GeminiTurnContent } from "../types/index.js";
|
|
19
|
+
import type { AgenticLoopAdapter, GeminiLoopAdapterConfig, GeminiStepRaw, GeminiStepRequest, GeminiTurnContent, GenerationRecord } from "../types/index.js";
|
|
20
|
+
/**
|
|
21
|
+
* @description Describes one Gemini request as the request half of a Langfuse generation.
|
|
22
|
+
* @param provider {string} - Provider name used to price the call.
|
|
23
|
+
* @param request {GeminiStepRequest} - Model, contents and generation config sent.
|
|
24
|
+
* @returns {GenerationRecord} Provider, model, sampling and thinking parameters, and the system instruction plus contents.
|
|
25
|
+
*/
|
|
26
|
+
export declare function describeGeminiRequest(provider: string, request: GeminiStepRequest): GenerationRecord;
|
|
20
27
|
export declare function createGeminiLoopAdapter(config: GeminiLoopAdapterConfig): AgenticLoopAdapter<GeminiTurnContent[], GeminiStepRaw>;
|
|
@@ -19,6 +19,28 @@
|
|
|
19
19
|
import { guardToolExecutor } from "./toolExecutionGuards.js";
|
|
20
20
|
import { resolveLiveTool } from "../tools/toolDiscovery.js";
|
|
21
21
|
import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
|
|
22
|
+
/**
|
|
23
|
+
* @description Describes one Gemini request as the request half of a Langfuse generation.
|
|
24
|
+
* @param provider {string} - Provider name used to price the call.
|
|
25
|
+
* @param request {GeminiStepRequest} - Model, contents and generation config sent.
|
|
26
|
+
* @returns {GenerationRecord} Provider, model, sampling and thinking parameters, and the system instruction plus contents.
|
|
27
|
+
*/
|
|
28
|
+
export function describeGeminiRequest(provider, request) {
|
|
29
|
+
const settings = (request.config ?? {});
|
|
30
|
+
const thinking = settings.thinkingConfig;
|
|
31
|
+
return {
|
|
32
|
+
provider,
|
|
33
|
+
model: request.model,
|
|
34
|
+
modelParameters: {
|
|
35
|
+
temperature: settings.temperature,
|
|
36
|
+
maxTokens: settings.maxOutputTokens,
|
|
37
|
+
topP: settings.topP,
|
|
38
|
+
thinkingLevel: thinking?.thinkingLevel,
|
|
39
|
+
thinkingBudget: thinking?.thinkingBudget,
|
|
40
|
+
},
|
|
41
|
+
input: { system: settings.systemInstruction, messages: request.contents },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
22
44
|
export function createGeminiLoopAdapter(config) {
|
|
23
45
|
/**
|
|
24
46
|
* Sanitized wire name -> the name the caller registered. Rebuilt per step
|
|
@@ -62,8 +84,10 @@ export function createGeminiLoopAdapter(config) {
|
|
|
62
84
|
const hydratedToolNames = config.declarations
|
|
63
85
|
? refreshNativeToolDeclarations(config.liveTools, config.declarations)
|
|
64
86
|
: [];
|
|
87
|
+
const raw = config.buildRequest(conversation, step);
|
|
65
88
|
return {
|
|
66
|
-
raw
|
|
89
|
+
raw,
|
|
90
|
+
generation: describeGeminiRequest(config.provider, raw),
|
|
67
91
|
...(hydratedToolNames.length > 0 ? { hydratedToolNames } : {}),
|
|
68
92
|
};
|
|
69
93
|
},
|
package/dist/core/loopEngine.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createStreamChannel } from "./streamChannel.js";
|
|
2
|
+
import { observeGeneration } from "../telemetry/observation.js";
|
|
3
|
+
import { StepSpans, withGeneration } from "../telemetry/withSpan.js";
|
|
2
4
|
import { logger } from "../utils/logger.js";
|
|
3
5
|
import { withProviderRetry } from "../utils/providerRetry.js";
|
|
4
6
|
import { resolveToolTimeoutMs } from "./constants.js";
|
|
@@ -266,6 +268,67 @@ async function dispatchStepTools(params) {
|
|
|
266
268
|
}
|
|
267
269
|
return { toolResults, executions, dispatched, abortedMidBatch };
|
|
268
270
|
}
|
|
271
|
+
async function executeObservedStep(adapter, request, channel, signal, span) {
|
|
272
|
+
// Pre-first-chunk 429/5xx retry: watch whether THIS attempt of
|
|
273
|
+
// THIS step pushes anything to the shared channel before it
|
|
274
|
+
// throws. `hasEmitted` resets at the top of every attempt
|
|
275
|
+
// withProviderRetry makes; the instant an attempt emits and then
|
|
276
|
+
// throws, the thrown error is rewrapped as a PostEmissionStepError
|
|
277
|
+
// (no status/branding info survives the rewrap), which
|
|
278
|
+
// isRetryableProviderError() duck-types as non-retryable — so
|
|
279
|
+
// withProviderRetry gives up immediately instead of sleeping and
|
|
280
|
+
// re-invoking executeStep, which would duplicate/interleave
|
|
281
|
+
// output already sent to the consumer. The original error (not
|
|
282
|
+
// the wrapper) is what the caller of runAgenticLoop ultimately
|
|
283
|
+
// sees, via the unwrap in the catch below.
|
|
284
|
+
let hasEmitted = false;
|
|
285
|
+
let firstChunkAt;
|
|
286
|
+
const watchedChannel = {
|
|
287
|
+
push: (chunk) => {
|
|
288
|
+
hasEmitted = true;
|
|
289
|
+
firstChunkAt ??= new Date();
|
|
290
|
+
channel.push(chunk);
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
return withGeneration(request.generation ?? {}, async (generationSpan) => {
|
|
294
|
+
let result;
|
|
295
|
+
try {
|
|
296
|
+
result = await withProviderRetry(async () => {
|
|
297
|
+
hasEmitted = false;
|
|
298
|
+
try {
|
|
299
|
+
return await adapter.executeStep(request, watchedChannel, signal);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
throw hasEmitted ? new PostEmissionStepError(err) : err;
|
|
303
|
+
}
|
|
304
|
+
}, span, `${adapter.providerLabel}.step`, undefined, signal);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
throw err instanceof PostEmissionStepError ? err.cause : err;
|
|
308
|
+
}
|
|
309
|
+
observeGeneration(generationSpan, {
|
|
310
|
+
provider: request.generation?.provider,
|
|
311
|
+
model: request.generation?.model,
|
|
312
|
+
responseModel: result.model,
|
|
313
|
+
output: {
|
|
314
|
+
text: result.text,
|
|
315
|
+
...(result.reasoning ? { reasoning: result.reasoning } : {}),
|
|
316
|
+
...(result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}),
|
|
317
|
+
},
|
|
318
|
+
usage: {
|
|
319
|
+
inputTokens: result.usage.inputTokens,
|
|
320
|
+
outputTokens: result.usage.outputTokens,
|
|
321
|
+
cacheReadTokens: result.usage.cacheReadTokens,
|
|
322
|
+
cacheCreationTokens: result.usage.cacheWriteTokens,
|
|
323
|
+
reasoningTokens: result.usage.reasoningTokens,
|
|
324
|
+
},
|
|
325
|
+
finishReason: result.rawStopReason,
|
|
326
|
+
aborted: signal.aborted,
|
|
327
|
+
completionStartTime: firstChunkAt,
|
|
328
|
+
});
|
|
329
|
+
return result;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
269
332
|
/**
|
|
270
333
|
* Run one adapter-parameterized agentic tool-calling turn. Owns the
|
|
271
334
|
* maxSteps-bounded loop, generic tool dispatch (with an opt-in
|
|
@@ -307,11 +370,13 @@ export function runAgenticLoop(adapter, initialConversation, options) {
|
|
|
307
370
|
// ever writes to it, so the loop is the same loop it was.
|
|
308
371
|
let stepCap = adapter.maxSteps;
|
|
309
372
|
const turnStartedAt = Date.now();
|
|
373
|
+
const stepSpans = new StepSpans();
|
|
310
374
|
try {
|
|
311
375
|
for (let step = 0; step < stepCap; step++) {
|
|
312
376
|
if (internalAbort.signal.aborted) {
|
|
313
377
|
break;
|
|
314
378
|
}
|
|
379
|
+
stepSpans.begin(step + 1);
|
|
315
380
|
if (adapter.planReclaim) {
|
|
316
381
|
const reclaimed = adapter.planReclaim(conversation, step);
|
|
317
382
|
if (reclaimed?.conversation !== undefined) {
|
|
@@ -333,44 +398,7 @@ export function runAgenticLoop(adapter, initialConversation, options) {
|
|
|
333
398
|
for (const name of request.hydratedToolNames ?? []) {
|
|
334
399
|
failedTools.delete(name);
|
|
335
400
|
}
|
|
336
|
-
|
|
337
|
-
// THIS step pushes anything to the shared channel before it
|
|
338
|
-
// throws. `hasEmitted` resets at the top of every attempt
|
|
339
|
-
// withProviderRetry makes; the instant an attempt emits and then
|
|
340
|
-
// throws, the thrown error is rewrapped as a PostEmissionStepError
|
|
341
|
-
// (no status/branding info survives the rewrap), which
|
|
342
|
-
// isRetryableProviderError() duck-types as non-retryable — so
|
|
343
|
-
// withProviderRetry gives up immediately instead of sleeping and
|
|
344
|
-
// re-invoking executeStep, which would duplicate/interleave
|
|
345
|
-
// output already sent to the consumer. The original error (not
|
|
346
|
-
// the wrapper) is what the caller of runAgenticLoop ultimately
|
|
347
|
-
// sees, via the unwrap in the catch below.
|
|
348
|
-
let hasEmitted = false;
|
|
349
|
-
const watchedChannel = {
|
|
350
|
-
push: (chunk) => {
|
|
351
|
-
hasEmitted = true;
|
|
352
|
-
channel.push(chunk);
|
|
353
|
-
},
|
|
354
|
-
};
|
|
355
|
-
let stepResult;
|
|
356
|
-
try {
|
|
357
|
-
stepResult = await withProviderRetry(async () => {
|
|
358
|
-
hasEmitted = false;
|
|
359
|
-
try {
|
|
360
|
-
return await adapter.executeStep(request, watchedChannel, internalAbort.signal);
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
throw hasEmitted ? new PostEmissionStepError(err) : err;
|
|
364
|
-
}
|
|
365
|
-
},
|
|
366
|
-
// The caller's span, when it passes one. withProviderRetry writes
|
|
367
|
-
// gen_ai.provider.total_attempts here, so a loop that threaded a
|
|
368
|
-
// span before it moved onto this engine keeps emitting it.
|
|
369
|
-
options.span, `${adapter.providerLabel}.step`, undefined, internalAbort.signal);
|
|
370
|
-
}
|
|
371
|
-
catch (err) {
|
|
372
|
-
throw err instanceof PostEmissionStepError ? err.cause : err;
|
|
373
|
-
}
|
|
401
|
+
const stepResult = await stepSpans.run(() => executeObservedStep(adapter, request, channel, internalAbort.signal, options.span));
|
|
374
402
|
usage = sumUsage(usage, stepResult.usage);
|
|
375
403
|
rawStopReason = stepResult.rawStopReason;
|
|
376
404
|
lastStepText = stepResult.text || lastStepText;
|
|
@@ -391,14 +419,14 @@ export function runAgenticLoop(adapter, initialConversation, options) {
|
|
|
391
419
|
if (step === stepCap - 1) {
|
|
392
420
|
hadToolCallsAtCap = true;
|
|
393
421
|
}
|
|
394
|
-
const dispatch = await dispatchStepTools({
|
|
422
|
+
const dispatch = await stepSpans.run(() => dispatchStepTools({
|
|
395
423
|
calls: stepResult.toolCalls,
|
|
396
424
|
adapter,
|
|
397
425
|
tools: options.tools,
|
|
398
426
|
failedTools,
|
|
399
427
|
abortSignal: internalAbort.signal,
|
|
400
428
|
toolTimeoutMs: resolveToolTimeoutMs(options.toolTimeoutMs),
|
|
401
|
-
});
|
|
429
|
+
}));
|
|
402
430
|
const toolResults = dispatch.toolResults;
|
|
403
431
|
allToolCalls.push(...dispatch.dispatched);
|
|
404
432
|
allToolExecutions.push(...dispatch.executions);
|
|
@@ -510,6 +538,7 @@ export function runAgenticLoop(adapter, initialConversation, options) {
|
|
|
510
538
|
throw err;
|
|
511
539
|
}
|
|
512
540
|
finally {
|
|
541
|
+
stepSpans.end();
|
|
513
542
|
// close() after error() is harmless: it only flips `done`, and
|
|
514
543
|
// streamChannel.error() keeps its error state intact regardless of a
|
|
515
544
|
// later close() call.
|
|
@@ -72,7 +72,7 @@ export class MessageBuilder {
|
|
|
72
72
|
*/
|
|
73
73
|
async buildMessages(options) {
|
|
74
74
|
return withSpan({
|
|
75
|
-
name: "
|
|
75
|
+
name: "message.build",
|
|
76
76
|
tracer: tracers.sdk,
|
|
77
77
|
attributes: {
|
|
78
78
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -220,7 +220,7 @@ export class MessageBuilder {
|
|
|
220
220
|
*/
|
|
221
221
|
async buildMessagesForStream(options) {
|
|
222
222
|
return withSpan({
|
|
223
|
-
name: "
|
|
223
|
+
name: "message.build_for_stream",
|
|
224
224
|
tracer: tracers.sdk,
|
|
225
225
|
attributes: {
|
|
226
226
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -35,7 +35,7 @@ export class StreamHandler {
|
|
|
35
35
|
* Validate stream options - consolidates validation from 7/10 providers
|
|
36
36
|
*/
|
|
37
37
|
validateStreamOptions(options) {
|
|
38
|
-
const span = tracers.stream.startSpan("
|
|
38
|
+
const span = tracers.stream.startSpan("stream.validate", {
|
|
39
39
|
attributes: {
|
|
40
40
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
41
41
|
[ATTR.NL_MODEL]: this.modelName,
|
|
@@ -189,7 +189,7 @@ export class StreamHandler {
|
|
|
189
189
|
*/
|
|
190
190
|
async createStreamAnalytics(result, startTime, options) {
|
|
191
191
|
return withSpan({
|
|
192
|
-
name: "
|
|
192
|
+
name: "stream.analytics",
|
|
193
193
|
tracer: tracers.stream,
|
|
194
194
|
attributes: {
|
|
195
195
|
[ATTR.NL_PROVIDER]: this.providerName,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telemetry Handler Module
|
|
3
3
|
*
|
|
4
|
-
* Handles analytics, evaluation
|
|
4
|
+
* Handles analytics, evaluation and performance metrics.
|
|
5
5
|
* Extracted from BaseProvider to follow Single Responsibility Principle.
|
|
6
6
|
*
|
|
7
7
|
* Responsibilities:
|
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
* - Evaluation generation
|
|
10
10
|
* - Performance metrics recording
|
|
11
11
|
* - Cost calculation
|
|
12
|
-
* - Telemetry configuration
|
|
13
12
|
*
|
|
14
13
|
* @module core/modules/TelemetryHandler
|
|
15
14
|
*/
|
|
16
15
|
import type { NeuroLink } from "../../neurolink.js";
|
|
17
|
-
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage
|
|
16
|
+
import type { StreamOptions, AIProviderName, AnalyticsData, EnhancedGenerateResult, EvaluationData, MemoryToolCallRecord, MemoryToolResultRecord, RawUsageObject, TextGenerationOptions, TokenUsage } from "../../types/index.js";
|
|
18
17
|
/**
|
|
19
18
|
* TelemetryHandler class - Handles analytics and telemetry for AI providers
|
|
20
19
|
*/
|
|
@@ -35,26 +34,11 @@ export declare class TelemetryHandler {
|
|
|
35
34
|
* Record performance metrics for a generation
|
|
36
35
|
*/
|
|
37
36
|
recordPerformanceMetrics(usage: RawUsageObject | undefined, responseTime: number): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Record a TTS synthesis failure without affecting the generation result.
|
|
40
|
-
*/
|
|
41
|
-
recordTTSFailure(ttsProvider: string, error: NonNullable<TTSMetadata["error"]>, latency: number): void;
|
|
42
37
|
/**
|
|
43
38
|
* Estimate cost only from known per-model rates. Unknown pricing remains
|
|
44
39
|
* absent from cost metrics instead of using another model's default rate.
|
|
45
40
|
*/
|
|
46
41
|
calculateActualCost(usage: TokenUsage): Promise<number | undefined>;
|
|
47
|
-
/**
|
|
48
|
-
* Create telemetry configuration for Vercel AI SDK experimental_telemetry
|
|
49
|
-
* This enables automatic OpenTelemetry tracing when telemetry is enabled
|
|
50
|
-
*/
|
|
51
|
-
getTelemetryConfig(options: StreamOptions | TextGenerationOptions, operationType?: "stream" | "generate"): {
|
|
52
|
-
isEnabled: boolean;
|
|
53
|
-
functionId?: string;
|
|
54
|
-
metadata?: Record<string, string | number | boolean>;
|
|
55
|
-
recordInputs?: boolean;
|
|
56
|
-
recordOutputs?: boolean;
|
|
57
|
-
} | undefined;
|
|
58
42
|
/**
|
|
59
43
|
* Handle tool execution storage if available
|
|
60
44
|
*/
|