@grafana/sigil-sdk-js-core 0.0.0 → 0.6.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/LICENSE +2 -200
- package/README.md +34 -1
- package/dist/cache-diagnostics.d.ts +15 -0
- package/dist/cache-diagnostics.d.ts.map +1 -0
- package/dist/cache-diagnostics.js +15 -0
- package/dist/cache-diagnostics.js.map +1 -0
- package/dist/client.d.ts +138 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1855 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +375 -0
- package/dist/config.js.map +1 -0
- package/dist/content_capture.d.ts +34 -0
- package/dist/content_capture.d.ts.map +1 -0
- package/dist/content_capture.js +121 -0
- package/dist/content_capture.js.map +1 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +80 -0
- package/dist/context.js.map +1 -0
- package/dist/core.d.ts +13 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +12 -0
- package/dist/core.js.map +1 -0
- package/dist/exporters/default.d.ts +3 -0
- package/dist/exporters/default.d.ts.map +1 -0
- package/dist/exporters/default.js +77 -0
- package/dist/exporters/default.js.map +1 -0
- package/dist/exporters/grpc.d.ts +14 -0
- package/dist/exporters/grpc.d.ts.map +1 -0
- package/dist/exporters/grpc.js +375 -0
- package/dist/exporters/grpc.js.map +1 -0
- package/dist/exporters/http.d.ts +10 -0
- package/dist/exporters/http.d.ts.map +1 -0
- package/dist/exporters/http.js +280 -0
- package/dist/exporters/http.js.map +1 -0
- package/dist/hooks.d.ts +33 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +464 -0
- package/dist/hooks.js.map +1 -0
- package/dist/redaction.d.ts +16 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +155 -0
- package/dist/redaction.js.map +1 -0
- package/dist/types.d.ts +597 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +355 -0
- package/dist/utils.js.map +1 -0
- package/package.json +46 -8
- package/proto/sigil/v1/generation_ingest.proto +187 -0
- package/index.js +0 -1
package/dist/client.js
ADDED
|
@@ -0,0 +1,1855 @@
|
|
|
1
|
+
import { context, metrics, SpanKind, SpanStatusCode, trace, } from '@opentelemetry/api';
|
|
2
|
+
import { CACHE_DIAGNOSTICS_MISS_REASON_KEY, CACHE_DIAGNOSTICS_MISSED_INPUT_TOKENS_KEY, CACHE_DIAGNOSTICS_PREVIOUS_MESSAGE_ID_KEY, } from './cache-diagnostics.js';
|
|
3
|
+
import { defaultLogger, mergeConfig } from './config.js';
|
|
4
|
+
import { callContentCaptureResolver, resolveClientContentCaptureMode, resolveContentCaptureMode, shouldIncludeToolContent, stampContentCaptureMetadata, stripContent, } from './content_capture.js';
|
|
5
|
+
import { agentNameFromContext, agentVersionFromContext, conversationIdFromContext, conversationTitleFromContext, userIdFromContext, } from './context.js';
|
|
6
|
+
import { createDefaultGenerationExporter } from './exporters/default.js';
|
|
7
|
+
import { evaluateHook as evaluateHookImpl } from './hooks.js';
|
|
8
|
+
import { asError, cloneArtifact, cloneEmbeddingResult, cloneEmbeddingStart, cloneGeneration, cloneGenerationResult, cloneGenerationStart, cloneMessage, cloneModelRef, cloneToolDefinition, cloneToolExecution, cloneToolExecutionResult, cloneToolExecutionStart, defaultOperationNameForMode, defaultSleep, encodedSizeBytes, maybeUnref, newLocalID, validateEmbeddingResult, validateEmbeddingStart, validateGeneration, validateToolExecution, } from './utils.js';
|
|
9
|
+
const spanAttrGenerationID = 'sigil.generation.id';
|
|
10
|
+
const spanAttrSDKName = 'sigil.sdk.name';
|
|
11
|
+
const spanAttrFrameworkRunID = 'sigil.framework.run_id';
|
|
12
|
+
const spanAttrFrameworkThreadID = 'sigil.framework.thread_id';
|
|
13
|
+
const spanAttrFrameworkParentRunID = 'sigil.framework.parent_run_id';
|
|
14
|
+
const spanAttrFrameworkComponentName = 'sigil.framework.component_name';
|
|
15
|
+
const spanAttrFrameworkRunType = 'sigil.framework.run_type';
|
|
16
|
+
const spanAttrFrameworkRetryAttempt = 'sigil.framework.retry_attempt';
|
|
17
|
+
const spanAttrFrameworkLangGraphNode = 'sigil.framework.langgraph.node';
|
|
18
|
+
const spanAttrFrameworkEventID = 'sigil.framework.event_id';
|
|
19
|
+
const spanAttrConversationID = 'gen_ai.conversation.id';
|
|
20
|
+
const spanAttrConversationTitle = 'sigil.conversation.title';
|
|
21
|
+
const spanAttrUserID = 'user.id';
|
|
22
|
+
const spanAttrAgentName = 'gen_ai.agent.name';
|
|
23
|
+
const spanAttrAgentVersion = 'gen_ai.agent.version';
|
|
24
|
+
const spanAttrErrorType = 'error.type';
|
|
25
|
+
const spanAttrErrorCategory = 'error.category';
|
|
26
|
+
const spanAttrOperationName = 'gen_ai.operation.name';
|
|
27
|
+
const spanAttrProviderName = 'gen_ai.provider.name';
|
|
28
|
+
const spanAttrRequestModel = 'gen_ai.request.model';
|
|
29
|
+
const spanAttrRequestMaxTokens = 'gen_ai.request.max_tokens';
|
|
30
|
+
const spanAttrRequestTemperature = 'gen_ai.request.temperature';
|
|
31
|
+
const spanAttrRequestTopP = 'gen_ai.request.top_p';
|
|
32
|
+
const spanAttrRequestToolChoice = 'sigil.gen_ai.request.tool_choice';
|
|
33
|
+
const spanAttrRequestThinkingEnabled = 'sigil.gen_ai.request.thinking.enabled';
|
|
34
|
+
const spanAttrRequestThinkingBudget = 'sigil.gen_ai.request.thinking.budget_tokens';
|
|
35
|
+
const spanAttrResponseID = 'gen_ai.response.id';
|
|
36
|
+
const spanAttrResponseModel = 'gen_ai.response.model';
|
|
37
|
+
const spanAttrFinishReasons = 'gen_ai.response.finish_reasons';
|
|
38
|
+
const spanAttrInputTokens = 'gen_ai.usage.input_tokens';
|
|
39
|
+
const spanAttrOutputTokens = 'gen_ai.usage.output_tokens';
|
|
40
|
+
const spanAttrEmbeddingInputCount = 'gen_ai.embeddings.input_count';
|
|
41
|
+
const spanAttrEmbeddingInputTexts = 'gen_ai.embeddings.input_texts';
|
|
42
|
+
const spanAttrEmbeddingDimCount = 'gen_ai.embeddings.dimension.count';
|
|
43
|
+
const spanAttrRequestEncodingFormats = 'gen_ai.request.encoding_formats';
|
|
44
|
+
const spanAttrCacheReadTokens = 'gen_ai.usage.cache_read_input_tokens';
|
|
45
|
+
const spanAttrCacheWriteTokens = 'gen_ai.usage.cache_write_input_tokens';
|
|
46
|
+
const spanAttrReasoningTokens = 'gen_ai.usage.reasoning_tokens';
|
|
47
|
+
const spanAttrToolName = 'gen_ai.tool.name';
|
|
48
|
+
const spanAttrToolCallID = 'gen_ai.tool.call.id';
|
|
49
|
+
const spanAttrToolType = 'gen_ai.tool.type';
|
|
50
|
+
const spanAttrToolDescription = 'gen_ai.tool.description';
|
|
51
|
+
const spanAttrToolCallArguments = 'gen_ai.tool.call.arguments';
|
|
52
|
+
const spanAttrToolCallResult = 'gen_ai.tool.call.result';
|
|
53
|
+
const maxRatingConversationIdLen = 255;
|
|
54
|
+
const maxRatingIdLen = 128;
|
|
55
|
+
const maxRatingGenerationIdLen = 255;
|
|
56
|
+
const maxRatingActorIdLen = 255;
|
|
57
|
+
const maxRatingSourceLen = 64;
|
|
58
|
+
const maxRatingCommentBytes = 4096;
|
|
59
|
+
const maxRatingMetadataBytes = 16 * 1024;
|
|
60
|
+
const metricOperationDuration = 'gen_ai.client.operation.duration';
|
|
61
|
+
const metricTokenUsage = 'gen_ai.client.token.usage';
|
|
62
|
+
const metricTimeToFirstToken = 'gen_ai.client.time_to_first_token';
|
|
63
|
+
const metricToolCallsPerOperation = 'gen_ai.client.tool_calls_per_operation';
|
|
64
|
+
const metricAttrTokenType = 'gen_ai.token.type';
|
|
65
|
+
const metricTokenTypeInput = 'input';
|
|
66
|
+
const metricTokenTypeOutput = 'output';
|
|
67
|
+
const metricTokenTypeCacheRead = 'cache_read';
|
|
68
|
+
const metricTokenTypeCacheWrite = 'cache_write';
|
|
69
|
+
const metricTokenTypeReasoning = 'reasoning';
|
|
70
|
+
const durationBucketsSeconds = [
|
|
71
|
+
0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92,
|
|
72
|
+
];
|
|
73
|
+
const tokenUsageBuckets = [
|
|
74
|
+
1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864,
|
|
75
|
+
];
|
|
76
|
+
const instrumentationName = 'github.com/grafana/sigil/sdks/js';
|
|
77
|
+
const sdkName = 'sdk-js';
|
|
78
|
+
const defaultEmbeddingOperationName = 'embeddings';
|
|
79
|
+
const metadataUserIDKey = 'sigil.user.id';
|
|
80
|
+
const metadataLegacyUserIDKey = 'user.id';
|
|
81
|
+
function serializeToolResultPayload(value) {
|
|
82
|
+
if (value == null) {
|
|
83
|
+
return { content: '' };
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return { content: value };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return { content: '', contentJSON: JSON.stringify(value) };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return { content: String(value) };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function buildToolResultMessage(toolName, toolCallId, result, isError, errorText) {
|
|
96
|
+
if (isError) {
|
|
97
|
+
return {
|
|
98
|
+
role: 'tool',
|
|
99
|
+
name: toolName,
|
|
100
|
+
parts: [
|
|
101
|
+
{
|
|
102
|
+
type: 'tool_result',
|
|
103
|
+
toolResult: {
|
|
104
|
+
toolCallId,
|
|
105
|
+
name: toolName,
|
|
106
|
+
content: errorText,
|
|
107
|
+
isError: true,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const { content, contentJSON } = serializeToolResultPayload(result);
|
|
114
|
+
const toolResult = contentJSON !== undefined
|
|
115
|
+
? { toolCallId, name: toolName, content, contentJSON }
|
|
116
|
+
: { toolCallId, name: toolName, content };
|
|
117
|
+
return {
|
|
118
|
+
role: 'tool',
|
|
119
|
+
name: toolName,
|
|
120
|
+
parts: [{ type: 'tool_result', toolResult }],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export class SigilClient {
|
|
124
|
+
config;
|
|
125
|
+
nowFn;
|
|
126
|
+
sleepFn;
|
|
127
|
+
logger;
|
|
128
|
+
generationExporter;
|
|
129
|
+
tracer;
|
|
130
|
+
meter;
|
|
131
|
+
operationDurationHistogram;
|
|
132
|
+
tokenUsageHistogram;
|
|
133
|
+
ttftHistogram;
|
|
134
|
+
toolCallsHistogram;
|
|
135
|
+
generations = [];
|
|
136
|
+
toolExecutions = [];
|
|
137
|
+
pendingGenerations = [];
|
|
138
|
+
flushPromise;
|
|
139
|
+
flushRequested = false;
|
|
140
|
+
flushTimer;
|
|
141
|
+
shutdownPromise;
|
|
142
|
+
shuttingDown = false;
|
|
143
|
+
closed = false;
|
|
144
|
+
/**
|
|
145
|
+
* Creates a Sigil SDK client.
|
|
146
|
+
*
|
|
147
|
+
* `inputConfig` is merged with defaults.
|
|
148
|
+
*/
|
|
149
|
+
constructor(inputConfig = {}) {
|
|
150
|
+
this.config = mergeConfig(inputConfig);
|
|
151
|
+
this.nowFn = this.config.now ?? (() => new Date());
|
|
152
|
+
this.sleepFn = this.config.sleep ?? defaultSleep;
|
|
153
|
+
this.logger = this.config.logger ?? defaultLogger;
|
|
154
|
+
this.generationExporter =
|
|
155
|
+
this.config.generationExporter ?? createDefaultGenerationExporter(this.config.generationExport);
|
|
156
|
+
this.tracer = this.config.tracer ?? trace.getTracer(instrumentationName);
|
|
157
|
+
this.meter = this.config.meter ?? metrics.getMeter(instrumentationName);
|
|
158
|
+
this.operationDurationHistogram = this.meter.createHistogram(metricOperationDuration, {
|
|
159
|
+
unit: 's',
|
|
160
|
+
advice: { explicitBucketBoundaries: durationBucketsSeconds },
|
|
161
|
+
});
|
|
162
|
+
this.tokenUsageHistogram = this.meter.createHistogram(metricTokenUsage, {
|
|
163
|
+
unit: 'token',
|
|
164
|
+
advice: { explicitBucketBoundaries: tokenUsageBuckets },
|
|
165
|
+
});
|
|
166
|
+
this.ttftHistogram = this.meter.createHistogram(metricTimeToFirstToken, {
|
|
167
|
+
unit: 's',
|
|
168
|
+
advice: { explicitBucketBoundaries: durationBucketsSeconds },
|
|
169
|
+
});
|
|
170
|
+
this.toolCallsHistogram = this.meter.createHistogram(metricToolCallsPerOperation, { unit: 'count' });
|
|
171
|
+
if (this.config.generationExport.flushIntervalMs > 0) {
|
|
172
|
+
this.flushTimer = setInterval(() => {
|
|
173
|
+
this.triggerAsyncFlush();
|
|
174
|
+
}, this.config.generationExport.flushIntervalMs);
|
|
175
|
+
maybeUnref(this.flushTimer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
startGeneration(start, callback) {
|
|
179
|
+
return this.startGenerationWithMode(start, 'SYNC', callback);
|
|
180
|
+
}
|
|
181
|
+
startStreamingGeneration(start, callback) {
|
|
182
|
+
return this.startGenerationWithMode(start, 'STREAM', callback);
|
|
183
|
+
}
|
|
184
|
+
startEmbedding(start, callback) {
|
|
185
|
+
this.assertOpen();
|
|
186
|
+
const seed = cloneEmbeddingStart(start);
|
|
187
|
+
if (!notEmpty(seed.agentName)) {
|
|
188
|
+
seed.agentName = agentNameFromContext();
|
|
189
|
+
}
|
|
190
|
+
if (!notEmpty(seed.agentName)) {
|
|
191
|
+
const fromConfig = this.internalAgentName();
|
|
192
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
193
|
+
seed.agentName = fromConfig;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!notEmpty(seed.agentVersion)) {
|
|
197
|
+
seed.agentVersion = agentVersionFromContext();
|
|
198
|
+
}
|
|
199
|
+
if (!notEmpty(seed.agentVersion)) {
|
|
200
|
+
const fromConfig = this.internalAgentVersion();
|
|
201
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
202
|
+
seed.agentVersion = fromConfig;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const recorder = new EmbeddingRecorderImpl(this, seed);
|
|
206
|
+
if (callback === undefined) {
|
|
207
|
+
return recorder;
|
|
208
|
+
}
|
|
209
|
+
return runWithRecorder(recorder, callback);
|
|
210
|
+
}
|
|
211
|
+
startToolExecution(start, callback) {
|
|
212
|
+
this.assertOpen();
|
|
213
|
+
const recorder = start.toolName.trim().length === 0 ? new NoopToolExecutionRecorder() : new ToolExecutionRecorderImpl(this, start);
|
|
214
|
+
if (callback === undefined) {
|
|
215
|
+
return recorder;
|
|
216
|
+
}
|
|
217
|
+
return runWithRecorder(recorder, callback);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Runs each `tool_call` part under `execute_tool` spans and returns tool messages.
|
|
221
|
+
*
|
|
222
|
+
* Walks `messages` (typically `GenerationResult.output`) and invokes `executor`
|
|
223
|
+
* for every tool-call part. Returns `tool` role messages with `tool_result` parts.
|
|
224
|
+
*/
|
|
225
|
+
async executeToolCalls(messages, executor, options = {}) {
|
|
226
|
+
this.assertOpen();
|
|
227
|
+
const opts = options;
|
|
228
|
+
const out = [];
|
|
229
|
+
const list = messages ?? [];
|
|
230
|
+
for (const msg of list) {
|
|
231
|
+
for (const part of msg.parts ?? []) {
|
|
232
|
+
if (part.type !== 'tool_call') {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const tc = part.toolCall;
|
|
236
|
+
const name = (tc.name ?? '').trim();
|
|
237
|
+
if (name.length === 0) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const callId = (tc.id ?? '').trim();
|
|
241
|
+
const raw = tc.inputJSON?.trim() ?? '';
|
|
242
|
+
let args = {};
|
|
243
|
+
if (raw.length > 0) {
|
|
244
|
+
try {
|
|
245
|
+
args = JSON.parse(raw);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
args = raw;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const rec = this.startToolExecution({
|
|
252
|
+
toolName: name,
|
|
253
|
+
toolCallId: callId.length > 0 ? callId : undefined,
|
|
254
|
+
toolType: opts.toolType ?? 'function',
|
|
255
|
+
conversationId: opts.conversationId,
|
|
256
|
+
conversationTitle: opts.conversationTitle,
|
|
257
|
+
agentName: opts.agentName,
|
|
258
|
+
agentVersion: opts.agentVersion,
|
|
259
|
+
requestModel: opts.requestModel,
|
|
260
|
+
requestProvider: opts.requestProvider,
|
|
261
|
+
contentCapture: opts.contentCapture,
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
const result = await executor(name, args);
|
|
265
|
+
rec.setResult({ arguments: args, result });
|
|
266
|
+
out.push(buildToolResultMessage(name, callId, result, false, ''));
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
rec.setCallError(err);
|
|
270
|
+
const msgText = err instanceof Error ? err.message : String(err);
|
|
271
|
+
out.push(buildToolResultMessage(name, callId, null, true, msgText));
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
rec.end();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Submits a user-facing conversation rating through Sigil HTTP API. */
|
|
281
|
+
async submitConversationRating(conversationId, input) {
|
|
282
|
+
this.assertOpen();
|
|
283
|
+
const normalizedConversationId = conversationId.trim();
|
|
284
|
+
if (normalizedConversationId.length === 0) {
|
|
285
|
+
throw new Error('sigil conversation rating validation failed: conversationId is required');
|
|
286
|
+
}
|
|
287
|
+
if (normalizedConversationId.length > maxRatingConversationIdLen) {
|
|
288
|
+
throw new Error('sigil conversation rating validation failed: conversationId is too long');
|
|
289
|
+
}
|
|
290
|
+
const normalizedInput = normalizeConversationRatingInput(input);
|
|
291
|
+
const endpoint = buildConversationRatingEndpoint(this.config.api.endpoint, this.config.generationExport.insecure, normalizedConversationId);
|
|
292
|
+
const requestBody = {
|
|
293
|
+
rating_id: normalizedInput.ratingId,
|
|
294
|
+
rating: normalizedInput.rating,
|
|
295
|
+
comment: normalizedInput.comment,
|
|
296
|
+
metadata: normalizedInput.metadata,
|
|
297
|
+
generation_id: normalizedInput.generationId,
|
|
298
|
+
rater_id: normalizedInput.raterId,
|
|
299
|
+
source: normalizedInput.source,
|
|
300
|
+
};
|
|
301
|
+
const response = await fetch(endpoint, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: {
|
|
304
|
+
'content-type': 'application/json',
|
|
305
|
+
...this.config.generationExport.headers,
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify(requestBody),
|
|
308
|
+
});
|
|
309
|
+
const responseText = (await response.text()).trim();
|
|
310
|
+
if (response.status === 400) {
|
|
311
|
+
throw new Error(`sigil conversation rating validation failed: ${ratingErrorText(responseText, response.status)}`);
|
|
312
|
+
}
|
|
313
|
+
if (response.status === 409) {
|
|
314
|
+
throw new Error(`sigil conversation rating conflict: ${ratingErrorText(responseText, response.status)}`);
|
|
315
|
+
}
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
throw new Error(`sigil conversation rating transport failed: status ${response.status}: ${ratingErrorText(responseText, response.status)}`);
|
|
318
|
+
}
|
|
319
|
+
if (responseText.length === 0) {
|
|
320
|
+
throw new Error('sigil conversation rating transport failed: empty response payload');
|
|
321
|
+
}
|
|
322
|
+
let payload;
|
|
323
|
+
try {
|
|
324
|
+
payload = JSON.parse(responseText);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
throw new Error(`sigil conversation rating transport failed: invalid JSON response: ${asError(error).message}`);
|
|
328
|
+
}
|
|
329
|
+
return parseSubmitConversationRatingResponse(payload);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Returns the resolved hook configuration. Framework adapters use this to
|
|
333
|
+
* decide whether to invoke `evaluateHook` and which phases are configured.
|
|
334
|
+
*/
|
|
335
|
+
get hooksConfig() {
|
|
336
|
+
return this.config.hooks;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Evaluates synchronous hook rules for the given request.
|
|
340
|
+
*
|
|
341
|
+
* Use this to enforce preflight or postflight guardrails (PII, content
|
|
342
|
+
* policy, etc.) on the LLM call's critical path. The server returns
|
|
343
|
+
* `{ action: 'deny' }` to block; framework adapters typically translate that
|
|
344
|
+
* into a `HookDeniedError`.
|
|
345
|
+
*
|
|
346
|
+
* When `hooks.enabled` is false, this short-circuits to `allow`. When
|
|
347
|
+
* `hooks.failOpen` is true (default), network/timeout failures also resolve
|
|
348
|
+
* to `allow` so the LLM call can proceed.
|
|
349
|
+
*
|
|
350
|
+
* Framework adapters can pass `hooksConfigOverride` to override specific
|
|
351
|
+
* fields of the client's hooks config (e.g., force `enabled: true` when the
|
|
352
|
+
* adapter has its own `enableHooks` option).
|
|
353
|
+
*/
|
|
354
|
+
async evaluateHook(request, hooksConfigOverride) {
|
|
355
|
+
this.assertOpen();
|
|
356
|
+
const effectiveHooks = hooksConfigOverride !== undefined ? { ...this.config.hooks, ...hooksConfigOverride } : this.config.hooks;
|
|
357
|
+
return evaluateHookImpl({
|
|
358
|
+
apiEndpoint: this.config.api.endpoint,
|
|
359
|
+
insecure: this.config.generationExport.insecure,
|
|
360
|
+
extraHeaders: this.config.generationExport.headers,
|
|
361
|
+
hooks: effectiveHooks,
|
|
362
|
+
request,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
/** Forces immediate drain of queued generation exports. */
|
|
366
|
+
async flush() {
|
|
367
|
+
this.assertOpen();
|
|
368
|
+
await this.flushInternal();
|
|
369
|
+
}
|
|
370
|
+
/** Flushes pending generations and shuts down the generation exporter. */
|
|
371
|
+
async shutdown() {
|
|
372
|
+
if (this.shutdownPromise !== undefined) {
|
|
373
|
+
await this.shutdownPromise;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
this.shuttingDown = true;
|
|
377
|
+
this.shutdownPromise = (async () => {
|
|
378
|
+
this.stopFlushTimer();
|
|
379
|
+
try {
|
|
380
|
+
await this.flushInternal();
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
this.logWarn('sigil generation export flush on shutdown failed', error);
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
await this.generationExporter.shutdown?.();
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
this.logWarn('sigil generation exporter shutdown failed', error);
|
|
390
|
+
}
|
|
391
|
+
this.closed = true;
|
|
392
|
+
})();
|
|
393
|
+
await this.shutdownPromise;
|
|
394
|
+
}
|
|
395
|
+
/** Returns a cloned in-memory snapshot for debugging and tests. */
|
|
396
|
+
debugSnapshot() {
|
|
397
|
+
return {
|
|
398
|
+
generations: this.generations.map(cloneGeneration),
|
|
399
|
+
toolExecutions: this.toolExecutions.map(cloneToolExecution),
|
|
400
|
+
queueSize: this.pendingGenerations.length,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
internalNow() {
|
|
404
|
+
return this.nowFn();
|
|
405
|
+
}
|
|
406
|
+
internalAgentName() {
|
|
407
|
+
return this.config.agentName;
|
|
408
|
+
}
|
|
409
|
+
internalAgentVersion() {
|
|
410
|
+
return this.config.agentVersion;
|
|
411
|
+
}
|
|
412
|
+
internalUserId() {
|
|
413
|
+
return this.config.userId;
|
|
414
|
+
}
|
|
415
|
+
internalTags() {
|
|
416
|
+
return this.config.tags;
|
|
417
|
+
}
|
|
418
|
+
internalRecordGeneration(generation) {
|
|
419
|
+
this.generations.push(cloneGeneration(generation));
|
|
420
|
+
}
|
|
421
|
+
internalRecordToolExecution(toolExecution) {
|
|
422
|
+
this.toolExecutions.push(cloneToolExecution(toolExecution));
|
|
423
|
+
}
|
|
424
|
+
internalEnqueueGeneration(generation) {
|
|
425
|
+
if (this.shuttingDown || this.closed) {
|
|
426
|
+
throw new Error('sigil client is shutdown');
|
|
427
|
+
}
|
|
428
|
+
const payloadMaxBytes = this.config.generationExport.payloadMaxBytes;
|
|
429
|
+
if (payloadMaxBytes > 0) {
|
|
430
|
+
const payloadBytes = encodedSizeBytes(generation);
|
|
431
|
+
if (payloadBytes > payloadMaxBytes) {
|
|
432
|
+
throw new Error(`generation payload exceeds max bytes (${payloadBytes} > ${payloadMaxBytes})`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const queueSize = Math.max(1, this.config.generationExport.queueSize);
|
|
436
|
+
if (this.pendingGenerations.length >= queueSize) {
|
|
437
|
+
throw new Error('generation queue is full');
|
|
438
|
+
}
|
|
439
|
+
this.pendingGenerations.push(cloneGeneration(generation));
|
|
440
|
+
const batchSize = Math.max(1, this.config.generationExport.batchSize);
|
|
441
|
+
if (this.pendingGenerations.length >= batchSize) {
|
|
442
|
+
this.triggerAsyncFlush();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
internalLogWarn(message, error) {
|
|
446
|
+
this.logWarn(message, error);
|
|
447
|
+
}
|
|
448
|
+
internalResolveGenerationContentCaptureMode(seed) {
|
|
449
|
+
const resolverMode = callContentCaptureResolver(this.config.contentCaptureResolver, seed.metadata);
|
|
450
|
+
const clientMode = resolveClientContentCaptureMode(resolveContentCaptureMode(resolverMode, this.config.contentCapture));
|
|
451
|
+
return resolveContentCaptureMode(seed.contentCapture ?? 'default', clientMode);
|
|
452
|
+
}
|
|
453
|
+
internalResolveEmbeddingContentCaptureMode(seed) {
|
|
454
|
+
// Mirror generation resolution so a per-call resolver can hide
|
|
455
|
+
// gen_ai.embeddings.input_texts without changing the client default.
|
|
456
|
+
const resolverMode = callContentCaptureResolver(this.config.contentCaptureResolver, seed.metadata);
|
|
457
|
+
return resolveClientContentCaptureMode(resolveContentCaptureMode(resolverMode, this.config.contentCapture));
|
|
458
|
+
}
|
|
459
|
+
internalResolveToolContentCaptureMode(seed) {
|
|
460
|
+
const resolverMode = callContentCaptureResolver(this.config.contentCaptureResolver, undefined);
|
|
461
|
+
const clientMode = resolveClientContentCaptureMode(resolveContentCaptureMode(resolverMode, this.config.contentCapture));
|
|
462
|
+
return resolveContentCaptureMode(seed.contentCapture ?? 'default', clientMode);
|
|
463
|
+
}
|
|
464
|
+
internalHasGenerationSanitizer() {
|
|
465
|
+
return this.config.generationSanitizer !== undefined;
|
|
466
|
+
}
|
|
467
|
+
internalSanitizeGeneration(generation) {
|
|
468
|
+
const sanitizer = this.config.generationSanitizer;
|
|
469
|
+
if (sanitizer === undefined) {
|
|
470
|
+
return generation;
|
|
471
|
+
}
|
|
472
|
+
const sanitized = sanitizer(cloneGeneration(generation));
|
|
473
|
+
if (sanitized === undefined) {
|
|
474
|
+
throw new Error('generation sanitizer must return a generation');
|
|
475
|
+
}
|
|
476
|
+
return cloneGeneration(sanitized);
|
|
477
|
+
}
|
|
478
|
+
internalStartGenerationSpan(seed, mode, startedAt, contentCaptureMode) {
|
|
479
|
+
const operationName = seed.operationName ?? defaultOperationNameForMode(mode);
|
|
480
|
+
const span = this.tracer.startSpan(generationSpanName(operationName, seed.model.name), {
|
|
481
|
+
kind: SpanKind.CLIENT,
|
|
482
|
+
startTime: startedAt,
|
|
483
|
+
});
|
|
484
|
+
// metadata_only and full_with_metadata_spans both drop the title from
|
|
485
|
+
// the span. Under full_with_metadata_spans the proto payload still
|
|
486
|
+
// carries the title — it is rebuilt from `seed.conversationTitle` in
|
|
487
|
+
// end(), so we only zero the value sent to the span here.
|
|
488
|
+
const spanTitle = contentCaptureMode === 'metadata_only' || contentCaptureMode === 'full_with_metadata_spans'
|
|
489
|
+
? undefined
|
|
490
|
+
: seed.conversationTitle;
|
|
491
|
+
setGenerationSpanAttributes(span, {
|
|
492
|
+
id: seed.id,
|
|
493
|
+
conversationId: seed.conversationId,
|
|
494
|
+
conversationTitle: spanTitle,
|
|
495
|
+
userId: seed.userId,
|
|
496
|
+
agentName: seed.agentName,
|
|
497
|
+
agentVersion: seed.agentVersion,
|
|
498
|
+
operationName,
|
|
499
|
+
model: seed.model,
|
|
500
|
+
maxTokens: seed.maxTokens,
|
|
501
|
+
temperature: seed.temperature,
|
|
502
|
+
topP: seed.topP,
|
|
503
|
+
toolChoice: seed.toolChoice,
|
|
504
|
+
thinkingEnabled: seed.thinkingEnabled,
|
|
505
|
+
metadata: seed.metadata,
|
|
506
|
+
});
|
|
507
|
+
return span;
|
|
508
|
+
}
|
|
509
|
+
internalStartEmbeddingSpan(seed, startedAt) {
|
|
510
|
+
const span = this.tracer.startSpan(embeddingSpanName(seed.model.name), {
|
|
511
|
+
kind: SpanKind.CLIENT,
|
|
512
|
+
startTime: startedAt,
|
|
513
|
+
});
|
|
514
|
+
setEmbeddingStartSpanAttributes(span, seed);
|
|
515
|
+
return span;
|
|
516
|
+
}
|
|
517
|
+
internalStartToolExecutionSpan(seed, startedAt) {
|
|
518
|
+
const span = this.tracer.startSpan(toolSpanName(seed.toolName), {
|
|
519
|
+
kind: SpanKind.INTERNAL,
|
|
520
|
+
startTime: startedAt,
|
|
521
|
+
});
|
|
522
|
+
setToolSpanAttributes(span, seed);
|
|
523
|
+
return span;
|
|
524
|
+
}
|
|
525
|
+
internalApplyTraceContextFromSpan(span, generation) {
|
|
526
|
+
const context = span.spanContext();
|
|
527
|
+
if (context.traceId.length > 0) {
|
|
528
|
+
generation.traceId = context.traceId;
|
|
529
|
+
}
|
|
530
|
+
if (context.spanId.length > 0) {
|
|
531
|
+
generation.spanId = context.spanId;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
internalSyncGenerationSpan(span, generation) {
|
|
535
|
+
setGenerationSpanAttributes(span, generation);
|
|
536
|
+
}
|
|
537
|
+
internalClearSpanConversationTitle(span) {
|
|
538
|
+
span.setAttribute(spanAttrConversationTitle, '');
|
|
539
|
+
}
|
|
540
|
+
internalFinalizeGenerationSpan(span, generation, callError, validationError, enqueueError, firstTokenAt, precomputedCallErrorCategory) {
|
|
541
|
+
span.updateName(generationSpanName(generation.operationName, generation.model.name));
|
|
542
|
+
if (callError !== undefined) {
|
|
543
|
+
span.recordException(new Error(callError));
|
|
544
|
+
}
|
|
545
|
+
if (validationError !== undefined) {
|
|
546
|
+
span.recordException(validationError);
|
|
547
|
+
}
|
|
548
|
+
if (enqueueError !== undefined) {
|
|
549
|
+
span.recordException(enqueueError);
|
|
550
|
+
}
|
|
551
|
+
let errorType = '';
|
|
552
|
+
let errorCategory = '';
|
|
553
|
+
if (callError !== undefined) {
|
|
554
|
+
errorType = 'provider_call_error';
|
|
555
|
+
errorCategory = precomputedCallErrorCategory ?? errorCategoryFromError(callError, true);
|
|
556
|
+
span.setAttribute(spanAttrErrorType, errorType);
|
|
557
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
558
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: callError });
|
|
559
|
+
}
|
|
560
|
+
else if (validationError !== undefined) {
|
|
561
|
+
errorType = 'validation_error';
|
|
562
|
+
errorCategory = 'sdk_error';
|
|
563
|
+
span.setAttribute(spanAttrErrorType, errorType);
|
|
564
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
565
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: validationError.message });
|
|
566
|
+
}
|
|
567
|
+
else if (enqueueError !== undefined) {
|
|
568
|
+
errorType = 'enqueue_error';
|
|
569
|
+
errorCategory = 'sdk_error';
|
|
570
|
+
span.setAttribute(spanAttrErrorType, errorType);
|
|
571
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
572
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: enqueueError.message });
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
576
|
+
}
|
|
577
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
578
|
+
context.with(spanCtx, () => {
|
|
579
|
+
this.recordGenerationMetrics(generation, errorType, errorCategory, firstTokenAt);
|
|
580
|
+
});
|
|
581
|
+
span.end(generation.completedAt);
|
|
582
|
+
}
|
|
583
|
+
internalFinalizeEmbeddingSpan(span, seed, result, hasResult, callError, localError, startedAt, completedAt, contentCaptureMode = 'default') {
|
|
584
|
+
span.updateName(embeddingSpanName(seed.model.name));
|
|
585
|
+
setEmbeddingEndSpanAttributes(span, result, hasResult, this.config.embeddingCapture, contentCaptureMode);
|
|
586
|
+
// Redact span-side error text under both stripped modes. Embeddings have
|
|
587
|
+
// no proto export, so the raw provider error never escapes the span
|
|
588
|
+
// path; matches the generation full_with_metadata_spans contract.
|
|
589
|
+
const redactSpanErrors = contentCaptureMode === 'metadata_only' || contentCaptureMode === 'full_with_metadata_spans';
|
|
590
|
+
if (callError !== undefined && !redactSpanErrors) {
|
|
591
|
+
span.recordException(callError);
|
|
592
|
+
}
|
|
593
|
+
if (localError !== undefined && !redactSpanErrors) {
|
|
594
|
+
span.recordException(localError);
|
|
595
|
+
}
|
|
596
|
+
let errorType = '';
|
|
597
|
+
let errorCategory = '';
|
|
598
|
+
if (callError !== undefined) {
|
|
599
|
+
errorType = 'provider_call_error';
|
|
600
|
+
errorCategory = errorCategoryFromError(callError, true);
|
|
601
|
+
span.setStatus({
|
|
602
|
+
code: SpanStatusCode.ERROR,
|
|
603
|
+
message: redactSpanErrors ? errorCategory : callError.message,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
else if (localError !== undefined) {
|
|
607
|
+
errorType = 'validation_error';
|
|
608
|
+
errorCategory = 'sdk_error';
|
|
609
|
+
span.setStatus({
|
|
610
|
+
code: SpanStatusCode.ERROR,
|
|
611
|
+
message: redactSpanErrors ? errorCategory : localError.message,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
616
|
+
}
|
|
617
|
+
if (errorType.length > 0) {
|
|
618
|
+
span.setAttribute(spanAttrErrorType, errorType);
|
|
619
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
620
|
+
}
|
|
621
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
622
|
+
context.with(spanCtx, () => {
|
|
623
|
+
this.recordEmbeddingMetrics(seed, result, startedAt, completedAt, errorType, errorCategory);
|
|
624
|
+
});
|
|
625
|
+
span.end(completedAt);
|
|
626
|
+
}
|
|
627
|
+
internalFinalizeToolExecutionSpan(span, toolExecution, localError, contentCaptureMode = 'default') {
|
|
628
|
+
setToolSpanAttributes(span, toolExecution);
|
|
629
|
+
if (toolExecution.includeContent) {
|
|
630
|
+
const argumentsResult = serializeToolContent(toolExecution.arguments);
|
|
631
|
+
if (argumentsResult.error !== undefined && localError === undefined) {
|
|
632
|
+
localError = argumentsResult.error;
|
|
633
|
+
}
|
|
634
|
+
else if (argumentsResult.value !== undefined) {
|
|
635
|
+
span.setAttribute(spanAttrToolCallArguments, argumentsResult.value);
|
|
636
|
+
}
|
|
637
|
+
const resultValue = serializeToolContent(toolExecution.result);
|
|
638
|
+
if (resultValue.error !== undefined && localError === undefined) {
|
|
639
|
+
localError = resultValue.error;
|
|
640
|
+
}
|
|
641
|
+
else if (resultValue.value !== undefined) {
|
|
642
|
+
span.setAttribute(spanAttrToolCallResult, resultValue.value);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Tools have no proto export; under both stripped modes the span must
|
|
646
|
+
// not echo raw provider exception text via recordException events or the
|
|
647
|
+
// status description.
|
|
648
|
+
const redactSpanErrors = contentCaptureMode === 'metadata_only' || contentCaptureMode === 'full_with_metadata_spans';
|
|
649
|
+
if (toolExecution.callError !== undefined) {
|
|
650
|
+
const errorCategory = errorCategoryFromError(toolExecution.callError, true);
|
|
651
|
+
if (!redactSpanErrors) {
|
|
652
|
+
span.recordException(new Error(toolExecution.callError));
|
|
653
|
+
}
|
|
654
|
+
span.setAttribute(spanAttrErrorType, 'tool_execution_error');
|
|
655
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
656
|
+
span.setStatus({
|
|
657
|
+
code: SpanStatusCode.ERROR,
|
|
658
|
+
message: redactSpanErrors ? errorCategory : toolExecution.callError,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else if (localError !== undefined) {
|
|
662
|
+
const errorCategory = errorCategoryFromError(localError, true);
|
|
663
|
+
if (!redactSpanErrors) {
|
|
664
|
+
span.recordException(localError);
|
|
665
|
+
}
|
|
666
|
+
span.setAttribute(spanAttrErrorType, 'tool_execution_error');
|
|
667
|
+
span.setAttribute(spanAttrErrorCategory, errorCategory);
|
|
668
|
+
span.setStatus({
|
|
669
|
+
code: SpanStatusCode.ERROR,
|
|
670
|
+
message: redactSpanErrors ? errorCategory : localError.message,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
675
|
+
}
|
|
676
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
677
|
+
context.with(spanCtx, () => {
|
|
678
|
+
this.recordToolExecutionMetrics(toolExecution, localError ?? (toolExecution.callError !== undefined ? new Error(toolExecution.callError) : undefined));
|
|
679
|
+
});
|
|
680
|
+
span.end(toolExecution.completedAt);
|
|
681
|
+
return localError;
|
|
682
|
+
}
|
|
683
|
+
recordGenerationMetrics(generation, errorType, errorCategory, firstTokenAt) {
|
|
684
|
+
const startedMs = generation.startedAt.getTime();
|
|
685
|
+
const completedMs = generation.completedAt.getTime();
|
|
686
|
+
const durationSeconds = Math.max(0, (completedMs - startedMs) / 1_000);
|
|
687
|
+
const identityAttributes = metricIdentityAttributes(generation.model.provider, generation.model.name, generation.agentName, generation.agentVersion);
|
|
688
|
+
this.operationDurationHistogram.record(durationSeconds, {
|
|
689
|
+
[spanAttrOperationName]: generation.operationName,
|
|
690
|
+
...identityAttributes,
|
|
691
|
+
[spanAttrErrorType]: errorType,
|
|
692
|
+
[spanAttrErrorCategory]: errorCategory,
|
|
693
|
+
});
|
|
694
|
+
const usage = generation.usage;
|
|
695
|
+
if (usage !== undefined) {
|
|
696
|
+
this.recordTokenUsage(generation, metricTokenTypeInput, usage.inputTokens);
|
|
697
|
+
this.recordTokenUsage(generation, metricTokenTypeOutput, usage.outputTokens);
|
|
698
|
+
this.recordTokenUsage(generation, metricTokenTypeCacheRead, usage.cacheReadInputTokens);
|
|
699
|
+
this.recordTokenUsage(generation, metricTokenTypeCacheWrite, usage.cacheWriteInputTokens);
|
|
700
|
+
this.recordTokenUsage(generation, metricTokenTypeReasoning, usage.reasoningTokens);
|
|
701
|
+
}
|
|
702
|
+
this.toolCallsHistogram.record(countToolCallParts(generation.output ?? []), {
|
|
703
|
+
...identityAttributes,
|
|
704
|
+
});
|
|
705
|
+
if (generation.operationName === 'streamText' && firstTokenAt !== undefined) {
|
|
706
|
+
const ttftSeconds = (firstTokenAt.getTime() - startedMs) / 1_000;
|
|
707
|
+
if (ttftSeconds >= 0) {
|
|
708
|
+
this.ttftHistogram.record(ttftSeconds, {
|
|
709
|
+
...identityAttributes,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
recordEmbeddingMetrics(seed, result, startedAt, completedAt, errorType, errorCategory) {
|
|
715
|
+
const durationSeconds = Math.max(0, (completedAt.getTime() - startedAt.getTime()) / 1_000);
|
|
716
|
+
const identityAttributes = metricIdentityAttributes(seed.model.provider, seed.model.name, seed.agentName, seed.agentVersion);
|
|
717
|
+
this.operationDurationHistogram.record(durationSeconds, {
|
|
718
|
+
[spanAttrOperationName]: defaultEmbeddingOperationName,
|
|
719
|
+
...identityAttributes,
|
|
720
|
+
[spanAttrErrorType]: errorType,
|
|
721
|
+
[spanAttrErrorCategory]: errorCategory,
|
|
722
|
+
});
|
|
723
|
+
if (result.inputTokens !== undefined && result.inputTokens !== 0) {
|
|
724
|
+
this.tokenUsageHistogram.record(result.inputTokens, {
|
|
725
|
+
[spanAttrOperationName]: defaultEmbeddingOperationName,
|
|
726
|
+
...identityAttributes,
|
|
727
|
+
[metricAttrTokenType]: metricTokenTypeInput,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
recordTokenUsage(generation, tokenType, value) {
|
|
732
|
+
if (value === undefined || value === 0) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
this.tokenUsageHistogram.record(value, {
|
|
736
|
+
[spanAttrOperationName]: generation.operationName,
|
|
737
|
+
...metricIdentityAttributes(generation.model.provider, generation.model.name, generation.agentName, generation.agentVersion),
|
|
738
|
+
[metricAttrTokenType]: tokenType,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
recordToolExecutionMetrics(toolExecution, finalError) {
|
|
742
|
+
const startedMs = toolExecution.startedAt.getTime();
|
|
743
|
+
const completedMs = toolExecution.completedAt.getTime();
|
|
744
|
+
const durationSeconds = Math.max(0, (completedMs - startedMs) / 1_000);
|
|
745
|
+
const errorType = finalError === undefined ? '' : 'tool_execution_error';
|
|
746
|
+
const errorCategory = finalError === undefined ? '' : errorCategoryFromError(finalError, true);
|
|
747
|
+
this.operationDurationHistogram.record(durationSeconds, {
|
|
748
|
+
[spanAttrOperationName]: 'execute_tool',
|
|
749
|
+
[spanAttrToolName]: toolExecution.toolName.trim(),
|
|
750
|
+
...metricIdentityAttributes(toolExecution.requestProvider ?? '', toolExecution.requestModel ?? '', toolExecution.agentName, toolExecution.agentVersion),
|
|
751
|
+
[spanAttrErrorType]: errorType,
|
|
752
|
+
[spanAttrErrorCategory]: errorCategory,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
assertOpen() {
|
|
756
|
+
if (this.shuttingDown || this.closed) {
|
|
757
|
+
throw new Error('sigil client is shutdown');
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
startGenerationWithMode(start, mode, callback) {
|
|
761
|
+
this.assertOpen();
|
|
762
|
+
const recorder = new GenerationRecorderImpl(this, start, mode);
|
|
763
|
+
if (callback === undefined) {
|
|
764
|
+
return recorder;
|
|
765
|
+
}
|
|
766
|
+
return runWithRecorder(recorder, callback);
|
|
767
|
+
}
|
|
768
|
+
triggerAsyncFlush() {
|
|
769
|
+
void this.flushInternal().catch((error) => {
|
|
770
|
+
this.logWarn('sigil generation export failed', error);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
flushInternal() {
|
|
774
|
+
if (this.flushPromise !== undefined) {
|
|
775
|
+
this.flushRequested = true;
|
|
776
|
+
return this.flushPromise;
|
|
777
|
+
}
|
|
778
|
+
this.flushPromise = this.drainPendingGenerations().finally(() => {
|
|
779
|
+
this.flushPromise = undefined;
|
|
780
|
+
});
|
|
781
|
+
return this.flushPromise;
|
|
782
|
+
}
|
|
783
|
+
async drainPendingGenerations() {
|
|
784
|
+
do {
|
|
785
|
+
this.flushRequested = false;
|
|
786
|
+
while (this.pendingGenerations.length > 0) {
|
|
787
|
+
const batchSize = Math.max(1, this.config.generationExport.batchSize);
|
|
788
|
+
const batch = this.pendingGenerations.splice(0, batchSize).map(cloneGeneration);
|
|
789
|
+
await this.exportWithRetry(batch);
|
|
790
|
+
}
|
|
791
|
+
} while (this.flushRequested || this.pendingGenerations.length > 0);
|
|
792
|
+
}
|
|
793
|
+
async exportWithRetry(generations) {
|
|
794
|
+
const maxRetries = Math.max(0, this.config.generationExport.maxRetries);
|
|
795
|
+
const attempts = maxRetries + 1;
|
|
796
|
+
const baseBackoffMs = this.config.generationExport.initialBackoffMs > 0 ? this.config.generationExport.initialBackoffMs : 100;
|
|
797
|
+
const maxBackoffMs = this.config.generationExport.maxBackoffMs > 0 ? this.config.generationExport.maxBackoffMs : baseBackoffMs;
|
|
798
|
+
let backoffMs = baseBackoffMs;
|
|
799
|
+
let lastError;
|
|
800
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
801
|
+
try {
|
|
802
|
+
const response = await this.generationExporter.exportGenerations({
|
|
803
|
+
generations: generations.map(cloneGeneration),
|
|
804
|
+
});
|
|
805
|
+
this.logRejectedResults(response.results);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
catch (error) {
|
|
809
|
+
lastError = asError(error);
|
|
810
|
+
if (attempt === attempts - 1) {
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
await this.sleepFn(backoffMs);
|
|
814
|
+
backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
throw lastError ?? new Error('generation export failed');
|
|
818
|
+
}
|
|
819
|
+
logRejectedResults(results) {
|
|
820
|
+
for (const result of results) {
|
|
821
|
+
if (!result.accepted) {
|
|
822
|
+
this.logWarn(`sigil generation rejected id=${result.generationId}`, result.error);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
stopFlushTimer() {
|
|
827
|
+
if (this.flushTimer !== undefined) {
|
|
828
|
+
clearInterval(this.flushTimer);
|
|
829
|
+
this.flushTimer = undefined;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
logWarn(message, error) {
|
|
833
|
+
if (error === undefined) {
|
|
834
|
+
this.logger.warn?.(message);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this.logger.warn?.(`${message}: ${asError(error).message}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
class GenerationRecorderImpl {
|
|
841
|
+
client;
|
|
842
|
+
seed;
|
|
843
|
+
startedAt;
|
|
844
|
+
mode;
|
|
845
|
+
span;
|
|
846
|
+
contentCaptureMode;
|
|
847
|
+
ended = false;
|
|
848
|
+
result;
|
|
849
|
+
callError;
|
|
850
|
+
localError;
|
|
851
|
+
firstTokenAt;
|
|
852
|
+
extraMetadata;
|
|
853
|
+
constructor(client, seed, defaultMode) {
|
|
854
|
+
this.client = client;
|
|
855
|
+
this.seed = cloneGenerationStart(seed);
|
|
856
|
+
if (!notEmpty(this.seed.conversationId)) {
|
|
857
|
+
this.seed.conversationId = conversationIdFromContext();
|
|
858
|
+
}
|
|
859
|
+
if (!notEmpty(this.seed.conversationTitle)) {
|
|
860
|
+
this.seed.conversationTitle = conversationTitleFromContext();
|
|
861
|
+
}
|
|
862
|
+
if (!notEmpty(this.seed.userId)) {
|
|
863
|
+
this.seed.userId = userIdFromContext();
|
|
864
|
+
}
|
|
865
|
+
if (!notEmpty(this.seed.userId)) {
|
|
866
|
+
const fromConfig = this.client.internalUserId();
|
|
867
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
868
|
+
this.seed.userId = fromConfig;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (!notEmpty(this.seed.agentName)) {
|
|
872
|
+
this.seed.agentName = agentNameFromContext();
|
|
873
|
+
}
|
|
874
|
+
if (!notEmpty(this.seed.agentName)) {
|
|
875
|
+
const fromConfig = this.client.internalAgentName();
|
|
876
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
877
|
+
this.seed.agentName = fromConfig;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (!notEmpty(this.seed.agentVersion)) {
|
|
881
|
+
this.seed.agentVersion = agentVersionFromContext();
|
|
882
|
+
}
|
|
883
|
+
if (!notEmpty(this.seed.agentVersion)) {
|
|
884
|
+
const fromConfig = this.client.internalAgentVersion();
|
|
885
|
+
if (fromConfig !== undefined && fromConfig.length > 0) {
|
|
886
|
+
this.seed.agentVersion = fromConfig;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const tags = this.client.internalTags();
|
|
890
|
+
if (tags !== undefined && Object.keys(tags).length > 0) {
|
|
891
|
+
this.seed.tags = { ...tags, ...(this.seed.tags ?? {}) };
|
|
892
|
+
}
|
|
893
|
+
if (!notEmpty(this.seed.operationName)) {
|
|
894
|
+
this.seed.operationName = defaultOperationNameForMode(this.seed.mode ?? defaultMode);
|
|
895
|
+
}
|
|
896
|
+
this.mode = this.seed.mode ?? defaultMode;
|
|
897
|
+
this.startedAt = this.seed.startedAt ?? this.client.internalNow();
|
|
898
|
+
this.contentCaptureMode = this.client.internalResolveGenerationContentCaptureMode(this.seed);
|
|
899
|
+
this.span = this.client.internalStartGenerationSpan(this.seed, this.mode, this.startedAt, this.contentCaptureMode);
|
|
900
|
+
}
|
|
901
|
+
setResult(result) {
|
|
902
|
+
if (this.ended) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.result = cloneGenerationResult(result);
|
|
906
|
+
}
|
|
907
|
+
setCallError(error) {
|
|
908
|
+
if (this.ended) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.callError = asError(error).message;
|
|
912
|
+
}
|
|
913
|
+
setFirstTokenAt(firstTokenAt) {
|
|
914
|
+
if (this.ended) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (!(firstTokenAt instanceof Date) || Number.isNaN(firstTokenAt.getTime())) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
this.firstTokenAt = new Date(firstTokenAt);
|
|
921
|
+
}
|
|
922
|
+
setCacheDiagnostics(missReason, opts) {
|
|
923
|
+
if (this.ended) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const trimmed = missReason.trim();
|
|
927
|
+
if (trimmed.length === 0) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (this.extraMetadata === undefined) {
|
|
931
|
+
this.extraMetadata = {};
|
|
932
|
+
}
|
|
933
|
+
delete this.extraMetadata[CACHE_DIAGNOSTICS_MISSED_INPUT_TOKENS_KEY];
|
|
934
|
+
delete this.extraMetadata[CACHE_DIAGNOSTICS_PREVIOUS_MESSAGE_ID_KEY];
|
|
935
|
+
this.extraMetadata[CACHE_DIAGNOSTICS_MISS_REASON_KEY] = trimmed;
|
|
936
|
+
if (opts?.missedInputTokens !== undefined) {
|
|
937
|
+
this.extraMetadata[CACHE_DIAGNOSTICS_MISSED_INPUT_TOKENS_KEY] = String(opts.missedInputTokens);
|
|
938
|
+
}
|
|
939
|
+
const prev = opts?.previousMessageId?.trim();
|
|
940
|
+
if (prev !== undefined && prev.length > 0) {
|
|
941
|
+
this.extraMetadata[CACHE_DIAGNOSTICS_PREVIOUS_MESSAGE_ID_KEY] = prev;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
end() {
|
|
945
|
+
if (this.ended) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this.ended = true;
|
|
949
|
+
let generation = {
|
|
950
|
+
id: this.seed.id ?? newLocalID('gen'),
|
|
951
|
+
conversationId: firstNonEmptyString(this.result?.conversationId, this.seed.conversationId),
|
|
952
|
+
conversationTitle: firstNonEmptyString(this.result?.conversationTitle, this.seed.conversationTitle),
|
|
953
|
+
userId: firstNonEmptyString(this.result?.userId, this.seed.userId),
|
|
954
|
+
agentName: firstNonEmptyString(this.result?.agentName, this.seed.agentName),
|
|
955
|
+
agentVersion: firstNonEmptyString(this.result?.agentVersion, this.seed.agentVersion),
|
|
956
|
+
mode: this.mode,
|
|
957
|
+
operationName: this.result?.operationName ?? this.seed.operationName ?? defaultOperationNameForMode(this.mode),
|
|
958
|
+
model: cloneModelRef(this.seed.model),
|
|
959
|
+
systemPrompt: this.seed.systemPrompt,
|
|
960
|
+
responseId: this.result?.responseId,
|
|
961
|
+
responseModel: this.result?.responseModel,
|
|
962
|
+
maxTokens: this.result?.maxTokens ?? this.seed.maxTokens,
|
|
963
|
+
temperature: this.result?.temperature ?? this.seed.temperature,
|
|
964
|
+
topP: this.result?.topP ?? this.seed.topP,
|
|
965
|
+
toolChoice: this.result?.toolChoice ?? this.seed.toolChoice,
|
|
966
|
+
thinkingEnabled: this.result?.thinkingEnabled ?? this.seed.thinkingEnabled,
|
|
967
|
+
parentGenerationIds: this.result?.parentGenerationIds?.length
|
|
968
|
+
? [...this.result.parentGenerationIds]
|
|
969
|
+
: this.seed.parentGenerationIds?.length
|
|
970
|
+
? [...this.seed.parentGenerationIds]
|
|
971
|
+
: undefined,
|
|
972
|
+
effectiveVersion: firstNonEmptyString(this.result?.effectiveVersion, this.seed.effectiveVersion),
|
|
973
|
+
input: this.result?.input?.map(cloneMessage),
|
|
974
|
+
output: this.result?.output?.map(cloneMessage),
|
|
975
|
+
tools: this.result?.tools?.map(cloneToolDefinition) ?? this.seed.tools?.map(cloneToolDefinition),
|
|
976
|
+
usage: this.result?.usage ? { ...this.result.usage } : undefined,
|
|
977
|
+
stopReason: this.result?.stopReason,
|
|
978
|
+
startedAt: new Date(this.startedAt),
|
|
979
|
+
completedAt: new Date(this.result?.completedAt ?? this.client.internalNow()),
|
|
980
|
+
tags: mergeStringRecords(this.seed.tags, this.result?.tags),
|
|
981
|
+
metadata: mergeUnknownRecords(mergeUnknownRecords(this.seed.metadata, this.result?.metadata), this.extraMetadata),
|
|
982
|
+
artifacts: this.result?.artifacts?.map(cloneArtifact),
|
|
983
|
+
callError: this.callError,
|
|
984
|
+
};
|
|
985
|
+
generation.conversationTitle = firstNonEmptyString(generation.conversationTitle, metadataStringValue(generation.metadata, spanAttrConversationTitle))?.trim();
|
|
986
|
+
if (notEmpty(generation.conversationTitle)) {
|
|
987
|
+
if (generation.metadata === undefined) {
|
|
988
|
+
generation.metadata = {};
|
|
989
|
+
}
|
|
990
|
+
generation.metadata[spanAttrConversationTitle] = generation.conversationTitle;
|
|
991
|
+
}
|
|
992
|
+
generation.userId = firstNonEmptyString(generation.userId, metadataStringValue(generation.metadata, metadataUserIDKey), metadataStringValue(generation.metadata, metadataLegacyUserIDKey))?.trim();
|
|
993
|
+
if (notEmpty(generation.userId)) {
|
|
994
|
+
if (generation.metadata === undefined) {
|
|
995
|
+
generation.metadata = {};
|
|
996
|
+
}
|
|
997
|
+
generation.metadata[metadataUserIDKey] = generation.userId;
|
|
998
|
+
}
|
|
999
|
+
if (this.callError !== undefined) {
|
|
1000
|
+
if (generation.metadata === undefined) {
|
|
1001
|
+
generation.metadata = {};
|
|
1002
|
+
}
|
|
1003
|
+
generation.metadata.call_error = this.callError;
|
|
1004
|
+
}
|
|
1005
|
+
if (generation.metadata === undefined) {
|
|
1006
|
+
generation.metadata = {};
|
|
1007
|
+
}
|
|
1008
|
+
generation.metadata[spanAttrSDKName] = sdkName;
|
|
1009
|
+
const callErrorCategory = errorCategoryFromError(this.callError, false);
|
|
1010
|
+
let effectiveContentCaptureMode = this.contentCaptureMode;
|
|
1011
|
+
let validationTarget = cloneGeneration(generation);
|
|
1012
|
+
stampContentCaptureMetadata(generation, this.contentCaptureMode);
|
|
1013
|
+
if (this.contentCaptureMode === 'metadata_only') {
|
|
1014
|
+
stripContent(generation, callErrorCategory);
|
|
1015
|
+
}
|
|
1016
|
+
else if (this.client.internalHasGenerationSanitizer()) {
|
|
1017
|
+
try {
|
|
1018
|
+
generation = this.client.internalSanitizeGeneration(generation);
|
|
1019
|
+
validationTarget = cloneGeneration(generation);
|
|
1020
|
+
}
|
|
1021
|
+
catch (error) {
|
|
1022
|
+
effectiveContentCaptureMode = 'metadata_only';
|
|
1023
|
+
stripContent(generation, callErrorCategory);
|
|
1024
|
+
stampContentCaptureMetadata(generation, effectiveContentCaptureMode);
|
|
1025
|
+
this.client.internalLogWarn('sigil generation sanitization failed; falling back to metadata_only', error);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const validationError = validateGeneration(validationTarget);
|
|
1029
|
+
// full_with_metadata_spans: proto export keeps the title, but the span
|
|
1030
|
+
// path must drop it. Pass a shallow copy with the title cleared so the
|
|
1031
|
+
// in-memory generation (the proto payload) stays untouched.
|
|
1032
|
+
const spanGeneration = effectiveContentCaptureMode === 'full_with_metadata_spans'
|
|
1033
|
+
? { ...generation, conversationTitle: '' }
|
|
1034
|
+
: generation;
|
|
1035
|
+
this.client.internalSyncGenerationSpan(this.span, spanGeneration);
|
|
1036
|
+
if (effectiveContentCaptureMode === 'metadata_only' &&
|
|
1037
|
+
this.contentCaptureMode !== 'metadata_only' &&
|
|
1038
|
+
this.contentCaptureMode !== 'full_with_metadata_spans') {
|
|
1039
|
+
// Sanitizer fallback downgrades effective mode to metadata_only.
|
|
1040
|
+
// Skipped when the original mode already left the attribute absent at
|
|
1041
|
+
// start time (metadata_only / full_with_metadata_spans) so the
|
|
1042
|
+
// start-span omission isn't re-emitted here as an empty value.
|
|
1043
|
+
this.client.internalClearSpanConversationTitle(this.span);
|
|
1044
|
+
}
|
|
1045
|
+
this.client.internalApplyTraceContextFromSpan(this.span, generation);
|
|
1046
|
+
this.client.internalRecordGeneration(generation);
|
|
1047
|
+
let enqueueError;
|
|
1048
|
+
if (validationError !== undefined) {
|
|
1049
|
+
this.localError = validationError;
|
|
1050
|
+
this.client.internalLogWarn('sigil generation validation failed', validationError);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
try {
|
|
1054
|
+
this.client.internalEnqueueGeneration(generation);
|
|
1055
|
+
}
|
|
1056
|
+
catch (error) {
|
|
1057
|
+
enqueueError = asError(error);
|
|
1058
|
+
this.localError = enqueueError;
|
|
1059
|
+
this.client.internalLogWarn('sigil generation enqueue failed', enqueueError);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Under metadata_only stripContent already replaced generation.callError
|
|
1063
|
+
// with the category, so the span path can read it back from the
|
|
1064
|
+
// generation. Under full_with_metadata_spans generation.callError stays
|
|
1065
|
+
// raw for the gRPC export, so we substitute the precomputed category for
|
|
1066
|
+
// the span path here.
|
|
1067
|
+
let finalCallError;
|
|
1068
|
+
if (this.callError === undefined) {
|
|
1069
|
+
finalCallError = undefined;
|
|
1070
|
+
}
|
|
1071
|
+
else if (effectiveContentCaptureMode === 'metadata_only') {
|
|
1072
|
+
finalCallError = generation.callError;
|
|
1073
|
+
}
|
|
1074
|
+
else if (effectiveContentCaptureMode === 'full_with_metadata_spans') {
|
|
1075
|
+
finalCallError = callErrorCategory.length > 0 ? callErrorCategory : 'sdk_error';
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
finalCallError = this.callError;
|
|
1079
|
+
}
|
|
1080
|
+
this.client.internalFinalizeGenerationSpan(this.span, generation, finalCallError, validationError, enqueueError, this.firstTokenAt, callErrorCategory.length > 0 ? callErrorCategory : undefined);
|
|
1081
|
+
}
|
|
1082
|
+
getError() {
|
|
1083
|
+
return this.localError;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
class EmbeddingRecorderImpl {
|
|
1087
|
+
client;
|
|
1088
|
+
seed;
|
|
1089
|
+
startedAt;
|
|
1090
|
+
span;
|
|
1091
|
+
contentCaptureMode;
|
|
1092
|
+
ended = false;
|
|
1093
|
+
callError;
|
|
1094
|
+
result;
|
|
1095
|
+
hasResult = false;
|
|
1096
|
+
localError;
|
|
1097
|
+
constructor(client, seed) {
|
|
1098
|
+
this.client = client;
|
|
1099
|
+
this.seed = cloneEmbeddingStart(seed);
|
|
1100
|
+
this.startedAt = this.seed.startedAt ?? this.client.internalNow();
|
|
1101
|
+
this.contentCaptureMode = this.client.internalResolveEmbeddingContentCaptureMode(this.seed);
|
|
1102
|
+
this.span = this.client.internalStartEmbeddingSpan(this.seed, this.startedAt);
|
|
1103
|
+
}
|
|
1104
|
+
setCallError(error) {
|
|
1105
|
+
if (this.ended) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
this.callError = asError(error);
|
|
1109
|
+
}
|
|
1110
|
+
setResult(result) {
|
|
1111
|
+
if (this.ended) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
this.result = cloneEmbeddingResult(result);
|
|
1115
|
+
this.hasResult = true;
|
|
1116
|
+
}
|
|
1117
|
+
end() {
|
|
1118
|
+
if (this.ended) {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
this.ended = true;
|
|
1122
|
+
const completedAt = this.client.internalNow();
|
|
1123
|
+
const normalizedResult = this.result ? cloneEmbeddingResult(this.result) : { inputCount: 0 };
|
|
1124
|
+
let localError = validateEmbeddingStart(this.seed);
|
|
1125
|
+
if (localError === undefined) {
|
|
1126
|
+
localError = validateEmbeddingResult(normalizedResult);
|
|
1127
|
+
}
|
|
1128
|
+
this.client.internalFinalizeEmbeddingSpan(this.span, this.seed, normalizedResult, this.hasResult, this.callError, localError, this.startedAt, completedAt, this.contentCaptureMode);
|
|
1129
|
+
this.localError = localError;
|
|
1130
|
+
}
|
|
1131
|
+
getError() {
|
|
1132
|
+
return this.localError;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
class ToolExecutionRecorderImpl {
|
|
1136
|
+
client;
|
|
1137
|
+
seed;
|
|
1138
|
+
startedAt;
|
|
1139
|
+
span;
|
|
1140
|
+
resolvedIncludeContent;
|
|
1141
|
+
toolMode;
|
|
1142
|
+
ended = false;
|
|
1143
|
+
result;
|
|
1144
|
+
callError;
|
|
1145
|
+
localError;
|
|
1146
|
+
constructor(client, seed) {
|
|
1147
|
+
this.client = client;
|
|
1148
|
+
this.seed = cloneToolExecutionStart(seed);
|
|
1149
|
+
if (!notEmpty(this.seed.conversationId)) {
|
|
1150
|
+
this.seed.conversationId = conversationIdFromContext();
|
|
1151
|
+
}
|
|
1152
|
+
if (!notEmpty(this.seed.conversationTitle)) {
|
|
1153
|
+
this.seed.conversationTitle = conversationTitleFromContext();
|
|
1154
|
+
}
|
|
1155
|
+
if (!notEmpty(this.seed.agentName)) {
|
|
1156
|
+
this.seed.agentName = agentNameFromContext();
|
|
1157
|
+
}
|
|
1158
|
+
if (!notEmpty(this.seed.agentName)) {
|
|
1159
|
+
const fromConfig = this.client.internalAgentName();
|
|
1160
|
+
if (notEmpty(fromConfig)) {
|
|
1161
|
+
this.seed.agentName = fromConfig;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (!notEmpty(this.seed.agentVersion)) {
|
|
1165
|
+
this.seed.agentVersion = agentVersionFromContext();
|
|
1166
|
+
}
|
|
1167
|
+
if (!notEmpty(this.seed.agentVersion)) {
|
|
1168
|
+
const fromConfig = this.client.internalAgentVersion();
|
|
1169
|
+
if (notEmpty(fromConfig)) {
|
|
1170
|
+
this.seed.agentVersion = fromConfig;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// Under metadata_only or full_with_metadata_spans, the start-time tool
|
|
1174
|
+
// span must not carry any content-bearing seed field. Tools have no
|
|
1175
|
+
// proto export, so dropping these on the seed is the only redaction
|
|
1176
|
+
// surface.
|
|
1177
|
+
this.toolMode = this.client.internalResolveToolContentCaptureMode(this.seed);
|
|
1178
|
+
if (this.toolMode === 'metadata_only' || this.toolMode === 'full_with_metadata_spans') {
|
|
1179
|
+
this.seed.conversationTitle = undefined;
|
|
1180
|
+
this.seed.toolDescription = undefined;
|
|
1181
|
+
}
|
|
1182
|
+
this.resolvedIncludeContent = shouldIncludeToolContent(this.toolMode, this.seed.includeContent ?? false);
|
|
1183
|
+
this.startedAt = this.seed.startedAt ?? this.client.internalNow();
|
|
1184
|
+
this.span = this.client.internalStartToolExecutionSpan(this.seed, this.startedAt);
|
|
1185
|
+
}
|
|
1186
|
+
setResult(result) {
|
|
1187
|
+
if (this.ended) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this.result = cloneToolExecutionResult(result);
|
|
1191
|
+
}
|
|
1192
|
+
setCallError(error) {
|
|
1193
|
+
if (this.ended) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
this.localError = asError(error);
|
|
1197
|
+
this.callError = this.localError.message;
|
|
1198
|
+
}
|
|
1199
|
+
end() {
|
|
1200
|
+
if (this.ended) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
this.ended = true;
|
|
1204
|
+
const toolExecution = {
|
|
1205
|
+
toolName: this.seed.toolName,
|
|
1206
|
+
toolCallId: this.seed.toolCallId,
|
|
1207
|
+
toolType: this.seed.toolType,
|
|
1208
|
+
toolDescription: this.seed.toolDescription,
|
|
1209
|
+
conversationId: this.seed.conversationId,
|
|
1210
|
+
conversationTitle: this.seed.conversationTitle,
|
|
1211
|
+
agentName: this.seed.agentName,
|
|
1212
|
+
agentVersion: this.seed.agentVersion,
|
|
1213
|
+
requestModel: this.seed.requestModel,
|
|
1214
|
+
requestProvider: this.seed.requestProvider,
|
|
1215
|
+
includeContent: this.resolvedIncludeContent,
|
|
1216
|
+
startedAt: new Date(this.startedAt),
|
|
1217
|
+
completedAt: new Date(this.result?.completedAt ?? this.client.internalNow()),
|
|
1218
|
+
arguments: this.result?.arguments,
|
|
1219
|
+
result: this.result?.result,
|
|
1220
|
+
callError: this.callError,
|
|
1221
|
+
};
|
|
1222
|
+
const validationError = validateToolExecution(toolExecution);
|
|
1223
|
+
if (validationError !== undefined) {
|
|
1224
|
+
this.localError = validationError;
|
|
1225
|
+
this.client.internalLogWarn('sigil tool execution validation failed', validationError);
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
this.client.internalRecordToolExecution(toolExecution);
|
|
1229
|
+
}
|
|
1230
|
+
this.localError = this.client.internalFinalizeToolExecutionSpan(this.span, toolExecution, this.localError, this.toolMode);
|
|
1231
|
+
}
|
|
1232
|
+
getError() {
|
|
1233
|
+
return this.localError;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
class NoopToolExecutionRecorder {
|
|
1237
|
+
setResult(_result) { }
|
|
1238
|
+
setCallError(_error) { }
|
|
1239
|
+
end() { }
|
|
1240
|
+
getError() {
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async function runWithRecorder(recorder, callback) {
|
|
1245
|
+
let callbackError;
|
|
1246
|
+
try {
|
|
1247
|
+
return await callback(recorder);
|
|
1248
|
+
}
|
|
1249
|
+
catch (error) {
|
|
1250
|
+
callbackError = error;
|
|
1251
|
+
recorder.setCallError(error);
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
finally {
|
|
1255
|
+
recorder.end();
|
|
1256
|
+
const recorderError = recorder.getError();
|
|
1257
|
+
if (callbackError === undefined && recorderError !== undefined) {
|
|
1258
|
+
// biome-ignore lint/correctness/noUnsafeFinally: intentional — only throws when callback succeeded but recorder detected an error
|
|
1259
|
+
throw recorderError;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function generationSpanName(operationName, modelName) {
|
|
1264
|
+
const operation = operationName.trim();
|
|
1265
|
+
const model = modelName.trim();
|
|
1266
|
+
if (model.length === 0) {
|
|
1267
|
+
return operation;
|
|
1268
|
+
}
|
|
1269
|
+
return `${operation} ${model}`;
|
|
1270
|
+
}
|
|
1271
|
+
function embeddingSpanName(modelName) {
|
|
1272
|
+
const model = modelName.trim();
|
|
1273
|
+
if (model.length === 0) {
|
|
1274
|
+
return defaultEmbeddingOperationName;
|
|
1275
|
+
}
|
|
1276
|
+
return `${defaultEmbeddingOperationName} ${model}`;
|
|
1277
|
+
}
|
|
1278
|
+
function toolSpanName(toolName) {
|
|
1279
|
+
const normalized = toolName.trim();
|
|
1280
|
+
if (normalized.length === 0) {
|
|
1281
|
+
return 'execute_tool unknown';
|
|
1282
|
+
}
|
|
1283
|
+
return `execute_tool ${normalized}`;
|
|
1284
|
+
}
|
|
1285
|
+
function metricIdentityAttributes(provider, model, agentName, agentVersion) {
|
|
1286
|
+
const attributes = {
|
|
1287
|
+
[spanAttrProviderName]: provider.trim(),
|
|
1288
|
+
[spanAttrRequestModel]: model.trim(),
|
|
1289
|
+
[spanAttrAgentName]: agentName?.trim() ?? '',
|
|
1290
|
+
};
|
|
1291
|
+
if (notEmpty(agentVersion)) {
|
|
1292
|
+
attributes[spanAttrAgentVersion] = agentVersion.trim();
|
|
1293
|
+
}
|
|
1294
|
+
return attributes;
|
|
1295
|
+
}
|
|
1296
|
+
function setGenerationSpanAttributes(span, generation) {
|
|
1297
|
+
span.setAttribute(spanAttrOperationName, generation.operationName);
|
|
1298
|
+
span.setAttribute(spanAttrSDKName, sdkName);
|
|
1299
|
+
if (notEmpty(generation.id)) {
|
|
1300
|
+
span.setAttribute(spanAttrGenerationID, generation.id);
|
|
1301
|
+
}
|
|
1302
|
+
if (notEmpty(generation.conversationId)) {
|
|
1303
|
+
span.setAttribute(spanAttrConversationID, generation.conversationId);
|
|
1304
|
+
}
|
|
1305
|
+
if (notEmpty(generation.conversationTitle)) {
|
|
1306
|
+
span.setAttribute(spanAttrConversationTitle, generation.conversationTitle);
|
|
1307
|
+
}
|
|
1308
|
+
if (notEmpty(generation.userId)) {
|
|
1309
|
+
span.setAttribute(spanAttrUserID, generation.userId);
|
|
1310
|
+
}
|
|
1311
|
+
if (notEmpty(generation.agentName)) {
|
|
1312
|
+
span.setAttribute(spanAttrAgentName, generation.agentName);
|
|
1313
|
+
}
|
|
1314
|
+
if (notEmpty(generation.agentVersion)) {
|
|
1315
|
+
span.setAttribute(spanAttrAgentVersion, generation.agentVersion);
|
|
1316
|
+
}
|
|
1317
|
+
if (notEmpty(generation.model.provider)) {
|
|
1318
|
+
span.setAttribute(spanAttrProviderName, generation.model.provider);
|
|
1319
|
+
}
|
|
1320
|
+
if (notEmpty(generation.model.name)) {
|
|
1321
|
+
span.setAttribute(spanAttrRequestModel, generation.model.name);
|
|
1322
|
+
}
|
|
1323
|
+
if (generation.maxTokens !== undefined) {
|
|
1324
|
+
span.setAttribute(spanAttrRequestMaxTokens, generation.maxTokens);
|
|
1325
|
+
}
|
|
1326
|
+
if (generation.temperature !== undefined) {
|
|
1327
|
+
span.setAttribute(spanAttrRequestTemperature, generation.temperature);
|
|
1328
|
+
}
|
|
1329
|
+
if (generation.topP !== undefined) {
|
|
1330
|
+
span.setAttribute(spanAttrRequestTopP, generation.topP);
|
|
1331
|
+
}
|
|
1332
|
+
if (notEmpty(generation.toolChoice)) {
|
|
1333
|
+
span.setAttribute(spanAttrRequestToolChoice, generation.toolChoice);
|
|
1334
|
+
}
|
|
1335
|
+
if (generation.thinkingEnabled !== undefined) {
|
|
1336
|
+
span.setAttribute(spanAttrRequestThinkingEnabled, generation.thinkingEnabled);
|
|
1337
|
+
}
|
|
1338
|
+
const thinkingBudget = thinkingBudgetFromMetadata(generation.metadata);
|
|
1339
|
+
if (thinkingBudget !== undefined) {
|
|
1340
|
+
span.setAttribute(spanAttrRequestThinkingBudget, thinkingBudget);
|
|
1341
|
+
}
|
|
1342
|
+
const frameworkRunId = metadataStringValue(generation.metadata, spanAttrFrameworkRunID);
|
|
1343
|
+
if (frameworkRunId !== undefined) {
|
|
1344
|
+
span.setAttribute(spanAttrFrameworkRunID, frameworkRunId);
|
|
1345
|
+
}
|
|
1346
|
+
const frameworkThreadId = metadataStringValue(generation.metadata, spanAttrFrameworkThreadID);
|
|
1347
|
+
if (frameworkThreadId !== undefined) {
|
|
1348
|
+
span.setAttribute(spanAttrFrameworkThreadID, frameworkThreadId);
|
|
1349
|
+
}
|
|
1350
|
+
const frameworkParentRunId = metadataStringValue(generation.metadata, spanAttrFrameworkParentRunID);
|
|
1351
|
+
if (frameworkParentRunId !== undefined) {
|
|
1352
|
+
span.setAttribute(spanAttrFrameworkParentRunID, frameworkParentRunId);
|
|
1353
|
+
}
|
|
1354
|
+
const frameworkComponentName = metadataStringValue(generation.metadata, spanAttrFrameworkComponentName);
|
|
1355
|
+
if (frameworkComponentName !== undefined) {
|
|
1356
|
+
span.setAttribute(spanAttrFrameworkComponentName, frameworkComponentName);
|
|
1357
|
+
}
|
|
1358
|
+
const frameworkRunType = metadataStringValue(generation.metadata, spanAttrFrameworkRunType);
|
|
1359
|
+
if (frameworkRunType !== undefined) {
|
|
1360
|
+
span.setAttribute(spanAttrFrameworkRunType, frameworkRunType);
|
|
1361
|
+
}
|
|
1362
|
+
const frameworkRetryAttempt = metadataIntValue(generation.metadata, spanAttrFrameworkRetryAttempt);
|
|
1363
|
+
if (frameworkRetryAttempt !== undefined) {
|
|
1364
|
+
span.setAttribute(spanAttrFrameworkRetryAttempt, frameworkRetryAttempt);
|
|
1365
|
+
}
|
|
1366
|
+
const frameworkLangGraphNode = metadataStringValue(generation.metadata, spanAttrFrameworkLangGraphNode);
|
|
1367
|
+
if (frameworkLangGraphNode !== undefined) {
|
|
1368
|
+
span.setAttribute(spanAttrFrameworkLangGraphNode, frameworkLangGraphNode);
|
|
1369
|
+
}
|
|
1370
|
+
const frameworkEventID = metadataStringValue(generation.metadata, spanAttrFrameworkEventID);
|
|
1371
|
+
if (frameworkEventID !== undefined) {
|
|
1372
|
+
span.setAttribute(spanAttrFrameworkEventID, frameworkEventID);
|
|
1373
|
+
}
|
|
1374
|
+
if (notEmpty(generation.responseId)) {
|
|
1375
|
+
span.setAttribute(spanAttrResponseID, generation.responseId);
|
|
1376
|
+
}
|
|
1377
|
+
if (notEmpty(generation.responseModel)) {
|
|
1378
|
+
span.setAttribute(spanAttrResponseModel, generation.responseModel);
|
|
1379
|
+
}
|
|
1380
|
+
if (notEmpty(generation.stopReason)) {
|
|
1381
|
+
span.setAttribute(spanAttrFinishReasons, [generation.stopReason]);
|
|
1382
|
+
}
|
|
1383
|
+
const usage = generation.usage;
|
|
1384
|
+
if (usage === undefined) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if ((usage.inputTokens ?? 0) !== 0) {
|
|
1388
|
+
span.setAttribute(spanAttrInputTokens, usage.inputTokens ?? 0);
|
|
1389
|
+
}
|
|
1390
|
+
if ((usage.outputTokens ?? 0) !== 0) {
|
|
1391
|
+
span.setAttribute(spanAttrOutputTokens, usage.outputTokens ?? 0);
|
|
1392
|
+
}
|
|
1393
|
+
if ((usage.cacheReadInputTokens ?? 0) !== 0) {
|
|
1394
|
+
span.setAttribute(spanAttrCacheReadTokens, usage.cacheReadInputTokens ?? 0);
|
|
1395
|
+
}
|
|
1396
|
+
if ((usage.cacheWriteInputTokens ?? 0) !== 0) {
|
|
1397
|
+
span.setAttribute(spanAttrCacheWriteTokens, usage.cacheWriteInputTokens ?? 0);
|
|
1398
|
+
}
|
|
1399
|
+
if ((usage.reasoningTokens ?? 0) !== 0) {
|
|
1400
|
+
span.setAttribute(spanAttrReasoningTokens, usage.reasoningTokens ?? 0);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function setEmbeddingStartSpanAttributes(span, start) {
|
|
1404
|
+
span.setAttribute(spanAttrOperationName, defaultEmbeddingOperationName);
|
|
1405
|
+
span.setAttribute(spanAttrSDKName, sdkName);
|
|
1406
|
+
if (notEmpty(start.model.provider)) {
|
|
1407
|
+
span.setAttribute(spanAttrProviderName, start.model.provider);
|
|
1408
|
+
}
|
|
1409
|
+
if (notEmpty(start.model.name)) {
|
|
1410
|
+
span.setAttribute(spanAttrRequestModel, start.model.name);
|
|
1411
|
+
}
|
|
1412
|
+
if (notEmpty(start.agentName)) {
|
|
1413
|
+
span.setAttribute(spanAttrAgentName, start.agentName);
|
|
1414
|
+
}
|
|
1415
|
+
if (notEmpty(start.agentVersion)) {
|
|
1416
|
+
span.setAttribute(spanAttrAgentVersion, start.agentVersion);
|
|
1417
|
+
}
|
|
1418
|
+
if (start.dimensions !== undefined) {
|
|
1419
|
+
span.setAttribute(spanAttrEmbeddingDimCount, start.dimensions);
|
|
1420
|
+
}
|
|
1421
|
+
if (notEmpty(start.encodingFormat)) {
|
|
1422
|
+
span.setAttribute(spanAttrRequestEncodingFormats, [start.encodingFormat]);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function setEmbeddingEndSpanAttributes(span, result, hasResult, captureConfig, contentCaptureMode = 'default') {
|
|
1426
|
+
if (hasResult) {
|
|
1427
|
+
span.setAttribute(spanAttrEmbeddingInputCount, result.inputCount);
|
|
1428
|
+
}
|
|
1429
|
+
if (result.inputTokens !== undefined && result.inputTokens !== 0) {
|
|
1430
|
+
span.setAttribute(spanAttrInputTokens, result.inputTokens);
|
|
1431
|
+
}
|
|
1432
|
+
if (notEmpty(result.responseModel)) {
|
|
1433
|
+
span.setAttribute(spanAttrResponseModel, result.responseModel);
|
|
1434
|
+
}
|
|
1435
|
+
if (result.dimensions !== undefined) {
|
|
1436
|
+
span.setAttribute(spanAttrEmbeddingDimCount, result.dimensions);
|
|
1437
|
+
}
|
|
1438
|
+
// Embeddings have no proto export; full_with_metadata_spans matches
|
|
1439
|
+
// metadata_only for input-text span attributes.
|
|
1440
|
+
const omitInputTexts = contentCaptureMode === 'metadata_only' || contentCaptureMode === 'full_with_metadata_spans';
|
|
1441
|
+
if (captureConfig.captureInput && result.inputTexts !== undefined && !omitInputTexts) {
|
|
1442
|
+
const texts = captureEmbeddingInputTexts(result.inputTexts, captureConfig.maxInputItems, captureConfig.maxTextLength);
|
|
1443
|
+
if (texts.length > 0) {
|
|
1444
|
+
span.setAttribute(spanAttrEmbeddingInputTexts, texts);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function setToolSpanAttributes(span, tool) {
|
|
1449
|
+
span.setAttribute(spanAttrOperationName, 'execute_tool');
|
|
1450
|
+
span.setAttribute(spanAttrToolName, tool.toolName);
|
|
1451
|
+
span.setAttribute(spanAttrSDKName, sdkName);
|
|
1452
|
+
if (notEmpty(tool.toolCallId)) {
|
|
1453
|
+
span.setAttribute(spanAttrToolCallID, tool.toolCallId);
|
|
1454
|
+
}
|
|
1455
|
+
if (notEmpty(tool.toolType)) {
|
|
1456
|
+
span.setAttribute(spanAttrToolType, tool.toolType);
|
|
1457
|
+
}
|
|
1458
|
+
if (notEmpty(tool.toolDescription)) {
|
|
1459
|
+
span.setAttribute(spanAttrToolDescription, tool.toolDescription);
|
|
1460
|
+
}
|
|
1461
|
+
if (notEmpty(tool.conversationId)) {
|
|
1462
|
+
span.setAttribute(spanAttrConversationID, tool.conversationId);
|
|
1463
|
+
}
|
|
1464
|
+
if (notEmpty(tool.conversationTitle)) {
|
|
1465
|
+
span.setAttribute(spanAttrConversationTitle, tool.conversationTitle);
|
|
1466
|
+
}
|
|
1467
|
+
if (notEmpty(tool.agentName)) {
|
|
1468
|
+
span.setAttribute(spanAttrAgentName, tool.agentName);
|
|
1469
|
+
}
|
|
1470
|
+
if (notEmpty(tool.agentVersion)) {
|
|
1471
|
+
span.setAttribute(spanAttrAgentVersion, tool.agentVersion);
|
|
1472
|
+
}
|
|
1473
|
+
if (notEmpty(tool.requestProvider)) {
|
|
1474
|
+
span.setAttribute(spanAttrProviderName, tool.requestProvider);
|
|
1475
|
+
}
|
|
1476
|
+
if (notEmpty(tool.requestModel)) {
|
|
1477
|
+
span.setAttribute(spanAttrRequestModel, tool.requestModel);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function serializeToolContent(value) {
|
|
1481
|
+
if (value === undefined || value === null) {
|
|
1482
|
+
return {};
|
|
1483
|
+
}
|
|
1484
|
+
if (typeof value === 'string') {
|
|
1485
|
+
const trimmed = value.trim();
|
|
1486
|
+
if (trimmed.length === 0) {
|
|
1487
|
+
return {};
|
|
1488
|
+
}
|
|
1489
|
+
if (isJSON(trimmed)) {
|
|
1490
|
+
return { value: trimmed };
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
return { value: JSON.stringify(trimmed) };
|
|
1494
|
+
}
|
|
1495
|
+
catch (error) {
|
|
1496
|
+
return { error: asError(error) };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
const encoded = JSON.stringify(value);
|
|
1501
|
+
if (encoded === undefined || encoded === 'null') {
|
|
1502
|
+
return {};
|
|
1503
|
+
}
|
|
1504
|
+
return { value: encoded };
|
|
1505
|
+
}
|
|
1506
|
+
catch (error) {
|
|
1507
|
+
return { error: asError(error) };
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function normalizeConversationRatingInput(input) {
|
|
1511
|
+
const normalized = {
|
|
1512
|
+
ratingId: input.ratingId.trim(),
|
|
1513
|
+
rating: input.rating.trim(),
|
|
1514
|
+
comment: input.comment?.trim(),
|
|
1515
|
+
metadata: input.metadata,
|
|
1516
|
+
generationId: input.generationId?.trim(),
|
|
1517
|
+
raterId: input.raterId?.trim(),
|
|
1518
|
+
source: input.source?.trim(),
|
|
1519
|
+
};
|
|
1520
|
+
if (normalized.ratingId.length === 0) {
|
|
1521
|
+
throw new Error('sigil conversation rating validation failed: ratingId is required');
|
|
1522
|
+
}
|
|
1523
|
+
if (normalized.ratingId.length > maxRatingIdLen) {
|
|
1524
|
+
throw new Error('sigil conversation rating validation failed: ratingId is too long');
|
|
1525
|
+
}
|
|
1526
|
+
if (normalized.rating !== 'CONVERSATION_RATING_VALUE_GOOD' && normalized.rating !== 'CONVERSATION_RATING_VALUE_BAD') {
|
|
1527
|
+
throw new Error('sigil conversation rating validation failed: rating must be CONVERSATION_RATING_VALUE_GOOD or CONVERSATION_RATING_VALUE_BAD');
|
|
1528
|
+
}
|
|
1529
|
+
if (normalized.comment !== undefined && encodedSizeBytes(normalized.comment) > maxRatingCommentBytes) {
|
|
1530
|
+
throw new Error('sigil conversation rating validation failed: comment is too long');
|
|
1531
|
+
}
|
|
1532
|
+
if (normalized.generationId !== undefined && normalized.generationId.length > maxRatingGenerationIdLen) {
|
|
1533
|
+
throw new Error('sigil conversation rating validation failed: generationId is too long');
|
|
1534
|
+
}
|
|
1535
|
+
if (normalized.raterId !== undefined && normalized.raterId.length > maxRatingActorIdLen) {
|
|
1536
|
+
throw new Error('sigil conversation rating validation failed: raterId is too long');
|
|
1537
|
+
}
|
|
1538
|
+
if (normalized.source !== undefined && normalized.source.length > maxRatingSourceLen) {
|
|
1539
|
+
throw new Error('sigil conversation rating validation failed: source is too long');
|
|
1540
|
+
}
|
|
1541
|
+
if (normalized.metadata !== undefined && encodedSizeBytes(normalized.metadata) > maxRatingMetadataBytes) {
|
|
1542
|
+
throw new Error('sigil conversation rating validation failed: metadata is too large');
|
|
1543
|
+
}
|
|
1544
|
+
return normalized;
|
|
1545
|
+
}
|
|
1546
|
+
function buildConversationRatingEndpoint(endpoint, insecure, conversationId) {
|
|
1547
|
+
const baseURL = baseURLFromAPIEndpoint(endpoint, insecure);
|
|
1548
|
+
return `${baseURL}/api/v1/conversations/${encodeURIComponent(conversationId)}/ratings`;
|
|
1549
|
+
}
|
|
1550
|
+
function baseURLFromAPIEndpoint(endpoint, insecure) {
|
|
1551
|
+
const trimmed = endpoint.trim();
|
|
1552
|
+
if (trimmed.length === 0) {
|
|
1553
|
+
throw new Error('sigil conversation rating transport failed: api endpoint is required');
|
|
1554
|
+
}
|
|
1555
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
1556
|
+
const parsed = new URL(trimmed);
|
|
1557
|
+
// Preserve a path prefix so prefix-mounted Sigil deployments
|
|
1558
|
+
// (https://host/sigil) route /api/v1/conversations/... under the prefix.
|
|
1559
|
+
const path = parsed.pathname.replace(/\/+$/, '');
|
|
1560
|
+
return `${parsed.protocol}//${parsed.host}${path}`;
|
|
1561
|
+
}
|
|
1562
|
+
const withoutScheme = trimmed.startsWith('grpc://') ? trimmed.slice('grpc://'.length) : trimmed;
|
|
1563
|
+
const host = withoutScheme.split('/')[0]?.trim();
|
|
1564
|
+
if (host === undefined || host.length === 0) {
|
|
1565
|
+
throw new Error('sigil conversation rating transport failed: api endpoint host is required');
|
|
1566
|
+
}
|
|
1567
|
+
return `${insecure ? 'http' : 'https'}://${host}`;
|
|
1568
|
+
}
|
|
1569
|
+
function parseSubmitConversationRatingResponse(payload) {
|
|
1570
|
+
if (!isObject(payload)) {
|
|
1571
|
+
throw new Error('sigil conversation rating transport failed: invalid response payload');
|
|
1572
|
+
}
|
|
1573
|
+
if (!isObject(payload.rating) || !isObject(payload.summary)) {
|
|
1574
|
+
throw new Error('sigil conversation rating transport failed: invalid response payload');
|
|
1575
|
+
}
|
|
1576
|
+
const rating = mapConversationRating(payload.rating);
|
|
1577
|
+
const summary = mapConversationRatingSummary(payload.summary);
|
|
1578
|
+
return { rating, summary };
|
|
1579
|
+
}
|
|
1580
|
+
function mapConversationRating(payload) {
|
|
1581
|
+
const ratingId = asString(payload.rating_id);
|
|
1582
|
+
const conversationId = asString(payload.conversation_id);
|
|
1583
|
+
const rating = asString(payload.rating);
|
|
1584
|
+
const createdAt = asString(payload.created_at);
|
|
1585
|
+
if (ratingId === undefined || conversationId === undefined || rating === undefined || createdAt === undefined) {
|
|
1586
|
+
throw new Error('sigil conversation rating transport failed: invalid rating payload');
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
ratingId,
|
|
1590
|
+
conversationId,
|
|
1591
|
+
generationId: asString(payload.generation_id),
|
|
1592
|
+
rating,
|
|
1593
|
+
comment: asString(payload.comment),
|
|
1594
|
+
metadata: asRecordUnknown(payload.metadata),
|
|
1595
|
+
raterId: asString(payload.rater_id),
|
|
1596
|
+
source: asString(payload.source),
|
|
1597
|
+
createdAt,
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function mapConversationRatingSummary(payload) {
|
|
1601
|
+
const totalCount = asNumber(payload.total_count);
|
|
1602
|
+
const goodCount = asNumber(payload.good_count);
|
|
1603
|
+
const badCount = asNumber(payload.bad_count);
|
|
1604
|
+
const latestRatedAt = asString(payload.latest_rated_at);
|
|
1605
|
+
const hasBadRating = asBoolean(payload.has_bad_rating);
|
|
1606
|
+
if (totalCount === undefined ||
|
|
1607
|
+
goodCount === undefined ||
|
|
1608
|
+
badCount === undefined ||
|
|
1609
|
+
latestRatedAt === undefined ||
|
|
1610
|
+
hasBadRating === undefined) {
|
|
1611
|
+
throw new Error('sigil conversation rating transport failed: invalid rating summary payload');
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
totalCount,
|
|
1615
|
+
goodCount,
|
|
1616
|
+
badCount,
|
|
1617
|
+
latestRating: asString(payload.latest_rating),
|
|
1618
|
+
latestRatedAt,
|
|
1619
|
+
latestBadAt: asString(payload.latest_bad_at),
|
|
1620
|
+
hasBadRating,
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
function asString(value) {
|
|
1624
|
+
return typeof value === 'string' ? value : undefined;
|
|
1625
|
+
}
|
|
1626
|
+
function asNumber(value) {
|
|
1627
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1628
|
+
}
|
|
1629
|
+
function asBoolean(value) {
|
|
1630
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
1631
|
+
}
|
|
1632
|
+
function asRecordUnknown(value) {
|
|
1633
|
+
return isObject(value) ? value : undefined;
|
|
1634
|
+
}
|
|
1635
|
+
function isObject(value) {
|
|
1636
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1637
|
+
}
|
|
1638
|
+
function ratingErrorText(responseText, status) {
|
|
1639
|
+
if (responseText.length > 0) {
|
|
1640
|
+
return responseText;
|
|
1641
|
+
}
|
|
1642
|
+
return `HTTP ${status}`;
|
|
1643
|
+
}
|
|
1644
|
+
function captureEmbeddingInputTexts(inputTexts, maxInputItems, maxTextLength) {
|
|
1645
|
+
if (inputTexts.length === 0) {
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
const itemLimit = maxInputItems > 0 ? maxInputItems : 20;
|
|
1649
|
+
const textLimit = maxTextLength > 0 ? maxTextLength : 1024;
|
|
1650
|
+
const output = [];
|
|
1651
|
+
const end = Math.min(itemLimit, inputTexts.length);
|
|
1652
|
+
for (let index = 0; index < end; index++) {
|
|
1653
|
+
output.push(truncateEmbeddingText(inputTexts[index] ?? '', textLimit));
|
|
1654
|
+
}
|
|
1655
|
+
return output;
|
|
1656
|
+
}
|
|
1657
|
+
function truncateEmbeddingText(text, maxTextLength) {
|
|
1658
|
+
if (text.length <= maxTextLength) {
|
|
1659
|
+
return text;
|
|
1660
|
+
}
|
|
1661
|
+
if (maxTextLength <= 3) {
|
|
1662
|
+
return text.slice(0, maxTextLength);
|
|
1663
|
+
}
|
|
1664
|
+
return `${text.slice(0, maxTextLength - 3)}...`;
|
|
1665
|
+
}
|
|
1666
|
+
function thinkingBudgetFromMetadata(metadata) {
|
|
1667
|
+
if (metadata === undefined) {
|
|
1668
|
+
return undefined;
|
|
1669
|
+
}
|
|
1670
|
+
const raw = metadata[spanAttrRequestThinkingBudget];
|
|
1671
|
+
if (raw === undefined || raw === null || typeof raw === 'boolean') {
|
|
1672
|
+
return undefined;
|
|
1673
|
+
}
|
|
1674
|
+
if (typeof raw === 'number') {
|
|
1675
|
+
if (!Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
1676
|
+
return undefined;
|
|
1677
|
+
}
|
|
1678
|
+
return raw;
|
|
1679
|
+
}
|
|
1680
|
+
if (typeof raw === 'string') {
|
|
1681
|
+
const trimmed = raw.trim();
|
|
1682
|
+
if (trimmed.length === 0) {
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1686
|
+
if (Number.isNaN(parsed)) {
|
|
1687
|
+
return undefined;
|
|
1688
|
+
}
|
|
1689
|
+
return parsed;
|
|
1690
|
+
}
|
|
1691
|
+
return undefined;
|
|
1692
|
+
}
|
|
1693
|
+
function metadataStringValue(metadata, key) {
|
|
1694
|
+
if (metadata === undefined) {
|
|
1695
|
+
return undefined;
|
|
1696
|
+
}
|
|
1697
|
+
const value = metadata[key];
|
|
1698
|
+
if (typeof value !== 'string') {
|
|
1699
|
+
return undefined;
|
|
1700
|
+
}
|
|
1701
|
+
const trimmed = value.trim();
|
|
1702
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
1703
|
+
}
|
|
1704
|
+
function metadataIntValue(metadata, key) {
|
|
1705
|
+
if (metadata === undefined) {
|
|
1706
|
+
return undefined;
|
|
1707
|
+
}
|
|
1708
|
+
const value = metadata[key];
|
|
1709
|
+
if (value === undefined || value === null || typeof value === 'boolean') {
|
|
1710
|
+
return undefined;
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof value === 'number') {
|
|
1713
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
return value;
|
|
1717
|
+
}
|
|
1718
|
+
if (typeof value === 'string') {
|
|
1719
|
+
const trimmed = value.trim();
|
|
1720
|
+
if (trimmed.length === 0) {
|
|
1721
|
+
return undefined;
|
|
1722
|
+
}
|
|
1723
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
1724
|
+
if (Number.isNaN(parsed)) {
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
return parsed;
|
|
1728
|
+
}
|
|
1729
|
+
return undefined;
|
|
1730
|
+
}
|
|
1731
|
+
function firstNonEmptyString(...values) {
|
|
1732
|
+
for (const value of values) {
|
|
1733
|
+
if (notEmpty(value)) {
|
|
1734
|
+
return value;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return undefined;
|
|
1738
|
+
}
|
|
1739
|
+
function mergeStringRecords(left, right) {
|
|
1740
|
+
if (left === undefined && right === undefined) {
|
|
1741
|
+
return undefined;
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
...(left ?? {}),
|
|
1745
|
+
...(right ?? {}),
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function mergeUnknownRecords(left, right) {
|
|
1749
|
+
if (left === undefined && right === undefined) {
|
|
1750
|
+
return undefined;
|
|
1751
|
+
}
|
|
1752
|
+
return {
|
|
1753
|
+
...(left ?? {}),
|
|
1754
|
+
...(right ?? {}),
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function countToolCallParts(messages) {
|
|
1758
|
+
let total = 0;
|
|
1759
|
+
for (const message of messages) {
|
|
1760
|
+
if (message.parts === undefined) {
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1763
|
+
for (const part of message.parts) {
|
|
1764
|
+
if (part.type === 'tool_call') {
|
|
1765
|
+
total += 1;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return total;
|
|
1770
|
+
}
|
|
1771
|
+
function errorCategoryFromError(error, fallbackSDK) {
|
|
1772
|
+
if (error === undefined || error === null) {
|
|
1773
|
+
return fallbackSDK ? 'sdk_error' : '';
|
|
1774
|
+
}
|
|
1775
|
+
if (typeof error === 'string') {
|
|
1776
|
+
return classifyErrorCategory(extractStatusCodeFromError(error), error, fallbackSDK);
|
|
1777
|
+
}
|
|
1778
|
+
const typed = error;
|
|
1779
|
+
const statusCode = extractStatusCodeFromObject(typed) ?? extractStatusCodeFromError(asError(error).message);
|
|
1780
|
+
const message = asError(error).message;
|
|
1781
|
+
return classifyErrorCategory(statusCode, message, fallbackSDK);
|
|
1782
|
+
}
|
|
1783
|
+
function classifyErrorCategory(statusCode, message, fallbackSDK) {
|
|
1784
|
+
const lowerMessage = message.toLowerCase();
|
|
1785
|
+
if (lowerMessage.includes('timeout') || lowerMessage.includes('deadline exceeded')) {
|
|
1786
|
+
return 'timeout';
|
|
1787
|
+
}
|
|
1788
|
+
if (statusCode === 429) {
|
|
1789
|
+
return 'rate_limit';
|
|
1790
|
+
}
|
|
1791
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1792
|
+
return 'auth_error';
|
|
1793
|
+
}
|
|
1794
|
+
if (statusCode === 408) {
|
|
1795
|
+
return 'timeout';
|
|
1796
|
+
}
|
|
1797
|
+
if (statusCode !== undefined && statusCode >= 500 && statusCode <= 599) {
|
|
1798
|
+
return 'server_error';
|
|
1799
|
+
}
|
|
1800
|
+
if (statusCode !== undefined && statusCode >= 400 && statusCode <= 499) {
|
|
1801
|
+
return 'client_error';
|
|
1802
|
+
}
|
|
1803
|
+
return fallbackSDK ? 'sdk_error' : '';
|
|
1804
|
+
}
|
|
1805
|
+
function extractStatusCodeFromObject(error) {
|
|
1806
|
+
const direct = asStatusCode(error.status) ?? asStatusCode(error.statusCode);
|
|
1807
|
+
if (direct !== undefined) {
|
|
1808
|
+
return direct;
|
|
1809
|
+
}
|
|
1810
|
+
if (isRecord(error.response)) {
|
|
1811
|
+
return asStatusCode(error.response.status) ?? asStatusCode(error.response.statusCode);
|
|
1812
|
+
}
|
|
1813
|
+
if (isRecord(error.error)) {
|
|
1814
|
+
return asStatusCode(error.error.status) ?? asStatusCode(error.error.statusCode);
|
|
1815
|
+
}
|
|
1816
|
+
return undefined;
|
|
1817
|
+
}
|
|
1818
|
+
function extractStatusCodeFromError(message) {
|
|
1819
|
+
const matches = message.match(/\b([1-5]\d\d)\b/g);
|
|
1820
|
+
if (matches === null) {
|
|
1821
|
+
return undefined;
|
|
1822
|
+
}
|
|
1823
|
+
for (const match of matches) {
|
|
1824
|
+
const parsed = Number.parseInt(match, 10);
|
|
1825
|
+
if (!Number.isNaN(parsed) && parsed >= 100 && parsed <= 599) {
|
|
1826
|
+
return parsed;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
return undefined;
|
|
1830
|
+
}
|
|
1831
|
+
function asStatusCode(value) {
|
|
1832
|
+
if (typeof value !== 'number') {
|
|
1833
|
+
return undefined;
|
|
1834
|
+
}
|
|
1835
|
+
if (!Number.isInteger(value) || value < 100 || value > 599) {
|
|
1836
|
+
return undefined;
|
|
1837
|
+
}
|
|
1838
|
+
return value;
|
|
1839
|
+
}
|
|
1840
|
+
function isRecord(value) {
|
|
1841
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1842
|
+
}
|
|
1843
|
+
function isJSON(value) {
|
|
1844
|
+
try {
|
|
1845
|
+
JSON.parse(value);
|
|
1846
|
+
return true;
|
|
1847
|
+
}
|
|
1848
|
+
catch {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function notEmpty(value) {
|
|
1853
|
+
return value !== undefined && value.trim().length > 0;
|
|
1854
|
+
}
|
|
1855
|
+
//# sourceMappingURL=client.js.map
|