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