@juspay/neurolink 12.12.9 → 12.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +394 -396
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +60 -18
- package/dist/cli/commands/proxyAnalyze.js +8 -1
- package/dist/core/baseProvider.d.ts +0 -22
- package/dist/core/baseProvider.js +18 -81
- package/dist/providers/anthropic/client.js +4 -1
- package/dist/providers/openaiChatCompletionsBase.js +4 -1
- package/dist/proxy/bodyCaptureProcessing.d.ts +22 -0
- package/dist/proxy/bodyCaptureProcessing.js +219 -0
- package/dist/proxy/bodyCaptureWorker.d.ts +16 -0
- package/dist/proxy/bodyCaptureWorker.js +232 -0
- package/dist/proxy/bodyCaptureWorkerEntry.d.ts +1 -0
- package/dist/proxy/bodyCaptureWorkerEntry.js +34 -0
- package/dist/proxy/proxyAnalysis.js +159 -17
- package/dist/proxy/proxyLifecycle.d.ts +25 -0
- package/dist/proxy/proxyLifecycle.js +111 -5
- package/dist/proxy/proxyRequestKind.d.ts +2 -0
- package/dist/proxy/proxyRequestKind.js +6 -0
- package/dist/proxy/proxyRuntimeMetrics.d.ts +3 -0
- package/dist/proxy/proxyRuntimeMetrics.js +34 -0
- package/dist/proxy/requestLogger.d.ts +10 -6
- package/dist/proxy/requestLogger.js +99 -231
- package/dist/proxy/rollingProxyServer.js +15 -4
- package/dist/proxy/rollingWorkerProcess.d.ts +4 -0
- package/dist/proxy/rollingWorkerProcess.js +25 -8
- package/dist/proxy/rollingWorkerProtocol.d.ts +6 -0
- package/dist/proxy/rollingWorkerProtocol.js +12 -1
- package/dist/proxy/rollingWorkerSupervisor.d.ts +28 -0
- package/dist/proxy/rollingWorkerSupervisor.js +73 -25
- package/dist/proxy/socketWorkerRuntime.d.ts +5 -0
- package/dist/proxy/socketWorkerRuntime.js +19 -2
- package/dist/server/routes/codexProxyRoutes.js +39 -3
- package/dist/services/server/ai/observability/instrumentation.js +7 -1
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/proxy.d.ts +69 -4
- package/package.json +1 -1
- package/dist/core/modules/GenerationHandler.d.ts +0 -145
- package/dist/core/modules/GenerationHandler.js +0 -754
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generation Handler Module
|
|
3
|
-
*
|
|
4
|
-
* Handles text generation execution, result formatting, and tool information extraction.
|
|
5
|
-
* Extracted from BaseProvider to follow Single Responsibility Principle.
|
|
6
|
-
*
|
|
7
|
-
* Responsibilities:
|
|
8
|
-
* - Generation execution with AI SDK
|
|
9
|
-
* - Tool information extraction
|
|
10
|
-
* - Result formatting and enhancement
|
|
11
|
-
* - Response analysis and logging
|
|
12
|
-
*
|
|
13
|
-
* @module core/modules/GenerationHandler
|
|
14
|
-
*/
|
|
15
|
-
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
16
|
-
import { getModelId } from "../../providers/providerTypeUtils.js";
|
|
17
|
-
import { tracers } from "../../telemetry/tracers.js";
|
|
18
|
-
import { logger } from "../../utils/logger.js";
|
|
19
|
-
import { calculateCost } from "../../utils/pricing.js";
|
|
20
|
-
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
21
|
-
import { parseTimeout } from "../../utils/timeout.js";
|
|
22
|
-
import { calculateCacheSavingsPercent, extractCacheCreationTokens, extractCacheReadTokens, extractTokenUsage, } from "../../utils/tokenUtils.js";
|
|
23
|
-
import { DEFAULT_MAX_STEPS, DEFAULT_WRAPUP_TIME_LEAD_MS, } from "../constants.js";
|
|
24
|
-
import { isTemperatureDeprecatedError, isSchemaComplexityError, isToolsSchemaConflictError, } from "./structuredOutputPolicy.js";
|
|
25
|
-
import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "../../utils/json/coerce.js";
|
|
26
|
-
import { NoObjectGeneratedError } from "../../utils/generationErrors.js";
|
|
27
|
-
const genTracer = tracers.generation;
|
|
28
|
-
/**
|
|
29
|
-
* Safely preview-serialize a value for debug logging.
|
|
30
|
-
* Handles undefined, circular references, and non-serializable values.
|
|
31
|
-
*/
|
|
32
|
-
function safePreview(v) {
|
|
33
|
-
if (v === undefined) {
|
|
34
|
-
return "";
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
const text = typeof v === "string" ? v : JSON.stringify(v);
|
|
38
|
-
return (text ?? "").substring(0, 200);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return "[unserializable]";
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Turn budget + wrap-up deadline (parity with the googleVertex native loops).
|
|
46
|
-
* A deadline is engaged only when the caller expressed one: turnTimeoutMs
|
|
47
|
-
* wins, else an explicit generate timeout. Callers that set neither keep the
|
|
48
|
-
* pre-existing behaviour (no wrap-up; the outer defensive timeout in
|
|
49
|
-
* executeStandardGenerateFlow still applies). With `wrapupTimeLeadMs` left of
|
|
50
|
-
* the deadline, the loop stops offering tools (toolChoice: "none") so the
|
|
51
|
-
* model spends the remaining budget producing a final answer instead of being
|
|
52
|
-
* guillotined mid-tool-loop with all work discarded. The lead is clamped to a
|
|
53
|
-
* quarter of the budget so short explicit timeouts (e.g. 30s) don't trigger
|
|
54
|
-
* wrap-up on the very first step.
|
|
55
|
-
*
|
|
56
|
-
* `turnStartMs` anchors the deadline to the ORIGINAL generation start:
|
|
57
|
-
* callGenerateText re-runs on executeGeneration's fallback retries
|
|
58
|
-
* (structured-output conflict, temperature-deprecated) and provider retries,
|
|
59
|
-
* and a deadline computed from Date.now() per attempt would hand each retry
|
|
60
|
-
* a fresh budget — multiplying the caller's wall-clock cap.
|
|
61
|
-
*/
|
|
62
|
-
export function resolveTurnBudget(options, turnStartMs) {
|
|
63
|
-
const callerTimeoutMs = parseTimeout(options.timeout);
|
|
64
|
-
const hasValidTurnTimeout = typeof options.turnTimeoutMs === "number" &&
|
|
65
|
-
Number.isFinite(options.turnTimeoutMs) &&
|
|
66
|
-
options.turnTimeoutMs > 0;
|
|
67
|
-
if (options.turnTimeoutMs !== undefined && !hasValidTurnTimeout) {
|
|
68
|
-
logger.warn("[GenerationHandler] Ignoring invalid turnTimeoutMs — expected a positive number of milliseconds; falling back to the timeout option", { turnTimeoutMs: options.turnTimeoutMs });
|
|
69
|
-
}
|
|
70
|
-
let turnBudgetMs = hasValidTurnTimeout
|
|
71
|
-
? options.turnTimeoutMs
|
|
72
|
-
: callerTimeoutMs;
|
|
73
|
-
let wrapupLeadMs = turnBudgetMs
|
|
74
|
-
? Math.min(options.wrapupTimeLeadMs ?? DEFAULT_WRAPUP_TIME_LEAD_MS, Math.floor(turnBudgetMs / 4))
|
|
75
|
-
: 0;
|
|
76
|
-
// When the budget is DERIVED from the generate `timeout`, the hard abort in
|
|
77
|
-
// executeStandardGenerateFlow fires at exactly callerTimeoutMs — the same
|
|
78
|
-
// instant as the turn deadline. Wrap-up would engage at (deadline − lead)
|
|
79
|
-
// but its final, tools-off generation then RACES the abort and loses on
|
|
80
|
-
// slow models (observed: wrap-up engaged at T−lead, final answer killed at
|
|
81
|
-
// exactly T → TimeoutError, all work discarded). Pull the turn deadline one
|
|
82
|
-
// wrap-up lead earlier so the final generation runs in EXCLUSIVE margin
|
|
83
|
-
// before the abort. An explicit turnTimeoutMs is left untouched — the
|
|
84
|
-
// caller separated the two deadlines deliberately.
|
|
85
|
-
if (!hasValidTurnTimeout && turnBudgetMs !== undefined && wrapupLeadMs > 0) {
|
|
86
|
-
turnBudgetMs = turnBudgetMs - wrapupLeadMs;
|
|
87
|
-
// Keep the quarter-budget clamp invariant against the reduced budget so
|
|
88
|
-
// short timeouts still don't wrap up on the very first step.
|
|
89
|
-
wrapupLeadMs = Math.min(wrapupLeadMs, Math.floor(turnBudgetMs / 4));
|
|
90
|
-
}
|
|
91
|
-
const turnDeadline = turnBudgetMs ? turnStartMs + turnBudgetMs : undefined;
|
|
92
|
-
return { callerTimeoutMs, turnBudgetMs, wrapupLeadMs, turnDeadline };
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* GenerationHandler class - Handles text generation operations for AI providers
|
|
96
|
-
*/
|
|
97
|
-
export class GenerationHandler {
|
|
98
|
-
providerName;
|
|
99
|
-
modelName;
|
|
100
|
-
supportsToolsFn;
|
|
101
|
-
getTelemetryConfigFn;
|
|
102
|
-
handleToolStorageFn;
|
|
103
|
-
deps;
|
|
104
|
-
constructor(providerName, modelName, supportsToolsFn, getTelemetryConfigFn, handleToolStorageFn,
|
|
105
|
-
/**
|
|
106
|
-
* The remaining, optional dependencies.
|
|
107
|
-
*
|
|
108
|
-
* Grouped rather than added as further positional parameters: the
|
|
109
|
-
* constructor is already at the six-parameter cap, and both of these are
|
|
110
|
-
* optional injection seams rather than required collaborators.
|
|
111
|
-
*
|
|
112
|
-
* `generateTextFn` exists because every other dependency of this class
|
|
113
|
-
* arrives through the constructor while `generateText` was reached by
|
|
114
|
-
* static import. `utils/generation.ts` is a bare re-export of `ai`, and an
|
|
115
|
-
* ESM re-export cannot be substituted from a test, so asserting on the
|
|
116
|
-
* arguments this class builds — the entire contract of the system-message
|
|
117
|
-
* hoisting below — had no seam to work through. Production passes neither.
|
|
118
|
-
*/
|
|
119
|
-
deps = {}) {
|
|
120
|
-
this.providerName = providerName;
|
|
121
|
-
this.modelName = modelName;
|
|
122
|
-
this.supportsToolsFn = supportsToolsFn;
|
|
123
|
-
this.getTelemetryConfigFn = getTelemetryConfigFn;
|
|
124
|
-
this.handleToolStorageFn = handleToolStorageFn;
|
|
125
|
-
this.deps = deps;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Helper method to call generateText with optional structured output
|
|
129
|
-
* @private
|
|
130
|
-
*/
|
|
131
|
-
/**
|
|
132
|
-
* The ai-package generate loop.
|
|
133
|
-
*
|
|
134
|
-
* Unreachable: every text provider now implements a native generate() and
|
|
135
|
-
* none of them return here. That was established by trapping the seam —
|
|
136
|
-
* replacing the ai package's generateText with a throwing stub left the full
|
|
137
|
-
* provider matrix passing and zero cells reaching it — and non-text request
|
|
138
|
-
* kinds return from runGenerateInActiveContext before this handler is
|
|
139
|
-
* consulted.
|
|
140
|
-
*
|
|
141
|
-
* Kept as an explicit failure rather than deleted outright so a provider
|
|
142
|
-
* added without a native generate() fails loudly here instead of silently
|
|
143
|
-
* reintroducing a dependency on the removed package.
|
|
144
|
-
*/
|
|
145
|
-
async callGenerateText(_model, _messages, _tools, _options, _callConfig) {
|
|
146
|
-
throw new Error("GenerationHandler.callGenerateText is no longer implemented: every provider must supply a native generate(). See docs/plans/2026-09-03-completing-the-ai-sdk-removal.md");
|
|
147
|
-
}
|
|
148
|
-
async executeGeneration(model, messages, tools, options) {
|
|
149
|
-
return genTracer.startActiveSpan("neurolink.executeGeneration", { kind: SpanKind.INTERNAL }, async (span) => {
|
|
150
|
-
const shouldUseTools = !options.disableTools && this.supportsToolsFn();
|
|
151
|
-
const toolCount = Object.keys(tools || {}).length;
|
|
152
|
-
const useStructuredOutput = !!options.schema ||
|
|
153
|
-
options.output?.format === "json" ||
|
|
154
|
-
options.output?.format === "structured";
|
|
155
|
-
span.setAttribute("gen_ai.system", this.providerName || "unknown");
|
|
156
|
-
span.setAttribute("neurolink.structured_output", useStructuredOutput);
|
|
157
|
-
span.setAttribute("neurolink.tool_count", toolCount);
|
|
158
|
-
span.setAttribute("neurolink.message_count", messages.length);
|
|
159
|
-
span.setAttribute("gen_ai.request.model", getModelId(model, this.modelName || "unknown"));
|
|
160
|
-
const requestId = options.requestId ||
|
|
161
|
-
options.context?.requestId ||
|
|
162
|
-
"unknown";
|
|
163
|
-
logger.info("[GenerationHandler] Calling generateText", {
|
|
164
|
-
requestId,
|
|
165
|
-
model: getModelId(model),
|
|
166
|
-
messageCount: messages.length,
|
|
167
|
-
toolCount,
|
|
168
|
-
maxSteps: options.maxSteps,
|
|
169
|
-
temperature: options.temperature,
|
|
170
|
-
});
|
|
171
|
-
if (logger.shouldLog("debug")) {
|
|
172
|
-
try {
|
|
173
|
-
logger.debug("[Observability] Full generateText parameters", {
|
|
174
|
-
requestId,
|
|
175
|
-
model: getModelId(model),
|
|
176
|
-
messageCount: messages.length,
|
|
177
|
-
messages: messages.map((msg, i) => ({
|
|
178
|
-
index: i,
|
|
179
|
-
role: msg.role,
|
|
180
|
-
contentLength: typeof msg.content === "string"
|
|
181
|
-
? msg.content.length
|
|
182
|
-
: safePreview(msg.content).length,
|
|
183
|
-
contentPreview: typeof msg.content === "string"
|
|
184
|
-
? msg.content.substring(0, 200)
|
|
185
|
-
: "[multimodal]",
|
|
186
|
-
})),
|
|
187
|
-
toolNames: Object.keys(tools || {}),
|
|
188
|
-
toolCount,
|
|
189
|
-
maxSteps: options.maxSteps,
|
|
190
|
-
temperature: options.temperature,
|
|
191
|
-
maxTokens: options.maxTokens,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
// Ignore serialization errors in debug logging
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const genStartTime = Date.now();
|
|
199
|
-
try {
|
|
200
|
-
const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, {
|
|
201
|
-
shouldUseTools,
|
|
202
|
-
includeStructuredOutput: true,
|
|
203
|
-
turnStartMs: genStartTime,
|
|
204
|
-
}), span, "generateText");
|
|
205
|
-
logger.info("[GenerationHandler] generateText returned", {
|
|
206
|
-
requestId,
|
|
207
|
-
durationMs: Date.now() - genStartTime,
|
|
208
|
-
finishReason: result.finishReason,
|
|
209
|
-
steps: result.steps?.length || 1,
|
|
210
|
-
toolCallsTotal: result.toolCalls?.length || 0,
|
|
211
|
-
responseChars: result.text?.length || 0,
|
|
212
|
-
});
|
|
213
|
-
if (logger.shouldLog("debug")) {
|
|
214
|
-
logger.debug("[Observability] LLM response metadata", {
|
|
215
|
-
requestId,
|
|
216
|
-
responseLength: result.text?.length || 0,
|
|
217
|
-
hasToolCalls: !!(result.toolCalls && result.toolCalls.length > 0),
|
|
218
|
-
toolCallCount: result.toolCalls?.length || 0,
|
|
219
|
-
toolNames: result.toolCalls?.map((tc) => tc.toolName),
|
|
220
|
-
finishReason: result.finishReason,
|
|
221
|
-
stepCount: result.steps?.length || 0,
|
|
222
|
-
steps: result.steps?.map((step, i) => ({
|
|
223
|
-
stepIndex: i,
|
|
224
|
-
stepType: step.stepType,
|
|
225
|
-
textLength: step.text?.length || 0,
|
|
226
|
-
toolCallCount: step.toolCalls?.length || 0,
|
|
227
|
-
toolNames: step.toolCalls?.map((tc) => tc.toolName),
|
|
228
|
-
toolResultCount: step.toolResults?.length || 0,
|
|
229
|
-
finishReason: step.finishReason,
|
|
230
|
-
})),
|
|
231
|
-
usage: result.usage,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
// Set token usage and completion attributes on span
|
|
235
|
-
this.setUsageSpanAttributes(span, result);
|
|
236
|
-
if (result.finishReason) {
|
|
237
|
-
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
238
|
-
}
|
|
239
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
// Fall back to text-mode (no experimental_output) when structured
|
|
244
|
-
// output + tools failed, in three cases:
|
|
245
|
-
// 1. NoObjectGeneratedError — the SDK couldn't coerce the object.
|
|
246
|
-
// 2. The provider rejected json-mode-with-tools outright (e.g. Groq:
|
|
247
|
-
// "json mode cannot be combined with tool/function calling").
|
|
248
|
-
// 3. The provider rejected the schema as too complex for its
|
|
249
|
-
// constrained decoding (Vertex Gemini 400 "too many states") —
|
|
250
|
-
// deterministic, so re-sending the schema can never succeed.
|
|
251
|
-
// In all cases we retry without structured output and let
|
|
252
|
-
// formatEnhancedResult coerce the text response into valid JSON.
|
|
253
|
-
const schemaTooComplex = useStructuredOutput && isSchemaComplexityError(error);
|
|
254
|
-
const isStructuredOutputConflict = useStructuredOutput &&
|
|
255
|
-
(error instanceof NoObjectGeneratedError ||
|
|
256
|
-
isToolsSchemaConflictError(error) ||
|
|
257
|
-
schemaTooComplex);
|
|
258
|
-
if (isStructuredOutputConflict) {
|
|
259
|
-
span.setAttribute("neurolink.has_fallback", true);
|
|
260
|
-
// NLK-GAP-007: Record initial failure event before fallback retry
|
|
261
|
-
span.addEvent("retry.initial_failure", {
|
|
262
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
263
|
-
"retry.attempt": 1,
|
|
264
|
-
"retry.reason": error instanceof NoObjectGeneratedError
|
|
265
|
-
? "NoObjectGeneratedError_structured_output_fallback"
|
|
266
|
-
: schemaTooComplex
|
|
267
|
-
? "schema_complexity_structured_output_fallback"
|
|
268
|
-
: "tools_schema_conflict_structured_output_fallback",
|
|
269
|
-
});
|
|
270
|
-
if (schemaTooComplex) {
|
|
271
|
-
// warn (not debug): callers should simplify their schema — the
|
|
272
|
-
// fallback keeps the turn alive but skips native enforcement.
|
|
273
|
-
logger.warn("[GenerationHandler] schema too complex for provider constrained decoding — retrying with prompt-based JSON", {
|
|
274
|
-
provider: this.providerName,
|
|
275
|
-
model: this.modelName,
|
|
276
|
-
error: error instanceof Error ? error.message : String(error),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
logger.debug("[GenerationHandler] structured-output conflict caught - falling back to manual JSON extraction", {
|
|
281
|
-
provider: this.providerName,
|
|
282
|
-
model: this.modelName,
|
|
283
|
-
error: error instanceof Error ? error.message : String(error),
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
// Retry without experimental_output - the formatEnhancedResult method
|
|
287
|
-
// will extract JSON from the text response
|
|
288
|
-
const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, options, {
|
|
289
|
-
shouldUseTools,
|
|
290
|
-
// includeStructuredOutput intentionally omitted
|
|
291
|
-
includeStructuredOutput: false,
|
|
292
|
-
turnStartMs: genStartTime,
|
|
293
|
-
}), span, "generateText(fallback)");
|
|
294
|
-
// NLK-GAP-007: Record recovery event after successful fallback
|
|
295
|
-
span.addEvent("retry.recovered", {
|
|
296
|
-
"retry.attempts": 2,
|
|
297
|
-
"retry.strategy": "structured_output_disabled",
|
|
298
|
-
});
|
|
299
|
-
span.setAttribute("retry.count", 1);
|
|
300
|
-
logger.info("[GenerationHandler] generateText returned (fallback)", {
|
|
301
|
-
requestId,
|
|
302
|
-
durationMs: Date.now() - genStartTime,
|
|
303
|
-
finishReason: result.finishReason,
|
|
304
|
-
steps: result.steps?.length || 1,
|
|
305
|
-
toolCallsTotal: result.toolCalls?.length || 0,
|
|
306
|
-
responseChars: result.text?.length || 0,
|
|
307
|
-
});
|
|
308
|
-
this.setUsageSpanAttributes(span, result);
|
|
309
|
-
if (result.finishReason) {
|
|
310
|
-
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
311
|
-
}
|
|
312
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
313
|
-
return result;
|
|
314
|
-
}
|
|
315
|
-
// Retry once without `temperature` when the model deprecated it. The
|
|
316
|
-
// newest Anthropic models (e.g. claude-opus-4-8 with tools + advanced
|
|
317
|
-
// beta features) reject `temperature` — "`temperature` is deprecated
|
|
318
|
-
// for this model." — in favour of reasoning-effort controls. Structured
|
|
319
|
-
// output is already excluded for the native anthropic surface, so this
|
|
320
|
-
// is the dominant failure mode for Opus there.
|
|
321
|
-
if (isTemperatureDeprecatedError(error) &&
|
|
322
|
-
typeof options.temperature === "number") {
|
|
323
|
-
span.setAttribute("neurolink.has_fallback", true);
|
|
324
|
-
span.addEvent("retry.initial_failure", {
|
|
325
|
-
"error.message": error instanceof Error ? error.message : String(error),
|
|
326
|
-
"retry.attempt": 1,
|
|
327
|
-
"retry.reason": "temperature_deprecated",
|
|
328
|
-
});
|
|
329
|
-
logger.debug("[GenerationHandler] temperature-deprecated error caught - retrying without temperature", {
|
|
330
|
-
provider: this.providerName,
|
|
331
|
-
model: this.modelName,
|
|
332
|
-
error: error instanceof Error ? error.message : String(error),
|
|
333
|
-
});
|
|
334
|
-
const result = await withProviderRetry(() => this.callGenerateText(model, messages, tools, { ...options, temperature: undefined }, {
|
|
335
|
-
shouldUseTools,
|
|
336
|
-
// mirror the initial call; the structured-output policy still applies
|
|
337
|
-
includeStructuredOutput: true,
|
|
338
|
-
turnStartMs: genStartTime,
|
|
339
|
-
}), span, "generateText(no-temperature)");
|
|
340
|
-
span.addEvent("retry.recovered", {
|
|
341
|
-
"retry.attempts": 2,
|
|
342
|
-
"retry.strategy": "temperature_omitted",
|
|
343
|
-
});
|
|
344
|
-
span.setAttribute("retry.count", 1);
|
|
345
|
-
this.setUsageSpanAttributes(span, result);
|
|
346
|
-
if (result.finishReason) {
|
|
347
|
-
span.setAttribute("gen_ai.response.finish_reason", result.finishReason);
|
|
348
|
-
}
|
|
349
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
span.setStatus({
|
|
353
|
-
code: SpanStatusCode.ERROR,
|
|
354
|
-
message: error instanceof Error ? error.message : String(error),
|
|
355
|
-
});
|
|
356
|
-
// Re-throw other errors
|
|
357
|
-
throw error;
|
|
358
|
-
}
|
|
359
|
-
finally {
|
|
360
|
-
span.end();
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Extract cache metrics from provider metadata (e.g. Anthropic's providerMetadata.anthropic)
|
|
366
|
-
* The AI SDK's LanguageModelUsage only has inputTokens/outputTokens.
|
|
367
|
-
* Cache metrics are surfaced via providerMetadata by provider-specific SDK adapters.
|
|
368
|
-
*/
|
|
369
|
-
/**
|
|
370
|
-
* Set gen_ai usage attributes + cache-aware cost on the span from the
|
|
371
|
-
* CROSS-STEP aggregate (result.totalUsage). result.usage is the LAST step
|
|
372
|
-
* only — using it undercounted every multi-step tool loop, and pricing the
|
|
373
|
-
* raw cache-inclusive inputTokens without the cache fields billed cache
|
|
374
|
-
* reads at the full input rate.
|
|
375
|
-
*/
|
|
376
|
-
setUsageSpanAttributes(span, result) {
|
|
377
|
-
const aggregate = result.totalUsage ?? result.usage;
|
|
378
|
-
if (!aggregate) {
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
span.setAttribute("gen_ai.usage.input_tokens", aggregate.inputTokens || 0);
|
|
382
|
-
span.setAttribute("gen_ai.usage.output_tokens", aggregate.outputTokens || 0);
|
|
383
|
-
// Cost on span so users can query "what did this trace cost?" —
|
|
384
|
-
// extractTokenUsage rebases input onto the uncached remainder and
|
|
385
|
-
// surfaces the cache fields so calculateCost prices each tier.
|
|
386
|
-
const cost = calculateCost(this.providerName, this.modelName, extractTokenUsage(aggregate));
|
|
387
|
-
span.setAttribute("neurolink.cost", cost ?? 0);
|
|
388
|
-
}
|
|
389
|
-
extractCacheMetricsFromProviderMetadata(generateResult) {
|
|
390
|
-
const providerMeta = generateResult.providerMetadata;
|
|
391
|
-
if (!providerMeta) {
|
|
392
|
-
return {};
|
|
393
|
-
}
|
|
394
|
-
// Anthropic surfaces cache metrics under providerMetadata.anthropic
|
|
395
|
-
const anthropicMeta = providerMeta.anthropic;
|
|
396
|
-
if (anthropicMeta) {
|
|
397
|
-
const cacheCreationTokens = extractCacheCreationTokens(anthropicMeta);
|
|
398
|
-
const cacheReadTokens = extractCacheReadTokens(anthropicMeta);
|
|
399
|
-
return {
|
|
400
|
-
...(cacheCreationTokens !== undefined && { cacheCreationTokens }),
|
|
401
|
-
...(cacheReadTokens !== undefined && { cacheReadTokens }),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
return {};
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Log generation completion information
|
|
408
|
-
*/
|
|
409
|
-
logGenerationComplete(generateResult) {
|
|
410
|
-
const cacheMetrics = this.extractCacheMetricsFromProviderMetadata(generateResult);
|
|
411
|
-
if (logger.shouldLog("debug")) {
|
|
412
|
-
logger.debug(`generateText completed`, {
|
|
413
|
-
provider: this.providerName,
|
|
414
|
-
model: this.modelName,
|
|
415
|
-
responseLength: generateResult.text?.length || 0,
|
|
416
|
-
toolResultsCount: generateResult.toolResults?.length || 0,
|
|
417
|
-
finishReason: generateResult.finishReason,
|
|
418
|
-
usage: generateResult.usage,
|
|
419
|
-
...(cacheMetrics.cacheCreationTokens !== undefined && {
|
|
420
|
-
cacheCreationTokens: cacheMetrics.cacheCreationTokens,
|
|
421
|
-
}),
|
|
422
|
-
...(cacheMetrics.cacheReadTokens !== undefined && {
|
|
423
|
-
cacheReadTokens: cacheMetrics.cacheReadTokens,
|
|
424
|
-
}),
|
|
425
|
-
timestamp: Date.now(),
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Extract tool information from generation result
|
|
431
|
-
*/
|
|
432
|
-
extractToolInformation(generateResult) {
|
|
433
|
-
const toolsUsed = [];
|
|
434
|
-
const toolExecutions = [];
|
|
435
|
-
// Extract tool names from tool calls
|
|
436
|
-
if (generateResult.toolCalls && generateResult.toolCalls.length > 0) {
|
|
437
|
-
toolsUsed.push(...generateResult.toolCalls.map((tc) => {
|
|
438
|
-
return tc.toolName || tc.name || "unknown";
|
|
439
|
-
}));
|
|
440
|
-
}
|
|
441
|
-
// Extract from steps
|
|
442
|
-
if (generateResult.steps && Array.isArray(generateResult.steps)) {
|
|
443
|
-
const toolCallArgsMap = new Map();
|
|
444
|
-
for (const step of generateResult.steps || []) {
|
|
445
|
-
// Collect tool calls and their arguments
|
|
446
|
-
if (step?.toolCalls && Array.isArray(step.toolCalls)) {
|
|
447
|
-
for (const toolCall of step.toolCalls) {
|
|
448
|
-
const tcRecord = toolCall;
|
|
449
|
-
const toolName = tcRecord.toolName ||
|
|
450
|
-
tcRecord.name ||
|
|
451
|
-
"unknown";
|
|
452
|
-
const toolId = tcRecord.toolCallId ||
|
|
453
|
-
tcRecord.id ||
|
|
454
|
-
toolName;
|
|
455
|
-
toolsUsed.push(toolName);
|
|
456
|
-
let callArgs = {};
|
|
457
|
-
if (tcRecord.input) {
|
|
458
|
-
// AI SDK v6 carries tool-call arguments as `input`.
|
|
459
|
-
callArgs = tcRecord.input;
|
|
460
|
-
}
|
|
461
|
-
else if (tcRecord.args) {
|
|
462
|
-
callArgs = tcRecord.args;
|
|
463
|
-
}
|
|
464
|
-
else if (tcRecord.arguments) {
|
|
465
|
-
callArgs = tcRecord.arguments;
|
|
466
|
-
}
|
|
467
|
-
else if (tcRecord.parameters) {
|
|
468
|
-
callArgs = tcRecord.parameters;
|
|
469
|
-
}
|
|
470
|
-
toolCallArgsMap.set(toolId, callArgs);
|
|
471
|
-
toolCallArgsMap.set(toolName, callArgs);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
// Process tool results
|
|
475
|
-
if (step?.toolResults && Array.isArray(step.toolResults)) {
|
|
476
|
-
for (const toolResult of step.toolResults) {
|
|
477
|
-
const trRecord = toolResult;
|
|
478
|
-
const toolName = trRecord.toolName || "unknown";
|
|
479
|
-
const toolId = trRecord.toolCallId || trRecord.id;
|
|
480
|
-
const toolArgs = trRecord.args ??
|
|
481
|
-
trRecord.arguments ??
|
|
482
|
-
trRecord.parameters ??
|
|
483
|
-
trRecord.input ??
|
|
484
|
-
toolCallArgsMap.get(toolId || toolName) ??
|
|
485
|
-
{};
|
|
486
|
-
toolExecutions.push({
|
|
487
|
-
name: toolName,
|
|
488
|
-
input: toolArgs,
|
|
489
|
-
output: (trRecord.output ?? trRecord.result) ?? "success",
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return { toolsUsed: [...new Set(toolsUsed)], toolExecutions };
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Format the enhanced result
|
|
499
|
-
*/
|
|
500
|
-
formatEnhancedResult(generateResult, tools, toolsUsed, toolExecutions, options) {
|
|
501
|
-
// Structured output check — schema alone is sufficient to activate
|
|
502
|
-
const useStructuredOutput = !!options.schema ||
|
|
503
|
-
options.output?.format === "json" ||
|
|
504
|
-
options.output?.format === "structured";
|
|
505
|
-
let content;
|
|
506
|
-
let structuredData;
|
|
507
|
-
let jsonRepaired = false;
|
|
508
|
-
let jsonTruncated = false;
|
|
509
|
-
// Strip an outer ```json fence and coerce raw model text into canonical
|
|
510
|
-
// JSON. Object/array roots are recovered via balanced-scan + jsonrepair;
|
|
511
|
-
// scalar JSON roots (string/number/bool) via plain JSON.parse. When
|
|
512
|
-
// nothing JSON-shaped is recoverable, the raw text is returned unchanged,
|
|
513
|
-
// structuredData stays unset, and a WARN makes the broken case observable.
|
|
514
|
-
const coerceTextMode = (rawText) => {
|
|
515
|
-
const strippedText = rawText
|
|
516
|
-
.replace(/^```(?:json)?\s*\n?/i, "")
|
|
517
|
-
.replace(/\n?```\s*$/i, "")
|
|
518
|
-
.trim();
|
|
519
|
-
const coerced = coerceJsonToSchema(strippedText, options.schema);
|
|
520
|
-
if (coerced) {
|
|
521
|
-
structuredData = coerced.structuredData;
|
|
522
|
-
if (coerced.repaired) {
|
|
523
|
-
jsonRepaired = true;
|
|
524
|
-
}
|
|
525
|
-
if (coerced.truncated) {
|
|
526
|
-
jsonTruncated = true;
|
|
527
|
-
}
|
|
528
|
-
return coerced.content;
|
|
529
|
-
}
|
|
530
|
-
const scalar = recoverScalarRoot(strippedText, options.schema);
|
|
531
|
-
switch (scalar.kind) {
|
|
532
|
-
case "empty":
|
|
533
|
-
// A JSON-encoded empty string is an EMPTY completion, not a
|
|
534
|
-
// recovered scalar — normalize to a true empty ('' content, no
|
|
535
|
-
// structuredData) so callers' empty-response handling fires
|
|
536
|
-
// instead of a literal '""' reaching the user.
|
|
537
|
-
logger.warn("[GenerationHandler] schema requested but the model returned an empty JSON string; normalizing to empty content", { provider: this.providerName, model: this.modelName });
|
|
538
|
-
return "";
|
|
539
|
-
case "accepted":
|
|
540
|
-
// A JSON scalar root is only real structured data when the caller's
|
|
541
|
-
// schema actually accepts it. Under an OBJECT schema a recovered
|
|
542
|
-
// string/number is the raw completion in disguise (the shape a
|
|
543
|
-
// truncated response degrades to) — publishing it would hand the
|
|
544
|
-
// caller a `structuredData` that violates the schema they passed.
|
|
545
|
-
structuredData = scalar.value;
|
|
546
|
-
return strippedText;
|
|
547
|
-
case "rejected":
|
|
548
|
-
logger.warn("[GenerationHandler] recovered a JSON scalar the requested schema rejects; leaving structuredData unset", {
|
|
549
|
-
provider: this.providerName,
|
|
550
|
-
model: this.modelName,
|
|
551
|
-
scalarType: typeof scalar.value,
|
|
552
|
-
});
|
|
553
|
-
return strippedText;
|
|
554
|
-
case "nullish":
|
|
555
|
-
case "not-json":
|
|
556
|
-
logger.warn("[GenerationHandler] schema requested but no JSON could be recovered from model text; returning raw text", { provider: this.providerName, model: this.modelName });
|
|
557
|
-
return strippedText;
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
if (useStructuredOutput) {
|
|
561
|
-
try {
|
|
562
|
-
const experimentalOutput = generateResult.experimental_output;
|
|
563
|
-
// ai@6 generateText resolves `output ?? text()` internally, so a
|
|
564
|
-
// result produced WITHOUT an output spec — the structured-output
|
|
565
|
-
// fallback retry, or the tools↔schema exclusion path — no longer
|
|
566
|
-
// throws here: `experimental_output` echoes the RAW MODEL TEXT.
|
|
567
|
-
// Treating that echo as parsed schema output double-encodes the
|
|
568
|
-
// content (JSON.stringify of a string) and, for an empty
|
|
569
|
-
// completion, turns '' into the literal '""'. Detect the echo
|
|
570
|
-
// (a string identical to the step text) and coerce it instead.
|
|
571
|
-
const rawTextEcho = typeof experimentalOutput === "string" &&
|
|
572
|
-
experimentalOutput === (generateResult.text ?? "");
|
|
573
|
-
// The equality check above only catches an EXACT echo. On a multi-step
|
|
574
|
-
// or truncated turn the echo can differ from `text` (a different step's
|
|
575
|
-
// text, a fence, trailing whitespace), and a raw string would then be
|
|
576
|
-
// published as `structuredData` under an object schema — the "returned
|
|
577
|
-
// a string instead of the schema object" failure. A string is trusted
|
|
578
|
-
// as structured output ONLY when the caller's schema accepts it
|
|
579
|
-
// (string-root schemas keep working); otherwise it is coerced like any
|
|
580
|
-
// other raw model text.
|
|
581
|
-
const untrustedStringOutput = typeof experimentalOutput === "string" &&
|
|
582
|
-
!!options.schema &&
|
|
583
|
-
!schemaAccepts(options.schema, experimentalOutput);
|
|
584
|
-
if (experimentalOutput !== undefined &&
|
|
585
|
-
!rawTextEcho &&
|
|
586
|
-
!untrustedStringOutput) {
|
|
587
|
-
// AI-SDK already parsed + schema-validated the object. Expose it
|
|
588
|
-
// directly and serialise canonically — no hand-parsing needed.
|
|
589
|
-
structuredData = experimentalOutput;
|
|
590
|
-
content = JSON.stringify(experimentalOutput);
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
content = coerceTextMode(generateResult.text || "");
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
catch (outputError) {
|
|
597
|
-
// experimental_output is a getter that can throw NoObjectGeneratedError.
|
|
598
|
-
logger.debug("[GenerationHandler] experimental_output threw, falling back to text parsing", {
|
|
599
|
-
error: outputError instanceof Error
|
|
600
|
-
? outputError.message
|
|
601
|
-
: String(outputError),
|
|
602
|
-
});
|
|
603
|
-
content = coerceTextMode(generateResult.text || "");
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
content = generateResult.text;
|
|
608
|
-
}
|
|
609
|
-
// Tie the coercion repair to the provider's truncation signal: if the
|
|
610
|
-
// response stopped on the token cap, treat the structured output as
|
|
611
|
-
// truncated regardless of which coerce candidate won, and warn.
|
|
612
|
-
if (useStructuredOutput && generateResult.finishReason === "length") {
|
|
613
|
-
jsonTruncated = true;
|
|
614
|
-
logger.warn("[GenerationHandler] Structured output truncated by token cap (finishReason=length); increase maxTokens", { provider: this.providerName, model: this.modelName });
|
|
615
|
-
}
|
|
616
|
-
// Extract usage with support for different formats and reasoning tokens.
|
|
617
|
-
// totalUsage is the CROSS-STEP aggregate; generateResult.usage is the
|
|
618
|
-
// LAST step only, which silently dropped every prior step of a
|
|
619
|
-
// multi-step tool loop.
|
|
620
|
-
const usage = extractTokenUsage(generateResult.totalUsage ?? generateResult.usage);
|
|
621
|
-
// Merge cache metrics from providerMetadata if not already present in usage
|
|
622
|
-
// The AI SDK's LanguageModelUsage doesn't include cache tokens; they come from
|
|
623
|
-
// provider-specific metadata (e.g. Anthropic's providerMetadata.anthropic)
|
|
624
|
-
if (usage.cacheCreationTokens === undefined ||
|
|
625
|
-
usage.cacheReadTokens === undefined) {
|
|
626
|
-
const cacheMetrics = this.extractCacheMetricsFromProviderMetadata(generateResult);
|
|
627
|
-
if (usage.cacheCreationTokens === undefined &&
|
|
628
|
-
cacheMetrics.cacheCreationTokens !== undefined) {
|
|
629
|
-
usage.cacheCreationTokens = cacheMetrics.cacheCreationTokens;
|
|
630
|
-
}
|
|
631
|
-
if (usage.cacheReadTokens === undefined &&
|
|
632
|
-
cacheMetrics.cacheReadTokens !== undefined) {
|
|
633
|
-
usage.cacheReadTokens = cacheMetrics.cacheReadTokens;
|
|
634
|
-
}
|
|
635
|
-
// Recalculate cache savings if we added cache metrics
|
|
636
|
-
if (usage.cacheReadTokens !== undefined) {
|
|
637
|
-
const savingsPercent = calculateCacheSavingsPercent(usage.cacheReadTokens, usage.input);
|
|
638
|
-
if (savingsPercent !== undefined) {
|
|
639
|
-
usage.cacheSavingsPercent = savingsPercent;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
// Extract reasoning from AI SDK response (Anthropic thinking, Gemini thought, OpenAI o1)
|
|
644
|
-
// Handle both string and array (AI SDK v5 returns string, v6 returns ReasoningOutput[])
|
|
645
|
-
const rawReasoning = generateResult.reasoning;
|
|
646
|
-
const reasoning = rawReasoning
|
|
647
|
-
? typeof rawReasoning === "string"
|
|
648
|
-
? rawReasoning
|
|
649
|
-
: Array.isArray(rawReasoning)
|
|
650
|
-
? rawReasoning
|
|
651
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
652
|
-
.map((r) => typeof r === "string" ? r : (r.text ?? JSON.stringify(r)))
|
|
653
|
-
.join("\n")
|
|
654
|
-
: String(rawReasoning)
|
|
655
|
-
: undefined;
|
|
656
|
-
const reasoningTokens = usage.reasoning ?? undefined;
|
|
657
|
-
// stopReason / stepsUsed parity with the native loops (Vertex Gemini /
|
|
658
|
-
// Claude / Bedrock): the AI-SDK loop path previously left both undefined,
|
|
659
|
-
// so consumers could not distinguish a completed turn from one truncated
|
|
660
|
-
// by the step cap or ended by the turn budget.
|
|
661
|
-
const steps = generateResult.steps;
|
|
662
|
-
const stepsUsed = Array.isArray(steps) ? steps.length : undefined;
|
|
663
|
-
const maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
664
|
-
let stopReason;
|
|
665
|
-
if (generateResult.__nlTurnWrapup === true) {
|
|
666
|
-
stopReason = "time-limit";
|
|
667
|
-
}
|
|
668
|
-
else if (stepsUsed !== undefined &&
|
|
669
|
-
stepsUsed >= maxSteps &&
|
|
670
|
-
generateResult.finishReason === "tool-calls") {
|
|
671
|
-
stopReason = "step-cap";
|
|
672
|
-
}
|
|
673
|
-
else if (generateResult.finishReason === "error") {
|
|
674
|
-
// Parity with resolveTurnStopReason (native loops): a turn that ended
|
|
675
|
-
// on a provider "error" finish is not a completion. length /
|
|
676
|
-
// content-filter DO map to "completed" — deliberately matching the
|
|
677
|
-
// native contract, where truncation is signaled via finishReason /
|
|
678
|
-
// rawFinishReason / jsonTruncated, never via stopReason.
|
|
679
|
-
stopReason = "provider-error";
|
|
680
|
-
}
|
|
681
|
-
else if (stepsUsed !== undefined) {
|
|
682
|
-
stopReason = "completed";
|
|
683
|
-
}
|
|
684
|
-
return {
|
|
685
|
-
content,
|
|
686
|
-
structuredData,
|
|
687
|
-
usage,
|
|
688
|
-
finishReason: generateResult.finishReason,
|
|
689
|
-
stopReason,
|
|
690
|
-
stepsUsed,
|
|
691
|
-
jsonRepaired: jsonRepaired || undefined,
|
|
692
|
-
jsonTruncated: jsonTruncated || undefined,
|
|
693
|
-
provider: this.providerName,
|
|
694
|
-
model: this.modelName,
|
|
695
|
-
reasoning,
|
|
696
|
-
reasoningTokens,
|
|
697
|
-
toolCalls: generateResult.toolCalls
|
|
698
|
-
? generateResult.toolCalls.map((tc) => ({
|
|
699
|
-
toolCallId: tc.toolCallId || "unknown",
|
|
700
|
-
toolName: tc.toolName || "unknown",
|
|
701
|
-
args: tc.args || {},
|
|
702
|
-
}))
|
|
703
|
-
: [],
|
|
704
|
-
toolResults: generateResult.toolResults ?? [],
|
|
705
|
-
toolsUsed,
|
|
706
|
-
toolExecutions,
|
|
707
|
-
availableTools: Object.keys(tools).map((name) => {
|
|
708
|
-
const tool = tools[name];
|
|
709
|
-
return {
|
|
710
|
-
name,
|
|
711
|
-
description: tool.description || "No description available",
|
|
712
|
-
parameters: tool.inputSchema || {},
|
|
713
|
-
server: tool.serverId || "direct",
|
|
714
|
-
};
|
|
715
|
-
}),
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Analyze AI response structure and log detailed debugging information
|
|
720
|
-
*/
|
|
721
|
-
analyzeAIResponse(rawResult) {
|
|
722
|
-
if (rawResult === null || typeof rawResult !== "object") {
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
const result = rawResult;
|
|
726
|
-
logger.debug("NeuroLink Raw AI Response Analysis", {
|
|
727
|
-
provider: this.providerName,
|
|
728
|
-
model: this.modelName,
|
|
729
|
-
responseTextLength: result.text?.length || 0,
|
|
730
|
-
responsePreview: result.text?.substring(0, 500) ?? "",
|
|
731
|
-
finishReason: result.finishReason,
|
|
732
|
-
usage: result.usage,
|
|
733
|
-
});
|
|
734
|
-
// Tool calls analysis
|
|
735
|
-
const toolCallsAnalysis = {
|
|
736
|
-
hasToolCalls: !!result.toolCalls,
|
|
737
|
-
toolCallsLength: result.toolCalls?.length || 0,
|
|
738
|
-
toolCalls: result.toolCalls?.map((toolCall, index) => {
|
|
739
|
-
const tcRecord = toolCall;
|
|
740
|
-
const toolName = tcRecord.toolName || tcRecord.name || "unknown";
|
|
741
|
-
return {
|
|
742
|
-
index: index + 1,
|
|
743
|
-
toolName,
|
|
744
|
-
toolId: tcRecord.toolCallId || tcRecord.id || "none",
|
|
745
|
-
hasArgs: !!tcRecord.args,
|
|
746
|
-
argsKeys: tcRecord.args && typeof tcRecord.args === "object"
|
|
747
|
-
? Object.keys(tcRecord.args)
|
|
748
|
-
: [],
|
|
749
|
-
};
|
|
750
|
-
}) || [],
|
|
751
|
-
};
|
|
752
|
-
logger.debug("Tool Calls Analysis", toolCallsAnalysis);
|
|
753
|
-
}
|
|
754
|
-
}
|