@bike4mind/cli 0.20.0 → 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-DD3DcC3-.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-CBGJMey9.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 }, 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,
@@ -2750,9 +2768,14 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope }, s
2750
2768
  entitlementKeys,
2751
2769
  retrievalFilter,
2752
2770
  kbScope,
2771
+ inlinedAttachmentIds,
2772
+ fullyInlinedAttachmentIds,
2773
+ suppressLakeArms,
2774
+ sessionRetrievalTags,
2753
2775
  codeMinifier,
2754
2776
  availableModels,
2755
- onToolLlmUsage
2777
+ onToolLlmUsage,
2778
+ getAbortSignal
2756
2779
  };
2757
2780
  return Object.entries(tools).reduce((acc, [key, tool]) => ({
2758
2781
  ...acc,
@@ -5436,6 +5459,61 @@ function extractCommandName(filename) {
5436
5459
  return name;
5437
5460
  }
5438
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
5439
5517
  //#region src/storage/CustomCommandStore.ts
5440
5518
  var CustomCommandStore = class {
5441
5519
  constructor(projectRoot, options = {}) {
@@ -5513,7 +5591,7 @@ var CustomCommandStore = class {
5513
5591
  async loadCommandsFromDirectory(directory, source) {
5514
5592
  try {
5515
5593
  if (!(await fs$1.stat(directory)).isDirectory()) return;
5516
- const commandFiles = await this.findCommandFiles(directory);
5594
+ const commandFiles = await findMarkdownFiles(directory);
5517
5595
  for (const filePath of commandFiles) try {
5518
5596
  await this.loadCommandFile(filePath, source);
5519
5597
  } catch (error) {
@@ -5524,28 +5602,6 @@ var CustomCommandStore = class {
5524
5602
  }
5525
5603
  }
5526
5604
  /**
5527
- * Recursively finds all .md files in a directory
5528
- *
5529
- * @param directory - Directory to search
5530
- * @returns Array of full file paths to .md files
5531
- */
5532
- async findCommandFiles(directory) {
5533
- const files = [];
5534
- try {
5535
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
5536
- for (const entry of entries) {
5537
- const fullPath = path.join(directory, entry.name);
5538
- if (entry.isDirectory()) {
5539
- const subFiles = await this.findCommandFiles(fullPath);
5540
- files.push(...subFiles);
5541
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
5542
- }
5543
- } catch (error) {
5544
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
5545
- }
5546
- return files;
5547
- }
5548
- /**
5549
5605
  * Loads a single command file
5550
5606
  *
5551
5607
  * @param filePath - Full path to the command file
@@ -7000,6 +7056,41 @@ const ChunkSchema = z$1.object({
7000
7056
  const isEmbeddingModel = (model, modelEnum) => {
7001
7057
  return Object.values(modelEnum).includes(model);
7002
7058
  };
7059
+ /**
7060
+ * The embedding model's context window in tokens - the HARD ceiling an embedding call accepts.
7061
+ * Extracted from SmartChunker so a caller (the chunk queue handler, #1662) can compute the same
7062
+ * effective passage limit for owner-altitude policy resolution and cross-lake conflict reporting
7063
+ * without constructing a chunker. Throws on an unsupported model, exactly as the chunker does.
7064
+ */
7065
+ function embeddingModelContextWindow(model) {
7066
+ if (isEmbeddingModel(model, OpenAIEmbeddingModel)) return OPENAI_EMBEDDING_MODEL_MAP[model].contextWindow;
7067
+ if (isEmbeddingModel(model, VoyageAIEmbeddingModel)) return VOYAGEAI_EMBEDDING_MODEL_MAP[model].contextWindow;
7068
+ if (isEmbeddingModel(model, BedrockEmbeddingModel)) return BEDROCK_EMBEDDING_MODEL_MAP[model].contextWindow;
7069
+ if (isEmbeddingModel(model, OllamaEmbeddingModel)) return OLLAMA_EMBEDDING_MODEL_MAP[model].contextWindow;
7070
+ throw new Error(`Unsupported embedding model: ${model}`);
7071
+ }
7072
+ /**
7073
+ * The buffer subtracted from the model window to absorb cross-provider tokenizer differences
7074
+ * (char/4 approximations can undercount by ~8-10% vs tiktoken). A value < 1 is a percent of the
7075
+ * window (floored to >= 32 tokens); a value >= 1 is an absolute token count.
7076
+ */
7077
+ function embeddingWindowBuffer(maxTokens, bufferPercentOrValue = .2) {
7078
+ return bufferPercentOrValue < 1 ? Math.max(Math.floor(maxTokens * bufferPercentOrValue), 32) : Math.floor(bufferPercentOrValue);
7079
+ }
7080
+ /**
7081
+ * The effective per-chunk token limit the chunker will actually use: the SOFT passage target
7082
+ * (retrieval granularity) hard-capped to the buffered model window (an oversized chunk fails the
7083
+ * embedding call). THE single source of truth for that clamp - SmartChunker's constructor and the
7084
+ * chunk handler's conflict/observability logic (#1662) both derive from it, so a resolved policy
7085
+ * value can never drift from the granularity it actually produces. An omitted/invalid target falls
7086
+ * back to DEFAULT_PASSAGE_TOKEN_TARGET; a supplied one is floored to MIN_PASSAGE_TOKEN_TARGET.
7087
+ */
7088
+ function effectiveChunkTokenLimit(opts) {
7089
+ const maxTokens = embeddingModelContextWindow(opts.model);
7090
+ const hardLimit = maxTokens - embeddingWindowBuffer(maxTokens, opts.bufferPercentOrValue ?? .2);
7091
+ const { passageTokenTarget } = opts;
7092
+ return Math.min(hardLimit, passageTokenTarget !== void 0 && Number.isFinite(passageTokenTarget) && passageTokenTarget > 0 ? Math.max(Math.floor(passageTokenTarget), 64) : 512);
7093
+ }
7003
7094
  var SmartChunker = class {
7004
7095
  logger;
7005
7096
  model;
@@ -7008,6 +7099,7 @@ var SmartChunker = class {
7008
7099
  encoder;
7009
7100
  storage;
7010
7101
  bufferPercentOrValue;
7102
+ lastExtractedText;
7011
7103
  /**
7012
7104
  * @param model - The embedding model name
7013
7105
  * @param storage - Storage instance for file content
@@ -7023,25 +7115,20 @@ var SmartChunker = class {
7023
7115
  passageTokenTarget: void 0
7024
7116
  } : options ?? {};
7025
7117
  this.model = model;
7026
- if (isEmbeddingModel(model, OpenAIEmbeddingModel)) this.maxTokens = OPENAI_EMBEDDING_MODEL_MAP[model].contextWindow;
7027
- else if (isEmbeddingModel(model, VoyageAIEmbeddingModel)) this.maxTokens = VOYAGEAI_EMBEDDING_MODEL_MAP[model].contextWindow;
7028
- else if (isEmbeddingModel(model, BedrockEmbeddingModel)) this.maxTokens = BEDROCK_EMBEDDING_MODEL_MAP[model].contextWindow;
7029
- else if (isEmbeddingModel(model, OllamaEmbeddingModel)) this.maxTokens = OLLAMA_EMBEDDING_MODEL_MAP[model].contextWindow;
7030
- else throw new Error(`Unsupported embedding model: ${model}`);
7118
+ this.maxTokens = embeddingModelContextWindow(model);
7031
7119
  this.bufferPercentOrValue = bufferPercentOrValue ?? .2;
7032
- let buffer;
7033
- if (this.bufferPercentOrValue < 1) buffer = Math.max(Math.floor(this.maxTokens * this.bufferPercentOrValue), 32);
7034
- else buffer = Math.floor(this.bufferPercentOrValue);
7035
- const hardLimit = this.maxTokens - buffer;
7036
- const target = passageTokenTarget !== void 0 && Number.isFinite(passageTokenTarget) && passageTokenTarget > 0 ? Math.max(Math.floor(passageTokenTarget), 64) : 512;
7037
- this.chunkTokenLimit = Math.min(hardLimit, target);
7120
+ this.chunkTokenLimit = effectiveChunkTokenLimit({
7121
+ model,
7122
+ passageTokenTarget,
7123
+ bufferPercentOrValue: this.bufferPercentOrValue
7124
+ });
7038
7125
  this.storage = storage;
7039
7126
  this.logger.updateMetadata({
7040
7127
  model,
7041
7128
  maxTokens: this.maxTokens,
7042
7129
  chunkTokenLimit: this.chunkTokenLimit,
7043
7130
  bufferPercentOrValue: this.bufferPercentOrValue,
7044
- passageTokenTarget: target
7131
+ passageTokenTarget
7045
7132
  });
7046
7133
  }
7047
7134
  async initializeEncoder() {
@@ -7066,6 +7153,13 @@ var SmartChunker = class {
7066
7153
  this.encoder = void 0;
7067
7154
  }
7068
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
+ }
7069
7163
  async chunkFile(fileOrContent, mimeType) {
7070
7164
  let content;
7071
7165
  if (Buffer.isBuffer(fileOrContent)) content = fileOrContent;
@@ -7075,6 +7169,7 @@ var SmartChunker = class {
7075
7169
  }
7076
7170
  this.logger.updateMetadata({ mimeType });
7077
7171
  this.logger.log(`Chunking file with type: ${mimeType}`);
7172
+ this.lastExtractedText = void 0;
7078
7173
  if (isAudioMimeType(mimeType)) {
7079
7174
  this.logger.log(`Skipping chunking for audio file type: ${mimeType}`);
7080
7175
  return [];
@@ -7112,12 +7207,17 @@ var SmartChunker = class {
7112
7207
  case SupportedFabFileMimeTypes.PHP:
7113
7208
  case SupportedFabFileMimeTypes.RUBY:
7114
7209
  case SupportedFabFileMimeTypes.SH:
7115
- case SupportedFabFileMimeTypes.BASH:
7116
- 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);
7117
7214
  break;
7215
+ }
7118
7216
  default:
7119
7217
  if (mimeType && mimeType.startsWith("text/")) {
7120
- chunks = await this.chunkText(content.toString());
7218
+ const textContent = content.toString();
7219
+ this.lastExtractedText = textContent;
7220
+ chunks = await this.chunkText(textContent);
7121
7221
  break;
7122
7222
  }
7123
7223
  this.logger.error(`Unsupported file type: ${mimeType}`);
@@ -7129,7 +7229,9 @@ var SmartChunker = class {
7129
7229
  return await this.storage.getContentAsBuffer(file.filePath);
7130
7230
  }
7131
7231
  async chunkCSV(content) {
7132
- 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);
7133
7235
  const chunks = [];
7134
7236
  let currentChunk = [];
7135
7237
  let currentTokens = 0;
@@ -7182,6 +7284,7 @@ var SmartChunker = class {
7182
7284
  async chunkPDF(content) {
7183
7285
  const pdf = await getDocumentProxy(new Uint8Array(content));
7184
7286
  const { text } = await extractText(pdf);
7287
+ this.lastExtractedText = Array.isArray(text) ? text.join("\n") : text;
7185
7288
  if (typeof text === "string") return this.chunkText(text);
7186
7289
  const chunks = [];
7187
7290
  let currentChunk = "";
@@ -7213,7 +7316,9 @@ var SmartChunker = class {
7213
7316
  return chunks;
7214
7317
  }
7215
7318
  async chunkJSON(content) {
7216
- const json = JSON.parse(content.toString());
7319
+ const jsonString = content.toString();
7320
+ this.lastExtractedText = jsonString;
7321
+ const json = JSON.parse(jsonString);
7217
7322
  return this.chunkObject(json);
7218
7323
  }
7219
7324
  async chunkObject(obj) {
@@ -7280,6 +7385,7 @@ var SmartChunker = class {
7280
7385
  }
7281
7386
  async chunkDOCX(content) {
7282
7387
  const result = await mammoth.extractRawText({ buffer: content });
7388
+ this.lastExtractedText = result.value;
7283
7389
  return this.chunkText(result.value);
7284
7390
  }
7285
7391
  async chunkPPTX(content) {
@@ -7298,6 +7404,7 @@ var SmartChunker = class {
7298
7404
  this.logger.warn("PPTX contained no extractable slide text");
7299
7405
  return [];
7300
7406
  }
7407
+ this.lastExtractedText = fullText;
7301
7408
  return this.chunkText(fullText);
7302
7409
  }
7303
7410
  async chunkText(content) {
@@ -7372,6 +7479,9 @@ var SmartChunker = class {
7372
7479
  async chunkExcel(content) {
7373
7480
  const { read, utils } = await import("xlsx");
7374
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");
7375
7485
  const chunks = [];
7376
7486
  let currentChunk = "";
7377
7487
  let currentTokens = 0;
@@ -7591,6 +7701,8 @@ const dnsResolve6 = promisify(dns.resolve6);
7591
7701
  * Check if an IPv4 address is in a private/internal range.
7592
7702
  */
7593
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;
7594
7706
  const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
7595
7707
  if (!ipv4Match) return false;
7596
7708
  const [, a, b, c] = ipv4Match.map(Number);
@@ -7611,44 +7723,153 @@ function isPrivateIPv4(ip) {
7611
7723
  /**
7612
7724
  * Check if an IPv6 address is in a private/internal range.
7613
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
+ }
7614
7791
  function isPrivateIPv6(ip) {
7615
- 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;
7616
7797
  if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
7617
7798
  if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
7618
- if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
7799
+ if (normalized.startsWith("fe")) return true;
7619
7800
  if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
7620
7801
  if (normalized.startsWith("ff")) return true;
7621
- const ipv4MappedMatch = normalized.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
7622
- 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
+ }
7623
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;
7624
7819
  if (normalized.startsWith("100::") || normalized.startsWith("0100::")) return true;
7625
7820
  if (normalized.startsWith("64:ff9b:") || normalized.startsWith("0064:ff9b:")) return true;
7626
7821
  return false;
7627
7822
  }
7628
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
+ /**
7629
7842
  * Check if an IP address (IPv4 or IPv6) is in a private/internal range.
7630
7843
  */
7631
7844
  function isPrivateIP(ip) {
7632
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return isPrivateIPv4(ip);
7633
- return isPrivateIPv6(ip);
7845
+ const address = stripZoneAndIpv4Port(ip);
7846
+ if (/^(\d+\.){3}\d+$/.test(address)) return isPrivateIPv4(address);
7847
+ return isPrivateIPv6(address);
7634
7848
  }
7635
7849
  /**
7636
7850
  * Check if a hostname is known to be private/internal.
7637
7851
  * This catches obvious cases before DNS resolution.
7638
7852
  */
7639
7853
  function isPrivateOrInternalHostname(hostname) {
7640
- const normalized = hostname.toLowerCase();
7854
+ const normalized = stripZoneAndIpv4Port(stripIpv6Brackets(hostname));
7641
7855
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "0.0.0.0" || normalized.endsWith(".localhost") || normalized.endsWith(".local")) return true;
7642
7856
  if (normalized === "169.254.169.254" || normalized === "instance-data" || normalized === "metadata.google.internal" || normalized === "metadata.internal") return true;
7643
7857
  if (normalized.endsWith(".cluster.local") || normalized.endsWith(".svc.cluster.local") || normalized.endsWith(".pod.cluster.local")) return true;
7644
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(normalized)) return isPrivateIPv4(normalized);
7858
+ if (/^(\d+\.){3}\d+$/.test(normalized)) return isPrivateIPv4(normalized);
7645
7859
  if (normalized.includes(":")) return isPrivateIPv6(normalized);
7646
7860
  return false;
7647
7861
  }
7648
7862
  /**
7649
7863
  * Validate a URL before fetching.
7650
7864
  * Blocks internal/private networks to prevent SSRF attacks.
7651
- * 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.
7652
7873
  *
7653
7874
  * @param url - The URL to validate
7654
7875
  * @returns Object with valid flag and optional error message
@@ -7660,15 +7881,16 @@ async function validateUrlForFetch(url) {
7660
7881
  valid: false,
7661
7882
  error: "URL must use HTTP or HTTPS protocol"
7662
7883
  };
7663
- if (isPrivateOrInternalHostname(parsed.hostname)) return {
7884
+ const hostname = stripIpv6Brackets(parsed.hostname);
7885
+ if (isPrivateOrInternalHostname(hostname)) return {
7664
7886
  valid: false,
7665
7887
  error: "URL points to a private or internal network"
7666
7888
  };
7667
- const isIPv4Address = /^(\d{1,3}\.){3}\d{1,3}$/.test(parsed.hostname);
7668
- const isIPv6Address = parsed.hostname.includes(":");
7889
+ const isIPv4Address = /^(\d+\.){3}\d+$/.test(hostname);
7890
+ const isIPv6Address = hostname.includes(":");
7669
7891
  if (!isIPv4Address && !isIPv6Address) try {
7670
- const ipv4Addresses = await dnsResolve4(parsed.hostname).catch(() => []);
7671
- const ipv6Addresses = await dnsResolve6(parsed.hostname).catch(() => []);
7892
+ const ipv4Addresses = await dnsResolve4(hostname).catch(() => []);
7893
+ const ipv6Addresses = await dnsResolve6(hostname).catch(() => []);
7672
7894
  const allAddresses = [...ipv4Addresses, ...ipv6Addresses];
7673
7895
  if (allAddresses.length === 0) return {
7674
7896
  valid: false,
@@ -7692,6 +7914,93 @@ async function validateUrlForFetch(url) {
7692
7914
  };
7693
7915
  }
7694
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
+ });
7695
8004
  const URL_REGEX = /https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?/gi;
7696
8005
  function detectURLs(string) {
7697
8006
  return string.match(URL_REGEX) || [];
@@ -7703,36 +8012,153 @@ function urlExists(stringWithPossibleUrl) {
7703
8012
  return detectURLs(stringWithPossibleUrl.replace(/\n/g, " ").replace(/,/g, " "));
7704
8013
  }
7705
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
+ }
7706
8119
  async function fetchAndParseURL(url, { logger }) {
7707
8120
  logger.updateMetadata({ failedUrl: null });
7708
8121
  try {
7709
- const ssrfValidation = await validateUrlForFetch(url);
7710
- if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
7711
- let urlMimeType = "text/plain";
7712
- if (url.split(".")?.pop()?.startsWith("pdf")) urlMimeType = "application/pdf";
7713
- const response = await axios.get(url, {
7714
- responseType: ["application/pdf"].includes(urlMimeType) ? "arraybuffer" : "text",
7715
- timeout: URL_FETCH_TIMEOUT_MS
7716
- });
7717
- const cheerio = await import("cheerio");
7718
- const htmlContent = response.data;
7719
- const $ = cheerio.load(htmlContent);
7720
- const title = $("title").text() || url.split("/")?.pop();
7721
- let urlContent = null;
7722
- switch (urlMimeType) {
7723
- case "application/pdf":
7724
- urlContent = Buffer.from(response.data);
7725
- break;
7726
- default: {
7727
- let textContent = "";
7728
- $("body").find("p").each((index, element) => {
7729
- textContent += $(element).text() + "\n";
7730
- });
7731
- urlContent = textContent || htmlContent;
7732
- break;
7733
- }
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;
7734
8157
  }
7735
- 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}`);
7736
8162
  return {
7737
8163
  title,
7738
8164
  textContent: urlContent,
@@ -7740,7 +8166,7 @@ async function fetchAndParseURL(url, { logger }) {
7740
8166
  ext: mime.extension(urlMimeType) || null
7741
8167
  };
7742
8168
  } catch (error) {
7743
- logger.updateMetadata({ failedUrl: url });
8169
+ logger.updateMetadata({ failedUrl: redactUrlCredentials(url) });
7744
8170
  logger.debug("Error fetching or parsing URL:", error);
7745
8171
  throw error;
7746
8172
  }
@@ -10698,7 +11124,6 @@ var ReActAgent = class extends EventEmitter {
10698
11124
  this.context = {
10699
11125
  ...context,
10700
11126
  maxIterations: context.maxIterations ?? 50,
10701
- maxTokens: context.maxTokens ?? 4096,
10702
11127
  temperature: context.temperature ?? .7
10703
11128
  };
10704
11129
  this.repeatedCallGuard = new RepeatedCallGuard(context.repeatedCallGuard);
@@ -10743,7 +11168,7 @@ var ReActAgent = class extends EventEmitter {
10743
11168
  */
10744
11169
  async completeText(prompt, options = {}) {
10745
11170
  const model = options.model ?? this.context.model;
10746
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11171
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10747
11172
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10748
11173
  let text = "";
10749
11174
  await this.context.llm.complete(model, [{
@@ -10784,7 +11209,7 @@ var ReActAgent = class extends EventEmitter {
10784
11209
  this.isReadOnlyToolFn = options.isReadOnlyTool ?? defaultIsReadOnlyTool;
10785
11210
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
10786
11211
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10787
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11212
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10788
11213
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
10789
11214
  const maxHistoryIterations = options.maxHistoryIterations ?? 4;
10790
11215
  let iterations = 0;
@@ -10825,7 +11250,8 @@ var ReActAgent = class extends EventEmitter {
10825
11250
  totalCacheWriteTokens: this.totalCacheWriteTokens > 0 ? this.totalCacheWriteTokens : void 0,
10826
11251
  iterations,
10827
11252
  toolCalls: this.toolCallCount,
10828
- reachedMaxIterations: false
11253
+ reachedMaxIterations: false,
11254
+ finishReason: this.lastStopReason
10829
11255
  }
10830
11256
  };
10831
11257
  this.emit("complete", result);
@@ -11059,6 +11485,7 @@ var ReActAgent = class extends EventEmitter {
11059
11485
  toolCalls: this.toolCallCount,
11060
11486
  reachedMaxIterations,
11061
11487
  reachedMaxTotalTokens: reachedMaxTotalTokens || void 0,
11488
+ finishReason: this.lastStopReason,
11062
11489
  averageConfidence: avgConfidence,
11063
11490
  minConfidence,
11064
11491
  confidenceLog: this.confidenceLog.length > 0 ? this.confidenceLog : void 0
@@ -11290,7 +11717,7 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
11290
11717
  async runIteration(query, options = {}) {
11291
11718
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
11292
11719
  const temperature = options.temperature ?? this.context.temperature ?? .7;
11293
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11720
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
11294
11721
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
11295
11722
  if (!this.iterationInitialized) {
11296
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.");
@@ -15075,6 +15502,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15075
15502
  buildApiKeyTable: () => buildApiKeyTable,
15076
15503
  buildSupersededIndex: () => buildSupersededIndex,
15077
15504
  buildThinkingParams: () => buildThinkingParams,
15505
+ cachedTokensFromUsage: () => cachedTokensFromUsage,
15078
15506
  catalogLifecycles: () => catalogLifecycles,
15079
15507
  catalogSuccessors: () => catalogSuccessors,
15080
15508
  checkStaleModelReferences: () => checkStaleModelReferences,
@@ -15110,6 +15538,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15110
15538
  resolveSuccessorChain: () => resolveSuccessorChain,
15111
15539
  setModelCatalogProvider: () => setModelCatalogProvider,
15112
15540
  setModelPriceRowsProvider: () => setModelPriceRowsProvider,
15541
+ splitCacheInclusiveInput: () => splitCacheInclusiveInput,
15113
15542
  stripAllToolBlocks: () => stripAllToolBlocks,
15114
15543
  stripToolDependentMessages: () => stripToolDependentMessages,
15115
15544
  toKimiEffort: () => toKimiEffort,
@@ -15195,6 +15624,44 @@ async function runWithConcurrency(tasks, limit) {
15195
15624
  return results;
15196
15625
  }
15197
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
+ /**
15198
15665
  * Helper function to handle tool result streaming for artifact-generating tools
15199
15666
  * This ensures tools like recharts that generate artifacts are streamed immediately
15200
15667
  * rather than waiting for recursive completion calls.
@@ -15710,6 +16177,25 @@ function getCachingAdapter(backend) {
15710
16177
  return ADAPTERS[backend] || new NoOpCachingAdapter();
15711
16178
  }
15712
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
+ /**
15713
16199
  * max_tokens floor for adaptive reasoning models (Claude 4.7+/Opus 5). These
15714
16200
  * models self-manage extended thinking *within* max_tokens, which is a ceiling
15715
16201
  * (they stop at end_turn), not a target - so a larger floor costs nothing on
@@ -15773,8 +16259,17 @@ function reasonsWithinOutputBudget(modelInfo) {
15773
16259
  * be consumed entirely by reasoning, leaving an empty visible reply.
15774
16260
  */
15775
16261
  function resolveOutputMaxTokens({ requested, fallback, modelInfo, modelMaxOutputTokens }) {
15776
- const preferred = requested ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
15777
- 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;
15778
16273
  }
15779
16274
  /**
15780
16275
  * Builds the correct thinking parameters for an Anthropic API call based on
@@ -15963,6 +16458,12 @@ const SLOW_MODEL_REQUEST_TIMEOUT_MS = 12e4;
15963
16458
  */
15964
16459
  const ANTHROPIC_NONSTREAMING_MAX_TOKENS = 21e3;
15965
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
+ /**
15966
16467
  * Accumulated multi-turn cache token total. Undefined when zero so turns
15967
16468
  * without cache activity keep the pre-cache callback shape.
15968
16469
  */
@@ -16573,7 +17074,8 @@ var AnthropicBackend = class {
16573
17074
  const systemMessages = messages.filter((m) => m.role === "system");
16574
17075
  const blocks = [];
16575
17076
  for (const sm of systemMessages) {
16576
- const text = typeof sm.content === "string" ? sm.content : JSON.stringify(sm.content);
17077
+ const text = systemContentToText(sm.content);
17078
+ if (text.trim() === "") continue;
16577
17079
  if (sm.cache === true) blocks.push({
16578
17080
  type: "text",
16579
17081
  text,
@@ -16612,7 +17114,11 @@ var AnthropicBackend = class {
16612
17114
  let { useCount: toolUseCount, resultCount: toolResultCount } = countToolBlocks(filteredMessages);
16613
17115
  if (toolUseCount > 0 || toolResultCount > 0) {
16614
17116
  this.logger.debug(`[Pre-API #6181] Sending ${filteredMessages.length} messages with ${toolUseCount} tool_use and ${toolResultCount} tool_result blocks`);
16615
- 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) {
16616
17122
  this.logger.warn(`[Pre-API #6181] Tool block mismatch! tool_use: ${toolUseCount}, tool_result: ${toolResultCount}. Attempting auto-repair...`);
16617
17123
  filteredMessages = ensureToolPairingIntegrity(filteredMessages, this.logger);
16618
17124
  ({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
@@ -16622,9 +17128,15 @@ var AnthropicBackend = class {
16622
17128
  }
16623
17129
  }
16624
17130
  }
17131
+ const currentModelInfo = this.modelRecordFor(model);
16625
17132
  const apiParams = {
16626
17133
  model,
16627
- 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,
16628
17140
  messages: filteredMessages.map((m) => ({
16629
17141
  role: m.role === "user" ? "user" : "assistant",
16630
17142
  content: m.content
@@ -16664,7 +17176,6 @@ var AnthropicBackend = class {
16664
17176
  }
16665
17177
  }
16666
17178
  const requestExtraHeaders = anyMessageCacheControlled ? { "anthropic-beta": "prompt-caching-2024-07-31" } : void 0;
16667
- const currentModelInfo = this.modelRecordFor(model);
16668
17179
  if (currentModelInfo?.can_think) {
16669
17180
  const thinkingOptions = options;
16670
17181
  const isQuestMaster = thinkingOptions.questMaster === true;
@@ -17045,6 +17556,10 @@ var AnthropicBackend = class {
17045
17556
  if (!parsedParams) {
17046
17557
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
17047
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);
17048
17563
  continue;
17049
17564
  }
17050
17565
  resolvedTools.push({
@@ -17112,6 +17627,10 @@ var AnthropicBackend = class {
17112
17627
  toolsUsed
17113
17628
  });
17114
17629
  });
17630
+ recordToolResult(toolsUsed, {
17631
+ id: outcome.id,
17632
+ name: outcome.name
17633
+ }, resultStr, true);
17115
17634
  this.pushToolMessages(messages, {
17116
17635
  id: toolId,
17117
17636
  name: outcome.name,
@@ -17119,16 +17638,22 @@ var AnthropicBackend = class {
17119
17638
  }, resultStr);
17120
17639
  } else {
17121
17640
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17641
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17122
17642
  this.logger.error("[Tool Execution] Tool failed", {
17123
17643
  model,
17124
17644
  toolName: outcome.name,
17125
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
17645
+ error: errorMessage
17126
17646
  });
17647
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
17648
+ recordToolResult(toolsUsed, {
17649
+ id: outcome.id,
17650
+ name: outcome.name
17651
+ }, observation, false);
17127
17652
  this.pushToolMessages(messages, {
17128
17653
  id: toolId,
17129
17654
  name: outcome.name,
17130
17655
  parameters: outcome.parameters
17131
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
17656
+ }, observation);
17132
17657
  }
17133
17658
  }
17134
17659
  await cb(["\n\n"], { toolsUsed });
@@ -17265,7 +17790,13 @@ var AnthropicBackend = class {
17265
17790
  isMcpTool,
17266
17791
  streaming: false
17267
17792
  }, messages);
17268
- 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
+ }
17269
17800
  resolvedTools.push({
17270
17801
  id: id ?? "",
17271
17802
  name,
@@ -17327,6 +17858,10 @@ var AnthropicBackend = class {
17327
17858
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17328
17859
  await cb(results, { toolsUsed });
17329
17860
  });
17861
+ recordToolResult(toolsUsed, {
17862
+ id: outcome.id,
17863
+ name: outcome.name
17864
+ }, resultStr, true);
17330
17865
  this.pushToolMessages(messages, {
17331
17866
  id: toolId,
17332
17867
  name: outcome.name,
@@ -17334,16 +17869,22 @@ var AnthropicBackend = class {
17334
17869
  }, resultStr);
17335
17870
  } else {
17336
17871
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17872
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17337
17873
  this.logger.error("[Tool Execution] Tool failed (non-streaming)", {
17338
17874
  model,
17339
17875
  toolName: outcome.name,
17340
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
17876
+ error: errorMessage
17341
17877
  });
17878
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
17879
+ recordToolResult(toolsUsed, {
17880
+ id: outcome.id,
17881
+ name: outcome.name
17882
+ }, observation, false);
17342
17883
  this.pushToolMessages(messages, {
17343
17884
  id: toolId,
17344
17885
  name: outcome.name,
17345
17886
  parameters: outcome.parameters
17346
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
17887
+ }, observation);
17347
17888
  }
17348
17889
  }
17349
17890
  this.logger.info("[Tool Execution] Making recursive call after tool execution (non-streaming)", {
@@ -17597,7 +18138,7 @@ var AnthropicBackend = class {
17597
18138
  consolidateSystemMessages(messages) {
17598
18139
  const systemMessages = messages.filter((m) => m.role === "system");
17599
18140
  if (systemMessages.length === 0) return void 0;
17600
- return systemMessages.map((m) => m.content).join("\n");
18141
+ return systemMessages.map((m) => systemContentToText(m.content)).filter((text) => text.trim() !== "").join("\n");
17601
18142
  }
17602
18143
  isToolUseEvent(event) {
17603
18144
  return typeof event === "object" && event !== null && "type" in event && event.type === "tool_use" && "name" in event && "input" in event && "id" in event;
@@ -17740,6 +18281,15 @@ var BaseBedrockBackend = class {
17740
18281
  const command = new InvokeModelWithResponseStreamCommand(input);
17741
18282
  return this._bedrockRuntime.send(command, { abortSignal });
17742
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
+ }
17743
18293
  updateClientForModel(model) {
17744
18294
  const requiredRegion = this.getRegionForModel(model);
17745
18295
  this._options.region = requiredRegion;
@@ -17765,8 +18315,14 @@ var BaseBedrockBackend = class {
17765
18315
  }, callback, toolsUsed);
17766
18316
  return;
17767
18317
  }
17768
- const messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
18318
+ let messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
17769
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
+ }
17770
18326
  let formattedMessages = this.formatMessages(messagesWithFormat);
17771
18327
  let input = this.getPayload(model, formattedMessages, options);
17772
18328
  const contextWindow = this.getModelContextWindow(model);
@@ -17900,6 +18456,12 @@ var BaseBedrockBackend = class {
17900
18456
  });
17901
18457
  } catch {
17902
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);
17903
18465
  }
17904
18466
  }
17905
18467
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -17927,32 +18489,40 @@ var BaseBedrockBackend = class {
17927
18489
  parameters: resolvedTools[i].parameters,
17928
18490
  error: outcome.error
17929
18491
  });
18492
+ const roundReasoningBlocks = this.takeReasoningBlocks();
17930
18493
  for (const outcome of outcomes) if (outcome.ok) {
17931
18494
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17932
18495
  await callback(results, buildCompletionInfo());
17933
18496
  });
18497
+ const resultStr = outcome.result.toString();
18498
+ recordToolResult(toolsUsed, {
18499
+ id: outcome.id,
18500
+ name: outcome.name
18501
+ }, resultStr, true);
17934
18502
  this.pushToolMessages(messages, {
17935
18503
  id: outcome.id,
17936
18504
  name: outcome.name,
17937
18505
  parameters: outcome.parameters
17938
- }, outcome.result.toString());
18506
+ }, resultStr, roundReasoningBlocks);
17939
18507
  } else {
17940
18508
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17941
18509
  if (isAbortError(outcome.error)) throw outcome.error;
17942
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);
17943
18517
  this.pushToolMessages(messages, {
17944
18518
  id: outcome.id,
17945
18519
  name: outcome.name,
17946
18520
  parameters: outcome.parameters
17947
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
18521
+ }, observation, roundReasoningBlocks);
17948
18522
  }
17949
18523
  await callback(["\n\n"], buildCompletionInfo());
17950
18524
  await this.complete(model, messages, {
17951
18525
  ...options,
17952
- thinking: {
17953
- enabled: false,
17954
- budget_tokens: 0
17955
- },
17956
18526
  tool_choice: "auto",
17957
18527
  _internal: {
17958
18528
  ...options._internal,
@@ -17996,35 +18566,38 @@ var BaseBedrockBackend = class {
17996
18566
  if (options.executeTools !== false) {
17997
18567
  const executable = toolChoices.map((tc) => tc.tool).filter((tool) => tool.id && tool.name && options.tools?.some((o) => o.toolSchema.name === tool.name));
17998
18568
  if (executable.length > 0) {
18569
+ const roundReasoningBlocks = this.takeReasoningBlocks();
17999
18570
  for (const { id, name, parameters } of executable) {
18000
18571
  const toolFn = options.tools?.find((o) => o.toolSchema.name === name)?.toolFn;
18001
18572
  if (!toolFn) continue;
18002
18573
  const safeParameters = parameters || "{}";
18003
18574
  let result;
18575
+ let succeeded = true;
18004
18576
  try {
18005
18577
  result = await toolFn(JSON.parse(safeParameters));
18006
18578
  } catch (err) {
18007
18579
  if (err instanceof PermissionDeniedError) throw err;
18008
18580
  if (isAbortError(err)) throw err;
18009
18581
  Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${name} failed:`, err instanceof Error ? err.message : String(err));
18582
+ succeeded = false;
18010
18583
  result = `Error processing ${name} tool: ${err instanceof Error ? err.message : "Unknown error"}`;
18011
18584
  }
18012
18585
  await handleToolResultStreaming(name, result, async (results) => {
18013
18586
  await callback(results, buildCompletionInfo());
18014
18587
  });
18588
+ recordToolResult(toolsUsed, {
18589
+ id,
18590
+ name
18591
+ }, result.toString(), succeeded);
18015
18592
  this.pushToolMessages(messages, {
18016
18593
  id,
18017
18594
  name,
18018
18595
  parameters
18019
- }, result.toString());
18596
+ }, result.toString(), roundReasoningBlocks);
18020
18597
  }
18021
18598
  await callback(["\n\n"], buildCompletionInfo());
18022
18599
  await this.complete(model, messages, {
18023
18600
  ...options,
18024
- thinking: {
18025
- enabled: false,
18026
- budget_tokens: 0
18027
- },
18028
18601
  tool_choice: "auto",
18029
18602
  _internal: {
18030
18603
  ...options._internal,
@@ -18119,6 +18692,9 @@ function isToolUseContentBlock(content_block) {
18119
18692
  function isThinkingContentBlock(content_block) {
18120
18693
  return isRecord(content_block) && content_block.type === "thinking";
18121
18694
  }
18695
+ function isRedactedThinkingContentBlock(content_block) {
18696
+ return isRecord(content_block) && content_block.type === "redacted_thinking";
18697
+ }
18122
18698
  function isTextDelta(delta) {
18123
18699
  return isRecord(delta) && delta.type === "text_delta" && "text" in delta;
18124
18700
  }
@@ -18128,6 +18704,9 @@ function isInputJsonDelta(delta) {
18128
18704
  function isThinkingDelta(delta) {
18129
18705
  return isRecord(delta) && delta.type === "thinking_delta" && "thinking" in delta;
18130
18706
  }
18707
+ function isSignatureDelta(delta) {
18708
+ return isRecord(delta) && delta.type === "signature_delta" && "signature" in delta;
18709
+ }
18131
18710
  function isMessageStart(chunk) {
18132
18711
  return isRecord(chunk) && chunk.type === "message_start";
18133
18712
  }
@@ -18155,12 +18734,35 @@ const TEMPERATURE_ONLY_MODELS = [
18155
18734
  ];
18156
18735
  var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18157
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 = [];
18158
18743
  /** Catalog view of the model being completed; see DispatchModel. */
18159
18744
  _dispatch = new DispatchModel();
18160
18745
  setDispatchModel(info) {
18161
18746
  this._dispatch.set(info);
18162
18747
  }
18163
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
+ /**
18164
18766
  * The record the payload is shaped from: the adapter table first, then the
18165
18767
  * catalog for a model the table never listed. Table-first keeps every
18166
18768
  * currently-dispatched Bedrock id on exactly today's payload.
@@ -18577,11 +19179,13 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18577
19179
  content: ""
18578
19180
  };
18579
19181
  }).filter((m) => m.content !== "" && (Array.isArray(m.content) ? m.content.length > 0 : true));
18580
- let systemMessage = messages.filter((m) => m.role === "system" && m.content).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
18581
- if (!options.omitIdentityReminder) {
18582
- const modelIdentity = `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
18583
- systemMessage = systemMessage ? `${systemMessage}\n${modelIdentity}` : modelIdentity;
18584
- }
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;
18585
19189
  const modelId = model.includes(":") || model.startsWith("global.") || model.startsWith("us.") || model.startsWith("anthropic.") ? model : `anthropic.${model}`;
18586
19190
  const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : 4096;
18587
19191
  const body = {
@@ -18589,7 +19193,27 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18589
19193
  max_tokens: maxTokens,
18590
19194
  messages: filteredMessages
18591
19195
  };
18592
- 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;
18593
19217
  if (options.tools?.length) body.tools = this.formatTools(options.tools);
18594
19218
  if (typeof options.temperature === "number" && !this.omitsSamplingParams(model)) body.temperature = options.temperature;
18595
19219
  if (typeof options.topP === "number" && !TEMPERATURE_ONLY_MODELS.includes(model) && !this.omitsSamplingParams(model)) body.top_p = options.topP;
@@ -18617,8 +19241,6 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18617
19241
  topP: options.topP
18618
19242
  }
18619
19243
  }, null, 2)}`);
18620
- const cacheStrategy = options.cacheStrategy;
18621
- const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
18622
19244
  if (cacheStrategy?.enableCaching && modelSupportsCaching) {
18623
19245
  const cachedBody = getCachingAdapter(ModelBackend.Bedrock).applyCaching(body, cacheStrategy);
18624
19246
  Object.assign(body, cachedBody);
@@ -18642,9 +19264,11 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18642
19264
  formatMessages(messages) {
18643
19265
  return messages.reduce((cur, value) => {
18644
19266
  const previousMessage = cur[cur.length - 1];
18645
- if (previousMessage && value.role === previousMessage.role) {
18646
- if (previousMessage.content === value.content) return cur;
18647
- 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)) {
18648
19272
  const lastIndex = cur.length - 1;
18649
19273
  const prevContent = typeof cur[lastIndex].content === "string" ? cur[lastIndex].content : "";
18650
19274
  const currContent = typeof value.content === "string" ? value.content : "";
@@ -18664,6 +19288,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18664
19288
  });
18665
19289
  if (contentArray.length > 0) cur[lastIndex].content = contentArray;
18666
19290
  }
19291
+ if (value.cache === true) cur[lastIndex].cache = true;
18667
19292
  } else {
18668
19293
  if (typeof value.content !== "string") {
18669
19294
  cur.push(value);
@@ -18675,6 +19300,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18675
19300
  type: "text",
18676
19301
  text: textContent
18677
19302
  }];
19303
+ if (value.cache === true) previousMessage.cache = true;
18678
19304
  }
18679
19305
  return cur;
18680
19306
  }
@@ -18685,11 +19311,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18685
19311
  translateChunk(model, chunk) {
18686
19312
  try {
18687
19313
  const response = chunk;
18688
- const thinkingBlocks = response.content.filter((c) => c.type === "thinking");
18689
- if (thinkingBlocks.length > 0) console.warn(`[AnthropicBedrockBackend] Unexpected thinking blocks in response`, {
18690
- thinkingBlockCount: thinkingBlocks.length,
18691
- thinkingLengths: thinkingBlocks.map((b) => b.thinking?.length || 0)
18692
- });
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`);
18693
19316
  const textContent = response.content.filter((item) => item.type === "text").map((item) => item.text || "").join("");
18694
19317
  const toolUseBlocks = response.content.filter((item) => item.type === "tool_use");
18695
19318
  let choice;
@@ -18743,6 +19366,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18743
19366
  try {
18744
19367
  if (isMessageStart(chunk)) {
18745
19368
  this.isInThinkingBlock = false;
19369
+ this.assistantReasoningBlocks = [];
18746
19370
  choice = {
18747
19371
  chunkText: "",
18748
19372
  usage: {
@@ -18764,8 +19388,12 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18764
19388
  };
18765
19389
  else if (isThinkingContentBlock(contentBlock)) {
18766
19390
  this.isInThinkingBlock = true;
19391
+ this.assistantReasoningBlocks[chunk.index] = {
19392
+ ...contentBlock,
19393
+ thinking: contentBlock.thinking ?? ""
19394
+ };
18767
19395
  choice.chunkText = "<think>";
18768
- }
19396
+ } else if (isRedactedThinkingContentBlock(contentBlock)) this.assistantReasoningBlocks[chunk.index] = { ...contentBlock };
18769
19397
  } else if (isContentBlockDelta(chunk)) {
18770
19398
  choice = {
18771
19399
  status: "stream",
@@ -18775,7 +19403,14 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18775
19403
  const delta = chunk.delta;
18776
19404
  if (isTextDelta(delta)) choice.chunkText = delta.text;
18777
19405
  else if (isInputJsonDelta(delta)) choice.chunkText = delta.partial_json;
18778
- 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
+ }
18779
19414
  } else if (isContentBlockStop(chunk)) {
18780
19415
  choice = {
18781
19416
  status: "stream",
@@ -18834,7 +19469,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18834
19469
  name: tool.name,
18835
19470
  input: JSON.parse(tool.parameters || "{}")
18836
19471
  };
18837
- 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];
18838
19474
  messages.push({
18839
19475
  role: "assistant",
18840
19476
  content: assistantContent
@@ -19966,7 +20602,7 @@ var BFLBackend = class {
19966
20602
  input: 400,
19967
20603
  output: 400
19968
20604
  } },
19969
- 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.",
19970
20606
  supportsSafetyTolerance: true,
19971
20607
  rank: 1
19972
20608
  },
@@ -20032,7 +20668,7 @@ var BFLBackend = class {
20032
20668
  input: 350,
20033
20669
  output: 350
20034
20670
  } },
20035
- 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.",
20036
20672
  supportsSafetyTolerance: true,
20037
20673
  rank: 4
20038
20674
  },
@@ -20048,7 +20684,7 @@ var BFLBackend = class {
20048
20684
  input: 450,
20049
20685
  output: 450
20050
20686
  } },
20051
- 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.",
20052
20688
  supportsSafetyTolerance: true,
20053
20689
  rank: 5
20054
20690
  }
@@ -20506,6 +21142,7 @@ var GeminiBackend = class {
20506
21142
  const toolCallCount = options._internal?.toolCallCount ?? 0;
20507
21143
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
20508
21144
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
21145
+ const liveToolUseIds = new Set(options._internal?.liveToolUseIds ?? []);
20509
21146
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
20510
21147
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
20511
21148
  this.logger.warn(`[Gemini] Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -20520,7 +21157,7 @@ var GeminiBackend = class {
20520
21157
  const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
20521
21158
  const systemInstruction = messagesWithFormat.filter((message) => message.role === "system").map((message) => message.content).join("\n");
20522
21159
  const nonsystemMessages = messagesWithFormat.filter((message) => message.role !== "system");
20523
- const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages);
21160
+ const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages, liveToolUseIds);
20524
21161
  const generationConfig = this.getGenerationConfig(modelInfo, options);
20525
21162
  const tools = !options.tools?.length ? void 0 : [{ functionDeclarations: options.tools.map((tool) => {
20526
21163
  const params = pick(tool.toolSchema.parameters, "type", "properties", "required");
@@ -20647,22 +21284,32 @@ var GeminiBackend = class {
20647
21284
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20648
21285
  await callback(results, { toolsUsed });
20649
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);
20650
21292
  messages.push({
20651
21293
  role: "tool",
20652
21294
  content: [{
20653
21295
  type: "tool_result",
20654
- content: JSON.stringify({ result: outcome.result }),
21296
+ content: resultContent,
20655
21297
  tool_use_id: outcome.toolCall.id
20656
21298
  }]
20657
21299
  });
20658
21300
  } else {
20659
21301
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20660
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);
20661
21308
  messages.push({
20662
21309
  role: "tool",
20663
21310
  content: [{
20664
21311
  type: "tool_result",
20665
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
21312
+ content: errorContent,
20666
21313
  tool_use_id: outcome.toolCall.id
20667
21314
  }]
20668
21315
  });
@@ -20674,7 +21321,8 @@ var GeminiBackend = class {
20674
21321
  ...options._internal,
20675
21322
  toolCallCount: toolCallCount + 1,
20676
21323
  accumInputTokens: accumInputTokens + turnInputTokens,
20677
- accumOutputTokens: accumOutputTokens + turnOutputTokens
21324
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
21325
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20678
21326
  }
20679
21327
  }, callback, toolsUsed);
20680
21328
  } else {
@@ -20773,22 +21421,32 @@ var GeminiBackend = class {
20773
21421
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20774
21422
  await callback(results, { toolsUsed });
20775
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);
20776
21429
  messages.push({
20777
21430
  role: "tool",
20778
21431
  content: [{
20779
21432
  type: "tool_result",
20780
- content: JSON.stringify({ result: outcome.result }),
21433
+ content: resultContent,
20781
21434
  tool_use_id: outcome.toolCall.id
20782
21435
  }]
20783
21436
  });
20784
21437
  } else {
20785
21438
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20786
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);
20787
21445
  messages.push({
20788
21446
  role: "tool",
20789
21447
  content: [{
20790
21448
  type: "tool_result",
20791
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
21449
+ content: errorContent,
20792
21450
  tool_use_id: outcome.toolCall.id
20793
21451
  }]
20794
21452
  });
@@ -20800,7 +21458,8 @@ var GeminiBackend = class {
20800
21458
  ...options._internal,
20801
21459
  toolCallCount: toolCallCount + 1,
20802
21460
  accumInputTokens: accumInputTokens + turnInputTokens,
20803
- accumOutputTokens: accumOutputTokens + turnOutputTokens
21461
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
21462
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20804
21463
  }
20805
21464
  }, callback, toolsUsed);
20806
21465
  } else {
@@ -20809,8 +21468,9 @@ var GeminiBackend = class {
20809
21468
  }
20810
21469
  }
20811
21470
  }
20812
- formatMessagesIntoGeminiContent(messages) {
21471
+ formatMessagesIntoGeminiContent(messages, liveToolUseIds) {
20813
21472
  const toolUseIdToName = /* @__PURE__ */ new Map();
21473
+ const droppedToolUseIds = /* @__PURE__ */ new Set();
20814
21474
  return messages.map((message) => {
20815
21475
  const mapRole = (role) => {
20816
21476
  switch (role) {
@@ -20824,25 +21484,41 @@ var GeminiBackend = class {
20824
21484
  role: mapRole(message.role),
20825
21485
  parts: [{ text: message.content }]
20826
21486
  };
20827
- 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 {
20828
21489
  role: mapRole(message.role),
20829
21490
  parts: [{ text: (message.content?.[0]).text }]
20830
21491
  };
20831
- if (message.content?.[0].type === "image") return {
21492
+ if (!hasToolUse && message.content?.[0].type === "image") return {
20832
21493
  role: mapRole(message.role),
20833
21494
  parts: [{ inlineData: {
20834
21495
  mimeType: (message.content?.[0]).source.media_type,
20835
21496
  data: (message.content?.[0]).source.data
20836
21497
  } }]
20837
21498
  };
20838
- if (message.content?.[0].type === "tool_use") {
20839
- 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) => {
20840
21516
  toolUseIdToName.set(toolUse.id, toolUse.name);
20841
21517
  const part = { functionCall: {
20842
21518
  name: toolUse.name,
20843
21519
  args: toolUse.input
20844
21520
  } };
20845
- if (index === 0 && toolUse.thought_signature) {
21521
+ if (index === 0) if (toolUse.thought_signature) {
20846
21522
  part.thoughtSignature = toolUse.thought_signature;
20847
21523
  part.thought_signature = toolUse.thought_signature;
20848
21524
  this.logger.debug("[Gemini] Including thought_signature in request (both formats):", {
@@ -20850,29 +21526,26 @@ var GeminiBackend = class {
20850
21526
  id: toolUse.id,
20851
21527
  position: "first"
20852
21528
  });
20853
- } else if (index === 0 && !toolUse.thought_signature) {
20854
- this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
20855
- name: toolUse.name,
20856
- id: toolUse.id,
20857
- messageRole: message.role
20858
- });
20859
- this.logger.warn("[Gemini] This may cause a 400 error with Gemini 3 Pro");
20860
- }
21529
+ } else this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
21530
+ name: toolUse.name,
21531
+ id: toolUse.id
21532
+ });
20861
21533
  return part;
20862
- });
21534
+ }));
20863
21535
  return {
20864
21536
  role: mapRole(message.role),
20865
21537
  parts
20866
21538
  };
20867
21539
  }
20868
21540
  if (message.content?.[0].type === "tool_result") {
20869
- 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;
20870
21546
  return {
20871
21547
  role: mapRole(message.role),
20872
- parts: [{ functionResponse: {
20873
- name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
20874
- response: { result: (message.content?.[0]).content }
20875
- } }]
21548
+ parts
20876
21549
  };
20877
21550
  }
20878
21551
  return null;
@@ -20959,6 +21632,54 @@ var GeminiBackend = class {
20959
21632
  }
20960
21633
  };
20961
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
+ /**
20962
21683
  * Request shaping for Moonshot's Kimi models. Kept separate from kimiBackend's
20963
21684
  * transport so every "which parameter does this id accept" rule is one pure
20964
21685
  * function with a test, rather than a conditional buried in a 400-line complete().
@@ -21347,7 +22068,7 @@ var KimiBackend = class {
21347
22068
  if (!(response instanceof Stream)) {
21348
22069
  const streamedText = [];
21349
22070
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from the Moonshot API");
21350
- const turnCacheReadTokens = this.cachedTokensOf(response.usage);
22071
+ const turnCacheReadTokens = cachedTokensFromUsage(response.usage);
21351
22072
  for (const c of response.choices) {
21352
22073
  if (!c.message) continue;
21353
22074
  const reasoningContent = c.message.reasoning_content;
@@ -21379,6 +22100,10 @@ var KimiBackend = class {
21379
22100
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
21380
22101
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
21381
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);
21382
22107
  }
21383
22108
  }
21384
22109
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21406,18 +22131,30 @@ var KimiBackend = class {
21406
22131
  parameters: resolvedTools[i].parameters,
21407
22132
  error: outcome.error
21408
22133
  });
21409
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21410
- id: outcome.id,
21411
- name: outcome.name,
21412
- parameters: outcome.parameters
21413
- }, outcome.result.toString());
21414
- 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 {
21415
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);
21416
22153
  this.pushToolMessages(messages, {
21417
22154
  id: outcome.id,
21418
22155
  name: outcome.name,
21419
22156
  parameters: outcome.parameters
21420
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22157
+ }, observation);
21421
22158
  }
21422
22159
  await this.complete(model, messages, {
21423
22160
  ...options,
@@ -21433,7 +22170,7 @@ var KimiBackend = class {
21433
22170
  } else {
21434
22171
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21435
22172
  await callback([null], {
21436
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
22173
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
21437
22174
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21438
22175
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21439
22176
  });
@@ -21456,7 +22193,7 @@ var KimiBackend = class {
21456
22193
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
21457
22194
  const totalCacheReadTokens = accumCacheReadTokens + turnCacheReadTokens;
21458
22195
  await callback(streamedText, {
21459
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
22196
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
21460
22197
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21461
22198
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21462
22199
  cacheStats,
@@ -21475,7 +22212,7 @@ var KimiBackend = class {
21475
22212
  if (chunk.usage) {
21476
22213
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
21477
22214
  outputTokens += chunk.usage?.completion_tokens || 0;
21478
- const chunkCached = this.cachedTokensOf(chunk.usage);
22215
+ const chunkCached = cachedTokensFromUsage(chunk.usage);
21479
22216
  if (chunkCached > 0) cachedTokensFromStream = chunkCached;
21480
22217
  }
21481
22218
  chunk?.choices.forEach((c) => {
@@ -21505,7 +22242,7 @@ var KimiBackend = class {
21505
22242
  if (streamedText.some((t) => t)) sawAnyText = true;
21506
22243
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
21507
22244
  await callback(streamedText, {
21508
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22245
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21509
22246
  outputTokens: accumOutputTokens + outputTokens,
21510
22247
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21511
22248
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -21513,7 +22250,7 @@ var KimiBackend = class {
21513
22250
  }
21514
22251
  if (isInThinkingBlock) {
21515
22252
  await callback(["</think>"], {
21516
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22253
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21517
22254
  outputTokens: accumOutputTokens + outputTokens,
21518
22255
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21519
22256
  });
@@ -21530,7 +22267,7 @@ var KimiBackend = class {
21530
22267
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
21531
22268
  }
21532
22269
  if (nativeFormat && func.length === 0) await callback([], {
21533
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22270
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21534
22271
  outputTokens: accumOutputTokens + outputTokens,
21535
22272
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21536
22273
  responseFormatMode: "native",
@@ -21566,6 +22303,10 @@ var KimiBackend = class {
21566
22303
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
21567
22304
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
21568
22305
  if (entry) entry.arguments = "{}";
22306
+ recordToolResult(toolsUsed, {
22307
+ id,
22308
+ name
22309
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
21569
22310
  }
21570
22311
  }
21571
22312
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21593,18 +22334,30 @@ var KimiBackend = class {
21593
22334
  parameters: resolvedTools[i].parameters,
21594
22335
  error: outcome.error
21595
22336
  });
21596
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21597
- id: outcome.id,
21598
- name: outcome.name,
21599
- parameters: outcome.parameters
21600
- }, outcome.result.toString());
21601
- 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 {
21602
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);
21603
22356
  this.pushToolMessages(messages, {
21604
22357
  id: outcome.id,
21605
22358
  name: outcome.name,
21606
22359
  parameters: outcome.parameters
21607
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22360
+ }, observation);
21608
22361
  }
21609
22362
  await this.complete(model, messages, {
21610
22363
  ...options,
@@ -21619,52 +22372,13 @@ var KimiBackend = class {
21619
22372
  } else {
21620
22373
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21621
22374
  await callback([null], {
21622
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22375
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21623
22376
  outputTokens: accumOutputTokens + outputTokens,
21624
22377
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21625
22378
  });
21626
22379
  }
21627
22380
  }
21628
22381
  }
21629
- /**
21630
- * Convert Moonshot's CACHE-INCLUSIVE `prompt_tokens` into the cache-EXCLUSIVE
21631
- * convention getTextModelCost expects, which is Anthropic's: `inputTokens`
21632
- * counts only uncached tokens and cache reads are billed separately at their
21633
- * own (much cheaper) rate.
21634
- *
21635
- * Verified live 2026-07-28: a repeated 1220-token prompt returned
21636
- * `prompt_tokens: 1220` WITH `cached_tokens: 1220` - the same tokens, reported
21637
- * twice, not 1220 fresh plus 1220 cached. So passing prompt_tokens through as
21638
- * inputTokens while also forwarding cacheReadInputTokens would bill the cached
21639
- * portion twice; passing it through and forwarding NOTHING (the previous
21640
- * behavior) charges the full input rate on tokens Moonshot billed at roughly a
21641
- * sixth of it - on k2.6, $0.95/MTok against $0.16. Subtracting is the only
21642
- * split that bills what the provider actually charged.
21643
- *
21644
- * Clamped at zero: if a feed ever reports more cached than prompt tokens, a
21645
- * negative input count would silently credit the user.
21646
- */
21647
- splitCachedInput(totalPromptTokens, cacheReadTokens) {
21648
- if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
21649
- const cached = Math.min(cacheReadTokens, totalPromptTokens);
21650
- return {
21651
- inputTokens: Math.max(0, totalPromptTokens - cached),
21652
- cacheReadInputTokens: cached
21653
- };
21654
- }
21655
- /**
21656
- * Cached prompt tokens from a usage object. Moonshot publishes BOTH a flat
21657
- * `usage.cached_tokens` and the nested OpenAI `prompt_tokens_details.cached_tokens`
21658
- * - confirmed live, both present with the same value - so either spelling is
21659
- * accepted and reading neither would bill every hit at the full input rate.
21660
- */
21661
- cachedTokensOf(usage) {
21662
- if (!usage) return 0;
21663
- const flat = usage.cached_tokens;
21664
- if (typeof flat === "number" && Number.isFinite(flat) && flat >= 0) return flat;
21665
- const nested = usage.prompt_tokens_details?.cached_tokens;
21666
- return typeof nested === "number" && Number.isFinite(nested) && nested >= 0 ? nested : 0;
21667
- }
21668
22382
  formatMessages(messages) {
21669
22383
  return convertMessagesToOpenAIFormat(messages);
21670
22384
  }
@@ -21969,7 +22683,7 @@ var OllamaBackend = class OllamaBackend {
21969
22683
  name: tc.name,
21970
22684
  parameters: tc.arguments || "{}"
21971
22685
  }, `Error: tool "${tc.name}" is not available. Do not call it again; answer directly or use a listed tool.`);
21972
- (await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
22686
+ const outcomes = await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
21973
22687
  let params = {};
21974
22688
  try {
21975
22689
  params = JSON.parse(tc.arguments || "{}");
@@ -21979,17 +22693,22 @@ var OllamaBackend = class OllamaBackend {
21979
22693
  }), {
21980
22694
  parallel: options.parallelToolExecution !== false,
21981
22695
  maxConcurrency: options.maxParallelTools
21982
- })).forEach((outcome, i) => {
22696
+ });
22697
+ const observations = [];
22698
+ outcomes.forEach((outcome, i) => {
21983
22699
  const { tc } = resolved[i];
21984
22700
  const params = tc.arguments || "{}";
21985
- if (outcome.ok) this.pushToolMessages(messages, {
21986
- id: tc.id,
21987
- name: tc.name,
21988
- parameters: params
21989
- }, outcome.result);
21990
- 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 {
21991
22709
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
21992
22710
  const errorMsg = `Error running ${tc.name}: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
22711
+ observations[i] = errorMsg;
21993
22712
  this.pushToolMessages(messages, {
21994
22713
  id: tc.id,
21995
22714
  name: tc.name,
@@ -21997,10 +22716,12 @@ var OllamaBackend = class OllamaBackend {
21997
22716
  }, errorMsg);
21998
22717
  }
21999
22718
  });
22000
- const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }) => ({
22719
+ const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }, i) => ({
22001
22720
  name: tc.name,
22002
22721
  arguments: tc.arguments,
22003
- id: tc.id
22722
+ id: tc.id,
22723
+ returnValue: truncateToolResult(String(observations[i])),
22724
+ success: outcomes[i].ok
22004
22725
  }))];
22005
22726
  if (options.abortSignal?.aborted) {
22006
22727
  await callback([""], {
@@ -22108,12 +22829,21 @@ var OllamaBackend = class OllamaBackend {
22108
22829
  }
22109
22830
  };
22110
22831
  }
22111
- /** 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
+ */
22112
22842
  normalizeToolCalls(toolCalls) {
22113
- return toolCalls.map((tc, i) => ({
22843
+ return toolCalls.map((tc) => ({
22114
22844
  name: tc.function.name,
22115
22845
  arguments: JSON.stringify(tc.function.arguments ?? {}),
22116
- id: `ollama-tool-${i}-${tc.function.name}`
22846
+ id: `ollama-tool-${v4()}`
22117
22847
  }));
22118
22848
  }
22119
22849
  /**
@@ -22151,7 +22881,7 @@ var OllamaBackend = class OllamaBackend {
22151
22881
  seen.add(key);
22152
22882
  calls.push({
22153
22883
  ...call,
22154
- id: `ollama-content-tool-${calls.length}-${call.name}`
22884
+ id: `ollama-content-tool-${v4()}`
22155
22885
  });
22156
22886
  }
22157
22887
  return calls;
@@ -22400,7 +23130,8 @@ var OpenAIBackend = class {
22400
23130
  can_stream: true,
22401
23131
  pricing: { 1047576: {
22402
23132
  input: 2 / 1e6,
22403
- output: 8 / 1e6
23133
+ output: 8 / 1e6,
23134
+ cache_read: .5 / 1e6
22404
23135
  } },
22405
23136
  supportsVision: true,
22406
23137
  supportsTools: true,
@@ -22420,7 +23151,8 @@ var OpenAIBackend = class {
22420
23151
  can_stream: true,
22421
23152
  pricing: { 1047576: {
22422
23153
  input: .4 / 1e6,
22423
- output: 1.6 / 1e6
23154
+ output: 1.6 / 1e6,
23155
+ cache_read: .1 / 1e6
22424
23156
  } },
22425
23157
  supportsVision: true,
22426
23158
  supportsTools: true,
@@ -22440,7 +23172,8 @@ var OpenAIBackend = class {
22440
23172
  can_stream: true,
22441
23173
  pricing: { 1047576: {
22442
23174
  input: .1 / 1e6,
22443
- output: .4 / 1e6
23175
+ output: .4 / 1e6,
23176
+ cache_read: .025 / 1e6
22444
23177
  } },
22445
23178
  supportsVision: true,
22446
23179
  supportsTools: true,
@@ -22503,7 +23236,8 @@ var OpenAIBackend = class {
22503
23236
  can_stream: true,
22504
23237
  pricing: { 2e5: {
22505
23238
  input: 2 / 1e6,
22506
- output: 8 / 1e6
23239
+ output: 8 / 1e6,
23240
+ cache_read: .5 / 1e6
22507
23241
  } },
22508
23242
  supportsVision: true,
22509
23243
  supportsTools: true,
@@ -22589,7 +23323,8 @@ var OpenAIBackend = class {
22589
23323
  can_think: true,
22590
23324
  pricing: { 2e5: {
22591
23325
  input: 1.1 / 1e6,
22592
- output: 4.4 / 1e6
23326
+ output: 4.4 / 1e6,
23327
+ cache_read: .275 / 1e6
22593
23328
  } },
22594
23329
  supportsVision: true,
22595
23330
  supportsImageVariation: false,
@@ -22965,7 +23700,8 @@ var OpenAIBackend = class {
22965
23700
  can_stream: true,
22966
23701
  pricing: { 8e3: {
22967
23702
  input: 2.5 / 1e6,
22968
- output: 10 / 1e6
23703
+ output: 10 / 1e6,
23704
+ cache_read: 1.25 / 1e6
22969
23705
  } },
22970
23706
  supportsVision: true,
22971
23707
  supportsImageVariation: false,
@@ -22984,7 +23720,8 @@ var OpenAIBackend = class {
22984
23720
  can_stream: true,
22985
23721
  pricing: { 8e3: {
22986
23722
  input: .15 / 1e6,
22987
- output: .6 / 1e6
23723
+ output: .6 / 1e6,
23724
+ cache_read: .075 / 1e6
22988
23725
  } },
22989
23726
  supportsVision: true,
22990
23727
  supportsImageVariation: false,
@@ -23175,6 +23912,7 @@ var OpenAIBackend = class {
23175
23912
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23176
23913
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23177
23914
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
23915
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23178
23916
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
23179
23917
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
23180
23918
  this.logger.warn(`⚠️ Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -23264,6 +24002,7 @@ var OpenAIBackend = class {
23264
24002
  let outputTokens = 0;
23265
24003
  if (!(response instanceof Stream)) {
23266
24004
  const streamedText = [];
24005
+ const totalCacheReadTokens = accumCacheReadTokens + cachedTokensFromUsage(response.usage);
23267
24006
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from OpenAI API");
23268
24007
  for (const c of response.choices) {
23269
24008
  if (!c.message) continue;
@@ -23297,6 +24036,10 @@ var OpenAIBackend = class {
23297
24036
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
23298
24037
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
23299
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);
23300
24043
  }
23301
24044
  }
23302
24045
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23333,6 +24076,10 @@ var OpenAIBackend = class {
23333
24076
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23334
24077
  const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
23335
24078
  streamedText[c.index] = errorMsg;
24079
+ recordToolResult(toolsUsed, {
24080
+ id: outcome.id,
24081
+ name: outcome.name
24082
+ }, errorMsg, false);
23336
24083
  this.pushToolMessages(messages, {
23337
24084
  id: outcome.id,
23338
24085
  name: outcome.name,
@@ -23353,6 +24100,10 @@ var OpenAIBackend = class {
23353
24100
  });
23354
24101
  });
23355
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);
23356
24107
  this.pushToolMessages(messages, {
23357
24108
  id: outcome.id,
23358
24109
  name: outcome.name,
@@ -23376,7 +24127,8 @@ var OpenAIBackend = class {
23376
24127
  ...options._internal,
23377
24128
  toolCallCount: toolCallCount + 1,
23378
24129
  accumInputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
23379
- accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0)
24130
+ accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
24131
+ accumCacheReadTokens: totalCacheReadTokens
23380
24132
  }
23381
24133
  }, recursiveCallback, toolsUsed);
23382
24134
  if (anyArtifactWasStreamed && recursiveBuffer) {
@@ -23387,7 +24139,7 @@ var OpenAIBackend = class {
23387
24139
  } else {
23388
24140
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23389
24141
  await callback([null], {
23390
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24142
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23391
24143
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23392
24144
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23393
24145
  });
@@ -23403,7 +24155,7 @@ var OpenAIBackend = class {
23403
24155
  }
23404
24156
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
23405
24157
  await callback(streamedText, {
23406
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24158
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23407
24159
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23408
24160
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23409
24161
  cacheStats,
@@ -23431,7 +24183,7 @@ var OpenAIBackend = class {
23431
24183
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
23432
24184
  outputTokens += chunk.usage?.completion_tokens || 0;
23433
24185
  if (chunk.usage.prompt_tokens_details?.cached_tokens !== void 0) {
23434
- cachedTokensFromStream = chunk.usage.prompt_tokens_details.cached_tokens;
24186
+ cachedTokensFromStream = Math.max(cachedTokensFromStream, chunk.usage.prompt_tokens_details.cached_tokens);
23435
24187
  if (cachedTokensFromStream > 0) this.logger.debug("[OpenAI] Captured cached tokens", { cachedTokens: cachedTokensFromStream });
23436
24188
  else this.logger.debug("[OpenAI] No cached tokens in chunk", { note: "possible cache miss or first request" });
23437
24189
  }
@@ -23448,7 +24200,7 @@ var OpenAIBackend = class {
23448
24200
  });
23449
24201
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
23450
24202
  await callback(streamedText, {
23451
- inputTokens: accumInputTokens + inputTokens,
24203
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23452
24204
  outputTokens: accumOutputTokens + outputTokens,
23453
24205
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23454
24206
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -23478,7 +24230,7 @@ var OpenAIBackend = class {
23478
24230
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
23479
24231
  }
23480
24232
  if ((isO1Model || func.length === 0) && options.responseFormat?.type === "json_schema") await callback([], {
23481
- inputTokens: accumInputTokens + inputTokens,
24233
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23482
24234
  outputTokens: accumOutputTokens + outputTokens,
23483
24235
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23484
24236
  cacheStats,
@@ -23516,6 +24268,10 @@ var OpenAIBackend = class {
23516
24268
  });
23517
24269
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
23518
24270
  if (entry) entry.arguments = "{}";
24271
+ recordToolResult(toolsUsed, {
24272
+ id,
24273
+ name
24274
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23519
24275
  }
23520
24276
  }
23521
24277
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23550,11 +24306,16 @@ var OpenAIBackend = class {
23550
24306
  for (const outcome of outcomes) {
23551
24307
  if (!outcome.ok) {
23552
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);
23553
24314
  this.pushToolMessages(messages, {
23554
24315
  id: outcome.id,
23555
24316
  name: outcome.name,
23556
24317
  parameters: outcome.parameters
23557
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
24318
+ }, errorMsg);
23558
24319
  continue;
23559
24320
  }
23560
24321
  const resultStr = outcome.result.toString();
@@ -23564,13 +24325,17 @@ var OpenAIBackend = class {
23564
24325
  thisToolHadArtifact = true;
23565
24326
  anyArtifactWasStreamed = true;
23566
24327
  await callback(results, {
23567
- inputTokens: accumInputTokens + inputTokens,
24328
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23568
24329
  outputTokens: accumOutputTokens + outputTokens,
23569
24330
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23570
24331
  cacheStats
23571
24332
  });
23572
24333
  });
23573
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);
23574
24339
  this.pushToolMessages(messages, {
23575
24340
  id: outcome.id,
23576
24341
  name: outcome.name,
@@ -23591,7 +24356,8 @@ var OpenAIBackend = class {
23591
24356
  ...options._internal,
23592
24357
  toolCallCount: toolCallCount + 1,
23593
24358
  accumInputTokens: accumInputTokens + inputTokens,
23594
- accumOutputTokens: accumOutputTokens + outputTokens
24359
+ accumOutputTokens: accumOutputTokens + outputTokens,
24360
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23595
24361
  }
23596
24362
  }, async (results, meta) => {
23597
24363
  for (const r of results) if (r != null) recursiveBuffer += r;
@@ -23607,13 +24373,14 @@ var OpenAIBackend = class {
23607
24373
  ...options._internal,
23608
24374
  toolCallCount: toolCallCount + 1,
23609
24375
  accumInputTokens: accumInputTokens + inputTokens,
23610
- accumOutputTokens: accumOutputTokens + outputTokens
24376
+ accumOutputTokens: accumOutputTokens + outputTokens,
24377
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23611
24378
  }
23612
24379
  }, callback, toolsUsed);
23613
24380
  } else {
23614
24381
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23615
24382
  await callback([null], {
23616
- inputTokens: accumInputTokens + inputTokens,
24383
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23617
24384
  outputTokens: accumOutputTokens + outputTokens,
23618
24385
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23619
24386
  cacheStats
@@ -23755,6 +24522,7 @@ var OpenAIBackend = class {
23755
24522
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23756
24523
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23757
24524
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
24525
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23758
24526
  const chatMessages = this.formatMessages(messages, false, model, options);
23759
24527
  const input = this.toResponsesInput(chatMessages);
23760
24528
  const reasoningEffort = this.resolveReasoningEffort(model, options);
@@ -23781,8 +24549,9 @@ var OpenAIBackend = class {
23781
24549
  let finalResponse;
23782
24550
  let inputTokens = 0;
23783
24551
  let outputTokens = 0;
24552
+ let cachedTokensFromStream = 0;
23784
24553
  for await (const event of stream) if (event.type === "response.output_text.delta") await callback([event.delta], {
23785
- inputTokens: accumInputTokens + inputTokens,
24554
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23786
24555
  outputTokens: accumOutputTokens + outputTokens,
23787
24556
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23788
24557
  });
@@ -23792,11 +24561,12 @@ var OpenAIBackend = class {
23792
24561
  if (!finalResponse) throw new Error(`OpenAI Responses stream for ${model} ended without a terminal response event`);
23793
24562
  inputTokens = finalResponse.usage?.input_tokens ?? 0;
23794
24563
  outputTokens = finalResponse.usage?.output_tokens ?? 0;
24564
+ cachedTokensFromStream = cachedTokensFromUsage(finalResponse.usage);
23795
24565
  const functionCalls = finalResponse.output.filter((item) => item.type === "function_call");
23796
24566
  if (functionCalls.length === 0) {
23797
24567
  const stopReason = normalizeOpenAIResponsesStopReason(finalResponse.incomplete_details?.reason);
23798
24568
  await callback([], {
23799
- inputTokens: accumInputTokens + inputTokens,
24569
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23800
24570
  outputTokens: accumOutputTokens + outputTokens,
23801
24571
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23802
24572
  ...stopReason ? { stopReason } : {}
@@ -23810,7 +24580,7 @@ var OpenAIBackend = class {
23810
24580
  });
23811
24581
  if (options.executeTools === false) {
23812
24582
  await callback([null], {
23813
- inputTokens: accumInputTokens + inputTokens,
24583
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23814
24584
  outputTokens: accumOutputTokens + outputTokens,
23815
24585
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23816
24586
  });
@@ -23831,6 +24601,10 @@ var OpenAIBackend = class {
23831
24601
  });
23832
24602
  } catch {
23833
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);
23834
24608
  }
23835
24609
  }
23836
24610
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23854,14 +24628,24 @@ var OpenAIBackend = class {
23854
24628
  for (let i = 0; i < batchOutcomes.length; i++) {
23855
24629
  const outcome = batchOutcomes[i];
23856
24630
  const r = resolved[i];
23857
- if (outcome.ok) this.pushToolMessages(messages, {
23858
- id: r.callId,
23859
- name: r.name,
23860
- parameters: r.args
23861
- }, outcome.result.result.toString());
23862
- 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 {
23863
24643
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23864
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);
23865
24649
  this.pushToolMessages(messages, {
23866
24650
  id: r.callId,
23867
24651
  name: r.name,
@@ -23878,7 +24662,8 @@ var OpenAIBackend = class {
23878
24662
  ...options._internal,
23879
24663
  toolCallCount: toolCallCount + 1,
23880
24664
  accumInputTokens: accumInputTokens + inputTokens,
23881
- accumOutputTokens: accumOutputTokens + outputTokens
24665
+ accumOutputTokens: accumOutputTokens + outputTokens,
24666
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23882
24667
  }
23883
24668
  }, callback, toolsUsed);
23884
24669
  }
@@ -24236,6 +25021,10 @@ var XAIBackend = class {
24236
25021
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
24237
25022
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
24238
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);
24239
25028
  }
24240
25029
  }
24241
25030
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24263,18 +25052,30 @@ var XAIBackend = class {
24263
25052
  parameters: resolvedTools[i].parameters,
24264
25053
  error: outcome.error
24265
25054
  });
24266
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24267
- id: outcome.id,
24268
- name: outcome.name,
24269
- parameters: outcome.parameters
24270
- }, outcome.result.toString());
24271
- 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 {
24272
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);
24273
25074
  this.pushToolMessages(messages, {
24274
25075
  id: outcome.id,
24275
25076
  name: outcome.name,
24276
25077
  parameters: outcome.parameters
24277
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25078
+ }, observation);
24278
25079
  }
24279
25080
  await this.complete(model, messages, {
24280
25081
  ...options,
@@ -24404,6 +25205,10 @@ var XAIBackend = class {
24404
25205
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
24405
25206
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
24406
25207
  if (entry) entry.arguments = "{}";
25208
+ recordToolResult(toolsUsed, {
25209
+ id,
25210
+ name
25211
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
24407
25212
  }
24408
25213
  }
24409
25214
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24431,18 +25236,30 @@ var XAIBackend = class {
24431
25236
  parameters: resolvedTools[i].parameters,
24432
25237
  error: outcome.error
24433
25238
  });
24434
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24435
- id: outcome.id,
24436
- name: outcome.name,
24437
- parameters: outcome.parameters
24438
- }, outcome.result.toString());
24439
- 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 {
24440
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);
24441
25258
  this.pushToolMessages(messages, {
24442
25259
  id: outcome.id,
24443
25260
  name: outcome.name,
24444
25261
  parameters: outcome.parameters
24445
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25262
+ }, observation);
24446
25263
  }
24447
25264
  await this.complete(model, messages, {
24448
25265
  ...options,
@@ -25603,6 +26420,10 @@ function checkStaleModelReferences(input) {
25603
26420
  for (const referencedId of chain) check("fallback-chain", chainKey, referencedId);
25604
26421
  }
25605
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
+ }
25606
26427
  for (const [source, target] of Object.entries(DEPRECATED_MODEL_MAP)) check("deprecated-model-map", source, target);
25607
26428
  for (const [source, target] of replacedByOverlayEntries()) check("replaced-by-overlay", source, target);
25608
26429
  return found.sort((a, b) => a.surface.localeCompare(b.surface) || a.key.localeCompare(b.key) || a.referencedId.localeCompare(b.referencedId));
@@ -26067,13 +26888,19 @@ const toolUseSchema = z$1.object({
26067
26888
  * Discriminated union of streaming events. `thinking` blocks are opaque
26068
26889
  * provider-shaped objects (Anthropic extended thinking) replayed verbatim into
26069
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.
26070
26896
  */
26071
26897
  const streamEventSchema = z$1.discriminatedUnion("type", [
26072
26898
  z$1.object({
26073
26899
  type: z$1.literal("content"),
26074
26900
  text: z$1.string().optional(),
26075
26901
  usage: usageSchema.optional(),
26076
- credits: creditsSchema.optional()
26902
+ credits: creditsSchema.optional(),
26903
+ stopReason: z$1.string().optional()
26077
26904
  }),
26078
26905
  z$1.object({
26079
26906
  type: z$1.literal("tool_use"),
@@ -26081,7 +26908,8 @@ const streamEventSchema = z$1.discriminatedUnion("type", [
26081
26908
  tools: z$1.array(toolUseSchema).optional(),
26082
26909
  thinking: z$1.array(z$1.unknown()).optional(),
26083
26910
  usage: usageSchema.optional(),
26084
- credits: creditsSchema.optional()
26911
+ credits: creditsSchema.optional(),
26912
+ stopReason: z$1.string().optional()
26085
26913
  }),
26086
26914
  z$1.object({
26087
26915
  type: z$1.literal("error"),
@@ -26150,12 +26978,14 @@ var StreamAccumulator = class {
26150
26978
  case "content":
26151
26979
  this.accumulatedText += event.text ?? "";
26152
26980
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
26981
+ if (event.stopReason) this.stopReason = event.stopReason;
26153
26982
  break;
26154
26983
  case "tool_use":
26155
26984
  if (event.text) this.accumulatedText += event.text;
26156
26985
  if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
26157
26986
  if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
26158
26987
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
26988
+ if (event.stopReason) this.stopReason = event.stopReason;
26159
26989
  }
26160
26990
  }
26161
26991
  /** True when neither text nor tools have been accumulated (stream produced nothing useful). */
@@ -26182,10 +27012,14 @@ var StreamAccumulator = class {
26182
27012
  const info = {
26183
27013
  toolsUsed: this.toolsUsed,
26184
27014
  thinking: this.thinkingBlocks.length > 0 ? this.thinkingBlocks : void 0,
26185
- ...this.lastUsageInfo
27015
+ ...this.lastUsageInfo,
27016
+ stopReason: this.stopReason
26186
27017
  };
26187
27018
  await callback([cleanedText], info);
26188
- } else if (cleanedText) await callback([cleanedText], this.lastUsageInfo);
27019
+ } else if (cleanedText) await callback([cleanedText], {
27020
+ ...this.lastUsageInfo,
27021
+ stopReason: this.stopReason
27022
+ });
26189
27023
  }
26190
27024
  };
26191
27025
  //#endregion
@@ -27296,7 +28130,7 @@ var AgentStore = class {
27296
28130
  async loadAgentsFromDirectory(directory, source) {
27297
28131
  try {
27298
28132
  if (!(await fs$1.stat(directory)).isDirectory()) return;
27299
- const files = await this.findAgentFiles(directory);
28133
+ const files = await findMarkdownFiles(directory);
27300
28134
  for (const filePath of files) try {
27301
28135
  const agent = await this.parseAgentFile(filePath, source);
27302
28136
  this.agents.set(agent.name, agent);
@@ -27308,25 +28142,6 @@ var AgentStore = class {
27308
28142
  }
27309
28143
  }
27310
28144
  /**
27311
- * Recursively find all .md files in directory
27312
- */
27313
- async findAgentFiles(directory) {
27314
- const files = [];
27315
- try {
27316
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
27317
- for (const entry of entries) {
27318
- const fullPath = path.join(directory, entry.name);
27319
- if (entry.isDirectory()) {
27320
- const subFiles = await this.findAgentFiles(fullPath);
27321
- files.push(...subFiles);
27322
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
27323
- }
27324
- } catch (error) {
27325
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
27326
- }
27327
- return files;
27328
- }
27329
- /**
27330
28145
  * Parse a single agent markdown file
27331
28146
  */
27332
28147
  async parseAgentFile(filePath, source) {
@@ -27643,7 +28458,8 @@ function buildFilenameMarkerRegex(markers) {
27643
28458
  * of the best-effort DB pre-filter. Fail-closed by design.
27644
28459
  */
27645
28460
  function isRetrievalExcluded(file, opts) {
27646
- 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;
27647
28463
  const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
27648
28464
  return !!re && re.test((file.fileName ?? "").toLowerCase());
27649
28465
  }
@@ -28153,6 +28969,41 @@ function attachedContentExtractionBudget(maxSafeInputTokens, systemPromptReserve
28153
28969
  const boundedReserve = Math.min(systemPromptReserve, Math.floor(maxSafeInputTokens * EXTRACTION_SYSTEM_RESERVE_MAX_SHARE));
28154
28970
  return Math.max(0, Math.max(Math.floor(maxSafeInputTokens * MIN_ATTACHED_CONTENT_EXTRACTION_SHARE), Math.floor((maxSafeInputTokens - boundedReserve) * ATTACHED_CONTENT_EXTRACTION_SHARE)));
28155
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
+ }
28156
29007
  /** The buffer buildAndSortMessages holds back before dividing the input window. */
28157
29008
  function assemblyTokenBuffer(maxInputTokens) {
28158
29009
  return Math.max(MIN_TOKEN_BUFFER, Math.floor(maxInputTokens * TOKEN_BUFFER_PERCENTAGE));
@@ -28418,7 +29269,125 @@ var AdminSettingsCache = class AdminSettingsCache {
28418
29269
  this.logger.info("🛑 Admin settings cache shutdown complete");
28419
29270
  }
28420
29271
  };
29272
+ /** Address of one cached override, shared by the cache and its callers so lookups are consistent. */
29273
+ function scopedOverrideKey(scopeLevel, scopeId, settingName) {
29274
+ return JSON.stringify([
29275
+ scopeLevel,
29276
+ scopeId,
29277
+ settingName
29278
+ ]);
29279
+ }
29280
+ function scopeKey(scopeLevel, scopeId) {
29281
+ return JSON.stringify([scopeLevel, scopeId]);
29282
+ }
29283
+ /**
29284
+ * In-memory cache for scoped setting OVERRIDES, sibling to `AdminSettingsCache` (which caches the
29285
+ * flat platform table). Unlike the platform cache it cannot hold one "all settings" blob - overrides
29286
+ * are per (rung, setting) and can be many (a value per lake), so each address is cached individually
29287
+ * and negatively (a confirmed absence is cached too, so an un-overridden setting does not re-query
29288
+ * every resolve).
29289
+ *
29290
+ * Entries are nested one level per rung address so a whole rung invalidates in one delete (the shape
29291
+ * a future scoped-override writer needs). The cache key carries the scope rung, which is the whole
29292
+ * reason this is separate from `AdminSettingsCache`: that one keys by bare `settingName`, so scoped
29293
+ * values would collide there. See #1660.
29294
+ */
29295
+ var ScopedSettingsCache = class ScopedSettingsCache {
29296
+ cache = /* @__PURE__ */ new Map();
29297
+ logger;
29298
+ maxScopes = 5e3;
29299
+ static DEFAULT_TTL = 3e5;
29300
+ static DEVELOPMENT_TTL = 3e4;
29301
+ constructor(logger) {
29302
+ this.logger = logger;
29303
+ }
29304
+ getTTL() {
29305
+ return process.env.NODE_ENV === "development" ? ScopedSettingsCache.DEVELOPMENT_TTL : ScopedSettingsCache.DEFAULT_TTL;
29306
+ }
29307
+ isValid(entry) {
29308
+ return Date.now() - entry.timestamp < entry.ttl;
29309
+ }
29310
+ /**
29311
+ * Resolve override values for the given rungs and setting names, reading through the cache. Returns
29312
+ * a map keyed by `scopedOverrideKey(level,id,name)` -> value|null for EVERY (scope, name) pair
29313
+ * requested, so the resolver can look up any rung deterministically. One DB query covers all misses.
29314
+ */
29315
+ async getOverrides(scopes, settingNames, db) {
29316
+ const out = /* @__PURE__ */ new Map();
29317
+ if (scopes.length === 0 || settingNames.length === 0) return out;
29318
+ const missingScopeRefs = /* @__PURE__ */ new Map();
29319
+ const missingNames = /* @__PURE__ */ new Set();
29320
+ for (const scope of scopes) {
29321
+ const inner = this.cache.get(scopeKey(scope.scopeLevel, scope.scopeId));
29322
+ for (const name of settingNames) {
29323
+ const outKey = scopedOverrideKey(scope.scopeLevel, scope.scopeId, name);
29324
+ const cached = inner?.get(name);
29325
+ if (cached && this.isValid(cached)) out.set(outKey, cached.value);
29326
+ else {
29327
+ if (cached) inner.delete(name);
29328
+ out.set(outKey, null);
29329
+ missingScopeRefs.set(scopeKey(scope.scopeLevel, scope.scopeId), scope);
29330
+ missingNames.add(name);
29331
+ }
29332
+ }
29333
+ }
29334
+ if (missingScopeRefs.size === 0) return out;
29335
+ const rows = await db.scopedSettings.findOverrides(Array.from(missingScopeRefs.values()), Array.from(missingNames));
29336
+ const ttl = this.getTTL();
29337
+ const now = Date.now();
29338
+ const found = /* @__PURE__ */ new Set();
29339
+ for (const row of rows) {
29340
+ out.set(scopedOverrideKey(row.scopeLevel, row.scopeId, row.settingName), row.settingValue);
29341
+ this.put(row.scopeLevel, row.scopeId, row.settingName, row.settingValue, now, ttl);
29342
+ found.add(scopedOverrideKey(row.scopeLevel, row.scopeId, row.settingName));
29343
+ }
29344
+ for (const scope of missingScopeRefs.values()) for (const name of missingNames) if (!found.has(scopedOverrideKey(scope.scopeLevel, scope.scopeId, name))) this.put(scope.scopeLevel, scope.scopeId, name, null, now, ttl);
29345
+ if (this.cache.size > this.maxScopes) this.evictOldestScopes();
29346
+ return out;
29347
+ }
29348
+ put(scopeLevel, scopeId, name, value, now, ttl) {
29349
+ const sk = scopeKey(scopeLevel, scopeId);
29350
+ let inner = this.cache.get(sk);
29351
+ if (!inner) {
29352
+ inner = /* @__PURE__ */ new Map();
29353
+ this.cache.set(sk, inner);
29354
+ }
29355
+ inner.set(name, {
29356
+ value,
29357
+ timestamp: now,
29358
+ ttl
29359
+ });
29360
+ }
29361
+ evictOldestScopes() {
29362
+ const freshness = (inner) => Math.max(...Array.from(inner.values()).map((e) => e.timestamp));
29363
+ const scopesByAge = Array.from(this.cache.entries()).sort((a, b) => freshness(a[1]) - freshness(b[1]));
29364
+ const toRemove = this.cache.size - this.maxScopes;
29365
+ for (let i = 0; i < toRemove; i++) this.cache.delete(scopesByAge[i][0]);
29366
+ this.logger.warn(`ScopedSettingsCache evicted ${toRemove} oldest rungs (size limit)`);
29367
+ }
29368
+ /** Invalidate every cached value for one rung address (call this from a future scoped-override writer). */
29369
+ invalidateScope(scopeLevel, scopeId) {
29370
+ this.cache.delete(scopeKey(scopeLevel, scopeId));
29371
+ }
29372
+ invalidateAll() {
29373
+ this.cache.clear();
29374
+ }
29375
+ /** Snapshot for monitoring, parity with AdminSettingsCache.getStats(). */
29376
+ getStats() {
29377
+ let entries = 0;
29378
+ for (const inner of this.cache.values()) entries += inner.size;
29379
+ return {
29380
+ scopes: this.cache.size,
29381
+ entries
29382
+ };
29383
+ }
29384
+ /** Graceful shutdown - no timers to stop (unlike AdminSettingsCache), just drop the map. */
29385
+ shutdown() {
29386
+ this.cache.clear();
29387
+ }
29388
+ };
28421
29389
  let globalSettingsCache = null;
29390
+ let globalScopedSettingsCache = null;
28422
29391
  /**
28423
29392
  * Get or create the global settings cache instance
28424
29393
  */
@@ -28426,6 +29395,32 @@ function getSettingsCache(logger) {
28426
29395
  if (!globalSettingsCache) globalSettingsCache = new AdminSettingsCache(logger || new Logger());
28427
29396
  return globalSettingsCache;
28428
29397
  }
29398
+ function getScopedSettingsCache(logger) {
29399
+ if (!globalScopedSettingsCache) globalScopedSettingsCache = new ScopedSettingsCache(logger || new Logger());
29400
+ return globalScopedSettingsCache;
29401
+ }
29402
+ /**
29403
+ * Read org/owner/lake OVERRIDE values for a set of rungs and setting names, through the scoped cache.
29404
+ * Returns a map keyed by `scopedOverrideKey(level,id,name)` -> value|null (see ScopedSettingsCache).
29405
+ * Platform values are NOT here - the resolver layers these over the platform read. Passing no rungs
29406
+ * (a platform-altitude resolve) returns an empty map and touches neither cache nor DB.
29407
+ */
29408
+ async function getScopedOverrides(scopes, settingNames, db, options) {
29409
+ return getScopedSettingsCache(options?.logger).getOverrides(scopes, settingNames, db);
29410
+ }
29411
+ /**
29412
+ * Invalidate cached overrides for one rung address, or all of them. Call from a scoped-override writer.
29413
+ *
29414
+ * In-process only: this clears the calling process's map. With negative caching and the 5-minute TTL,
29415
+ * a newly-written override can stay invisible on other Lambda/container instances for up to one TTL.
29416
+ * That is acceptable while no writer ships (this PR), but a future write path needs a cross-instance
29417
+ * invalidation story (shared cache or short TTL) rather than relying on this alone.
29418
+ */
29419
+ function invalidateScopedSettingsCache(scope) {
29420
+ if (!globalScopedSettingsCache) return;
29421
+ if (scope) globalScopedSettingsCache.invalidateScope(scope.scopeLevel, scope.scopeId);
29422
+ else globalScopedSettingsCache.invalidateAll();
29423
+ }
28429
29424
  function getSettingsValue(key, settings, defaultValue) {
28430
29425
  const settingConfig = settingsMap[key];
28431
29426
  if (!settingConfig) {
@@ -28478,7 +29473,7 @@ async function getSettingsByNames(settingNames, db, options) {
28478
29473
  const allSettings = await getSettingsCache(logger).getSettingsMap(db);
28479
29474
  const result = {};
28480
29475
  settingNames.forEach((name) => {
28481
- result[name] = allSettings[name] || null;
29476
+ result[name] = allSettings[name] ?? null;
28482
29477
  });
28483
29478
  return result;
28484
29479
  }
@@ -28503,6 +29498,10 @@ function shutdownSettingsCache() {
28503
29498
  globalSettingsCache.shutdown();
28504
29499
  globalSettingsCache = null;
28505
29500
  }
29501
+ if (globalScopedSettingsCache) {
29502
+ globalScopedSettingsCache.shutdown();
29503
+ globalScopedSettingsCache = null;
29504
+ }
28506
29505
  }
28507
29506
  /**
28508
29507
  * Get cache statistics for monitoring
@@ -28860,6 +29859,12 @@ const EMBEDDING_TOKEN_LIMITS = {
28860
29859
  */
28861
29860
  const KNOWLEDGE_FILE_TOKEN_ALLOCATION = .7;
28862
29861
  /**
29862
+ * The two always-on blocks this function injects itself, downstream of the caller's own
29863
+ * systemPromptDetails assembly - so a caller reporting per-source token telemetry (see
29864
+ * ChatCompletionProcess) cannot see them without reading this list back off the return value.
29865
+ */
29866
+ const BUILDER_INJECTED_BLOCK_IDS = ["formatPrompt", "imagePrompt"];
29867
+ /**
28863
29868
  * Rounds the final safety pass may spend shrinking the payload. It has to re-measure between rounds
28864
29869
  * (see the pass for why one shot overshoots), and each round costs two real tokenizer calls, so this
28865
29870
  * bounds the work. Converges in one or two rounds in practice.
@@ -29005,12 +30010,12 @@ const estimateMessagesTokens = (messages) => messages.reduce((sum, message) => s
29005
30010
  * staying synchronous (no N async tokenizer calls over a long history). Mirrors
29006
30011
  * the fields the conversion below actually emits into the prompt.
29007
30012
  */
29008
- function estimateQuestTokenLength(item) {
30013
+ function estimateQuestTokenLength(item, disableToolReplay = false) {
29009
30014
  const parts = [item.prompt ?? ""];
29010
30015
  if (item.structuredReplies?.length) parts.push(JSON.stringify(item.structuredReplies));
29011
30016
  else if (item.replies?.length) parts.push(item.replies.join("\n"));
29012
30017
  if (item.toolResults?.length) parts.push(JSON.stringify(item.toolResults));
29013
- if (!item.structuredReplies?.length) {
30018
+ if (!item.structuredReplies?.length && !disableToolReplay) {
29014
30019
  const toolCalls = replayableToolCalls(item.promptMeta?.functionCalls);
29015
30020
  if (toolCalls.length) parts.push(JSON.stringify(toolCalls));
29016
30021
  }
@@ -29090,20 +30095,21 @@ async function generateSafeEmbedding(embeddingService, text, logger) {
29090
30095
  * history at all, and UNLIMITED_HISTORY_COUNT means no window (which still pages, since the
29091
30096
  * fetch needs some limit).
29092
30097
  */
29093
- async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget }) {
30098
+ async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget, excludeCurrentPrompt = false, model }) {
29094
30099
  if (!isUnlimitedHistory(historyCount) && historyCount !== null && historyCount <= 0) return [
29095
30100
  [],
29096
30101
  0,
29097
30102
  { cacheHit: false }
29098
30103
  ];
29099
30104
  const limit = resolveHistoryFetchLimit(historyCount);
30105
+ const disableToolReplay = !!model && isGeminiModelId(model);
29100
30106
  const startTime = Date.now();
29101
30107
  const chatHistoryItems = await db.quests.getMostRecentChatHistory(session.id, limit + 1);
29102
30108
  const fetchTime = Date.now() - startTime;
29103
30109
  const cacheIndicator = fetchTime < 50 ? "CACHE_HIT" : "CACHE_MISS";
29104
30110
  Logger.globalInstance.log(`⚡ Message History ${cacheIndicator}: ${fetchTime}ms for session ${session.id.slice(-8)} (${chatHistoryItems.length} items)`);
29105
30111
  chatHistoryItems.reverse();
29106
- if (chatHistoryItems.length > 1) chatHistoryItems.pop();
30112
+ if (chatHistoryItems.length > 1 || excludeCurrentPrompt) chatHistoryItems.pop();
29107
30113
  if (session.contextSummaryUpToQuestId) {
29108
30114
  const boundary = session.contextSummaryUpToQuestId;
29109
30115
  const filtered = chatHistoryItems.filter((item) => item.id > boundary);
@@ -29114,7 +30120,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29114
30120
  let usedTokens = 0;
29115
30121
  let keepFromIndex = 0;
29116
30122
  for (let i = chatHistoryItems.length - 1; i >= 0; i--) {
29117
- usedTokens += estimateQuestTokenLength(chatHistoryItems[i]);
30123
+ usedTokens += estimateQuestTokenLength(chatHistoryItems[i], disableToolReplay);
29118
30124
  if (usedTokens > verbatimTokenBudget && i < chatHistoryItems.length - 1) {
29119
30125
  keepFromIndex = i + 1;
29120
30126
  break;
@@ -29146,7 +30152,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29146
30152
  is_error: tr.is_error
29147
30153
  }))
29148
30154
  });
29149
- } else if (toolCalls.length > 0) {
30155
+ } else if (toolCalls.length > 0 && !disableToolReplay) {
29150
30156
  const textReply = cur.replies?.find((reply) => !reply.trim().startsWith("<think>")) || "";
29151
30157
  const assistantContent = [];
29152
30158
  if (textReply) assistantContent.push({
@@ -29194,6 +30200,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29194
30200
  });
29195
30201
  }
29196
30202
  }
30203
+ const priorToolNames = chatHistoryItems.flatMap((item) => (item.promptMeta?.functionCalls ?? []).map((fc) => fc.name)).filter((name) => Boolean(name));
29197
30204
  return [
29198
30205
  convertedMessages,
29199
30206
  chatHistoryItems.length,
@@ -29203,7 +30210,8 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29203
30210
  itemCount: chatHistoryItems.length,
29204
30211
  oldestIncludedQuestId,
29205
30212
  excludedOlderQuestCount,
29206
- recentGeneratedImages
30213
+ recentGeneratedImages,
30214
+ priorToolNames
29207
30215
  }
29208
30216
  ];
29209
30217
  }
@@ -29495,12 +30503,16 @@ const noopResize = async (imageBuffer) => imageBuffer;
29495
30503
  async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, attachedContentTokenBudget, modelInfo, sendStatusUpdate, { logger, storage, db, resizeImageForModel = noopResize }, progressCallback) {
29496
30504
  if (!fabFiles || fabFiles.length === 0) return {
29497
30505
  userMessages: [],
29498
- errorMessages: []
30506
+ errorMessages: [],
30507
+ deliveredFileIds: [],
30508
+ fullyDeliveredFileIds: []
29499
30509
  };
29500
30510
  const fileProcessingStartTime = Date.now();
29501
30511
  let systemContent = "";
29502
30512
  const userMessages = [];
29503
30513
  const errorMessages = [];
30514
+ const deliveredFileIds = /* @__PURE__ */ new Set();
30515
+ const fullyDeliveredFileIds = /* @__PURE__ */ new Set();
29504
30516
  const contextFiles = [];
29505
30517
  const supportsVision = modelInfo?.supportsVision ?? false;
29506
30518
  if (fabFiles.length > 0) sendStatusUpdate("Munching attached files...");
@@ -29524,6 +30536,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29524
30536
  const maxTokens = Math.max(1, Math.floor(attachedContentTokenBudget / textFileCount));
29525
30537
  const fileContentCache = /* @__PURE__ */ new Map();
29526
30538
  const processFileInParallel = async (file) => {
30539
+ let delivered = false;
30540
+ let fullyDelivered = false;
29527
30541
  try {
29528
30542
  if (isAudioMimeType(file.mimeType)) {
29529
30543
  logger.warn(`[processFabFilesServer] Skipping audio file ${file.fileName} — audio is not attachable to an LLM.`);
@@ -29552,6 +30566,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29552
30566
  type: "text",
29553
30567
  text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
29554
30568
  });
30569
+ delivered = true;
30570
+ fullyDelivered = true;
29555
30571
  break;
29556
30572
  }
29557
30573
  case ModelBackend.Anthropic:
@@ -29586,6 +30602,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29586
30602
  type: "text",
29587
30603
  text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
29588
30604
  });
30605
+ delivered = true;
30606
+ fullyDelivered = true;
29589
30607
  } else if (modelInfo.id.startsWith("moonshot")) {
29590
30608
  const moonshotBuffer = await resizeImageForModel(await storage.download(file.filePath), void 0, logger);
29591
30609
  const { mime: moonshotMimeType } = await getFileType(moonshotBuffer, file.fileName, file.mimeType);
@@ -29609,6 +30627,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29609
30627
  type: "text",
29610
30628
  text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
29611
30629
  });
30630
+ delivered = true;
30631
+ fullyDelivered = true;
29612
30632
  } else logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
29613
30633
  break;
29614
30634
  case ModelBackend.Ollama: {
@@ -29634,6 +30654,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29634
30654
  type: "text",
29635
30655
  text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}". Do not rename based on image content.`
29636
30656
  });
30657
+ delivered = true;
30658
+ fullyDelivered = true;
29637
30659
  break;
29638
30660
  }
29639
30661
  default: logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
@@ -29680,7 +30702,9 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29680
30702
  }
29681
30703
  if (truncatedResults.length > 0) {
29682
30704
  deliveredViaCosine = true;
30705
+ delivered = true;
29683
30706
  const deliveredEveryChunk = !scanTruncated && totalChunks === truncatedResults.length;
30707
+ fullyDelivered = deliveredEveryChunk && !anyChunkCut;
29684
30708
  let notice = "";
29685
30709
  if (!deliveredEveryChunk) notice = excerptNotice(file.fileName);
29686
30710
  else if (anyChunkCut) notice = CONTENT_TRUNCATION_NOTICE;
@@ -29707,6 +30731,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29707
30731
  const finalMaxFileSize = maxTokens > 0 ? maxSizeBasedonMaxTokens : MAX_FILE_SIZE;
29708
30732
  logger.log(`[processFabFilesServer] Final max file size: ${finalMaxFileSize}`);
29709
30733
  sendStatusUpdate("Adding file content to prompt...");
30734
+ fullyDelivered = fabContent.length <= finalMaxFileSize;
29710
30735
  if (fabContent.length > finalMaxFileSize) {
29711
30736
  await sendStatusUpdate("File is too large, truncating...");
29712
30737
  const originalFileSize = fabContent.length;
@@ -29717,6 +30742,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29717
30742
  content: errorMsg
29718
30743
  });
29719
30744
  } else errorMsg = null;
30745
+ delivered = true;
29720
30746
  if (file.system) systemContent += fabContent;
29721
30747
  else contextFiles.push({
29722
30748
  fileName: file.fileName,
@@ -29746,6 +30772,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29746
30772
  }
29747
30773
  }
29748
30774
  }
30775
+ if (delivered) deliveredFileIds.add(file.id);
30776
+ if (fullyDelivered) fullyDeliveredFileIds.add(file.id);
29749
30777
  } catch (error) {
29750
30778
  logger.updateMetadata({ fileId: file.id });
29751
30779
  logger.error(`🕐 [processFabFilesServer] Error processing file ${file.fileName}: ${error}`);
@@ -29784,7 +30812,9 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29784
30812
  logger.info(`📁 File processing completed in ${fileProcessingTime}ms for ${fabFiles.length} files`);
29785
30813
  return {
29786
30814
  userMessages,
29787
- errorMessages
30815
+ errorMessages,
30816
+ deliveredFileIds: Array.from(deliveredFileIds),
30817
+ fullyDeliveredFileIds: Array.from(fullyDeliveredFileIds)
29788
30818
  };
29789
30819
  }
29790
30820
  /**
@@ -29950,7 +30980,11 @@ const processMessages = (messages, tokenBudget, { truncationNotice } = {}) => {
29950
30980
  async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, maxInputTokens, settings, historyCount = 0, logger, tokenizer, options = { verbose: false }) {
29951
30981
  if (!(maxInputTokens > 0)) {
29952
30982
  logger.error(`Invalid maxInputTokens: ${maxInputTokens}. Must be greater than 0.`);
29953
- return [];
30983
+ return {
30984
+ messages: [],
30985
+ messageTruncation: null,
30986
+ injectedBlocks: []
30987
+ };
29954
30988
  }
29955
30989
  if (process.env.VERBOSE_CHAT_CONTEXT !== "false") {
29956
30990
  if (options.verbose) {
@@ -29990,18 +31024,28 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
29990
31024
  const systemMessages = [];
29991
31025
  let systemTokenCount = 0;
29992
31026
  const builderInjectedPriorities = /* @__PURE__ */ new Map();
29993
- if (!options.skipAdminPromptTemplates) {
31027
+ const injectedBlockGateResults = /* @__PURE__ */ new Map();
31028
+ if (options.skipAdminPromptTemplates) {
31029
+ injectedBlockGateResults.set("formatPrompt", { reason: "mode_skipped" });
31030
+ injectedBlockGateResults.set("imagePrompt", { reason: "mode_skipped" });
31031
+ } else {
29994
31032
  if (getSettingsValue("UseFormatPrompt", settings)) {
29995
31033
  const formatPromptTemplate = settings.FormatPromptTemplate;
29996
31034
  const withFormatPrompt = includeHardcodedSystemMessage(fabMessages, formatPromptTemplate);
29997
- if (withFormatPrompt.length > fabMessages.length) builderInjectedPriorities.set(withFormatPrompt[0], 60);
31035
+ if (withFormatPrompt.length > fabMessages.length) {
31036
+ builderInjectedPriorities.set(withFormatPrompt[0], 60);
31037
+ injectedBlockGateResults.set("formatPrompt", { message: withFormatPrompt[0] });
31038
+ } else injectedBlockGateResults.set("formatPrompt", { reason: "not_triggered" });
29998
31039
  fabMessages = withFormatPrompt;
29999
- }
31040
+ } else injectedBlockGateResults.set("formatPrompt", { reason: "setting_disabled" });
30000
31041
  if (getSettingsValue("UseImagePrompt", settings)) {
30001
31042
  const withImagePrompt = includeImagePromptSystemMessage(fabMessages, userPromptContent, options.imageGenerationAvailable ?? false);
30002
- if (withImagePrompt.length > fabMessages.length) builderInjectedPriorities.set(withImagePrompt[0], 50);
31043
+ if (withImagePrompt.length > fabMessages.length) {
31044
+ builderInjectedPriorities.set(withImagePrompt[0], 50);
31045
+ injectedBlockGateResults.set("imagePrompt", { message: withImagePrompt[0] });
31046
+ } else injectedBlockGateResults.set("imagePrompt", { reason: "not_triggered" });
30003
31047
  fabMessages = withImagePrompt;
30004
- }
31048
+ } else injectedBlockGateResults.set("imagePrompt", { reason: "setting_disabled" });
30005
31049
  }
30006
31050
  const imageMessages = [];
30007
31051
  const nonImageMessages = [];
@@ -30067,6 +31111,21 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
30067
31111
  admittedSystemMessages.add(message);
30068
31112
  }
30069
31113
  systemMessages.push(...systemCandidates.filter((message) => admittedSystemMessages.has(message)));
31114
+ const injectedBlocks = BUILDER_INJECTED_BLOCK_IDS.map((id) => {
31115
+ const gateResult = injectedBlockGateResults.get(id);
31116
+ if (!gateResult?.message) return {
31117
+ id,
31118
+ injected: false,
31119
+ delivered: false,
31120
+ ...gateResult?.reason ? { reason: gateResult.reason } : {}
31121
+ };
31122
+ return {
31123
+ id,
31124
+ injected: true,
31125
+ delivered: admittedSystemMessages.has(gateResult.message),
31126
+ ...typeof gateResult.message.content === "string" ? { content: gateResult.message.content } : {}
31127
+ };
31128
+ });
30070
31129
  if (systemCandidates.length > 0 && systemMessages.length === 0) logger.warn(`No system instructions fit the budget: ${systemCandidates.length} message(s) dropped, cap ${systemTokenCap} of ${preSystemBudget} est. tokens (${Math.round(65)}% after the attached-content reserve of ${contentReserve}). Smallest was ${Math.min(...systemCandidates.map(systemMessageTokens))} est. tokens.`);
30071
31130
  tokenBudget -= systemTokenCount;
30072
31131
  const historyMessages = isUnlimitedHistory(historyCount) ? previousMessages : historyCount > 0 ? previousMessages.slice(-historyCount * 2) : [];
@@ -30197,16 +31256,16 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
30197
31256
  ];
30198
31257
  };
30199
31258
  const messages = assemble(processedContentMessages, processedPreviousMessages);
30200
- const recordDebugInfo = (finalContentMessages) => {
31259
+ const buildDebugInfo = (finalContentMessages) => {
30201
31260
  const historyWindowed = previousMessages.length > historyMessages.length;
30202
31261
  const budgetTruncated = allRemovedMessages.length > 0 || contentSqueezed || historyCutMidMessage;
30203
- buildAndSortMessages.lastDebugInfo = { messageTruncation: {
31262
+ return {
30204
31263
  wasTruncated: budgetTruncated,
30205
31264
  originalMessageCount: originalTotalMessageCount,
30206
31265
  truncatedMessageCount: processedPreviousMessages.length + finalContentMessages.length,
30207
31266
  truncationMethod: budgetTruncated ? "token-budget" : historyWindowed ? "history-limit" : void 0,
30208
31267
  removedMessages: allRemovedMessages.length > 0 ? allRemovedMessages : void 0
30209
- } };
31268
+ };
30210
31269
  };
30211
31270
  const finalTokenCount = await calculateTotalTokenLength(messages, {
30212
31271
  estimateOnly: false,
@@ -30251,8 +31310,11 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
30251
31310
  else break;
30252
31311
  }
30253
31312
  if (currentTokenCount > maxInputTokens) logger.warn(`Final safety pass could not bring the payload under maxInputTokens (${currentTokenCount} > ${maxInputTokens}). Remaining: ${systemMessages.length} system, ${processedPreviousMessages.length} history, ${reducedContentMessages.length} content, ${imageMessages.length} image message(s) plus the user prompt` + (lastRatio === null ? ", with nothing shrinkable to measure." : `, at ${lastRatio.toFixed(2)} real tokens per estimated token.`));
30254
- recordDebugInfo(reducedContentMessages);
30255
- return ensureToolPairingIntegrity(assemble(reducedContentMessages, processedPreviousMessages), logger);
31313
+ return {
31314
+ messages: ensureToolPairingIntegrity(assemble(reducedContentMessages, processedPreviousMessages), logger),
31315
+ messageTruncation: buildDebugInfo(reducedContentMessages),
31316
+ injectedBlocks
31317
+ };
30256
31318
  }
30257
31319
  if (process.env.VERBOSE_MESSAGE_BUILDING === "true") {
30258
31320
  logger.log("=== Verbose Message Building Log ===");
@@ -30265,18 +31327,17 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
30265
31327
  logger.log(`\nTotal messages: ${messages.length}`);
30266
31328
  logger.log("=== End of Verbose Message Building Log ===");
30267
31329
  }
30268
- recordDebugInfo(processedContentMessages);
30269
- return ensureToolPairingIntegrity(messages, logger);
30270
- }
30271
- /**
30272
- * Returns the debug info populated by the most recent buildAndSortMessages call.
30273
- */
30274
- function getLastBuildDebugInfo() {
30275
- return buildAndSortMessages.lastDebugInfo?.messageTruncation || null;
31330
+ return {
31331
+ messages: ensureToolPairingIntegrity(messages, logger),
31332
+ messageTruncation: buildDebugInfo(processedContentMessages),
31333
+ injectedBlocks
31334
+ };
30276
31335
  }
30277
31336
  var llm_exports = /* @__PURE__ */ __exportAll({
30278
31337
  ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
30279
31338
  ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
31339
+ BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
31340
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
30280
31341
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
30281
31342
  FORMAT_PROMPT_PRIORITY: () => 60,
30282
31343
  IMAGE_PROMPT_PRIORITY: () => 50,
@@ -30292,13 +31353,13 @@ var llm_exports = /* @__PURE__ */ __exportAll({
30292
31353
  buildAndSortMessages: () => buildAndSortMessages,
30293
31354
  calculateTotalTokenLength: () => calculateTotalTokenLength,
30294
31355
  computeCosineSimilarity: () => computeCosineSimilarity,
31356
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
30295
31357
  effectiveContextWindow: () => effectiveContextWindow,
30296
31358
  fetchAgentConversationHistory: () => fetchAgentConversationHistory,
30297
31359
  fetchAndConvertFabFiles: () => fetchAndConvertFabFiles,
30298
31360
  fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
30299
31361
  generateSafeEmbedding: () => generateSafeEmbedding,
30300
31362
  getCachedSignedUrl: () => getCachedSignedUrl,
30301
- getLastBuildDebugInfo: () => getLastBuildDebugInfo,
30302
31363
  includeHardcodedSystemMessage: () => includeHardcodedSystemMessage,
30303
31364
  includeImagePromptSystemMessage: () => includeImagePromptSystemMessage,
30304
31365
  processFabFilesServer: () => processFabFilesServer,
@@ -32198,7 +33259,12 @@ var BFLImageService = class extends AIImageService {
32198
33259
  if (height) requestBody.height = height;
32199
33260
  }
32200
33261
  const cleanedBody = this.stripNullFields(requestBody);
32201
- 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);
32202
33268
  const submitResponse = await axios.post(`${this.baseUrl}/${model}`, cleanedBody, { headers: {
32203
33269
  accept: "application/json",
32204
33270
  "x-key": this.apiKey,
@@ -32211,7 +33277,7 @@ var BFLImageService = class extends AIImageService {
32211
33277
  pollingUrl,
32212
33278
  responseData: submitResponse.data,
32213
33279
  endpoint: `${this.baseUrl}/${model}`,
32214
- requestBody
33280
+ requestBody: safeRequestBody
32215
33281
  });
32216
33282
  const imageUrl = await this.pollForResult(requestId, pollingUrl);
32217
33283
  Logger.globalInstance.debug("[DEBUG] Received BFL image URL:", {
@@ -32620,8 +33686,6 @@ var GeminiImageService = class extends AIImageService {
32620
33686
  const aspectRatio = this.resolveAspectRatio(options);
32621
33687
  if (aspectRatio) config.aspectRatio = aspectRatio;
32622
33688
  if (options.guidance !== null && options.guidance !== void 0) config.guidanceScale = options.guidance;
32623
- if (options.prompt_upsampling !== void 0) config.enhancePrompt = options.prompt_upsampling;
32624
- if (options.seed !== null && options.seed !== void 0) config.seed = options.seed;
32625
33689
  const outputMimeType = this.resolveMimeType(options.output_format);
32626
33690
  if (outputMimeType) config.outputMimeType = outputMimeType;
32627
33691
  return config;
@@ -33932,11 +34996,11 @@ const DEFAULT_FALLBACK_CHAIN = [
33932
34996
  */
33933
34997
  function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logger, excludeModelIds, preferUntriedBackend) {
33934
34998
  logger.info(`🔍 Finding automatic fallback for ${originalModel.id}`);
33935
- const hasValidKey = (m) => !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
34999
+ const hasValidKey = (m) => !m.disabled && !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
33936
35000
  const preferences = [...FALLBACK_PREFERENCES[originalModel.id] ?? []];
33937
35001
  if (preferences.length === 0) preferences.push(...DEFAULT_FALLBACK_CHAIN);
33938
35002
  if (preferUntriedBackend) {
33939
- const triedBackends = /* @__PURE__ */ new Set([originalModel.backend]);
35003
+ const triedBackends = new Set(originalModel.backend ? [originalModel.backend] : []);
33940
35004
  for (const id of excludeModelIds ?? []) {
33941
35005
  const tried = availableModels.find((m) => m.id === id);
33942
35006
  if (tried) triedBackends.add(tried.backend);
@@ -33963,7 +35027,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
33963
35027
  return model;
33964
35028
  }
33965
35029
  }
33966
- logger.error(" No suitable automatic fallback model found");
35030
+ logger.warn("⚠️ No suitable automatic fallback model found");
33967
35031
  return null;
33968
35032
  }
33969
35033
  /**
@@ -34033,6 +35097,39 @@ async function getLlmWithFallback(originalModel, fallbackModelId, availableModel
34033
35097
  return null;
34034
35098
  }
34035
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
+ /**
34036
35133
  * Tiktoken-based implementation of the tokenizer interface
34037
35134
  * Provides caching for performance and configurable logging
34038
35135
  *
@@ -34541,6 +35638,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34541
35638
  AdminSettingsCache: () => AdminSettingsCache,
34542
35639
  BEDROCK_EMBEDDING_MODEL_MAP: () => BEDROCK_EMBEDDING_MODEL_MAP,
34543
35640
  BFLImageService: () => BFLImageService,
35641
+ BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
34544
35642
  BadRequestError: () => BadRequestError,
34545
35643
  BaseStorage: () => BaseStorage,
34546
35644
  BedrockEmbeddingService: () => BedrockEmbeddingService,
@@ -34552,6 +35650,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34552
35650
  ClientMessageSender: () => ClientMessageSender,
34553
35651
  CorruptedFileError: () => CorruptedFileError,
34554
35652
  DEFAULT_FALLBACK_CHAIN: () => DEFAULT_FALLBACK_CHAIN,
35653
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
34555
35654
  DEFAULT_PASSAGE_TOKEN_TARGET: () => 512,
34556
35655
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
34557
35656
  ElevenLabsMusicGenerator: () => ElevenLabsMusicGenerator,
@@ -34602,6 +35701,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34602
35701
  RapidReplyMappingsCache: () => RapidReplyMappingsCache,
34603
35702
  S3Storage: () => S3Storage,
34604
35703
  SQSService: () => SQSService,
35704
+ ScopedSettingsCache: () => ScopedSettingsCache,
34605
35705
  SmartChunker: () => SmartChunker,
34606
35706
  TOKEN_BUFFER_PERCENTAGE: () => TOKEN_BUFFER_PERCENTAGE,
34607
35707
  TOOL_RESULT_NOT_RECORDED: () => TOOL_RESULT_NOT_RECORDED,
@@ -34638,6 +35738,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34638
35738
  checkStorageLimitForFile: () => checkStorageLimitForFile,
34639
35739
  cleanMermaidSyntax: () => cleanMermaidSyntax,
34640
35740
  computeCosineSimilarity: () => computeCosineSimilarity,
35741
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
34641
35742
  convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
34642
35743
  createQuestPlanToolSchema: () => createQuestPlanToolSchema,
34643
35744
  createTokenizer: () => createTokenizer,
@@ -34653,18 +35754,19 @@ __reExport(/* @__PURE__ */ __exportAll({
34653
35754
  fetchAndParseURL: () => fetchAndParseURL,
34654
35755
  fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
34655
35756
  filterRetrievalExcluded: () => filterRetrievalExcluded,
35757
+ findFallbackForMissingModel: () => findFallbackForMissingModel,
34656
35758
  formatVoiceHistory: () => formatVoiceHistory,
34657
35759
  generateSafeEmbedding: () => generateSafeEmbedding,
34658
35760
  getCachedSignedUrl: () => getCachedSignedUrl,
34659
35761
  getFileContent: () => getFileContent,
34660
35762
  getFileExtension: () => getFileExtension,
34661
35763
  getFileType: () => getFileType,
34662
- getLastBuildDebugInfo: () => getLastBuildDebugInfo,
34663
35764
  getLlmWithFallback: () => getLlmWithFallback,
34664
35765
  getMimeTypeByExtension: () => getMimeTypeByExtension,
34665
35766
  getNotificationDeduplicator: () => getNotificationDeduplicator,
34666
35767
  getProviderFromModel: () => getProviderFromModel,
34667
35768
  getRetryAfterMs: () => getRetryAfterMs,
35769
+ getScopedOverrides: () => getScopedOverrides,
34668
35770
  getSettingByName: () => getSettingByName,
34669
35771
  getSettingsByNames: () => getSettingsByNames,
34670
35772
  getSettingsCacheStats: () => getSettingsCacheStats,
@@ -34674,6 +35776,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34674
35776
  includeHardcodedSystemMessage: () => includeHardcodedSystemMessage,
34675
35777
  includeImagePromptSystemMessage: () => includeImagePromptSystemMessage,
34676
35778
  initializeConfig: () => initializeConfig,
35779
+ invalidateScopedSettingsCache: () => invalidateScopedSettingsCache,
34677
35780
  invalidateSettingsCache: () => invalidateSettingsCache,
34678
35781
  isAiEditableOfficeMime: () => isAiEditableOfficeMime,
34679
35782
  isGPT5ModelWithToolSupport: () => isGPT5ModelWithToolSupport,
@@ -34708,6 +35811,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34708
35811
  resolveEmbeddingConfig: () => resolveEmbeddingConfig,
34709
35812
  resolveSupportedMimeType: () => resolveSupportedMimeType,
34710
35813
  safeInputWindow: () => safeInputWindow,
35814
+ scopedOverrideKey: () => scopedOverrideKey,
34711
35815
  secureParameters: () => secureParameters,
34712
35816
  sendToConnection: () => sendToConnection,
34713
35817
  shouldTriggerFallback: () => shouldTriggerFallback,