@bike4mind/cli 0.20.1 → 0.20.2

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,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as WORK_ITEM_STATUSES, A as HttpStatus, At as usdToCredits, B as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Ct as mapMimeTypeToArtifactType, D as FORMAT_PROMPT_TEMPLATE, Dt as settingsMap, E as FIXED_TEMPERATURE_MODELS, Et as secureParameters, F as NO_TEMPERATURE_MODELS, Ft as isNearLimit, G as SupportedFabFileMimeTypes, H as REFUSAL_FALLBACK_MODELS, I as NotFoundError, It as parseRateLimitHeaders, J as UnauthorizedError, K as TTS_MAX_INPUT_CHARS, L as OllamaEmbeddingModel, M as InternalServerError, Mt as withRetry, N as MODEL_INFO_FIELD_GROUP_OF, Nt as buildRateLimitLogEntry, O as ForbiddenError, Ot as toModelInfo, P as ModelBackend, Pt as extractSnippetMeta, Q as VoyageAIEmbeddingModel, R as OpenAIEmbeddingModel, S as CorruptedFileError, St as isZodError, T as FIELD_GROUP_OF, Tt as resolveHistoryFetchLimit, U as RESPONSES_API_TOOL_MODELS, V as REASONING_SUPPORTED_MODELS, W as SpeechToTextModels, X as VIDEO_SIZE_CONSTRAINTS, Y as UnprocessableEntityError, Z as VideoModels, _ as BadRequestError, _t as isRenderableModelType, at as getQuestErrorCode, bt as isUnlimitedHistory, ct as isFieldGroup, dt as isImageAttachment, et as applyModelPriceCatalog, ft as isImageServeable, g as BFL_SAFETY_TOLERANCE, gt as isPlaceholderApiKey, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as isModelDeprecated, it as getMcpProviderMetadata, j as ImageModels, jt as usdToCreditsStochastic, k as HTTPError, kt as toModelRecord, lt as isGPTImage2Model, m as ApiKeyType, mt as isModelAccessible, n as logger, nt as dayjsConfig_default, ot as getRetryAfterMs, p as ARTIFACT_ATTRS_PATTERN, pt as isMediaModelType, q as TooManyRequestsError, rt as defaultEmbeddingModelForEnv, st as isAudioMimeType, tt as calculateRetryDelay, ut as isGPTImageModel, v as BedrockEmbeddingModel, vt as isRetryableError, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as obfuscateApiKey, x as ChatModels, xt as isUserInitiatedAbort, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, yt as isSupportedFabFileMimeType, z as PermissionDeniedError } from "./ConfigStore-CNfbeaJf.mjs";
2
+ import { $ as VoyageAIEmbeddingModel, A as HTTPError, At as resolveHistoryFetchLimit, B as PermissionDeniedError, Bt as isNearLimit, Ct as isRetryableError, D as FIXED_TEMPERATURE_MODELS, Dt as isZodError, E as FIELD_GROUP_OF, Et as isUserInitiatedAbort, F as ModelBackend, Ft as usdToCredits, G as SpeechToTextModels, H as REASONING_SUPPORTED_MODELS, I as NO_TEMPERATURE_MODELS, It as usdToCreditsStochastic, J as TooManyRequestsError, K as SupportedFabFileMimeTypes, L as NotFoundError, Lt as withRetry, M as ImageModels, Mt as settingsMap, N as InternalServerError, Nt as toModelInfo, O as FORMAT_PROMPT_TEMPLATE, Ot as mapMimeTypeToArtifactType, P as MODEL_INFO_FIELD_GROUP_OF, Pt as toModelRecord, Q as VideoModels, R as OllamaEmbeddingModel, Rt as buildRateLimitLogEntry, S as CorruptedFileError, St as isRenderableModelType, Tt as isUnlimitedHistory, U as REFUSAL_FALLBACK_MODELS, V as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Vt as parseRateLimitHeaders, W as RESPONSES_API_TOOL_MODELS, X as UnprocessableEntityError, Y as UnauthorizedError, Z as VIDEO_SIZE_CONSTRAINTS, _ as BadRequestError, _t as isImageServeable, at as getMcpProviderMetadata, bt as isModelDeprecated, ct as isAudioMimeType, et as WORK_ITEM_STATUSES, ft as isFieldGroup, g as BFL_SAFETY_TOLERANCE, gt as isImageAttachment, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as isGeminiModelId, it as defaultEmbeddingModelForEnv, j as HttpStatus, jt as secureParameters, k as ForbiddenError, kt as obfuscateApiKey, lt as isChunkRebuildPending, m as ApiKeyType, mt as isGPTImageModel, n as logger, nt as calculateRetryDelay, ot as getQuestErrorCode, p as ARTIFACT_ATTRS_PATTERN, pt as isGPTImage2Model, q as TTS_MAX_INPUT_CHARS, rt as dayjsConfig_default, st as getRetryAfterMs, tt as applyModelPriceCatalog, ut as isConvergencePausedNote, v as BedrockEmbeddingModel, vt as isMediaModelType, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isSupportedFabFileMimeType, x as ChatModels, xt as isPlaceholderApiKey, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, yt as isModelAccessible, z as OpenAIEmbeddingModel, zt as extractSnippetMeta } from "./ConfigStore-cIyF7hDg.mjs";
3
3
  import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
4
4
  import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
5
5
  import { execFile, execFileSync, spawn } from "child_process";
@@ -37,13 +37,14 @@ import OpenAI from "openai";
37
37
  import { VoyageAIClient } from "voyageai";
38
38
  import mime from "mime-types";
39
39
  import dns from "dns";
40
+ import http from "http";
41
+ import https, { Agent } from "https";
40
42
  import { promisify } from "util";
41
43
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
42
44
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
43
45
  import { Readable } from "stream";
44
46
  import { fileTypeFromBuffer } from "file-type";
45
47
  import { NodeHttpHandler } from "@smithy/node-http-handler";
46
- import { Agent } from "https";
47
48
  import "@opensearch-project/opensearch";
48
49
  import "@aws-sdk/credential-provider-node";
49
50
  import "@opensearch-project/opensearch/aws-v3";
@@ -110,6 +111,18 @@ const COMPACTION_SUMMARY_MARKER = "[Previous conversation summary]";
110
111
  //#endregion
111
112
  //#region src/utils/fileSearch.ts
