@stackfactor/agent-utils 1.4.1 → 1.7.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/README.md +88 -18
- package/dist/cjs/index.d.ts +5 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/langChain.d.ts +33 -14
- package/dist/cjs/langChain.d.ts.map +1 -1
- package/dist/cjs/langChain.js +267 -290
- package/dist/cjs/runtimeContext.d.ts +3 -0
- package/dist/cjs/runtimeContext.d.ts.map +1 -1
- package/dist/cjs/serve.d.ts +88 -0
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +230 -3
- package/dist/cjs/tavily.d.ts +4 -0
- package/dist/cjs/tavily.d.ts.map +1 -1
- package/dist/cjs/tavily.js +17 -18
- package/dist/cjs/usage.d.ts +74 -0
- package/dist/cjs/usage.d.ts.map +1 -0
- package/dist/cjs/usage.js +160 -0
- package/dist/esm/index.d.ts +5 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/langChain.d.ts +33 -14
- package/dist/esm/langChain.d.ts.map +1 -1
- package/dist/esm/langChain.js +267 -290
- package/dist/esm/runtimeContext.d.ts +3 -0
- package/dist/esm/runtimeContext.d.ts.map +1 -1
- package/dist/esm/serve.d.ts +88 -0
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +226 -3
- package/dist/esm/tavily.d.ts +4 -0
- package/dist/esm/tavily.d.ts.map +1 -1
- package/dist/esm/tavily.js +17 -18
- package/dist/esm/usage.d.ts +74 -0
- package/dist/esm/usage.d.ts.map +1 -0
- package/dist/esm/usage.js +150 -0
- package/package.json +9 -4
package/dist/cjs/langChain.js
CHANGED
|
@@ -16,6 +16,7 @@ const logger_js_1 = __importDefault(require("./logger.js"));
|
|
|
16
16
|
const zod_1 = require("zod");
|
|
17
17
|
const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
|
|
18
18
|
const runtimeContext_js_1 = require("./runtimeContext.js");
|
|
19
|
+
const usage_js_1 = require("./usage.js");
|
|
19
20
|
const tavily_js_1 = require("./tavily.js");
|
|
20
21
|
const JSON_ESCAPE_INSTRUCTION = `
|
|
21
22
|
CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
|
|
@@ -30,9 +31,10 @@ Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values
|
|
|
30
31
|
* Providers for which `getLLMModel` configures native, schema-constrained JSON
|
|
31
32
|
* output (OpenAI `response_format`, Anthropic `output_config.format`, Gemini
|
|
32
33
|
* `responseSchema` + JSON mode). For these the model emits JSON directly, so the
|
|
33
|
-
* prompt-injected schema and the schema-validation retry are skipped.
|
|
34
|
-
*
|
|
35
|
-
* support
|
|
34
|
+
* prompt-injected schema and the schema-validation retry are skipped. Every
|
|
35
|
+
* supported provider is listed here; the check remains so that adding one
|
|
36
|
+
* without native support falls back to prompt instructions + validation
|
|
37
|
+
* rather than silently skipping them.
|
|
36
38
|
* @param modelName - The model identifier being routed
|
|
37
39
|
* @returns `true` when native structured output is wired up for the provider
|
|
38
40
|
*/
|
|
@@ -103,29 +105,36 @@ const checkIfAIProviderConfigured = (config) => {
|
|
|
103
105
|
}
|
|
104
106
|
};
|
|
105
107
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* absent or not numeric, which leaves cost contribution zero.
|
|
108
|
+
* Counts `amount` of `unit` for `modelName`: into the request's usage ledger,
|
|
109
|
+
* which is reported to the platform, and into the caller's tracker when it
|
|
110
|
+
* passed one. Nothing is counted for an amount that is not positive.
|
|
110
111
|
*/
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const countUsage = (tracker, modelName, unit, amount) => {
|
|
113
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
114
|
+
return;
|
|
115
|
+
(0, usage_js_1.recordUsage)(modelName, unit, amount);
|
|
116
|
+
if (!tracker)
|
|
117
|
+
return;
|
|
118
|
+
const key = `${modelName}_${unit}`;
|
|
119
|
+
tracker.tokens[key] = (tracker.tokens[key] || 0) + amount;
|
|
116
120
|
};
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
* tracker is null/undefined.
|
|
121
|
-
*/
|
|
122
|
-
const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
123
|
-
if (!tracker || !usage || !modelName)
|
|
121
|
+
/** Makes `{}` a tracker. `cost` stays 0: the platform prices what was used. */
|
|
122
|
+
const prepareTracker = (tracker) => {
|
|
123
|
+
if (!tracker)
|
|
124
124
|
return;
|
|
125
125
|
if (typeof tracker.cost !== "number")
|
|
126
126
|
tracker.cost = 0;
|
|
127
127
|
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
128
128
|
tracker.tokens = {};
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Counts a single call's token usage. `config` is no longer read: it carried
|
|
132
|
+
* the rates, and what usage costs is the platform's to say.
|
|
133
|
+
*/
|
|
134
|
+
const updateUsageTracker = (tracker, modelName, usage, _config) => {
|
|
135
|
+
if (!usage || !modelName)
|
|
136
|
+
return;
|
|
137
|
+
prepareTracker(tracker);
|
|
129
138
|
const inputTokens = usage.input_tokens || 0;
|
|
130
139
|
const visibleOutputTokens = usage.output_tokens || 0;
|
|
131
140
|
const totalTokens = usage.total_tokens || 0;
|
|
@@ -134,40 +143,24 @@ const updateUsageTracker = (tracker, modelName, usage, config) => {
|
|
|
134
143
|
// `total - input - visibleOutput` is the thinking output the caller was still
|
|
135
144
|
// billed for. Providers that already fold reasoning into `output_tokens`
|
|
136
145
|
// (Anthropic, OpenAI) report `total == input + output`, so this adds 0.
|
|
137
|
-
// Reasoning is charged at the output rate, so
|
|
138
|
-
// the token counters and the cost.
|
|
146
|
+
// Reasoning is charged at the output rate, so it is counted as output.
|
|
139
147
|
const reasoningTokens = totalTokens > 0
|
|
140
148
|
? Math.max(0, totalTokens - inputTokens - visibleOutputTokens)
|
|
141
149
|
: 0;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const outputRate = getModelRate(modelName, config, "output-token");
|
|
145
|
-
const addedCost = (inputTokens / 1_000_000) * inputRate +
|
|
146
|
-
(outputTokens / 1_000_000) * outputRate;
|
|
147
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
148
|
-
tracker.cost += addedCost;
|
|
149
|
-
const inputKey = `${modelName}_inputTokens`;
|
|
150
|
-
const outputKey = `${modelName}_outputTokens`;
|
|
151
|
-
tracker.tokens[inputKey] = (tracker.tokens[inputKey] || 0) + inputTokens;
|
|
152
|
-
tracker.tokens[outputKey] = (tracker.tokens[outputKey] || 0) + outputTokens;
|
|
150
|
+
countUsage(tracker, modelName, "inputTokens", inputTokens);
|
|
151
|
+
countUsage(tracker, modelName, "outputTokens", visibleOutputTokens + reasoningTokens);
|
|
153
152
|
};
|
|
154
153
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* `
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* `<model>-image-input-token-costs`, and `<model>-image-output-token-costs`.
|
|
162
|
-
* No-ops when the tracker or response usage info is absent.
|
|
154
|
+
* Counts a single image-generation call's usage. Extracts token counts from
|
|
155
|
+
* provider-specific response shapes: OpenAI returns `response.usage` with
|
|
156
|
+
* `input_tokens_details.{text_tokens,image_tokens}` and `output_tokens`;
|
|
157
|
+
* Google returns `response.usageMetadata` with `promptTokensDetails`
|
|
158
|
+
* (modality-keyed) and `candidatesTokenCount` for the generated image.
|
|
159
|
+
* No-ops when the response carries no usage.
|
|
163
160
|
*/
|
|
164
|
-
const updateImageUsageTracker = (tracker, modelName, response,
|
|
165
|
-
if (!
|
|
161
|
+
const updateImageUsageTracker = (tracker, modelName, response, _config, provider) => {
|
|
162
|
+
if (!modelName || !response)
|
|
166
163
|
return;
|
|
167
|
-
if (typeof tracker.cost !== "number")
|
|
168
|
-
tracker.cost = 0;
|
|
169
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
170
|
-
tracker.tokens = {};
|
|
171
164
|
let textInputTokens = 0;
|
|
172
165
|
let imageInputTokens = 0;
|
|
173
166
|
let imageOutputTokens = 0;
|
|
@@ -204,85 +197,42 @@ const updateImageUsageTracker = (tracker, modelName, response, config, provider)
|
|
|
204
197
|
}
|
|
205
198
|
imageOutputTokens = Number(um.candidatesTokenCount) || 0;
|
|
206
199
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const imageInputRate = getModelRate(modelName, config, "image-input-token") || textInputRate;
|
|
214
|
-
const imageOutputRate = getModelRate(modelName, config, "image-output-token");
|
|
215
|
-
const addedCost = (textInputTokens / 1_000_000) * textInputRate +
|
|
216
|
-
(imageInputTokens / 1_000_000) * imageInputRate +
|
|
217
|
-
(imageOutputTokens / 1_000_000) * imageOutputRate;
|
|
218
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
219
|
-
tracker.cost += addedCost;
|
|
220
|
-
if (textInputTokens > 0) {
|
|
221
|
-
const key = `${modelName}_inputTokens`;
|
|
222
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + textInputTokens;
|
|
223
|
-
}
|
|
224
|
-
if (imageInputTokens > 0) {
|
|
225
|
-
const key = `${modelName}_imageInputTokens`;
|
|
226
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageInputTokens;
|
|
227
|
-
}
|
|
228
|
-
if (imageOutputTokens > 0) {
|
|
229
|
-
const key = `${modelName}_imageOutputTokens`;
|
|
230
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + imageOutputTokens;
|
|
231
|
-
}
|
|
200
|
+
prepareTracker(tracker);
|
|
201
|
+
countUsage(tracker, modelName, "inputTokens", textInputTokens);
|
|
202
|
+
// The platform prices an image a model is given as its other input, unless
|
|
203
|
+
// the model has a rate of its own for it.
|
|
204
|
+
countUsage(tracker, modelName, "imageInputTokens", imageInputTokens);
|
|
205
|
+
countUsage(tracker, modelName, "imageOutputTokens", imageOutputTokens);
|
|
232
206
|
};
|
|
233
207
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* `<model>-character-costs` (USD per million characters) and accumulates
|
|
238
|
-
* under `<model>_characters`. No-ops on missing tracker / model / count.
|
|
208
|
+
* Counts character-billed usage (e.g. ElevenLabs TTS). There is no provider
|
|
209
|
+
* response to parse — billing is deterministic from the input character
|
|
210
|
+
* count. No-ops on a missing model or count.
|
|
239
211
|
*/
|
|
240
|
-
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount,
|
|
241
|
-
if (!
|
|
212
|
+
const updateUsageTrackerForCharacters = (tracker, modelName, characterCount, _config) => {
|
|
213
|
+
if (!modelName || !characterCount)
|
|
242
214
|
return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
246
|
-
tracker.tokens = {};
|
|
247
|
-
const rate = getModelRate(modelName, config, "character");
|
|
248
|
-
const addedCost = (characterCount / 1_000_000) * rate;
|
|
249
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
250
|
-
tracker.cost += addedCost;
|
|
251
|
-
const key = `${modelName}_characters`;
|
|
252
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + characterCount;
|
|
215
|
+
prepareTracker(tracker);
|
|
216
|
+
countUsage(tracker, modelName, "characters", characterCount);
|
|
253
217
|
};
|
|
254
218
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* Video is the first output here that providers bill by DURATION rather than by
|
|
258
|
-
* tokens or characters, so the rate constant `<model>-second-costs` is read as
|
|
259
|
-
* USD PER SECOND — not per million, as the `input-token` and `character` rates
|
|
260
|
-
* are. A Veo or Sora second costs cents, and a per-million reading would bill
|
|
261
|
-
* it as effectively free.
|
|
219
|
+
* Counts second-billed usage (video generation): providers bill video by
|
|
220
|
+
* DURATION rather than by tokens or characters.
|
|
262
221
|
*
|
|
263
|
-
* The rate is per model, not per model-and-resolution, so a caller
|
|
264
|
-
* tenant change resolution on a model whose price varies by it
|
|
265
|
-
* $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
|
|
266
|
-
* describes, or
|
|
222
|
+
* The platform's rate is per model, not per model-and-resolution, so a caller
|
|
223
|
+
* that lets a tenant change resolution on a model whose price varies by it
|
|
224
|
+
* (sora-2-pro is $0.30/$0.50/$0.70 at 720p/1024p/1080p) must pin the
|
|
225
|
+
* resolution the rate describes, or the wrong number is charged.
|
|
267
226
|
*
|
|
268
|
-
* @param tracker - The accumulator
|
|
269
|
-
* @param modelName - The video model id
|
|
227
|
+
* @param tracker - The caller's accumulator, when it passed one
|
|
228
|
+
* @param modelName - The video model id
|
|
270
229
|
* @param seconds - Billed duration of the generated clip
|
|
271
|
-
* @param config - Integration config carrying `<model>-second-costs`
|
|
272
230
|
*/
|
|
273
|
-
const updateVideoUsageTracker = (tracker, modelName, seconds,
|
|
274
|
-
if (!
|
|
231
|
+
const updateVideoUsageTracker = (tracker, modelName, seconds, _config) => {
|
|
232
|
+
if (!modelName || !seconds)
|
|
275
233
|
return;
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (!tracker.tokens || typeof tracker.tokens !== "object")
|
|
279
|
-
tracker.tokens = {};
|
|
280
|
-
const rate = getModelRate(modelName, config, "second");
|
|
281
|
-
const addedCost = seconds * rate;
|
|
282
|
-
if (Number.isFinite(addedCost) && addedCost > 0)
|
|
283
|
-
tracker.cost += addedCost;
|
|
284
|
-
const key = `${modelName}_seconds`;
|
|
285
|
-
tracker.tokens[key] = (tracker.tokens[key] || 0) + seconds;
|
|
234
|
+
prepareTracker(tracker);
|
|
235
|
+
countUsage(tracker, modelName, "seconds", seconds);
|
|
286
236
|
};
|
|
287
237
|
/**
|
|
288
238
|
* Extracts a normalized token-usage object from a single LangChain `invoke()`
|
|
@@ -543,36 +493,6 @@ const extractJSONFromResponse = (text) => {
|
|
|
543
493
|
}
|
|
544
494
|
return null;
|
|
545
495
|
};
|
|
546
|
-
/**
|
|
547
|
-
* Maps a model name prefix to its OpenAI-compatible provider configuration. DeepSeek,
|
|
548
|
-
* Kimi (Moonshot), and GLM (Zhipu) all expose OpenAI-compatible chat completion APIs
|
|
549
|
-
* and can be routed through `ChatOpenAI` with a custom base URL and API key.
|
|
550
|
-
* @param modelName - The model identifier to inspect
|
|
551
|
-
* @param config - Configuration object containing provider-specific API keys
|
|
552
|
-
* @returns An object with `apiKey` and `baseURL` when the model matches a known
|
|
553
|
-
* OpenAI-compatible provider, or `null` otherwise
|
|
554
|
-
*/
|
|
555
|
-
const getOpenAICompatibleProvider = (modelName, config) => {
|
|
556
|
-
if (modelName.startsWith("deepseek-")) {
|
|
557
|
-
return {
|
|
558
|
-
apiKey: config.deepSeekAPIKey,
|
|
559
|
-
baseURL: "https://api.deepseek.com/v1",
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
if (modelName.startsWith("kimi-") || modelName.startsWith("moonshot-")) {
|
|
563
|
-
return {
|
|
564
|
-
apiKey: config.kimiAPIKey,
|
|
565
|
-
baseURL: "https://api.moonshot.ai/v1",
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
if (modelName.startsWith("glm-")) {
|
|
569
|
-
return {
|
|
570
|
-
apiKey: config.glmAPIKey,
|
|
571
|
-
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
return null;
|
|
575
|
-
};
|
|
576
496
|
/**
|
|
577
497
|
* Whether a model accepts the `temperature` sampling parameter at all. Some
|
|
578
498
|
* models reject it with an HTTP 400, so it must be omitted rather than clamped:
|
|
@@ -582,7 +502,7 @@ const getOpenAICompatibleProvider = (modelName, config) => {
|
|
|
582
502
|
* accept it.
|
|
583
503
|
* - OpenAI reasoning models (the GPT-5 family and the o-series) reject it;
|
|
584
504
|
* gpt-4o still accepts it.
|
|
585
|
-
*
|
|
505
|
+
* Gemini accepts temperature.
|
|
586
506
|
* @param modelName - the model identifier being routed
|
|
587
507
|
* @returns `true` when the model accepts a `temperature` parameter
|
|
588
508
|
*/
|
|
@@ -603,18 +523,14 @@ const modelSupportsTemperature = (modelName) => {
|
|
|
603
523
|
};
|
|
604
524
|
/**
|
|
605
525
|
* Per-provider valid `temperature` range, used to clamp caller-supplied values.
|
|
606
|
-
* Anthropic
|
|
607
|
-
*
|
|
608
|
-
* at all (see `modelSupportsTemperature`).
|
|
526
|
+
* Anthropic caps at 1.0; OpenAI and Gemini accept up to 2.0. Only consulted for
|
|
527
|
+
* models that support temperature at all (see `modelSupportsTemperature`).
|
|
609
528
|
*/
|
|
610
529
|
const getTemperatureRange = (modelName) => {
|
|
611
|
-
if (modelName.startsWith("claude-")
|
|
612
|
-
modelName.startsWith("kimi-") ||
|
|
613
|
-
modelName.startsWith("moonshot-") ||
|
|
614
|
-
modelName.startsWith("glm-")) {
|
|
530
|
+
if (modelName.startsWith("claude-")) {
|
|
615
531
|
return { min: 0, max: 1 };
|
|
616
532
|
}
|
|
617
|
-
// gpt-, gemini
|
|
533
|
+
// gpt-, gemini-
|
|
618
534
|
return { min: 0, max: 2 };
|
|
619
535
|
};
|
|
620
536
|
/**
|
|
@@ -643,46 +559,6 @@ const resolveTemperatureSetting = (modelName, config) => {
|
|
|
643
559
|
}
|
|
644
560
|
return { temperature: clamped };
|
|
645
561
|
};
|
|
646
|
-
/**
|
|
647
|
-
* Gemini's ceiling on `maxOutputTokens`. The API validates the field and
|
|
648
|
-
* rejects the request outright rather than clamping it:
|
|
649
|
-
* "Unable to submit request because it has a maxOutputTokens value of N but the
|
|
650
|
-
* supported range is from 1 (inclusive) to 65537 (exclusive)" (HTTP 400,
|
|
651
|
-
* INVALID_ARGUMENT).
|
|
652
|
-
*
|
|
653
|
-
* Set to 64000 rather than that 65536 validation ceiling: the Pro models report
|
|
654
|
-
* a generation capacity of 64000, so 64000 is the value every model across the
|
|
655
|
-
* Gemini 3 line (3.1 Pro, 3.5/3.7/3.8 Flash) can both accept and actually
|
|
656
|
-
* produce. One safe constant beats a per-model table that would need editing
|
|
657
|
-
* every time Google ships a model — and the 1536 tokens of headroom given up on
|
|
658
|
-
* Flash are far beyond what any caller asks for.
|
|
659
|
-
*
|
|
660
|
-
* Deliberately Gemini-only. Anthropic and OpenAI take the caller's `maxTokens`
|
|
661
|
-
* unchanged, since neither rejects the values configured today.
|
|
662
|
-
*/
|
|
663
|
-
const GEMINI_MAX_OUTPUT_TOKENS = 64000;
|
|
664
|
-
/**
|
|
665
|
-
* Resolves `maxOutputTokens` for a Gemini model. Callers share one `maxTokens`
|
|
666
|
-
* setting across every provider, and the other providers tolerate values far
|
|
667
|
-
* above what Gemini accepts, so anything above Gemini's ceiling is clamped here
|
|
668
|
-
* (with a warning) instead of being sent through to a guaranteed HTTP 400. With
|
|
669
|
-
* no caller value the ceiling itself is used, which is what the previous
|
|
670
|
-
* 200000 default was reaching for.
|
|
671
|
-
* @param modelName - The Gemini model identifier being routed
|
|
672
|
-
* @param config - Configuration object; only `maxTokens` is consulted
|
|
673
|
-
* @returns A token count within Gemini's supported range
|
|
674
|
-
*/
|
|
675
|
-
const resolveGeminiMaxOutputTokens = (modelName, config) => {
|
|
676
|
-
const maxTokens = config?.maxTokens;
|
|
677
|
-
if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens)) {
|
|
678
|
-
return GEMINI_MAX_OUTPUT_TOKENS;
|
|
679
|
-
}
|
|
680
|
-
if (maxTokens > GEMINI_MAX_OUTPUT_TOKENS) {
|
|
681
|
-
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `maxTokens ${maxTokens} exceeds the ${GEMINI_MAX_OUTPUT_TOKENS}-token output ceiling of "${modelName}"; clamping to ${GEMINI_MAX_OUTPUT_TOKENS}.`);
|
|
682
|
-
return GEMINI_MAX_OUTPUT_TOKENS;
|
|
683
|
-
}
|
|
684
|
-
return maxTokens;
|
|
685
|
-
};
|
|
686
562
|
/**
|
|
687
563
|
* Flattens LangChain message content into plain text. Content is a string for
|
|
688
564
|
* ordinary completions, but providers switch to an array of blocks whenever the
|
|
@@ -710,23 +586,22 @@ const extractTextContent = (content) => {
|
|
|
710
586
|
/**
|
|
711
587
|
* Instantiates and returns the appropriate LangChain chat model based on the model
|
|
712
588
|
* name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
|
|
713
|
-
* `ChatGoogleGenerativeAI`, and `gpt-` maps to `ChatOpenAI`.
|
|
714
|
-
* Kimi/Moonshot (`kimi-`, `moonshot-`), and GLM/Zhipu (`glm-`) models are routed
|
|
715
|
-
* through `ChatOpenAI` against each provider's OpenAI-compatible endpoint. When a Zod
|
|
589
|
+
* `ChatGoogleGenerativeAI`, and `gpt-` maps to `ChatOpenAI`. When a Zod
|
|
716
590
|
* `schema` is provided, native structured output is configured per provider: OpenAI
|
|
717
591
|
* via `response_format` with `json_schema`, Anthropic via `output_config.format`, and
|
|
718
592
|
* Gemini via JSON mode (`json: true`) plus `responseSchema`. In every case the model
|
|
719
593
|
* emits JSON as the message text, so the caller's parse/validate pipeline is unchanged.
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
594
|
+
* Throws a `BAD_REQUEST` error for unrecognised model names.
|
|
595
|
+
*
|
|
596
|
+
* Output token limits are not caller-configurable. Anthropic requires
|
|
597
|
+
* `max_tokens` on every request and @langchain/anthropic defaults it to 4096,
|
|
598
|
+
* so the Claude branch pins it explicitly; OpenAI and Gemini are sent no limit
|
|
599
|
+
* and fall back to each provider's own default, which is that model's maximum.
|
|
723
600
|
* @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
|
|
724
|
-
* `"gemini-1.5-pro"
|
|
601
|
+
* `"gemini-1.5-pro"`
|
|
725
602
|
* @param config - Configuration object containing API keys (`openAIAPIKey`,
|
|
726
|
-
* `anthropicAPIKey`, `googleAPIKey
|
|
727
|
-
* optional `maxTokens`, and optional `temperature`
|
|
603
|
+
* `anthropicAPIKey`, `googleAPIKey`) and optional `temperature`
|
|
728
604
|
* @param schema - Optional Zod schema used to configure native structured JSON output
|
|
729
|
-
* for GPT / Claude / Gemini models; ignored for OpenAI-compatible providers
|
|
730
605
|
* @returns A configured LangChain chat model (or bound runnable) instance
|
|
731
606
|
*/
|
|
732
607
|
// Exported (but not re-exported from index.ts / the package's public entry
|
|
@@ -737,12 +612,17 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
737
612
|
const modelSettings = resolveTemperatureSetting(modelName, config);
|
|
738
613
|
// Claude models (Anthropic)
|
|
739
614
|
if (modelName.startsWith("claude-")) {
|
|
615
|
+
// Anthropic requires `max_tokens` on every request and @langchain/anthropic
|
|
616
|
+
// defaults it to 4096 when omitted — low enough to truncate agentic turns
|
|
617
|
+
// mid-tool-call — so it is pinned here rather than left to the library.
|
|
618
|
+
// 64000 is Anthropic's recommended value for streamed requests and sits
|
|
619
|
+
// within the 128000-token output limit of every Claude model routed here.
|
|
620
|
+
//
|
|
740
621
|
// Anthropic's SDK rejects non-streamed requests when max_tokens is large
|
|
741
|
-
// enough that the operation could exceed 10 minutes (threshold: 21333)
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
const maxTokens = config.maxTokens || 16384;
|
|
622
|
+
// enough that the operation could exceed 10 minutes (threshold: 21333), so
|
|
623
|
+
// streaming is always on: `.invoke()` routes through the streaming endpoint
|
|
624
|
+
// internally and callers see no interface change.
|
|
625
|
+
const maxTokens = 64000;
|
|
746
626
|
// Native structured output: Anthropic's `output_config.format` constrains
|
|
747
627
|
// decoding to the schema while still emitting JSON as the message text, so
|
|
748
628
|
// the downstream streaming + parse/validate pipeline is unchanged.
|
|
@@ -763,7 +643,7 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
763
643
|
apiKey: config.anthropicAPIKey,
|
|
764
644
|
maxTokens,
|
|
765
645
|
modelName: modelName,
|
|
766
|
-
streaming:
|
|
646
|
+
streaming: true,
|
|
767
647
|
invocationKwargs: { cache_control: { type: "ephemeral" } },
|
|
768
648
|
...outputConfig,
|
|
769
649
|
...modelSettings,
|
|
@@ -774,7 +654,9 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
774
654
|
else if (modelName.startsWith("gemini-")) {
|
|
775
655
|
const model = new google_genai_1.ChatGoogleGenerativeAI({
|
|
776
656
|
apiKey: config.googleAPIKey,
|
|
777
|
-
maxOutputTokens
|
|
657
|
+
// No `maxOutputTokens`: omitting it leaves Gemini's own default, which is
|
|
658
|
+
// the model's maximum output. Sending a value meant tracking a per-model
|
|
659
|
+
// ceiling that the API rejects outright rather than clamps.
|
|
778
660
|
model: modelName,
|
|
779
661
|
// `json: true` forces responseMimeType=application/json, guaranteeing the
|
|
780
662
|
// model returns valid JSON text (never prose) for the parse pipeline.
|
|
@@ -796,17 +678,26 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
796
678
|
else if (modelName.startsWith("gpt-")) {
|
|
797
679
|
const openAISettings = {
|
|
798
680
|
apiKey: config.openAIAPIKey,
|
|
799
|
-
|
|
681
|
+
// No output limit is sent: OpenAI defaults to the model's maximum. The
|
|
682
|
+
// snake_case `max_tokens` that used to sit here was never a recognised
|
|
683
|
+
// ChatOpenAI field and was silently dropped by LangChain.
|
|
800
684
|
modelName: modelName,
|
|
801
685
|
...modelSettings,
|
|
802
686
|
};
|
|
803
|
-
// gpt-5.6's /v1/chat/completions endpoint 400s whenever function
|
|
804
|
-
// bound, because it rejects the model's own server-side default
|
|
805
|
-
// effort in that combination ("use /v1/responses or set
|
|
806
|
-
// to 'none'"); forcing it to "none" here avoids the
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
|
|
687
|
+
// The gpt-5.6 family's /v1/chat/completions endpoint 400s whenever function
|
|
688
|
+
// tools are bound, because it rejects the model's own server-side default
|
|
689
|
+
// reasoning effort in that combination ("use /v1/responses or set
|
|
690
|
+
// reasoning_effort to 'none'"); forcing it to "none" here avoids the
|
|
691
|
+
// collision. The restriction belongs to the family, not to one id: the
|
|
692
|
+
// `gpt-5.6` alias resolves to `gpt-5.6-sol`, and `gpt-5.6-terra` /
|
|
693
|
+
// `gpt-5.6-luna` are separately addressable ids that hit the same 400, as
|
|
694
|
+
// do dated snapshots (`gpt-5.6-2026-08-22`). Matching the alias alone left
|
|
695
|
+
// every one of those unpatched, so match the whole family. Still excludes
|
|
696
|
+
// sibling releases (gpt-5.5, gpt-5.7): they are not confirmed to share the
|
|
697
|
+
// bug, and reasoning effort is worth keeping wherever it is accepted.
|
|
698
|
+
const reasoningEffortOverride = /^gpt-5\.6($|-)/.test(modelName)
|
|
699
|
+
? { reasoning_effort: "none" }
|
|
700
|
+
: {};
|
|
810
701
|
// Use native structured output with a JSON schema. `modelKwargs` is spread
|
|
811
702
|
// verbatim into the Chat Completions request. Merge (not overwrite) with
|
|
812
703
|
// the reasoning-effort override above, since both may apply at once.
|
|
@@ -829,17 +720,6 @@ const getLLMModel = (modelName, config, schema = null) => {
|
|
|
829
720
|
}
|
|
830
721
|
return new openai_1.ChatOpenAI(openAISettings);
|
|
831
722
|
}
|
|
832
|
-
// OpenAI-compatible providers: DeepSeek, Kimi (Moonshot), GLM (Zhipu)
|
|
833
|
-
const openAICompatible = getOpenAICompatibleProvider(modelName, config);
|
|
834
|
-
if (openAICompatible) {
|
|
835
|
-
return new openai_1.ChatOpenAI({
|
|
836
|
-
apiKey: openAICompatible.apiKey,
|
|
837
|
-
maxTokens: config.maxTokens || 200000,
|
|
838
|
-
modelName: modelName,
|
|
839
|
-
configuration: { baseURL: openAICompatible.baseURL },
|
|
840
|
-
...modelSettings,
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
723
|
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.BAD_REQUEST, const_js_1.default.ERROR.UNSUPPORTED_MODEL + ": " + modelName);
|
|
844
724
|
};
|
|
845
725
|
exports.getLLMModel = getLLMModel;
|
|
@@ -1169,6 +1049,65 @@ const parseAndValidateJSONResponse = (rawContent, schema) => {
|
|
|
1169
1049
|
* original base) so that across multiple retries the full failure history
|
|
1170
1050
|
* accumulates and the model can avoid oscillating between previous broken outputs.
|
|
1171
1051
|
*/
|
|
1052
|
+
/**
|
|
1053
|
+
* Whether a response that fails schema validation is retried with the errors
|
|
1054
|
+
* appended (buildValidationRetryMessages), up to MAX_VALIDATION_RETRIES.
|
|
1055
|
+
*
|
|
1056
|
+
* This used to be skipped whenever the model supports native structured
|
|
1057
|
+
* output, on the reasoning that a natively constrained response cannot be
|
|
1058
|
+
* invalid, so a failure had to be a schema bug worth surfacing at once. It
|
|
1059
|
+
* can be invalid, and routinely is: constrained decoding enforces the JSON
|
|
1060
|
+
* *shape*, not the Zod schema. Anthropic's SDK strips `enum` into a
|
|
1061
|
+
* description before sending, Gemini ignores `additionalProperties`, and no
|
|
1062
|
+
* provider runs a refine or a superRefine. Production, 2026-09-23/24: one
|
|
1063
|
+
* agent failed one call in six on extra keys the strict schema refused, and
|
|
1064
|
+
* another was rejected three runs in a row at its last stage on a value off
|
|
1065
|
+
* an enumerated list — every one on a native-schema model, every one reported
|
|
1066
|
+
* straight to the user, and every one restarted from zero by the caller
|
|
1067
|
+
* because the stage that failed was never asked again.
|
|
1068
|
+
*
|
|
1069
|
+
* So the retry now applies whenever a schema is in play. A genuine schema bug
|
|
1070
|
+
* still surfaces: it fails the same way three times and the last error is
|
|
1071
|
+
* thrown as before, at the cost of two extra calls.
|
|
1072
|
+
* @param expectsJsonResponse - Whether the call expects JSON at all
|
|
1073
|
+
* @param schema - The Zod schema the response is validated against, if any
|
|
1074
|
+
* @returns Whether a validation failure is retried
|
|
1075
|
+
*/
|
|
1076
|
+
const canRetryValidation = (expectsJsonResponse, schema) => !!expectsJsonResponse && !!schema;
|
|
1077
|
+
/**
|
|
1078
|
+
* Whether a failure of the JSON stage is one the model can fix if asked again:
|
|
1079
|
+
* a response that failed schema validation, or one that was not JSON at all
|
|
1080
|
+
* (prose, or JSON with a preamble the extractor could not recover). Everything
|
|
1081
|
+
* else — no messages, a provider error, a transport error — is not.
|
|
1082
|
+
* @param error - The failure
|
|
1083
|
+
* @returns Whether a retry with the errors appended makes sense
|
|
1084
|
+
*/
|
|
1085
|
+
const isRetryableJsonFailure = (error) => error?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY ||
|
|
1086
|
+
/non-JSON response when JSON was expected/.test(String(error?.message ?? ""));
|
|
1087
|
+
/**
|
|
1088
|
+
* The agentic run takes one user prompt rather than a message list, so a retry
|
|
1089
|
+
* carries the failed output and the errors inside that prompt: the model sees
|
|
1090
|
+
* what it produced and what was wrong with it, the same information
|
|
1091
|
+
* buildValidationRetryMessages gives the non-agentic paths. Previous attempts
|
|
1092
|
+
* accumulate so the model does not oscillate between earlier broken outputs.
|
|
1093
|
+
* @param userPrompt - The prompt of the attempt that failed
|
|
1094
|
+
* @param rawContent - What the model answered
|
|
1095
|
+
* @param error - Why it was refused
|
|
1096
|
+
* @returns The user prompt for the next attempt
|
|
1097
|
+
*/
|
|
1098
|
+
const buildAgenticRetryPrompt = (userPrompt, rawContent, error) => {
|
|
1099
|
+
const previous = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1100
|
+
return `${userPrompt}
|
|
1101
|
+
|
|
1102
|
+
---
|
|
1103
|
+
Your previous response was rejected:
|
|
1104
|
+
${String(error?.message ?? error)}
|
|
1105
|
+
|
|
1106
|
+
Your previous response was:
|
|
1107
|
+
${previous}
|
|
1108
|
+
|
|
1109
|
+
Return a corrected JSON response that fixes ALL of these issues. Respond with the JSON only.`;
|
|
1110
|
+
};
|
|
1172
1111
|
const buildValidationRetryMessages = (priorMessages, rawContent, validationError) => {
|
|
1173
1112
|
const assistantContent = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent);
|
|
1174
1113
|
return [
|
|
@@ -1180,6 +1119,58 @@ const buildValidationRetryMessages = (priorMessages, rawContent, validationError
|
|
|
1180
1119
|
},
|
|
1181
1120
|
];
|
|
1182
1121
|
};
|
|
1122
|
+
/**
|
|
1123
|
+
* The agentic path's JSON stage: the agent's final text as validated JSON.
|
|
1124
|
+
* Direct parse when it looks like JSON, the markdown/preamble extractor
|
|
1125
|
+
* otherwise; a schema failure throws UNPROCESSABLE_ENTITY (validateWithSchema),
|
|
1126
|
+
* anything the extractor cannot recover throws the "non-JSON response" error.
|
|
1127
|
+
* Both are what runPromptWithModel's agentic retry loop asks the model to fix.
|
|
1128
|
+
* @param rawContent - The agent's final message text
|
|
1129
|
+
* @param schema - The Zod schema to validate against, if any
|
|
1130
|
+
* @returns The JSON, re-serialised
|
|
1131
|
+
*/
|
|
1132
|
+
const parseAgenticJsonResponse = (rawContent, schema) => {
|
|
1133
|
+
if (typeof rawContent === "string") {
|
|
1134
|
+
const trimmed = rawContent.trim();
|
|
1135
|
+
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
1136
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(trimmed);
|
|
1139
|
+
if (schema) {
|
|
1140
|
+
validateWithSchema(parsed, schema);
|
|
1141
|
+
}
|
|
1142
|
+
return JSON.stringify(parsed);
|
|
1143
|
+
}
|
|
1144
|
+
catch (parseError) {
|
|
1145
|
+
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
1146
|
+
throw parseError;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// Try to extract JSON from markdown or other wrapping
|
|
1151
|
+
const extracted = extractJSONFromResponse(rawContent);
|
|
1152
|
+
if (extracted) {
|
|
1153
|
+
if (schema) {
|
|
1154
|
+
validateWithSchema(extracted, schema);
|
|
1155
|
+
}
|
|
1156
|
+
return JSON.stringify(extracted);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
let preview = "";
|
|
1160
|
+
if (typeof rawContent === "string") {
|
|
1161
|
+
preview = rawContent.substring(0, 100);
|
|
1162
|
+
}
|
|
1163
|
+
else if (typeof rawContent === "object" && rawContent !== null) {
|
|
1164
|
+
preview = JSON.stringify(rawContent).substring(0, 100);
|
|
1165
|
+
}
|
|
1166
|
+
else if (rawContent !== undefined && rawContent !== null) {
|
|
1167
|
+
preview = String(rawContent).substring(0, 100);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
preview = "[empty response]";
|
|
1171
|
+
}
|
|
1172
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
|
|
1173
|
+
};
|
|
1183
1174
|
/**
|
|
1184
1175
|
* Sends a prompt to an LLM and returns the response, with support for streaming,
|
|
1185
1176
|
* agentic execution, progress reporting, JSON extraction, and Zod schema validation.
|
|
@@ -1262,65 +1253,43 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1262
1253
|
// being reconciled from the response afterwards.
|
|
1263
1254
|
const agent = createAgent(agentName, modelName, systemPrompt, [...tools], null, // responseFormat
|
|
1264
1255
|
config, usageTracker);
|
|
1265
|
-
//
|
|
1266
|
-
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
return rawContent;
|
|
1279
|
-
}
|
|
1280
|
-
// Process JSON response similar to non-agentic path
|
|
1281
|
-
if (typeof rawContent === "string") {
|
|
1282
|
-
const trimmed = rawContent.trim();
|
|
1283
|
-
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
1284
|
-
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
1285
|
-
try {
|
|
1286
|
-
const parsed = JSON.parse(trimmed);
|
|
1287
|
-
if (schema) {
|
|
1288
|
-
validateWithSchema(parsed, schema);
|
|
1289
|
-
}
|
|
1290
|
-
return JSON.stringify(parsed);
|
|
1291
|
-
}
|
|
1292
|
-
catch (parseError) {
|
|
1293
|
-
if (parseError?.code === const_js_1.default.HTTP_CODES.UNPROCESSABLE_ENTITY) {
|
|
1294
|
-
throw parseError;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
// Try to extract JSON from markdown or other wrapping
|
|
1299
|
-
const extracted = extractJSONFromResponse(rawContent);
|
|
1300
|
-
if (extracted) {
|
|
1301
|
-
if (schema) {
|
|
1302
|
-
validateWithSchema(extracted, schema);
|
|
1303
|
-
}
|
|
1304
|
-
return JSON.stringify(extracted);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
if (expectsJsonResponse) {
|
|
1308
|
-
let preview = "";
|
|
1309
|
-
if (typeof rawContent === "string") {
|
|
1310
|
-
preview = rawContent.substring(0, 100);
|
|
1256
|
+
// The JSON stage is retried like the non-agentic paths (canRetryValidation):
|
|
1257
|
+
// the agent is run again with its rejected output and the errors appended
|
|
1258
|
+
// to the user prompt, up to MAX_VALIDATION_RETRIES. Until now this path
|
|
1259
|
+
// had no retry at all, and it is the one every tool-using generation takes.
|
|
1260
|
+
let activeUserPrompt = userPrompt;
|
|
1261
|
+
let attempt = 0;
|
|
1262
|
+
for (;;) {
|
|
1263
|
+
// Run the agent with progress callback
|
|
1264
|
+
const response = await runAgent(agent, activeUserPrompt, config, onProgressReport || null, usageTracker);
|
|
1265
|
+
// Extract content from agent response
|
|
1266
|
+
const messages = response?.messages || [];
|
|
1267
|
+
if (messages.length === 0) {
|
|
1268
|
+
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, "Agent returned no messages");
|
|
1311
1269
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1270
|
+
const lastMessage = messages[messages.length - 1];
|
|
1271
|
+
// Flattens the array content blocks that Gemini/Claude agent responses and
|
|
1272
|
+
// any server-tool turn (e.g. web search) return.
|
|
1273
|
+
const rawContent = extractTextContent(lastMessage?.content);
|
|
1274
|
+
// If not expecting JSON, return raw content directly
|
|
1275
|
+
if (!expectsJsonResponse) {
|
|
1276
|
+
return rawContent;
|
|
1314
1277
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1278
|
+
try {
|
|
1279
|
+
return parseAgenticJsonResponse(rawContent, schema);
|
|
1317
1280
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1281
|
+
catch (err) {
|
|
1282
|
+
if (canRetryValidation(expectsJsonResponse, schema) &&
|
|
1283
|
+
isRetryableJsonFailure(err) &&
|
|
1284
|
+
attempt < MAX_VALIDATION_RETRIES) {
|
|
1285
|
+
attempt++;
|
|
1286
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Schema validation failed for "${modelName}", retrying (${attempt}/${MAX_VALIDATION_RETRIES}): ${err.message}`);
|
|
1287
|
+
activeUserPrompt = buildAgenticRetryPrompt(activeUserPrompt, rawContent, err);
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
throw err;
|
|
1320
1291
|
}
|
|
1321
|
-
throw errorHandling_js_1.default.create(const_js_1.default.HTTP_CODES.INTERNAL_SERVER_ERROR, `Agent returned non-JSON response when JSON was expected: ${preview}...`);
|
|
1322
1292
|
}
|
|
1323
|
-
return rawContent;
|
|
1324
1293
|
}
|
|
1325
1294
|
let messages;
|
|
1326
1295
|
if (typeof prompt === "string") {
|
|
@@ -1365,13 +1334,13 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1365
1334
|
else {
|
|
1366
1335
|
messagesToSend = messages;
|
|
1367
1336
|
}
|
|
1368
|
-
// Retry
|
|
1337
|
+
// Retry on schema validation failure. The retry is silent to the progress
|
|
1369
1338
|
// callback — same "Generating content..." message and the progress curve
|
|
1370
1339
|
// continues from the original startTime so the bar doesn't visibly reset.
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1340
|
+
// Each attempt accumulates independently into usageTracker so retry usage
|
|
1341
|
+
// is still visible. Native structured output no longer exempts a model:
|
|
1342
|
+
// see canRetryValidation.
|
|
1343
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1375
1344
|
const progressReportInterval = 10; // Report every N chunks
|
|
1376
1345
|
const overallStartTime = Date.now();
|
|
1377
1346
|
// Use a time-based asymptotic curve: progress approaches maxPercent but never
|
|
@@ -1523,9 +1492,9 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
|
|
|
1523
1492
|
else {
|
|
1524
1493
|
messagesToSend = messages;
|
|
1525
1494
|
}
|
|
1526
|
-
// Retry
|
|
1495
|
+
// Retry on schema validation failure (see streaming branch for rationale).
|
|
1527
1496
|
// Each attempt accumulates independently into usageTracker.
|
|
1528
|
-
const canRetryOnValidation = expectsJsonResponse
|
|
1497
|
+
const canRetryOnValidation = canRetryValidation(expectsJsonResponse, schema);
|
|
1529
1498
|
let activeMessages = messagesToSend;
|
|
1530
1499
|
let attempt = 0;
|
|
1531
1500
|
while (true) {
|
|
@@ -2034,6 +2003,11 @@ const runPromptWithModelForVideoGeneration = async (modelName, config, prompt, o
|
|
|
2034
2003
|
}
|
|
2035
2004
|
};
|
|
2036
2005
|
exports.default = {
|
|
2006
|
+
canRetryValidation,
|
|
2007
|
+
isRetryableJsonFailure,
|
|
2008
|
+
buildAgenticRetryPrompt,
|
|
2009
|
+
parseAgenticJsonResponse,
|
|
2010
|
+
supportsNativeSchema,
|
|
2037
2011
|
checkIfAIProviderConfigured,
|
|
2038
2012
|
createAgent,
|
|
2039
2013
|
runAgent,
|
|
@@ -2041,6 +2015,9 @@ exports.default = {
|
|
|
2041
2015
|
runPromptWithModelForImageGeneration,
|
|
2042
2016
|
runPromptWithModelForVideoGeneration,
|
|
2043
2017
|
throwErrorIfNotSuccessful,
|
|
2018
|
+
updateImageUsageTracker,
|
|
2019
|
+
updateUsageTracker,
|
|
2044
2020
|
updateUsageTrackerForCharacters,
|
|
2021
|
+
updateVideoUsageTracker,
|
|
2045
2022
|
validateModel,
|
|
2046
2023
|
};
|