@genesislcap/foundation-ai 15.14.2 → 15.15.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/dist/dts/index.d.ts +1 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/transports/anthropic-transport.d.ts +9 -0
- package/dist/dts/transports/anthropic-transport.d.ts.map +1 -1
- package/dist/dts/transports/gemini-transport.d.ts +11 -7
- package/dist/dts/transports/gemini-transport.d.ts.map +1 -1
- package/dist/dts/types/chat.types.d.ts +103 -5
- package/dist/dts/types/chat.types.d.ts.map +1 -1
- package/dist/dts/utils/image-mime.d.ts +34 -0
- package/dist/dts/utils/image-mime.d.ts.map +1 -0
- package/dist/esm/transports/anthropic-transport.js +178 -32
- package/dist/esm/transports/gemini-transport.js +178 -35
- package/dist/esm/utils/image-mime.js +37 -0
- package/dist/foundation-ai.api.json +405 -90
- package/dist/foundation-ai.d.ts +129 -15
- package/package.json +11 -11
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
2
|
import { SUPPORTED_ANTHROPIC_MODEL_IDS, } from '../types';
|
|
3
|
+
import { normalizeImageMime, stripImageDataUrl } from '../utils/image-mime';
|
|
3
4
|
import { logger } from '../utils/logger';
|
|
4
5
|
import { scaleTemperature } from '../utils/temperature';
|
|
5
6
|
import { anthropicTokenCost } from '../utils/token-cost';
|
|
@@ -136,9 +137,58 @@ function anthropicThinking(model, policy) {
|
|
|
136
137
|
if (policy === 'auto') {
|
|
137
138
|
return supportsAdaptiveThinking(model) ? adaptive : undefined;
|
|
138
139
|
}
|
|
140
|
+
// A graded level asks for reasoning DEPTH, which on Anthropic is `output_config.effort`
|
|
141
|
+
// riding alongside adaptive thinking (see `anthropicEffort`). Asking for a depth on a model
|
|
142
|
+
// with no adaptive support leaves it at its default rather than 400ing the turn.
|
|
143
|
+
if (policy !== undefined) {
|
|
144
|
+
return supportsAdaptiveThinking(model) ? adaptive : undefined;
|
|
145
|
+
}
|
|
139
146
|
// policy === undefined → the per-model default, unchanged.
|
|
140
147
|
return defaultsToThinking ? adaptive : undefined;
|
|
141
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether the model accepts `output_config.effort`.
|
|
151
|
+
*
|
|
152
|
+
* An allow-list, and deliberately the same generation {@link supportsNativeStructuredOutput}
|
|
153
|
+
* treats as new, minus Haiku: `effort` and `format` share the one `output_config` container, so
|
|
154
|
+
* the two gates must agree on which models understand it. `output_config` arrived with the
|
|
155
|
+
* Fable 5 / Opus 4.8 / Sonnet 5 generation — Opus 4.7 and Sonnet 4.6 predate it and 400 on a
|
|
156
|
+
* field inside it, so a graded policy is dropped for them (they run their default posture, with a
|
|
157
|
+
* one-time warning from {@link AnthropicTransport}) rather than forwarded into a failed turn.
|
|
158
|
+
*
|
|
159
|
+
* Haiku 4.5 is the lone asymmetry with `supportsNativeStructuredOutput`, which lists it: Haiku
|
|
160
|
+
* took `output_config.format` but not `.effort`, and has no adaptive thinking for effort to ride
|
|
161
|
+
* on anyway (`anthropicEffort` also gates on {@link supportsAdaptiveThinking}). An allow-list is
|
|
162
|
+
* the safe default for a "send-it-and-400" parameter — a newly added model gets no `effort` until
|
|
163
|
+
* it is confirmed to accept it.
|
|
164
|
+
*/
|
|
165
|
+
function supportsEffort(model) {
|
|
166
|
+
return model === 'claude-fable-5' || model === 'claude-opus-4-8' || model === 'claude-sonnet-5';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* `output_config.effort` for a graded policy.
|
|
170
|
+
*
|
|
171
|
+
* An exhaustive `Record`, not a condition chain: adding a member to {@link ChatThinkingLevel}
|
|
172
|
+
* then fails to compile here instead of silently falling through to the model default, which is
|
|
173
|
+
* the failure mode this mapping is most likely to acquire.
|
|
174
|
+
*
|
|
175
|
+
* `'minimal'` clamps UP to `low` — Anthropic's ladder starts there. `xhigh` is deliberately
|
|
176
|
+
* unreachable; see {@link ChatThinkingPolicy}.
|
|
177
|
+
*/
|
|
178
|
+
const ANTHROPIC_EFFORT = {
|
|
179
|
+
minimal: 'low',
|
|
180
|
+
low: 'low',
|
|
181
|
+
medium: 'medium',
|
|
182
|
+
high: 'high',
|
|
183
|
+
max: 'max',
|
|
184
|
+
};
|
|
185
|
+
function anthropicEffort(model, policy) {
|
|
186
|
+
if (policy === undefined || policy === 'auto' || policy === 'off')
|
|
187
|
+
return undefined;
|
|
188
|
+
if (!supportsEffort(model) || !supportsAdaptiveThinking(model))
|
|
189
|
+
return undefined;
|
|
190
|
+
return ANTHROPIC_EFFORT[policy];
|
|
191
|
+
}
|
|
142
192
|
/**
|
|
143
193
|
* Key under which this transport stashes its round-trip state on a tool call's
|
|
144
194
|
* provider-neutral `providerMetadata` bag. Private to AnthropicTransport.
|
|
@@ -213,14 +263,48 @@ export class AnthropicTransport {
|
|
|
213
263
|
warnIfThinkingUnclampable(policy) {
|
|
214
264
|
if (this.warnedThinkingClamped)
|
|
215
265
|
return;
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
if (!clamped)
|
|
266
|
+
const message = this.clampWarning(policy);
|
|
267
|
+
if (!message)
|
|
219
268
|
return;
|
|
220
269
|
this.warnedThinkingClamped = true;
|
|
221
|
-
logger.warn(
|
|
222
|
-
|
|
223
|
-
|
|
270
|
+
logger.warn(message);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The warning for a policy this model cannot honour, or `undefined` when it can.
|
|
274
|
+
*
|
|
275
|
+
* An exhaustive `switch` with a `never` check rather than a condition chain: a new
|
|
276
|
+
* {@link ChatThinkingPolicy} member must be considered HERE or the build fails. The condition
|
|
277
|
+
* chain this replaced would have let every graded level clamp in silence, which is the one
|
|
278
|
+
* outcome the type's own docs promise callers never happens.
|
|
279
|
+
*/
|
|
280
|
+
clampWarning(policy) {
|
|
281
|
+
switch (policy) {
|
|
282
|
+
case undefined:
|
|
283
|
+
return undefined;
|
|
284
|
+
case 'off':
|
|
285
|
+
return thinkingIsMandatory(this.model)
|
|
286
|
+
? `AnthropicTransport: thinkingPolicy 'off' ignored — ${this.model} always thinks and rejects an explicit disable. Reasoning tokens are still billed as output; switch model if you need them gone.`
|
|
287
|
+
: undefined;
|
|
288
|
+
case 'auto':
|
|
289
|
+
return supportsAdaptiveThinking(this.model)
|
|
290
|
+
? undefined
|
|
291
|
+
: `AnthropicTransport: thinkingPolicy 'auto' ignored — ${this.model} does not support adaptive thinking, so this turn runs without reasoning. Use a Sonnet or Opus tier if the agent needs it.`;
|
|
292
|
+
case 'minimal':
|
|
293
|
+
case 'low':
|
|
294
|
+
case 'medium':
|
|
295
|
+
case 'high':
|
|
296
|
+
case 'max':
|
|
297
|
+
if (!supportsAdaptiveThinking(this.model) || !supportsEffort(this.model)) {
|
|
298
|
+
return `AnthropicTransport: thinkingPolicy '${policy}' ignored — ${this.model} predates output_config.effort, so this turn runs at its default posture. Use Opus 4.8, Sonnet 5, or Fable 5 to set reasoning depth.`;
|
|
299
|
+
}
|
|
300
|
+
// A clamp WITHIN the supported ladder is documented on the type (Anthropic has no
|
|
301
|
+
// 'minimal'), costs nothing, and would be noise on every turn of a vision loop.
|
|
302
|
+
return undefined;
|
|
303
|
+
default: {
|
|
304
|
+
const exhaustive = policy;
|
|
305
|
+
return exhaustive;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
224
308
|
}
|
|
225
309
|
constructor(config = {}) {
|
|
226
310
|
var _a, _b, _c, _d;
|
|
@@ -308,9 +392,7 @@ export class AnthropicTransport {
|
|
|
308
392
|
const useNative = responseSchema != null && supportsNativeStructuredOutput(this.model);
|
|
309
393
|
const useForcedTool = responseSchema != null && !useNative;
|
|
310
394
|
if (useNative) {
|
|
311
|
-
body.output_config = {
|
|
312
|
-
format: { type: 'json_schema', schema: responseSchema },
|
|
313
|
-
};
|
|
395
|
+
body.output_config = Object.assign(Object.assign({}, body.output_config), { format: { type: 'json_schema', schema: responseSchema } });
|
|
314
396
|
}
|
|
315
397
|
else if (useForcedTool) {
|
|
316
398
|
body.tools = [
|
|
@@ -344,7 +426,7 @@ export class AnthropicTransport {
|
|
|
344
426
|
// ── ChatTransport (multi-turn chat) ────────────────────────────────────
|
|
345
427
|
sendChatMessage(history, userMessage, options) {
|
|
346
428
|
return __awaiter(this, void 0, void 0, function* () {
|
|
347
|
-
var _a, _b, _c, _d;
|
|
429
|
+
var _a, _b, _c, _d, _e;
|
|
348
430
|
// The models this request could actually be served by — the configured one plus any
|
|
349
431
|
// fallback target. Needed when replaying reasoning: a signature is only valid for the
|
|
350
432
|
// model that produced it, so stored blocks are replayed only when their producer is
|
|
@@ -356,12 +438,13 @@ export class AnthropicTransport {
|
|
|
356
438
|
const messages = this.toAnthropicMessages(history, userMessage, options === null || options === void 0 ? void 0 : options.attachments, reachableModels);
|
|
357
439
|
const body = {
|
|
358
440
|
model: this.model,
|
|
359
|
-
|
|
441
|
+
// Per-turn override wins over the instance default; see ChatRequestOptions.maxTokens.
|
|
442
|
+
max_tokens: (_b = options === null || options === void 0 ? void 0 : options.maxTokens) !== null && _b !== void 0 ? _b : this.maxTokens,
|
|
360
443
|
messages,
|
|
361
444
|
};
|
|
362
445
|
if (options === null || options === void 0 ? void 0 : options.systemPrompt)
|
|
363
446
|
body.system = options.systemPrompt;
|
|
364
|
-
if ((
|
|
447
|
+
if ((_c = options === null || options === void 0 ? void 0 : options.tools) === null || _c === void 0 ? void 0 : _c.length) {
|
|
365
448
|
// `enforceSchema` is a per-tool opt-in, and the provider compiles per tool:
|
|
366
449
|
// an unenforced tool is never compiled and costs nothing against the
|
|
367
450
|
// compilation limits, so marking one tool in a large surface is legitimate.
|
|
@@ -375,7 +458,7 @@ export class AnthropicTransport {
|
|
|
375
458
|
// Map the requested tool-call mode to Anthropic's tool_choice — only
|
|
376
459
|
// meaningful when tools exist. `'required'`/`{ tool }` force a tool call so
|
|
377
460
|
// a turn can only end via one (e.g. a sub-agent's completion tool).
|
|
378
|
-
if ((
|
|
461
|
+
if ((_d = body.tools) === null || _d === void 0 ? void 0 : _d.length) {
|
|
379
462
|
const toolChoice = toAnthropicToolChoice(options === null || options === void 0 ? void 0 : options.toolChoice);
|
|
380
463
|
if (toolChoice)
|
|
381
464
|
body.tool_choice = toolChoice;
|
|
@@ -388,6 +471,13 @@ export class AnthropicTransport {
|
|
|
388
471
|
const thinking = anthropicThinking(this.model, options === null || options === void 0 ? void 0 : options.thinkingPolicy);
|
|
389
472
|
if (thinking)
|
|
390
473
|
body.thinking = thinking;
|
|
474
|
+
// Reasoning DEPTH rides in `output_config.effort`, a different field from `thinking`.
|
|
475
|
+
// MERGED, never assigned: structured output writes `format` into the same object further
|
|
476
|
+
// down, and either write clobbering the other is silent — the request stays valid, it just
|
|
477
|
+
// quietly stops doing one of the two things it was asked to do.
|
|
478
|
+
const effort = anthropicEffort(this.model, options === null || options === void 0 ? void 0 : options.thinkingPolicy);
|
|
479
|
+
if (effort)
|
|
480
|
+
body.output_config = Object.assign(Object.assign({}, body.output_config), { effort });
|
|
391
481
|
// Normalized [0,1] temperature → Anthropic's native range, anchored so 0.5 maps to its default.
|
|
392
482
|
// (Default == max here, so the upper half is flat.) Skipped in two cases:
|
|
393
483
|
//
|
|
@@ -411,14 +501,12 @@ export class AnthropicTransport {
|
|
|
411
501
|
// models that support it natively; elsewhere the schema is dropped here (the caller keeps a
|
|
412
502
|
// prompt-instruction + validator fallback). No beta header needed.
|
|
413
503
|
if ((options === null || options === void 0 ? void 0 : options.responseSchema) && supportsNativeStructuredOutput(this.model)) {
|
|
414
|
-
body.output_config = {
|
|
415
|
-
format: { type: 'json_schema', schema: options.responseSchema },
|
|
416
|
-
};
|
|
504
|
+
body.output_config = Object.assign(Object.assign({}, body.output_config), { format: { type: 'json_schema', schema: options.responseSchema } });
|
|
417
505
|
}
|
|
418
506
|
// Refusal fallback chain (e.g. Fable 5 → Opus 4.8). Sent as the server-side `fallbacks`
|
|
419
507
|
// param; `post` adds the required beta header when this is present. A refused turn is
|
|
420
508
|
// re-run on the next model in one round trip.
|
|
421
|
-
if ((
|
|
509
|
+
if ((_e = options === null || options === void 0 ? void 0 : options.fallbacks) === null || _e === void 0 ? void 0 : _e.length) {
|
|
422
510
|
body.fallbacks = options.fallbacks.map((f) => f.maxTokens != null ? { model: f.model, max_tokens: f.maxTokens } : { model: f.model });
|
|
423
511
|
}
|
|
424
512
|
// Place prompt-cache breakpoints per the resolved policy (no-op for `'default'`/absent).
|
|
@@ -431,7 +519,7 @@ export class AnthropicTransport {
|
|
|
431
519
|
this.appendTailContext(body, options.tailContext);
|
|
432
520
|
}
|
|
433
521
|
const response = yield this.post(body, options === null || options === void 0 ? void 0 : options.signal);
|
|
434
|
-
return this.fromAnthropicResponse(response);
|
|
522
|
+
return this.fromAnthropicResponse(response, body.max_tokens);
|
|
435
523
|
});
|
|
436
524
|
}
|
|
437
525
|
/**
|
|
@@ -533,7 +621,7 @@ export class AnthropicTransport {
|
|
|
533
621
|
* the payload tidy.
|
|
534
622
|
*/
|
|
535
623
|
toAnthropicMessages(history, userMessage, attachments, reachableModels = new Set([this.model])) {
|
|
536
|
-
var _a, _b, _c;
|
|
624
|
+
var _a, _b, _c, _d;
|
|
537
625
|
const messages = [];
|
|
538
626
|
const pushBlock = (role, block) => {
|
|
539
627
|
const last = messages[messages.length - 1];
|
|
@@ -543,6 +631,33 @@ export class AnthropicTransport {
|
|
|
543
631
|
}
|
|
544
632
|
messages.push({ role, content: [block] });
|
|
545
633
|
};
|
|
634
|
+
/**
|
|
635
|
+
* Split attachments by kind. Images are emitted BEFORE the accompanying text (the order
|
|
636
|
+
* Anthropic's vision guidance specifies); text attachments keep their existing position
|
|
637
|
+
* AFTER it, so a turn with no images produces a byte-identical request to before.
|
|
638
|
+
*
|
|
639
|
+
* Branches on `kind`, never on `mimeType` — see {@link ChatImageAttachment}.
|
|
640
|
+
*/
|
|
641
|
+
const splitAttachments = (atts) => {
|
|
642
|
+
const images = [];
|
|
643
|
+
const texts = [];
|
|
644
|
+
for (const att of atts) {
|
|
645
|
+
if (att.kind === 'image') {
|
|
646
|
+
images.push({
|
|
647
|
+
type: 'image',
|
|
648
|
+
source: {
|
|
649
|
+
type: 'base64',
|
|
650
|
+
media_type: normalizeImageMime(att.mimeType),
|
|
651
|
+
data: stripImageDataUrl(att.data),
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
texts.push({ type: 'text', text: `[File: ${att.name}]\n${att.content}` });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return { images, texts };
|
|
660
|
+
};
|
|
546
661
|
for (const msg of history) {
|
|
547
662
|
if (msg.role === 'system' ||
|
|
548
663
|
msg.role === 'system-event' ||
|
|
@@ -554,14 +669,34 @@ export class AnthropicTransport {
|
|
|
554
669
|
)
|
|
555
670
|
continue;
|
|
556
671
|
if (msg.toolResult) {
|
|
672
|
+
const resultImages = (_a = msg.toolResult.attachments) !== null && _a !== void 0 ? _a : [];
|
|
557
673
|
pushBlock('user', {
|
|
558
674
|
type: 'tool_result',
|
|
559
675
|
tool_use_id: msg.toolResult.toolCallId,
|
|
560
|
-
|
|
676
|
+
// Bare string when there are no images — the historical shape, byte for byte.
|
|
677
|
+
content: resultImages.length
|
|
678
|
+
? [
|
|
679
|
+
// Text block only when there IS text. A handler can return an image with no
|
|
680
|
+
// caption (`{ content: '', attachments: [img] }` — the mainline vision-agent
|
|
681
|
+
// "just look at this" shape), and Anthropic 400s an empty text block, live and on
|
|
682
|
+
// replay. Guarded like every user-turn path; the array is then image-only.
|
|
683
|
+
...(msg.toolResult.content
|
|
684
|
+
? [{ type: 'text', text: msg.toolResult.content }]
|
|
685
|
+
: []),
|
|
686
|
+
...resultImages.map((att) => ({
|
|
687
|
+
type: 'image',
|
|
688
|
+
source: {
|
|
689
|
+
type: 'base64',
|
|
690
|
+
media_type: normalizeImageMime(att.mimeType),
|
|
691
|
+
data: stripImageDataUrl(att.data),
|
|
692
|
+
},
|
|
693
|
+
})),
|
|
694
|
+
]
|
|
695
|
+
: msg.toolResult.content,
|
|
561
696
|
});
|
|
562
697
|
continue;
|
|
563
698
|
}
|
|
564
|
-
if ((
|
|
699
|
+
if ((_b = msg.toolCalls) === null || _b === void 0 ? void 0 : _b.length) {
|
|
565
700
|
// Reasoning first, then text, then the calls — the order the model produced them,
|
|
566
701
|
// which is the order the API validates. A tool-use loop is one assistant turn, so
|
|
567
702
|
// resuming it requires the thinking that led to the call to still be present and
|
|
@@ -578,29 +713,38 @@ export class AnthropicTransport {
|
|
|
578
713
|
type: 'tool_use',
|
|
579
714
|
id: tc.id,
|
|
580
715
|
name: tc.name,
|
|
581
|
-
input: (
|
|
716
|
+
input: (_c = tc.args) !== null && _c !== void 0 ? _c : {},
|
|
582
717
|
});
|
|
583
718
|
}
|
|
584
719
|
continue;
|
|
585
720
|
}
|
|
586
721
|
const role = msg.role === 'user' ? 'user' : 'assistant';
|
|
587
|
-
if (role === 'user' && ((
|
|
588
|
-
|
|
589
|
-
for (const
|
|
590
|
-
pushBlock(role,
|
|
591
|
-
|
|
722
|
+
if (role === 'user' && ((_d = msg.attachments) === null || _d === void 0 ? void 0 : _d.length)) {
|
|
723
|
+
const { images, texts } = splitAttachments(msg.attachments);
|
|
724
|
+
for (const image of images)
|
|
725
|
+
pushBlock(role, image);
|
|
726
|
+
// Guarded like the live turn below: an image-only message has empty `content`, and
|
|
727
|
+
// Anthropic 400s an empty text block. Unguarded, such a turn succeeded when sent and
|
|
728
|
+
// then failed on REPLAY — the nastiest shape of this bug, and "image, no caption" is
|
|
729
|
+
// mainline input for a vision agent.
|
|
730
|
+
if (msg.content)
|
|
731
|
+
pushBlock(role, { type: 'text', text: msg.content });
|
|
732
|
+
for (const text of texts)
|
|
733
|
+
pushBlock(role, text);
|
|
592
734
|
}
|
|
593
735
|
else if (msg.content) {
|
|
594
736
|
pushBlock(role, { type: 'text', text: msg.content });
|
|
595
737
|
}
|
|
596
738
|
}
|
|
597
739
|
if (userMessage || (attachments === null || attachments === void 0 ? void 0 : attachments.length)) {
|
|
740
|
+
const { images, texts } = splitAttachments(attachments !== null && attachments !== void 0 ? attachments : []);
|
|
741
|
+
for (const image of images)
|
|
742
|
+
pushBlock('user', image);
|
|
598
743
|
if (userMessage) {
|
|
599
744
|
pushBlock('user', { type: 'text', text: userMessage });
|
|
600
745
|
}
|
|
601
|
-
for (const
|
|
602
|
-
pushBlock('user',
|
|
603
|
-
}
|
|
746
|
+
for (const text of texts)
|
|
747
|
+
pushBlock('user', text);
|
|
604
748
|
}
|
|
605
749
|
return messages;
|
|
606
750
|
}
|
|
@@ -716,7 +860,9 @@ export class AnthropicTransport {
|
|
|
716
860
|
const cacheCreation = (_c = usage.cache_creation_input_tokens) !== null && _c !== void 0 ? _c : (breakdown ? ((_d = breakdown.ephemeral_5m_input_tokens) !== null && _d !== void 0 ? _d : 0) + cacheCreation1h : 0);
|
|
717
861
|
return this.logTokenUsage(model, (_e = usage.input_tokens) !== null && _e !== void 0 ? _e : 0, (_f = usage.output_tokens) !== null && _f !== void 0 ? _f : 0, cacheRead, cacheCreation, cacheCreation1h);
|
|
718
862
|
}
|
|
719
|
-
fromAnthropicResponse(response
|
|
863
|
+
fromAnthropicResponse(response,
|
|
864
|
+
/** The cap ACTUALLY sent this turn — a per-turn `maxTokens` overrides the instance one. */
|
|
865
|
+
effectiveMaxTokens = this.maxTokens) {
|
|
720
866
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
721
867
|
let inputTokens;
|
|
722
868
|
let outputTokens;
|
|
@@ -844,7 +990,7 @@ export class AnthropicTransport {
|
|
|
844
990
|
// missing args and get retried into the same wall). Usage/cost is already
|
|
845
991
|
// logged above, so the spent tokens are still accounted for on this instance.
|
|
846
992
|
if (response.stop_reason === 'max_tokens' && toolCalls.length > 0) {
|
|
847
|
-
throw new ResponseTruncatedError(this.model,
|
|
993
|
+
throw new ResponseTruncatedError(this.model, effectiveMaxTokens, outputTokens, toolCalls.map((tc) => tc.name));
|
|
848
994
|
}
|
|
849
995
|
// Reasoning (extended-thinking summary) and answer/narration travel in separate channels:
|
|
850
996
|
// `content` is the answer text, `reasoning` is the summary. The driver splits them into
|