112
113
  /**
114
+ * stat() that yields null instead of throwing. Following a symlink is best-effort here:
115
+ * a dangling link, or one pointing somewhere unreadable, must not take down a whole
116
+ * directory listing.
117
+ */
118
+ function statOrNull(targetPath) {
119
+ try {
120
+ return fs$2.statSync(targetPath);
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ /**
113
126
  * Load gitignore rules from project root
114
127
  */
115
128
  function loadIgnoreRules(projectRoot) {
@@ -200,13 +213,17 @@ function listAbsoluteDirectory(absolutePath, filterQuery) {
200
213
  }
201
214
  return filteredEntries.slice(0, 15).map((entry) => {
202
215
  const fullPath = path$1.join(normalizedPath, entry.name);
216
+ const target = entry.isSymbolicLink() ? statOrNull(fullPath) : null;
217
+ const isDirectory = target ? target.isDirectory() : entry.isDirectory();
218
+ const isFile = target ? target.isFile() : entry.isFile();
203
219
  const result = {
204
220
  path: fullPath,
205
- isDirectory: entry.isDirectory()
221
+ isDirectory
206
222
  };
207
- if (entry.isFile()) try {
208
- result.size = fs$2.statSync(fullPath).size;
209
- } catch {}
223
+ if (isFile) {
224
+ const fileStats = target ?? statOrNull(fullPath);
225
+ if (fileStats) result.size = fileStats.size;
226
+ }
210
227
  return result;
211
228
  });
212
229
  } catch {
@@ -1555,7 +1572,7 @@ function webFetchBody(result) {
1555
1572
  if (result.extractedChars === 0 && result.offset > 0) return `[web_fetch: offset ${result.offset} is at or beyond the end of the content (~${result.originalChars} chars); nothing further to read.]`;
1556
1573
  return result.truncated ? result.markdown + truncationMarker(result) : result.markdown;
1557
1574
  }
1558
- function isPdfUrl(url) {
1575
+ function isPdfUrl$2(url) {
1559
1576
  try {
1560
1577
  const { pathname } = new URL(url);
1561
1578
  return pathname.toLowerCase().endsWith(".pdf");
@@ -1615,7 +1632,7 @@ async function probeLlmsTxt(pageUrl) {
1615
1632
  */
1616
1633
  async function firecrawlFetch(adapters, url, options) {
1617
1634
  if (!/^https?:\/\/.+/i.test(url)) throw new Error(`Invalid URL format: ${url}. URL must start with http:// or https://`);
1618
- const isPdf = isPdfUrl(url);
1635
+ const isPdf = isPdfUrl$2(url);
1619
1636
  const desiredTimeout = isPdf ? PDF_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
1620
1637
  const timeoutMs = options?.maxTimeoutMs ? Math.min(desiredTimeout, options.maxTimeoutMs) : desiredTimeout;
1621
1638
  const app = createFirecrawlApp(await (0, apiKeyService_exports.getFirecrawlConfig)(adapters));
@@ -2044,7 +2061,7 @@ const webSearchTool = {
2044
2061
  })
2045
2062
  };
2046
2063
  //#endregion
2047
- //#region ../../b4m-core/services/dist/toolGenerators-BVUZiwqE.mjs
2064
+ //#region ../../b4m-core/services/dist/toolGenerators-D3QFkvc-.mjs
2048
2065
  const diceRoll = async (parameters) => {
2049
2066
  if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
2050
2067
  return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
@@ -2731,11 +2748,12 @@ const askUserQuestionTool = {
2731
2748
  * re-export them without pulling the full tool graph. `index.ts` re-exports them
2732
2749
  * so the server barrel's public API is unchanged.
2733
2750
  */
2734
- const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2751
+ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2735
2752
  const context = {
2736
2753
  userId,
2737
2754
  user,
2738
2755
  sessionId,
2756
+ questId,
2739
2757
  logger,
2740
2758
  db,
2741
2759
  storage,
@@ -2752,9 +2770,12 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
2752
2770
  kbScope,
2753
2771
  inlinedAttachmentIds,
2754
2772
  fullyInlinedAttachmentIds,
2773
+ suppressLakeArms,
2774
+ sessionRetrievalTags,
2755
2775
  codeMinifier,
2756
2776
  availableModels,
2757
- onToolLlmUsage
2777
+ onToolLlmUsage,
2778
+ getAbortSignal
2758
2779
  };
2759
2780
  return Object.entries(tools).reduce((acc, [key, tool]) => ({
2760
2781
  ...acc,
@@ -5438,6 +5459,61 @@ function extractCommandName(filename) {
5438
5459
  return name;
5439
5460
  }
5440
5461
  //#endregion
5462
+ //#region src/utils/findMarkdownFiles.ts
5463
+ /**
5464
+ * Classify a directory entry, resolving symlinks by their target.
5465
+ *
5466
+ * `fs.readdir(withFileTypes)` builds each Dirent from the entry itself, never
5467
+ * the link target, so a symlink reports `isFile() === false` AND
5468
+ * `isDirectory() === false`. A walk keyed off those two predicates alone skips
5469
+ * every symlinked entry.
5470
+ */
5471
+ async function classifyEntry(fullPath, entry) {
5472
+ if (!entry.isSymbolicLink()) {
5473
+ if (entry.isDirectory()) return "directory";
5474
+ return entry.isFile() ? "file" : "other";
5475
+ }
5476
+ try {
5477
+ const stats = await fs$1.stat(fullPath);
5478
+ if (stats.isDirectory()) return "directory";
5479
+ return stats.isFile() ? "file" : "other";
5480
+ } catch {
5481
+ return "other";
5482
+ }
5483
+ }
5484
+ /**
5485
+ * Recursively collect every `.md` file under `directory`, following symlinks.
5486
+ *
5487
+ * Symlink support is load-bearing, not a nicety: dotfile managers (nix
5488
+ * home-manager, chezmoi, a plain dotfiles repo) materialize
5489
+ * `~/.claude/skills/<name>/SKILL.md` as a symlink into an immutable store, so a
5490
+ * walk that only accepts real files finds zero global skills on those machines.
5491
+ *
5492
+ * Directories are deduped by realpath so a cyclic link cannot spin forever.
5493
+ */
5494
+ async function findMarkdownFiles(directory, visitedRealPaths = /* @__PURE__ */ new Set()) {
5495
+ const files = [];
5496
+ try {
5497
+ const realDirectory = await fs$1.realpath(directory);
5498
+ if (visitedRealPaths.has(realDirectory)) return files;
5499
+ visitedRealPaths.add(realDirectory);
5500
+ } catch {}
5501
+ let entries;
5502
+ try {
5503
+ entries = await fs$1.readdir(directory, { withFileTypes: true });
5504
+ } catch (error) {
5505
+ console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
5506
+ return files;
5507
+ }
5508
+ for (const entry of entries) {
5509
+ const fullPath = path.join(directory, entry.name);
5510
+ const kind = await classifyEntry(fullPath, entry);
5511
+ if (kind === "directory") files.push(...await findMarkdownFiles(fullPath, visitedRealPaths));
5512
+ else if (kind === "file" && entry.name.endsWith(".md")) files.push(fullPath);
5513
+ }
5514
+ return files;
5515
+ }
5516
+ //#endregion
5441
5517
  //#region src/storage/CustomCommandStore.ts
5442
5518
  var CustomCommandStore = class {
5443
5519
  constructor(projectRoot, options = {}) {
@@ -5515,7 +5591,7 @@ var CustomCommandStore = class {
5515
5591
  async loadCommandsFromDirectory(directory, source) {
5516
5592
  try {
5517
5593
  if (!(await fs$1.stat(directory)).isDirectory()) return;
5518
- const commandFiles = await this.findCommandFiles(directory);
5594
+ const commandFiles = await findMarkdownFiles(directory);
5519
5595
  for (const filePath of commandFiles) try {
5520
5596
  await this.loadCommandFile(filePath, source);
5521
5597
  } catch (error) {
@@ -5526,28 +5602,6 @@ var CustomCommandStore = class {
5526
5602
  }
5527
5603
  }
5528
5604
  /**
5529
- * Recursively finds all .md files in a directory
5530
- *
5531
- * @param directory - Directory to search
5532
- * @returns Array of full file paths to .md files
5533
- */
5534
- async findCommandFiles(directory) {
5535
- const files = [];
5536
- try {
5537
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
5538
- for (const entry of entries) {
5539
- const fullPath = path.join(directory, entry.name);
5540
- if (entry.isDirectory()) {
5541
- const subFiles = await this.findCommandFiles(fullPath);
5542
- files.push(...subFiles);
5543
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
5544
- }
5545
- } catch (error) {
5546
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
5547
- }
5548
- return files;
5549
- }
5550
- /**
5551
5605
  * Loads a single command file
5552
5606
  *
5553
5607
  * @param filePath - Full path to the command file
@@ -7045,6 +7099,7 @@ var SmartChunker = class {
7045
7099
  encoder;
7046
7100
  storage;
7047
7101
  bufferPercentOrValue;
7102
+ lastExtractedText;
7048
7103
  /**
7049
7104
  * @param model - The embedding model name
7050
7105
  * @param storage - Storage instance for file content
@@ -7098,6 +7153,13 @@ var SmartChunker = class {
7098
7153
  this.encoder = void 0;
7099
7154
  }
7100
7155
  }
7156
+ /**
7157
+ * The canonical extracted text from the most recent chunkFile() call - policy-independent, unlike
7158
+ * the returned chunks. Undefined when the file yielded no extractable text. See lastExtractedText.
7159
+ */
7160
+ getExtractedText() {
7161
+ return this.lastExtractedText;
7162
+ }
7101
7163
  async chunkFile(fileOrContent, mimeType) {
7102
7164
  let content;
7103
7165
  if (Buffer.isBuffer(fileOrContent)) content = fileOrContent;
@@ -7107,6 +7169,7 @@ var SmartChunker = class {
7107
7169
  }
7108
7170
  this.logger.updateMetadata({ mimeType });
7109
7171
  this.logger.log(`Chunking file with type: ${mimeType}`);
7172
+ this.lastExtractedText = void 0;
7110
7173
  if (isAudioMimeType(mimeType)) {
7111
7174
  this.logger.log(`Skipping chunking for audio file type: ${mimeType}`);
7112
7175
  return [];
@@ -7144,12 +7207,17 @@ var SmartChunker = class {
7144
7207
  case SupportedFabFileMimeTypes.PHP:
7145
7208
  case SupportedFabFileMimeTypes.RUBY:
7146
7209
  case SupportedFabFileMimeTypes.SH:
7147
- case SupportedFabFileMimeTypes.BASH:
7148
- chunks = await this.chunkText(content.toString());
7210
+ case SupportedFabFileMimeTypes.BASH: {
7211
+ const textContent = content.toString();
7212
+ this.lastExtractedText = textContent;
7213
+ chunks = await this.chunkText(textContent);
7149
7214
  break;
7215
+ }
7150
7216
  default:
7151
7217
  if (mimeType && mimeType.startsWith("text/")) {
7152
- chunks = await this.chunkText(content.toString());
7218
+ const textContent = content.toString();
7219
+ this.lastExtractedText = textContent;
7220
+ chunks = await this.chunkText(textContent);
7153
7221
  break;
7154
7222
  }
7155
7223
  this.logger.error(`Unsupported file type: ${mimeType}`);
@@ -7161,7 +7229,9 @@ var SmartChunker = class {
7161
7229
  return await this.storage.getContentAsBuffer(file.filePath);
7162
7230
  }
7163
7231
  async chunkCSV(content) {
7164
- const rows = content.toString("utf8").split(/\r?\n/).filter((row) => row.trim().length > 0);
7232
+ const csvString = content.toString("utf8");
7233
+ this.lastExtractedText = csvString;
7234
+ const rows = csvString.split(/\r?\n/).filter((row) => row.trim().length > 0);
7165
7235
  const chunks = [];
7166
7236
  let currentChunk = [];
7167
7237
  let currentTokens = 0;
@@ -7214,6 +7284,7 @@ var SmartChunker = class {
7214
7284
  async chunkPDF(content) {
7215
7285
  const pdf = await getDocumentProxy(new Uint8Array(content));
7216
7286
  const { text } = await extractText(pdf);
7287
+ this.lastExtractedText = Array.isArray(text) ? text.join("\n") : text;
7217
7288
  if (typeof text === "string") return this.chunkText(text);
7218
7289
  const chunks = [];
7219
7290
  let currentChunk = "";
@@ -7245,7 +7316,9 @@ var SmartChunker = class {
7245
7316
  return chunks;
7246
7317
  }
7247
7318
  async chunkJSON(content) {
7248
- const json = JSON.parse(content.toString());
7319
+ const jsonString = content.toString();
7320
+ this.lastExtractedText = jsonString;
7321
+ const json = JSON.parse(jsonString);
7249
7322
  return this.chunkObject(json);
7250
7323
  }
7251
7324
  async chunkObject(obj) {
@@ -7312,6 +7385,7 @@ var SmartChunker = class {
7312
7385
  }
7313
7386
  async chunkDOCX(content) {
7314
7387
  const result = await mammoth.extractRawText({ buffer: content });
7388
+ this.lastExtractedText = result.value;
7315
7389
  return this.chunkText(result.value);
7316
7390
  }
7317
7391
  async chunkPPTX(content) {
@@ -7330,6 +7404,7 @@ var SmartChunker = class {
7330
7404
  this.logger.warn("PPTX contained no extractable slide text");
7331
7405
  return [];
7332
7406
  }
7407
+ this.lastExtractedText = fullText;
7333
7408
  return this.chunkText(fullText);
7334
7409
  }
7335
7410
  async chunkText(content) {
@@ -7404,6 +7479,9 @@ var SmartChunker = class {
7404
7479
  async chunkExcel(content) {
7405
7480
  const { read, utils } = await import("xlsx");
7406
7481
  const workbook = read(content, { type: "buffer" });
7482
+ this.lastExtractedText = workbook.SheetNames.map((sheetName) => {
7483
+ return `--- Sheet: ${sheetName} ---\n${utils.sheet_to_json(workbook.Sheets[sheetName], { header: 1 }).filter(Array.isArray).map((row) => JSON.stringify(row)).join("\n")}\n--- End of Sheet: ${sheetName} ---`;
7484
+ }).join("\n");
7407
7485
  const chunks = [];
7408
7486
  let currentChunk = "";
7409
7487
  let currentTokens = 0;
@@ -7623,6 +7701,8 @@ const dnsResolve6 = promisify(dns.resolve6);
7623
7701
  * Check if an IPv4 address is in a private/internal range.
7624
7702
  */
7625
7703
  function isPrivateIPv4(ip) {
7704
+ const nonCanonical = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
7705
+ if (nonCanonical && nonCanonical.slice(1).some((octet) => octet.length > 1 && octet.startsWith("0"))) return true;
7626
7706
  const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
7627
7707
  if (!ipv4Match) return false;
7628
7708
  const [, a, b, c] = ipv4Match.map(Number);
@@ -7643,44 +7723,153 @@ function isPrivateIPv4(ip) {
7643
7723
  /**
7644
7724
  * Check if an IPv6 address is in a private/internal range.
7645
7725
  */
7726
+ /**
7727
+ * Strip the brackets WHATWG URL keeps on an IPv6 hostname: `new URL('http://[::1]/').hostname` is
7728
+ * `'[::1]'`, not `'::1'`. Every literal check below compares against unbracketed forms, so without
7729
+ * this a bracketed address matched nothing and fell through as safe.
7730
+ *
7731
+ * Same treatment as the sibling guards in this repo - `ssrfGuard.ts` and `external-image.ts` both
7732
+ * strip brackets before their literal checks.
7733
+ */
7734
+ function stripIpv6Brackets(hostname) {
7735
+ const h = hostname.toLowerCase();
7736
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
7737
+ }
7738
+ /**
7739
+ * Canonicalize an IPv6 literal to the RFC 5952 form - leading zeros dropped per hextet, longest run of
7740
+ * two or more zero hextets compressed to `::`. That is the shape both feeders already hand this module
7741
+ * (WHATWG `URL.hostname` at the `validateUrlForFetch` call site, and getaddrinfo answers via
7742
+ * `ssrfSafeLookup`), so it is the shape every prefix arm in `isPrivateIPv6` was written against.
7743
+ * Canonicalizing once here is what lets those arms cover a family instead of enumerating its legal
7744
+ * spellings: `0:0:0:0:0:ffff:127.0.0.1` arrives as `::ffff:127.0.0.1` instead of matching nothing.
7745
+ *
7746
+ * A dotted IPv4 tail is deliberately kept dotted, unlike WHATWG which hexifies it. The mapped branch
7747
+ * below decodes a dotted tail exactly through `isPrivateIPv4` and only blanket-refuses when the tail is
7748
+ * hex, so hexifying here would turn `::ffff:8.8.8.8` into an over-block.
7749
+ *
7750
+ * This cannot over-block: dropping leading zeros only shortens hextets below 0x1000, and every
7751
+ * globally routable address is inside 2000::/3 (first hextet 0x2000-0x3fff), which never carries one.
7752
+ * Input that does not parse as IPv6 is returned untouched, so it keeps whatever verdict it has today -
7753
+ * which is also why the redundant zero-padded arms further down (`2001:0db8:`, `2001:0000:`,
7754
+ * `0064:ff9b:`, `0100::`) are left in place rather than deleted.
7755
+ */
7756
+ function normalizeIpv6(ip) {
7757
+ const ported = ip.match(/^(\[[^\]]*\]):\d+$/);
7758
+ const bare = stripIpv6Brackets(ported ? ported[1] : ip).replace(/%.*$/, "");
7759
+ const halves = bare.split("::");
7760
+ if (halves.length > 2) return bare;
7761
+ if (halves.length === 2 && halves[0].includes(".")) return bare;
7762
+ const tokens = halves.flatMap((half) => half === "" ? [] : half.split(":"));
7763
+ const dotted = tokens.length > 0 && tokens[tokens.length - 1].includes(".") ? tokens.pop() : void 0;
7764
+ if (!tokens.every((token) => /^[0-9a-f]{1,4}$/.test(token))) return bare;
7765
+ const compressed = halves.length === 2;
7766
+ const width = tokens.length + (dotted ? 2 : 0);
7767
+ if (compressed ? width > 8 : width !== 8) return bare;
7768
+ const hextets = tokens.map((token) => token.replace(/^0+(?=.)/, ""));
7769
+ if (compressed) {
7770
+ const headWidth = halves[0] === "" ? 0 : halves[0].split(":").length;
7771
+ hextets.splice(headWidth, 0, ...new Array(8 - width).fill("0"));
7772
+ }
7773
+ const run = {
7774
+ start: -1,
7775
+ length: 0
7776
+ };
7777
+ for (let i = 0; i < hextets.length; i++) {
7778
+ if (hextets[i] !== "0") continue;
7779
+ let end = i;
7780
+ while (end < hextets.length && hextets[end] === "0") end++;
7781
+ if (end - i > run.length) {
7782
+ run.start = i;
7783
+ run.length = end - i;
7784
+ }
7785
+ i = end;
7786
+ }
7787
+ const body = run.length >= 2 ? `${hextets.slice(0, run.start).join(":")}::${hextets.slice(run.start + run.length).join(":")}` : hextets.join(":");
7788
+ if (dotted === void 0) return body;
7789
+ return body.endsWith(":") ? `${body}${dotted}` : `${body}:${dotted}`;
7790
+ }
7646
7791
  function isPrivateIPv6(ip) {
7647
- const normalized = ip.toLowerCase();
7792
+ const normalized = normalizeIpv6(ip);
7793
+ if (!normalized.includes(":")) return false;
7794
+ if (/[[\]]/.test(normalized)) return true;
7795
+ if (!/^[0-9a-f:.]+$/.test(normalized)) return false;
7796
+ if (normalized.includes(".") && !/^::(ffff:)?\d+\.\d+\.\d+\.\d+$/.test(normalized)) return true;
7648
7797
  if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
7649
7798
  if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
7650
- if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
7799
+ if (normalized.startsWith("fe")) return true;
7651
7800
  if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
7652
7801
  if (normalized.startsWith("ff")) return true;
7653
- const ipv4MappedMatch = normalized.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
7654
- if (ipv4MappedMatch) return isPrivateIPv4(ipv4MappedMatch[1]);
7802
+ if (normalized.startsWith("::ffff:")) {
7803
+ const tail = normalized.slice(7);
7804
+ return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
7805
+ }
7806
+ if (normalized.startsWith("::")) {
7807
+ const tail = normalized.slice(2);
7808
+ return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
7809
+ }
7810
+ if (normalized.startsWith("0:")) return true;
7811
+ if (normalized.startsWith("5f00:")) return true;
7812
+ if (normalized.startsWith("3fff:")) {
7813
+ const rest = normalized.slice(5);
7814
+ if ((rest.startsWith(":") ? 0 : parseInt(rest.split(":")[0], 16)) <= 4095) return true;
7815
+ }
7655
7816
  if (normalized.startsWith("2001:db8:") || normalized.startsWith("2001:0db8:")) return true;
7817
+ if (normalized.startsWith("2002:")) return true;
7818
+ if (normalized.startsWith("2001:0:") || normalized.startsWith("2001:0000:") || normalized.startsWith("2001::")) return true;
7656
7819
  if (normalized.startsWith("100::") || normalized.startsWith("0100::")) return true;
7657
7820
  if (normalized.startsWith("64:ff9b:") || normalized.startsWith("0064:ff9b:")) return true;
7658
7821
  return false;
7659
7822
  }
7660
7823
  /**
7824
+ * Strip what belongs to the interface or the transport rather than to the address: a zone index
7825
+ * (`fe80::1%eth0`) and, for an IPv4 literal, a trailing port (`8.8.8.8:443`).
7826
+ *
7827
+ * MUST be shared by every exported entry point. `isPrivateIP` and `isPrivateOrInternalHostname` each carry
7828
+ * their own family gate, and the comment on those gates says why they are kept identical: three gates
7829
+ * disagreeing about what counts as IPv4 is how the bracketed-IPv6 hole happened. Stripping in one of them
7830
+ * only reproduced exactly that - `8.8.8.8:443` came back public from one export and private from the other,
7831
+ * because the second missed its IPv4 branch and was then caught by the misplaced-quad arm.
7832
+ *
7833
+ * Only a DOTTED port is stripped. An unbracketed `ipv6:port` is genuinely ambiguous - a bare IPv6 address is
7834
+ * mostly colons - so it stays refused, and the bracketed spelling is what `normalizeIpv6` handles.
7835
+ */
7836
+ function stripZoneAndIpv4Port(host) {
7837
+ const head = host.split("%")[0];
7838
+ const zoneless = head.includes(":") || /^(\d+\.){3}\d+$/.test(head) ? head : host;
7839
+ return zoneless.match(/^((?:\d+\.){3}\d+):\d+$/)?.[1] ?? zoneless;
7840
+ }
7841
+ /**
7661
7842
  * Check if an IP address (IPv4 or IPv6) is in a private/internal range.
7662
7843
  */
7663
7844
  function isPrivateIP(ip) {
7664
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return isPrivateIPv4(ip);
7665
- return isPrivateIPv6(ip);
7845
+ const address = stripZoneAndIpv4Port(ip);
7846
+ if (/^(\d+\.){3}\d+$/.test(address)) return isPrivateIPv4(address);
7847
+ return isPrivateIPv6(address);
7666
7848
  }
7667
7849
  /**
7668
7850
  * Check if a hostname is known to be private/internal.
7669
7851
  * This catches obvious cases before DNS resolution.
7670
7852
  */
7671
7853
  function isPrivateOrInternalHostname(hostname) {
7672
- const normalized = hostname.toLowerCase();
7854
+ const normalized = stripZoneAndIpv4Port(stripIpv6Brackets(hostname));
7673
7855
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "0.0.0.0" || normalized.endsWith(".localhost") || normalized.endsWith(".local")) return true;
7674
7856
  if (normalized === "169.254.169.254" || normalized === "instance-data" || normalized === "metadata.google.internal" || normalized === "metadata.internal") return true;
7675
7857
  if (normalized.endsWith(".cluster.local") || normalized.endsWith(".svc.cluster.local") || normalized.endsWith(".pod.cluster.local")) return true;
7676
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(normalized)) return isPrivateIPv4(normalized);
7858
+ if (/^(\d+\.){3}\d+$/.test(normalized)) return isPrivateIPv4(normalized);
7677
7859
  if (normalized.includes(":")) return isPrivateIPv6(normalized);
7678
7860
  return false;
7679
7861
  }
7680
7862
  /**
7681
7863
  * Validate a URL before fetching.
7682
7864
  * Blocks internal/private networks to prevent SSRF attacks.
7683
- * Resolves DNS and validates resolved IPs to prevent DNS rebinding attacks.
7865
+ *
7866
+ * Resolves DNS and rejects the URL if any resolved IP is private. This is a PRE-FLIGHT check, and on
7867
+ * its own it does NOT stop DNS rebinding: the address it validates is not the address the eventual
7868
+ * socket dials, because the HTTP client resolves the hostname again when it connects. A name that
7869
+ * answers with a public IP here and a private one microseconds later passes this check and still
7870
+ * reaches the internal destination. `ssrfSafeLookup` below is what closes that window; this function
7871
+ * exists to fail fast, to produce a specific user-facing error, and to check the things a connect-time
7872
+ * hook cannot see - the scheme, and the literal address the caller actually typed.
7684
7873
  *
7685
7874
  * @param url - The URL to validate
7686
7875
  * @returns Object with valid flag and optional error message
@@ -7692,15 +7881,16 @@ async function validateUrlForFetch(url) {
7692
7881
  valid: false,
7693
7882
  error: "URL must use HTTP or HTTPS protocol"
7694
7883
  };
7695
- if (isPrivateOrInternalHostname(parsed.hostname)) return {
7884
+ const hostname = stripIpv6Brackets(parsed.hostname);
7885
+ if (isPrivateOrInternalHostname(hostname)) return {
7696
7886
  valid: false,
7697
7887
  error: "URL points to a private or internal network"
7698
7888
  };
7699
- const isIPv4Address = /^(\d{1,3}\.){3}\d{1,3}$/.test(parsed.hostname);
7700
- const isIPv6Address = parsed.hostname.includes(":");
7889
+ const isIPv4Address = /^(\d+\.){3}\d+$/.test(hostname);
7890
+ const isIPv6Address = hostname.includes(":");
7701
7891
  if (!isIPv4Address && !isIPv6Address) try {
7702
- const ipv4Addresses = await dnsResolve4(parsed.hostname).catch(() => []);
7703
- const ipv6Addresses = await dnsResolve6(parsed.hostname).catch(() => []);
7892
+ const ipv4Addresses = await dnsResolve4(hostname).catch(() => []);
7893
+ const ipv6Addresses = await dnsResolve6(hostname).catch(() => []);
7704
7894
  const allAddresses = [...ipv4Addresses, ...ipv6Addresses];
7705
7895
  if (allAddresses.length === 0) return {
7706
7896
  valid: false,
@@ -7724,6 +7914,93 @@ async function validateUrlForFetch(url) {
7724
7914
  };
7725
7915
  }
7726
7916
  }
7917
+ /** Marks a refusal that came from the connect-time hook, so callers can tell it from a DNS failure. */
7918
+ const SSRF_BLOCKED_CODE = "ERR_SSRF_BLOCKED_ADDRESS";
7919
+ /**
7920
+ * DNS lookup that re-validates at CONNECT time. THIS is the check that stops DNS rebinding.
7921
+ *
7922
+ * The pre-flight in `validateUrlForFetch` resolves the hostname and then hands the NAME to the HTTP
7923
+ * client, which resolves it a second time before opening the socket. Those are two different
7924
+ * resolutions, so an attacker who controls the authoritative server can answer the first with a
7925
+ * public address and the second with `169.254.169.254` - a textbook TOCTOU, and the reason the old
7926
+ * "this prevents DNS rebinding attacks" comment on that function was false.
7927
+ *
7928
+ * Installing this as the agent's `lookup` removes the gap rather than narrowing it: Node passes the
7929
+ * address this function returns straight to `net.connect`, so the IP that gets validated is by
7930
+ * construction the IP the socket dials. There is no third resolution in between for a rebind to win.
7931
+ *
7932
+ * Refuses if ANY resolved address is private, matching `validateUrlForFetch` - a dual-stack host must
7933
+ * not become reachable just because Node happened to prefer the healthy family this time.
7934
+ *
7935
+ * The two match in POLICY but deliberately differ in RESOLVER: `validateUrlForFetch` uses
7936
+ * `dns.resolve4`/`resolve6` (c-ares, straight to DNS) while this uses `dns.lookup` (getaddrinfo, which
7937
+ * also reads `/etc/hosts` and the OS cache). They can therefore legitimately disagree - an
7938
+ * `/etc/hosts` entry passes the pre-flight and is refused here. That is fail-closed and the right way
7939
+ * round, but it means "the URL validated and then the connection was blocked" is reachable in normal
7940
+ * operation and is NOT evidence that the pin is broken.
7941
+ */
7942
+ const ssrfSafeLookup = (hostname, options, callback) => {
7943
+ const resolveOptions = {
7944
+ ...options,
7945
+ all: true
7946
+ };
7947
+ dns.lookup(hostname, resolveOptions, (err, addresses) => {
7948
+ if (err) {
7949
+ callback(err, "", 0);
7950
+ return;
7951
+ }
7952
+ if (!addresses || addresses.length === 0) {
7953
+ const empty = /* @__PURE__ */ new Error(`No addresses resolved for hostname ${hostname}`);
7954
+ empty.code = "ENOTFOUND";
7955
+ callback(empty, "", 0);
7956
+ return;
7957
+ }
7958
+ const privateHit = addresses.find((entry) => isPrivateIP(entry.address));
7959
+ if (privateHit) {
7960
+ const blocked = /* @__PURE__ */ new Error(`Blocked connection to private IP address (${privateHit.address}) for hostname ${hostname}`);
7961
+ blocked.code = SSRF_BLOCKED_CODE;
7962
+ callback(blocked, "", 0);
7963
+ return;
7964
+ }
7965
+ if (options.all) {
7966
+ callback(null, addresses);
7967
+ return;
7968
+ }
7969
+ callback(null, addresses[0].address, addresses[0].family);
7970
+ });
7971
+ };
7972
+ /**
7973
+ * Agents that pin every connection through `ssrfSafeLookup`.
7974
+ *
7975
+ * Module-level singletons so sockets and their validation are shared, and deliberately WITHOUT
7976
+ * `keepAlive`: a pooled socket outlives the lookup that approved it, and reusing one would skip the
7977
+ * connect-time check on every request after the first.
7978
+ *
7979
+ * Any caller fetching an attacker-influenced URL should pass BOTH - the scheme is not known until
7980
+ * after redirects, and an https URL that 302s to http would otherwise slip past a single agent.
7981
+ *
7982
+ * SCOPE - these protect callers that fetch through Node's http/https stack, which today means
7983
+ * `fetchAndParseURL` in `ingest.ts` and nothing else. That does NOT mean other fetchers are unpinned:
7984
+ * the webfetch LLM tool (`services/src/llm/tools/implementation/webfetch/plainFetch.ts`) reaches the
7985
+ * same guarantee by a different route, and a reader should not go looking for a gap there that is
7986
+ * already closed. It vets via `ssrfGuard.ts`, then for http rewrites the URL's hostname to the vetted
7987
+ * IP while preserving `Host`, and sets `redirect: 'error'` so a public origin cannot 302-pivot at all.
7988
+ * That is connect-by-IP under global `fetch` - so the technique IS available there, and an
7989
+ * undici `Agent` with a validating `connect` is not required to pin.
7990
+ *
7991
+ * The honest residual over there is narrower: https keeps the hostname and leans on TLS validation, so
7992
+ * what is left is an SYN-level probe oracle rather than a rebind to a private target. The reason to
7993
+ * use the agents here instead is that axios drives a manual redirect chain over an arbitrary number of
7994
+ * hops and schemes, where per-request agent selection is the tractable place to enforce this.
7995
+ */
7996
+ const ssrfSafeHttpAgent = new http.Agent({
7997
+ lookup: ssrfSafeLookup,
7998
+ keepAlive: false
7999
+ });
8000
+ const ssrfSafeHttpsAgent = new https.Agent({
8001
+ lookup: ssrfSafeLookup,
8002
+ keepAlive: false
8003
+ });
7727
8004
  const URL_REGEX = /https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?/gi;
7728
8005
  function detectURLs(string) {
7729
8006
  return string.match(URL_REGEX) || [];
@@ -7735,36 +8012,153 @@ function urlExists(stringWithPossibleUrl) {
7735
8012
  return detectURLs(stringWithPossibleUrl.replace(/\n/g, " ").replace(/,/g, " "));
7736
8013
  }
7737
8014
  const URL_FETCH_TIMEOUT_MS = 1e4;
8015
+ /**
8016
+ * Redirect hops followed before giving up. Deliberately far below axios's own default of 21: every
8017
+ * hop costs a DNS resolution plus a request, and no legitimate document needs more than a couple.
8018
+ */
8019
+ const MAX_REDIRECTS = 5;
8020
+ /**
8021
+ * Hard ceiling on a fetched body. A SAFETY NET against an unbounded response, not a policy limit -
8022
+ * `createFabFile` still enforces the `MaxFileSize` admin setting afterwards. Set generously (the
8023
+ * same 50MB as the Slack attachment ceiling) so it can never refuse something the app would accept;
8024
+ * without it axios defaults to `maxContentLength: -1`, i.e. buffer whatever the server sends, and
8025
+ * `@datalake add <link>` takes URLs from anyone who can type in a Slack channel.
8026
+ */
8027
+ const URL_MAX_RESPONSE_BYTES = 52428800;
8028
+ /**
8029
+ * PDF test against the URL's PATH only. The previous form (`url.split('.').pop().startsWith('pdf')`)
8030
+ * also matched a query string, so `?doc=report.pdf` on an HTML page was fetched as a PDF.
8031
+ */
8032
+ function isPdfUrl(url) {
8033
+ return new URL(url).pathname.toLowerCase().endsWith(".pdf");
8034
+ }
8035
+ /**
8036
+ * True when the body opens with the PDF signature.
8037
+ *
8038
+ * Closes the door `isPdfUrl` cannot reach: a download endpoint with no `.pdf` in its path, served as
8039
+ * `application/octet-stream`, produced neither a Content-Type signal nor an extension signal and was
8040
+ * decoded as text - the same `toString('utf8')` corruption the Content-Type fallback exists to
8041
+ * prevent, arriving through the one remaining door. `/download?id=123` and `Content-Disposition`
8042
+ * attachment links are exactly this shape.
8043
+ *
8044
+ * Checked at offset 0 only. The PDF spec tolerates leading bytes before the header and readers scan
8045
+ * ahead for it, but scanning here would mean sniffing arbitrary attacker-supplied content to
8046
+ * RE-CLASSIFY it, and a false positive sends a real text document into the PDF parser. The strict
8047
+ * check costs nothing on well-formed files, which is every file this has been observed to affect.
8048
+ *
8049
+ * Deliberately consulted ONLY on the generic-binary branch, never to override a server that stated a
8050
+ * type. A server declaring `text/html` while sending PDF bytes is a different (and unobserved) bug,
8051
+ * and overriding an explicit Content-Type is a wider behaviour change than this fix needs.
8052
+ */
8053
+ function hasPdfMagicBytes(body) {
8054
+ return body.subarray(0, 5).toString("latin1") === "%PDF-";
8055
+ }
8056
+ /**
8057
+ * Strip embedded credentials before a URL is written to a log.
8058
+ *
8059
+ * `https://user:pass@host/doc` is a legitimate paste, and this function is reached from the Slack
8060
+ * `@datalake add` path and the LLM URL-fetch path - both of which take URLs from whoever can type in
8061
+ * a channel or a chat. The FETCH still uses the original URL; only what is recorded is redacted, and a
8062
+ * log line outlives the message that produced it.
8063
+ *
8064
+ * MUST STAY IN SYNC with `sanitizeUrlForRecord` in `apps/client/server/slack/dataLakeLinkIngest.ts`,
8065
+ * which does the same job for the PERSISTED provenance record. Deliberately duplicated rather than
8066
+ * shared: exporting this would change `fab-pipeline`'s public surface, which its own `index.test.ts`
8067
+ * pins as an explicit list of names.
8068
+ */
8069
+ function redactUrlCredentials(raw) {
8070
+ try {
8071
+ const parsed = new URL(raw);
8072
+ if (!parsed.username && !parsed.password) return raw;
8073
+ parsed.username = "";
8074
+ parsed.password = "";
8075
+ return parsed.toString();
8076
+ } catch {
8077
+ return "[unparseable url]";
8078
+ }
8079
+ }
8080
+ /** Last path segment, used only as a display-name fallback when a page has no `<title>`. */
8081
+ function lastPathSegment(url) {
8082
+ try {
8083
+ return new URL(url).pathname.split("/").filter(Boolean).pop() ?? url;
8084
+ } catch {
8085
+ return url.split("/")?.pop() ?? url;
8086
+ }
8087
+ }
8088
+ /**
8089
+ * Fetch one URL without following redirects, so the caller can SSRF-validate each hop itself.
8090
+ *
8091
+ * SECURITY: this is why `maxRedirects: 0` is set rather than left at axios's default. Validating
8092
+ * only the URL the user supplied is not enough - axios would follow the redirect chain internally,
8093
+ * so any public host could answer `302 Location: http://169.254.169.254/latest/meta-data/` and the
8094
+ * guard would never see the address actually fetched.
8095
+ *
8096
+ * SECURITY: the agents are the OTHER half, and the two guard different attacks. Per-hop
8097
+ * `validateUrlForFetch` judges each address the chain names; the agents' `ssrfSafeLookup` judges the
8098
+ * IP each socket actually dials. Without the agents a hostname that passes validation and then
8099
+ * re-resolves to a private address on connect - DNS rebinding - reaches the internal destination with
8100
+ * every URL-level check having passed. Both are needed: the pre-flight sees the scheme and the typed
8101
+ * literal, the lookup sees the truth at connect time.
8102
+ *
8103
+ * `timeoutMs` is the budget REMAINING for the whole operation, not a fresh per-hop allowance - see
8104
+ * the deadline in `fetchAndParseURL`.
8105
+ */
8106
+ async function fetchWithoutRedirects(url, timeoutMs) {
8107
+ return axios.get(url, {
8108
+ httpAgent: ssrfSafeHttpAgent,
8109
+ httpsAgent: ssrfSafeHttpsAgent,
8110
+ proxy: false,
8111
+ responseType: "arraybuffer",
8112
+ timeout: timeoutMs,
8113
+ maxRedirects: 0,
8114
+ maxContentLength: URL_MAX_RESPONSE_BYTES,
8115
+ maxBodyLength: URL_MAX_RESPONSE_BYTES,
8116
+ validateStatus: (status) => status >= 200 && status < 300 || status >= 300 && status < 400
8117
+ });
8118
+ }
7738
8119
  async function fetchAndParseURL(url, { logger }) {
7739
8120
  logger.updateMetadata({ failedUrl: null });
7740
8121
  try {
7741
- const ssrfValidation = await validateUrlForFetch(url);
7742
- if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
7743
- let urlMimeType = "text/plain";
7744
- if (url.split(".")?.pop()?.startsWith("pdf")) urlMimeType = "application/pdf";
7745
- const response = await axios.get(url, {
7746
- responseType: ["application/pdf"].includes(urlMimeType) ? "arraybuffer" : "text",
7747
- timeout: URL_FETCH_TIMEOUT_MS
7748
- });
7749
- const cheerio = await import("cheerio");
7750
- const htmlContent = response.data;
7751
- const $ = cheerio.load(htmlContent);
7752
- const title = $("title").text() || url.split("/")?.pop();
7753
- let urlContent = null;
7754
- switch (urlMimeType) {
7755
- case "application/pdf":
7756
- urlContent = Buffer.from(response.data);
7757
- break;
7758
- default: {
7759
- let textContent = "";
7760
- $("body").find("p").each((index, element) => {
7761
- textContent += $(element).text() + "\n";
7762
- });
7763
- urlContent = textContent || htmlContent;
7764
- break;
7765
- }
8122
+ let currentUrl = url;
8123
+ let response = null;
8124
+ const deadline = Date.now() + URL_FETCH_TIMEOUT_MS;
8125
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
8126
+ const ssrfValidation = await validateUrlForFetch(currentUrl);
8127
+ if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
8128
+ const remainingMs = deadline - Date.now();
8129
+ if (remainingMs <= 0) throw new Error("Timed out while following redirects for URL");
8130
+ response = await fetchWithoutRedirects(currentUrl, remainingMs);
8131
+ if (!(response.status >= 300 && response.status < 400)) break;
8132
+ const location = response.headers?.location;
8133
+ if (typeof location !== "string" || location.length === 0) break;
8134
+ if (hop === MAX_REDIRECTS) throw new Error(`Too many redirects (more than ${MAX_REDIRECTS}) while fetching URL`);
8135
+ currentUrl = new URL(location, currentUrl).toString();
8136
+ }
8137
+ if (!response) throw new Error("URL fetch produced no response");
8138
+ const body = Buffer.isBuffer(response.data) ? response.data : Buffer.from(response.data);
8139
+ const contentType = String(response.headers?.["content-type"] ?? "").toLowerCase();
8140
+ const isGenericBinary = !contentType || contentType.includes("application/octet-stream") || contentType.includes("binary/octet-stream");
8141
+ const urlMimeType = contentType.includes("application/pdf") || isGenericBinary && (isPdfUrl(currentUrl) || hasPdfMagicBytes(body)) ? "application/pdf" : "text/plain";
8142
+ let title;
8143
+ let urlContent;
8144
+ if (urlMimeType === "application/pdf") {
8145
+ urlContent = body;
8146
+ title = lastPathSegment(currentUrl);
8147
+ } else {
8148
+ const cheerio = await import("cheerio");
8149
+ const htmlContent = body.toString("utf8");
8150
+ const $ = cheerio.load(htmlContent);
8151
+ title = $("title").text() || lastPathSegment(currentUrl);
8152
+ let textContent = "";
8153
+ $("body").find("p").each((index, element) => {
8154
+ textContent += $(element).text() + "\n";
8155
+ });
8156
+ urlContent = textContent || htmlContent;
7766
8157
  }
7767
- logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${url}`);
8158
+ const original = redactUrlCredentials(url);
8159
+ const final = redactUrlCredentials(currentUrl);
8160
+ const fetched = original === final ? original : `${original} -> ${final}`;
8161
+ logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
7768
8162
  return {
7769
8163
  title,
7770
8164
  textContent: urlContent,
@@ -7772,7 +8166,7 @@ async function fetchAndParseURL(url, { logger }) {
7772
8166
  ext: mime.extension(urlMimeType) || null
7773
8167
  };
7774
8168
  } catch (error) {
7775
- logger.updateMetadata({ failedUrl: url });
8169
+ logger.updateMetadata({ failedUrl: redactUrlCredentials(url) });
7776
8170
  logger.debug("Error fetching or parsing URL:", error);
7777
8171
  throw error;
7778
8172
  }
@@ -10730,7 +11124,6 @@ var ReActAgent = class extends EventEmitter {
10730
11124
  this.context = {
10731
11125
  ...context,
10732
11126
  maxIterations: context.maxIterations ?? 50,
10733
- maxTokens: context.maxTokens ?? 4096,
10734
11127
  temperature: context.temperature ?? .7
10735
11128
  };
10736
11129
  this.repeatedCallGuard = new RepeatedCallGuard(context.repeatedCallGuard);
@@ -10775,7 +11168,7 @@ var ReActAgent = class extends EventEmitter {
10775
11168
  */
10776
11169
  async completeText(prompt, options = {}) {
10777
11170
  const model = options.model ?? this.context.model;
10778
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11171
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10779
11172
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10780
11173
  let text = "";
10781
11174
  await this.context.llm.complete(model, [{
@@ -10816,7 +11209,7 @@ var ReActAgent = class extends EventEmitter {
10816
11209
  this.isReadOnlyToolFn = options.isReadOnlyTool ?? defaultIsReadOnlyTool;
10817
11210
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
10818
11211
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10819
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11212
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10820
11213
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
10821
11214
  const maxHistoryIterations = options.maxHistoryIterations ?? 4;
10822
11215
  let iterations = 0;
@@ -10857,7 +11250,8 @@ var ReActAgent = class extends EventEmitter {
10857
11250
  totalCacheWriteTokens: this.totalCacheWriteTokens > 0 ? this.totalCacheWriteTokens : void 0,
10858
11251
  iterations,
10859
11252
  toolCalls: this.toolCallCount,
10860
- reachedMaxIterations: false
11253
+ reachedMaxIterations: false,
11254
+ finishReason: this.lastStopReason
10861
11255
  }
10862
11256
  };
10863
11257
  this.emit("complete", result);
@@ -11091,6 +11485,7 @@ var ReActAgent = class extends EventEmitter {
11091
11485
  toolCalls: this.toolCallCount,
11092
11486
  reachedMaxIterations,
11093
11487
  reachedMaxTotalTokens: reachedMaxTotalTokens || void 0,
11488
+ finishReason: this.lastStopReason,
11094
11489
  averageConfidence: avgConfidence,
11095
11490
  minConfidence,
11096
11491
  confidenceLog: this.confidenceLog.length > 0 ? this.confidenceLog : void 0
@@ -11322,7 +11717,7 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
11322
11717
  async runIteration(query, options = {}) {
11323
11718
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
11324
11719
  const temperature = options.temperature ?? this.context.temperature ?? .7;
11325
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11720
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
11326
11721
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
11327
11722
  if (!this.iterationInitialized) {
11328
11723
  if (!query) throw new Error("query is required on the first call to runIteration(). Pass the user query, or call fromCheckpoint() first to resume.");
@@ -15107,6 +15502,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15107
15502
  buildApiKeyTable: () => buildApiKeyTable,
15108
15503
  buildSupersededIndex: () => buildSupersededIndex,
15109
15504
  buildThinkingParams: () => buildThinkingParams,
15505
+ cachedTokensFromUsage: () => cachedTokensFromUsage,
15110
15506
  catalogLifecycles: () => catalogLifecycles,
15111
15507
  catalogSuccessors: () => catalogSuccessors,
15112
15508
  checkStaleModelReferences: () => checkStaleModelReferences,
@@ -15142,6 +15538,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15142
15538
  resolveSuccessorChain: () => resolveSuccessorChain,
15143
15539
  setModelCatalogProvider: () => setModelCatalogProvider,
15144
15540
  setModelPriceRowsProvider: () => setModelPriceRowsProvider,
15541
+ splitCacheInclusiveInput: () => splitCacheInclusiveInput,
15145
15542
  stripAllToolBlocks: () => stripAllToolBlocks,
15146
15543
  stripToolDependentMessages: () => stripToolDependentMessages,
15147
15544
  toKimiEffort: () => toKimiEffort,
@@ -15227,6 +15624,44 @@ async function runWithConcurrency(tasks, limit) {
15227
15624
  return results;
15228
15625
  }
15229
15626
  /**
15627
+ * Attaches a tool call's outcome onto its `toolsUsed` entry so it survives into
15628
+ * `promptMeta.functionCalls.returnValue`/`.success` (see ChatCompletionProcess.ts's mapper
15629
+ * and utils.ts's `replayableToolCalls`, which gates a whole replay path on at least one
15630
+ * recorded `returnValue`). Every backend pushes a `toolsUsed` entry before executing the
15631
+ * tool and only learns the real outcome a few lines later - this is the merge-back.
15632
+ */
15633
+ /**
15634
+ * Cap applied to a persisted `returnValue` before it reaches Mongo (chars, not bytes). Also the
15635
+ * cap on what a later turn replays back to the model for this call (utils.ts's Priority 2
15636
+ * reconstruction reads the same persisted, already-truncated value) - this is not a
15637
+ * persistence-only limit, it is what a continued conversation sees of an older tool result too.
15638
+ */
15639
+ const MAX_RECORDED_TOOL_RESULT_CHARS = 8e3;
15640
+ const TOOL_RESULT_TRUNCATION_NOTICE = "\n[tool result truncated]";
15641
+ function truncateToolResult(observation) {
15642
+ if (observation.length <= 8e3) return observation;
15643
+ return observation.slice(0, MAX_RECORDED_TOOL_RESULT_CHARS) + TOOL_RESULT_TRUNCATION_NOTICE;
15644
+ }
15645
+ /**
15646
+ * Finds the NOT-YET-STAMPED `toolsUsed` entry for this call (id-first, falling back to the
15647
+ * first unstamped entry with the same name when a provider omitted an id) and attaches the
15648
+ * truncated result. The "not yet stamped" filter (`success === undefined`) is what keeps this
15649
+ * safe across recursive tool-call turns: `toolsUsed` accumulates across rounds, so a later
15650
+ * turn's call to the same tool must not overwrite an earlier turn's already-recorded entry.
15651
+ * Never throws - a call with no matching entry (e.g. one filtered out before execution) is a
15652
+ * silent no-op, matching the tolerance the existing normalize-by-id sites already have.
15653
+ */
15654
+ function recordToolResult(toolsUsed, call, observation, success) {
15655
+ const wantId = call.id || void 0;
15656
+ const entry = toolsUsed.find((t) => t.success === void 0 && t.name === call.name && (wantId === void 0 || t.id === wantId));
15657
+ if (!entry) {
15658
+ Logger.globalInstance.debug(`[recordToolResult] no unstamped toolsUsed entry matched name=${call.name} id=${call.id ?? "(none)"} - result not recorded`);
15659
+ return;
15660
+ }
15661
+ entry.returnValue = truncateToolResult(String(observation));
15662
+ entry.success = success;
15663
+ }
15664
+ /**
15230
15665
  * Helper function to handle tool result streaming for artifact-generating tools
15231
15666
  * This ensures tools like recharts that generate artifacts are streamed immediately
15232
15667
  * rather than waiting for recursive completion calls.
@@ -15742,6 +16177,25 @@ function getCachingAdapter(backend) {
15742
16177
  return ADAPTERS[backend] || new NoOpCachingAdapter();
15743
16178
  }
15744
16179
  /**
16180
+ * Flatten a system message's content to the plain text a provider should receive.
16181
+ *
16182
+ * System content is usually a string, but the assembly pipeline can hand back an
16183
+ * array of content blocks. Both Anthropic-family adapters previously coerced that
16184
+ * array with `JSON.stringify` (or bare `String()`), which sent the model literal
16185
+ * JSON syntax - escaped quotes and `type`/`text` keys - in place of the prompt.
16186
+ *
16187
+ * Only `text` blocks are read: Anthropic's `system` accepts text only, so an image
16188
+ * or tool block there is already invalid and is dropped rather than serialized into
16189
+ * the prompt. Blocks are joined on a newline, matching how separate system messages
16190
+ * are joined by the callers.
16191
+ */
16192
+ function systemContentToText(content) {
16193
+ if (content === void 0 || content === null) return "";
16194
+ if (typeof content === "string") return content;
16195
+ if (!Array.isArray(content)) return "";
16196
+ return content.filter((block) => block?.type === "text").map((block) => block.text ?? "").filter((text) => text.trim() !== "").join("\n");
16197
+ }
16198
+ /**
15745
16199
  * max_tokens floor for adaptive reasoning models (Claude 4.7+/Opus 5). These
15746
16200
  * models self-manage extended thinking *within* max_tokens, which is a ceiling
15747
16201
  * (they stop at end_turn), not a target - so a larger floor costs nothing on
@@ -15805,8 +16259,17 @@ function reasonsWithinOutputBudget(modelInfo) {
15805
16259
  * be consumed entirely by reasoning, leaving an empty visible reply.
15806
16260
  */
15807
16261
  function resolveOutputMaxTokens({ requested, fallback, modelInfo, modelMaxOutputTokens }) {
15808
- const preferred = requested ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
15809
- return Math.min(preferred, modelMaxOutputTokens);
16262
+ const preferred = usableTokenCount(requested) ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
16263
+ const cap = usableTokenCount(modelMaxOutputTokens);
16264
+ return cap === void 0 ? preferred : Math.min(preferred, cap);
16265
+ }
16266
+ /**
16267
+ * Token counts reaching this module come from catalog rows and external callers, so they are
16268
+ * only trustworthy when finite and positive - a zero or negative cap would clamp the budget
16269
+ * to an unsendable value just as surely as NaN poisons it.
16270
+ */
16271
+ function usableTokenCount(value) {
16272
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
15810
16273
  }
15811
16274
  /**
15812
16275
  * Builds the correct thinking parameters for an Anthropic API call based on
@@ -15995,6 +16458,12 @@ const SLOW_MODEL_REQUEST_TIMEOUT_MS = 12e4;
15995
16458
  */
15996
16459
  const ANTHROPIC_NONSTREAMING_MAX_TOKENS = 21e3;
15997
16460
  /**
16461
+ * Output budget used when the caller names none. Only applies to models that do NOT spend
16462
+ * reasoning inside the output budget - resolveOutputMaxTokens sizes the rest for the model,
16463
+ * so this value stays the historical one and non-reasoning behavior is unchanged.
16464
+ */
16465
+ const DEFAULT_ANTHROPIC_MAX_TOKENS = 4096;
16466
+ /**
15998
16467
  * Accumulated multi-turn cache token total. Undefined when zero so turns
15999
16468
  * without cache activity keep the pre-cache callback shape.
16000
16469
  */
@@ -16605,7 +17074,8 @@ var AnthropicBackend = class {
16605
17074
  const systemMessages = messages.filter((m) => m.role === "system");
16606
17075
  const blocks = [];
16607
17076
  for (const sm of systemMessages) {
16608
- const text = typeof sm.content === "string" ? sm.content : JSON.stringify(sm.content);
17077
+ const text = systemContentToText(sm.content);
17078
+ if (text.trim() === "") continue;
16609
17079
  if (sm.cache === true) blocks.push({
16610
17080
  type: "text",
16611
17081
  text,
@@ -16644,7 +17114,11 @@ var AnthropicBackend = class {
16644
17114
  let { useCount: toolUseCount, resultCount: toolResultCount } = countToolBlocks(filteredMessages);
16645
17115
  if (toolUseCount > 0 || toolResultCount > 0) {
16646
17116
  this.logger.debug(`[Pre-API #6181] Sending ${filteredMessages.length} messages with ${toolUseCount} tool_use and ${toolResultCount} tool_result blocks`);
16647
- if (toolUseCount !== toolResultCount) {
17117
+ if (!options.tools?.length) {
17118
+ this.logger.warn(`[Pre-API #6181] Tool blocks present (tool_use: ${toolUseCount}, tool_result: ${toolResultCount}) but no tools offered this turn. Stripping all tool blocks.`);
17119
+ filteredMessages = stripAllToolBlocks(filteredMessages, this.logger);
17120
+ ({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
17121
+ } else if (toolUseCount !== toolResultCount) {
16648
17122
  this.logger.warn(`[Pre-API #6181] Tool block mismatch! tool_use: ${toolUseCount}, tool_result: ${toolResultCount}. Attempting auto-repair...`);
16649
17123
  filteredMessages = ensureToolPairingIntegrity(filteredMessages, this.logger);
16650
17124
  ({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
@@ -16654,9 +17128,15 @@ var AnthropicBackend = class {
16654
17128
  }
16655
17129
  }
16656
17130
  }
17131
+ const currentModelInfo = this.modelRecordFor(model);
16657
17132
  const apiParams = {
16658
17133
  model,
16659
- max_tokens: options.maxTokens ?? 4096,
17134
+ max_tokens: currentModelInfo ? resolveOutputMaxTokens({
17135
+ requested: options.maxTokens,
17136
+ fallback: DEFAULT_ANTHROPIC_MAX_TOKENS,
17137
+ modelInfo: currentModelInfo,
17138
+ modelMaxOutputTokens: currentModelInfo.max_tokens
17139
+ }) : options.maxTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS,
16660
17140
  messages: filteredMessages.map((m) => ({
16661
17141
  role: m.role === "user" ? "user" : "assistant",
16662
17142
  content: m.content
@@ -16696,7 +17176,6 @@ var AnthropicBackend = class {
16696
17176
  }
16697
17177
  }
16698
17178
  const requestExtraHeaders = anyMessageCacheControlled ? { "anthropic-beta": "prompt-caching-2024-07-31" } : void 0;
16699
- const currentModelInfo = this.modelRecordFor(model);
16700
17179
  if (currentModelInfo?.can_think) {
16701
17180
  const thinkingOptions = options;
16702
17181
  const isQuestMaster = thinkingOptions.questMaster === true;
@@ -17077,6 +17556,10 @@ var AnthropicBackend = class {
17077
17556
  if (!parsedParams) {
17078
17557
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
17079
17558
  if (entry) entry.arguments = "{}";
17559
+ recordToolResult(toolsUsed, {
17560
+ id,
17561
+ name
17562
+ }, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
17080
17563
  continue;
17081
17564
  }
17082
17565
  resolvedTools.push({
@@ -17144,6 +17627,10 @@ var AnthropicBackend = class {
17144
17627
  toolsUsed
17145
17628
  });
17146
17629
  });
17630
+ recordToolResult(toolsUsed, {
17631
+ id: outcome.id,
17632
+ name: outcome.name
17633
+ }, resultStr, true);
17147
17634
  this.pushToolMessages(messages, {
17148
17635
  id: toolId,
17149
17636
  name: outcome.name,
@@ -17151,16 +17638,22 @@ var AnthropicBackend = class {
17151
17638
  }, resultStr);
17152
17639
  } else {
17153
17640
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17641
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17154
17642
  this.logger.error("[Tool Execution] Tool failed", {
17155
17643
  model,
17156
17644
  toolName: outcome.name,
17157
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
17645
+ error: errorMessage
17158
17646
  });
17647
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
17648
+ recordToolResult(toolsUsed, {
17649
+ id: outcome.id,
17650
+ name: outcome.name
17651
+ }, observation, false);
17159
17652
  this.pushToolMessages(messages, {
17160
17653
  id: toolId,
17161
17654
  name: outcome.name,
17162
17655
  parameters: outcome.parameters
17163
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
17656
+ }, observation);
17164
17657
  }
17165
17658
  }
17166
17659
  await cb(["\n\n"], { toolsUsed });
@@ -17297,7 +17790,13 @@ var AnthropicBackend = class {
17297
17790
  isMcpTool,
17298
17791
  streaming: false
17299
17792
  }, messages);
17300
- if (!parsedParams) continue;
17793
+ if (!parsedParams) {
17794
+ recordToolResult(toolsUsed, {
17795
+ id,
17796
+ name
17797
+ }, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
17798
+ continue;
17799
+ }
17301
17800
  resolvedTools.push({
17302
17801
  id: id ?? "",
17303
17802
  name,
@@ -17359,6 +17858,10 @@ var AnthropicBackend = class {
17359
17858
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17360
17859
  await cb(results, { toolsUsed });
17361
17860
  });
17861
+ recordToolResult(toolsUsed, {
17862
+ id: outcome.id,
17863
+ name: outcome.name
17864
+ }, resultStr, true);
17362
17865
  this.pushToolMessages(messages, {
17363
17866
  id: toolId,
17364
17867
  name: outcome.name,
@@ -17366,16 +17869,22 @@ var AnthropicBackend = class {
17366
17869
  }, resultStr);
17367
17870
  } else {
17368
17871
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17872
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17369
17873
  this.logger.error("[Tool Execution] Tool failed (non-streaming)", {
17370
17874
  model,
17371
17875
  toolName: outcome.name,
17372
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
17876
+ error: errorMessage
17373
17877
  });
17878
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
17879
+ recordToolResult(toolsUsed, {
17880
+ id: outcome.id,
17881
+ name: outcome.name
17882
+ }, observation, false);
17374
17883
  this.pushToolMessages(messages, {
17375
17884
  id: toolId,
17376
17885
  name: outcome.name,
17377
17886
  parameters: outcome.parameters
17378
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
17887
+ }, observation);
17379
17888
  }
17380
17889
  }
17381
17890
  this.logger.info("[Tool Execution] Making recursive call after tool execution (non-streaming)", {
@@ -17629,7 +18138,7 @@ var AnthropicBackend = class {
17629
18138
  consolidateSystemMessages(messages) {
17630
18139
  const systemMessages = messages.filter((m) => m.role === "system");
17631
18140
  if (systemMessages.length === 0) return void 0;
17632
- return systemMessages.map((m) => m.content).join("\n");
18141
+ return systemMessages.map((m) => systemContentToText(m.content)).filter((text) => text.trim() !== "").join("\n");
17633
18142
  }
17634
18143
  isToolUseEvent(event) {
17635
18144
  return typeof event === "object" && event !== null && "type" in event && event.type === "tool_use" && "name" in event && "input" in event && "id" in event;
@@ -17772,6 +18281,15 @@ var BaseBedrockBackend = class {
17772
18281
  const command = new InvokeModelWithResponseStreamCommand(input);
17773
18282
  return this._bedrockRuntime.send(command, { abortSignal });
17774
18283
  }
18284
+ /**
18285
+ * The reasoning blocks the just-translated assistant turn produced, cleared as they are
18286
+ * taken. A backend whose provider signs thinking blocks overrides this so the tool loop
18287
+ * below can replay them onto the assistant turns it rebuilds; providers that sign nothing
18288
+ * keep the default. @see AnthropicBedrockBackend.takeReasoningBlocks
18289
+ */
18290
+ takeReasoningBlocks() {
18291
+ return [];
18292
+ }
17775
18293
  updateClientForModel(model) {
17776
18294
  const requiredRegion = this.getRegionForModel(model);
17777
18295
  this._options.region = requiredRegion;
@@ -17797,8 +18315,14 @@ var BaseBedrockBackend = class {
17797
18315
  }, callback, toolsUsed);
17798
18316
  return;
17799
18317
  }
17800
- const messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
18318
+ let messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
17801
18319
  const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
18320
+ if (!options.tools?.length) {
18321
+ if (messagesWithFormat.some((m) => Array.isArray(m.content) && m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"))) {
18322
+ Logger.globalInstance.warn("[BaseBedrockBackend Pre-API #6181] Tool blocks present but no tools offered this turn. Stripping all tool blocks.");
18323
+ messagesWithFormat = stripAllToolBlocks(messagesWithFormat, Logger.globalInstance);
18324
+ }
18325
+ }
17802
18326
  let formattedMessages = this.formatMessages(messagesWithFormat);
17803
18327
  let input = this.getPayload(model, formattedMessages, options);
17804
18328
  const contextWindow = this.getModelContextWindow(model);
@@ -17932,6 +18456,12 @@ var BaseBedrockBackend = class {
17932
18456
  });
17933
18457
  } catch {
17934
18458
  Logger.globalInstance.warn("[BaseBedrockBackend] Tool parameter parse error, skipping tool:", name);
18459
+ const entry = toolsUsed.find((t) => t.name === name && t.id === id);
18460
+ if (entry) entry.arguments = "{}";
18461
+ recordToolResult(toolsUsed, {
18462
+ id,
18463
+ name
18464
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
17935
18465
  }
17936
18466
  }
17937
18467
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -17959,32 +18489,40 @@ var BaseBedrockBackend = class {
17959
18489
  parameters: resolvedTools[i].parameters,
17960
18490
  error: outcome.error
17961
18491
  });
18492
+ const roundReasoningBlocks = this.takeReasoningBlocks();
17962
18493
  for (const outcome of outcomes) if (outcome.ok) {
17963
18494
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17964
18495
  await callback(results, buildCompletionInfo());
17965
18496
  });
18497
+ const resultStr = outcome.result.toString();
18498
+ recordToolResult(toolsUsed, {
18499
+ id: outcome.id,
18500
+ name: outcome.name
18501
+ }, resultStr, true);
17966
18502
  this.pushToolMessages(messages, {
17967
18503
  id: outcome.id,
17968
18504
  name: outcome.name,
17969
18505
  parameters: outcome.parameters
17970
- }, outcome.result.toString());
18506
+ }, resultStr, roundReasoningBlocks);
17971
18507
  } else {
17972
18508
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17973
18509
  if (isAbortError(outcome.error)) throw outcome.error;
17974
18510
  Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${outcome.name} failed:`, outcome.error instanceof Error ? outcome.error.message : String(outcome.error));
18511
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
18512
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
18513
+ recordToolResult(toolsUsed, {
18514
+ id: outcome.id,
18515
+ name: outcome.name
18516
+ }, observation, false);
17975
18517
  this.pushToolMessages(messages, {
17976
18518
  id: outcome.id,
17977
18519
  name: outcome.name,
17978
18520
  parameters: outcome.parameters
17979
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
18521
+ }, observation, roundReasoningBlocks);
17980
18522
  }
17981
18523
  await callback(["\n\n"], buildCompletionInfo());
17982
18524
  await this.complete(model, messages, {
17983
18525
  ...options,
17984
- thinking: {
17985
- enabled: false,
17986
- budget_tokens: 0
17987
- },
17988
18526
  tool_choice: "auto",
17989
18527
  _internal: {
17990
18528
  ...options._internal,
@@ -18028,35 +18566,38 @@ var BaseBedrockBackend = class {
18028
18566
  if (options.executeTools !== false) {
18029
18567
  const executable = toolChoices.map((tc) => tc.tool).filter((tool) => tool.id && tool.name && options.tools?.some((o) => o.toolSchema.name === tool.name));
18030
18568
  if (executable.length > 0) {
18569
+ const roundReasoningBlocks = this.takeReasoningBlocks();
18031
18570
  for (const { id, name, parameters } of executable) {
18032
18571
  const toolFn = options.tools?.find((o) => o.toolSchema.name === name)?.toolFn;
18033
18572
  if (!toolFn) continue;
18034
18573
  const safeParameters = parameters || "{}";
18035
18574
  let result;
18575
+ let succeeded = true;
18036
18576
  try {
18037
18577
  result = await toolFn(JSON.parse(safeParameters));
18038
18578
  } catch (err) {
18039
18579
  if (err instanceof PermissionDeniedError) throw err;
18040
18580
  if (isAbortError(err)) throw err;
18041
18581
  Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${name} failed:`, err instanceof Error ? err.message : String(err));
18582
+ succeeded = false;
18042
18583
  result = `Error processing ${name} tool: ${err instanceof Error ? err.message : "Unknown error"}`;
18043
18584
  }
18044
18585
  await handleToolResultStreaming(name, result, async (results) => {
18045
18586
  await callback(results, buildCompletionInfo());
18046
18587
  });
18588
+ recordToolResult(toolsUsed, {
18589
+ id,
18590
+ name
18591
+ }, result.toString(), succeeded);
18047
18592
  this.pushToolMessages(messages, {
18048
18593
  id,
18049
18594
  name,
18050
18595
  parameters
18051
- }, result.toString());
18596
+ }, result.toString(), roundReasoningBlocks);
18052
18597
  }
18053
18598
  await callback(["\n\n"], buildCompletionInfo());
18054
18599
  await this.complete(model, messages, {
18055
18600
  ...options,
18056
- thinking: {
18057
- enabled: false,
18058
- budget_tokens: 0
18059
- },
18060
18601
  tool_choice: "auto",
18061
18602
  _internal: {
18062
18603
  ...options._internal,
@@ -18151,6 +18692,9 @@ function isToolUseContentBlock(content_block) {
18151
18692
  function isThinkingContentBlock(content_block) {
18152
18693
  return isRecord(content_block) && content_block.type === "thinking";
18153
18694
  }
18695
+ function isRedactedThinkingContentBlock(content_block) {
18696
+ return isRecord(content_block) && content_block.type === "redacted_thinking";
18697
+ }
18154
18698
  function isTextDelta(delta) {
18155
18699
  return isRecord(delta) && delta.type === "text_delta" && "text" in delta;
18156
18700
  }
@@ -18160,6 +18704,9 @@ function isInputJsonDelta(delta) {
18160
18704
  function isThinkingDelta(delta) {
18161
18705
  return isRecord(delta) && delta.type === "thinking_delta" && "thinking" in delta;
18162
18706
  }
18707
+ function isSignatureDelta(delta) {
18708
+ return isRecord(delta) && delta.type === "signature_delta" && "signature" in delta;
18709
+ }
18163
18710
  function isMessageStart(chunk) {
18164
18711
  return isRecord(chunk) && chunk.type === "message_start";
18165
18712
  }
@@ -18187,12 +18734,35 @@ const TEMPERATURE_ONLY_MODELS = [
18187
18734
  ];
18188
18735
  var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18189
18736
  isInThinkingBlock = false;
18737
+ /**
18738
+ * Reasoning blocks of the assistant turn currently being translated, indexed by the
18739
+ * stream's content-block index. Reset at `message_start` and consumed by
18740
+ * `takeReasoningBlocks` when that turn is rebuilt for a tool continuation.
18741
+ */
18742
+ assistantReasoningBlocks = [];
18190
18743
  /** Catalog view of the model being completed; see DispatchModel. */
18191
18744
  _dispatch = new DispatchModel();
18192
18745
  setDispatchModel(info) {
18193
18746
  this._dispatch.set(info);
18194
18747
  }
18195
18748
  /**
18749
+ * The reasoning blocks the last translated assistant turn produced, cleared as they
18750
+ * are taken. Anthropic requires a `tool_use` assistant turn to replay its own signed
18751
+ * thinking blocks whenever extended thinking is active on the turn, and an adaptive
18752
+ * model thinks on every turn whether or not the request asked it to - so dropping
18753
+ * them is what makes the synthesis round of a multi-round tool turn come back empty.
18754
+ *
18755
+ * Taken once per provider turn. base.ts takes them before its tool loop and hands the
18756
+ * same array to every assistant message it rebuilds for that round, because a parallel
18757
+ * round splits one provider turn across several synthetic turns and each of them has to
18758
+ * carry the reasoning.
18759
+ */
18760
+ takeReasoningBlocks() {
18761
+ const blocks = this.assistantReasoningBlocks.filter((b) => b != null);
18762
+ this.assistantReasoningBlocks = [];
18763
+ return blocks;
18764
+ }
18765
+ /**
18196
18766
  * The record the payload is shaped from: the adapter table first, then the
18197
18767
  * catalog for a model the table never listed. Table-first keeps every
18198
18768
  * currently-dispatched Bedrock id on exactly today's payload.
@@ -18609,11 +19179,13 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18609
19179
  content: ""
18610
19180
  };
18611
19181
  }).filter((m) => m.content !== "" && (Array.isArray(m.content) ? m.content.length > 0 : true));
18612
- let systemMessage = messages.filter((m) => m.role === "system" && m.content).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
18613
- if (!options.omitIdentityReminder) {
18614
- const modelIdentity = `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
18615
- systemMessage = systemMessage ? `${systemMessage}\n${modelIdentity}` : modelIdentity;
18616
- }
19182
+ const systemBlocks = messages.filter((m) => m.role === "system" && m.content).map((m) => ({
19183
+ text: systemContentToText(m.content),
19184
+ cache: m.cache === true
19185
+ })).filter((block) => block.text.trim() !== "");
19186
+ const identityReminder = options.omitIdentityReminder ? null : `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
19187
+ let systemMessage = systemBlocks.map((block) => block.text).join("\n");
19188
+ if (identityReminder) systemMessage = systemMessage ? `${systemMessage}\n${identityReminder}` : identityReminder;
18617
19189
  const modelId = model.includes(":") || model.startsWith("global.") || model.startsWith("us.") || model.startsWith("anthropic.") ? model : `anthropic.${model}`;
18618
19190
  const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : 4096;
18619
19191
  const body = {
@@ -18621,7 +19193,27 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18621
19193
  max_tokens: maxTokens,
18622
19194
  messages: filteredMessages
18623
19195
  };
18624
- if (systemMessage) body.system = systemMessage;
19196
+ const cacheStrategy = options.cacheStrategy;
19197
+ const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
19198
+ if (Boolean(cacheStrategy?.enableCaching) && modelSupportsCaching && systemBlocks.some((block) => block.cache)) {
19199
+ const cacheControl = {
19200
+ type: "ephemeral",
19201
+ ...cacheStrategy?.cacheTTL === "1h" ? { ttl: cacheStrategy.cacheTTL } : {}
19202
+ };
19203
+ const blocks = systemBlocks.map((block) => block.cache ? {
19204
+ type: "text",
19205
+ text: block.text,
19206
+ cache_control: cacheControl
19207
+ } : {
19208
+ type: "text",
19209
+ text: block.text
19210
+ });
19211
+ if (identityReminder) blocks.push({
19212
+ type: "text",
19213
+ text: identityReminder
19214
+ });
19215
+ body.system = blocks;
19216
+ } else if (systemMessage) body.system = systemMessage;
18625
19217
  if (options.tools?.length) body.tools = this.formatTools(options.tools);
18626
19218
  if (typeof options.temperature === "number" && !this.omitsSamplingParams(model)) body.temperature = options.temperature;
18627
19219
  if (typeof options.topP === "number" && !TEMPERATURE_ONLY_MODELS.includes(model) && !this.omitsSamplingParams(model)) body.top_p = options.topP;
@@ -18649,8 +19241,6 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18649
19241
  topP: options.topP
18650
19242
  }
18651
19243
  }, null, 2)}`);
18652
- const cacheStrategy = options.cacheStrategy;
18653
- const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
18654
19244
  if (cacheStrategy?.enableCaching && modelSupportsCaching) {
18655
19245
  const cachedBody = getCachingAdapter(ModelBackend.Bedrock).applyCaching(body, cacheStrategy);
18656
19246
  Object.assign(body, cachedBody);
@@ -18674,9 +19264,11 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18674
19264
  formatMessages(messages) {
18675
19265
  return messages.reduce((cur, value) => {
18676
19266
  const previousMessage = cur[cur.length - 1];
18677
- if (previousMessage && value.role === previousMessage.role) {
18678
- if (previousMessage.content === value.content) return cur;
18679
- else if (!Array.isArray(previousMessage.content)) {
19267
+ if (previousMessage && value.role === previousMessage.role && previousMessage.cache !== true) {
19268
+ if (previousMessage.content === value.content) {
19269
+ if (value.cache === true) previousMessage.cache = true;
19270
+ return cur;
19271
+ } else if (!Array.isArray(previousMessage.content)) {
18680
19272
  const lastIndex = cur.length - 1;
18681
19273
  const prevContent = typeof cur[lastIndex].content === "string" ? cur[lastIndex].content : "";
18682
19274
  const currContent = typeof value.content === "string" ? value.content : "";
@@ -18696,6 +19288,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18696
19288
  });
18697
19289
  if (contentArray.length > 0) cur[lastIndex].content = contentArray;
18698
19290
  }
19291
+ if (value.cache === true) cur[lastIndex].cache = true;
18699
19292
  } else {
18700
19293
  if (typeof value.content !== "string") {
18701
19294
  cur.push(value);
@@ -18707,6 +19300,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18707
19300
  type: "text",
18708
19301
  text: textContent
18709
19302
  }];
19303
+ if (value.cache === true) previousMessage.cache = true;
18710
19304
  }
18711
19305
  return cur;
18712
19306
  }
@@ -18717,11 +19311,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18717
19311
  translateChunk(model, chunk) {
18718
19312
  try {
18719
19313
  const response = chunk;
18720
- const thinkingBlocks = response.content.filter((c) => c.type === "thinking");
18721
- if (thinkingBlocks.length > 0) console.warn(`[AnthropicBedrockBackend] Unexpected thinking blocks in response`, {
18722
- thinkingBlockCount: thinkingBlocks.length,
18723
- thinkingLengths: thinkingBlocks.map((b) => b.thinking?.length || 0)
18724
- });
19314
+ this.assistantReasoningBlocks = response.content.filter((c) => c.type === "thinking" || c.type === "redacted_thinking");
19315
+ if (this.assistantReasoningBlocks.length > 0) console.log(`[AnthropicBedrockBackend] Captured ${this.assistantReasoningBlocks.length} reasoning block(s) for tool continuation`);
18725
19316
  const textContent = response.content.filter((item) => item.type === "text").map((item) => item.text || "").join("");
18726
19317
  const toolUseBlocks = response.content.filter((item) => item.type === "tool_use");
18727
19318
  let choice;
@@ -18775,6 +19366,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18775
19366
  try {
18776
19367
  if (isMessageStart(chunk)) {
18777
19368
  this.isInThinkingBlock = false;
19369
+ this.assistantReasoningBlocks = [];
18778
19370
  choice = {
18779
19371
  chunkText: "",
18780
19372
  usage: {
@@ -18796,8 +19388,12 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18796
19388
  };
18797
19389
  else if (isThinkingContentBlock(contentBlock)) {
18798
19390
  this.isInThinkingBlock = true;
19391
+ this.assistantReasoningBlocks[chunk.index] = {
19392
+ ...contentBlock,
19393
+ thinking: contentBlock.thinking ?? ""
19394
+ };
18799
19395
  choice.chunkText = "<think>";
18800
- }
19396
+ } else if (isRedactedThinkingContentBlock(contentBlock)) this.assistantReasoningBlocks[chunk.index] = { ...contentBlock };
18801
19397
  } else if (isContentBlockDelta(chunk)) {
18802
19398
  choice = {
18803
19399
  status: "stream",
@@ -18807,7 +19403,14 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18807
19403
  const delta = chunk.delta;
18808
19404
  if (isTextDelta(delta)) choice.chunkText = delta.text;
18809
19405
  else if (isInputJsonDelta(delta)) choice.chunkText = delta.partial_json;
18810
- else if (isThinkingDelta(delta)) choice.chunkText = delta.thinking;
19406
+ else if (isThinkingDelta(delta)) {
19407
+ choice.chunkText = delta.thinking;
19408
+ const block = this.assistantReasoningBlocks[chunk.index];
19409
+ if (block?.type === "thinking") block.thinking += delta.thinking;
19410
+ } else if (isSignatureDelta(delta)) {
19411
+ const block = this.assistantReasoningBlocks[chunk.index];
19412
+ if (block?.type === "thinking") block.signature = delta.signature;
19413
+ }
18811
19414
  } else if (isContentBlockStop(chunk)) {
18812
19415
  choice = {
18813
19416
  status: "stream",
@@ -18866,7 +19469,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18866
19469
  name: tool.name,
18867
19470
  input: JSON.parse(tool.parameters || "{}")
18868
19471
  };
18869
- const assistantContent = thinkingBlocks && thinkingBlocks.length > 0 ? [...thinkingBlocks, toolUseBlock] : [toolUseBlock];
19472
+ const reasoningBlocks = thinkingBlocks?.length ? thinkingBlocks : this.takeReasoningBlocks();
19473
+ const assistantContent = reasoningBlocks.length > 0 ? [...reasoningBlocks, toolUseBlock] : [toolUseBlock];
18870
19474
  messages.push({
18871
19475
  role: "assistant",
18872
19476
  content: assistantContent
@@ -19998,7 +20602,7 @@ var BFLBackend = class {
19998
20602
  input: 400,
19999
20603
  output: 400
20000
20604
  } },
20001
- description: "FLUX Kontext Pro - Advanced image-to-image transformation with high-quality output and versatile editing capabilities.",
20605
+ description: "FLUX Pro 1.1 - Professional quality image generation with strong prompt adherence and fast turnaround.",
20002
20606
  supportsSafetyTolerance: true,
20003
20607
  rank: 1
20004
20608
  },
@@ -20064,7 +20668,7 @@ var BFLBackend = class {
20064
20668
  input: 350,
20065
20669
  output: 350
20066
20670
  } },
20067
- description: "FLUX Kontext Max - Premium image-to-image transformation with maximum quality and advanced editing capabilities.",
20671
+ description: "FLUX Kontext Pro - Advanced image-to-image transformation with high-quality output and versatile editing capabilities.",
20068
20672
  supportsSafetyTolerance: true,
20069
20673
  rank: 4
20070
20674
  },
@@ -20080,7 +20684,7 @@ var BFLBackend = class {
20080
20684
  input: 450,
20081
20685
  output: 450
20082
20686
  } },
20083
- description: "BlackForest Labs FLUX Kontext Max - Premium image-to-image transformation with maximum quality and capabilities",
20687
+ description: "FLUX Kontext Max - Premium image-to-image transformation with maximum quality and capabilities.",
20084
20688
  supportsSafetyTolerance: true,
20085
20689
  rank: 5
20086
20690
  }
@@ -20538,6 +21142,7 @@ var GeminiBackend = class {
20538
21142
  const toolCallCount = options._internal?.toolCallCount ?? 0;
20539
21143
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
20540
21144
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
21145
+ const liveToolUseIds = new Set(options._internal?.liveToolUseIds ?? []);
20541
21146
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
20542
21147
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
20543
21148
  this.logger.warn(`[Gemini] Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -20552,7 +21157,7 @@ var GeminiBackend = class {
20552
21157
  const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
20553
21158
  const systemInstruction = messagesWithFormat.filter((message) => message.role === "system").map((message) => message.content).join("\n");
20554
21159
  const nonsystemMessages = messagesWithFormat.filter((message) => message.role !== "system");
20555
- const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages);
21160
+ const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages, liveToolUseIds);
20556
21161
  const generationConfig = this.getGenerationConfig(modelInfo, options);
20557
21162
  const tools = !options.tools?.length ? void 0 : [{ functionDeclarations: options.tools.map((tool) => {
20558
21163
  const params = pick(tool.toolSchema.parameters, "type", "properties", "required");
@@ -20679,22 +21284,32 @@ var GeminiBackend = class {
20679
21284
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20680
21285
  await callback(results, { toolsUsed });
20681
21286
  });
21287
+ const resultContent = JSON.stringify({ result: outcome.result });
21288
+ recordToolResult(toolsUsed, {
21289
+ id: outcome.toolCall.id,
21290
+ name: outcome.toolCall.name
21291
+ }, resultContent, true);
20682
21292
  messages.push({
20683
21293
  role: "tool",
20684
21294
  content: [{
20685
21295
  type: "tool_result",
20686
- content: JSON.stringify({ result: outcome.result }),
21296
+ content: resultContent,
20687
21297
  tool_use_id: outcome.toolCall.id
20688
21298
  }]
20689
21299
  });
20690
21300
  } else {
20691
21301
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20692
21302
  this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
21303
+ const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
21304
+ recordToolResult(toolsUsed, {
21305
+ id: outcome.toolCall.id,
21306
+ name: outcome.toolCall.name
21307
+ }, errorContent, false);
20693
21308
  messages.push({
20694
21309
  role: "tool",
20695
21310
  content: [{
20696
21311
  type: "tool_result",
20697
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
21312
+ content: errorContent,
20698
21313
  tool_use_id: outcome.toolCall.id
20699
21314
  }]
20700
21315
  });
@@ -20706,7 +21321,8 @@ var GeminiBackend = class {
20706
21321
  ...options._internal,
20707
21322
  toolCallCount: toolCallCount + 1,
20708
21323
  accumInputTokens: accumInputTokens + turnInputTokens,
20709
- accumOutputTokens: accumOutputTokens + turnOutputTokens
21324
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
21325
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20710
21326
  }
20711
21327
  }, callback, toolsUsed);
20712
21328
  } else {
@@ -20805,22 +21421,32 @@ var GeminiBackend = class {
20805
21421
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20806
21422
  await callback(results, { toolsUsed });
20807
21423
  });
21424
+ const resultContent = JSON.stringify({ result: outcome.result });
21425
+ recordToolResult(toolsUsed, {
21426
+ id: outcome.toolCall.id,
21427
+ name: outcome.toolCall.name
21428
+ }, resultContent, true);
20808
21429
  messages.push({
20809
21430
  role: "tool",
20810
21431
  content: [{
20811
21432
  type: "tool_result",
20812
- content: JSON.stringify({ result: outcome.result }),
21433
+ content: resultContent,
20813
21434
  tool_use_id: outcome.toolCall.id
20814
21435
  }]
20815
21436
  });
20816
21437
  } else {
20817
21438
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20818
21439
  this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
21440
+ const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
21441
+ recordToolResult(toolsUsed, {
21442
+ id: outcome.toolCall.id,
21443
+ name: outcome.toolCall.name
21444
+ }, errorContent, false);
20819
21445
  messages.push({
20820
21446
  role: "tool",
20821
21447
  content: [{
20822
21448
  type: "tool_result",
20823
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
21449
+ content: errorContent,
20824
21450
  tool_use_id: outcome.toolCall.id
20825
21451
  }]
20826
21452
  });
@@ -20832,7 +21458,8 @@ var GeminiBackend = class {
20832
21458
  ...options._internal,
20833
21459
  toolCallCount: toolCallCount + 1,
20834
21460
  accumInputTokens: accumInputTokens + turnInputTokens,
20835
- accumOutputTokens: accumOutputTokens + turnOutputTokens
21461
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
21462
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20836
21463
  }
20837
21464
  }, callback, toolsUsed);
20838
21465
  } else {
@@ -20841,8 +21468,9 @@ var GeminiBackend = class {
20841
21468
  }
20842
21469
  }
20843
21470
  }
20844
- formatMessagesIntoGeminiContent(messages) {
21471
+ formatMessagesIntoGeminiContent(messages, liveToolUseIds) {
20845
21472
  const toolUseIdToName = /* @__PURE__ */ new Map();
21473
+ const droppedToolUseIds = /* @__PURE__ */ new Set();
20846
21474
  return messages.map((message) => {
20847
21475
  const mapRole = (role) => {
20848
21476
  switch (role) {
@@ -20856,25 +21484,41 @@ var GeminiBackend = class {
20856
21484
  role: mapRole(message.role),
20857
21485
  parts: [{ text: message.content }]
20858
21486
  };
20859
- if (message.content?.[0].type === "text") return {
21487
+ const hasToolUse = Array.isArray(message.content) && message.content.some((item) => item.type === "tool_use");
21488
+ if (!hasToolUse && message.content?.[0].type === "text") return {
20860
21489
  role: mapRole(message.role),
20861
21490
  parts: [{ text: (message.content?.[0]).text }]
20862
21491
  };
20863
- if (message.content?.[0].type === "image") return {
21492
+ if (!hasToolUse && message.content?.[0].type === "image") return {
20864
21493
  role: mapRole(message.role),
20865
21494
  parts: [{ inlineData: {
20866
21495
  mimeType: (message.content?.[0]).source.media_type,
20867
21496
  data: (message.content?.[0]).source.data
20868
21497
  } }]
20869
21498
  };
20870
- if (message.content?.[0].type === "tool_use") {
20871
- const parts = message.content.filter((item) => item.type === "tool_use").map((toolUse, index) => {
21499
+ if (hasToolUse) {
21500
+ const toolUseBlocks = message.content.filter((item) => item.type === "tool_use");
21501
+ const textParts = message.content.filter((item) => item.type === "text").map((item) => ({ text: item.text }));
21502
+ if (!toolUseBlocks.some((t) => liveToolUseIds.has(t.id)) && /^gemini-3(\D|$)/.test(this.currentModel) && !toolUseBlocks[0]?.thought_signature) {
21503
+ this.logger.warn("[Gemini] Dropping replayed tool_use block(s) with no thought_signature on the first call:", {
21504
+ names: toolUseBlocks.map((t) => t.name),
21505
+ messageRole: message.role
21506
+ });
21507
+ toolUseBlocks.forEach((t) => droppedToolUseIds.add(t.id));
21508
+ if (textParts.length === 0) return null;
21509
+ return {
21510
+ role: mapRole(message.role),
21511
+ parts: textParts
21512
+ };
21513
+ }
21514
+ const parts = [...textParts];
21515
+ parts.push(...toolUseBlocks.map((toolUse, index) => {
20872
21516
  toolUseIdToName.set(toolUse.id, toolUse.name);
20873
21517
  const part = { functionCall: {
20874
21518
  name: toolUse.name,
20875
21519
  args: toolUse.input
20876
21520
  } };
20877
- if (index === 0 && toolUse.thought_signature) {
21521
+ if (index === 0) if (toolUse.thought_signature) {
20878
21522
  part.thoughtSignature = toolUse.thought_signature;
20879
21523
  part.thought_signature = toolUse.thought_signature;
20880
21524
  this.logger.debug("[Gemini] Including thought_signature in request (both formats):", {
@@ -20882,29 +21526,26 @@ var GeminiBackend = class {
20882
21526
  id: toolUse.id,
20883
21527
  position: "first"
20884
21528
  });
20885
- } else if (index === 0 && !toolUse.thought_signature) {
20886
- this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
20887
- name: toolUse.name,
20888
- id: toolUse.id,
20889
- messageRole: message.role
20890
- });
20891
- this.logger.warn("[Gemini] This may cause a 400 error with Gemini 3 Pro");
20892
- }
21529
+ } else this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
21530
+ name: toolUse.name,
21531
+ id: toolUse.id
21532
+ });
20893
21533
  return part;
20894
- });
21534
+ }));
20895
21535
  return {
20896
21536
  role: mapRole(message.role),
20897
21537
  parts
20898
21538
  };
20899
21539
  }
20900
21540
  if (message.content?.[0].type === "tool_result") {
20901
- const toolResult = message.content[0];
21541
+ const parts = message.content.filter((item) => item.type === "tool_result").filter((toolResult) => !droppedToolUseIds.has(toolResult.tool_use_id)).map((toolResult) => ({ functionResponse: {
21542
+ name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
21543
+ response: { result: toolResult.content }
21544
+ } }));
21545
+ if (parts.length === 0) return null;
20902
21546
  return {
20903
21547
  role: mapRole(message.role),
20904
- parts: [{ functionResponse: {
20905
- name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
20906
- response: { result: (message.content?.[0]).content }
20907
- } }]
21548
+ parts
20908
21549
  };
20909
21550
  }
20910
21551
  return null;
@@ -20991,6 +21632,54 @@ var GeminiBackend = class {
20991
21632
  }
20992
21633
  };
20993
21634
  /**
21635
+ * The cache-inclusive-to-cache-exclusive conversion, shared by every adapter whose
21636
+ * provider reports cached tokens as a SUBSET of the prompt count.
21637
+ *
21638
+ * getTextModelCost expects Anthropic's convention: `inputTokens` counts only uncached
21639
+ * tokens and cache reads bill separately at their own (much cheaper) rate. Anthropic
21640
+ * and Claude-on-Bedrock deliver that natively. OpenAI and Moonshot do not - their
21641
+ * prompt total already CONTAINS the cached tokens - so those adapters must subtract
21642
+ * here before forwarding, or settlement double-bills the cached portion.
21643
+ *
21644
+ * Must stay in sync with the disjoint-fields assumption documented at the settlement
21645
+ * site in ChatCompletionProcess.
21646
+ */
21647
+ /**
21648
+ * Split a cache-INCLUSIVE prompt total into the disjoint pair CompletionInfo carries.
21649
+ *
21650
+ * Forwarding the cached count without subtracting double-bills it; forwarding nothing
21651
+ * charges the full input rate on tokens the provider billed at a fraction of it.
21652
+ * Subtracting is the only split that bills what the provider actually charged.
21653
+ *
21654
+ * Clamped at zero: if a feed ever reports more cached than prompt tokens, a negative
21655
+ * input count would silently credit the user.
21656
+ */
21657
+ function splitCacheInclusiveInput(totalPromptTokens, cacheReadTokens) {
21658
+ if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
21659
+ const cached = Math.min(cacheReadTokens, totalPromptTokens);
21660
+ return {
21661
+ inputTokens: Math.max(0, totalPromptTokens - cached),
21662
+ cacheReadInputTokens: cached
21663
+ };
21664
+ }
21665
+ /**
21666
+ * Cached prompt tokens from a raw provider usage object, across every spelling in use:
21667
+ * OpenAI Chat Completions nests them under `prompt_tokens_details`, the OpenAI
21668
+ * Responses API under `input_tokens_details`, and Moonshot publishes a flat
21669
+ * `cached_tokens` alongside the OpenAI-shaped nesting. Reading only one spelling
21670
+ * silently bills every cache hit on the other transports at the full input rate.
21671
+ */
21672
+ function cachedTokensFromUsage(usage) {
21673
+ if (!usage) return 0;
21674
+ const candidates = [
21675
+ usage.cached_tokens,
21676
+ usage.prompt_tokens_details?.cached_tokens,
21677
+ usage.input_tokens_details?.cached_tokens
21678
+ ];
21679
+ for (const value of candidates) if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
21680
+ return 0;
21681
+ }
21682
+ /**
20994
21683
  * Request shaping for Moonshot's Kimi models. Kept separate from kimiBackend's
20995
21684
  * transport so every "which parameter does this id accept" rule is one pure
20996
21685
  * function with a test, rather than a conditional buried in a 400-line complete().
@@ -21379,7 +22068,7 @@ var KimiBackend = class {
21379
22068
  if (!(response instanceof Stream)) {
21380
22069
  const streamedText = [];
21381
22070
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from the Moonshot API");
21382
- const turnCacheReadTokens = this.cachedTokensOf(response.usage);
22071
+ const turnCacheReadTokens = cachedTokensFromUsage(response.usage);
21383
22072
  for (const c of response.choices) {
21384
22073
  if (!c.message) continue;
21385
22074
  const reasoningContent = c.message.reasoning_content;
@@ -21411,6 +22100,10 @@ var KimiBackend = class {
21411
22100
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
21412
22101
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
21413
22102
  if (entry) entry.arguments = "{}";
22103
+ recordToolResult(toolsUsed, {
22104
+ id: toolCall.id,
22105
+ name: toolCall.function.name
22106
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
21414
22107
  }
21415
22108
  }
21416
22109
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21438,18 +22131,30 @@ var KimiBackend = class {
21438
22131
  parameters: resolvedTools[i].parameters,
21439
22132
  error: outcome.error
21440
22133
  });
21441
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21442
- id: outcome.id,
21443
- name: outcome.name,
21444
- parameters: outcome.parameters
21445
- }, outcome.result.toString());
21446
- else {
22134
+ for (const outcome of outcomes) if (outcome.ok) {
22135
+ const resultStr = outcome.result.toString();
22136
+ recordToolResult(toolsUsed, {
22137
+ id: outcome.id,
22138
+ name: outcome.name
22139
+ }, resultStr, true);
22140
+ this.pushToolMessages(messages, {
22141
+ id: outcome.id,
22142
+ name: outcome.name,
22143
+ parameters: outcome.parameters
22144
+ }, resultStr);
22145
+ } else {
21447
22146
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22147
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
22148
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
22149
+ recordToolResult(toolsUsed, {
22150
+ id: outcome.id,
22151
+ name: outcome.name
22152
+ }, observation, false);
21448
22153
  this.pushToolMessages(messages, {
21449
22154
  id: outcome.id,
21450
22155
  name: outcome.name,
21451
22156
  parameters: outcome.parameters
21452
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22157
+ }, observation);
21453
22158
  }
21454
22159
  await this.complete(model, messages, {
21455
22160
  ...options,
@@ -21465,7 +22170,7 @@ var KimiBackend = class {
21465
22170
  } else {
21466
22171
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21467
22172
  await callback([null], {
21468
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
22173
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
21469
22174
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21470
22175
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21471
22176
  });
@@ -21488,7 +22193,7 @@ var KimiBackend = class {
21488
22193
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
21489
22194
  const totalCacheReadTokens = accumCacheReadTokens + turnCacheReadTokens;
21490
22195
  await callback(streamedText, {
21491
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
22196
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
21492
22197
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21493
22198
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21494
22199
  cacheStats,
@@ -21507,7 +22212,7 @@ var KimiBackend = class {
21507
22212
  if (chunk.usage) {
21508
22213
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
21509
22214
  outputTokens += chunk.usage?.completion_tokens || 0;
21510
- const chunkCached = this.cachedTokensOf(chunk.usage);
22215
+ const chunkCached = cachedTokensFromUsage(chunk.usage);
21511
22216
  if (chunkCached > 0) cachedTokensFromStream = chunkCached;
21512
22217
  }
21513
22218
  chunk?.choices.forEach((c) => {
@@ -21537,7 +22242,7 @@ var KimiBackend = class {
21537
22242
  if (streamedText.some((t) => t)) sawAnyText = true;
21538
22243
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
21539
22244
  await callback(streamedText, {
21540
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22245
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21541
22246
  outputTokens: accumOutputTokens + outputTokens,
21542
22247
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21543
22248
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -21545,7 +22250,7 @@ var KimiBackend = class {
21545
22250
  }
21546
22251
  if (isInThinkingBlock) {
21547
22252
  await callback(["</think>"], {
21548
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22253
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21549
22254
  outputTokens: accumOutputTokens + outputTokens,
21550
22255
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21551
22256
  });
@@ -21562,7 +22267,7 @@ var KimiBackend = class {
21562
22267
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
21563
22268
  }
21564
22269
  if (nativeFormat && func.length === 0) await callback([], {
21565
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22270
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21566
22271
  outputTokens: accumOutputTokens + outputTokens,
21567
22272
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21568
22273
  responseFormatMode: "native",
@@ -21598,6 +22303,10 @@ var KimiBackend = class {
21598
22303
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
21599
22304
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
21600
22305
  if (entry) entry.arguments = "{}";
22306
+ recordToolResult(toolsUsed, {
22307
+ id,
22308
+ name
22309
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
21601
22310
  }
21602
22311
  }
21603
22312
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21625,18 +22334,30 @@ var KimiBackend = class {
21625
22334
  parameters: resolvedTools[i].parameters,
21626
22335
  error: outcome.error
21627
22336
  });
21628
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21629
- id: outcome.id,
21630
- name: outcome.name,
21631
- parameters: outcome.parameters
21632
- }, outcome.result.toString());
21633
- else {
22337
+ for (const outcome of outcomes) if (outcome.ok) {
22338
+ const resultStr = outcome.result.toString();
22339
+ recordToolResult(toolsUsed, {
22340
+ id: outcome.id,
22341
+ name: outcome.name
22342
+ }, resultStr, true);
22343
+ this.pushToolMessages(messages, {
22344
+ id: outcome.id,
22345
+ name: outcome.name,
22346
+ parameters: outcome.parameters
22347
+ }, resultStr);
22348
+ } else {
21634
22349
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22350
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
22351
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
22352
+ recordToolResult(toolsUsed, {
22353
+ id: outcome.id,
22354
+ name: outcome.name
22355
+ }, observation, false);
21635
22356
  this.pushToolMessages(messages, {
21636
22357
  id: outcome.id,
21637
22358
  name: outcome.name,
21638
22359
  parameters: outcome.parameters
21639
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22360
+ }, observation);
21640
22361
  }
21641
22362
  await this.complete(model, messages, {
21642
22363
  ...options,
@@ -21651,52 +22372,13 @@ var KimiBackend = class {
21651
22372
  } else {
21652
22373
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21653
22374
  await callback([null], {
21654
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22375
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21655
22376
  outputTokens: accumOutputTokens + outputTokens,
21656
22377
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21657
22378
  });
21658
22379
  }
21659
22380
  }
21660
22381
  }
21661
- /**
21662
- * Convert Moonshot's CACHE-INCLUSIVE `prompt_tokens` into the cache-EXCLUSIVE
21663
- * convention getTextModelCost expects, which is Anthropic's: `inputTokens`
21664
- * counts only uncached tokens and cache reads are billed separately at their
21665
- * own (much cheaper) rate.
21666
- *
21667
- * Verified live 2026-07-28: a repeated 1220-token prompt returned
21668
- * `prompt_tokens: 1220` WITH `cached_tokens: 1220` - the same tokens, reported
21669
- * twice, not 1220 fresh plus 1220 cached. So passing prompt_tokens through as
21670
- * inputTokens while also forwarding cacheReadInputTokens would bill the cached
21671
- * portion twice; passing it through and forwarding NOTHING (the previous
21672
- * behavior) charges the full input rate on tokens Moonshot billed at roughly a
21673
- * sixth of it - on k2.6, $0.95/MTok against $0.16. Subtracting is the only
21674
- * split that bills what the provider actually charged.
21675
- *
21676
- * Clamped at zero: if a feed ever reports more cached than prompt tokens, a
21677
- * negative input count would silently credit the user.
21678
- */
21679
- splitCachedInput(totalPromptTokens, cacheReadTokens) {
21680
- if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
21681
- const cached = Math.min(cacheReadTokens, totalPromptTokens);
21682
- return {
21683
- inputTokens: Math.max(0, totalPromptTokens - cached),
21684
- cacheReadInputTokens: cached
21685
- };
21686
- }
21687
- /**
21688
- * Cached prompt tokens from a usage object. Moonshot publishes BOTH a flat
21689
- * `usage.cached_tokens` and the nested OpenAI `prompt_tokens_details.cached_tokens`
21690
- * - confirmed live, both present with the same value - so either spelling is
21691
- * accepted and reading neither would bill every hit at the full input rate.
21692
- */
21693
- cachedTokensOf(usage) {
21694
- if (!usage) return 0;
21695
- const flat = usage.cached_tokens;
21696
- if (typeof flat === "number" && Number.isFinite(flat) && flat >= 0) return flat;
21697
- const nested = usage.prompt_tokens_details?.cached_tokens;
21698
- return typeof nested === "number" && Number.isFinite(nested) && nested >= 0 ? nested : 0;
21699
- }
21700
22382
  formatMessages(messages) {
21701
22383
  return convertMessagesToOpenAIFormat(messages);
21702
22384
  }
@@ -22001,7 +22683,7 @@ var OllamaBackend = class OllamaBackend {
22001
22683
  name: tc.name,
22002
22684
  parameters: tc.arguments || "{}"
22003
22685
  }, `Error: tool "${tc.name}" is not available. Do not call it again; answer directly or use a listed tool.`);
22004
- (await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
22686
+ const outcomes = await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
22005
22687
  let params = {};
22006
22688
  try {
22007
22689
  params = JSON.parse(tc.arguments || "{}");
@@ -22011,17 +22693,22 @@ var OllamaBackend = class OllamaBackend {
22011
22693
  }), {
22012
22694
  parallel: options.parallelToolExecution !== false,
22013
22695
  maxConcurrency: options.maxParallelTools
22014
- })).forEach((outcome, i) => {
22696
+ });
22697
+ const observations = [];
22698
+ outcomes.forEach((outcome, i) => {
22015
22699
  const { tc } = resolved[i];
22016
22700
  const params = tc.arguments || "{}";
22017
- if (outcome.ok) this.pushToolMessages(messages, {
22018
- id: tc.id,
22019
- name: tc.name,
22020
- parameters: params
22021
- }, outcome.result);
22022
- else {
22701
+ if (outcome.ok) {
22702
+ observations[i] = outcome.result;
22703
+ this.pushToolMessages(messages, {
22704
+ id: tc.id,
22705
+ name: tc.name,
22706
+ parameters: params
22707
+ }, outcome.result);
22708
+ } else {
22023
22709
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22024
22710
  const errorMsg = `Error running ${tc.name}: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
22711
+ observations[i] = errorMsg;
22025
22712
  this.pushToolMessages(messages, {
22026
22713
  id: tc.id,
22027
22714
  name: tc.name,
@@ -22029,10 +22716,12 @@ var OllamaBackend = class OllamaBackend {
22029
22716
  }, errorMsg);
22030
22717
  }
22031
22718
  });
22032
- const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }) => ({
22719
+ const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }, i) => ({
22033
22720
  name: tc.name,
22034
22721
  arguments: tc.arguments,
22035
- id: tc.id
22722
+ id: tc.id,
22723
+ returnValue: truncateToolResult(String(observations[i])),
22724
+ success: outcomes[i].ok
22036
22725
  }))];
22037
22726
  if (options.abortSignal?.aborted) {
22038
22727
  await callback([""], {
@@ -22140,12 +22829,21 @@ var OllamaBackend = class OllamaBackend {
22140
22829
  }
22141
22830
  };
22142
22831
  }
22143
- /** Normalize Ollama's native tool_calls into the shared NormalizedToolCall shape. */
22832
+ /**
22833
+ * Normalize Ollama's native tool_calls into the shared NormalizedToolCall shape.
22834
+ *
22835
+ * Ids are real uuids, not a position-derived string. A prior version keyed ids off
22836
+ * `accumulated-count + round-local-index`, but the accumulated count is measured AFTER
22837
+ * hallucinated calls are filtered out while the round-local index is assigned BEFORE that
22838
+ * filter runs, so the two can drift and mint the same id for two different real calls across
22839
+ * rounds - replayableToolCalls dedupes by id and silently drops the later one. A uuid makes
22840
+ * the whole collision class unrepresentable, matching how the other backends already mint ids.
22841
+ */
22144
22842
  normalizeToolCalls(toolCalls) {
22145
- return toolCalls.map((tc, i) => ({
22843
+ return toolCalls.map((tc) => ({
22146
22844
  name: tc.function.name,
22147
22845
  arguments: JSON.stringify(tc.function.arguments ?? {}),
22148
- id: `ollama-tool-${i}-${tc.function.name}`
22846
+ id: `ollama-tool-${v4()}`
22149
22847
  }));
22150
22848
  }
22151
22849
  /**
@@ -22183,7 +22881,7 @@ var OllamaBackend = class OllamaBackend {
22183
22881
  seen.add(key);
22184
22882
  calls.push({
22185
22883
  ...call,
22186
- id: `ollama-content-tool-${calls.length}-${call.name}`
22884
+ id: `ollama-content-tool-${v4()}`
22187
22885
  });
22188
22886
  }
22189
22887
  return calls;
@@ -22432,7 +23130,8 @@ var OpenAIBackend = class {
22432
23130
  can_stream: true,
22433
23131
  pricing: { 1047576: {
22434
23132
  input: 2 / 1e6,
22435
- output: 8 / 1e6
23133
+ output: 8 / 1e6,
23134
+ cache_read: .5 / 1e6
22436
23135
  } },
22437
23136
  supportsVision: true,
22438
23137
  supportsTools: true,
@@ -22452,7 +23151,8 @@ var OpenAIBackend = class {
22452
23151
  can_stream: true,
22453
23152
  pricing: { 1047576: {
22454
23153
  input: .4 / 1e6,
22455
- output: 1.6 / 1e6
23154
+ output: 1.6 / 1e6,
23155
+ cache_read: .1 / 1e6
22456
23156
  } },
22457
23157
  supportsVision: true,
22458
23158
  supportsTools: true,
@@ -22472,7 +23172,8 @@ var OpenAIBackend = class {
22472
23172
  can_stream: true,
22473
23173
  pricing: { 1047576: {
22474
23174
  input: .1 / 1e6,
22475
- output: .4 / 1e6
23175
+ output: .4 / 1e6,
23176
+ cache_read: .025 / 1e6
22476
23177
  } },
22477
23178
  supportsVision: true,
22478
23179
  supportsTools: true,
@@ -22535,7 +23236,8 @@ var OpenAIBackend = class {
22535
23236
  can_stream: true,
22536
23237
  pricing: { 2e5: {
22537
23238
  input: 2 / 1e6,
22538
- output: 8 / 1e6
23239
+ output: 8 / 1e6,
23240
+ cache_read: .5 / 1e6
22539
23241
  } },
22540
23242
  supportsVision: true,
22541
23243
  supportsTools: true,
@@ -22621,7 +23323,8 @@ var OpenAIBackend = class {
22621
23323
  can_think: true,
22622
23324
  pricing: { 2e5: {
22623
23325
  input: 1.1 / 1e6,
22624
- output: 4.4 / 1e6
23326
+ output: 4.4 / 1e6,
23327
+ cache_read: .275 / 1e6
22625
23328
  } },
22626
23329
  supportsVision: true,
22627
23330
  supportsImageVariation: false,
@@ -22997,7 +23700,8 @@ var OpenAIBackend = class {
22997
23700
  can_stream: true,
22998
23701
  pricing: { 8e3: {
22999
23702
  input: 2.5 / 1e6,
23000
- output: 10 / 1e6
23703
+ output: 10 / 1e6,
23704
+ cache_read: 1.25 / 1e6
23001
23705
  } },
23002
23706
  supportsVision: true,
23003
23707
  supportsImageVariation: false,
@@ -23016,7 +23720,8 @@ var OpenAIBackend = class {
23016
23720
  can_stream: true,
23017
23721
  pricing: { 8e3: {
23018
23722
  input: .15 / 1e6,
23019
- output: .6 / 1e6
23723
+ output: .6 / 1e6,
23724
+ cache_read: .075 / 1e6
23020
23725
  } },
23021
23726
  supportsVision: true,
23022
23727
  supportsImageVariation: false,
@@ -23207,6 +23912,7 @@ var OpenAIBackend = class {
23207
23912
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23208
23913
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23209
23914
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
23915
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23210
23916
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
23211
23917
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
23212
23918
  this.logger.warn(`⚠️ Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -23296,6 +24002,7 @@ var OpenAIBackend = class {
23296
24002
  let outputTokens = 0;
23297
24003
  if (!(response instanceof Stream)) {
23298
24004
  const streamedText = [];
24005
+ const totalCacheReadTokens = accumCacheReadTokens + cachedTokensFromUsage(response.usage);
23299
24006
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from OpenAI API");
23300
24007
  for (const c of response.choices) {
23301
24008
  if (!c.message) continue;
@@ -23329,6 +24036,10 @@ var OpenAIBackend = class {
23329
24036
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
23330
24037
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
23331
24038
  if (entry) entry.arguments = "{}";
24039
+ recordToolResult(toolsUsed, {
24040
+ id: toolCall.id,
24041
+ name: toolCall.function.name
24042
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23332
24043
  }
23333
24044
  }
23334
24045
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23365,6 +24076,10 @@ var OpenAIBackend = class {
23365
24076
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23366
24077
  const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
23367
24078
  streamedText[c.index] = errorMsg;
24079
+ recordToolResult(toolsUsed, {
24080
+ id: outcome.id,
24081
+ name: outcome.name
24082
+ }, errorMsg, false);
23368
24083
  this.pushToolMessages(messages, {
23369
24084
  id: outcome.id,
23370
24085
  name: outcome.name,
@@ -23385,6 +24100,10 @@ var OpenAIBackend = class {
23385
24100
  });
23386
24101
  });
23387
24102
  const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
24103
+ recordToolResult(toolsUsed, {
24104
+ id: outcome.id,
24105
+ name: outcome.name
24106
+ }, sanitizedResult, true);
23388
24107
  this.pushToolMessages(messages, {
23389
24108
  id: outcome.id,
23390
24109
  name: outcome.name,
@@ -23408,7 +24127,8 @@ var OpenAIBackend = class {
23408
24127
  ...options._internal,
23409
24128
  toolCallCount: toolCallCount + 1,
23410
24129
  accumInputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
23411
- accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0)
24130
+ accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
24131
+ accumCacheReadTokens: totalCacheReadTokens
23412
24132
  }
23413
24133
  }, recursiveCallback, toolsUsed);
23414
24134
  if (anyArtifactWasStreamed && recursiveBuffer) {
@@ -23419,7 +24139,7 @@ var OpenAIBackend = class {
23419
24139
  } else {
23420
24140
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23421
24141
  await callback([null], {
23422
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24142
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23423
24143
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23424
24144
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23425
24145
  });
@@ -23435,7 +24155,7 @@ var OpenAIBackend = class {
23435
24155
  }
23436
24156
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
23437
24157
  await callback(streamedText, {
23438
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24158
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23439
24159
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23440
24160
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23441
24161
  cacheStats,
@@ -23463,7 +24183,7 @@ var OpenAIBackend = class {
23463
24183
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
23464
24184
  outputTokens += chunk.usage?.completion_tokens || 0;
23465
24185
  if (chunk.usage.prompt_tokens_details?.cached_tokens !== void 0) {
23466
- cachedTokensFromStream = chunk.usage.prompt_tokens_details.cached_tokens;
24186
+ cachedTokensFromStream = Math.max(cachedTokensFromStream, chunk.usage.prompt_tokens_details.cached_tokens);
23467
24187
  if (cachedTokensFromStream > 0) this.logger.debug("[OpenAI] Captured cached tokens", { cachedTokens: cachedTokensFromStream });
23468
24188
  else this.logger.debug("[OpenAI] No cached tokens in chunk", { note: "possible cache miss or first request" });
23469
24189
  }
@@ -23480,7 +24200,7 @@ var OpenAIBackend = class {
23480
24200
  });
23481
24201
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
23482
24202
  await callback(streamedText, {
23483
- inputTokens: accumInputTokens + inputTokens,
24203
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23484
24204
  outputTokens: accumOutputTokens + outputTokens,
23485
24205
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23486
24206
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -23510,7 +24230,7 @@ var OpenAIBackend = class {
23510
24230
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
23511
24231
  }
23512
24232
  if ((isO1Model || func.length === 0) && options.responseFormat?.type === "json_schema") await callback([], {
23513
- inputTokens: accumInputTokens + inputTokens,
24233
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23514
24234
  outputTokens: accumOutputTokens + outputTokens,
23515
24235
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23516
24236
  cacheStats,
@@ -23548,6 +24268,10 @@ var OpenAIBackend = class {
23548
24268
  });
23549
24269
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
23550
24270
  if (entry) entry.arguments = "{}";
24271
+ recordToolResult(toolsUsed, {
24272
+ id,
24273
+ name
24274
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23551
24275
  }
23552
24276
  }
23553
24277
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23582,11 +24306,16 @@ var OpenAIBackend = class {
23582
24306
  for (const outcome of outcomes) {
23583
24307
  if (!outcome.ok) {
23584
24308
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
24309
+ const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
24310
+ recordToolResult(toolsUsed, {
24311
+ id: outcome.id,
24312
+ name: outcome.name
24313
+ }, errorMsg, false);
23585
24314
  this.pushToolMessages(messages, {
23586
24315
  id: outcome.id,
23587
24316
  name: outcome.name,
23588
24317
  parameters: outcome.parameters
23589
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
24318
+ }, errorMsg);
23590
24319
  continue;
23591
24320
  }
23592
24321
  const resultStr = outcome.result.toString();
@@ -23596,13 +24325,17 @@ var OpenAIBackend = class {
23596
24325
  thisToolHadArtifact = true;
23597
24326
  anyArtifactWasStreamed = true;
23598
24327
  await callback(results, {
23599
- inputTokens: accumInputTokens + inputTokens,
24328
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23600
24329
  outputTokens: accumOutputTokens + outputTokens,
23601
24330
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23602
24331
  cacheStats
23603
24332
  });
23604
24333
  });
23605
24334
  const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
24335
+ recordToolResult(toolsUsed, {
24336
+ id: outcome.id,
24337
+ name: outcome.name
24338
+ }, sanitizedResult, true);
23606
24339
  this.pushToolMessages(messages, {
23607
24340
  id: outcome.id,
23608
24341
  name: outcome.name,
@@ -23623,7 +24356,8 @@ var OpenAIBackend = class {
23623
24356
  ...options._internal,
23624
24357
  toolCallCount: toolCallCount + 1,
23625
24358
  accumInputTokens: accumInputTokens + inputTokens,
23626
- accumOutputTokens: accumOutputTokens + outputTokens
24359
+ accumOutputTokens: accumOutputTokens + outputTokens,
24360
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23627
24361
  }
23628
24362
  }, async (results, meta) => {
23629
24363
  for (const r of results) if (r != null) recursiveBuffer += r;
@@ -23639,13 +24373,14 @@ var OpenAIBackend = class {
23639
24373
  ...options._internal,
23640
24374
  toolCallCount: toolCallCount + 1,
23641
24375
  accumInputTokens: accumInputTokens + inputTokens,
23642
- accumOutputTokens: accumOutputTokens + outputTokens
24376
+ accumOutputTokens: accumOutputTokens + outputTokens,
24377
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23643
24378
  }
23644
24379
  }, callback, toolsUsed);
23645
24380
  } else {
23646
24381
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23647
24382
  await callback([null], {
23648
- inputTokens: accumInputTokens + inputTokens,
24383
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23649
24384
  outputTokens: accumOutputTokens + outputTokens,
23650
24385
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23651
24386
  cacheStats
@@ -23787,6 +24522,7 @@ var OpenAIBackend = class {
23787
24522
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23788
24523
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23789
24524
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
24525
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23790
24526
  const chatMessages = this.formatMessages(messages, false, model, options);
23791
24527
  const input = this.toResponsesInput(chatMessages);
23792
24528
  const reasoningEffort = this.resolveReasoningEffort(model, options);
@@ -23813,8 +24549,9 @@ var OpenAIBackend = class {
23813
24549
  let finalResponse;
23814
24550
  let inputTokens = 0;
23815
24551
  let outputTokens = 0;
24552
+ let cachedTokensFromStream = 0;
23816
24553
  for await (const event of stream) if (event.type === "response.output_text.delta") await callback([event.delta], {
23817
- inputTokens: accumInputTokens + inputTokens,
24554
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23818
24555
  outputTokens: accumOutputTokens + outputTokens,
23819
24556
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23820
24557
  });
@@ -23824,11 +24561,12 @@ var OpenAIBackend = class {
23824
24561
  if (!finalResponse) throw new Error(`OpenAI Responses stream for ${model} ended without a terminal response event`);
23825
24562
  inputTokens = finalResponse.usage?.input_tokens ?? 0;
23826
24563
  outputTokens = finalResponse.usage?.output_tokens ?? 0;
24564
+ cachedTokensFromStream = cachedTokensFromUsage(finalResponse.usage);
23827
24565
  const functionCalls = finalResponse.output.filter((item) => item.type === "function_call");
23828
24566
  if (functionCalls.length === 0) {
23829
24567
  const stopReason = normalizeOpenAIResponsesStopReason(finalResponse.incomplete_details?.reason);
23830
24568
  await callback([], {
23831
- inputTokens: accumInputTokens + inputTokens,
24569
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23832
24570
  outputTokens: accumOutputTokens + outputTokens,
23833
24571
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23834
24572
  ...stopReason ? { stopReason } : {}
@@ -23842,7 +24580,7 @@ var OpenAIBackend = class {
23842
24580
  });
23843
24581
  if (options.executeTools === false) {
23844
24582
  await callback([null], {
23845
- inputTokens: accumInputTokens + inputTokens,
24583
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23846
24584
  outputTokens: accumOutputTokens + outputTokens,
23847
24585
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23848
24586
  });
@@ -23863,6 +24601,10 @@ var OpenAIBackend = class {
23863
24601
  });
23864
24602
  } catch {
23865
24603
  this.logger.warn(`JSON parse error for ${fc.name} arguments (Responses path)`);
24604
+ recordToolResult(toolsUsed, {
24605
+ id: fc.call_id,
24606
+ name: fc.name
24607
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23866
24608
  }
23867
24609
  }
23868
24610
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23886,14 +24628,24 @@ var OpenAIBackend = class {
23886
24628
  for (let i = 0; i < batchOutcomes.length; i++) {
23887
24629
  const outcome = batchOutcomes[i];
23888
24630
  const r = resolved[i];
23889
- if (outcome.ok) this.pushToolMessages(messages, {
23890
- id: r.callId,
23891
- name: r.name,
23892
- parameters: r.args
23893
- }, outcome.result.result.toString());
23894
- else {
24631
+ if (outcome.ok) {
24632
+ const resultStr = outcome.result.result.toString();
24633
+ recordToolResult(toolsUsed, {
24634
+ id: r.callId,
24635
+ name: r.name
24636
+ }, resultStr, true);
24637
+ this.pushToolMessages(messages, {
24638
+ id: r.callId,
24639
+ name: r.name,
24640
+ parameters: r.args
24641
+ }, resultStr);
24642
+ } else {
23895
24643
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23896
24644
  const errorMsg = `Error processing ${r.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
24645
+ recordToolResult(toolsUsed, {
24646
+ id: r.callId,
24647
+ name: r.name
24648
+ }, errorMsg, false);
23897
24649
  this.pushToolMessages(messages, {
23898
24650
  id: r.callId,
23899
24651
  name: r.name,
@@ -23910,7 +24662,8 @@ var OpenAIBackend = class {
23910
24662
  ...options._internal,
23911
24663
  toolCallCount: toolCallCount + 1,
23912
24664
  accumInputTokens: accumInputTokens + inputTokens,
23913
- accumOutputTokens: accumOutputTokens + outputTokens
24665
+ accumOutputTokens: accumOutputTokens + outputTokens,
24666
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23914
24667
  }
23915
24668
  }, callback, toolsUsed);
23916
24669
  }
@@ -24268,6 +25021,10 @@ var XAIBackend = class {
24268
25021
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
24269
25022
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
24270
25023
  if (entry) entry.arguments = "{}";
25024
+ recordToolResult(toolsUsed, {
25025
+ id: toolCall.id,
25026
+ name: toolCall.function.name
25027
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
24271
25028
  }
24272
25029
  }
24273
25030
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24295,18 +25052,30 @@ var XAIBackend = class {
24295
25052
  parameters: resolvedTools[i].parameters,
24296
25053
  error: outcome.error
24297
25054
  });
24298
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24299
- id: outcome.id,
24300
- name: outcome.name,
24301
- parameters: outcome.parameters
24302
- }, outcome.result.toString());
24303
- else {
25055
+ for (const outcome of outcomes) if (outcome.ok) {
25056
+ const resultStr = outcome.result.toString();
25057
+ recordToolResult(toolsUsed, {
25058
+ id: outcome.id,
25059
+ name: outcome.name
25060
+ }, resultStr, true);
25061
+ this.pushToolMessages(messages, {
25062
+ id: outcome.id,
25063
+ name: outcome.name,
25064
+ parameters: outcome.parameters
25065
+ }, resultStr);
25066
+ } else {
24304
25067
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
25068
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
25069
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
25070
+ recordToolResult(toolsUsed, {
25071
+ id: outcome.id,
25072
+ name: outcome.name
25073
+ }, observation, false);
24305
25074
  this.pushToolMessages(messages, {
24306
25075
  id: outcome.id,
24307
25076
  name: outcome.name,
24308
25077
  parameters: outcome.parameters
24309
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25078
+ }, observation);
24310
25079
  }
24311
25080
  await this.complete(model, messages, {
24312
25081
  ...options,
@@ -24436,6 +25205,10 @@ var XAIBackend = class {
24436
25205
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
24437
25206
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
24438
25207
  if (entry) entry.arguments = "{}";
25208
+ recordToolResult(toolsUsed, {
25209
+ id,
25210
+ name
25211
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
24439
25212
  }
24440
25213
  }
24441
25214
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24463,18 +25236,30 @@ var XAIBackend = class {
24463
25236
  parameters: resolvedTools[i].parameters,
24464
25237
  error: outcome.error
24465
25238
  });
24466
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24467
- id: outcome.id,
24468
- name: outcome.name,
24469
- parameters: outcome.parameters
24470
- }, outcome.result.toString());
24471
- else {
25239
+ for (const outcome of outcomes) if (outcome.ok) {
25240
+ const resultStr = outcome.result.toString();
25241
+ recordToolResult(toolsUsed, {
25242
+ id: outcome.id,
25243
+ name: outcome.name
25244
+ }, resultStr, true);
25245
+ this.pushToolMessages(messages, {
25246
+ id: outcome.id,
25247
+ name: outcome.name,
25248
+ parameters: outcome.parameters
25249
+ }, resultStr);
25250
+ } else {
24472
25251
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
25252
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
25253
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
25254
+ recordToolResult(toolsUsed, {
25255
+ id: outcome.id,
25256
+ name: outcome.name
25257
+ }, observation, false);
24473
25258
  this.pushToolMessages(messages, {
24474
25259
  id: outcome.id,
24475
25260
  name: outcome.name,
24476
25261
  parameters: outcome.parameters
24477
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25262
+ }, observation);
24478
25263
  }
24479
25264
  await this.complete(model, messages, {
24480
25265
  ...options,
@@ -25635,6 +26420,10 @@ function checkStaleModelReferences(input) {
25635
26420
  for (const referencedId of chain) check("fallback-chain", chainKey, referencedId);
25636
26421
  }
25637
26422
  for (const referencedId of input.defaultChain ?? []) check("fallback-default", "default", referencedId);
26423
+ for (const [agentName, decl] of Object.entries(input.agentModels ?? {})) {
26424
+ if (decl.model) check("agent-model", agentName, decl.model);
26425
+ for (const referencedId of decl.fallbackModels ?? []) check("agent-fallback", agentName, referencedId);
26426
+ }
25638
26427
  for (const [source, target] of Object.entries(DEPRECATED_MODEL_MAP)) check("deprecated-model-map", source, target);
25639
26428
  for (const [source, target] of replacedByOverlayEntries()) check("replaced-by-overlay", source, target);
25640
26429
  return found.sort((a, b) => a.surface.localeCompare(b.surface) || a.key.localeCompare(b.key) || a.referencedId.localeCompare(b.referencedId));
@@ -26099,13 +26888,19 @@ const toolUseSchema = z$1.object({
26099
26888
  * Discriminated union of streaming events. `thinking` blocks are opaque
26100
26889
  * provider-shaped objects (Anthropic extended thinking) replayed verbatim into
26101
26890
  * the next request, so they stay `unknown[]` rather than being modeled.
26891
+ *
26892
+ * `stopReason` must be declared to survive: this schema strips unknown keys, so a
26893
+ * field absent here is dropped at the boundary no matter what the server sends.
26894
+ * 'max_tokens' is the one value with user-visible consequences - it means the reply
26895
+ * was cut off rather than finished.
26102
26896
  */
26103
26897
  const streamEventSchema = z$1.discriminatedUnion("type", [
26104
26898
  z$1.object({
26105
26899
  type: z$1.literal("content"),
26106
26900
  text: z$1.string().optional(),
26107
26901
  usage: usageSchema.optional(),
26108
- credits: creditsSchema.optional()
26902
+ credits: creditsSchema.optional(),
26903
+ stopReason: z$1.string().optional()
26109
26904
  }),
26110
26905
  z$1.object({
26111
26906
  type: z$1.literal("tool_use"),
@@ -26113,7 +26908,8 @@ const streamEventSchema = z$1.discriminatedUnion("type", [
26113
26908
  tools: z$1.array(toolUseSchema).optional(),
26114
26909
  thinking: z$1.array(z$1.unknown()).optional(),
26115
26910
  usage: usageSchema.optional(),
26116
- credits: creditsSchema.optional()
26911
+ credits: creditsSchema.optional(),
26912
+ stopReason: z$1.string().optional()
26117
26913
  }),
26118
26914
  z$1.object({
26119
26915
  type: z$1.literal("error"),
@@ -26182,12 +26978,14 @@ var StreamAccumulator = class {
26182
26978
  case "content":
26183
26979
  this.accumulatedText += event.text ?? "";
26184
26980
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
26981
+ if (event.stopReason) this.stopReason = event.stopReason;
26185
26982
  break;
26186
26983
  case "tool_use":
26187
26984
  if (event.text) this.accumulatedText += event.text;
26188
26985
  if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
26189
26986
  if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
26190
26987
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
26988
+ if (event.stopReason) this.stopReason = event.stopReason;
26191
26989
  }
26192
26990
  }
26193
26991
  /** True when neither text nor tools have been accumulated (stream produced nothing useful). */
@@ -26214,10 +27012,14 @@ var StreamAccumulator = class {
26214
27012
  const info = {
26215
27013
  toolsUsed: this.toolsUsed,
26216
27014
  thinking: this.thinkingBlocks.length > 0 ? this.thinkingBlocks : void 0,
26217
- ...this.lastUsageInfo
27015
+ ...this.lastUsageInfo,
27016
+ stopReason: this.stopReason
26218
27017
  };
26219
27018
  await callback([cleanedText], info);
26220
- } else if (cleanedText) await callback([cleanedText], this.lastUsageInfo);
27019
+ } else if (cleanedText) await callback([cleanedText], {
27020
+ ...this.lastUsageInfo,
27021
+ stopReason: this.stopReason
27022
+ });
26221
27023
  }
26222
27024
  };
26223
27025
  //#endregion
@@ -27328,7 +28130,7 @@ var AgentStore = class {
27328
28130
  async loadAgentsFromDirectory(directory, source) {
27329
28131
  try {
27330
28132
  if (!(await fs$1.stat(directory)).isDirectory()) return;
27331
- const files = await this.findAgentFiles(directory);
28133
+ const files = await findMarkdownFiles(directory);
27332
28134
  for (const filePath of files) try {
27333
28135
  const agent = await this.parseAgentFile(filePath, source);
27334
28136
  this.agents.set(agent.name, agent);
@@ -27340,25 +28142,6 @@ var AgentStore = class {
27340
28142
  }
27341
28143
  }
27342
28144
  /**
27343
- * Recursively find all .md files in directory
27344
- */
27345
- async findAgentFiles(directory) {
27346
- const files = [];
27347
- try {
27348
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
27349
- for (const entry of entries) {
27350
- const fullPath = path.join(directory, entry.name);
27351
- if (entry.isDirectory()) {
27352
- const subFiles = await this.findAgentFiles(fullPath);
27353
- files.push(...subFiles);
27354
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
27355
- }
27356
- } catch (error) {
27357
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
27358
- }
27359
- return files;
27360
- }
27361
- /**
27362
28145
  * Parse a single agent markdown file
27363
28146
  */
27364
28147
  async parseAgentFile(filePath, source) {
@@ -27675,7 +28458,8 @@ function buildFilenameMarkerRegex(markers) {
27675
28458
  * of the best-effort DB pre-filter. Fail-closed by design.
27676
28459
  */
27677
28460
  function isRetrievalExcluded(file, opts) {
27678
- if (opts.vectorizedOnly && !file.vectorized) return true;
28461
+ const stalledByConvergence = isConvergencePausedNote(file.notes) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
28462
+ if (opts.vectorizedOnly && !file.vectorized && !stalledByConvergence) return true;
27679
28463
  const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
27680
28464
  return !!re && re.test((file.fileName ?? "").toLowerCase());
27681
28465
  }
@@ -28185,6 +28969,41 @@ function attachedContentExtractionBudget(maxSafeInputTokens, systemPromptReserve
28185
28969
  const boundedReserve = Math.min(systemPromptReserve, Math.floor(maxSafeInputTokens * EXTRACTION_SYSTEM_RESERVE_MAX_SHARE));
28186
28970
  return Math.max(0, Math.max(Math.floor(maxSafeInputTokens * MIN_ATTACHED_CONTENT_EXTRACTION_SHARE), Math.floor((maxSafeInputTokens - boundedReserve) * ATTACHED_CONTENT_EXTRACTION_SHARE)));
28187
28971
  }
28972
+ /**
28973
+ * Output budget used when a caller supplies no max_tokens. Within supported output limits for
28974
+ * every configured non-reasoning model; models that reason inside the output budget default to
28975
+ * ADAPTIVE_THINKING_MAX_TOKENS_FLOOR instead, since their reasoning would otherwise consume this
28976
+ * whole budget (see resolveOutputMaxTokens/reasonsWithinOutputBudget for which those are).
28977
+ * Distinct from the catalog's DEFAULT_MAX_OUTPUT_TOKENS, which fills in a model's *capability*
28978
+ * when its record omits one.
28979
+ */
28980
+ const DEFAULT_OUTPUT_MAX_TOKENS = 4096;
28981
+ /**
28982
+ * Verbatim-history token budget shared by every caller that sizes one: how much of the model's
28983
+ * safe input window recent conversation turns may claim before older ones fall out to
28984
+ * contextSummary. Pulled into one place because ChatCompletionProcess.ts and
28985
+ * ChatCompletionFeatures.ts (QuestMaster) both need it and must not drift apart - the first
28986
+ * version of the QuestMaster call site hand-copied the formula and got the window wrong for
28987
+ * every model below the unknown-model floor.
28988
+ *
28989
+ * Always returns at least 1, even on a window so small or a message so long that the raw
28990
+ * subtraction goes to zero or negative: `Math.max(0, budget) && budget > 0` downstream treats a
28991
+ * falsy budget as "no budget given" and skips trimming entirely, which is the exact unbounded
28992
+ * history this function exists to prevent. A budget of 1 still trims to the single most recent
28993
+ * turn instead of disabling the trim.
28994
+ */
28995
+ function computeVerbatimTokenBudget(modelInfo, requestedMaxTokens, opts) {
28996
+ const modelMaxOutput = modelInfo.max_tokens ?? 16384;
28997
+ const safeMaxTokens = resolveOutputMaxTokens({
28998
+ requested: requestedMaxTokens,
28999
+ fallback: DEFAULT_OUTPUT_MAX_TOKENS,
29000
+ modelInfo,
29001
+ modelMaxOutputTokens: modelMaxOutput
29002
+ });
29003
+ const safeInputTokens = Math.max(0, safeInputWindow(modelInfo, safeMaxTokens));
29004
+ const availableForVerbatim = Math.max(0, safeInputTokens - opts.nonHistoryOverheadTokens);
29005
+ return Math.max(1, Math.floor(availableForVerbatim * opts.verbatimWindowFraction));
29006
+ }
28188
29007
  /** The buffer buildAndSortMessages holds back before dividing the input window. */
28189
29008
  function assemblyTokenBuffer(maxInputTokens) {
28190
29009
  return Math.max(MIN_TOKEN_BUFFER, Math.floor(maxInputTokens * TOKEN_BUFFER_PERCENTAGE));
@@ -29191,12 +30010,12 @@ const estimateMessagesTokens = (messages) => messages.reduce((sum, message) => s
29191
30010
  * staying synchronous (no N async tokenizer calls over a long history). Mirrors
29192
30011
  * the fields the conversion below actually emits into the prompt.
29193
30012
  */
29194
- function estimateQuestTokenLength(item) {
30013
+ function estimateQuestTokenLength(item, disableToolReplay = false) {
29195
30014
  const parts = [item.prompt ?? ""];
29196
30015
  if (item.structuredReplies?.length) parts.push(JSON.stringify(item.structuredReplies));
29197
30016
  else if (item.replies?.length) parts.push(item.replies.join("\n"));
29198
30017
  if (item.toolResults?.length) parts.push(JSON.stringify(item.toolResults));
29199
- if (!item.structuredReplies?.length) {
30018
+ if (!item.structuredReplies?.length && !disableToolReplay) {
29200
30019
  const toolCalls = replayableToolCalls(item.promptMeta?.functionCalls);
29201
30020
  if (toolCalls.length) parts.push(JSON.stringify(toolCalls));
29202
30021
  }
@@ -29276,20 +30095,21 @@ async function generateSafeEmbedding(embeddingService, text, logger) {
29276
30095
  * history at all, and UNLIMITED_HISTORY_COUNT means no window (which still pages, since the
29277
30096
  * fetch needs some limit).
29278
30097
  */
29279
- async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget }) {
30098
+ async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget, excludeCurrentPrompt = false, model }) {
29280
30099
  if (!isUnlimitedHistory(historyCount) && historyCount !== null && historyCount <= 0) return [
29281
30100
  [],
29282
30101
  0,
29283
30102
  { cacheHit: false }
29284
30103
  ];
29285
30104
  const limit = resolveHistoryFetchLimit(historyCount);
30105
+ const disableToolReplay = !!model && isGeminiModelId(model);
29286
30106
  const startTime = Date.now();
29287
30107
  const chatHistoryItems = await db.quests.getMostRecentChatHistory(session.id, limit + 1);
29288
30108
  const fetchTime = Date.now() - startTime;
29289
30109
  const cacheIndicator = fetchTime < 50 ? "CACHE_HIT" : "CACHE_MISS";
29290
30110
  Logger.globalInstance.log(`⚡ Message History ${cacheIndicator}: ${fetchTime}ms for session ${session.id.slice(-8)} (${chatHistoryItems.length} items)`);
29291
30111
  chatHistoryItems.reverse();
29292
- if (chatHistoryItems.length > 1) chatHistoryItems.pop();
30112
+ if (chatHistoryItems.length > 1 || excludeCurrentPrompt) chatHistoryItems.pop();
29293
30113
  if (session.contextSummaryUpToQuestId) {
29294
30114
  const boundary = session.contextSummaryUpToQuestId;
29295
30115
  const filtered = chatHistoryItems.filter((item) => item.id > boundary);
@@ -29300,7 +30120,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29300
30120
  let usedTokens = 0;
29301
30121
  let keepFromIndex = 0;
29302
30122
  for (let i = chatHistoryItems.length - 1; i >= 0; i--) {
29303
- usedTokens += estimateQuestTokenLength(chatHistoryItems[i]);
30123
+ usedTokens += estimateQuestTokenLength(chatHistoryItems[i], disableToolReplay);
29304
30124
  if (usedTokens > verbatimTokenBudget && i < chatHistoryItems.length - 1) {
29305
30125
  keepFromIndex = i + 1;
29306
30126
  break;
@@ -29332,7 +30152,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29332
30152
  is_error: tr.is_error
29333
30153
  }))
29334
30154
  });
29335
- } else if (toolCalls.length > 0) {
30155
+ } else if (toolCalls.length > 0 && !disableToolReplay) {
29336
30156
  const textReply = cur.replies?.find((reply) => !reply.trim().startsWith("<think>")) || "";
29337
30157
  const assistantContent = [];
29338
30158
  if (textReply) assistantContent.push({
@@ -30517,6 +31337,7 @@ var llm_exports = /* @__PURE__ */ __exportAll({
30517
31337
  ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
30518
31338
  ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
30519
31339
  BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
31340
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
30520
31341
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
30521
31342
  FORMAT_PROMPT_PRIORITY: () => 60,
30522
31343
  IMAGE_PROMPT_PRIORITY: () => 50,
@@ -30532,6 +31353,7 @@ var llm_exports = /* @__PURE__ */ __exportAll({
30532
31353
  buildAndSortMessages: () => buildAndSortMessages,
30533
31354
  calculateTotalTokenLength: () => calculateTotalTokenLength,
30534
31355
  computeCosineSimilarity: () => computeCosineSimilarity,
31356
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
30535
31357
  effectiveContextWindow: () => effectiveContextWindow,
30536
31358
  fetchAgentConversationHistory: () => fetchAgentConversationHistory,
30537
31359
  fetchAndConvertFabFiles: () => fetchAndConvertFabFiles,
@@ -32437,7 +33259,12 @@ var BFLImageService = class extends AIImageService {
32437
33259
  if (height) requestBody.height = height;
32438
33260
  }
32439
33261
  const cleanedBody = this.stripNullFields(requestBody);
32440
- Logger.globalInstance.debug("[DEBUG] BFL Image generation request body:", cleanedBody);
33262
+ const safeRequestBody = {
33263
+ ...cleanedBody,
33264
+ prompt: prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt,
33265
+ ...cleanedBody.image_prompt ? { image_prompt: `[BASE64_DATA_${cleanedBody.image_prompt.length}_CHARS]` } : {}
33266
+ };
33267
+ Logger.globalInstance.log("BFL Image generation request body:", safeRequestBody);
32441
33268
  const submitResponse = await axios.post(`${this.baseUrl}/${model}`, cleanedBody, { headers: {
32442
33269
  accept: "application/json",
32443
33270
  "x-key": this.apiKey,
@@ -32450,7 +33277,7 @@ var BFLImageService = class extends AIImageService {
32450
33277
  pollingUrl,
32451
33278
  responseData: submitResponse.data,
32452
33279
  endpoint: `${this.baseUrl}/${model}`,
32453
- requestBody
33280
+ requestBody: safeRequestBody
32454
33281
  });
32455
33282
  const imageUrl = await this.pollForResult(requestId, pollingUrl);
32456
33283
  Logger.globalInstance.debug("[DEBUG] Received BFL image URL:", {
@@ -32859,8 +33686,6 @@ var GeminiImageService = class extends AIImageService {
32859
33686
  const aspectRatio = this.resolveAspectRatio(options);
32860
33687
  if (aspectRatio) config.aspectRatio = aspectRatio;
32861
33688
  if (options.guidance !== null && options.guidance !== void 0) config.guidanceScale = options.guidance;
32862
- if (options.prompt_upsampling !== void 0) config.enhancePrompt = options.prompt_upsampling;
32863
- if (options.seed !== null && options.seed !== void 0) config.seed = options.seed;
32864
33689
  const outputMimeType = this.resolveMimeType(options.output_format);
32865
33690
  if (outputMimeType) config.outputMimeType = outputMimeType;
32866
33691
  return config;
@@ -34171,11 +34996,11 @@ const DEFAULT_FALLBACK_CHAIN = [
34171
34996
  */
34172
34997
  function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logger, excludeModelIds, preferUntriedBackend) {
34173
34998
  logger.info(`🔍 Finding automatic fallback for ${originalModel.id}`);
34174
- const hasValidKey = (m) => !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
34999
+ const hasValidKey = (m) => !m.disabled && !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
34175
35000
  const preferences = [...FALLBACK_PREFERENCES[originalModel.id] ?? []];
34176
35001
  if (preferences.length === 0) preferences.push(...DEFAULT_FALLBACK_CHAIN);
34177
35002
  if (preferUntriedBackend) {
34178
- const triedBackends = /* @__PURE__ */ new Set([originalModel.backend]);
35003
+ const triedBackends = new Set(originalModel.backend ? [originalModel.backend] : []);
34179
35004
  for (const id of excludeModelIds ?? []) {
34180
35005
  const tried = availableModels.find((m) => m.id === id);
34181
35006
  if (tried) triedBackends.add(tried.backend);
@@ -34202,7 +35027,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
34202
35027
  return model;
34203
35028
  }
34204
35029
  }
34205
- logger.error(" No suitable automatic fallback model found");
35030
+ logger.warn("⚠️ No suitable automatic fallback model found");
34206
35031
  return null;
34207
35032
  }
34208
35033
  /**
@@ -34272,6 +35097,39 @@ async function getLlmWithFallback(originalModel, fallbackModelId, availableModel
34272
35097
  return null;
34273
35098
  }
34274
35099
  /**
35100
+ * Pick a runnable substitute for a model id that is NOT in `availableModels` at all -
35101
+ * a sunset id a catalog lifecycle row has hidden, or one disabled since whatever
35102
+ * persisted it (a session pin, a stored mapping row) was written.
35103
+ *
35104
+ * `getLlmWithFallback` cannot serve this case: its `originalModel` is a `ModelInfo`,
35105
+ * which by definition does not exist here. This walks the same
35106
+ * `FALLBACK_PREFERENCES` -> `DEFAULT_FALLBACK_CHAIN` -> any-keyed-model selection
35107
+ * through the same `findAutomaticFallback`, so the substitute a hidden id lands on
35108
+ * cannot diverge from the one a listed-but-failing id would.
35109
+ *
35110
+ * Callers should run `resolveDeprecatedModelId` first: a sunset id with a known
35111
+ * successor should be forwarded to it (and counted as a `[model-sunset]`) rather
35112
+ * than treated as a model with no answer.
35113
+ */
35114
+ function findFallbackForMissingModel(missingModelId, availableModels, apiKeyTable, logger, endUserId) {
35115
+ const fallbackModel = findAutomaticFallback({ id: missingModelId }, availableModels, apiKeyTable, logger);
35116
+ if (!fallbackModel) return null;
35117
+ const backend = (0, llm_exports.getLlmByModel)(apiKeyTable, {
35118
+ modelInfo: fallbackModel,
35119
+ logger,
35120
+ endUserId
35121
+ });
35122
+ if (!backend) {
35123
+ logger.warn(`⚠️ Fallback for unavailable model ${missingModelId} failed to initialize: ${fallbackModel.id}`);
35124
+ return null;
35125
+ }
35126
+ return {
35127
+ model: fallbackModel,
35128
+ backend,
35129
+ attempt: 1
35130
+ };
35131
+ }
35132
+ /**
34275
35133
  * Tiktoken-based implementation of the tokenizer interface
34276
35134
  * Provides caching for performance and configurable logging
34277
35135
  *
@@ -34792,6 +35650,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34792
35650
  ClientMessageSender: () => ClientMessageSender,
34793
35651
  CorruptedFileError: () => CorruptedFileError,
34794
35652
  DEFAULT_FALLBACK_CHAIN: () => DEFAULT_FALLBACK_CHAIN,
35653
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
34795
35654
  DEFAULT_PASSAGE_TOKEN_TARGET: () => 512,
34796
35655
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
34797
35656
  ElevenLabsMusicGenerator: () => ElevenLabsMusicGenerator,
@@ -34879,6 +35738,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34879
35738
  checkStorageLimitForFile: () => checkStorageLimitForFile,
34880
35739
  cleanMermaidSyntax: () => cleanMermaidSyntax,
34881
35740
  computeCosineSimilarity: () => computeCosineSimilarity,
35741
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
34882
35742
  convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
34883
35743
  createQuestPlanToolSchema: () => createQuestPlanToolSchema,
34884
35744
  createTokenizer: () => createTokenizer,
@@ -34894,6 +35754,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34894
35754
  fetchAndParseURL: () => fetchAndParseURL,
34895
35755
  fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
34896
35756
  filterRetrievalExcluded: () => filterRetrievalExcluded,
35757
+ findFallbackForMissingModel: () => findFallbackForMissingModel,
34897
35758
  formatVoiceHistory: () => formatVoiceHistory,
34898
35759
  generateSafeEmbedding: () => generateSafeEmbedding,
34899
35760
  getCachedSignedUrl: () => getCachedSignedUrl,