@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.
@@ -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 clamped = (policy === 'off' && thinkingIsMandatory(this.model)) ||
217
- (policy === 'auto' && !supportsAdaptiveThinking(this.model));
218
- if (!clamped)
266
+ const message = this.clampWarning(policy);
267
+ if (!message)
219
268
  return;
220
269
  this.warnedThinkingClamped = true;
221
- logger.warn(policy === 'off'
222
- ? `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.`
223
- : `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.`);
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
- max_tokens: this.maxTokens,
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 ((_b = options === null || options === void 0 ? void 0 : options.tools) === null || _b === void 0 ? void 0 : _b.length) {
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 ((_c = body.tools) === null || _c === void 0 ? void 0 : _c.length) {
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 ((_d = options === null || options === void 0 ? void 0 : options.fallbacks) === null || _d === void 0 ? void 0 : _d.length) {
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
- content: msg.toolResult.content,
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 ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
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: (_b = tc.args) !== null && _b !== void 0 ? _b : {},
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' && ((_c = msg.attachments) === null || _c === void 0 ? void 0 : _c.length)) {
588
- pushBlock(role, { type: 'text', text: msg.content });
589
- for (const att of msg.attachments) {
590
- pushBlock(role, { type: 'text', text: `[File: ${att.name}]\n${att.content}` });
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 att of attachments !== null && attachments !== void 0 ? attachments : []) {
602
- pushBlock('user', { type: 'text', text: `[File: ${att.name}]\n${att.content}` });
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, this.maxTokens, outputTokens, toolCalls.map((tc) => tc.name));
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