@bike4mind/cli 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as WORK_ITEM_STATUSES, A as HttpStatus, At as usdToCredits, B as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Ct as mapMimeTypeToArtifactType, D as FORMAT_PROMPT_TEMPLATE, Dt as settingsMap, E as FIXED_TEMPERATURE_MODELS, Et as secureParameters, F as NO_TEMPERATURE_MODELS, Ft as isNearLimit, G as SupportedFabFileMimeTypes, H as REFUSAL_FALLBACK_MODELS, I as NotFoundError, It as parseRateLimitHeaders, J as UnauthorizedError, K as TTS_MAX_INPUT_CHARS, L as OllamaEmbeddingModel, M as InternalServerError, Mt as withRetry, N as MODEL_INFO_FIELD_GROUP_OF, Nt as buildRateLimitLogEntry, O as ForbiddenError, Ot as toModelInfo, P as ModelBackend, Pt as extractSnippetMeta, Q as VoyageAIEmbeddingModel, R as OpenAIEmbeddingModel, S as CorruptedFileError, St as isZodError, T as FIELD_GROUP_OF, Tt as resolveHistoryFetchLimit, U as RESPONSES_API_TOOL_MODELS, V as REASONING_SUPPORTED_MODELS, W as SpeechToTextModels, X as VIDEO_SIZE_CONSTRAINTS, Y as UnprocessableEntityError, Z as VideoModels, _ as BadRequestError, _t as isRenderableModelType, at as getQuestErrorCode, bt as isUnlimitedHistory, ct as isFieldGroup, dt as isImageAttachment, et as applyModelPriceCatalog, ft as isImageServeable, g as BFL_SAFETY_TOLERANCE, gt as isPlaceholderApiKey, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as isModelDeprecated, it as getMcpProviderMetadata, j as ImageModels, jt as usdToCreditsStochastic, k as HTTPError, kt as toModelRecord, lt as isGPTImage2Model, m as ApiKeyType, mt as isModelAccessible, n as logger, nt as dayjsConfig_default, ot as getRetryAfterMs, p as ARTIFACT_ATTRS_PATTERN, pt as isMediaModelType, q as TooManyRequestsError, rt as defaultEmbeddingModelForEnv, st as isAudioMimeType, tt as calculateRetryDelay, ut as isGPTImageModel, v as BedrockEmbeddingModel, vt as isRetryableError, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as obfuscateApiKey, x as ChatModels, xt as isUserInitiatedAbort, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, yt as isSupportedFabFileMimeType, z as PermissionDeniedError } from "./ConfigStore-CNfbeaJf.mjs";
2
+ import { $ as SupportedFabFileMimeTypes, $t as buildRateLimitLogEntry, A as HTTPError, At as isRenderableModelType, B as OPENAI_GPT_IMAGE_1_IMAGE_SIZES, Bt as resolveHistoryFetchLimit, Ct as isGeminiModelId, D as FIXED_TEMPERATURE_MODELS, Dt as isModelAccessible, E as FIELD_GROUP_OF, Et as isMediaModelType, F as MODEL_INFO_FIELD_GROUP_OF, Ft as isZodError, G as PermissionDeniedError, Gt as usdToCredits, H as OllamaEmbeddingModel, Ht as settingsMap, I as McpServerName, It as mapMimeTypeToArtifactType, J as REFUSAL_FALLBACK_MODELS, K as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Kt as usdToCreditsStochastic, L as ModelBackend, Lt as obfuscateApiKey, M as IMAGE_SIZE_CONSTRAINTS, Mt as isSupportedFabFileMimeType, N as ImageModels, Nt as isUnlimitedHistory, O as FORMAT_PROMPT_TEMPLATE, Ot as isModelDeprecated, P as InternalServerError, Pt as isUserInitiatedAbort, Q as SpeechToTextModels, R as NO_TEMPERATURE_MODELS, Rt as parseEmbeddingRateLimitHeaders, S as CorruptedFileError, St as isGPTImageModel, Tt as isImageServeable, U as OpenAIEmbeddingModel, Ut as toModelInfo, V as OPENAI_GPT_IMAGE_2_IMAGE_SIZES, Vt as secureParameters, Wt as toModelRecord, Y as RESPONSES_API_TOOL_MODELS, _ as BadRequestError, _t as isChunkRebuildPending, at as VideoModels, bt as isFieldGroup, ct as applyModelPriceCatalog, dt as defaultEmbeddingModelForEnv, en as extractSnippetMeta, et as TTS_MAX_INPUT_CHARS, ft as getMcpProviderMetadata, g as BFL_SAFETY_TOLERANCE, gt as isAudioMimeType, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as hasUsableLimits, it as VIDEO_SIZE_CONSTRAINTS, j as HttpStatus, jt as isRetryableError, k as ForbiddenError, kt as isPlaceholderApiKey, lt as calculateRetryDelay, m as ApiKeyType, mt as getRetryAfterMs, n as logger, nn as parseRateLimitHeaders, nt as UnauthorizedError, ot as VoyageAIEmbeddingModel, p as ARTIFACT_ATTRS_PATTERN, pt as getQuestErrorCode, q as REASONING_SUPPORTED_MODELS, qt as withRetry, rt as UnprocessableEntityError, st as WORK_ITEM_STATUSES, tn as isNearLimit, tt as TooManyRequestsError, ut as dayjsConfig_default, v as BedrockEmbeddingModel, vt as isChunkStalledFile, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isImageAttachment, x as ChatModels, xt as isGPTImage2Model, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, z as NotFoundError, zt as reservationOutputTokens } from "./ConfigStore-CoY0l0gr.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";
@@ -51,9 +52,9 @@ import mongoose from "mongoose";
51
52
  import { parse } from "shell-quote";
52
53
  import { homedir as homedir$1 } from "node:os";
53
54
  import { EventEmitter } from "events";
55
+ import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
54
56
  import { fileURLToPath } from "url";
55
57
  import { Anthropic, RateLimitError } from "@anthropic-ai/sdk";
56
- import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
57
58
  import { GoogleGenAI } from "@google/genai";
58
59
  import pick from "lodash/pick.js";
59
60
  import { Stream } from "openai/streaming";
@@ -65,6 +66,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
65
66
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
66
67
  import { getDomain } from "tldts";
67
68
  import * as dotenv from "dotenv";
69
+ import { createHash as createHash$1 } from "node:crypto";
68
70
  import invert from "lodash/invert.js";
69
71
  import * as util from "node:util";
70
72
  import * as zlib from "node:zlib";
@@ -110,6 +112,18 @@ const COMPACTION_SUMMARY_MARKER = "[Previous conversation summary]";
110
112
  //#endregion
111
113
  //#region src/utils/fileSearch.ts
112
114
  /**
115
+ * stat() that yields null instead of throwing. Following a symlink is best-effort here:
116
+ * a dangling link, or one pointing somewhere unreadable, must not take down a whole
117
+ * directory listing.
118
+ */
119
+ function statOrNull(targetPath) {
120
+ try {
121
+ return fs$2.statSync(targetPath);
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
113
127
  * Load gitignore rules from project root
114
128
  */
115
129
  function loadIgnoreRules(projectRoot) {
@@ -200,13 +214,17 @@ function listAbsoluteDirectory(absolutePath, filterQuery) {
200
214
  }
201
215
  return filteredEntries.slice(0, 15).map((entry) => {
202
216
  const fullPath = path$1.join(normalizedPath, entry.name);
217
+ const target = entry.isSymbolicLink() ? statOrNull(fullPath) : null;
218
+ const isDirectory = target ? target.isDirectory() : entry.isDirectory();
219
+ const isFile = target ? target.isFile() : entry.isFile();
203
220
  const result = {
204
221
  path: fullPath,
205
- isDirectory: entry.isDirectory()
222
+ isDirectory
206
223
  };
207
- if (entry.isFile()) try {
208
- result.size = fs$2.statSync(fullPath).size;
209
- } catch {}
224
+ if (isFile) {
225
+ const fileStats = target ?? statOrNull(fullPath);
226
+ if (fileStats) result.size = fileStats.size;
227
+ }
210
228
  return result;
211
229
  });
212
230
  } catch {
@@ -1555,7 +1573,7 @@ function webFetchBody(result) {
1555
1573
  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
1574
  return result.truncated ? result.markdown + truncationMarker(result) : result.markdown;
1557
1575
  }
1558
- function isPdfUrl(url) {
1576
+ function isPdfUrl$2(url) {
1559
1577
  try {
1560
1578
  const { pathname } = new URL(url);
1561
1579
  return pathname.toLowerCase().endsWith(".pdf");
@@ -1615,7 +1633,7 @@ async function probeLlmsTxt(pageUrl) {
1615
1633
  */
1616
1634
  async function firecrawlFetch(adapters, url, options) {
1617
1635
  if (!/^https?:\/\/.+/i.test(url)) throw new Error(`Invalid URL format: ${url}. URL must start with http:// or https://`);
1618
- const isPdf = isPdfUrl(url);
1636
+ const isPdf = isPdfUrl$2(url);
1619
1637
  const desiredTimeout = isPdf ? PDF_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
1620
1638
  const timeoutMs = options?.maxTimeoutMs ? Math.min(desiredTimeout, options.maxTimeoutMs) : desiredTimeout;
1621
1639
  const app = createFirecrawlApp(await (0, apiKeyService_exports.getFirecrawlConfig)(adapters));
@@ -1819,7 +1837,27 @@ const webFetchTool = {
1819
1837
  })
1820
1838
  };
1821
1839
  //#endregion
1822
- //#region ../../b4m-core/services/dist/websearch-DpUKKZyj.mjs
1840
+ //#region ../../b4m-core/services/dist/websearch-BLmQCbHG.mjs
1841
+ /**
1842
+ * The coarse recency bucket both providers speak, as the smallest one containing `recencyDays`.
1843
+ * Null when there is no constraint, or when the window is wider than the widest bucket - a
1844
+ * "within 10 years" filter is not a filter, and sending one would exclude undated pages for nothing.
1845
+ */
1846
+ function recencyBucket(recencyDays) {
1847
+ if (typeof recencyDays !== "number" || !Number.isFinite(recencyDays) || recencyDays <= 0) return null;
1848
+ if (recencyDays <= 1) return "day";
1849
+ if (recencyDays <= 7) return "week";
1850
+ if (recencyDays <= 31) return "month";
1851
+ if (recencyDays <= 366) return "year";
1852
+ return null;
1853
+ }
1854
+ /** SerpAPI spells the buckets `qdr:d|w|m|y` on the `tbs` parameter. */
1855
+ const SERPAPI_QDR = {
1856
+ day: "qdr:d",
1857
+ week: "qdr:w",
1858
+ month: "qdr:m",
1859
+ year: "qdr:y"
1860
+ };
1823
1861
  const DEFAULT_NUM_RESULTS = 3;
1824
1862
  const SEARCH_TIMEOUT_MS = 6e4;
1825
1863
  /**
@@ -1828,14 +1866,14 @@ const SEARCH_TIMEOUT_MS = 6e4;
1828
1866
  * on a non-OK response so the tool surfaces the failure. Exported (re-exported from index) for the
1829
1867
  * REST endpoint and existing tests.
1830
1868
  */
1831
- async function serpApiSearch(adapters, query, num_results) {
1869
+ async function serpApiSearch(adapters, query, num_results, options) {
1832
1870
  const apiKey = await (0, apiKeyService_exports.getSerperKey)(adapters);
1833
1871
  const url = new URL("https://serpapi.com/search");
1834
1872
  if (!apiKey) {
1835
1873
  Logger.globalInstance.error("❌ WebSearch Tool: No API key configured. Skipping search.");
1836
1874
  return { organic_results: [] };
1837
1875
  }
1838
- url.search = new URLSearchParams({
1876
+ const searchParams = new URLSearchParams({
1839
1877
  engine: "google",
1840
1878
  api_key: apiKey,
1841
1879
  q: query,
@@ -1844,7 +1882,10 @@ async function serpApiSearch(adapters, query, num_results) {
1844
1882
  gl: "us",
1845
1883
  hl: "en",
1846
1884
  num: (num_results || DEFAULT_NUM_RESULTS).toString()
1847
- }).toString();
1885
+ });
1886
+ const bucket = recencyBucket(options?.recencyDays);
1887
+ if (bucket) searchParams.set("tbs", SERPAPI_QDR[bucket]);
1888
+ url.search = searchParams.toString();
1848
1889
  const controller = new AbortController();
1849
1890
  const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
1850
1891
  let response;
@@ -1872,8 +1913,8 @@ async function serpApiSearch(adapters, query, num_results) {
1872
1913
  function createSerpApiProvider(adapters) {
1873
1914
  return {
1874
1915
  name: "serpapi",
1875
- async search(query, numResults) {
1876
- const data = await serpApiSearch(adapters, query, numResults);
1916
+ async search(query, numResults, options) {
1917
+ const data = await serpApiSearch(adapters, query, numResults, options);
1877
1918
  return (Array.isArray(data.organic_results) ? data.organic_results : []).filter((r) => !!r && typeof r.link === "string").map((r) => ({
1878
1919
  title: r.title ?? r.link,
1879
1920
  url: r.link,
@@ -1912,16 +1953,19 @@ function parseSearxngResults(data, numResults) {
1912
1953
  function createSearxngProvider(baseUrl) {
1913
1954
  return {
1914
1955
  name: "searxng",
1915
- async search(query, numResults) {
1956
+ async search(query, numResults, options) {
1916
1957
  const limit = numResults && numResults > 0 ? numResults : DEFAULT_NUM_RESULTS;
1917
1958
  const trimmed = baseUrl.replace(/\/+$/, "");
1918
1959
  const url = new URL(`${trimmed}/search`);
1919
- url.search = new URLSearchParams({
1960
+ const params = new URLSearchParams({
1920
1961
  q: query,
1921
1962
  format: "json",
1922
1963
  language: "en",
1923
1964
  safesearch: "1"
1924
- }).toString();
1965
+ });
1966
+ const bucket = recencyBucket(options?.recencyDays);
1967
+ if (bucket) params.set("time_range", bucket);
1968
+ url.search = params.toString();
1925
1969
  const controller = new AbortController();
1926
1970
  const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
1927
1971
  try {
@@ -2044,7 +2088,7 @@ const webSearchTool = {
2044
2088
  })
2045
2089
  };
2046
2090
  //#endregion
2047
- //#region ../../b4m-core/services/dist/toolGenerators-BVUZiwqE.mjs
2091
+ //#region ../../b4m-core/services/dist/toolGenerators-hk-Robqc.mjs
2048
2092
  const diceRoll = async (parameters) => {
2049
2093
  if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
2050
2094
  return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
@@ -2731,11 +2775,12 @@ const askUserQuestionTool = {
2731
2775
  * re-export them without pulling the full tool graph. `index.ts` re-exports them
2732
2776
  * so the server barrel's public API is unchanged.
2733
2777
  */
2734
- const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2778
+ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionPreauthorizedLakeIds, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
2735
2779
  const context = {
2736
2780
  userId,
2737
2781
  user,
2738
2782
  sessionId,
2783
+ questId,
2739
2784
  logger,
2740
2785
  db,
2741
2786
  storage,
@@ -2752,9 +2797,13 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
2752
2797
  kbScope,
2753
2798
  inlinedAttachmentIds,
2754
2799
  fullyInlinedAttachmentIds,
2800
+ suppressLakeArms,
2801
+ sessionRetrievalTags,
2802
+ sessionPreauthorizedLakeIds,
2755
2803
  codeMinifier,
2756
2804
  availableModels,
2757
- onToolLlmUsage
2805
+ onToolLlmUsage,
2806
+ getAbortSignal
2758
2807
  };
2759
2808
  return Object.entries(tools).reduce((acc, [key, tool]) => ({
2760
2809
  ...acc,
@@ -4221,7 +4270,7 @@ const latticeAddEntityTool = {
4221
4270
  };
4222
4271
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4223
4272
  const model = await context.db.latticeModels.findById(modelId);
4224
- if (model) {
4273
+ if (model && model.userId === context.userId) {
4225
4274
  const existingIndex = model.data.entities.findIndex((e) => e.id === entityId);
4226
4275
  if (existingIndex >= 0) model.data.entities[existingIndex] = entityData;
4227
4276
  else model.data.entities.push(entityData);
@@ -4231,7 +4280,23 @@ const latticeAddEntityTool = {
4231
4280
  updatedAt: /* @__PURE__ */ new Date()
4232
4281
  });
4233
4282
  context.logger.info(`[Lattice] Added entity ${entityId} to model ${modelId}`);
4234
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4283
+ } else if (model) {
4284
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4285
+ return JSON.stringify({
4286
+ success: false,
4287
+ action: "ADD_ENTITY",
4288
+ modelId,
4289
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4290
+ });
4291
+ } else {
4292
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4293
+ return JSON.stringify({
4294
+ success: false,
4295
+ action: "ADD_ENTITY",
4296
+ modelId,
4297
+ error: `Model ${modelId} not found`
4298
+ });
4299
+ }
4235
4300
  } catch (error) {
4236
4301
  context.logger.error(`[Lattice] Failed to persist entity to database:`, error);
4237
4302
  }
@@ -4349,7 +4414,7 @@ const latticeSetValueTool = {
4349
4414
  const entityId = entityName.toLowerCase().replace(/\s+/g, "_");
4350
4415
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4351
4416
  const model = await context.db.latticeModels.findById(modelId);
4352
- if (model) {
4417
+ if (model && model.userId === context.userId) {
4353
4418
  const entity = model.data.entities.find((e) => e.id === entityId || e.name === entityName);
4354
4419
  if (entity) {
4355
4420
  const attrIndex = entity.attributes.findIndex((a) => a.key === attributeKey);
@@ -4369,7 +4434,23 @@ const latticeSetValueTool = {
4369
4434
  });
4370
4435
  context.logger.info(`[Lattice] Set ${entityId}.${attributeKey} = ${value} in model ${modelId}`);
4371
4436
  } else context.logger.warn(`[Lattice] Entity ${entityName} not found in model ${modelId}`);
4372
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4437
+ } else if (model) {
4438
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4439
+ return JSON.stringify({
4440
+ success: false,
4441
+ action: "SET_VALUE",
4442
+ modelId,
4443
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4444
+ });
4445
+ } else {
4446
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4447
+ return JSON.stringify({
4448
+ success: false,
4449
+ action: "SET_VALUE",
4450
+ modelId,
4451
+ error: `Model ${modelId} not found`
4452
+ });
4453
+ }
4373
4454
  } catch (error) {
4374
4455
  context.logger.error(`[Lattice] Failed to persist value to database:`, error);
4375
4456
  }
@@ -4468,7 +4549,7 @@ const latticeCreateRuleTool = {
4468
4549
  let entityCreatedMessage = "";
4469
4550
  if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
4470
4551
  const model = await context.db.latticeModels.findById(modelId);
4471
- if (model) {
4552
+ if (model && model.userId === context.userId) {
4472
4553
  if (!model.data.entities.some((e) => e.id === outputEntityId || e.name.toLowerCase() === parsedRule.outputEntity.toLowerCase()) && parsedRule.outputEntity !== "unknown") {
4473
4554
  const now = /* @__PURE__ */ new Date();
4474
4555
  const newEntity = {
@@ -4505,7 +4586,23 @@ const latticeCreateRuleTool = {
4505
4586
  updatedAt: /* @__PURE__ */ new Date()
4506
4587
  });
4507
4588
  context.logger.info(`[Lattice] Created rule ${ruleId} in model ${modelId}`);
4508
- } else context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4589
+ } else if (model) {
4590
+ context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
4591
+ return JSON.stringify({
4592
+ success: false,
4593
+ action: "CREATE_RULE",
4594
+ modelId,
4595
+ error: `Access denied: you do not have permission to modify model ${modelId}`
4596
+ });
4597
+ } else {
4598
+ context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
4599
+ return JSON.stringify({
4600
+ success: false,
4601
+ action: "CREATE_RULE",
4602
+ modelId,
4603
+ error: `Model ${modelId} not found`
4604
+ });
4605
+ }
4509
4606
  } catch (error) {
4510
4607
  context.logger.error(`[Lattice] Failed to persist rule to database:`, error);
4511
4608
  }
@@ -5438,6 +5535,61 @@ function extractCommandName(filename) {
5438
5535
  return name;
5439
5536
  }
5440
5537
  //#endregion
5538
+ //#region src/utils/findMarkdownFiles.ts
5539
+ /**
5540
+ * Classify a directory entry, resolving symlinks by their target.
5541
+ *
5542
+ * `fs.readdir(withFileTypes)` builds each Dirent from the entry itself, never
5543
+ * the link target, so a symlink reports `isFile() === false` AND
5544
+ * `isDirectory() === false`. A walk keyed off those two predicates alone skips
5545
+ * every symlinked entry.
5546
+ */
5547
+ async function classifyEntry(fullPath, entry) {
5548
+ if (!entry.isSymbolicLink()) {
5549
+ if (entry.isDirectory()) return "directory";
5550
+ return entry.isFile() ? "file" : "other";
5551
+ }
5552
+ try {
5553
+ const stats = await fs$1.stat(fullPath);
5554
+ if (stats.isDirectory()) return "directory";
5555
+ return stats.isFile() ? "file" : "other";
5556
+ } catch {
5557
+ return "other";
5558
+ }
5559
+ }
5560
+ /**
5561
+ * Recursively collect every `.md` file under `directory`, following symlinks.
5562
+ *
5563
+ * Symlink support is load-bearing, not a nicety: dotfile managers (nix
5564
+ * home-manager, chezmoi, a plain dotfiles repo) materialize
5565
+ * `~/.claude/skills/<name>/SKILL.md` as a symlink into an immutable store, so a
5566
+ * walk that only accepts real files finds zero global skills on those machines.
5567
+ *
5568
+ * Directories are deduped by realpath so a cyclic link cannot spin forever.
5569
+ */
5570
+ async function findMarkdownFiles(directory, visitedRealPaths = /* @__PURE__ */ new Set()) {
5571
+ const files = [];
5572
+ try {
5573
+ const realDirectory = await fs$1.realpath(directory);
5574
+ if (visitedRealPaths.has(realDirectory)) return files;
5575
+ visitedRealPaths.add(realDirectory);
5576
+ } catch {}
5577
+ let entries;
5578
+ try {
5579
+ entries = await fs$1.readdir(directory, { withFileTypes: true });
5580
+ } catch (error) {
5581
+ console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
5582
+ return files;
5583
+ }
5584
+ for (const entry of entries) {
5585
+ const fullPath = path.join(directory, entry.name);
5586
+ const kind = await classifyEntry(fullPath, entry);
5587
+ if (kind === "directory") files.push(...await findMarkdownFiles(fullPath, visitedRealPaths));
5588
+ else if (kind === "file" && entry.name.endsWith(".md")) files.push(fullPath);
5589
+ }
5590
+ return files;
5591
+ }
5592
+ //#endregion
5441
5593
  //#region src/storage/CustomCommandStore.ts
5442
5594
  var CustomCommandStore = class {
5443
5595
  constructor(projectRoot, options = {}) {
@@ -5515,7 +5667,7 @@ var CustomCommandStore = class {
5515
5667
  async loadCommandsFromDirectory(directory, source) {
5516
5668
  try {
5517
5669
  if (!(await fs$1.stat(directory)).isDirectory()) return;
5518
- const commandFiles = await this.findCommandFiles(directory);
5670
+ const commandFiles = await findMarkdownFiles(directory);
5519
5671
  for (const filePath of commandFiles) try {
5520
5672
  await this.loadCommandFile(filePath, source);
5521
5673
  } catch (error) {
@@ -5526,28 +5678,6 @@ var CustomCommandStore = class {
5526
5678
  }
5527
5679
  }
5528
5680
  /**
5529
- * Recursively finds all .md files in a directory
5530
- *
5531
- * @param directory - Directory to search
5532
- * @returns Array of full file paths to .md files
5533
- */
5534
- async findCommandFiles(directory) {
5535
- const files = [];
5536
- try {
5537
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
5538
- for (const entry of entries) {
5539
- const fullPath = path.join(directory, entry.name);
5540
- if (entry.isDirectory()) {
5541
- const subFiles = await this.findCommandFiles(fullPath);
5542
- files.push(...subFiles);
5543
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
5544
- }
5545
- } catch (error) {
5546
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
5547
- }
5548
- return files;
5549
- }
5550
- /**
5551
5681
  * Loads a single command file
5552
5682
  *
5553
5683
  * @param filePath - Full path to the command file
@@ -6359,6 +6489,130 @@ var EmbeddingAuthError = class extends Error {
6359
6489
  this.provider = provider;
6360
6490
  }
6361
6491
  };
6492
+ /**
6493
+ * Passive reporting of embedding-provider rate-limit ceilings.
6494
+ *
6495
+ * A rate limit belongs to the provider organization behind the key, and every embedding response
6496
+ * already carries the ceiling in its headers, so reading them costs no extra request and no extra
6497
+ * tokens. Providers that do not report them (Bedrock, Ollama) simply never produce an observation.
6498
+ *
6499
+ * This module owns the "what did the provider say" half only. It has no opinion about data lakes
6500
+ * or about the throughput levers configured against these numbers; interpreting a ceiling against
6501
+ * a lever belongs to the layer that knows what the levers are.
6502
+ */
6503
+ /**
6504
+ * Remaining/limit ratio at or below which the provider counts as under pressure. A bulk re-index
6505
+ * draws its window down steadily, so this sits low enough that ordinary throughput does not trip
6506
+ * it and only genuine starvation does.
6507
+ */
6508
+ const PRESSURE_RATIO = .1;
6509
+ /**
6510
+ * Pressure lasts as long as the window does, and every call in that window reports it. Throttle to
6511
+ * one line per interval so a starved ingest leaves a readable trace instead of flooding the log.
6512
+ */
6513
+ const PRESSURE_LOG_INTERVAL_MS = 6e4;
6514
+ /**
6515
+ * Process-local, and deliberately so: a cold start re-reports what it measures rather than leaving
6516
+ * a gap shared storage would have to close. Keyed by provider+model+account, so the map is bounded
6517
+ * by the model list times the number of distinct credentials the process serves.
6518
+ */
6519
+ const stateByKey = /* @__PURE__ */ new Map();
6520
+ /**
6521
+ * A broken reporter is indistinguishable from a steady ceiling - both are silence - so the first
6522
+ * fault has to be loud. Subsequent ones drop to debug: whatever breaks here breaks on every
6523
+ * embedding call, and a bulk re-index would drown the log in it.
6524
+ */
6525
+ let hasReportedFailure = false;
6526
+ const keyFor = (provider, model, account) => `${provider}:${model}:${account}`;
6527
+ const ceilingChanged = (previous, next) => previous.limitTokens !== next.limitTokens || previous.limitRequests !== next.limitRequests;
6528
+ const describeCeiling = (snapshot) => `${snapshot.limitTokens ?? "unreported"} tokens/min, ${snapshot.limitRequests ?? "unreported"} requests/min`;
6529
+ /** Ratio of the window still available, or null when the provider did not report that dimension. */
6530
+ const remainingRatio = (remaining, limit) => {
6531
+ if (remaining === null || limit === null || limit <= 0) return null;
6532
+ return remaining / limit;
6533
+ };
6534
+ const pressuredDimensions = (snapshot) => {
6535
+ const tokens = remainingRatio(snapshot.remainingTokens, snapshot.limitTokens);
6536
+ const requests = remainingRatio(snapshot.remainingRequests, snapshot.limitRequests);
6537
+ const dimensions = [];
6538
+ if (tokens !== null && tokens <= PRESSURE_RATIO) dimensions.push("tokens");
6539
+ if (requests !== null && requests <= PRESSURE_RATIO) dimensions.push("requests");
6540
+ return dimensions;
6541
+ };
6542
+ /**
6543
+ * Read the rate-limit headers off an embedding response and report the ceiling when it is worth
6544
+ * reporting: the first sighting in this process, a change since the last sighting, or the window
6545
+ * running down. Returns the observation when the provider reported a usable ceiling, else null.
6546
+ *
6547
+ * `account` identifies the provider account the reading belongs to and is part of the memo key,
6548
+ * not just the log line. The credential is resolved per user - a stored personal key beats the
6549
+ * platform key in `getEffectiveLLMApiKeys` - so one process can see several accounts on the same
6550
+ * provider+model. Without the discriminator their readings would collapse into one entry that
6551
+ * flaps between unrelated ceilings and attributes each figure to whoever reads the log next. The
6552
+ * caller supplies it; it must never be key material.
6553
+ *
6554
+ * Never throws. This hangs off the hot path of every embedding call, and a reporting fault must
6555
+ * not be able to fail an embedding that otherwise succeeded.
6556
+ */
6557
+ function recordEmbeddingRateLimitHeaders(provider, model, account, headers, now = Date.now()) {
6558
+ try {
6559
+ const snapshot = parseEmbeddingRateLimitHeaders(headers);
6560
+ if (!hasUsableLimits(snapshot)) return null;
6561
+ const key = keyFor(provider, model, account);
6562
+ const previous = stateByKey.get(key);
6563
+ const observation = {
6564
+ provider,
6565
+ model,
6566
+ account,
6567
+ snapshot,
6568
+ observedAt: now
6569
+ };
6570
+ const subject = `${provider} ${model} (account ${account})`;
6571
+ if (!previous) Logger.globalInstance.info(`[embedding-limits] ${subject} ceiling measured: ${describeCeiling(snapshot)}`, {
6572
+ provider,
6573
+ model,
6574
+ account,
6575
+ limitTokens: snapshot.limitTokens,
6576
+ limitRequests: snapshot.limitRequests
6577
+ });
6578
+ else if (ceilingChanged(previous.last.snapshot, snapshot)) Logger.globalInstance.warn(`[embedding-limits] ${subject} ceiling CHANGED: was ${describeCeiling(previous.last.snapshot)}, now ${describeCeiling(snapshot)}. Reconcile any throughput lever governed by this account against the new figure.`, {
6579
+ provider,
6580
+ model,
6581
+ account,
6582
+ previousLimitTokens: previous.last.snapshot.limitTokens,
6583
+ previousLimitRequests: previous.last.snapshot.limitRequests,
6584
+ limitTokens: snapshot.limitTokens,
6585
+ limitRequests: snapshot.limitRequests
6586
+ });
6587
+ const pressured = pressuredDimensions(snapshot);
6588
+ const dueForPressureLog = previous?.lastPressureLogAt == null || now - previous.lastPressureLogAt >= PRESSURE_LOG_INTERVAL_MS;
6589
+ const logPressure = pressured.length > 0 && dueForPressureLog;
6590
+ if (logPressure) Logger.globalInstance.warn(`[embedding-limits] ${subject} is at or below ${PRESSURE_RATIO * 100}% of its ${pressured.join(" and ")} window`, {
6591
+ provider,
6592
+ model,
6593
+ account,
6594
+ remainingTokens: snapshot.remainingTokens,
6595
+ remainingRequests: snapshot.remainingRequests,
6596
+ limitTokens: snapshot.limitTokens,
6597
+ limitRequests: snapshot.limitRequests,
6598
+ resetTokensMs: snapshot.resetTokensMs,
6599
+ resetRequestsMs: snapshot.resetRequestsMs
6600
+ });
6601
+ stateByKey.set(key, {
6602
+ last: observation,
6603
+ lastPressureLogAt: logPressure ? now : previous?.lastPressureLogAt ?? null
6604
+ });
6605
+ return observation;
6606
+ } catch (error) {
6607
+ const message = `[embedding-limits] failed to record rate-limit headers: ${error}`;
6608
+ if (hasReportedFailure) Logger.globalInstance.debug(message);
6609
+ else {
6610
+ hasReportedFailure = true;
6611
+ Logger.globalInstance.warn(message);
6612
+ }
6613
+ return null;
6614
+ }
6615
+ }
6362
6616
  const OPENAI_EMBEDDING_MODEL_MAP = {
6363
6617
  [OpenAIEmbeddingModel.TEXT_EMBEDDING_3_SMALL]: {
6364
6618
  provider: "OpenAI",
@@ -6379,34 +6633,72 @@ const OPENAI_EMBEDDING_MODEL_MAP = {
6379
6633
  dimensions: [1536]
6380
6634
  }
6381
6635
  };
6382
- var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6636
+ /**
6637
+ * Non-reversible stand-in for a credential, for use where two accounts have to be told apart in a
6638
+ * log. Same construction as the API-key logging hash in the request middleware. Never emit the key.
6639
+ */
6640
+ const fingerprintCredential = (apiKey) => `key:${createHash("sha256").update(apiKey).digest("hex").slice(0, 16)}`;
6641
+ /**
6642
+ * Total by construction. The only caller runs inside processSingleBatch's classifying try, where a
6643
+ * throw would be misread as a provider error and re-issue the batch.
6644
+ */
6645
+ const headerOrNull = (httpResponse, name) => {
6646
+ try {
6647
+ return httpResponse.headers?.get(name) ?? null;
6648
+ } catch {
6649
+ return null;
6650
+ }
6651
+ };
6652
+ /**
6653
+ * The ceilings `generateEmbeddingBatch` splits on, at module scope and exported because a cost
6654
+ * PREFLIGHT has to model the same split before it spends (packages/scripts/retrieval/capturePlan.ts).
6655
+ * A second copy of these numbers in a script cannot track a provider change.
6656
+ */
6657
+ const OPENAI_MAX_INPUTS_PER_REQUEST = 2048;
6658
+ const OPENAI_MAX_TOKENS_PER_INPUT = 8192;
6659
+ /**
6660
+ * Effective token limit with a 10% safety buffer.
6661
+ * The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
6662
+ * but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
6663
+ * that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
6664
+ */
6665
+ const OPENAI_EFFECTIVE_TOKEN_LIMIT = Math.floor(27e4);
6666
+ var OpenAIEmbeddingService = class {
6383
6667
  client;
6384
6668
  model;
6385
- /** Hard limit imposed by OpenAI's embeddings API. */
6386
- static MAX_TOKENS_PER_REQUEST = 3e5;
6387
- /**
6388
- * Effective token limit with a 10% safety buffer.
6389
- * The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
6390
- * but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
6391
- * that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
6392
- */
6393
- static EFFECTIVE_TOKEN_LIMIT = Math.floor(OpenAIEmbeddingService.MAX_TOKENS_PER_REQUEST * .9);
6669
+ credentialFingerprint;
6394
6670
  constructor(apiKey, model = OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002) {
6395
6671
  this.client = new OpenAI({ apiKey });
6396
6672
  this.validateModel(model);
6397
6673
  this.model = model;
6674
+ this.credentialFingerprint = fingerprintCredential(apiKey);
6675
+ }
6676
+ /**
6677
+ * Report the provider ceiling carried on a response we already received. Covers ingest and
6678
+ * query alike: both reach OpenAI through this class, so neither needs its own sampling point.
6679
+ *
6680
+ * The ceiling belongs to the organization behind the key, and the key is resolved per user
6681
+ * (getEffectiveLLMApiKeys prefers a stored personal key over the platform one), so the reading
6682
+ * has to say whose it is. `openai-organization` is the provider's own answer to that; the
6683
+ * credential fingerprint covers the case where the response omits it, and still keeps two
6684
+ * distinct keys as two readings rather than one that flaps between them.
6685
+ */
6686
+ recordRateLimit(httpResponse) {
6687
+ const account = headerOrNull(httpResponse, "openai-organization") || this.credentialFingerprint;
6688
+ recordEmbeddingRateLimitHeaders("OpenAI", this.model, account, httpResponse.headers);
6398
6689
  }
6399
6690
  validateModel(model) {
6400
6691
  if (!OPENAI_EMBEDDING_MODEL_MAP[model]) throw new Error(`Invalid OpenAI embedding model: ${model}`);
6401
6692
  }
6402
6693
  async generateEmbedding(text) {
6403
- const response = await this.client.embeddings.create({
6694
+ const { data: body, response: httpResponse } = await this.client.embeddings.create({
6404
6695
  model: this.model,
6405
6696
  input: text
6406
- }).catch((error) => {
6697
+ }).withResponse().catch((error) => {
6407
6698
  throw this.toActionableAuthError(error);
6408
6699
  });
6409
- if (response.data && response.data.length > 0) return response.data[0].embedding;
6700
+ this.recordRateLimit(httpResponse);
6701
+ if (body.data && body.data.length > 0) return body.data[0].embedding;
6410
6702
  throw new Error("No embedding data received from OpenAI");
6411
6703
  }
6412
6704
  /**
@@ -6437,8 +6729,6 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6437
6729
  */
6438
6730
  async generateEmbeddingBatch(texts, tokenCounts) {
6439
6731
  if (texts.length === 0) return [];
6440
- const MAX_INPUTS_PER_REQUEST = 2048;
6441
- const MAX_TOKENS_PER_INPUT = 8192;
6442
6732
  let tokens;
6443
6733
  let needsRecalculation = false;
6444
6734
  if (!tokenCounts || tokenCounts.length !== texts.length) needsRecalculation = true;
@@ -6453,12 +6743,12 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6453
6743
  let totalTokens = 0;
6454
6744
  for (let i = 0; i < texts.length; i++) {
6455
6745
  const tokenCount = tokens[i];
6456
- if (tokenCount > MAX_TOKENS_PER_INPUT) throw new Error(`Input at index ${i} exceeds ${MAX_TOKENS_PER_INPUT} token limit (${tokenCount} tokens)`);
6746
+ if (tokenCount > 8192) throw new Error(`Input at index ${i} exceeds ${OPENAI_MAX_TOKENS_PER_INPUT} token limit (${tokenCount} tokens)`);
6457
6747
  totalTokens += tokenCount;
6458
6748
  }
6459
6749
  Logger.globalInstance.debug(`[OpenAI] Batch embedding: ${texts.length} inputs, ${totalTokens} total tokens`);
6460
- const batches = this.createBatches(texts, tokens, MAX_INPUTS_PER_REQUEST, OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT);
6461
- Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT} tokens)`);
6750
+ const batches = this.createBatches(texts, tokens, OPENAI_MAX_INPUTS_PER_REQUEST, OPENAI_EFFECTIVE_TOKEN_LIMIT);
6751
+ Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens)`);
6462
6752
  if (batches.length === 1) return await this.processSingleBatch(batches[0].texts);
6463
6753
  const allEmbeddings = new Array(texts.length);
6464
6754
  for (const batch of batches) (await this.processSingleBatch(batch.texts)).forEach((embedding, batchIndex) => {
@@ -6524,8 +6814,8 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6524
6814
  async processSingleBatch(texts, preCalculatedTokens) {
6525
6815
  const tokenCounts = preCalculatedTokens || await this.calculateTokenCounts(texts);
6526
6816
  const batchTokens = tokenCounts.reduce((sum, count) => sum + count, 0);
6527
- if (batchTokens > OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT) {
6528
- Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${OpenAIEmbeddingService.EFFECTIVE_TOKEN_LIMIT} tokens), splitting recursively`);
6817
+ if (batchTokens > 27e4) {
6818
+ Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens), splitting recursively`);
6529
6819
  const mid = Math.ceil(texts.length / 2);
6530
6820
  const firstHalf = texts.slice(0, mid);
6531
6821
  const secondHalf = texts.slice(mid);
@@ -6534,13 +6824,14 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
6534
6824
  const [firstEmbeddings, secondEmbeddings] = await Promise.all([this.processSingleBatch(firstHalf, firstTokens), this.processSingleBatch(secondHalf, secondTokens)]);
6535
6825
  return [...firstEmbeddings, ...secondEmbeddings];
6536
6826
  }
6537
- for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's 8192 token limit per input (${tokenCounts[i]} tokens). This indicates a data integrity issue - chunk should have been smaller. This chunk cannot be processed and the entire batch must fail.`);
6827
+ for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's ${OPENAI_MAX_TOKENS_PER_INPUT} token limit per input (${tokenCounts[i]} tokens). This indicates a data integrity issue - chunk should have been smaller. This chunk cannot be processed and the entire batch must fail.`);
6538
6828
  try {
6539
- const response = await this.client.embeddings.create({
6829
+ const { data: body, response: httpResponse } = await this.client.embeddings.create({
6540
6830
  model: this.model,
6541
6831
  input: texts
6542
- });
6543
- if (response.data && response.data.length > 0) return response.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
6832
+ }).withResponse();
6833
+ this.recordRateLimit(httpResponse);
6834
+ if (body.data && body.data.length > 0) return body.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
6544
6835
  else throw new Error("No embedding data received from OpenAI");
6545
6836
  } catch (error) {
6546
6837
  if (error instanceof OpenAI.AuthenticationError) throw this.toActionableAuthError(error);
@@ -7045,6 +7336,7 @@ var SmartChunker = class {
7045
7336
  encoder;
7046
7337
  storage;
7047
7338
  bufferPercentOrValue;
7339
+ lastExtractedText;
7048
7340
  /**
7049
7341
  * @param model - The embedding model name
7050
7342
  * @param storage - Storage instance for file content
@@ -7098,6 +7390,13 @@ var SmartChunker = class {
7098
7390
  this.encoder = void 0;
7099
7391
  }
7100
7392
  }
7393
+ /**
7394
+ * The canonical extracted text from the most recent chunkFile() call - policy-independent, unlike
7395
+ * the returned chunks. Undefined when the file yielded no extractable text. See lastExtractedText.
7396
+ */
7397
+ getExtractedText() {
7398
+ return this.lastExtractedText;
7399
+ }
7101
7400
  async chunkFile(fileOrContent, mimeType) {
7102
7401
  let content;
7103
7402
  if (Buffer.isBuffer(fileOrContent)) content = fileOrContent;
@@ -7107,6 +7406,7 @@ var SmartChunker = class {
7107
7406
  }
7108
7407
  this.logger.updateMetadata({ mimeType });
7109
7408
  this.logger.log(`Chunking file with type: ${mimeType}`);
7409
+ this.lastExtractedText = void 0;
7110
7410
  if (isAudioMimeType(mimeType)) {
7111
7411
  this.logger.log(`Skipping chunking for audio file type: ${mimeType}`);
7112
7412
  return [];
@@ -7144,12 +7444,17 @@ var SmartChunker = class {
7144
7444
  case SupportedFabFileMimeTypes.PHP:
7145
7445
  case SupportedFabFileMimeTypes.RUBY:
7146
7446
  case SupportedFabFileMimeTypes.SH:
7147
- case SupportedFabFileMimeTypes.BASH:
7148
- chunks = await this.chunkText(content.toString());
7447
+ case SupportedFabFileMimeTypes.BASH: {
7448
+ const textContent = content.toString();
7449
+ this.lastExtractedText = textContent;
7450
+ chunks = await this.chunkText(textContent);
7149
7451
  break;
7452
+ }
7150
7453
  default:
7151
7454
  if (mimeType && mimeType.startsWith("text/")) {
7152
- chunks = await this.chunkText(content.toString());
7455
+ const textContent = content.toString();
7456
+ this.lastExtractedText = textContent;
7457
+ chunks = await this.chunkText(textContent);
7153
7458
  break;
7154
7459
  }
7155
7460
  this.logger.error(`Unsupported file type: ${mimeType}`);
@@ -7161,7 +7466,9 @@ var SmartChunker = class {
7161
7466
  return await this.storage.getContentAsBuffer(file.filePath);
7162
7467
  }
7163
7468
  async chunkCSV(content) {
7164
- const rows = content.toString("utf8").split(/\r?\n/).filter((row) => row.trim().length > 0);
7469
+ const csvString = content.toString("utf8");
7470
+ this.lastExtractedText = csvString;
7471
+ const rows = csvString.split(/\r?\n/).filter((row) => row.trim().length > 0);
7165
7472
  const chunks = [];
7166
7473
  let currentChunk = [];
7167
7474
  let currentTokens = 0;
@@ -7214,6 +7521,7 @@ var SmartChunker = class {
7214
7521
  async chunkPDF(content) {
7215
7522
  const pdf = await getDocumentProxy(new Uint8Array(content));
7216
7523
  const { text } = await extractText(pdf);
7524
+ this.lastExtractedText = Array.isArray(text) ? text.join("\n") : text;
7217
7525
  if (typeof text === "string") return this.chunkText(text);
7218
7526
  const chunks = [];
7219
7527
  let currentChunk = "";
@@ -7245,7 +7553,9 @@ var SmartChunker = class {
7245
7553
  return chunks;
7246
7554
  }
7247
7555
  async chunkJSON(content) {
7248
- const json = JSON.parse(content.toString());
7556
+ const jsonString = content.toString();
7557
+ this.lastExtractedText = jsonString;
7558
+ const json = JSON.parse(jsonString);
7249
7559
  return this.chunkObject(json);
7250
7560
  }
7251
7561
  async chunkObject(obj) {
@@ -7312,6 +7622,7 @@ var SmartChunker = class {
7312
7622
  }
7313
7623
  async chunkDOCX(content) {
7314
7624
  const result = await mammoth.extractRawText({ buffer: content });
7625
+ this.lastExtractedText = result.value;
7315
7626
  return this.chunkText(result.value);
7316
7627
  }
7317
7628
  async chunkPPTX(content) {
@@ -7330,6 +7641,7 @@ var SmartChunker = class {
7330
7641
  this.logger.warn("PPTX contained no extractable slide text");
7331
7642
  return [];
7332
7643
  }
7644
+ this.lastExtractedText = fullText;
7333
7645
  return this.chunkText(fullText);
7334
7646
  }
7335
7647
  async chunkText(content) {
@@ -7404,6 +7716,9 @@ var SmartChunker = class {
7404
7716
  async chunkExcel(content) {
7405
7717
  const { read, utils } = await import("xlsx");
7406
7718
  const workbook = read(content, { type: "buffer" });
7719
+ this.lastExtractedText = workbook.SheetNames.map((sheetName) => {
7720
+ 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} ---`;
7721
+ }).join("\n");
7407
7722
  const chunks = [];
7408
7723
  let currentChunk = "";
7409
7724
  let currentTokens = 0;
@@ -7623,6 +7938,8 @@ const dnsResolve6 = promisify(dns.resolve6);
7623
7938
  * Check if an IPv4 address is in a private/internal range.
7624
7939
  */
7625
7940
  function isPrivateIPv4(ip) {
7941
+ const nonCanonical = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
7942
+ if (nonCanonical && nonCanonical.slice(1).some((octet) => octet.length > 1 && octet.startsWith("0"))) return true;
7626
7943
  const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
7627
7944
  if (!ipv4Match) return false;
7628
7945
  const [, a, b, c] = ipv4Match.map(Number);
@@ -7643,44 +7960,153 @@ function isPrivateIPv4(ip) {
7643
7960
  /**
7644
7961
  * Check if an IPv6 address is in a private/internal range.
7645
7962
  */
7963
+ /**
7964
+ * Strip the brackets WHATWG URL keeps on an IPv6 hostname: `new URL('http://[::1]/').hostname` is
7965
+ * `'[::1]'`, not `'::1'`. Every literal check below compares against unbracketed forms, so without
7966
+ * this a bracketed address matched nothing and fell through as safe.
7967
+ *
7968
+ * Same treatment as the sibling guards in this repo - `ssrfGuard.ts` and `external-image.ts` both
7969
+ * strip brackets before their literal checks.
7970
+ */
7971
+ function stripIpv6Brackets(hostname) {
7972
+ const h = hostname.toLowerCase();
7973
+ return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
7974
+ }
7975
+ /**
7976
+ * Canonicalize an IPv6 literal to the RFC 5952 form - leading zeros dropped per hextet, longest run of
7977
+ * two or more zero hextets compressed to `::`. That is the shape both feeders already hand this module
7978
+ * (WHATWG `URL.hostname` at the `validateUrlForFetch` call site, and getaddrinfo answers via
7979
+ * `ssrfSafeLookup`), so it is the shape every prefix arm in `isPrivateIPv6` was written against.
7980
+ * Canonicalizing once here is what lets those arms cover a family instead of enumerating its legal
7981
+ * spellings: `0:0:0:0:0:ffff:127.0.0.1` arrives as `::ffff:127.0.0.1` instead of matching nothing.
7982
+ *
7983
+ * A dotted IPv4 tail is deliberately kept dotted, unlike WHATWG which hexifies it. The mapped branch
7984
+ * below decodes a dotted tail exactly through `isPrivateIPv4` and only blanket-refuses when the tail is
7985
+ * hex, so hexifying here would turn `::ffff:8.8.8.8` into an over-block.
7986
+ *
7987
+ * This cannot over-block: dropping leading zeros only shortens hextets below 0x1000, and every
7988
+ * globally routable address is inside 2000::/3 (first hextet 0x2000-0x3fff), which never carries one.
7989
+ * Input that does not parse as IPv6 is returned untouched, so it keeps whatever verdict it has today -
7990
+ * which is also why the redundant zero-padded arms further down (`2001:0db8:`, `2001:0000:`,
7991
+ * `0064:ff9b:`, `0100::`) are left in place rather than deleted.
7992
+ */
7993
+ function normalizeIpv6(ip) {
7994
+ const ported = ip.match(/^(\[[^\]]*\]):\d+$/);
7995
+ const bare = stripIpv6Brackets(ported ? ported[1] : ip).replace(/%.*$/, "");
7996
+ const halves = bare.split("::");
7997
+ if (halves.length > 2) return bare;
7998
+ if (halves.length === 2 && halves[0].includes(".")) return bare;
7999
+ const tokens = halves.flatMap((half) => half === "" ? [] : half.split(":"));
8000
+ const dotted = tokens.length > 0 && tokens[tokens.length - 1].includes(".") ? tokens.pop() : void 0;
8001
+ if (!tokens.every((token) => /^[0-9a-f]{1,4}$/.test(token))) return bare;
8002
+ const compressed = halves.length === 2;
8003
+ const width = tokens.length + (dotted ? 2 : 0);
8004
+ if (compressed ? width > 8 : width !== 8) return bare;
8005
+ const hextets = tokens.map((token) => token.replace(/^0+(?=.)/, ""));
8006
+ if (compressed) {
8007
+ const headWidth = halves[0] === "" ? 0 : halves[0].split(":").length;
8008
+ hextets.splice(headWidth, 0, ...new Array(8 - width).fill("0"));
8009
+ }
8010
+ const run = {
8011
+ start: -1,
8012
+ length: 0
8013
+ };
8014
+ for (let i = 0; i < hextets.length; i++) {
8015
+ if (hextets[i] !== "0") continue;
8016
+ let end = i;
8017
+ while (end < hextets.length && hextets[end] === "0") end++;
8018
+ if (end - i > run.length) {
8019
+ run.start = i;
8020
+ run.length = end - i;
8021
+ }
8022
+ i = end;
8023
+ }
8024
+ const body = run.length >= 2 ? `${hextets.slice(0, run.start).join(":")}::${hextets.slice(run.start + run.length).join(":")}` : hextets.join(":");
8025
+ if (dotted === void 0) return body;
8026
+ return body.endsWith(":") ? `${body}${dotted}` : `${body}:${dotted}`;
8027
+ }
7646
8028
  function isPrivateIPv6(ip) {
7647
- const normalized = ip.toLowerCase();
8029
+ const normalized = normalizeIpv6(ip);
8030
+ if (!normalized.includes(":")) return false;
8031
+ if (/[[\]]/.test(normalized)) return true;
8032
+ if (!/^[0-9a-f:.]+$/.test(normalized)) return false;
8033
+ if (normalized.includes(".") && !/^::(ffff:)?\d+\.\d+\.\d+\.\d+$/.test(normalized)) return true;
7648
8034
  if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
7649
8035
  if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
7650
- if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
8036
+ if (normalized.startsWith("fe")) return true;
7651
8037
  if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
7652
8038
  if (normalized.startsWith("ff")) return true;
7653
- const ipv4MappedMatch = normalized.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
7654
- if (ipv4MappedMatch) return isPrivateIPv4(ipv4MappedMatch[1]);
8039
+ if (normalized.startsWith("::ffff:")) {
8040
+ const tail = normalized.slice(7);
8041
+ return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
8042
+ }
8043
+ if (normalized.startsWith("::")) {
8044
+ const tail = normalized.slice(2);
8045
+ return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
8046
+ }
8047
+ if (normalized.startsWith("0:")) return true;
8048
+ if (normalized.startsWith("5f00:")) return true;
8049
+ if (normalized.startsWith("3fff:")) {
8050
+ const rest = normalized.slice(5);
8051
+ if ((rest.startsWith(":") ? 0 : parseInt(rest.split(":")[0], 16)) <= 4095) return true;
8052
+ }
7655
8053
  if (normalized.startsWith("2001:db8:") || normalized.startsWith("2001:0db8:")) return true;
8054
+ if (normalized.startsWith("2002:")) return true;
8055
+ if (normalized.startsWith("2001:0:") || normalized.startsWith("2001:0000:") || normalized.startsWith("2001::")) return true;
7656
8056
  if (normalized.startsWith("100::") || normalized.startsWith("0100::")) return true;
7657
8057
  if (normalized.startsWith("64:ff9b:") || normalized.startsWith("0064:ff9b:")) return true;
7658
8058
  return false;
7659
8059
  }
7660
8060
  /**
8061
+ * Strip what belongs to the interface or the transport rather than to the address: a zone index
8062
+ * (`fe80::1%eth0`) and, for an IPv4 literal, a trailing port (`8.8.8.8:443`).
8063
+ *
8064
+ * MUST be shared by every exported entry point. `isPrivateIP` and `isPrivateOrInternalHostname` each carry
8065
+ * their own family gate, and the comment on those gates says why they are kept identical: three gates
8066
+ * disagreeing about what counts as IPv4 is how the bracketed-IPv6 hole happened. Stripping in one of them
8067
+ * only reproduced exactly that - `8.8.8.8:443` came back public from one export and private from the other,
8068
+ * because the second missed its IPv4 branch and was then caught by the misplaced-quad arm.
8069
+ *
8070
+ * Only a DOTTED port is stripped. An unbracketed `ipv6:port` is genuinely ambiguous - a bare IPv6 address is
8071
+ * mostly colons - so it stays refused, and the bracketed spelling is what `normalizeIpv6` handles.
8072
+ */
8073
+ function stripZoneAndIpv4Port(host) {
8074
+ const head = host.split("%")[0];
8075
+ const zoneless = head.includes(":") || /^(\d+\.){3}\d+$/.test(head) ? head : host;
8076
+ return zoneless.match(/^((?:\d+\.){3}\d+):\d+$/)?.[1] ?? zoneless;
8077
+ }
8078
+ /**
7661
8079
  * Check if an IP address (IPv4 or IPv6) is in a private/internal range.
7662
8080
  */
7663
8081
  function isPrivateIP(ip) {
7664
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return isPrivateIPv4(ip);
7665
- return isPrivateIPv6(ip);
8082
+ const address = stripZoneAndIpv4Port(ip);
8083
+ if (/^(\d+\.){3}\d+$/.test(address)) return isPrivateIPv4(address);
8084
+ return isPrivateIPv6(address);
7666
8085
  }
7667
8086
  /**
7668
8087
  * Check if a hostname is known to be private/internal.
7669
8088
  * This catches obvious cases before DNS resolution.
7670
8089
  */
7671
8090
  function isPrivateOrInternalHostname(hostname) {
7672
- const normalized = hostname.toLowerCase();
8091
+ const normalized = stripZoneAndIpv4Port(stripIpv6Brackets(hostname));
7673
8092
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "0.0.0.0" || normalized.endsWith(".localhost") || normalized.endsWith(".local")) return true;
7674
8093
  if (normalized === "169.254.169.254" || normalized === "instance-data" || normalized === "metadata.google.internal" || normalized === "metadata.internal") return true;
7675
8094
  if (normalized.endsWith(".cluster.local") || normalized.endsWith(".svc.cluster.local") || normalized.endsWith(".pod.cluster.local")) return true;
7676
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(normalized)) return isPrivateIPv4(normalized);
8095
+ if (/^(\d+\.){3}\d+$/.test(normalized)) return isPrivateIPv4(normalized);
7677
8096
  if (normalized.includes(":")) return isPrivateIPv6(normalized);
7678
8097
  return false;
7679
8098
  }
7680
8099
  /**
7681
8100
  * Validate a URL before fetching.
7682
8101
  * Blocks internal/private networks to prevent SSRF attacks.
7683
- * Resolves DNS and validates resolved IPs to prevent DNS rebinding attacks.
8102
+ *
8103
+ * Resolves DNS and rejects the URL if any resolved IP is private. This is a PRE-FLIGHT check, and on
8104
+ * its own it does NOT stop DNS rebinding: the address it validates is not the address the eventual
8105
+ * socket dials, because the HTTP client resolves the hostname again when it connects. A name that
8106
+ * answers with a public IP here and a private one microseconds later passes this check and still
8107
+ * reaches the internal destination. `ssrfSafeLookup` below is what closes that window; this function
8108
+ * exists to fail fast, to produce a specific user-facing error, and to check the things a connect-time
8109
+ * hook cannot see - the scheme, and the literal address the caller actually typed.
7684
8110
  *
7685
8111
  * @param url - The URL to validate
7686
8112
  * @returns Object with valid flag and optional error message
@@ -7692,15 +8118,16 @@ async function validateUrlForFetch(url) {
7692
8118
  valid: false,
7693
8119
  error: "URL must use HTTP or HTTPS protocol"
7694
8120
  };
7695
- if (isPrivateOrInternalHostname(parsed.hostname)) return {
8121
+ const hostname = stripIpv6Brackets(parsed.hostname);
8122
+ if (isPrivateOrInternalHostname(hostname)) return {
7696
8123
  valid: false,
7697
8124
  error: "URL points to a private or internal network"
7698
8125
  };
7699
- const isIPv4Address = /^(\d{1,3}\.){3}\d{1,3}$/.test(parsed.hostname);
7700
- const isIPv6Address = parsed.hostname.includes(":");
8126
+ const isIPv4Address = /^(\d+\.){3}\d+$/.test(hostname);
8127
+ const isIPv6Address = hostname.includes(":");
7701
8128
  if (!isIPv4Address && !isIPv6Address) try {
7702
- const ipv4Addresses = await dnsResolve4(parsed.hostname).catch(() => []);
7703
- const ipv6Addresses = await dnsResolve6(parsed.hostname).catch(() => []);
8129
+ const ipv4Addresses = await dnsResolve4(hostname).catch(() => []);
8130
+ const ipv6Addresses = await dnsResolve6(hostname).catch(() => []);
7704
8131
  const allAddresses = [...ipv4Addresses, ...ipv6Addresses];
7705
8132
  if (allAddresses.length === 0) return {
7706
8133
  valid: false,
@@ -7724,6 +8151,93 @@ async function validateUrlForFetch(url) {
7724
8151
  };
7725
8152
  }
7726
8153
  }
8154
+ /** Marks a refusal that came from the connect-time hook, so callers can tell it from a DNS failure. */
8155
+ const SSRF_BLOCKED_CODE = "ERR_SSRF_BLOCKED_ADDRESS";
8156
+ /**
8157
+ * DNS lookup that re-validates at CONNECT time. THIS is the check that stops DNS rebinding.
8158
+ *
8159
+ * The pre-flight in `validateUrlForFetch` resolves the hostname and then hands the NAME to the HTTP
8160
+ * client, which resolves it a second time before opening the socket. Those are two different
8161
+ * resolutions, so an attacker who controls the authoritative server can answer the first with a
8162
+ * public address and the second with `169.254.169.254` - a textbook TOCTOU, and the reason the old
8163
+ * "this prevents DNS rebinding attacks" comment on that function was false.
8164
+ *
8165
+ * Installing this as the agent's `lookup` removes the gap rather than narrowing it: Node passes the
8166
+ * address this function returns straight to `net.connect`, so the IP that gets validated is by
8167
+ * construction the IP the socket dials. There is no third resolution in between for a rebind to win.
8168
+ *
8169
+ * Refuses if ANY resolved address is private, matching `validateUrlForFetch` - a dual-stack host must
8170
+ * not become reachable just because Node happened to prefer the healthy family this time.
8171
+ *
8172
+ * The two match in POLICY but deliberately differ in RESOLVER: `validateUrlForFetch` uses
8173
+ * `dns.resolve4`/`resolve6` (c-ares, straight to DNS) while this uses `dns.lookup` (getaddrinfo, which
8174
+ * also reads `/etc/hosts` and the OS cache). They can therefore legitimately disagree - an
8175
+ * `/etc/hosts` entry passes the pre-flight and is refused here. That is fail-closed and the right way
8176
+ * round, but it means "the URL validated and then the connection was blocked" is reachable in normal
8177
+ * operation and is NOT evidence that the pin is broken.
8178
+ */
8179
+ const ssrfSafeLookup = (hostname, options, callback) => {
8180
+ const resolveOptions = {
8181
+ ...options,
8182
+ all: true
8183
+ };
8184
+ dns.lookup(hostname, resolveOptions, (err, addresses) => {
8185
+ if (err) {
8186
+ callback(err, "", 0);
8187
+ return;
8188
+ }
8189
+ if (!addresses || addresses.length === 0) {
8190
+ const empty = /* @__PURE__ */ new Error(`No addresses resolved for hostname ${hostname}`);
8191
+ empty.code = "ENOTFOUND";
8192
+ callback(empty, "", 0);
8193
+ return;
8194
+ }
8195
+ const privateHit = addresses.find((entry) => isPrivateIP(entry.address));
8196
+ if (privateHit) {
8197
+ const blocked = /* @__PURE__ */ new Error(`Blocked connection to private IP address (${privateHit.address}) for hostname ${hostname}`);
8198
+ blocked.code = SSRF_BLOCKED_CODE;
8199
+ callback(blocked, "", 0);
8200
+ return;
8201
+ }
8202
+ if (options.all) {
8203
+ callback(null, addresses);
8204
+ return;
8205
+ }
8206
+ callback(null, addresses[0].address, addresses[0].family);
8207
+ });
8208
+ };
8209
+ /**
8210
+ * Agents that pin every connection through `ssrfSafeLookup`.
8211
+ *
8212
+ * Module-level singletons so sockets and their validation are shared, and deliberately WITHOUT
8213
+ * `keepAlive`: a pooled socket outlives the lookup that approved it, and reusing one would skip the
8214
+ * connect-time check on every request after the first.
8215
+ *
8216
+ * Any caller fetching an attacker-influenced URL should pass BOTH - the scheme is not known until
8217
+ * after redirects, and an https URL that 302s to http would otherwise slip past a single agent.
8218
+ *
8219
+ * SCOPE - these protect callers that fetch through Node's http/https stack, which today means
8220
+ * `fetchAndParseURL` in `ingest.ts` and nothing else. That does NOT mean other fetchers are unpinned:
8221
+ * the webfetch LLM tool (`services/src/llm/tools/implementation/webfetch/plainFetch.ts`) reaches the
8222
+ * same guarantee by a different route, and a reader should not go looking for a gap there that is
8223
+ * already closed. It vets via `ssrfGuard.ts`, then for http rewrites the URL's hostname to the vetted
8224
+ * IP while preserving `Host`, and sets `redirect: 'error'` so a public origin cannot 302-pivot at all.
8225
+ * That is connect-by-IP under global `fetch` - so the technique IS available there, and an
8226
+ * undici `Agent` with a validating `connect` is not required to pin.
8227
+ *
8228
+ * The honest residual over there is narrower: https keeps the hostname and leans on TLS validation, so
8229
+ * what is left is an SYN-level probe oracle rather than a rebind to a private target. The reason to
8230
+ * use the agents here instead is that axios drives a manual redirect chain over an arbitrary number of
8231
+ * hops and schemes, where per-request agent selection is the tractable place to enforce this.
8232
+ */
8233
+ const ssrfSafeHttpAgent = new http.Agent({
8234
+ lookup: ssrfSafeLookup,
8235
+ keepAlive: false
8236
+ });
8237
+ const ssrfSafeHttpsAgent = new https.Agent({
8238
+ lookup: ssrfSafeLookup,
8239
+ keepAlive: false
8240
+ });
7727
8241
  const URL_REGEX = /https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?/gi;
7728
8242
  function detectURLs(string) {
7729
8243
  return string.match(URL_REGEX) || [];
@@ -7735,36 +8249,194 @@ function urlExists(stringWithPossibleUrl) {
7735
8249
  return detectURLs(stringWithPossibleUrl.replace(/\n/g, " ").replace(/,/g, " "));
7736
8250
  }
7737
8251
  const URL_FETCH_TIMEOUT_MS = 1e4;
8252
+ /**
8253
+ * Redirect hops followed before giving up. Deliberately far below axios's own default of 21: every
8254
+ * hop costs a DNS resolution plus a request, and no legitimate document needs more than a couple.
8255
+ */
8256
+ const MAX_REDIRECTS = 5;
8257
+ /**
8258
+ * Hard ceiling on a fetched body. A SAFETY NET against an unbounded response, not a policy limit -
8259
+ * `createFabFile` still enforces the `MaxFileSize` admin setting afterwards. Set generously (the
8260
+ * same 50MB as the Slack attachment ceiling) so it can never refuse something the app would accept;
8261
+ * without it axios defaults to `maxContentLength: -1`, i.e. buffer whatever the server sends, and
8262
+ * `@datalake add <link>` takes URLs from anyone who can type in a Slack channel.
8263
+ */
8264
+ const URL_MAX_RESPONSE_BYTES = 52428800;
8265
+ /**
8266
+ * PDF test against the URL's PATH only. The previous form (`url.split('.').pop().startsWith('pdf')`)
8267
+ * also matched a query string, so `?doc=report.pdf` on an HTML page was fetched as a PDF.
8268
+ */
8269
+ function isPdfUrl(url) {
8270
+ return new URL(url).pathname.toLowerCase().endsWith(".pdf");
8271
+ }
8272
+ /**
8273
+ * True when the body opens with the PDF signature.
8274
+ *
8275
+ * Closes the door `isPdfUrl` cannot reach: a download endpoint with no `.pdf` in its path, served as
8276
+ * `application/octet-stream`, produced neither a Content-Type signal nor an extension signal and was
8277
+ * decoded as text - the same `toString('utf8')` corruption the Content-Type fallback exists to
8278
+ * prevent, arriving through the one remaining door. `/download?id=123` and `Content-Disposition`
8279
+ * attachment links are exactly this shape.
8280
+ *
8281
+ * Checked at offset 0 only. The PDF spec tolerates leading bytes before the header and readers scan
8282
+ * ahead for it, but scanning here would mean sniffing arbitrary attacker-supplied content to
8283
+ * RE-CLASSIFY it, and a false positive sends a real text document into the PDF parser. The strict
8284
+ * check costs nothing on well-formed files, which is every file this has been observed to affect.
8285
+ *
8286
+ * Deliberately consulted ONLY on the generic-binary branch, never to override a server that stated a
8287
+ * type. A server declaring `text/html` while sending PDF bytes is a different (and unobserved) bug,
8288
+ * and overriding an explicit Content-Type is a wider behaviour change than this fix needs.
8289
+ */
8290
+ function hasPdfMagicBytes(body) {
8291
+ return body.subarray(0, 5).toString("latin1") === "%PDF-";
8292
+ }
8293
+ /**
8294
+ * Strip embedded credentials before a URL is written to a log.
8295
+ *
8296
+ * `https://user:pass@host/doc` is a legitimate paste, and this function is reached from the Slack
8297
+ * `@datalake add` path and the LLM URL-fetch path - both of which take URLs from whoever can type in
8298
+ * a channel or a chat. The FETCH still uses the original URL; only what is recorded is redacted, and a
8299
+ * log line outlives the message that produced it.
8300
+ *
8301
+ * MUST STAY IN SYNC with `sanitizeUrlForRecord` in `apps/client/server/slack/dataLakeLinkIngest.ts`,
8302
+ * which does the same job for the PERSISTED provenance record. Deliberately duplicated rather than
8303
+ * shared: exporting this would change `fab-pipeline`'s public surface, which its own `index.test.ts`
8304
+ * pins as an explicit list of names.
8305
+ */
8306
+ function redactUrlCredentials(raw) {
8307
+ try {
8308
+ const parsed = new URL(raw);
8309
+ if (!parsed.username && !parsed.password) return raw;
8310
+ parsed.username = "";
8311
+ parsed.password = "";
8312
+ return parsed.toString();
8313
+ } catch {
8314
+ return "[unparseable url]";
8315
+ }
8316
+ }
8317
+ /** Last path segment, used only as a display-name fallback when a page has no `<title>`. */
8318
+ function lastPathSegment(url) {
8319
+ try {
8320
+ return new URL(url).pathname.split("/").filter(Boolean).pop() ?? url;
8321
+ } catch {
8322
+ return url.split("/")?.pop() ?? url;
8323
+ }
8324
+ }
8325
+ /**
8326
+ * Fetch one URL without following redirects, so the caller can SSRF-validate each hop itself.
8327
+ *
8328
+ * SECURITY: this is why `maxRedirects: 0` is set rather than left at axios's default. Validating
8329
+ * only the URL the user supplied is not enough - axios would follow the redirect chain internally,
8330
+ * so any public host could answer `302 Location: http://169.254.169.254/latest/meta-data/` and the
8331
+ * guard would never see the address actually fetched.
8332
+ *
8333
+ * SECURITY: the agents are the OTHER half, and the two guard different attacks. Per-hop
8334
+ * `validateUrlForFetch` judges each address the chain names; the agents' `ssrfSafeLookup` judges the
8335
+ * IP each socket actually dials. Without the agents a hostname that passes validation and then
8336
+ * re-resolves to a private address on connect - DNS rebinding - reaches the internal destination with
8337
+ * every URL-level check having passed. Both are needed: the pre-flight sees the scheme and the typed
8338
+ * literal, the lookup sees the truth at connect time.
8339
+ *
8340
+ * `timeoutMs` is the budget REMAINING for the whole operation, not a fresh per-hop allowance - see
8341
+ * the deadline in `fetchAndParseURL`.
8342
+ */
8343
+ async function fetchWithoutRedirects(url, timeoutMs) {
8344
+ return axios.get(url, {
8345
+ httpAgent: ssrfSafeHttpAgent,
8346
+ httpsAgent: ssrfSafeHttpsAgent,
8347
+ proxy: false,
8348
+ responseType: "arraybuffer",
8349
+ timeout: timeoutMs,
8350
+ maxRedirects: 0,
8351
+ maxContentLength: URL_MAX_RESPONSE_BYTES,
8352
+ maxBodyLength: URL_MAX_RESPONSE_BYTES,
8353
+ validateStatus: (status) => status >= 200 && status < 300 || status >= 300 && status < 400
8354
+ });
8355
+ }
8356
+ const BLOCK_LEVEL_SELECTOR = `*:not(${"a, span, em, strong, b, i, u, code, kbd, samp, var, sub, sup, small, abbr, cite, q, time, mark, s, del, ins, bdi, bdo, wbr, ruby, rt, rp".split(", ").join("):not(")}):not(td):not(th)`;
8357
+ /**
8358
+ * Extract readable text from the WHOLE document, not just `<p>` elements. The single collector
8359
+ * this replaced was `<p>`-only and fell back to the raw HTML when it found none: on a page whose
8360
+ * content isn't inside `<p>` (an RFC page using `<pre>`) that meant the fallback fired and stored
8361
+ * markup verbatim; on a page with real substance in headings, list items, table cells or code
8362
+ * blocks alongside its `<p>`s, that content was silently dropped.
8363
+ *
8364
+ * `head` (title/meta/script/style all live there, and the caller already reads `<title>`
8365
+ * separately) plus any stray `script`/`style`/`noscript` outside it are removed before extraction,
8366
+ * so none of that reaches what gets embedded. `<pre>` content is pulled out and stashed BEFORE the
8367
+ * rest of the document is collapsed, and spliced back in verbatim afterward - it needs to skip the
8368
+ * whitespace-collapse below (a code block's leading-space indentation is meaningful, unlike prose
8369
+ * whitespace) but still needs to land in the right place relative to everything else. Table cells
8370
+ * get a trailing space (still the same row, but no longer jammed into the next cell's word); every
8371
+ * other block-level element gets a trailing newline; runs of whitespace and blank lines are then
8372
+ * collapsed. Returns `''` when nothing extractable was found, so the caller stores nothing rather
8373
+ * than falling back to raw HTML.
8374
+ */
8375
+ function extractReadableText($) {
8376
+ $("head, script, style, noscript").remove();
8377
+ $("br").replaceWith("\n");
8378
+ const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
8379
+ const markerFor = (index) => `\uE000PRE${nonce}_${index}\uE000`;
8380
+ const markerPattern = new RegExp(`\\uE000PRE${nonce}_(\\d+)\\uE000`, "g");
8381
+ const preBlocks = [];
8382
+ $("pre").each((_index, element) => {
8383
+ const text = $(element).text();
8384
+ if (text) {
8385
+ preBlocks.push(text);
8386
+ $(element).replaceWith(`${markerFor(preBlocks.length - 1)}\n`);
8387
+ } else $(element).remove();
8388
+ });
8389
+ $("td, th").each((_index, cell) => {
8390
+ $(cell).after(" ");
8391
+ });
8392
+ $(BLOCK_LEVEL_SELECTOR).each((_index, element) => {
8393
+ $(element).after("\n");
8394
+ });
8395
+ return $.root().text().split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter(Boolean).join("\n").replace(markerPattern, (match, indexStr) => {
8396
+ const index = Number(indexStr);
8397
+ return index >= 0 && index < preBlocks.length ? preBlocks[index] : match;
8398
+ });
8399
+ }
7738
8400
  async function fetchAndParseURL(url, { logger }) {
7739
8401
  logger.updateMetadata({ failedUrl: null });
7740
8402
  try {
7741
- const ssrfValidation = await validateUrlForFetch(url);
7742
- if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
7743
- let urlMimeType = "text/plain";
7744
- if (url.split(".")?.pop()?.startsWith("pdf")) urlMimeType = "application/pdf";
7745
- const response = await axios.get(url, {
7746
- responseType: ["application/pdf"].includes(urlMimeType) ? "arraybuffer" : "text",
7747
- timeout: URL_FETCH_TIMEOUT_MS
7748
- });
7749
- const cheerio = await import("cheerio");
7750
- const htmlContent = response.data;
7751
- const $ = cheerio.load(htmlContent);
7752
- const title = $("title").text() || url.split("/")?.pop();
7753
- let urlContent = null;
7754
- switch (urlMimeType) {
7755
- case "application/pdf":
7756
- urlContent = Buffer.from(response.data);
7757
- break;
7758
- default: {
7759
- let textContent = "";
7760
- $("body").find("p").each((index, element) => {
7761
- textContent += $(element).text() + "\n";
7762
- });
7763
- urlContent = textContent || htmlContent;
7764
- break;
7765
- }
7766
- }
7767
- logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${url}`);
8403
+ let currentUrl = url;
8404
+ let response = null;
8405
+ const deadline = Date.now() + URL_FETCH_TIMEOUT_MS;
8406
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
8407
+ const ssrfValidation = await validateUrlForFetch(currentUrl);
8408
+ if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
8409
+ const remainingMs = deadline - Date.now();
8410
+ if (remainingMs <= 0) throw new Error("Timed out while following redirects for URL");
8411
+ response = await fetchWithoutRedirects(currentUrl, remainingMs);
8412
+ if (!(response.status >= 300 && response.status < 400)) break;
8413
+ const location = response.headers?.location;
8414
+ if (typeof location !== "string" || location.length === 0) break;
8415
+ if (hop === MAX_REDIRECTS) throw new Error(`Too many redirects (more than ${MAX_REDIRECTS}) while fetching URL`);
8416
+ currentUrl = new URL(location, currentUrl).toString();
8417
+ }
8418
+ if (!response) throw new Error("URL fetch produced no response");
8419
+ const body = Buffer.isBuffer(response.data) ? response.data : Buffer.from(response.data);
8420
+ const contentType = String(response.headers?.["content-type"] ?? "").toLowerCase();
8421
+ const isGenericBinary = !contentType || contentType.includes("application/octet-stream") || contentType.includes("binary/octet-stream");
8422
+ const urlMimeType = contentType.includes("application/pdf") || isGenericBinary && (isPdfUrl(currentUrl) || hasPdfMagicBytes(body)) ? "application/pdf" : "text/plain";
8423
+ let title;
8424
+ let urlContent;
8425
+ if (urlMimeType === "application/pdf") {
8426
+ urlContent = body;
8427
+ title = lastPathSegment(currentUrl);
8428
+ } else {
8429
+ const cheerio = await import("cheerio");
8430
+ const htmlContent = body.toString("utf8");
8431
+ const $ = cheerio.load(htmlContent);
8432
+ title = $("title").text() || lastPathSegment(currentUrl);
8433
+ urlContent = extractReadableText($);
8434
+ }
8435
+ const original = redactUrlCredentials(url);
8436
+ const final = redactUrlCredentials(currentUrl);
8437
+ const fetched = original === final ? original : `${original} -> ${final}`;
8438
+ if (urlContent === "") logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}, but no extractable text was found`);
8439
+ else logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
7768
8440
  return {
7769
8441
  title,
7770
8442
  textContent: urlContent,
@@ -7772,7 +8444,7 @@ async function fetchAndParseURL(url, { logger }) {
7772
8444
  ext: mime.extension(urlMimeType) || null
7773
8445
  };
7774
8446
  } catch (error) {
7775
- logger.updateMetadata({ failedUrl: url });
8447
+ logger.updateMetadata({ failedUrl: redactUrlCredentials(url) });
7776
8448
  logger.debug("Error fetching or parsing URL:", error);
7777
8449
  throw error;
7778
8450
  }
@@ -10730,7 +11402,6 @@ var ReActAgent = class extends EventEmitter {
10730
11402
  this.context = {
10731
11403
  ...context,
10732
11404
  maxIterations: context.maxIterations ?? 50,
10733
- maxTokens: context.maxTokens ?? 4096,
10734
11405
  temperature: context.temperature ?? .7
10735
11406
  };
10736
11407
  this.repeatedCallGuard = new RepeatedCallGuard(context.repeatedCallGuard);
@@ -10775,7 +11446,7 @@ var ReActAgent = class extends EventEmitter {
10775
11446
  */
10776
11447
  async completeText(prompt, options = {}) {
10777
11448
  const model = options.model ?? this.context.model;
10778
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11449
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10779
11450
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10780
11451
  let text = "";
10781
11452
  await this.context.llm.complete(model, [{
@@ -10816,7 +11487,7 @@ var ReActAgent = class extends EventEmitter {
10816
11487
  this.isReadOnlyToolFn = options.isReadOnlyTool ?? defaultIsReadOnlyTool;
10817
11488
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
10818
11489
  const temperature = options.temperature ?? this.context.temperature ?? .7;
10819
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11490
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
10820
11491
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
10821
11492
  const maxHistoryIterations = options.maxHistoryIterations ?? 4;
10822
11493
  let iterations = 0;
@@ -10857,7 +11528,8 @@ var ReActAgent = class extends EventEmitter {
10857
11528
  totalCacheWriteTokens: this.totalCacheWriteTokens > 0 ? this.totalCacheWriteTokens : void 0,
10858
11529
  iterations,
10859
11530
  toolCalls: this.toolCallCount,
10860
- reachedMaxIterations: false
11531
+ reachedMaxIterations: false,
11532
+ finishReason: this.lastStopReason
10861
11533
  }
10862
11534
  };
10863
11535
  this.emit("complete", result);
@@ -11091,6 +11763,7 @@ var ReActAgent = class extends EventEmitter {
11091
11763
  toolCalls: this.toolCallCount,
11092
11764
  reachedMaxIterations,
11093
11765
  reachedMaxTotalTokens: reachedMaxTotalTokens || void 0,
11766
+ finishReason: this.lastStopReason,
11094
11767
  averageConfidence: avgConfidence,
11095
11768
  minConfidence,
11096
11769
  confidenceLog: this.confidenceLog.length > 0 ? this.confidenceLog : void 0
@@ -11322,7 +11995,7 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
11322
11995
  async runIteration(query, options = {}) {
11323
11996
  const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
11324
11997
  const temperature = options.temperature ?? this.context.temperature ?? .7;
11325
- const maxTokens = options.maxTokens ?? this.context.maxTokens ?? 4096;
11998
+ const maxTokens = options.maxTokens ?? this.context.maxTokens;
11326
11999
  const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
11327
12000
  if (!this.iterationInitialized) {
11328
12001
  if (!query) throw new Error("query is required on the first call to runIteration(). Pass the user query, or call fromCheckpoint() first to resume.");
@@ -12506,10 +13179,117 @@ const vm = require('node:vm');
12506
13179
  const STDOUT_HEAD_BYTES = ${5e3};
12507
13180
  const STDOUT_TAIL_BYTES = ${2e3};
12508
13181
  const HARD_PER_LINE_BYTES = ${5e4};
13182
+ const MIRROR_TAIL_FLUSH_MS = ${100};
12509
13183
 
12510
13184
  let stdoutChunks = [];
12511
- let stdoutBytes = 0;
12512
13185
  let truncated = false;
13186
+ // --- Mirror state --------------------------------------------------------
13187
+ // Mirrors this run's stdout to the main thread as it is produced, in the same
13188
+ // head + marker + tail shape collectStdout() produces, so a retired run and a
13189
+ // completed one report the same thing by the same rule.
13190
+ //
13191
+ // The two halves are cost-bounded differently. The head is mirrored line by
13192
+ // line, so a chatty loop stops paying per line once the head is full. Past
13193
+ // that the tail is kept locally in a rolling window and posted on a timer, so
13194
+ // the message rate stops tracking the line rate entirely.
13195
+ let currentRunId = null;
13196
+ let mirroredHeadBytes = 0;
13197
+ let headMirrorFull = false;
13198
+ let tailChunks = [];
13199
+ let tailBytes = 0;
13200
+ let elidedBytes = 0;
13201
+ let tailFlushTimer = null;
13202
+
13203
+ /**
13204
+ * What the rolling tail may hold: whatever the head did not use of the same
13205
+ * HEAD + TAIL total collectStdout() reports within. A fixed TAIL budget made
13206
+ * the two disagree whenever the head came up short - a single 6KB first line
13207
+ * does not fit the head, so the mirror would have kept 2KB of a run that
13208
+ * collectStdout() reports whole, and called it truncated. mirroredHeadBytes
13209
+ * is frozen once the head is full, so this is stable for the rest of the run.
13210
+ */
13211
+ function tailBudget() {
13212
+ return STDOUT_HEAD_BYTES + STDOUT_TAIL_BYTES - mirroredHeadBytes;
13213
+ }
13214
+
13215
+ function postToMain(msg) {
13216
+ try { parentPort.postMessage(msg); } catch { /* worker being torn down; nothing to preserve */ }
13217
+ }
13218
+ function cancelTailFlush() {
13219
+ if (tailFlushTimer === null) return;
13220
+ clearTimeout(tailFlushTimer);
13221
+ tailFlushTimer = null;
13222
+ }
13223
+ function flushTail() {
13224
+ if (currentRunId === null || !headMirrorFull) return;
13225
+ const joined = tailChunks.join('\n');
13226
+ // The rolling window is trimmed line by line, so it can only exceed the
13227
+ // budget by holding ONE line longer than the whole budget. Slice to the
13228
+ // same last-N-chars rule collectStdout() uses, which both matches that
13229
+ // path and keeps the flush payload bounded - a guest printing 50KB lines
13230
+ // would otherwise re-send 50KB on every tick.
13231
+ const overflow = Math.max(0, joined.length - tailBudget());
13232
+ postToMain({
13233
+ type: 'stdoutTail',
13234
+ id: currentRunId,
13235
+ tail: overflow > 0 ? joined.slice(overflow) : joined,
13236
+ // Counted from what was actually DROPPED - lines the rolling window
13237
+ // evicted, plus whatever this payload's own slice cuts - rather than
13238
+ // derived from the byte totals. The derived form read zero on the first
13239
+ // flush by construction (every line was still in the head, so the
13240
+ // subtraction cancelled), which made "truncated" unreportable on exactly
13241
+ // the run the mirror exists for.
13242
+ elidedBytes: elidedBytes + overflow,
13243
+ });
13244
+ }
13245
+ function scheduleTailFlush() {
13246
+ if (tailFlushTimer !== null) return;
13247
+ tailFlushTimer = setTimeout(() => {
13248
+ tailFlushTimer = null;
13249
+ flushTail();
13250
+ }, MIRROR_TAIL_FLUSH_MS);
13251
+ }
13252
+ function mirrorLine(capped) {
13253
+ if (currentRunId === null) return;
13254
+ if (!headMirrorFull) {
13255
+ // Does THIS line fit, rather than "is the running total already over".
13256
+ // Both of the orderings tried before this were wrong in one direction
13257
+ // each: gating on the running total let one line of up to
13258
+ // HARD_PER_LINE_BYTES past a 5KB budget (mirrored head ~55KB, disagreeing
13259
+ // with collectStdout's head and with the "~7K chars" codeExecuteTool
13260
+ // advertises to the model), while adding first and checking after moved
13261
+ // the boundary but kept the crossing line in the head - so the tail was
13262
+ // still empty at the immediate flush below and a run killed right there
13263
+ // dropped the last line before the hang and reported itself complete.
13264
+ //
13265
+ // A fit check does both: the head stops at STDOUT_HEAD_BYTES exactly, and
13266
+ // the line that did not fit STARTS the tail, so the flush that fires on
13267
+ // this same call carries it.
13268
+ if (mirroredHeadBytes + capped.length + 1 <= STDOUT_HEAD_BYTES) {
13269
+ mirroredHeadBytes += capped.length + 1;
13270
+ postToMain({ type: 'stdout', id: currentRunId, chunk: capped });
13271
+ return;
13272
+ }
13273
+ headMirrorFull = true;
13274
+ tailChunks.push(capped);
13275
+ tailBytes += capped.length + 1;
13276
+ // Post once immediately: a run killed before the first timed flush would
13277
+ // otherwise report a short mirror as if it were complete.
13278
+ flushTail();
13279
+ return;
13280
+ }
13281
+ tailChunks.push(capped);
13282
+ tailBytes += capped.length + 1;
13283
+ // Never evict the only line held: a line larger than the whole budget is
13284
+ // still the last thing the run printed, which is what the mirror is for.
13285
+ const budget = tailBudget();
13286
+ while (tailBytes > budget && tailChunks.length > 1) {
13287
+ const dropped = tailChunks.shift();
13288
+ tailBytes -= dropped.length + 1;
13289
+ elidedBytes += dropped.length + 1;
13290
+ }
13291
+ scheduleTailFlush();
13292
+ }
12513
13293
  function captureLine(args) {
12514
13294
  const line = args.map(a => {
12515
13295
  if (typeof a === 'string') return a;
@@ -12521,7 +13301,7 @@ function captureLine(args) {
12521
13301
  ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
12522
13302
  : line;
12523
13303
  stdoutChunks.push(capped);
12524
- stdoutBytes += capped.length + 1;
13304
+ mirrorLine(capped);
12525
13305
  }
12526
13306
  function jsonReplacer(_k, v) {
12527
13307
  if (v instanceof Error) return { name: v.name, message: v.message };
@@ -12622,7 +13402,11 @@ parentPort.on('message', async (msg) => {
12622
13402
  }
12623
13403
  if (msg.type === 'runCode') {
12624
13404
  const t0 = Date.now();
12625
- stdoutChunks = []; stdoutBytes = 0; truncated = false;
13405
+ stdoutChunks = []; truncated = false;
13406
+ cancelTailFlush();
13407
+ currentRunId = msg.id;
13408
+ mirroredHeadBytes = 0; headMirrorFull = false;
13409
+ tailChunks = []; tailBytes = 0; elidedBytes = 0;
12626
13410
  let error = null;
12627
13411
  const wrapped = '(async () => {\n' + msg.code + '\n})()';
12628
13412
  try {
@@ -12634,6 +13418,11 @@ parentPort.on('message', async (msg) => {
12634
13418
  } catch (e) {
12635
13419
  error = serializeError(e);
12636
13420
  }
13421
+ // Stop mirroring before the authoritative result goes out, so a late
13422
+ // console.log from an abandoned continuation cannot attach to this run,
13423
+ // and a pending tail flush cannot land after it.
13424
+ currentRunId = null;
13425
+ cancelTailFlush();
12637
13426
  parentPort.postMessage({
12638
13427
  type: 'runResult',
12639
13428
  id: msg.id,
@@ -12647,6 +13436,17 @@ parentPort.on('message', async (msg) => {
12647
13436
  });
12648
13437
  `;
12649
13438
  String.raw`
13439
+ // Wrapped in an IIFE deliberately. A script's top-level const/let bind into the
13440
+ // context's SHARED global lexical scope (and its function declarations become
13441
+ // globalThis properties), so without this wrapper every bootstrap-local name is
13442
+ // directly referenceable by LLM-authored code run later in the same context:
13443
+ // __RealFunction('...')() walks straight around the codegen block below, and
13444
+ // __cap.applySync(...) / __cap.release() forges or permanently kills stdout
13445
+ // capture. Function scope keeps them unreachable. Note the leak is invisible to
13446
+ // listGlobals(), which reads Object.getOwnPropertyNames(globalThis) and never
13447
+ // saw the lexical bindings - so RESERVED_GLOBAL_NAMES cannot backstop it either.
13448
+ // Anything guest code IS meant to see is assigned onto globalThis explicitly.
13449
+ (function () {
12650
13450
  const __cap = _captureLine;
12651
13451
  const __callTool = _callTool;
12652
13452
  delete globalThis._captureLine;
@@ -12654,28 +13454,68 @@ delete globalThis._callTool;
12654
13454
 
12655
13455
  const HARD_PER_LINE_BYTES = ${5e4};
12656
13456
 
13457
+ // Every intrinsic the formatter below reaches for is captured HERE, while the
13458
+ // context is still pristine. Resolving \`args.map\` / \`.join\` / \`line.slice\`
13459
+ // at CALL time walks a prototype chain the guest owns, so one
13460
+ // \`Array.prototype.join = () => 'X'\` - deliberate, or an innocent polyfill -
13461
+ // forges every stdout line for the rest of the session, and the run still
13462
+ // reports error=null / truncated=false. That is the same integrity failure the
13463
+ // frozen \`console\` below exists to prevent, one level down: freezing the
13464
+ // binding is worthless if the formatter behind it is guest-reachable.
13465
+ const __stringify = JSON.stringify;
13466
+ const __String = String;
13467
+ const __apply = Reflect.apply;
13468
+ const __strSlice = String.prototype.slice;
13469
+
12657
13470
  function __jsonReplacer(_k, v) {
12658
13471
  if (v instanceof Error) return { name: v.name, message: v.message };
12659
13472
  if (typeof v === 'bigint') return v.toString() + 'n';
12660
13473
  return v;
12661
13474
  }
13475
+ // Indexed loop and \`+=\` rather than map/join: string concatenation is an
13476
+ // operator, not a lookup, so there is nothing here for the guest to replace.
13477
+ // What a guest CAN still steer is how its own values render - a \`toJSON\` or
13478
+ // \`toString\` on the object it passed - which is content it already owns, not
13479
+ // the channel.
12662
13480
  function __formatLine(args) {
12663
- const line = args.map(a => {
12664
- if (typeof a === 'string') return a;
12665
- if (a === undefined) return 'undefined';
12666
- if (a === null) return 'null';
12667
- try { return JSON.stringify(a, __jsonReplacer, 2); } catch { return String(a); }
12668
- }).join(' ');
13481
+ let line = '';
13482
+ for (let i = 0; i < args.length; i++) {
13483
+ if (i > 0) line += ' ';
13484
+ const a = args[i];
13485
+ if (typeof a === 'string') { line += a; continue; }
13486
+ if (a === undefined) { line += 'undefined'; continue; }
13487
+ if (a === null) { line += 'null'; continue; }
13488
+ try { line += __stringify(a, __jsonReplacer, 2); } catch { line += __String(a); }
13489
+ }
12669
13490
  return line.length > HARD_PER_LINE_BYTES
12670
- ? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
13491
+ ? __apply(__strSlice, line, [0, HARD_PER_LINE_BYTES]) + ' [...line truncated]'
12671
13492
  : line;
12672
13493
  }
12673
- globalThis.console = {
13494
+ // stdout is the channel the HOST reports back as the run's observation, so its
13495
+ // integrity is ours, not the guest's. A plain assignment left \`console\`
13496
+ // writable and configurable: guest code could set globalThis.console = {log(){}}
13497
+ // (or just reassign console.log) and every later run in the session would come
13498
+ // back with stdout="" or forged lines, error=null, and a clean listGlobals().
13499
+ // Frozen object + non-writable, non-configurable property: the guest's
13500
+ // assignment is a silent no-op in sloppy mode and a TypeError under 'use
13501
+ // strict', and either way capture keeps working.
13502
+ //
13503
+ // The BINDING is what this protects, and the binding is only half of it: a
13504
+ // frozen console whose formatter resolved its intrinsics at call time would
13505
+ // still hand the guest every line. That half is closed above, where
13506
+ // __formatLine captures what it needs.
13507
+ const __console = Object.freeze({
12674
13508
  log: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
12675
13509
  warn: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
12676
13510
  error: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
12677
13511
  info: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
12678
- };
13512
+ });
13513
+ Object.defineProperty(globalThis, 'console', {
13514
+ value: __console,
13515
+ writable: false,
13516
+ configurable: false,
13517
+ enumerable: true,
13518
+ });
12679
13519
 
12680
13520
  // A bare isolate has no structuredClone (it's a host/web API, not a V8
12681
13521
  // intrinsic). The in-process + worker backends expose the *host's* real
@@ -12763,11 +13603,33 @@ for (const __Ctor of [__RealFunction, __AsyncFunction, __GeneratorFunction, __As
12763
13603
  globalThis.eval = __blockCodegen;
12764
13604
  globalThis.Function = __blockCodegen;
12765
13605
 
13606
+ // WebAssembly is removed, not stubbed. Its compile/instantiate promises never
13607
+ // settle inside an isolated-vm isolate (there is no host task runner to drive
13608
+ // them), so \`await WebAssembly.instantiate(...)\` is a one-line way for guest
13609
+ // code to park a run until the host deadline fires - and that deadline kills
13610
+ // the isolate, costing the whole session its sandbox. Deleting it turns that
13611
+ // into an immediate ReferenceError. It is also codegen-from-bytes, so it
13612
+ // belongs on the same side of the line as eval / Function anyway.
13613
+ delete globalThis.WebAssembly;
13614
+
12766
13615
  // Tool-stub registry. Each registered tool becomes a top-level async
12767
13616
  // function that round-trips through the host dispatcher and re-throws on
12768
13617
  // the { ok:false } envelope.
13618
+ //
13619
+ // Assigned to globalThis only so the constructor can lift a Reference to it;
13620
+ // the constructor deletes the global immediately afterwards and calls it
13621
+ // through that Reference forever after. It must NOT stay guest-reachable: a
13622
+ // guest could call __registerTools(['console']) to overwrite the frozen
13623
+ // console binding with a tool stub, or \`delete\` it and make the host's next
13624
+ // setTools() throw.
13625
+ //
13626
+ // Indexed loop, not for..of, deliberately: the host calls this with a copied
13627
+ // array whose iterator comes from the GUEST's Array.prototype, so an
13628
+ // overridden Symbol.iterator would let guest code hang or hijack a host-side
13629
+ // setTools() call. Indexing touches only the copy's own properties.
12769
13630
  globalThis.__registerTools = function (names) {
12770
- for (const name of names) {
13631
+ for (let i = 0; i < names.length; i++) {
13632
+ const name = names[i];
12771
13633
  globalThis[name] = async (...args) => {
12772
13634
  const envJson = await __callTool.apply(
12773
13635
  undefined,
@@ -12780,6 +13642,7 @@ globalThis.__registerTools = function (names) {
12780
13642
  };
12781
13643
  }
12782
13644
  };
13645
+ })();
12783
13646
  `;
12784
13647
  z$1.object({
12785
13648
  reflection: z$1.string().min(1),
@@ -15072,6 +15935,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15072
15935
  AnthropicBackend: () => AnthropicBackend,
15073
15936
  AnthropicBatchService: () => AnthropicBatchService,
15074
15937
  AnthropicBedrockBackend: () => AnthropicBedrockBackend,
15938
+ BEDROCK_REQUEST_HANDLER: () => BEDROCK_REQUEST_HANDLER,
15075
15939
  BFLBackend: () => BFLBackend,
15076
15940
  BaseBedrockBackend: () => BaseBedrockBackend,
15077
15941
  ChoiceEndReason: () => ChoiceEndReason,
@@ -15107,6 +15971,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15107
15971
  buildApiKeyTable: () => buildApiKeyTable,
15108
15972
  buildSupersededIndex: () => buildSupersededIndex,
15109
15973
  buildThinkingParams: () => buildThinkingParams,
15974
+ cachedTokensFromUsage: () => cachedTokensFromUsage,
15110
15975
  catalogLifecycles: () => catalogLifecycles,
15111
15976
  catalogSuccessors: () => catalogSuccessors,
15112
15977
  checkStaleModelReferences: () => checkStaleModelReferences,
@@ -15128,6 +15993,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15128
15993
  logExpiringModels: () => logExpiringModels,
15129
15994
  mergeCatalog: () => mergeCatalog,
15130
15995
  mergeCatalogWithDrops: () => mergeCatalogWithDrops,
15996
+ normalizeToolUseInputs: () => normalizeToolUseInputs,
15131
15997
  reasonsWithinOutputBudget: () => reasonsWithinOutputBudget,
15132
15998
  recordDeprecatedModelRequest: () => recordDeprecatedModelRequest,
15133
15999
  replaceLastToolResultObservationCanonical: () => replaceLastToolResultObservationCanonical,
@@ -15142,6 +16008,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
15142
16008
  resolveSuccessorChain: () => resolveSuccessorChain,
15143
16009
  setModelCatalogProvider: () => setModelCatalogProvider,
15144
16010
  setModelPriceRowsProvider: () => setModelPriceRowsProvider,
16011
+ splitCacheInclusiveInput: () => splitCacheInclusiveInput,
15145
16012
  stripAllToolBlocks: () => stripAllToolBlocks,
15146
16013
  stripToolDependentMessages: () => stripToolDependentMessages,
15147
16014
  toKimiEffort: () => toKimiEffort,
@@ -15227,6 +16094,44 @@ async function runWithConcurrency(tasks, limit) {
15227
16094
  return results;
15228
16095
  }
15229
16096
  /**
16097
+ * Attaches a tool call's outcome onto its `toolsUsed` entry so it survives into
16098
+ * `promptMeta.functionCalls.returnValue`/`.success` (see ChatCompletionProcess.ts's mapper
16099
+ * and utils.ts's `replayableToolCalls`, which gates a whole replay path on at least one
16100
+ * recorded `returnValue`). Every backend pushes a `toolsUsed` entry before executing the
16101
+ * tool and only learns the real outcome a few lines later - this is the merge-back.
16102
+ */
16103
+ /**
16104
+ * Cap applied to a persisted `returnValue` before it reaches Mongo (chars, not bytes). Also the
16105
+ * cap on what a later turn replays back to the model for this call (utils.ts's Priority 2
16106
+ * reconstruction reads the same persisted, already-truncated value) - this is not a
16107
+ * persistence-only limit, it is what a continued conversation sees of an older tool result too.
16108
+ */
16109
+ const MAX_RECORDED_TOOL_RESULT_CHARS = 8e3;
16110
+ const TOOL_RESULT_TRUNCATION_NOTICE = "\n[tool result truncated]";
16111
+ function truncateToolResult(observation) {
16112
+ if (observation.length <= 8e3) return observation;
16113
+ return observation.slice(0, MAX_RECORDED_TOOL_RESULT_CHARS) + TOOL_RESULT_TRUNCATION_NOTICE;
16114
+ }
16115
+ /**
16116
+ * Finds the NOT-YET-STAMPED `toolsUsed` entry for this call (id-first, falling back to the
16117
+ * first unstamped entry with the same name when a provider omitted an id) and attaches the
16118
+ * truncated result. The "not yet stamped" filter (`success === undefined`) is what keeps this
16119
+ * safe across recursive tool-call turns: `toolsUsed` accumulates across rounds, so a later
16120
+ * turn's call to the same tool must not overwrite an earlier turn's already-recorded entry.
16121
+ * Never throws - a call with no matching entry (e.g. one filtered out before execution) is a
16122
+ * silent no-op, matching the tolerance the existing normalize-by-id sites already have.
16123
+ */
16124
+ function recordToolResult(toolsUsed, call, observation, success) {
16125
+ const wantId = call.id || void 0;
16126
+ const entry = toolsUsed.find((t) => t.success === void 0 && t.name === call.name && (wantId === void 0 || t.id === wantId));
16127
+ if (!entry) {
16128
+ Logger.globalInstance.debug(`[recordToolResult] no unstamped toolsUsed entry matched name=${call.name} id=${call.id ?? "(none)"} - result not recorded`);
16129
+ return;
16130
+ }
16131
+ entry.returnValue = truncateToolResult(String(observation));
16132
+ entry.success = success;
16133
+ }
16134
+ /**
15230
16135
  * Helper function to handle tool result streaming for artifact-generating tools
15231
16136
  * This ensures tools like recharts that generate artifacts are streamed immediately
15232
16137
  * rather than waiting for recursive completion calls.
@@ -15463,27 +16368,112 @@ const stripAllToolBlocks = (messages, logger) => {
15463
16368
  return result;
15464
16369
  };
15465
16370
  /**
16371
+ * Restores `input: {}` on any `tool_use` block that reached us without one.
16372
+ *
16373
+ * `MessageContentToolUse.input` is non-optional in the type system, so nothing upstream checks it -
16374
+ * but a message that round-trips through a persistence or serialization layer can lose it. The
16375
+ * known offender is Mongoose's default `minimize`, which deletes empty objects on
16376
+ * `toObject()`/`toJSON()`: a zero-argument tool call (`current_datetime`, `mission_status`, ...)
16377
+ * stores `input: {}` and reads back with the key gone. Anthropic then rejects the whole request
16378
+ * with "messages.N.content.M.tool_use.input: Field required", killing a resumed agent run or a
16379
+ * chat turn that replays history.
16380
+ *
16381
+ * The schema that caused it is fixed at the source (`minimize: false` on AgentExecutionModel), so
16382
+ * this is the last line of defense for every other store that replays blocks verbatim -
16383
+ * `QuestModel.structuredReplies[].content` is the same Mixed-under-default-minimize shape and is
16384
+ * deliberately covered here rather than by widening that hot collection's schema. The cost of a
16385
+ * miss is a hard 400, and `{}` is the only value a zero-argument call could have had.
16386
+ *
16387
+ * Returns the input array unchanged (same reference) when nothing needed repair.
16388
+ */
16389
+ const normalizeToolUseInputs = (messages, logger) => {
16390
+ let repaired = 0;
16391
+ const result = messages.map((message) => {
16392
+ if (!Array.isArray(message.content)) return message;
16393
+ let messageChanged = false;
16394
+ const content = message.content.map((block) => {
16395
+ if (block.type !== "tool_use") return block;
16396
+ const toolUse = block;
16397
+ if (toolUse.input !== null && typeof toolUse.input === "object") return block;
16398
+ repaired++;
16399
+ messageChanged = true;
16400
+ return {
16401
+ ...toolUse,
16402
+ input: {}
16403
+ };
16404
+ });
16405
+ return messageChanged ? {
16406
+ ...message,
16407
+ content
16408
+ } : message;
16409
+ });
16410
+ if (repaired === 0) return messages;
16411
+ logger?.warn(`[Tool Input Repair] Restored empty input on ${repaired} tool_use block(s) that lost it in serialization`);
16412
+ return result;
16413
+ };
16414
+ /**
16415
+ * Anthropic's hard ceiling on `cache_control` markers per request. Exceeding it fails the
16416
+ * WHOLE request with `ValidationException: A maximum of 4 blocks with cache_control may be
16417
+ * provided`, which is non-retryable - so an over-budget request loses the turn outright,
16418
+ * after the user has already waited for it.
16419
+ */
16420
+ const MAX_CACHE_CONTROL_BLOCKS = 4;
16421
+ /**
16422
+ * Does this block already carry a marker? Re-marking one costs no budget.
16423
+ *
16424
+ * Tests the VALUE, not just key presence: a block carrying an explicit
16425
+ * `cache_control: undefined` is not a marker as far as the provider is concerned, and counting
16426
+ * it would spend budget on nothing and drop a breakpoint we could have kept.
16427
+ */
16428
+ function hasMarker(block) {
16429
+ return !!block && typeof block === "object" && !!block.cache_control;
16430
+ }
16431
+ /**
16432
+ * Markers already on the request. Callers upstream attach their own before this runs -
16433
+ * `bedrockBackend/anthropic.ts` marks each system block flagged `cache: true` (the mid-stack
16434
+ * shareable-prefix breakpoint) - so this adapter's budget is whatever they left, not the full four.
16435
+ */
16436
+ function censusMarkers(params) {
16437
+ const tools = Array.isArray(params.tools) ? params.tools.filter(hasMarker).length : 0;
16438
+ const system = Array.isArray(params.system) ? params.system.filter(hasMarker).length : 0;
16439
+ let messages = 0;
16440
+ if (Array.isArray(params.messages)) for (const message of params.messages) {
16441
+ const content = message?.content;
16442
+ if (Array.isArray(content)) messages += content.filter(hasMarker).length;
16443
+ }
16444
+ return {
16445
+ tools,
16446
+ system,
16447
+ messages,
16448
+ total: tools + system + messages
16449
+ };
16450
+ }
16451
+ /**
15466
16452
  * Anthropic-specific caching adapter
15467
16453
  * Adds explicit cache_control markers to content blocks
15468
16454
  */
15469
16455
  var AnthropicCachingAdapter = class {
15470
- applyCaching(apiParams, strategy) {
16456
+ applyCaching(apiParams, strategy, logger) {
15471
16457
  if (!strategy.enableCaching) return apiParams;
15472
16458
  const ttl = strategy.cacheTTL ?? "5m";
15473
16459
  const modifiedParams = { ...apiParams };
15474
- const tools = modifiedParams.tools;
15475
- if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
15476
- const toolsCopy = [...tools];
15477
- const lastTool = toolsCopy[toolsCopy.length - 1];
15478
- toolsCopy[toolsCopy.length - 1] = {
15479
- ...lastTool,
15480
- cache_control: {
15481
- type: "ephemeral",
15482
- ...ttl === "1h" ? { ttl } : {}
15483
- }
15484
- };
15485
- modifiedParams.tools = toolsCopy;
15486
- }
16460
+ const cacheControl = {
16461
+ type: "ephemeral",
16462
+ ...ttl === "1h" ? { ttl } : {}
16463
+ };
16464
+ const inbound = censusMarkers(modifiedParams);
16465
+ let budget = MAX_CACHE_CONTROL_BLOCKS - inbound.total;
16466
+ const dropped = [];
16467
+ /** Claim one marker slot, or record the miss. Re-marking a marked block is free. */
16468
+ const claim = (name, alreadyMarked) => {
16469
+ if (alreadyMarked) return true;
16470
+ if (budget <= 0) {
16471
+ dropped.push(name);
16472
+ return false;
16473
+ }
16474
+ budget -= 1;
16475
+ return true;
16476
+ };
15487
16477
  const systemParam = modifiedParams.system;
15488
16478
  if (strategy.cacheSystemPrompt && systemParam) {
15489
16479
  const systemArray = Array.isArray(systemParam) ? [...systemParam] : [{
@@ -15492,14 +16482,13 @@ var AnthropicCachingAdapter = class {
15492
16482
  }];
15493
16483
  if (systemArray.length > 0) {
15494
16484
  const lastBlock = systemArray[systemArray.length - 1];
15495
- systemArray[systemArray.length - 1] = {
15496
- ...lastBlock,
15497
- cache_control: {
15498
- type: "ephemeral",
15499
- ...ttl === "1h" ? { ttl } : {}
15500
- }
15501
- };
15502
- modifiedParams.system = systemArray;
16485
+ if (claim("system", hasMarker(lastBlock))) {
16486
+ systemArray[systemArray.length - 1] = {
16487
+ ...lastBlock,
16488
+ cache_control: cacheControl
16489
+ };
16490
+ modifiedParams.system = systemArray;
16491
+ }
15503
16492
  }
15504
16493
  }
15505
16494
  const messagesParam = modifiedParams.messages;
@@ -15515,24 +16504,65 @@ var AnthropicCachingAdapter = class {
15515
16504
  text: msgContent
15516
16505
  }];
15517
16506
  else if (Array.isArray(msgContent)) contentArray = [...msgContent];
15518
- else return modifiedParams;
15519
- if (contentArray.length > 0) {
16507
+ if (contentArray && contentArray.length > 0) {
15520
16508
  const lastBlock = contentArray[contentArray.length - 1];
15521
- contentArray[contentArray.length - 1] = {
15522
- ...lastBlock,
15523
- cache_control: {
15524
- type: "ephemeral",
15525
- ...ttl === "1h" ? { ttl } : {}
15526
- }
15527
- };
15528
- messages[anchorIndex] = {
15529
- ...anchorMsg,
15530
- content: contentArray
15531
- };
15532
- modifiedParams.messages = messages;
16509
+ if (claim("history", hasMarker(lastBlock))) {
16510
+ contentArray[contentArray.length - 1] = {
16511
+ ...lastBlock,
16512
+ cache_control: cacheControl
16513
+ };
16514
+ messages[anchorIndex] = {
16515
+ ...anchorMsg,
16516
+ content: contentArray
16517
+ };
16518
+ modifiedParams.messages = messages;
16519
+ }
15533
16520
  }
15534
16521
  }
15535
16522
  }
16523
+ const tools = modifiedParams.tools;
16524
+ if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
16525
+ const toolsCopy = [...tools];
16526
+ const lastTool = toolsCopy[toolsCopy.length - 1];
16527
+ if (claim("tools", hasMarker(lastTool))) {
16528
+ toolsCopy[toolsCopy.length - 1] = {
16529
+ ...lastTool,
16530
+ cache_control: cacheControl
16531
+ };
16532
+ modifiedParams.tools = toolsCopy;
16533
+ }
16534
+ }
16535
+ const outbound = censusMarkers(modifiedParams);
16536
+ const census = {
16537
+ inbound,
16538
+ outbound,
16539
+ limit: MAX_CACHE_CONTROL_BLOCKS
16540
+ };
16541
+ if (outbound.total >= MAX_CACHE_CONTROL_BLOCKS) {
16542
+ const message = "[PromptCache] cache_control census at the ceiling";
16543
+ if (logger) logger.info(message, census);
16544
+ else console.info(message, JSON.stringify(census));
16545
+ } else if (logger) logger.debug("[PromptCache] cache_control census", census);
16546
+ if (outbound.total > MAX_CACHE_CONTROL_BLOCKS) {
16547
+ const message = `[PromptCache] request exceeds the ${MAX_CACHE_CONTROL_BLOCKS}-block cache_control limit on arrival (${outbound.total}); the provider will reject it`;
16548
+ const detail = {
16549
+ inbound,
16550
+ outbound,
16551
+ limit: MAX_CACHE_CONTROL_BLOCKS
16552
+ };
16553
+ if (logger) logger.error(message, detail);
16554
+ else console.error(message, JSON.stringify(detail));
16555
+ } else if (dropped.length > 0) {
16556
+ const message = `[PromptCache] cache_control budget exhausted (limit ${MAX_CACHE_CONTROL_BLOCKS}); skipped breakpoints: ${dropped.join(", ")}`;
16557
+ const detail = {
16558
+ dropped,
16559
+ inbound,
16560
+ outbound,
16561
+ limit: MAX_CACHE_CONTROL_BLOCKS
16562
+ };
16563
+ if (logger) logger.warn(message, detail);
16564
+ else console.warn(message, JSON.stringify(detail));
16565
+ }
15536
16566
  return modifiedParams;
15537
16567
  }
15538
16568
  extractCacheStats(response, model) {
@@ -15742,6 +16772,25 @@ function getCachingAdapter(backend) {
15742
16772
  return ADAPTERS[backend] || new NoOpCachingAdapter();
15743
16773
  }
15744
16774
  /**
16775
+ * Flatten a system message's content to the plain text a provider should receive.
16776
+ *
16777
+ * System content is usually a string, but the assembly pipeline can hand back an
16778
+ * array of content blocks. Both Anthropic-family adapters previously coerced that
16779
+ * array with `JSON.stringify` (or bare `String()`), which sent the model literal
16780
+ * JSON syntax - escaped quotes and `type`/`text` keys - in place of the prompt.
16781
+ *
16782
+ * Only `text` blocks are read: Anthropic's `system` accepts text only, so an image
16783
+ * or tool block there is already invalid and is dropped rather than serialized into
16784
+ * the prompt. Blocks are joined on a newline, matching how separate system messages
16785
+ * are joined by the callers.
16786
+ */
16787
+ function systemContentToText(content) {
16788
+ if (content === void 0 || content === null) return "";
16789
+ if (typeof content === "string") return content;
16790
+ if (!Array.isArray(content)) return "";
16791
+ return content.filter((block) => block?.type === "text").map((block) => block.text ?? "").filter((text) => text.trim() !== "").join("\n");
16792
+ }
16793
+ /**
15745
16794
  * max_tokens floor for adaptive reasoning models (Claude 4.7+/Opus 5). These
15746
16795
  * models self-manage extended thinking *within* max_tokens, which is a ceiling
15747
16796
  * (they stop at end_turn), not a target - so a larger floor costs nothing on
@@ -15805,8 +16854,17 @@ function reasonsWithinOutputBudget(modelInfo) {
15805
16854
  * be consumed entirely by reasoning, leaving an empty visible reply.
15806
16855
  */
15807
16856
  function resolveOutputMaxTokens({ requested, fallback, modelInfo, modelMaxOutputTokens }) {
15808
- const preferred = requested ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
15809
- return Math.min(preferred, modelMaxOutputTokens);
16857
+ const preferred = usableTokenCount(requested) ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
16858
+ const cap = usableTokenCount(modelMaxOutputTokens);
16859
+ return cap === void 0 ? preferred : Math.min(preferred, cap);
16860
+ }
16861
+ /**
16862
+ * Token counts reaching this module come from catalog rows and external callers, so they are
16863
+ * only trustworthy when finite and positive - a zero or negative cap would clamp the budget
16864
+ * to an unsendable value just as surely as NaN poisons it.
16865
+ */
16866
+ function usableTokenCount(value) {
16867
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
15810
16868
  }
15811
16869
  /**
15812
16870
  * Builds the correct thinking parameters for an Anthropic API call based on
@@ -15995,6 +17053,12 @@ const SLOW_MODEL_REQUEST_TIMEOUT_MS = 12e4;
15995
17053
  */
15996
17054
  const ANTHROPIC_NONSTREAMING_MAX_TOKENS = 21e3;
15997
17055
  /**
17056
+ * Output budget used when the caller names none. Only applies to models that do NOT spend
17057
+ * reasoning inside the output budget - resolveOutputMaxTokens sizes the rest for the model,
17058
+ * so this value stays the historical one and non-reasoning behavior is unchanged.
17059
+ */
17060
+ const DEFAULT_ANTHROPIC_MAX_TOKENS = 4096;
17061
+ /**
15998
17062
  * Accumulated multi-turn cache token total. Undefined when zero so turns
15999
17063
  * without cache activity keep the pre-cache callback shape.
16000
17064
  */
@@ -16605,7 +17669,8 @@ var AnthropicBackend = class {
16605
17669
  const systemMessages = messages.filter((m) => m.role === "system");
16606
17670
  const blocks = [];
16607
17671
  for (const sm of systemMessages) {
16608
- const text = typeof sm.content === "string" ? sm.content : JSON.stringify(sm.content);
17672
+ const text = systemContentToText(sm.content);
17673
+ if (text.trim() === "") continue;
16609
17674
  if (sm.cache === true) blocks.push({
16610
17675
  type: "text",
16611
17676
  text,
@@ -16625,7 +17690,7 @@ var AnthropicBackend = class {
16625
17690
  const parts = [this.consolidateSystemMessages(messages), identityReminder].filter(Boolean);
16626
17691
  system = parts.length > 0 ? parts.join("\n") : void 0;
16627
17692
  }
16628
- let filteredMessages = ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger);
17693
+ let filteredMessages = normalizeToolUseInputs(ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger), this.logger);
16629
17694
  const countToolBlocks = (msgs) => {
16630
17695
  let useCount = 0;
16631
17696
  let resultCount = 0;
@@ -16644,7 +17709,11 @@ var AnthropicBackend = class {
16644
17709
  let { useCount: toolUseCount, resultCount: toolResultCount } = countToolBlocks(filteredMessages);
16645
17710
  if (toolUseCount > 0 || toolResultCount > 0) {
16646
17711
  this.logger.debug(`[Pre-API #6181] Sending ${filteredMessages.length} messages with ${toolUseCount} tool_use and ${toolResultCount} tool_result blocks`);
16647
- if (toolUseCount !== toolResultCount) {
17712
+ if (!options.tools?.length) {
17713
+ 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.`);
17714
+ filteredMessages = stripAllToolBlocks(filteredMessages, this.logger);
17715
+ ({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
17716
+ } else if (toolUseCount !== toolResultCount) {
16648
17717
  this.logger.warn(`[Pre-API #6181] Tool block mismatch! tool_use: ${toolUseCount}, tool_result: ${toolResultCount}. Attempting auto-repair...`);
16649
17718
  filteredMessages = ensureToolPairingIntegrity(filteredMessages, this.logger);
16650
17719
  ({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
@@ -16654,9 +17723,15 @@ var AnthropicBackend = class {
16654
17723
  }
16655
17724
  }
16656
17725
  }
17726
+ const currentModelInfo = this.modelRecordFor(model);
16657
17727
  const apiParams = {
16658
17728
  model,
16659
- max_tokens: options.maxTokens ?? 4096,
17729
+ max_tokens: currentModelInfo ? resolveOutputMaxTokens({
17730
+ requested: options.maxTokens,
17731
+ fallback: DEFAULT_ANTHROPIC_MAX_TOKENS,
17732
+ modelInfo: currentModelInfo,
17733
+ modelMaxOutputTokens: currentModelInfo.max_tokens
17734
+ }) : options.maxTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS,
16660
17735
  messages: filteredMessages.map((m) => ({
16661
17736
  role: m.role === "user" ? "user" : "assistant",
16662
17737
  content: m.content
@@ -16696,7 +17771,6 @@ var AnthropicBackend = class {
16696
17771
  }
16697
17772
  }
16698
17773
  const requestExtraHeaders = anyMessageCacheControlled ? { "anthropic-beta": "prompt-caching-2024-07-31" } : void 0;
16699
- const currentModelInfo = this.modelRecordFor(model);
16700
17774
  if (currentModelInfo?.can_think) {
16701
17775
  const thinkingOptions = options;
16702
17776
  const isQuestMaster = thinkingOptions.questMaster === true;
@@ -16716,7 +17790,7 @@ var AnthropicBackend = class {
16716
17790
  } else this.isThinkingEnabled = false;
16717
17791
  const cacheStrategy = options.cacheStrategy;
16718
17792
  if (cacheStrategy?.enableCaching) {
16719
- const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy);
17793
+ const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy, this.logger);
16720
17794
  Object.assign(apiParams, cachedParams);
16721
17795
  this.logger.debug("[Anthropic] Applying cache control", {
16722
17796
  cacheSystemPrompt: cacheStrategy.cacheSystemPrompt,
@@ -17077,6 +18151,10 @@ var AnthropicBackend = class {
17077
18151
  if (!parsedParams) {
17078
18152
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
17079
18153
  if (entry) entry.arguments = "{}";
18154
+ recordToolResult(toolsUsed, {
18155
+ id,
18156
+ name
18157
+ }, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
17080
18158
  continue;
17081
18159
  }
17082
18160
  resolvedTools.push({
@@ -17144,6 +18222,10 @@ var AnthropicBackend = class {
17144
18222
  toolsUsed
17145
18223
  });
17146
18224
  });
18225
+ recordToolResult(toolsUsed, {
18226
+ id: outcome.id,
18227
+ name: outcome.name
18228
+ }, resultStr, true);
17147
18229
  this.pushToolMessages(messages, {
17148
18230
  id: toolId,
17149
18231
  name: outcome.name,
@@ -17151,16 +18233,22 @@ var AnthropicBackend = class {
17151
18233
  }, resultStr);
17152
18234
  } else {
17153
18235
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
18236
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17154
18237
  this.logger.error("[Tool Execution] Tool failed", {
17155
18238
  model,
17156
18239
  toolName: outcome.name,
17157
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
18240
+ error: errorMessage
17158
18241
  });
18242
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
18243
+ recordToolResult(toolsUsed, {
18244
+ id: outcome.id,
18245
+ name: outcome.name
18246
+ }, observation, false);
17159
18247
  this.pushToolMessages(messages, {
17160
18248
  id: toolId,
17161
18249
  name: outcome.name,
17162
18250
  parameters: outcome.parameters
17163
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
18251
+ }, observation);
17164
18252
  }
17165
18253
  }
17166
18254
  await cb(["\n\n"], { toolsUsed });
@@ -17297,7 +18385,13 @@ var AnthropicBackend = class {
17297
18385
  isMcpTool,
17298
18386
  streaming: false
17299
18387
  }, messages);
17300
- if (!parsedParams) continue;
18388
+ if (!parsedParams) {
18389
+ recordToolResult(toolsUsed, {
18390
+ id,
18391
+ name
18392
+ }, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
18393
+ continue;
18394
+ }
17301
18395
  resolvedTools.push({
17302
18396
  id: id ?? "",
17303
18397
  name,
@@ -17359,6 +18453,10 @@ var AnthropicBackend = class {
17359
18453
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17360
18454
  await cb(results, { toolsUsed });
17361
18455
  });
18456
+ recordToolResult(toolsUsed, {
18457
+ id: outcome.id,
18458
+ name: outcome.name
18459
+ }, resultStr, true);
17362
18460
  this.pushToolMessages(messages, {
17363
18461
  id: toolId,
17364
18462
  name: outcome.name,
@@ -17366,16 +18464,22 @@ var AnthropicBackend = class {
17366
18464
  }, resultStr);
17367
18465
  } else {
17368
18466
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
18467
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
17369
18468
  this.logger.error("[Tool Execution] Tool failed (non-streaming)", {
17370
18469
  model,
17371
18470
  toolName: outcome.name,
17372
- error: outcome.error instanceof Error ? outcome.error.message : "Unknown error"
18471
+ error: errorMessage
17373
18472
  });
18473
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
18474
+ recordToolResult(toolsUsed, {
18475
+ id: outcome.id,
18476
+ name: outcome.name
18477
+ }, observation, false);
17374
18478
  this.pushToolMessages(messages, {
17375
18479
  id: toolId,
17376
18480
  name: outcome.name,
17377
18481
  parameters: outcome.parameters
17378
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
18482
+ }, observation);
17379
18483
  }
17380
18484
  }
17381
18485
  this.logger.info("[Tool Execution] Making recursive call after tool execution (non-streaming)", {
@@ -17629,7 +18733,7 @@ var AnthropicBackend = class {
17629
18733
  consolidateSystemMessages(messages) {
17630
18734
  const systemMessages = messages.filter((m) => m.role === "system");
17631
18735
  if (systemMessages.length === 0) return void 0;
17632
- return systemMessages.map((m) => m.content).join("\n");
18736
+ return systemMessages.map((m) => systemContentToText(m.content)).filter((text) => text.trim() !== "").join("\n");
17633
18737
  }
17634
18738
  isToolUseEvent(event) {
17635
18739
  return typeof event === "object" && event !== null && "type" in event && event.type === "tool_use" && "name" in event && "input" in event && "id" in event;
@@ -17710,6 +18814,11 @@ const BEDROCK_RETRY_CONFIG = {
17710
18814
  maxAttempts: 6,
17711
18815
  retryMode: "adaptive"
17712
18816
  };
18817
+ const BEDROCK_REQUEST_HANDLER = {
18818
+ requestTimeout: 12e4,
18819
+ sessionTimeout: 13e4,
18820
+ disableConcurrentStreams: true
18821
+ };
17713
18822
  /**
17714
18823
  * Detect cancellation errors so they propagate past tool-error containment to
17715
18824
  * the outer catch (which has dedicated abort handling). Without this, aborts
@@ -17751,7 +18860,8 @@ var BaseBedrockBackend = class {
17751
18860
  };
17752
18861
  this._bedrockRuntime = new BedrockRuntimeClient({
17753
18862
  region: this._options.region,
17754
- ...BEDROCK_RETRY_CONFIG
18863
+ ...BEDROCK_RETRY_CONFIG,
18864
+ requestHandler: BEDROCK_REQUEST_HANDLER
17755
18865
  });
17756
18866
  }
17757
18867
  getRegionForModel(model) {
@@ -17772,12 +18882,40 @@ var BaseBedrockBackend = class {
17772
18882
  const command = new InvokeModelWithResponseStreamCommand(input);
17773
18883
  return this._bedrockRuntime.send(command, { abortSignal });
17774
18884
  }
18885
+ /**
18886
+ * The reasoning blocks the just-translated assistant turn produced, cleared as they are
18887
+ * taken. A backend whose provider signs thinking blocks overrides this so the tool loop
18888
+ * below can replay them onto the assistant turns it rebuilds; providers that sign nothing
18889
+ * keep the default. @see AnthropicBedrockBackend.takeReasoningBlocks
18890
+ */
18891
+ takeReasoningBlocks() {
18892
+ return [];
18893
+ }
18894
+ /**
18895
+ * Whether this adapter's `translateStreamChunk` reports `done: true` ONLY on the provider's
18896
+ * terminal event. When true, complete() treats a stream that produced output but never
18897
+ * reported done as a TRUNCATED response and throws instead of returning the partial text.
18898
+ *
18899
+ * Opt-in rather than the default because "reports done terminally" is a per-adapter contract
18900
+ * the base class cannot infer, and getting it wrong turns every healthy completion into an
18901
+ * error. Three groups exist today:
18902
+ * - terminal-only, so they override this to true: anthropic, deepseek, llama, jurassicTwo
18903
+ * - `done: true` on EVERY content chunk, so the check would be inert: titan, moonshot
18904
+ * (the better fix for those is a stopReason passthrough, as moonshot.ts already does)
18905
+ * - never report done, incl. the test doubles in this directory: left false
18906
+ *
18907
+ * A new streaming backend must opt in deliberately; silence keeps the old behaviour.
18908
+ */
18909
+ get signalsStreamTermination() {
18910
+ return false;
18911
+ }
17775
18912
  updateClientForModel(model) {
17776
18913
  const requiredRegion = this.getRegionForModel(model);
17777
18914
  this._options.region = requiredRegion;
17778
18915
  this._bedrockRuntime = new BedrockRuntimeClient({
17779
18916
  region: this._options.region,
17780
- ...BEDROCK_RETRY_CONFIG
18917
+ ...BEDROCK_RETRY_CONFIG,
18918
+ requestHandler: BEDROCK_REQUEST_HANDLER
17781
18919
  });
17782
18920
  }
17783
18921
  async complete(model, messages, options, callback, toolsUsed = []) {
@@ -17797,8 +18935,14 @@ var BaseBedrockBackend = class {
17797
18935
  }, callback, toolsUsed);
17798
18936
  return;
17799
18937
  }
17800
- const messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
18938
+ let messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
17801
18939
  const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
18940
+ if (!options.tools?.length) {
18941
+ if (messagesWithFormat.some((m) => Array.isArray(m.content) && m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"))) {
18942
+ Logger.globalInstance.warn("[BaseBedrockBackend Pre-API #6181] Tool blocks present but no tools offered this turn. Stripping all tool blocks.");
18943
+ messagesWithFormat = stripAllToolBlocks(messagesWithFormat, Logger.globalInstance);
18944
+ }
18945
+ }
17802
18946
  let formattedMessages = this.formatMessages(messagesWithFormat);
17803
18947
  let input = this.getPayload(model, formattedMessages, options);
17804
18948
  const contextWindow = this.getModelContextWindow(model);
@@ -17879,9 +19023,11 @@ var BaseBedrockBackend = class {
17879
19023
  if (!response.body) throw new Error("No response body");
17880
19024
  const func = [];
17881
19025
  let emittedTextChars = 0;
19026
+ let sawTerminalEvent = false;
17882
19027
  for await (const streamEvent of response.body) if (streamEvent.chunk?.bytes) {
17883
19028
  const json = new TextDecoder().decode(streamEvent.chunk.bytes);
17884
- const { chunk } = this.translateStreamChunk(model, JSON.parse(json));
19029
+ const { done, chunk } = this.translateStreamChunk(model, JSON.parse(json));
19030
+ sawTerminalEvent ||= done;
17885
19031
  if (chunk?.stopReason) stopReason = chunk.stopReason;
17886
19032
  chunk?.choices?.forEach((choice) => {
17887
19033
  func[choice.index] ||= {};
@@ -17905,6 +19051,7 @@ var BaseBedrockBackend = class {
17905
19051
  await callback(streamedText, buildCompletionInfo());
17906
19052
  }
17907
19053
  if (emittedTextChars === 0 && !func.some((f) => f.name)) throw new Error(`[BaseBedrockBackend] model "${model}" returned an EMPTY response in region ${this._options.region} (no text, no tool call, no output tokens). A "global." cross-region inference profile served from a region that does not host it does exactly this - try the "us." variant, or confirm the model/profile is granted in ${this._options.region}.`);
19054
+ if (this.signalsStreamTermination && !sawTerminalEvent && !options.abortSignal?.aborted) throw new Error(`[BaseBedrockBackend] stream timeout - model "${model}" in region ${this._options.region} ended after ${emittedTextChars} chars without a terminal event, so the response is TRUNCATED. Usually a stalled Bedrock socket cut the stream short; the partial text is withheld deliberately rather than returned as a finished answer.`);
17908
19055
  if (func.some((f) => f.name)) {
17909
19056
  for await (const tool of func) {
17910
19057
  const { id, name, parameters } = tool;
@@ -17932,6 +19079,12 @@ var BaseBedrockBackend = class {
17932
19079
  });
17933
19080
  } catch {
17934
19081
  Logger.globalInstance.warn("[BaseBedrockBackend] Tool parameter parse error, skipping tool:", name);
19082
+ const entry = toolsUsed.find((t) => t.name === name && t.id === id);
19083
+ if (entry) entry.arguments = "{}";
19084
+ recordToolResult(toolsUsed, {
19085
+ id,
19086
+ name
19087
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
17935
19088
  }
17936
19089
  }
17937
19090
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -17959,32 +19112,40 @@ var BaseBedrockBackend = class {
17959
19112
  parameters: resolvedTools[i].parameters,
17960
19113
  error: outcome.error
17961
19114
  });
19115
+ const roundReasoningBlocks = this.takeReasoningBlocks();
17962
19116
  for (const outcome of outcomes) if (outcome.ok) {
17963
19117
  await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
17964
19118
  await callback(results, buildCompletionInfo());
17965
19119
  });
19120
+ const resultStr = outcome.result.toString();
19121
+ recordToolResult(toolsUsed, {
19122
+ id: outcome.id,
19123
+ name: outcome.name
19124
+ }, resultStr, true);
17966
19125
  this.pushToolMessages(messages, {
17967
19126
  id: outcome.id,
17968
19127
  name: outcome.name,
17969
19128
  parameters: outcome.parameters
17970
- }, outcome.result.toString());
19129
+ }, resultStr, roundReasoningBlocks);
17971
19130
  } else {
17972
19131
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
17973
19132
  if (isAbortError(outcome.error)) throw outcome.error;
17974
19133
  Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${outcome.name} failed:`, outcome.error instanceof Error ? outcome.error.message : String(outcome.error));
19134
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
19135
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
19136
+ recordToolResult(toolsUsed, {
19137
+ id: outcome.id,
19138
+ name: outcome.name
19139
+ }, observation, false);
17975
19140
  this.pushToolMessages(messages, {
17976
19141
  id: outcome.id,
17977
19142
  name: outcome.name,
17978
19143
  parameters: outcome.parameters
17979
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
19144
+ }, observation, roundReasoningBlocks);
17980
19145
  }
17981
19146
  await callback(["\n\n"], buildCompletionInfo());
17982
19147
  await this.complete(model, messages, {
17983
19148
  ...options,
17984
- thinking: {
17985
- enabled: false,
17986
- budget_tokens: 0
17987
- },
17988
19149
  tool_choice: "auto",
17989
19150
  _internal: {
17990
19151
  ...options._internal,
@@ -18028,35 +19189,38 @@ var BaseBedrockBackend = class {
18028
19189
  if (options.executeTools !== false) {
18029
19190
  const executable = toolChoices.map((tc) => tc.tool).filter((tool) => tool.id && tool.name && options.tools?.some((o) => o.toolSchema.name === tool.name));
18030
19191
  if (executable.length > 0) {
19192
+ const roundReasoningBlocks = this.takeReasoningBlocks();
18031
19193
  for (const { id, name, parameters } of executable) {
18032
19194
  const toolFn = options.tools?.find((o) => o.toolSchema.name === name)?.toolFn;
18033
19195
  if (!toolFn) continue;
18034
19196
  const safeParameters = parameters || "{}";
18035
19197
  let result;
19198
+ let succeeded = true;
18036
19199
  try {
18037
19200
  result = await toolFn(JSON.parse(safeParameters));
18038
19201
  } catch (err) {
18039
19202
  if (err instanceof PermissionDeniedError) throw err;
18040
19203
  if (isAbortError(err)) throw err;
18041
19204
  Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${name} failed:`, err instanceof Error ? err.message : String(err));
19205
+ succeeded = false;
18042
19206
  result = `Error processing ${name} tool: ${err instanceof Error ? err.message : "Unknown error"}`;
18043
19207
  }
18044
19208
  await handleToolResultStreaming(name, result, async (results) => {
18045
19209
  await callback(results, buildCompletionInfo());
18046
19210
  });
19211
+ recordToolResult(toolsUsed, {
19212
+ id,
19213
+ name
19214
+ }, result.toString(), succeeded);
18047
19215
  this.pushToolMessages(messages, {
18048
19216
  id,
18049
19217
  name,
18050
19218
  parameters
18051
- }, result.toString());
19219
+ }, result.toString(), roundReasoningBlocks);
18052
19220
  }
18053
19221
  await callback(["\n\n"], buildCompletionInfo());
18054
19222
  await this.complete(model, messages, {
18055
19223
  ...options,
18056
- thinking: {
18057
- enabled: false,
18058
- budget_tokens: 0
18059
- },
18060
19224
  tool_choice: "auto",
18061
19225
  _internal: {
18062
19226
  ...options._internal,
@@ -18151,6 +19315,9 @@ function isToolUseContentBlock(content_block) {
18151
19315
  function isThinkingContentBlock(content_block) {
18152
19316
  return isRecord(content_block) && content_block.type === "thinking";
18153
19317
  }
19318
+ function isRedactedThinkingContentBlock(content_block) {
19319
+ return isRecord(content_block) && content_block.type === "redacted_thinking";
19320
+ }
18154
19321
  function isTextDelta(delta) {
18155
19322
  return isRecord(delta) && delta.type === "text_delta" && "text" in delta;
18156
19323
  }
@@ -18160,6 +19327,9 @@ function isInputJsonDelta(delta) {
18160
19327
  function isThinkingDelta(delta) {
18161
19328
  return isRecord(delta) && delta.type === "thinking_delta" && "thinking" in delta;
18162
19329
  }
19330
+ function isSignatureDelta(delta) {
19331
+ return isRecord(delta) && delta.type === "signature_delta" && "signature" in delta;
19332
+ }
18163
19333
  function isMessageStart(chunk) {
18164
19334
  return isRecord(chunk) && chunk.type === "message_start";
18165
19335
  }
@@ -18186,13 +19356,40 @@ const TEMPERATURE_ONLY_MODELS = [
18186
19356
  ChatModels.CLAUDE_4_6_OPUS_BEDROCK
18187
19357
  ];
18188
19358
  var AnthropicBedrockBackend = class extends BaseBedrockBackend {
19359
+ /** Reports done only on message_stop (anthropic.ts translateStreamChunk), so a missing terminal event means a truncated stream. */
19360
+ get signalsStreamTermination() {
19361
+ return true;
19362
+ }
18189
19363
  isInThinkingBlock = false;
19364
+ /**
19365
+ * Reasoning blocks of the assistant turn currently being translated, indexed by the
19366
+ * stream's content-block index. Reset at `message_start` and consumed by
19367
+ * `takeReasoningBlocks` when that turn is rebuilt for a tool continuation.
19368
+ */
19369
+ assistantReasoningBlocks = [];
18190
19370
  /** Catalog view of the model being completed; see DispatchModel. */
18191
19371
  _dispatch = new DispatchModel();
18192
19372
  setDispatchModel(info) {
18193
19373
  this._dispatch.set(info);
18194
19374
  }
18195
19375
  /**
19376
+ * The reasoning blocks the last translated assistant turn produced, cleared as they
19377
+ * are taken. Anthropic requires a `tool_use` assistant turn to replay its own signed
19378
+ * thinking blocks whenever extended thinking is active on the turn, and an adaptive
19379
+ * model thinks on every turn whether or not the request asked it to - so dropping
19380
+ * them is what makes the synthesis round of a multi-round tool turn come back empty.
19381
+ *
19382
+ * Taken once per provider turn. base.ts takes them before its tool loop and hands the
19383
+ * same array to every assistant message it rebuilds for that round, because a parallel
19384
+ * round splits one provider turn across several synthetic turns and each of them has to
19385
+ * carry the reasoning.
19386
+ */
19387
+ takeReasoningBlocks() {
19388
+ const blocks = this.assistantReasoningBlocks.filter((b) => b != null);
19389
+ this.assistantReasoningBlocks = [];
19390
+ return blocks;
19391
+ }
19392
+ /**
18196
19393
  * The record the payload is shaped from: the adapter table first, then the
18197
19394
  * catalog for a model the table never listed. Table-first keeps every
18198
19395
  * currently-dispatched Bedrock id on exactly today's payload.
@@ -18609,11 +19806,13 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18609
19806
  content: ""
18610
19807
  };
18611
19808
  }).filter((m) => m.content !== "" && (Array.isArray(m.content) ? m.content.length > 0 : true));
18612
- let systemMessage = messages.filter((m) => m.role === "system" && m.content).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n");
18613
- if (!options.omitIdentityReminder) {
18614
- const modelIdentity = `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
18615
- systemMessage = systemMessage ? `${systemMessage}\n${modelIdentity}` : modelIdentity;
18616
- }
19809
+ const systemBlocks = messages.filter((m) => m.role === "system" && m.content).map((m) => ({
19810
+ text: systemContentToText(m.content),
19811
+ cache: m.cache === true
19812
+ })).filter((block) => block.text.trim() !== "");
19813
+ const identityReminder = options.omitIdentityReminder ? null : `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
19814
+ let systemMessage = systemBlocks.map((block) => block.text).join("\n");
19815
+ if (identityReminder) systemMessage = systemMessage ? `${systemMessage}\n${identityReminder}` : identityReminder;
18617
19816
  const modelId = model.includes(":") || model.startsWith("global.") || model.startsWith("us.") || model.startsWith("anthropic.") ? model : `anthropic.${model}`;
18618
19817
  const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : 4096;
18619
19818
  const body = {
@@ -18621,7 +19820,27 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18621
19820
  max_tokens: maxTokens,
18622
19821
  messages: filteredMessages
18623
19822
  };
18624
- if (systemMessage) body.system = systemMessage;
19823
+ const cacheStrategy = options.cacheStrategy;
19824
+ const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
19825
+ if (Boolean(cacheStrategy?.enableCaching) && modelSupportsCaching && systemBlocks.some((block) => block.cache)) {
19826
+ const cacheControl = {
19827
+ type: "ephemeral",
19828
+ ...cacheStrategy?.cacheTTL === "1h" ? { ttl: cacheStrategy.cacheTTL } : {}
19829
+ };
19830
+ const blocks = systemBlocks.map((block) => block.cache ? {
19831
+ type: "text",
19832
+ text: block.text,
19833
+ cache_control: cacheControl
19834
+ } : {
19835
+ type: "text",
19836
+ text: block.text
19837
+ });
19838
+ if (identityReminder) blocks.push({
19839
+ type: "text",
19840
+ text: identityReminder
19841
+ });
19842
+ body.system = blocks;
19843
+ } else if (systemMessage) body.system = systemMessage;
18625
19844
  if (options.tools?.length) body.tools = this.formatTools(options.tools);
18626
19845
  if (typeof options.temperature === "number" && !this.omitsSamplingParams(model)) body.temperature = options.temperature;
18627
19846
  if (typeof options.topP === "number" && !TEMPERATURE_ONLY_MODELS.includes(model) && !this.omitsSamplingParams(model)) body.top_p = options.topP;
@@ -18649,8 +19868,6 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18649
19868
  topP: options.topP
18650
19869
  }
18651
19870
  }, null, 2)}`);
18652
- const cacheStrategy = options.cacheStrategy;
18653
- const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
18654
19871
  if (cacheStrategy?.enableCaching && modelSupportsCaching) {
18655
19872
  const cachedBody = getCachingAdapter(ModelBackend.Bedrock).applyCaching(body, cacheStrategy);
18656
19873
  Object.assign(body, cachedBody);
@@ -18674,9 +19891,11 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18674
19891
  formatMessages(messages) {
18675
19892
  return messages.reduce((cur, value) => {
18676
19893
  const previousMessage = cur[cur.length - 1];
18677
- if (previousMessage && value.role === previousMessage.role) {
18678
- if (previousMessage.content === value.content) return cur;
18679
- else if (!Array.isArray(previousMessage.content)) {
19894
+ if (previousMessage && value.role === previousMessage.role && previousMessage.cache !== true) {
19895
+ if (previousMessage.content === value.content) {
19896
+ if (value.cache === true) previousMessage.cache = true;
19897
+ return cur;
19898
+ } else if (!Array.isArray(previousMessage.content)) {
18680
19899
  const lastIndex = cur.length - 1;
18681
19900
  const prevContent = typeof cur[lastIndex].content === "string" ? cur[lastIndex].content : "";
18682
19901
  const currContent = typeof value.content === "string" ? value.content : "";
@@ -18696,6 +19915,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18696
19915
  });
18697
19916
  if (contentArray.length > 0) cur[lastIndex].content = contentArray;
18698
19917
  }
19918
+ if (value.cache === true) cur[lastIndex].cache = true;
18699
19919
  } else {
18700
19920
  if (typeof value.content !== "string") {
18701
19921
  cur.push(value);
@@ -18707,6 +19927,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18707
19927
  type: "text",
18708
19928
  text: textContent
18709
19929
  }];
19930
+ if (value.cache === true) previousMessage.cache = true;
18710
19931
  }
18711
19932
  return cur;
18712
19933
  }
@@ -18717,11 +19938,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18717
19938
  translateChunk(model, chunk) {
18718
19939
  try {
18719
19940
  const response = chunk;
18720
- const thinkingBlocks = response.content.filter((c) => c.type === "thinking");
18721
- if (thinkingBlocks.length > 0) console.warn(`[AnthropicBedrockBackend] Unexpected thinking blocks in response`, {
18722
- thinkingBlockCount: thinkingBlocks.length,
18723
- thinkingLengths: thinkingBlocks.map((b) => b.thinking?.length || 0)
18724
- });
19941
+ this.assistantReasoningBlocks = response.content.filter((c) => c.type === "thinking" || c.type === "redacted_thinking");
19942
+ if (this.assistantReasoningBlocks.length > 0) console.log(`[AnthropicBedrockBackend] Captured ${this.assistantReasoningBlocks.length} reasoning block(s) for tool continuation`);
18725
19943
  const textContent = response.content.filter((item) => item.type === "text").map((item) => item.text || "").join("");
18726
19944
  const toolUseBlocks = response.content.filter((item) => item.type === "tool_use");
18727
19945
  let choice;
@@ -18775,6 +19993,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18775
19993
  try {
18776
19994
  if (isMessageStart(chunk)) {
18777
19995
  this.isInThinkingBlock = false;
19996
+ this.assistantReasoningBlocks = [];
18778
19997
  choice = {
18779
19998
  chunkText: "",
18780
19999
  usage: {
@@ -18796,8 +20015,12 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18796
20015
  };
18797
20016
  else if (isThinkingContentBlock(contentBlock)) {
18798
20017
  this.isInThinkingBlock = true;
20018
+ this.assistantReasoningBlocks[chunk.index] = {
20019
+ ...contentBlock,
20020
+ thinking: contentBlock.thinking ?? ""
20021
+ };
18799
20022
  choice.chunkText = "<think>";
18800
- }
20023
+ } else if (isRedactedThinkingContentBlock(contentBlock)) this.assistantReasoningBlocks[chunk.index] = { ...contentBlock };
18801
20024
  } else if (isContentBlockDelta(chunk)) {
18802
20025
  choice = {
18803
20026
  status: "stream",
@@ -18807,7 +20030,14 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18807
20030
  const delta = chunk.delta;
18808
20031
  if (isTextDelta(delta)) choice.chunkText = delta.text;
18809
20032
  else if (isInputJsonDelta(delta)) choice.chunkText = delta.partial_json;
18810
- else if (isThinkingDelta(delta)) choice.chunkText = delta.thinking;
20033
+ else if (isThinkingDelta(delta)) {
20034
+ choice.chunkText = delta.thinking;
20035
+ const block = this.assistantReasoningBlocks[chunk.index];
20036
+ if (block?.type === "thinking") block.thinking += delta.thinking;
20037
+ } else if (isSignatureDelta(delta)) {
20038
+ const block = this.assistantReasoningBlocks[chunk.index];
20039
+ if (block?.type === "thinking") block.signature = delta.signature;
20040
+ }
18811
20041
  } else if (isContentBlockStop(chunk)) {
18812
20042
  choice = {
18813
20043
  status: "stream",
@@ -18866,7 +20096,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
18866
20096
  name: tool.name,
18867
20097
  input: JSON.parse(tool.parameters || "{}")
18868
20098
  };
18869
- const assistantContent = thinkingBlocks && thinkingBlocks.length > 0 ? [...thinkingBlocks, toolUseBlock] : [toolUseBlock];
20099
+ const reasoningBlocks = thinkingBlocks?.length ? thinkingBlocks : this.takeReasoningBlocks();
20100
+ const assistantContent = reasoningBlocks.length > 0 ? [...reasoningBlocks, toolUseBlock] : [toolUseBlock];
18870
20101
  messages.push({
18871
20102
  role: "assistant",
18872
20103
  content: assistantContent
@@ -18891,6 +20122,10 @@ function isReasoningBlock(block) {
18891
20122
  return "reasoningContent" in block;
18892
20123
  }
18893
20124
  var DeepSeekBedrockBackend = class extends BaseBedrockBackend {
20125
+ /** Reports done only on event.messageStop, so a missing terminal event means a truncated stream. */
20126
+ get signalsStreamTermination() {
20127
+ return true;
20128
+ }
18894
20129
  /** Suppresses reasoning/thinking output for summary and title generation calls. */
18895
20130
  isSpecialTask = false;
18896
20131
  /** Tracks whether the stream is currently inside a reasoning span, to emit one <think>/</think> pair per span. */
@@ -19235,6 +20470,10 @@ var JurassicTwoBedrockBackend = class extends BaseBedrockBackend {
19235
20470
  }
19236
20471
  };
19237
20472
  var LlamaBedrockBackend = class extends BaseBedrockBackend {
20473
+ /** Reports done only on response.stop_reason on the terminal chunk, so a missing terminal event means a truncated stream. */
20474
+ get signalsStreamTermination() {
20475
+ return true;
20476
+ }
19238
20477
  async getModelInfo() {
19239
20478
  return [
19240
20479
  {
@@ -19998,7 +21237,7 @@ var BFLBackend = class {
19998
21237
  input: 400,
19999
21238
  output: 400
20000
21239
  } },
20001
- description: "FLUX Kontext Pro - Advanced image-to-image transformation with high-quality output and versatile editing capabilities.",
21240
+ description: "FLUX Pro 1.1 - Professional quality image generation with strong prompt adherence and fast turnaround.",
20002
21241
  supportsSafetyTolerance: true,
20003
21242
  rank: 1
20004
21243
  },
@@ -20064,7 +21303,7 @@ var BFLBackend = class {
20064
21303
  input: 350,
20065
21304
  output: 350
20066
21305
  } },
20067
- description: "FLUX Kontext Max - Premium image-to-image transformation with maximum quality and advanced editing capabilities.",
21306
+ description: "FLUX Kontext Pro - Advanced image-to-image transformation with high-quality output and versatile editing capabilities.",
20068
21307
  supportsSafetyTolerance: true,
20069
21308
  rank: 4
20070
21309
  },
@@ -20080,7 +21319,7 @@ var BFLBackend = class {
20080
21319
  input: 450,
20081
21320
  output: 450
20082
21321
  } },
20083
- description: "BlackForest Labs FLUX Kontext Max - Premium image-to-image transformation with maximum quality and capabilities",
21322
+ description: "FLUX Kontext Max - Premium image-to-image transformation with maximum quality and capabilities.",
20084
21323
  supportsSafetyTolerance: true,
20085
21324
  rank: 5
20086
21325
  }
@@ -20380,7 +21619,7 @@ var GeminiBackend = class {
20380
21619
  rank: 8,
20381
21620
  trainingCutoff: "2025-01-31",
20382
21621
  releaseDate: "2025-06-01",
20383
- deprecationDate: "2026-10-16",
21622
+ deprecationDate: "2026-09-02",
20384
21623
  description: "Google's Gemini 2.5 Flash, offering well-rounded price-performance. Best for large scale processing, low-latency, high volume tasks that require thinking, and agentic use cases"
20385
21624
  },
20386
21625
  {
@@ -20538,6 +21777,7 @@ var GeminiBackend = class {
20538
21777
  const toolCallCount = options._internal?.toolCallCount ?? 0;
20539
21778
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
20540
21779
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
21780
+ const liveToolUseIds = new Set(options._internal?.liveToolUseIds ?? []);
20541
21781
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
20542
21782
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
20543
21783
  this.logger.warn(`[Gemini] Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -20552,7 +21792,7 @@ var GeminiBackend = class {
20552
21792
  const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
20553
21793
  const systemInstruction = messagesWithFormat.filter((message) => message.role === "system").map((message) => message.content).join("\n");
20554
21794
  const nonsystemMessages = messagesWithFormat.filter((message) => message.role !== "system");
20555
- const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages);
21795
+ const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages, liveToolUseIds);
20556
21796
  const generationConfig = this.getGenerationConfig(modelInfo, options);
20557
21797
  const tools = !options.tools?.length ? void 0 : [{ functionDeclarations: options.tools.map((tool) => {
20558
21798
  const params = pick(tool.toolSchema.parameters, "type", "properties", "required");
@@ -20679,22 +21919,32 @@ var GeminiBackend = class {
20679
21919
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20680
21920
  await callback(results, { toolsUsed });
20681
21921
  });
21922
+ const resultContent = JSON.stringify({ result: outcome.result });
21923
+ recordToolResult(toolsUsed, {
21924
+ id: outcome.toolCall.id,
21925
+ name: outcome.toolCall.name
21926
+ }, resultContent, true);
20682
21927
  messages.push({
20683
21928
  role: "tool",
20684
21929
  content: [{
20685
21930
  type: "tool_result",
20686
- content: JSON.stringify({ result: outcome.result }),
21931
+ content: resultContent,
20687
21932
  tool_use_id: outcome.toolCall.id
20688
21933
  }]
20689
21934
  });
20690
21935
  } else {
20691
21936
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20692
21937
  this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
21938
+ const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
21939
+ recordToolResult(toolsUsed, {
21940
+ id: outcome.toolCall.id,
21941
+ name: outcome.toolCall.name
21942
+ }, errorContent, false);
20693
21943
  messages.push({
20694
21944
  role: "tool",
20695
21945
  content: [{
20696
21946
  type: "tool_result",
20697
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
21947
+ content: errorContent,
20698
21948
  tool_use_id: outcome.toolCall.id
20699
21949
  }]
20700
21950
  });
@@ -20706,7 +21956,8 @@ var GeminiBackend = class {
20706
21956
  ...options._internal,
20707
21957
  toolCallCount: toolCallCount + 1,
20708
21958
  accumInputTokens: accumInputTokens + turnInputTokens,
20709
- accumOutputTokens: accumOutputTokens + turnOutputTokens
21959
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
21960
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20710
21961
  }
20711
21962
  }, callback, toolsUsed);
20712
21963
  } else {
@@ -20805,22 +22056,32 @@ var GeminiBackend = class {
20805
22056
  await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
20806
22057
  await callback(results, { toolsUsed });
20807
22058
  });
22059
+ const resultContent = JSON.stringify({ result: outcome.result });
22060
+ recordToolResult(toolsUsed, {
22061
+ id: outcome.toolCall.id,
22062
+ name: outcome.toolCall.name
22063
+ }, resultContent, true);
20808
22064
  messages.push({
20809
22065
  role: "tool",
20810
22066
  content: [{
20811
22067
  type: "tool_result",
20812
- content: JSON.stringify({ result: outcome.result }),
22068
+ content: resultContent,
20813
22069
  tool_use_id: outcome.toolCall.id
20814
22070
  }]
20815
22071
  });
20816
22072
  } else {
20817
22073
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
20818
22074
  this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
22075
+ const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
22076
+ recordToolResult(toolsUsed, {
22077
+ id: outcome.toolCall.id,
22078
+ name: outcome.toolCall.name
22079
+ }, errorContent, false);
20819
22080
  messages.push({
20820
22081
  role: "tool",
20821
22082
  content: [{
20822
22083
  type: "tool_result",
20823
- content: JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" }),
22084
+ content: errorContent,
20824
22085
  tool_use_id: outcome.toolCall.id
20825
22086
  }]
20826
22087
  });
@@ -20832,7 +22093,8 @@ var GeminiBackend = class {
20832
22093
  ...options._internal,
20833
22094
  toolCallCount: toolCallCount + 1,
20834
22095
  accumInputTokens: accumInputTokens + turnInputTokens,
20835
- accumOutputTokens: accumOutputTokens + turnOutputTokens
22096
+ accumOutputTokens: accumOutputTokens + turnOutputTokens,
22097
+ liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
20836
22098
  }
20837
22099
  }, callback, toolsUsed);
20838
22100
  } else {
@@ -20841,8 +22103,9 @@ var GeminiBackend = class {
20841
22103
  }
20842
22104
  }
20843
22105
  }
20844
- formatMessagesIntoGeminiContent(messages) {
22106
+ formatMessagesIntoGeminiContent(messages, liveToolUseIds) {
20845
22107
  const toolUseIdToName = /* @__PURE__ */ new Map();
22108
+ const droppedToolUseIds = /* @__PURE__ */ new Set();
20846
22109
  return messages.map((message) => {
20847
22110
  const mapRole = (role) => {
20848
22111
  switch (role) {
@@ -20856,25 +22119,41 @@ var GeminiBackend = class {
20856
22119
  role: mapRole(message.role),
20857
22120
  parts: [{ text: message.content }]
20858
22121
  };
20859
- if (message.content?.[0].type === "text") return {
22122
+ const hasToolUse = Array.isArray(message.content) && message.content.some((item) => item.type === "tool_use");
22123
+ if (!hasToolUse && message.content?.[0].type === "text") return {
20860
22124
  role: mapRole(message.role),
20861
22125
  parts: [{ text: (message.content?.[0]).text }]
20862
22126
  };
20863
- if (message.content?.[0].type === "image") return {
22127
+ if (!hasToolUse && message.content?.[0].type === "image") return {
20864
22128
  role: mapRole(message.role),
20865
22129
  parts: [{ inlineData: {
20866
22130
  mimeType: (message.content?.[0]).source.media_type,
20867
22131
  data: (message.content?.[0]).source.data
20868
22132
  } }]
20869
22133
  };
20870
- if (message.content?.[0].type === "tool_use") {
20871
- const parts = message.content.filter((item) => item.type === "tool_use").map((toolUse, index) => {
22134
+ if (hasToolUse) {
22135
+ const toolUseBlocks = message.content.filter((item) => item.type === "tool_use");
22136
+ const textParts = message.content.filter((item) => item.type === "text").map((item) => ({ text: item.text }));
22137
+ if (!toolUseBlocks.some((t) => liveToolUseIds.has(t.id)) && /^gemini-3(\D|$)/.test(this.currentModel) && !toolUseBlocks[0]?.thought_signature) {
22138
+ this.logger.warn("[Gemini] Dropping replayed tool_use block(s) with no thought_signature on the first call:", {
22139
+ names: toolUseBlocks.map((t) => t.name),
22140
+ messageRole: message.role
22141
+ });
22142
+ toolUseBlocks.forEach((t) => droppedToolUseIds.add(t.id));
22143
+ if (textParts.length === 0) return null;
22144
+ return {
22145
+ role: mapRole(message.role),
22146
+ parts: textParts
22147
+ };
22148
+ }
22149
+ const parts = [...textParts];
22150
+ parts.push(...toolUseBlocks.map((toolUse, index) => {
20872
22151
  toolUseIdToName.set(toolUse.id, toolUse.name);
20873
22152
  const part = { functionCall: {
20874
22153
  name: toolUse.name,
20875
22154
  args: toolUse.input
20876
22155
  } };
20877
- if (index === 0 && toolUse.thought_signature) {
22156
+ if (index === 0) if (toolUse.thought_signature) {
20878
22157
  part.thoughtSignature = toolUse.thought_signature;
20879
22158
  part.thought_signature = toolUse.thought_signature;
20880
22159
  this.logger.debug("[Gemini] Including thought_signature in request (both formats):", {
@@ -20882,29 +22161,26 @@ var GeminiBackend = class {
20882
22161
  id: toolUse.id,
20883
22162
  position: "first"
20884
22163
  });
20885
- } else if (index === 0 && !toolUse.thought_signature) {
20886
- this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
20887
- name: toolUse.name,
20888
- id: toolUse.id,
20889
- messageRole: message.role
20890
- });
20891
- this.logger.warn("[Gemini] This may cause a 400 error with Gemini 3 Pro");
20892
- }
22164
+ } else this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
22165
+ name: toolUse.name,
22166
+ id: toolUse.id
22167
+ });
20893
22168
  return part;
20894
- });
22169
+ }));
20895
22170
  return {
20896
22171
  role: mapRole(message.role),
20897
22172
  parts
20898
22173
  };
20899
22174
  }
20900
22175
  if (message.content?.[0].type === "tool_result") {
20901
- const toolResult = message.content[0];
22176
+ const parts = message.content.filter((item) => item.type === "tool_result").filter((toolResult) => !droppedToolUseIds.has(toolResult.tool_use_id)).map((toolResult) => ({ functionResponse: {
22177
+ name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
22178
+ response: { result: toolResult.content }
22179
+ } }));
22180
+ if (parts.length === 0) return null;
20902
22181
  return {
20903
22182
  role: mapRole(message.role),
20904
- parts: [{ functionResponse: {
20905
- name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
20906
- response: { result: (message.content?.[0]).content }
20907
- } }]
22183
+ parts
20908
22184
  };
20909
22185
  }
20910
22186
  return null;
@@ -20991,6 +22267,54 @@ var GeminiBackend = class {
20991
22267
  }
20992
22268
  };
20993
22269
  /**
22270
+ * The cache-inclusive-to-cache-exclusive conversion, shared by every adapter whose
22271
+ * provider reports cached tokens as a SUBSET of the prompt count.
22272
+ *
22273
+ * getTextModelCost expects Anthropic's convention: `inputTokens` counts only uncached
22274
+ * tokens and cache reads bill separately at their own (much cheaper) rate. Anthropic
22275
+ * and Claude-on-Bedrock deliver that natively. OpenAI and Moonshot do not - their
22276
+ * prompt total already CONTAINS the cached tokens - so those adapters must subtract
22277
+ * here before forwarding, or settlement double-bills the cached portion.
22278
+ *
22279
+ * Must stay in sync with the disjoint-fields assumption documented at the settlement
22280
+ * site in ChatCompletionProcess.
22281
+ */
22282
+ /**
22283
+ * Split a cache-INCLUSIVE prompt total into the disjoint pair CompletionInfo carries.
22284
+ *
22285
+ * Forwarding the cached count without subtracting double-bills it; forwarding nothing
22286
+ * charges the full input rate on tokens the provider billed at a fraction of it.
22287
+ * Subtracting is the only split that bills what the provider actually charged.
22288
+ *
22289
+ * Clamped at zero: if a feed ever reports more cached than prompt tokens, a negative
22290
+ * input count would silently credit the user.
22291
+ */
22292
+ function splitCacheInclusiveInput(totalPromptTokens, cacheReadTokens) {
22293
+ if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
22294
+ const cached = Math.min(cacheReadTokens, totalPromptTokens);
22295
+ return {
22296
+ inputTokens: Math.max(0, totalPromptTokens - cached),
22297
+ cacheReadInputTokens: cached
22298
+ };
22299
+ }
22300
+ /**
22301
+ * Cached prompt tokens from a raw provider usage object, across every spelling in use:
22302
+ * OpenAI Chat Completions nests them under `prompt_tokens_details`, the OpenAI
22303
+ * Responses API under `input_tokens_details`, and Moonshot publishes a flat
22304
+ * `cached_tokens` alongside the OpenAI-shaped nesting. Reading only one spelling
22305
+ * silently bills every cache hit on the other transports at the full input rate.
22306
+ */
22307
+ function cachedTokensFromUsage(usage) {
22308
+ if (!usage) return 0;
22309
+ const candidates = [
22310
+ usage.cached_tokens,
22311
+ usage.prompt_tokens_details?.cached_tokens,
22312
+ usage.input_tokens_details?.cached_tokens
22313
+ ];
22314
+ for (const value of candidates) if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
22315
+ return 0;
22316
+ }
22317
+ /**
20994
22318
  * Request shaping for Moonshot's Kimi models. Kept separate from kimiBackend's
20995
22319
  * transport so every "which parameter does this id accept" rule is one pure
20996
22320
  * function with a test, rather than a conditional buried in a 400-line complete().
@@ -21379,7 +22703,7 @@ var KimiBackend = class {
21379
22703
  if (!(response instanceof Stream)) {
21380
22704
  const streamedText = [];
21381
22705
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from the Moonshot API");
21382
- const turnCacheReadTokens = this.cachedTokensOf(response.usage);
22706
+ const turnCacheReadTokens = cachedTokensFromUsage(response.usage);
21383
22707
  for (const c of response.choices) {
21384
22708
  if (!c.message) continue;
21385
22709
  const reasoningContent = c.message.reasoning_content;
@@ -21411,6 +22735,10 @@ var KimiBackend = class {
21411
22735
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
21412
22736
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
21413
22737
  if (entry) entry.arguments = "{}";
22738
+ recordToolResult(toolsUsed, {
22739
+ id: toolCall.id,
22740
+ name: toolCall.function.name
22741
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
21414
22742
  }
21415
22743
  }
21416
22744
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21438,18 +22766,30 @@ var KimiBackend = class {
21438
22766
  parameters: resolvedTools[i].parameters,
21439
22767
  error: outcome.error
21440
22768
  });
21441
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21442
- id: outcome.id,
21443
- name: outcome.name,
21444
- parameters: outcome.parameters
21445
- }, outcome.result.toString());
21446
- else {
22769
+ for (const outcome of outcomes) if (outcome.ok) {
22770
+ const resultStr = outcome.result.toString();
22771
+ recordToolResult(toolsUsed, {
22772
+ id: outcome.id,
22773
+ name: outcome.name
22774
+ }, resultStr, true);
22775
+ this.pushToolMessages(messages, {
22776
+ id: outcome.id,
22777
+ name: outcome.name,
22778
+ parameters: outcome.parameters
22779
+ }, resultStr);
22780
+ } else {
21447
22781
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22782
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
22783
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
22784
+ recordToolResult(toolsUsed, {
22785
+ id: outcome.id,
22786
+ name: outcome.name
22787
+ }, observation, false);
21448
22788
  this.pushToolMessages(messages, {
21449
22789
  id: outcome.id,
21450
22790
  name: outcome.name,
21451
22791
  parameters: outcome.parameters
21452
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22792
+ }, observation);
21453
22793
  }
21454
22794
  await this.complete(model, messages, {
21455
22795
  ...options,
@@ -21465,7 +22805,7 @@ var KimiBackend = class {
21465
22805
  } else {
21466
22806
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21467
22807
  await callback([null], {
21468
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
22808
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
21469
22809
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21470
22810
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21471
22811
  });
@@ -21488,7 +22828,7 @@ var KimiBackend = class {
21488
22828
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
21489
22829
  const totalCacheReadTokens = accumCacheReadTokens + turnCacheReadTokens;
21490
22830
  await callback(streamedText, {
21491
- ...this.splitCachedInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
22831
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
21492
22832
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
21493
22833
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21494
22834
  cacheStats,
@@ -21507,7 +22847,7 @@ var KimiBackend = class {
21507
22847
  if (chunk.usage) {
21508
22848
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
21509
22849
  outputTokens += chunk.usage?.completion_tokens || 0;
21510
- const chunkCached = this.cachedTokensOf(chunk.usage);
22850
+ const chunkCached = cachedTokensFromUsage(chunk.usage);
21511
22851
  if (chunkCached > 0) cachedTokensFromStream = chunkCached;
21512
22852
  }
21513
22853
  chunk?.choices.forEach((c) => {
@@ -21537,7 +22877,7 @@ var KimiBackend = class {
21537
22877
  if (streamedText.some((t) => t)) sawAnyText = true;
21538
22878
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
21539
22879
  await callback(streamedText, {
21540
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22880
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21541
22881
  outputTokens: accumOutputTokens + outputTokens,
21542
22882
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21543
22883
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -21545,7 +22885,7 @@ var KimiBackend = class {
21545
22885
  }
21546
22886
  if (isInThinkingBlock) {
21547
22887
  await callback(["</think>"], {
21548
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22888
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21549
22889
  outputTokens: accumOutputTokens + outputTokens,
21550
22890
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21551
22891
  });
@@ -21562,7 +22902,7 @@ var KimiBackend = class {
21562
22902
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
21563
22903
  }
21564
22904
  if (nativeFormat && func.length === 0) await callback([], {
21565
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
22905
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21566
22906
  outputTokens: accumOutputTokens + outputTokens,
21567
22907
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
21568
22908
  responseFormatMode: "native",
@@ -21598,6 +22938,10 @@ var KimiBackend = class {
21598
22938
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
21599
22939
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
21600
22940
  if (entry) entry.arguments = "{}";
22941
+ recordToolResult(toolsUsed, {
22942
+ id,
22943
+ name
22944
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
21601
22945
  }
21602
22946
  }
21603
22947
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -21625,18 +22969,30 @@ var KimiBackend = class {
21625
22969
  parameters: resolvedTools[i].parameters,
21626
22970
  error: outcome.error
21627
22971
  });
21628
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
21629
- id: outcome.id,
21630
- name: outcome.name,
21631
- parameters: outcome.parameters
21632
- }, outcome.result.toString());
21633
- else {
22972
+ for (const outcome of outcomes) if (outcome.ok) {
22973
+ const resultStr = outcome.result.toString();
22974
+ recordToolResult(toolsUsed, {
22975
+ id: outcome.id,
22976
+ name: outcome.name
22977
+ }, resultStr, true);
22978
+ this.pushToolMessages(messages, {
22979
+ id: outcome.id,
22980
+ name: outcome.name,
22981
+ parameters: outcome.parameters
22982
+ }, resultStr);
22983
+ } else {
21634
22984
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22985
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
22986
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
22987
+ recordToolResult(toolsUsed, {
22988
+ id: outcome.id,
22989
+ name: outcome.name
22990
+ }, observation, false);
21635
22991
  this.pushToolMessages(messages, {
21636
22992
  id: outcome.id,
21637
22993
  name: outcome.name,
21638
22994
  parameters: outcome.parameters
21639
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
22995
+ }, observation);
21640
22996
  }
21641
22997
  await this.complete(model, messages, {
21642
22998
  ...options,
@@ -21651,52 +23007,13 @@ var KimiBackend = class {
21651
23007
  } else {
21652
23008
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
21653
23009
  await callback([null], {
21654
- ...this.splitCachedInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23010
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
21655
23011
  outputTokens: accumOutputTokens + outputTokens,
21656
23012
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
21657
23013
  });
21658
23014
  }
21659
23015
  }
21660
23016
  }
21661
- /**
21662
- * Convert Moonshot's CACHE-INCLUSIVE `prompt_tokens` into the cache-EXCLUSIVE
21663
- * convention getTextModelCost expects, which is Anthropic's: `inputTokens`
21664
- * counts only uncached tokens and cache reads are billed separately at their
21665
- * own (much cheaper) rate.
21666
- *
21667
- * Verified live 2026-07-28: a repeated 1220-token prompt returned
21668
- * `prompt_tokens: 1220` WITH `cached_tokens: 1220` - the same tokens, reported
21669
- * twice, not 1220 fresh plus 1220 cached. So passing prompt_tokens through as
21670
- * inputTokens while also forwarding cacheReadInputTokens would bill the cached
21671
- * portion twice; passing it through and forwarding NOTHING (the previous
21672
- * behavior) charges the full input rate on tokens Moonshot billed at roughly a
21673
- * sixth of it - on k2.6, $0.95/MTok against $0.16. Subtracting is the only
21674
- * split that bills what the provider actually charged.
21675
- *
21676
- * Clamped at zero: if a feed ever reports more cached than prompt tokens, a
21677
- * negative input count would silently credit the user.
21678
- */
21679
- splitCachedInput(totalPromptTokens, cacheReadTokens) {
21680
- if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
21681
- const cached = Math.min(cacheReadTokens, totalPromptTokens);
21682
- return {
21683
- inputTokens: Math.max(0, totalPromptTokens - cached),
21684
- cacheReadInputTokens: cached
21685
- };
21686
- }
21687
- /**
21688
- * Cached prompt tokens from a usage object. Moonshot publishes BOTH a flat
21689
- * `usage.cached_tokens` and the nested OpenAI `prompt_tokens_details.cached_tokens`
21690
- * - confirmed live, both present with the same value - so either spelling is
21691
- * accepted and reading neither would bill every hit at the full input rate.
21692
- */
21693
- cachedTokensOf(usage) {
21694
- if (!usage) return 0;
21695
- const flat = usage.cached_tokens;
21696
- if (typeof flat === "number" && Number.isFinite(flat) && flat >= 0) return flat;
21697
- const nested = usage.prompt_tokens_details?.cached_tokens;
21698
- return typeof nested === "number" && Number.isFinite(nested) && nested >= 0 ? nested : 0;
21699
- }
21700
23017
  formatMessages(messages) {
21701
23018
  return convertMessagesToOpenAIFormat(messages);
21702
23019
  }
@@ -22001,7 +23318,7 @@ var OllamaBackend = class OllamaBackend {
22001
23318
  name: tc.name,
22002
23319
  parameters: tc.arguments || "{}"
22003
23320
  }, `Error: tool "${tc.name}" is not available. Do not call it again; answer directly or use a listed tool.`);
22004
- (await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
23321
+ const outcomes = await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
22005
23322
  let params = {};
22006
23323
  try {
22007
23324
  params = JSON.parse(tc.arguments || "{}");
@@ -22011,17 +23328,22 @@ var OllamaBackend = class OllamaBackend {
22011
23328
  }), {
22012
23329
  parallel: options.parallelToolExecution !== false,
22013
23330
  maxConcurrency: options.maxParallelTools
22014
- })).forEach((outcome, i) => {
23331
+ });
23332
+ const observations = [];
23333
+ outcomes.forEach((outcome, i) => {
22015
23334
  const { tc } = resolved[i];
22016
23335
  const params = tc.arguments || "{}";
22017
- if (outcome.ok) this.pushToolMessages(messages, {
22018
- id: tc.id,
22019
- name: tc.name,
22020
- parameters: params
22021
- }, outcome.result);
22022
- else {
23336
+ if (outcome.ok) {
23337
+ observations[i] = outcome.result;
23338
+ this.pushToolMessages(messages, {
23339
+ id: tc.id,
23340
+ name: tc.name,
23341
+ parameters: params
23342
+ }, outcome.result);
23343
+ } else {
22023
23344
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
22024
23345
  const errorMsg = `Error running ${tc.name}: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
23346
+ observations[i] = errorMsg;
22025
23347
  this.pushToolMessages(messages, {
22026
23348
  id: tc.id,
22027
23349
  name: tc.name,
@@ -22029,10 +23351,12 @@ var OllamaBackend = class OllamaBackend {
22029
23351
  }, errorMsg);
22030
23352
  }
22031
23353
  });
22032
- const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }) => ({
23354
+ const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }, i) => ({
22033
23355
  name: tc.name,
22034
23356
  arguments: tc.arguments,
22035
- id: tc.id
23357
+ id: tc.id,
23358
+ returnValue: truncateToolResult(String(observations[i])),
23359
+ success: outcomes[i].ok
22036
23360
  }))];
22037
23361
  if (options.abortSignal?.aborted) {
22038
23362
  await callback([""], {
@@ -22140,12 +23464,21 @@ var OllamaBackend = class OllamaBackend {
22140
23464
  }
22141
23465
  };
22142
23466
  }
22143
- /** Normalize Ollama's native tool_calls into the shared NormalizedToolCall shape. */
23467
+ /**
23468
+ * Normalize Ollama's native tool_calls into the shared NormalizedToolCall shape.
23469
+ *
23470
+ * Ids are real uuids, not a position-derived string. A prior version keyed ids off
23471
+ * `accumulated-count + round-local-index`, but the accumulated count is measured AFTER
23472
+ * hallucinated calls are filtered out while the round-local index is assigned BEFORE that
23473
+ * filter runs, so the two can drift and mint the same id for two different real calls across
23474
+ * rounds - replayableToolCalls dedupes by id and silently drops the later one. A uuid makes
23475
+ * the whole collision class unrepresentable, matching how the other backends already mint ids.
23476
+ */
22144
23477
  normalizeToolCalls(toolCalls) {
22145
- return toolCalls.map((tc, i) => ({
23478
+ return toolCalls.map((tc) => ({
22146
23479
  name: tc.function.name,
22147
23480
  arguments: JSON.stringify(tc.function.arguments ?? {}),
22148
- id: `ollama-tool-${i}-${tc.function.name}`
23481
+ id: `ollama-tool-${v4()}`
22149
23482
  }));
22150
23483
  }
22151
23484
  /**
@@ -22183,7 +23516,7 @@ var OllamaBackend = class OllamaBackend {
22183
23516
  seen.add(key);
22184
23517
  calls.push({
22185
23518
  ...call,
22186
- id: `ollama-content-tool-${calls.length}-${call.name}`
23519
+ id: `ollama-content-tool-${v4()}`
22187
23520
  });
22188
23521
  }
22189
23522
  return calls;
@@ -22432,7 +23765,8 @@ var OpenAIBackend = class {
22432
23765
  can_stream: true,
22433
23766
  pricing: { 1047576: {
22434
23767
  input: 2 / 1e6,
22435
- output: 8 / 1e6
23768
+ output: 8 / 1e6,
23769
+ cache_read: .5 / 1e6
22436
23770
  } },
22437
23771
  supportsVision: true,
22438
23772
  supportsTools: true,
@@ -22452,7 +23786,8 @@ var OpenAIBackend = class {
22452
23786
  can_stream: true,
22453
23787
  pricing: { 1047576: {
22454
23788
  input: .4 / 1e6,
22455
- output: 1.6 / 1e6
23789
+ output: 1.6 / 1e6,
23790
+ cache_read: .1 / 1e6
22456
23791
  } },
22457
23792
  supportsVision: true,
22458
23793
  supportsTools: true,
@@ -22472,7 +23807,8 @@ var OpenAIBackend = class {
22472
23807
  can_stream: true,
22473
23808
  pricing: { 1047576: {
22474
23809
  input: .1 / 1e6,
22475
- output: .4 / 1e6
23810
+ output: .4 / 1e6,
23811
+ cache_read: .025 / 1e6
22476
23812
  } },
22477
23813
  supportsVision: true,
22478
23814
  supportsTools: true,
@@ -22535,7 +23871,8 @@ var OpenAIBackend = class {
22535
23871
  can_stream: true,
22536
23872
  pricing: { 2e5: {
22537
23873
  input: 2 / 1e6,
22538
- output: 8 / 1e6
23874
+ output: 8 / 1e6,
23875
+ cache_read: .5 / 1e6
22539
23876
  } },
22540
23877
  supportsVision: true,
22541
23878
  supportsTools: true,
@@ -22621,7 +23958,8 @@ var OpenAIBackend = class {
22621
23958
  can_think: true,
22622
23959
  pricing: { 2e5: {
22623
23960
  input: 1.1 / 1e6,
22624
- output: 4.4 / 1e6
23961
+ output: 4.4 / 1e6,
23962
+ cache_read: .275 / 1e6
22625
23963
  } },
22626
23964
  supportsVision: true,
22627
23965
  supportsImageVariation: false,
@@ -22997,7 +24335,8 @@ var OpenAIBackend = class {
22997
24335
  can_stream: true,
22998
24336
  pricing: { 8e3: {
22999
24337
  input: 2.5 / 1e6,
23000
- output: 10 / 1e6
24338
+ output: 10 / 1e6,
24339
+ cache_read: 1.25 / 1e6
23001
24340
  } },
23002
24341
  supportsVision: true,
23003
24342
  supportsImageVariation: false,
@@ -23016,7 +24355,8 @@ var OpenAIBackend = class {
23016
24355
  can_stream: true,
23017
24356
  pricing: { 8e3: {
23018
24357
  input: .15 / 1e6,
23019
- output: .6 / 1e6
24358
+ output: .6 / 1e6,
24359
+ cache_read: .075 / 1e6
23020
24360
  } },
23021
24361
  supportsVision: true,
23022
24362
  supportsImageVariation: false,
@@ -23207,6 +24547,7 @@ var OpenAIBackend = class {
23207
24547
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23208
24548
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23209
24549
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
24550
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23210
24551
  const maxToolCalls = options._internal?.maxToolCalls ?? 10;
23211
24552
  if (toolCallCount >= maxToolCalls && options.tools?.length) {
23212
24553
  this.logger.warn(`⚠️ Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
@@ -23296,6 +24637,7 @@ var OpenAIBackend = class {
23296
24637
  let outputTokens = 0;
23297
24638
  if (!(response instanceof Stream)) {
23298
24639
  const streamedText = [];
24640
+ const totalCacheReadTokens = accumCacheReadTokens + cachedTokensFromUsage(response.usage);
23299
24641
  if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from OpenAI API");
23300
24642
  for (const c of response.choices) {
23301
24643
  if (!c.message) continue;
@@ -23329,6 +24671,10 @@ var OpenAIBackend = class {
23329
24671
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
23330
24672
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
23331
24673
  if (entry) entry.arguments = "{}";
24674
+ recordToolResult(toolsUsed, {
24675
+ id: toolCall.id,
24676
+ name: toolCall.function.name
24677
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23332
24678
  }
23333
24679
  }
23334
24680
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23365,6 +24711,10 @@ var OpenAIBackend = class {
23365
24711
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23366
24712
  const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
23367
24713
  streamedText[c.index] = errorMsg;
24714
+ recordToolResult(toolsUsed, {
24715
+ id: outcome.id,
24716
+ name: outcome.name
24717
+ }, errorMsg, false);
23368
24718
  this.pushToolMessages(messages, {
23369
24719
  id: outcome.id,
23370
24720
  name: outcome.name,
@@ -23385,6 +24735,10 @@ var OpenAIBackend = class {
23385
24735
  });
23386
24736
  });
23387
24737
  const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
24738
+ recordToolResult(toolsUsed, {
24739
+ id: outcome.id,
24740
+ name: outcome.name
24741
+ }, sanitizedResult, true);
23388
24742
  this.pushToolMessages(messages, {
23389
24743
  id: outcome.id,
23390
24744
  name: outcome.name,
@@ -23408,7 +24762,8 @@ var OpenAIBackend = class {
23408
24762
  ...options._internal,
23409
24763
  toolCallCount: toolCallCount + 1,
23410
24764
  accumInputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
23411
- accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0)
24765
+ accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
24766
+ accumCacheReadTokens: totalCacheReadTokens
23412
24767
  }
23413
24768
  }, recursiveCallback, toolsUsed);
23414
24769
  if (anyArtifactWasStreamed && recursiveBuffer) {
@@ -23419,7 +24774,7 @@ var OpenAIBackend = class {
23419
24774
  } else {
23420
24775
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23421
24776
  await callback([null], {
23422
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24777
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23423
24778
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23424
24779
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23425
24780
  });
@@ -23435,7 +24790,7 @@ var OpenAIBackend = class {
23435
24790
  }
23436
24791
  const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
23437
24792
  await callback(streamedText, {
23438
- inputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
24793
+ ...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
23439
24794
  outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
23440
24795
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23441
24796
  cacheStats,
@@ -23463,7 +24818,7 @@ var OpenAIBackend = class {
23463
24818
  inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
23464
24819
  outputTokens += chunk.usage?.completion_tokens || 0;
23465
24820
  if (chunk.usage.prompt_tokens_details?.cached_tokens !== void 0) {
23466
- cachedTokensFromStream = chunk.usage.prompt_tokens_details.cached_tokens;
24821
+ cachedTokensFromStream = Math.max(cachedTokensFromStream, chunk.usage.prompt_tokens_details.cached_tokens);
23467
24822
  if (cachedTokensFromStream > 0) this.logger.debug("[OpenAI] Captured cached tokens", { cachedTokens: cachedTokensFromStream });
23468
24823
  else this.logger.debug("[OpenAI] No cached tokens in chunk", { note: "possible cache miss or first request" });
23469
24824
  }
@@ -23480,7 +24835,7 @@ var OpenAIBackend = class {
23480
24835
  });
23481
24836
  const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
23482
24837
  await callback(streamedText, {
23483
- inputTokens: accumInputTokens + inputTokens,
24838
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23484
24839
  outputTokens: accumOutputTokens + outputTokens,
23485
24840
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23486
24841
  ...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
@@ -23510,7 +24865,7 @@ var OpenAIBackend = class {
23510
24865
  if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
23511
24866
  }
23512
24867
  if ((isO1Model || func.length === 0) && options.responseFormat?.type === "json_schema") await callback([], {
23513
- inputTokens: accumInputTokens + inputTokens,
24868
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23514
24869
  outputTokens: accumOutputTokens + outputTokens,
23515
24870
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23516
24871
  cacheStats,
@@ -23548,6 +24903,10 @@ var OpenAIBackend = class {
23548
24903
  });
23549
24904
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
23550
24905
  if (entry) entry.arguments = "{}";
24906
+ recordToolResult(toolsUsed, {
24907
+ id,
24908
+ name
24909
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23551
24910
  }
23552
24911
  }
23553
24912
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23582,11 +24941,16 @@ var OpenAIBackend = class {
23582
24941
  for (const outcome of outcomes) {
23583
24942
  if (!outcome.ok) {
23584
24943
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
24944
+ const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
24945
+ recordToolResult(toolsUsed, {
24946
+ id: outcome.id,
24947
+ name: outcome.name
24948
+ }, errorMsg, false);
23585
24949
  this.pushToolMessages(messages, {
23586
24950
  id: outcome.id,
23587
24951
  name: outcome.name,
23588
24952
  parameters: outcome.parameters
23589
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
24953
+ }, errorMsg);
23590
24954
  continue;
23591
24955
  }
23592
24956
  const resultStr = outcome.result.toString();
@@ -23596,13 +24960,17 @@ var OpenAIBackend = class {
23596
24960
  thisToolHadArtifact = true;
23597
24961
  anyArtifactWasStreamed = true;
23598
24962
  await callback(results, {
23599
- inputTokens: accumInputTokens + inputTokens,
24963
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23600
24964
  outputTokens: accumOutputTokens + outputTokens,
23601
24965
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23602
24966
  cacheStats
23603
24967
  });
23604
24968
  });
23605
24969
  const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
24970
+ recordToolResult(toolsUsed, {
24971
+ id: outcome.id,
24972
+ name: outcome.name
24973
+ }, sanitizedResult, true);
23606
24974
  this.pushToolMessages(messages, {
23607
24975
  id: outcome.id,
23608
24976
  name: outcome.name,
@@ -23623,7 +24991,8 @@ var OpenAIBackend = class {
23623
24991
  ...options._internal,
23624
24992
  toolCallCount: toolCallCount + 1,
23625
24993
  accumInputTokens: accumInputTokens + inputTokens,
23626
- accumOutputTokens: accumOutputTokens + outputTokens
24994
+ accumOutputTokens: accumOutputTokens + outputTokens,
24995
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23627
24996
  }
23628
24997
  }, async (results, meta) => {
23629
24998
  for (const r of results) if (r != null) recursiveBuffer += r;
@@ -23639,13 +25008,14 @@ var OpenAIBackend = class {
23639
25008
  ...options._internal,
23640
25009
  toolCallCount: toolCallCount + 1,
23641
25010
  accumInputTokens: accumInputTokens + inputTokens,
23642
- accumOutputTokens: accumOutputTokens + outputTokens
25011
+ accumOutputTokens: accumOutputTokens + outputTokens,
25012
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23643
25013
  }
23644
25014
  }, callback, toolsUsed);
23645
25015
  } else {
23646
25016
  this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
23647
25017
  await callback([null], {
23648
- inputTokens: accumInputTokens + inputTokens,
25018
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23649
25019
  outputTokens: accumOutputTokens + outputTokens,
23650
25020
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23651
25021
  cacheStats
@@ -23787,6 +25157,7 @@ var OpenAIBackend = class {
23787
25157
  const toolCallCount = options._internal?.toolCallCount ?? 0;
23788
25158
  const accumInputTokens = options._internal?.accumInputTokens ?? 0;
23789
25159
  const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
25160
+ const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
23790
25161
  const chatMessages = this.formatMessages(messages, false, model, options);
23791
25162
  const input = this.toResponsesInput(chatMessages);
23792
25163
  const reasoningEffort = this.resolveReasoningEffort(model, options);
@@ -23813,8 +25184,9 @@ var OpenAIBackend = class {
23813
25184
  let finalResponse;
23814
25185
  let inputTokens = 0;
23815
25186
  let outputTokens = 0;
25187
+ let cachedTokensFromStream = 0;
23816
25188
  for await (const event of stream) if (event.type === "response.output_text.delta") await callback([event.delta], {
23817
- inputTokens: accumInputTokens + inputTokens,
25189
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23818
25190
  outputTokens: accumOutputTokens + outputTokens,
23819
25191
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23820
25192
  });
@@ -23824,11 +25196,12 @@ var OpenAIBackend = class {
23824
25196
  if (!finalResponse) throw new Error(`OpenAI Responses stream for ${model} ended without a terminal response event`);
23825
25197
  inputTokens = finalResponse.usage?.input_tokens ?? 0;
23826
25198
  outputTokens = finalResponse.usage?.output_tokens ?? 0;
25199
+ cachedTokensFromStream = cachedTokensFromUsage(finalResponse.usage);
23827
25200
  const functionCalls = finalResponse.output.filter((item) => item.type === "function_call");
23828
25201
  if (functionCalls.length === 0) {
23829
25202
  const stopReason = normalizeOpenAIResponsesStopReason(finalResponse.incomplete_details?.reason);
23830
25203
  await callback([], {
23831
- inputTokens: accumInputTokens + inputTokens,
25204
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23832
25205
  outputTokens: accumOutputTokens + outputTokens,
23833
25206
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
23834
25207
  ...stopReason ? { stopReason } : {}
@@ -23842,7 +25215,7 @@ var OpenAIBackend = class {
23842
25215
  });
23843
25216
  if (options.executeTools === false) {
23844
25217
  await callback([null], {
23845
- inputTokens: accumInputTokens + inputTokens,
25218
+ ...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
23846
25219
  outputTokens: accumOutputTokens + outputTokens,
23847
25220
  toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
23848
25221
  });
@@ -23863,6 +25236,10 @@ var OpenAIBackend = class {
23863
25236
  });
23864
25237
  } catch {
23865
25238
  this.logger.warn(`JSON parse error for ${fc.name} arguments (Responses path)`);
25239
+ recordToolResult(toolsUsed, {
25240
+ id: fc.call_id,
25241
+ name: fc.name
25242
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
23866
25243
  }
23867
25244
  }
23868
25245
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -23886,14 +25263,24 @@ var OpenAIBackend = class {
23886
25263
  for (let i = 0; i < batchOutcomes.length; i++) {
23887
25264
  const outcome = batchOutcomes[i];
23888
25265
  const r = resolved[i];
23889
- if (outcome.ok) this.pushToolMessages(messages, {
23890
- id: r.callId,
23891
- name: r.name,
23892
- parameters: r.args
23893
- }, outcome.result.result.toString());
23894
- else {
25266
+ if (outcome.ok) {
25267
+ const resultStr = outcome.result.result.toString();
25268
+ recordToolResult(toolsUsed, {
25269
+ id: r.callId,
25270
+ name: r.name
25271
+ }, resultStr, true);
25272
+ this.pushToolMessages(messages, {
25273
+ id: r.callId,
25274
+ name: r.name,
25275
+ parameters: r.args
25276
+ }, resultStr);
25277
+ } else {
23895
25278
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
23896
25279
  const errorMsg = `Error processing ${r.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
25280
+ recordToolResult(toolsUsed, {
25281
+ id: r.callId,
25282
+ name: r.name
25283
+ }, errorMsg, false);
23897
25284
  this.pushToolMessages(messages, {
23898
25285
  id: r.callId,
23899
25286
  name: r.name,
@@ -23910,7 +25297,8 @@ var OpenAIBackend = class {
23910
25297
  ...options._internal,
23911
25298
  toolCallCount: toolCallCount + 1,
23912
25299
  accumInputTokens: accumInputTokens + inputTokens,
23913
- accumOutputTokens: accumOutputTokens + outputTokens
25300
+ accumOutputTokens: accumOutputTokens + outputTokens,
25301
+ accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
23914
25302
  }
23915
25303
  }, callback, toolsUsed);
23916
25304
  }
@@ -24268,6 +25656,10 @@ var XAIBackend = class {
24268
25656
  this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
24269
25657
  const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
24270
25658
  if (entry) entry.arguments = "{}";
25659
+ recordToolResult(toolsUsed, {
25660
+ id: toolCall.id,
25661
+ name: toolCall.function.name
25662
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
24271
25663
  }
24272
25664
  }
24273
25665
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24295,18 +25687,30 @@ var XAIBackend = class {
24295
25687
  parameters: resolvedTools[i].parameters,
24296
25688
  error: outcome.error
24297
25689
  });
24298
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24299
- id: outcome.id,
24300
- name: outcome.name,
24301
- parameters: outcome.parameters
24302
- }, outcome.result.toString());
24303
- else {
25690
+ for (const outcome of outcomes) if (outcome.ok) {
25691
+ const resultStr = outcome.result.toString();
25692
+ recordToolResult(toolsUsed, {
25693
+ id: outcome.id,
25694
+ name: outcome.name
25695
+ }, resultStr, true);
25696
+ this.pushToolMessages(messages, {
25697
+ id: outcome.id,
25698
+ name: outcome.name,
25699
+ parameters: outcome.parameters
25700
+ }, resultStr);
25701
+ } else {
24304
25702
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
25703
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
25704
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
25705
+ recordToolResult(toolsUsed, {
25706
+ id: outcome.id,
25707
+ name: outcome.name
25708
+ }, observation, false);
24305
25709
  this.pushToolMessages(messages, {
24306
25710
  id: outcome.id,
24307
25711
  name: outcome.name,
24308
25712
  parameters: outcome.parameters
24309
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25713
+ }, observation);
24310
25714
  }
24311
25715
  await this.complete(model, messages, {
24312
25716
  ...options,
@@ -24436,6 +25840,10 @@ var XAIBackend = class {
24436
25840
  this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
24437
25841
  const entry = toolsUsed.find((t) => t.name === name && t.id === id);
24438
25842
  if (entry) entry.arguments = "{}";
25843
+ recordToolResult(toolsUsed, {
25844
+ id,
25845
+ name
25846
+ }, "Error: Tool arguments were malformed and could not be parsed.", false);
24439
25847
  }
24440
25848
  }
24441
25849
  const parallelEnabled = options.parallelToolExecution !== false;
@@ -24463,18 +25871,30 @@ var XAIBackend = class {
24463
25871
  parameters: resolvedTools[i].parameters,
24464
25872
  error: outcome.error
24465
25873
  });
24466
- for (const outcome of outcomes) if (outcome.ok) this.pushToolMessages(messages, {
24467
- id: outcome.id,
24468
- name: outcome.name,
24469
- parameters: outcome.parameters
24470
- }, outcome.result.toString());
24471
- else {
25874
+ for (const outcome of outcomes) if (outcome.ok) {
25875
+ const resultStr = outcome.result.toString();
25876
+ recordToolResult(toolsUsed, {
25877
+ id: outcome.id,
25878
+ name: outcome.name
25879
+ }, resultStr, true);
25880
+ this.pushToolMessages(messages, {
25881
+ id: outcome.id,
25882
+ name: outcome.name,
25883
+ parameters: outcome.parameters
25884
+ }, resultStr);
25885
+ } else {
24472
25886
  if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
25887
+ const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
25888
+ const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
25889
+ recordToolResult(toolsUsed, {
25890
+ id: outcome.id,
25891
+ name: outcome.name
25892
+ }, observation, false);
24473
25893
  this.pushToolMessages(messages, {
24474
25894
  id: outcome.id,
24475
25895
  name: outcome.name,
24476
25896
  parameters: outcome.parameters
24477
- }, `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`);
25897
+ }, observation);
24478
25898
  }
24479
25899
  await this.complete(model, messages, {
24480
25900
  ...options,
@@ -25164,6 +26584,7 @@ const DEPRECATED_MODEL_MAP = {
25164
26584
  "claude-3-haiku-20240307": "claude-haiku-4-5-20251001",
25165
26585
  "gpt-5-chat-latest": "gpt-5.5",
25166
26586
  "gpt-5.1-chat-latest": "gpt-5.5",
26587
+ "gemini-2.5-flash": "gemini-3.1-flash-lite",
25167
26588
  "grok-3": "grok-4.5",
25168
26589
  "grok-3-fast": "grok-4.5",
25169
26590
  "grok-2-1212": "grok-4.5",
@@ -25635,6 +27056,10 @@ function checkStaleModelReferences(input) {
25635
27056
  for (const referencedId of chain) check("fallback-chain", chainKey, referencedId);
25636
27057
  }
25637
27058
  for (const referencedId of input.defaultChain ?? []) check("fallback-default", "default", referencedId);
27059
+ for (const [agentName, decl] of Object.entries(input.agentModels ?? {})) {
27060
+ if (decl.model) check("agent-model", agentName, decl.model);
27061
+ for (const referencedId of decl.fallbackModels ?? []) check("agent-fallback", agentName, referencedId);
27062
+ }
25638
27063
  for (const [source, target] of Object.entries(DEPRECATED_MODEL_MAP)) check("deprecated-model-map", source, target);
25639
27064
  for (const [source, target] of replacedByOverlayEntries()) check("replaced-by-overlay", source, target);
25640
27065
  return found.sort((a, b) => a.surface.localeCompare(b.surface) || a.key.localeCompare(b.key) || a.referencedId.localeCompare(b.referencedId));
@@ -26099,13 +27524,19 @@ const toolUseSchema = z$1.object({
26099
27524
  * Discriminated union of streaming events. `thinking` blocks are opaque
26100
27525
  * provider-shaped objects (Anthropic extended thinking) replayed verbatim into
26101
27526
  * the next request, so they stay `unknown[]` rather than being modeled.
27527
+ *
27528
+ * `stopReason` must be declared to survive: this schema strips unknown keys, so a
27529
+ * field absent here is dropped at the boundary no matter what the server sends.
27530
+ * 'max_tokens' is the one value with user-visible consequences - it means the reply
27531
+ * was cut off rather than finished.
26102
27532
  */
26103
27533
  const streamEventSchema = z$1.discriminatedUnion("type", [
26104
27534
  z$1.object({
26105
27535
  type: z$1.literal("content"),
26106
27536
  text: z$1.string().optional(),
26107
27537
  usage: usageSchema.optional(),
26108
- credits: creditsSchema.optional()
27538
+ credits: creditsSchema.optional(),
27539
+ stopReason: z$1.string().optional()
26109
27540
  }),
26110
27541
  z$1.object({
26111
27542
  type: z$1.literal("tool_use"),
@@ -26113,7 +27544,8 @@ const streamEventSchema = z$1.discriminatedUnion("type", [
26113
27544
  tools: z$1.array(toolUseSchema).optional(),
26114
27545
  thinking: z$1.array(z$1.unknown()).optional(),
26115
27546
  usage: usageSchema.optional(),
26116
- credits: creditsSchema.optional()
27547
+ credits: creditsSchema.optional(),
27548
+ stopReason: z$1.string().optional()
26117
27549
  }),
26118
27550
  z$1.object({
26119
27551
  type: z$1.literal("error"),
@@ -26182,12 +27614,14 @@ var StreamAccumulator = class {
26182
27614
  case "content":
26183
27615
  this.accumulatedText += event.text ?? "";
26184
27616
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27617
+ if (event.stopReason) this.stopReason = event.stopReason;
26185
27618
  break;
26186
27619
  case "tool_use":
26187
27620
  if (event.text) this.accumulatedText += event.text;
26188
27621
  if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
26189
27622
  if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
26190
27623
  if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
27624
+ if (event.stopReason) this.stopReason = event.stopReason;
26191
27625
  }
26192
27626
  }
26193
27627
  /** True when neither text nor tools have been accumulated (stream produced nothing useful). */
@@ -26214,10 +27648,14 @@ var StreamAccumulator = class {
26214
27648
  const info = {
26215
27649
  toolsUsed: this.toolsUsed,
26216
27650
  thinking: this.thinkingBlocks.length > 0 ? this.thinkingBlocks : void 0,
26217
- ...this.lastUsageInfo
27651
+ ...this.lastUsageInfo,
27652
+ stopReason: this.stopReason
26218
27653
  };
26219
27654
  await callback([cleanedText], info);
26220
- } else if (cleanedText) await callback([cleanedText], this.lastUsageInfo);
27655
+ } else if (cleanedText) await callback([cleanedText], {
27656
+ ...this.lastUsageInfo,
27657
+ stopReason: this.stopReason
27658
+ });
26221
27659
  }
26222
27660
  };
26223
27661
  //#endregion
@@ -26720,6 +28158,109 @@ async function createSseBackend(input, deps = defaultSseTransportDeps) {
26720
28158
  }
26721
28159
  //#endregion
26722
28160
  //#region ../../b4m-core/mcp/dist/index.mjs
28161
+ /**
28162
+ * Environment construction for the MCP stdio child process.
28163
+ *
28164
+ * The child is spawned from a process that also holds platform credentials - provider API keys,
28165
+ * database URIs, signing secrets - so what it inherits is a trust decision, not a convenience.
28166
+ * Two rules follow:
28167
+ *
28168
+ * 1. The child environment is built from an allowlist, never spread from `process.env`. The base
28169
+ * layer is the MCP SDK's own `getDefaultEnvironment()`, which the stdio transport merges
28170
+ * underneath whatever we pass (PATH, HOME, SHELL, TERM, USER on POSIX; the equivalent set on
28171
+ * Windows). Everything above that base comes from the table below.
28172
+ * 2. A stored variable is provider data, never runtime configuration. The child is a Node
28173
+ * process, so a key like NODE_OPTIONS is applied by the runtime before a single line of
28174
+ * server code loads: `--require /tmp/x.js` would turn a credential field into arbitrary code
28175
+ * execution inside the child. Those keys are refused rather than dropped quietly.
28176
+ *
28177
+ * MUST STAY IN SYNC with the `process.env` reads under each server directory
28178
+ * (`github/config.ts`, `notion/config.ts`, `atlassian/config.ts`, `linkedin/index.ts`). A
28179
+ * variable a server reads but this table omits arrives `undefined`, so add it here in the same
28180
+ * change. `childEnv.test.ts` pins that both ways.
28181
+ */
28182
+ const MCP_SERVER_ENV_KEYS = {
28183
+ [McpServerName.LinkedIn]: ["LINKEDIN_ACCESS_TOKEN", "COMPANY_NAME"],
28184
+ [McpServerName.Github]: ["GITHUB_ACCESS_TOKEN"],
28185
+ [McpServerName.Atlassian]: [
28186
+ "ATLASSIAN_ACCESS_TOKEN",
28187
+ "ATLASSIAN_CLOUD_ID",
28188
+ "ATLASSIAN_SITE_URL"
28189
+ ],
28190
+ [McpServerName.Notion]: [
28191
+ "NOTION_ACCESS_TOKEN",
28192
+ "NOTION_WORKSPACE_ID",
28193
+ "NOTION_WRITE_ENABLED",
28194
+ "NOTION_ROOT_PAGE_ID",
28195
+ "NOTION_ACCESS_MODE",
28196
+ "NOTION_ALLOWED_PAGES",
28197
+ "NOTION_EXCLUDED_PAGE_IDS",
28198
+ "NOTION_DEBUG"
28199
+ ]
28200
+ };
28201
+ /**
28202
+ * Keys that make the runtime execute caller-chosen code before the server's entry point runs:
28203
+ * NODE_OPTIONS can `--require` a file, the loader variables preload a shared object, and
28204
+ * ELECTRON_RUN_AS_NODE changes what the binary is. Matching is case-insensitive because Windows
28205
+ * environment names are.
28206
+ */
28207
+ const CODE_INJECTING_ENV_KEY_PATTERNS = [
28208
+ /^NODE_/i,
28209
+ /^ELECTRON_RUN_AS_NODE$/i,
28210
+ /^LD_/i,
28211
+ /^DYLD_/i
28212
+ ];
28213
+ /**
28214
+ * Keys that steer where the child resolves things rather than what it executes: npm_* redirects
28215
+ * package resolution, PATH decides which binary a bare command name finds, and the proxy
28216
+ * variables redirect outbound traffic.
28217
+ */
28218
+ const RESOLUTION_STEERING_ENV_KEY_PATTERNS = [
28219
+ /^npm_/i,
28220
+ /^PATH$/i,
28221
+ /^PATHEXT$/i,
28222
+ /^(HTTP|HTTPS|ALL|NO|FTP)_PROXY$/i,
28223
+ /^GLOBAL_AGENT_/i
28224
+ ];
28225
+ [...CODE_INJECTING_ENV_KEY_PATTERNS, ...RESOLUTION_STEERING_ENV_KEY_PATTERNS];
28226
+ const matchesAny = (patterns, key) => {
28227
+ const normalized = key.trim();
28228
+ return patterns.some((pattern) => pattern.test(normalized));
28229
+ };
28230
+ /** True when `key` would have the runtime load caller-chosen code before the server starts. */
28231
+ function isCodeInjectingMcpEnvKey(key) {
28232
+ return matchesAny(CODE_INJECTING_ENV_KEY_PATTERNS, key);
28233
+ }
28234
+ /**
28235
+ * Build the environment for a stdio MCP child.
28236
+ *
28237
+ * A bundled server gets exactly its declared variables - the allowlist decides, and the denylist
28238
+ * above is never consulted.
28239
+ *
28240
+ * A caller-defined command has no declared contract to check against, so it gets everything
28241
+ * except the code-injecting keys. Only the `b4m` CLI config reaches this branch, and that file
28242
+ * already lets its owner set `command` and `args` to any binary - so withholding PATH or a proxy
28243
+ * variable from them protects nobody while breaking a wrapper script or a corporate proxy, and
28244
+ * the warning that says so goes to a stderr the TUI hides. The code-injecting half stays because
28245
+ * an env-only `--require` is the one lever that is easy to set by accident.
28246
+ */
28247
+ function buildMcpChildEnv({ serverName, envVariables, hasCustomCommand = false }) {
28248
+ const declaredKeys = hasCustomCommand ? void 0 : MCP_SERVER_ENV_KEYS[serverName];
28249
+ const isAllowed = declaredKeys ? (key) => declaredKeys.includes(key) : (key) => !isCodeInjectingMcpEnvKey(key);
28250
+ const env = {};
28251
+ const droppedKeys = [];
28252
+ for (const { key, value } of envVariables) {
28253
+ if (!isAllowed(key)) {
28254
+ droppedKeys.push(key);
28255
+ continue;
28256
+ }
28257
+ env[key] = value;
28258
+ }
28259
+ return {
28260
+ env,
28261
+ droppedKeys
28262
+ };
28263
+ }
26723
28264
  var MCPClient = class {
26724
28265
  mcp;
26725
28266
  transport = null;
@@ -26763,14 +28304,11 @@ var MCPClient = class {
26763
28304
  }));
26764
28305
  return;
26765
28306
  }
26766
- const envVarsObject = this.envVariables.reduce((acc, env) => ({
26767
- ...acc,
26768
- [env.key]: env.value
26769
- }), {});
26770
28307
  let command;
26771
28308
  let args;
26772
- if (this.customCommand && this.customCommand.trim() !== "") {
26773
- command = this.customCommand;
28309
+ const customCommand = this.customCommand?.trim() ? this.customCommand : void 0;
28310
+ if (customCommand) {
28311
+ command = customCommand;
26774
28312
  args = this.customArgs ?? [];
26775
28313
  } else {
26776
28314
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -26787,13 +28325,16 @@ var MCPClient = class {
26787
28325
  console.log(`[MCP] Using server: ${this.serverName} at ${serverScriptPath}`);
26788
28326
  }
26789
28327
  const stderrMode = this.suppressStderr ? "ignore" : this.onStderrLine ? "pipe" : void 0;
28328
+ const { env, droppedKeys } = buildMcpChildEnv({
28329
+ serverName: this.serverName,
28330
+ envVariables: this.envVariables,
28331
+ hasCustomCommand: Boolean(customCommand)
28332
+ });
28333
+ if (droppedKeys.length > 0) console.warn(`[MCP] Withheld ${droppedKeys.length} undeclared env variable(s) from ${this.serverName}: ${droppedKeys.join(", ")}`);
26790
28334
  const transportConfig = {
26791
28335
  command,
26792
28336
  args,
26793
- env: {
26794
- ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0)),
26795
- ...envVarsObject
26796
- },
28337
+ env,
26797
28338
  ...stderrMode && { stderr: stderrMode }
26798
28339
  };
26799
28340
  const stdioTransport = new StdioClientTransport(transportConfig);
@@ -27221,7 +28762,7 @@ const MODEL_ALIASES = {
27221
28762
  "o4-mini": ChatModels.O4_MINI,
27222
28763
  gemini: ChatModels.GEMINI_2_5_PRO,
27223
28764
  "gemini-pro": ChatModels.GEMINI_2_5_PRO,
27224
- "gemini-flash": ChatModels.GEMINI_2_5_FLASH,
28765
+ "gemini-flash": ChatModels.GEMINI_3_5_FLASH,
27225
28766
  "gemini-flash-lite": ChatModels.GEMINI_2_5_FLASH_LITE,
27226
28767
  "gemini-3": ChatModels.GEMINI_3_PRO_PREVIEW,
27227
28768
  "gemini-3-pro": ChatModels.GEMINI_3_PRO_PREVIEW,
@@ -27328,7 +28869,7 @@ var AgentStore = class {
27328
28869
  async loadAgentsFromDirectory(directory, source) {
27329
28870
  try {
27330
28871
  if (!(await fs$1.stat(directory)).isDirectory()) return;
27331
- const files = await this.findAgentFiles(directory);
28872
+ const files = await findMarkdownFiles(directory);
27332
28873
  for (const filePath of files) try {
27333
28874
  const agent = await this.parseAgentFile(filePath, source);
27334
28875
  this.agents.set(agent.name, agent);
@@ -27340,25 +28881,6 @@ var AgentStore = class {
27340
28881
  }
27341
28882
  }
27342
28883
  /**
27343
- * Recursively find all .md files in directory
27344
- */
27345
- async findAgentFiles(directory) {
27346
- const files = [];
27347
- try {
27348
- const entries = await fs$1.readdir(directory, { withFileTypes: true });
27349
- for (const entry of entries) {
27350
- const fullPath = path.join(directory, entry.name);
27351
- if (entry.isDirectory()) {
27352
- const subFiles = await this.findAgentFiles(fullPath);
27353
- files.push(...subFiles);
27354
- } else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
27355
- }
27356
- } catch (error) {
27357
- console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
27358
- }
27359
- return files;
27360
- }
27361
- /**
27362
28884
  * Parse a single agent markdown file
27363
28885
  */
27364
28886
  async parseAgentFile(filePath, source) {
@@ -27675,7 +29197,8 @@ function buildFilenameMarkerRegex(markers) {
27675
29197
  * of the best-effort DB pre-filter. Fail-closed by design.
27676
29198
  */
27677
29199
  function isRetrievalExcluded(file, opts) {
27678
- if (opts.vectorizedOnly && !file.vectorized) return true;
29200
+ const stalledByConvergence = isChunkStalledFile(file) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
29201
+ if (opts.vectorizedOnly && !file.vectorized && !stalledByConvergence) return true;
27679
29202
  const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
27680
29203
  return !!re && re.test((file.fileName ?? "").toLowerCase());
27681
29204
  }
@@ -28185,6 +29708,41 @@ function attachedContentExtractionBudget(maxSafeInputTokens, systemPromptReserve
28185
29708
  const boundedReserve = Math.min(systemPromptReserve, Math.floor(maxSafeInputTokens * EXTRACTION_SYSTEM_RESERVE_MAX_SHARE));
28186
29709
  return Math.max(0, Math.max(Math.floor(maxSafeInputTokens * MIN_ATTACHED_CONTENT_EXTRACTION_SHARE), Math.floor((maxSafeInputTokens - boundedReserve) * ATTACHED_CONTENT_EXTRACTION_SHARE)));
28187
29710
  }
29711
+ /**
29712
+ * Output budget used when a caller supplies no max_tokens. Within supported output limits for
29713
+ * every configured non-reasoning model; models that reason inside the output budget default to
29714
+ * ADAPTIVE_THINKING_MAX_TOKENS_FLOOR instead, since their reasoning would otherwise consume this
29715
+ * whole budget (see resolveOutputMaxTokens/reasonsWithinOutputBudget for which those are).
29716
+ * Distinct from the catalog's DEFAULT_MAX_OUTPUT_TOKENS, which fills in a model's *capability*
29717
+ * when its record omits one.
29718
+ */
29719
+ const DEFAULT_OUTPUT_MAX_TOKENS = 4096;
29720
+ /**
29721
+ * Verbatim-history token budget shared by every caller that sizes one: how much of the model's
29722
+ * safe input window recent conversation turns may claim before older ones fall out to
29723
+ * contextSummary. Pulled into one place because ChatCompletionProcess.ts and
29724
+ * ChatCompletionFeatures.ts (QuestMaster) both need it and must not drift apart - the first
29725
+ * version of the QuestMaster call site hand-copied the formula and got the window wrong for
29726
+ * every model below the unknown-model floor.
29727
+ *
29728
+ * Always returns at least 1, even on a window so small or a message so long that the raw
29729
+ * subtraction goes to zero or negative: `Math.max(0, budget) && budget > 0` downstream treats a
29730
+ * falsy budget as "no budget given" and skips trimming entirely, which is the exact unbounded
29731
+ * history this function exists to prevent. A budget of 1 still trims to the single most recent
29732
+ * turn instead of disabling the trim.
29733
+ */
29734
+ function computeVerbatimTokenBudget(modelInfo, requestedMaxTokens, opts) {
29735
+ const modelMaxOutput = modelInfo.max_tokens ?? 16384;
29736
+ const safeMaxTokens = resolveOutputMaxTokens({
29737
+ requested: requestedMaxTokens,
29738
+ fallback: DEFAULT_OUTPUT_MAX_TOKENS,
29739
+ modelInfo,
29740
+ modelMaxOutputTokens: modelMaxOutput
29741
+ });
29742
+ const safeInputTokens = Math.max(0, safeInputWindow(modelInfo, safeMaxTokens));
29743
+ const availableForVerbatim = Math.max(0, safeInputTokens - opts.nonHistoryOverheadTokens);
29744
+ return Math.max(1, Math.floor(availableForVerbatim * opts.verbatimWindowFraction));
29745
+ }
28188
29746
  /** The buffer buildAndSortMessages holds back before dividing the input window. */
28189
29747
  function assemblyTokenBuffer(maxInputTokens) {
28190
29748
  return Math.max(MIN_TOKEN_BUFFER, Math.floor(maxInputTokens * TOKEN_BUFFER_PERCENTAGE));
@@ -28223,6 +29781,18 @@ function attachedContentBudgetsAgree(maxSafeInputTokens, systemPromptReserve) {
28223
29781
  var AdminSettingsCache = class AdminSettingsCache {
28224
29782
  cache = /* @__PURE__ */ new Map();
28225
29783
  individualCache = /* @__PURE__ */ new Map();
29784
+ /**
29785
+ * Every call through this field is optional-chained (`this.logger.debug?.()`).
29786
+ *
29787
+ * A cache must not throw because it could not log, and this one is exposed to that: it is a
29788
+ * process-wide singleton created with whichever logger happens to reach `getSettingsCache` first.
29789
+ * What each caller then does with a throw varies, and it is mostly NOT a degrade-to-defaults
29790
+ * guard: `getSettingsByNames` has none at all, the scoped resolver guards one layer out in
29791
+ * `resolveAll`, and `resolveSpendLevers` deliberately rethrows to halt spend. So a logger missing
29792
+ * a quieter level could surface as a silent wrong VALUE, as an unhandled rejection, or as a hard
29793
+ * fail-closed, depending on who asked. `ScopedSettingsCache` is built by the same factory pair
29794
+ * and still has one unguarded call - the same hazard, not a solved one.
29795
+ */
28226
29796
  logger;
28227
29797
  cleanupInterval = null;
28228
29798
  maxCacheSize = 1e3;
@@ -28238,13 +29808,13 @@ var AdminSettingsCache = class AdminSettingsCache {
28238
29808
  */
28239
29809
  startCleanupTimer() {
28240
29810
  if (process.env.NODE_ENV !== "production" || process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
28241
- this.logger.debug("Skipping cleanup timer in serverless environment");
29811
+ this.logger.debug?.("Skipping cleanup timer in serverless environment");
28242
29812
  return;
28243
29813
  }
28244
29814
  this.cleanupInterval = setInterval(() => {
28245
29815
  this.performCleanup();
28246
29816
  }, AdminSettingsCache.CLEANUP_INTERVAL);
28247
- this.logger.debug("Started cache cleanup timer");
29817
+ this.logger.debug?.("Started cache cleanup timer");
28248
29818
  }
28249
29819
  /**
28250
29820
  * Stop cleanup timer (for graceful shutdown)
@@ -28253,7 +29823,7 @@ var AdminSettingsCache = class AdminSettingsCache {
28253
29823
  if (this.cleanupInterval) {
28254
29824
  clearInterval(this.cleanupInterval);
28255
29825
  this.cleanupInterval = null;
28256
- this.logger.debug("Stopped cache cleanup timer");
29826
+ this.logger.debug?.("Stopped cache cleanup timer");
28257
29827
  }
28258
29828
  }
28259
29829
  /**
@@ -28278,9 +29848,9 @@ var AdminSettingsCache = class AdminSettingsCache {
28278
29848
  this.individualCache.delete(entries[i][0]);
28279
29849
  removedCount++;
28280
29850
  }
28281
- this.logger.warn(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
29851
+ this.logger.warn?.(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
28282
29852
  }
28283
- if (removedCount > 0) this.logger.debug(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
29853
+ if (removedCount > 0) this.logger.debug?.(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
28284
29854
  }
28285
29855
  /**
28286
29856
  * Get TTL based on environment
@@ -28301,18 +29871,18 @@ var AdminSettingsCache = class AdminSettingsCache {
28301
29871
  const cacheKey = "all_settings";
28302
29872
  const cached = this.cache.get(cacheKey);
28303
29873
  if (cached && this.isValid(cached.timestamp, cached.ttl)) {
28304
- this.logger.debug("📦 Admin settings cache HIT");
29874
+ this.logger.debug?.("📦 Admin settings cache HIT");
28305
29875
  return cached.data;
28306
29876
  }
28307
29877
  if (cached) this.cache.delete(cacheKey);
28308
- this.logger.debug("🔍 Admin settings cache MISS - fetching from database");
29878
+ this.logger.debug?.("🔍 Admin settings cache MISS - fetching from database");
28309
29879
  const fetchStart = Date.now();
28310
29880
  const settingsMap = (await db.adminSettings.findAll()).reduce((out, s) => {
28311
29881
  out[s.settingName] = s.settingValue;
28312
29882
  return out;
28313
29883
  }, {});
28314
29884
  const fetchTime = Date.now() - fetchStart;
28315
- this.logger.info(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
29885
+ this.logger.info?.(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
28316
29886
  const ttl = this.getTTL();
28317
29887
  this.cache.set(cacheKey, {
28318
29888
  data: settingsMap,
@@ -28334,15 +29904,15 @@ var AdminSettingsCache = class AdminSettingsCache {
28334
29904
  async getSettingByName(settingName, db) {
28335
29905
  const cached = this.individualCache.get(settingName);
28336
29906
  if (cached && this.isValid(cached.timestamp, cached.ttl)) {
28337
- this.logger.debug(`📦 Individual setting '${settingName}' cache HIT`);
29907
+ this.logger.debug?.(`📦 Individual setting '${settingName}' cache HIT`);
28338
29908
  return cached.value;
28339
29909
  }
28340
29910
  if (cached) this.individualCache.delete(settingName);
28341
- this.logger.debug(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
29911
+ this.logger.debug?.(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
28342
29912
  const fetchStart = Date.now();
28343
29913
  const value = (await db.adminSettings.findBySettingName(settingName))?.settingValue ?? null;
28344
29914
  const fetchTime = Date.now() - fetchStart;
28345
- this.logger.debug(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
29915
+ this.logger.debug?.(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
28346
29916
  this.individualCache.set(settingName, {
28347
29917
  value,
28348
29918
  timestamp: Date.now(),
@@ -28366,11 +29936,11 @@ var AdminSettingsCache = class AdminSettingsCache {
28366
29936
  }
28367
29937
  }
28368
29938
  if (uncachedSettings.length > 0) {
28369
- this.logger.debug(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
29939
+ this.logger.debug?.(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
28370
29940
  const fetchStart = Date.now();
28371
29941
  const settings = await db.adminSettings.findBySettingNames(uncachedSettings);
28372
29942
  const fetchTime = Date.now() - fetchStart;
28373
- this.logger.debug(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
29943
+ this.logger.debug?.(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
28374
29944
  const ttl = this.getTTL();
28375
29945
  settings.forEach((setting) => {
28376
29946
  result[setting.settingName] = setting.settingValue;
@@ -28388,7 +29958,7 @@ var AdminSettingsCache = class AdminSettingsCache {
28388
29958
  });
28389
29959
  });
28390
29960
  }
28391
- this.logger.debug(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
29961
+ this.logger.debug?.(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
28392
29962
  return result;
28393
29963
  }
28394
29964
  /**
@@ -28397,7 +29967,7 @@ var AdminSettingsCache = class AdminSettingsCache {
28397
29967
  invalidateSetting(settingName) {
28398
29968
  this.individualCache.delete(settingName);
28399
29969
  this.cache.delete("all_settings");
28400
- this.logger.info(`🗑️ Invalidated cache for setting: ${settingName}`);
29970
+ this.logger.info?.(`🗑️ Invalidated cache for setting: ${settingName}`);
28401
29971
  }
28402
29972
  /**
28403
29973
  * Invalidate all cached admin settings
@@ -28405,7 +29975,7 @@ var AdminSettingsCache = class AdminSettingsCache {
28405
29975
  invalidateAll() {
28406
29976
  this.cache.clear();
28407
29977
  this.individualCache.clear();
28408
- this.logger.info("🗑️ Invalidated all admin settings cache");
29978
+ this.logger.info?.("🗑️ Invalidated all admin settings cache");
28409
29979
  }
28410
29980
  /**
28411
29981
  * Get cache statistics for monitoring
@@ -28438,16 +30008,16 @@ var AdminSettingsCache = class AdminSettingsCache {
28438
30008
  * Warm up the cache by fetching all settings
28439
30009
  */
28440
30010
  async warmUp(db) {
28441
- this.logger.info("🔥 Warming up admin settings cache...");
30011
+ this.logger.info?.("🔥 Warming up admin settings cache...");
28442
30012
  await this.getSettingsMap(db);
28443
- this.logger.info("✅ Admin settings cache warmed up");
30013
+ this.logger.info?.("✅ Admin settings cache warmed up");
28444
30014
  }
28445
30015
  /**
28446
30016
  * Graceful shutdown - cleanup timers
28447
30017
  */
28448
30018
  shutdown() {
28449
30019
  this.stopCleanupTimer();
28450
- this.logger.info("🛑 Admin settings cache shutdown complete");
30020
+ this.logger.info?.("🛑 Admin settings cache shutdown complete");
28451
30021
  }
28452
30022
  };
28453
30023
  /** Address of one cached override, shared by the cache and its callers so lookups are consistent. */
@@ -28796,6 +30366,17 @@ const getFileContent = async (fabFile, { storage, logger }) => {
28796
30366
  }
28797
30367
  return content;
28798
30368
  };
30369
+ /**
30370
+ * Content hash for per-lake FabFile dedup (`findByContentHashesInDataLake`). Shared by every
30371
+ * ingest path that needs to hash bytes before creating a FabFile - the Slack attachment path
30372
+ * (raw downloaded buffer) and the URL/link path (`fetchAndParseURL`'s extracted `textContent`) -
30373
+ * so at least the HASHING ITSELF cannot drift between two copies of the same algorithm.
30374
+ *
30375
+ * This does NOT make `contentHash` one hash domain: the two callers feed it different inputs
30376
+ * (raw bytes vs. extracted text), so the same document added once as an attachment and once as a
30377
+ * link produces two different hashes and is not caught as a duplicate by this field.
30378
+ */
30379
+ const computeContentHash = (content) => createHash$1("sha256").update(content).digest("hex");
28799
30380
  /** The next 1-based version number given the existing (possibly absent) version history. */
28800
30381
  const nextVersionNumber = (versions) => {
28801
30382
  if (!versions || versions.length === 0) return 1;
@@ -29006,19 +30587,6 @@ const EDITABLE_IMAGE_KEY_RE = /\.(jpe?g|png|webp|gif)$/i;
29006
30587
  const PREVIEW_CHUNK = 700;
29007
30588
  const CHARS_PER_TOKEN = 3.5;
29008
30589
  /**
29009
- * Chunks per attached file that cosine retrieval feeds to the model. Three starved small embedders: a
29010
- * chunk is the embedding model's context window less a 20% buffer (see SmartChunker), so three chunks
29011
- * is roughly 69k chars on an 8192-token embedder but only 4.3k on a 512-token one, which answers a
29012
- * question about a 200-row table from 43 rows without saying so.
29013
- *
29014
- * 10 is borrowed from rankChunksForFiles' topK default, but note the two caps differ in shape: that
29015
- * one is global across every file in the search, this one is PER FILE, so a multi-file attachment can
29016
- * yield more chunks here. What bounds the payload is the per-file character budget applied to these
29017
- * results (maxChars in processFabFilesServer), not this count - and that budget now derives from the
29018
- * model's input window rather than its output limit; see attachedContentExtractionBudget.
29019
- */
29020
- const COSINE_SEARCH_TOP_K = 10;
29021
- /**
29022
30590
  * How much of one attached file the cosine scan will read, and in what size pages.
29023
30591
  *
29024
30592
  * Module constants rather than admin settings: unlike a data lake, an attachment is one file the
@@ -29191,12 +30759,12 @@ const estimateMessagesTokens = (messages) => messages.reduce((sum, message) => s
29191
30759
  * staying synchronous (no N async tokenizer calls over a long history). Mirrors
29192
30760
  * the fields the conversion below actually emits into the prompt.
29193
30761
  */
29194
- function estimateQuestTokenLength(item) {
30762
+ function estimateQuestTokenLength(item, disableToolReplay = false) {
29195
30763
  const parts = [item.prompt ?? ""];
29196
30764
  if (item.structuredReplies?.length) parts.push(JSON.stringify(item.structuredReplies));
29197
30765
  else if (item.replies?.length) parts.push(item.replies.join("\n"));
29198
30766
  if (item.toolResults?.length) parts.push(JSON.stringify(item.toolResults));
29199
- if (!item.structuredReplies?.length) {
30767
+ if (!item.structuredReplies?.length && !disableToolReplay) {
29200
30768
  const toolCalls = replayableToolCalls(item.promptMeta?.functionCalls);
29201
30769
  if (toolCalls.length) parts.push(JSON.stringify(toolCalls));
29202
30770
  }
@@ -29276,20 +30844,21 @@ async function generateSafeEmbedding(embeddingService, text, logger) {
29276
30844
  * history at all, and UNLIMITED_HISTORY_COUNT means no window (which still pages, since the
29277
30845
  * fetch needs some limit).
29278
30846
  */
29279
- async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget }) {
30847
+ async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget, excludeCurrentPrompt = false, model }) {
29280
30848
  if (!isUnlimitedHistory(historyCount) && historyCount !== null && historyCount <= 0) return [
29281
30849
  [],
29282
30850
  0,
29283
30851
  { cacheHit: false }
29284
30852
  ];
29285
30853
  const limit = resolveHistoryFetchLimit(historyCount);
30854
+ const disableToolReplay = !!model && isGeminiModelId(model);
29286
30855
  const startTime = Date.now();
29287
30856
  const chatHistoryItems = await db.quests.getMostRecentChatHistory(session.id, limit + 1);
29288
30857
  const fetchTime = Date.now() - startTime;
29289
30858
  const cacheIndicator = fetchTime < 50 ? "CACHE_HIT" : "CACHE_MISS";
29290
30859
  Logger.globalInstance.log(`⚡ Message History ${cacheIndicator}: ${fetchTime}ms for session ${session.id.slice(-8)} (${chatHistoryItems.length} items)`);
29291
30860
  chatHistoryItems.reverse();
29292
- if (chatHistoryItems.length > 1) chatHistoryItems.pop();
30861
+ if (chatHistoryItems.length > 1 || excludeCurrentPrompt) chatHistoryItems.pop();
29293
30862
  if (session.contextSummaryUpToQuestId) {
29294
30863
  const boundary = session.contextSummaryUpToQuestId;
29295
30864
  const filtered = chatHistoryItems.filter((item) => item.id > boundary);
@@ -29300,7 +30869,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29300
30869
  let usedTokens = 0;
29301
30870
  let keepFromIndex = 0;
29302
30871
  for (let i = chatHistoryItems.length - 1; i >= 0; i--) {
29303
- usedTokens += estimateQuestTokenLength(chatHistoryItems[i]);
30872
+ usedTokens += estimateQuestTokenLength(chatHistoryItems[i], disableToolReplay);
29304
30873
  if (usedTokens > verbatimTokenBudget && i < chatHistoryItems.length - 1) {
29305
30874
  keepFromIndex = i + 1;
29306
30875
  break;
@@ -29332,7 +30901,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
29332
30901
  is_error: tr.is_error
29333
30902
  }))
29334
30903
  });
29335
- } else if (toolCalls.length > 0) {
30904
+ } else if (toolCalls.length > 0 && !disableToolReplay) {
29336
30905
  const textReply = cur.replies?.find((reply) => !reply.trim().startsWith("<think>")) || "";
29337
30906
  const assistantContent = [];
29338
30907
  if (textReply) assistantContent.push({
@@ -29429,14 +30998,28 @@ async function fetchAgentConversationHistory(session, questCount, { db }) {
29429
30998
  return acc;
29430
30999
  }, new Array());
29431
31000
  }
29432
- async function fetchAndConvertFabFiles(fabFileIds, { scope }, { db, storage }) {
29433
- const fabFiles = await db.fabfiles.getAccessibleFiles(fabFileIds, scope);
29434
- return await Promise.all(fabFiles.map(async (file) => {
31001
+ /**
31002
+ * Resolves attachment ids to documents, and reports the ones it could NOT resolve. The missing set
31003
+ * is the point: `getAccessibleFiles` applies a permission scope and simply omits what it rejects, so
31004
+ * an id dropped by the scope filter or by a delete/upload race used to leave no trace anywhere - the
31005
+ * turn ran as though the file had never been attached (#2228). Callers report `missingIds` through
31006
+ * the same channel as the per-file notices rather than inferring the drop from a shorter array.
31007
+ */
31008
+ async function fetchAndConvertFabFiles(fabFileIds, { scope, lakeAccess }, { db, storage, logger }) {
31009
+ const fabFiles = await db.fabfiles.getAccessibleFiles(fabFileIds, scope, lakeAccess);
31010
+ const files = await Promise.all(fabFiles.map(async (file) => {
29435
31011
  return {
29436
31012
  ...file,
29437
31013
  userId: file.userId.toString()
29438
31014
  };
29439
31015
  }));
31016
+ const returnedIds = new Set(files.map((file) => String(file.id)));
31017
+ const missingIds = Array.from(new Set(fabFileIds)).filter((id) => !returnedIds.has(String(id)));
31018
+ if (missingIds.length > 0) logger?.warn(`[fetchAndConvertFabFiles] ${missingIds.length} of ${fabFileIds.length} requested file id(s) were not returned by getAccessibleFiles and contribute nothing to this turn: ${missingIds.join(", ")}`);
31019
+ return {
31020
+ files,
31021
+ missingIds
31022
+ };
29440
31023
  }
29441
31024
  async function getCachedSignedUrl(filePath, storage, db) {
29442
31025
  const key = `cachedSignedUrl:${filePath}`;
@@ -29638,7 +31221,7 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
29638
31221
  for (const chunk of usable) {
29639
31222
  const position = scanned;
29640
31223
  scanned++;
29641
- if (head.length < COSINE_SEARCH_TOP_K) head.push({
31224
+ if (head.length < 10) head.push({
29642
31225
  chunkId: chunk.id,
29643
31226
  content: chunk.text,
29644
31227
  score: 0
@@ -29659,9 +31242,9 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
29659
31242
  position
29660
31243
  });
29661
31244
  }
29662
- if (ranked.length > COSINE_SEARCH_TOP_K) {
31245
+ if (ranked.length > 10) {
29663
31246
  ranked.sort(compareRankedChunks);
29664
- ranked.length = COSINE_SEARCH_TOP_K;
31247
+ ranked.length = 10;
29665
31248
  }
29666
31249
  if (!moreExist) break;
29667
31250
  }
@@ -29683,14 +31266,14 @@ const noopResize = async (imageBuffer) => imageBuffer;
29683
31266
  async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, attachedContentTokenBudget, modelInfo, sendStatusUpdate, { logger, storage, db, resizeImageForModel = noopResize }, progressCallback) {
29684
31267
  if (!fabFiles || fabFiles.length === 0) return {
29685
31268
  userMessages: [],
29686
- errorMessages: [],
31269
+ fileNotices: [],
29687
31270
  deliveredFileIds: [],
29688
31271
  fullyDeliveredFileIds: []
29689
31272
  };
29690
31273
  const fileProcessingStartTime = Date.now();
29691
31274
  let systemContent = "";
29692
31275
  const userMessages = [];
29693
- const errorMessages = [];
31276
+ const fileNotices = [];
29694
31277
  const deliveredFileIds = /* @__PURE__ */ new Set();
29695
31278
  const fullyDeliveredFileIds = /* @__PURE__ */ new Set();
29696
31279
  const contextFiles = [];
@@ -29721,11 +31304,25 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29721
31304
  try {
29722
31305
  if (isAudioMimeType(file.mimeType)) {
29723
31306
  logger.warn(`[processFabFilesServer] Skipping audio file ${file.fileName} — audio is not attachable to an LLM.`);
31307
+ fileNotices.push({
31308
+ fabFileId: file.id,
31309
+ fileName: file.fileName,
31310
+ band: "audio",
31311
+ message: `"${noticeFileName(file.fileName)}" is an audio file and was not sent: no model accepts audio as input.`,
31312
+ delivered: false
31313
+ });
29724
31314
  return;
29725
31315
  }
29726
31316
  if (supportsVision && isImageAttachment(file.mimeType)) {
29727
31317
  if (!isImageServeable(file)) {
29728
31318
  logger.warn(`[processFabFilesServer] Skipping image file ${file.fileName} — held pending moderation or blocked (#9776 Q2b).`);
31319
+ fileNotices.push({
31320
+ fabFileId: file.id,
31321
+ fileName: file.fileName,
31322
+ band: "image_not_serveable",
31323
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: it is held pending moderation or has been blocked.`,
31324
+ delivered: false
31325
+ });
29729
31326
  return;
29730
31327
  }
29731
31328
  sendStatusUpdate(`Processing image file ${file.fileName}...`);
@@ -29761,9 +31358,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29761
31358
  const errorMsg = `⚠️ Image "${file.fileName}" (${fileSizeMB.toFixed(1)}MB) is too large for ${backendName}. Max: ${MAX_IMAGE_SIZE_MB}MB. Please delete this file and re-upload to auto-resize.`;
29762
31359
  logger.warn(errorMsg);
29763
31360
  await sendStatusUpdate(errorMsg);
29764
- errorMessages.push({
29765
- role: "error",
29766
- content: errorMsg
31361
+ fileNotices.push({
31362
+ fabFileId: file.id,
31363
+ fileName: file.fileName,
31364
+ band: "image_too_large",
31365
+ message: errorMsg,
31366
+ delivered: false
29767
31367
  });
29768
31368
  return;
29769
31369
  }
@@ -29793,9 +31393,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29793
31393
  const errorMsg = `⚠️ Image "${file.fileName}" (${encodedMB}MB encoded) is too large for ${modelInfo.name}. Max ~3MB. Please delete this file and re-upload a smaller image.`;
29794
31394
  logger.warn(errorMsg);
29795
31395
  await sendStatusUpdate(errorMsg);
29796
- errorMessages.push({
29797
- role: "error",
29798
- content: errorMsg
31396
+ fileNotices.push({
31397
+ fabFileId: file.id,
31398
+ fileName: file.fileName,
31399
+ band: "image_too_large",
31400
+ message: errorMsg,
31401
+ delivered: false
29799
31402
  });
29800
31403
  return;
29801
31404
  }
@@ -29809,7 +31412,16 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29809
31412
  });
29810
31413
  delivered = true;
29811
31414
  fullyDelivered = true;
29812
- } else logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
31415
+ } else {
31416
+ logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
31417
+ fileNotices.push({
31418
+ fabFileId: file.id,
31419
+ fileName: file.fileName,
31420
+ band: "vision_unsupported",
31421
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: image input is not implemented for ${modelInfo.name ?? modelInfo.id}.`,
31422
+ delivered: false
31423
+ });
31424
+ }
29813
31425
  break;
29814
31426
  case ModelBackend.Ollama: {
29815
31427
  const imageBuffer = await resizeImageForModel(await storage.download(file.filePath), void 0, logger);
@@ -29838,10 +31450,26 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29838
31450
  fullyDelivered = true;
29839
31451
  break;
29840
31452
  }
29841
- default: logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
31453
+ default:
31454
+ logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
31455
+ fileNotices.push({
31456
+ fabFileId: file.id,
31457
+ fileName: file.fileName,
31458
+ band: "unsupported_backend",
31459
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: this model's backend does not accept image attachments.`,
31460
+ delivered: false
31461
+ });
29842
31462
  }
29843
- } else if (!supportsVision && isImageAttachment(file.mimeType)) logger.warn(`File ${file.fileName} is an image but model does not support vision. Skipping...`);
29844
- else {
31463
+ } else if (!supportsVision && isImageAttachment(file.mimeType)) {
31464
+ logger.warn(`File ${file.fileName} is an image but model does not support vision. Skipping...`);
31465
+ fileNotices.push({
31466
+ fabFileId: file.id,
31467
+ fileName: file.fileName,
31468
+ band: "vision_unsupported",
31469
+ message: `Image "${noticeFileName(file.fileName)}" was not sent: ${modelInfo?.name ?? modelInfo?.id ?? "this model"} cannot read images.`,
31470
+ delivered: false
31471
+ });
31472
+ } else {
29845
31473
  const embeddingModel = file.embeddingModel ?? OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002;
29846
31474
  const userVector = userVectorPrompt[embeddingModel];
29847
31475
  const canCosineSearch = file.vectorized && !!userVector && userVector.length > 0;
@@ -29917,9 +31545,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29917
31545
  const originalFileSize = fabContent.length;
29918
31546
  fabContent = fabContent.substring(0, finalMaxFileSize ?? PREVIEW_CHUNK) + CONTENT_TRUNCATION_NOTICE;
29919
31547
  errorMsg = `Knowledge in the workbench with the fileName ${file.fileName} is ${originalFileSize} long which exceeds ${finalMaxFileSize}. ` + (canCosineSearch ? "None of its vectorized chunks could be searched with this turn's embedding model, so it was sent as raw text and truncated. Re-vectorize it under the current embedding model, or select a model with a higher context window." : "Vectorize your large file or select a model with higher context window.");
29920
- errorMessages.push({
29921
- role: "error",
29922
- content: errorMsg
31548
+ fileNotices.push({
31549
+ fabFileId: file.id,
31550
+ fileName: file.fileName,
31551
+ band: "truncated",
31552
+ message: `"${noticeFileName(file.fileName)}" was too large to send whole; only the first ${Math.floor(finalMaxFileSize)} characters of ${originalFileSize} reached this conversation.`,
31553
+ delivered: true
29923
31554
  });
29924
31555
  } else errorMsg = null;
29925
31556
  delivered = true;
@@ -29933,19 +31564,41 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29933
31564
  error: errorMsg
29934
31565
  });
29935
31566
  } catch (e) {
29936
- if (e instanceof BadRequestError && e.message.includes("Unsupported file type")) logger.warn(`Unsupported file type: ${file.fileName}`);
29937
- else if (isAxiosError(e) && e.response?.status === 404) {
31567
+ if (e instanceof BadRequestError && e.message.includes("Unsupported file type")) {
31568
+ logger.warn(`Unsupported file type: ${file.fileName}`);
31569
+ fileNotices.push({
31570
+ fabFileId: file.id,
31571
+ fileName: file.fileName,
31572
+ band: "unsupported_type",
31573
+ message: `"${noticeFileName(file.fileName)}" was not sent: its file type (${file.mimeType}) cannot be read as text.`,
31574
+ delivered: false
31575
+ });
31576
+ } else if (isAxiosError(e) && e.response?.status === 404) {
29938
31577
  await sendStatusUpdate(`Skipping file ${file.fileName}. File might be corrupted or deleted`);
29939
31578
  await db.fabfiles.update({
29940
31579
  id: file.id,
29941
31580
  error: "This file appears to be corrupted or may have been deleted. Please try uploading the file again."
29942
31581
  });
31582
+ fileNotices.push({
31583
+ fabFileId: file.id,
31584
+ fileName: file.fileName,
31585
+ band: "read_failed",
31586
+ message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: it appears to be corrupted or deleted. Try uploading it again.`,
31587
+ delivered: false
31588
+ });
29943
31589
  } else if (e instanceof CorruptedFileError) {
29944
31590
  await sendStatusUpdate(`Skipping corrupted file ${file.fileName}. Please try re-uploading`);
29945
31591
  await db.fabfiles.update({
29946
31592
  id: file.id,
29947
31593
  error: e.message
29948
31594
  });
31595
+ fileNotices.push({
31596
+ fabFileId: file.id,
31597
+ fileName: file.fileName,
31598
+ band: "read_failed",
31599
+ message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: ${e.message}`,
31600
+ delivered: false
31601
+ });
29949
31602
  } else {
29950
31603
  logger.updateMetadata({ filePath: file.filePath });
29951
31604
  throw e;
@@ -29965,6 +31618,18 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29965
31618
  processedFiles++;
29966
31619
  if (progressCallback) await progressCallback(processedFiles, totalFiles);
29967
31620
  }))));
31621
+ const noticedFileIds = new Set(fileNotices.map((notice) => notice.fabFileId));
31622
+ for (const file of fabFiles) {
31623
+ if (deliveredFileIds.has(file.id) || noticedFileIds.has(file.id)) continue;
31624
+ logger.warn(`[processFabFilesServer] "${file.fileName}" (${file.id}) contributed no content and produced no notice; reporting it as undelivered.`);
31625
+ fileNotices.push({
31626
+ fabFileId: file.id,
31627
+ fileName: file.fileName,
31628
+ band: "no_readable_content",
31629
+ message: `"${noticeFileName(file.fileName)}" was not sent: no readable content could be extracted from it.`,
31630
+ delivered: false
31631
+ });
31632
+ }
29968
31633
  if (imageContent.length > 0) userMessages.push({
29969
31634
  role: "user",
29970
31635
  content: imageContent
@@ -29992,7 +31657,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
29992
31657
  logger.info(`📁 File processing completed in ${fileProcessingTime}ms for ${fabFiles.length} files`);
29993
31658
  return {
29994
31659
  userMessages,
29995
- errorMessages,
31660
+ fileNotices,
29996
31661
  deliveredFileIds: Array.from(deliveredFileIds),
29997
31662
  fullyDeliveredFileIds: Array.from(fullyDeliveredFileIds)
29998
31663
  };
@@ -30517,6 +32182,8 @@ var llm_exports = /* @__PURE__ */ __exportAll({
30517
32182
  ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
30518
32183
  ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
30519
32184
  BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
32185
+ COSINE_SEARCH_TOP_K: () => 10,
32186
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
30520
32187
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
30521
32188
  FORMAT_PROMPT_PRIORITY: () => 60,
30522
32189
  IMAGE_PROMPT_PRIORITY: () => 50,
@@ -30532,6 +32199,7 @@ var llm_exports = /* @__PURE__ */ __exportAll({
30532
32199
  buildAndSortMessages: () => buildAndSortMessages,
30533
32200
  calculateTotalTokenLength: () => calculateTotalTokenLength,
30534
32201
  computeCosineSimilarity: () => computeCosineSimilarity,
32202
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
30535
32203
  effectiveContextWindow: () => effectiveContextWindow,
30536
32204
  fetchAgentConversationHistory: () => fetchAgentConversationHistory,
30537
32205
  fetchAndConvertFabFiles: () => fetchAndConvertFabFiles,
@@ -32133,6 +33801,7 @@ const OPENAI_IMAGE_CLIENT_OPTS = {
32133
33801
  maxRetries: 0
32134
33802
  };
32135
33803
  const ALTERNATIVE_IMAGE_MODELS = "Flux Pro, Flux Dev, or Grok";
33804
+ const truncatePromptForLog = (prompt) => prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
32136
33805
  /**
32137
33806
  * Builds a user-friendly error when OpenAI's safety system blocks an image
32138
33807
  * request, guiding the user to rephrase or switch to an alternative model.
@@ -32160,6 +33829,53 @@ function buildModerationBlockedError(error) {
32160
33829
 
32161
33830
  Tip: Switch to an alternative model with different content policies — e.g. ${ALTERNATIVE_IMAGE_MODELS} — which may accept this prompt.\n\nIf you believe this is an error, you can report it to OpenAI with request ID: ${requestId}`);
32162
33831
  }
33832
+ /**
33833
+ * Splits a WIDTHxHEIGHT size into its two edges, or null when the value is not a
33834
+ * pair of non-zero numbers (e.g. 'auto', '', 'wide'). Null means "not a custom
33835
+ * resolution" rather than "invalid": generate() has always left such values
33836
+ * untouched, and that behaviour is preserved.
33837
+ */
33838
+ function parseSizeEdges(size) {
33839
+ if (typeof size !== "string") return null;
33840
+ const [width, height] = size.split("x").map(Number);
33841
+ if (!width || !height) return null;
33842
+ return {
33843
+ width,
33844
+ height
33845
+ };
33846
+ }
33847
+ /**
33848
+ * True when a custom gpt-image-2 resolution meets OpenAI's documented limits.
33849
+ * gpt-image-2 accepts any resolution satisfying these, not only the presets in
33850
+ * OPENAI_GPT_IMAGE_2_IMAGE_SIZES, so a flat preset check would reject valid
33851
+ * custom sizes. Must stay the single source of this rule for generate() and edit().
33852
+ */
33853
+ function satisfiesGptImage2Constraints({ width, height }) {
33854
+ const { maxEdge, minTotalPixels, maxTotalPixels, edgeMultiple, maxAspectRatio } = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.constraints;
33855
+ const longEdge = Math.max(width, height);
33856
+ const shortEdge = Math.min(width, height);
33857
+ const totalPixels = width * height;
33858
+ return longEdge <= maxEdge && width % edgeMultiple === 0 && height % edgeMultiple === 0 && longEdge / shortEdge <= maxAspectRatio && totalPixels >= minTotalPixels && totalPixels <= maxTotalPixels;
33859
+ }
33860
+ /**
33861
+ * True when `size` may be forwarded to images.edit for `model`. gpt-image-2 takes
33862
+ * its presets (including 'auto') or any custom WIDTHxHEIGHT meeting the same
33863
+ * constraints generate() enforces; the gpt-image-1 family is limited to its three
33864
+ * fixed sizes. An unsupported size is dropped by the caller so OpenAI applies its
33865
+ * own default instead of rejecting the whole request with a 400.
33866
+ *
33867
+ * GPT-Image tiers only: dall-e-2 has its own size list and passes size through
33868
+ * untouched, so do not route that model here.
33869
+ */
33870
+ function isSupportedEditSize(model, size) {
33871
+ if (typeof size !== "string") return false;
33872
+ if (isGPTImage2Model(model)) {
33873
+ if (OPENAI_GPT_IMAGE_2_IMAGE_SIZES.includes(size)) return true;
33874
+ const edges = parseSizeEdges(size);
33875
+ return edges !== null && satisfiesGptImage2Constraints(edges);
33876
+ }
33877
+ return OPENAI_GPT_IMAGE_1_IMAGE_SIZES.includes(size);
33878
+ }
32163
33879
  var OpenAIImageService = class extends AIImageService {
32164
33880
  async generate(prompt, options) {
32165
33881
  const openai = new OpenAI({
@@ -32184,16 +33900,11 @@ var OpenAIImageService = class extends AIImageService {
32184
33900
  }
32185
33901
  if (isGPTImage2Model(options.model)) {
32186
33902
  if (openaiOptions.size && openaiOptions.size !== "auto") {
32187
- const [w, h] = openaiOptions.size.split("x").map(Number);
32188
- if (w && h) {
32189
- const maxEdge = Math.max(w, h);
32190
- const minEdge = Math.min(w, h);
32191
- const totalPixels = w * h;
32192
- if (maxEdge > 3840 || w % 16 !== 0 || h % 16 !== 0 || maxEdge / minEdge > 3 || totalPixels < 655360 || totalPixels > 8294400) {
32193
- const originalSize = openaiOptions.size;
32194
- openaiOptions.size = "1024x1024";
32195
- parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
32196
- }
33903
+ const edges = parseSizeEdges(openaiOptions.size);
33904
+ if (edges && !satisfiesGptImage2Constraints(edges)) {
33905
+ const originalSize = openaiOptions.size;
33906
+ openaiOptions.size = "1024x1024";
33907
+ parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
32197
33908
  }
32198
33909
  } else if (!openaiOptions.size) openaiOptions.size = "auto";
32199
33910
  } else {
@@ -32246,6 +33957,10 @@ var OpenAIImageService = class extends AIImageService {
32246
33957
  const imageFile = new File([pngBuffer], "image.png", { type: "image/png" });
32247
33958
  if (isGPTImageModel(options.model)) {
32248
33959
  const editModel = options.model || ImageModels.GPT_IMAGE_2;
33960
+ this.logger.log("OpenAI image generation request (edit endpoint, image-to-image):", {
33961
+ model: editModel,
33962
+ prompt: truncatePromptForLog(prompt)
33963
+ });
32249
33964
  result = await openai.images.edit({
32250
33965
  model: editModel,
32251
33966
  image: [imageFile],
@@ -32253,20 +33968,31 @@ var OpenAIImageService = class extends AIImageService {
32253
33968
  });
32254
33969
  } else {
32255
33970
  const { style, quality, model, ...opts } = openaiOptions;
33971
+ const variationSize = [
33972
+ "256x256",
33973
+ "512x512",
33974
+ "1024x1024"
33975
+ ].find((s) => s === openaiOptions.size);
33976
+ this.logger.log("OpenAI image generation request (variation endpoint):", {
33977
+ ...opts,
33978
+ size: variationSize
33979
+ });
32256
33980
  result = await openai.images.createVariation({
32257
33981
  ...opts,
32258
33982
  image: imageFile,
32259
- size: [
32260
- "256x256",
32261
- "512x512",
32262
- "1024x1024"
32263
- ].find((s) => s === openaiOptions.size)
33983
+ size: variationSize
32264
33984
  });
32265
33985
  }
32266
- } else result = await openai.images.generate({
32267
- prompt,
32268
- ...openaiOptions
32269
- });
33986
+ } else {
33987
+ this.logger.log("OpenAI image generation request:", {
33988
+ prompt: truncatePromptForLog(prompt),
33989
+ ...openaiOptions
33990
+ });
33991
+ result = await openai.images.generate({
33992
+ prompt,
33993
+ ...openaiOptions
33994
+ });
33995
+ }
32270
33996
  images = this.imageResponseToUrl(result);
32271
33997
  return images;
32272
33998
  } catch (error) {
@@ -32323,10 +34049,21 @@ var OpenAIImageService = class extends AIImageService {
32323
34049
  Logger.globalInstance.debug(`[DEBUG] ⚠️ Edit endpoint doesn't support ${model}, defaulting to gpt-image-2`);
32324
34050
  editModel = ImageModels.GPT_IMAGE_2;
32325
34051
  }
34052
+ const forwardSize = isSupportedEditSize(editModel, size);
34053
+ this.logger.log("OpenAI image edit request:", {
34054
+ model: editModel,
34055
+ prompt: truncatePromptForLog(prompt),
34056
+ hasMask: !!maskFile,
34057
+ n,
34058
+ size,
34059
+ response_format
34060
+ });
32326
34061
  const response = await openai.images.edit(isGPTImageModel(editModel) ? {
32327
34062
  model: editModel,
32328
34063
  image: [imageFile],
32329
- prompt
34064
+ prompt,
34065
+ ...forwardSize ? { size } : {},
34066
+ ...maskFile ? { mask: maskFile } : {}
32330
34067
  } : {
32331
34068
  model: editModel,
32332
34069
  image: imageFile,
@@ -32437,7 +34174,12 @@ var BFLImageService = class extends AIImageService {
32437
34174
  if (height) requestBody.height = height;
32438
34175
  }
32439
34176
  const cleanedBody = this.stripNullFields(requestBody);
32440
- Logger.globalInstance.debug("[DEBUG] BFL Image generation request body:", cleanedBody);
34177
+ const safeRequestBody = {
34178
+ ...cleanedBody,
34179
+ prompt: prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt,
34180
+ ...cleanedBody.image_prompt ? { image_prompt: `[BASE64_DATA_${cleanedBody.image_prompt.length}_CHARS]` } : {}
34181
+ };
34182
+ Logger.globalInstance.log("BFL Image generation request body:", safeRequestBody);
32441
34183
  const submitResponse = await axios.post(`${this.baseUrl}/${model}`, cleanedBody, { headers: {
32442
34184
  accept: "application/json",
32443
34185
  "x-key": this.apiKey,
@@ -32450,7 +34192,7 @@ var BFLImageService = class extends AIImageService {
32450
34192
  pollingUrl,
32451
34193
  responseData: submitResponse.data,
32452
34194
  endpoint: `${this.baseUrl}/${model}`,
32453
- requestBody
34195
+ requestBody: safeRequestBody
32454
34196
  });
32455
34197
  const imageUrl = await this.pollForResult(requestId, pollingUrl);
32456
34198
  Logger.globalInstance.debug("[DEBUG] Received BFL image URL:", {
@@ -32859,8 +34601,6 @@ var GeminiImageService = class extends AIImageService {
32859
34601
  const aspectRatio = this.resolveAspectRatio(options);
32860
34602
  if (aspectRatio) config.aspectRatio = aspectRatio;
32861
34603
  if (options.guidance !== null && options.guidance !== void 0) config.guidanceScale = options.guidance;
32862
- if (options.prompt_upsampling !== void 0) config.enhancePrompt = options.prompt_upsampling;
32863
- if (options.seed !== null && options.seed !== void 0) config.seed = options.seed;
32864
34604
  const outputMimeType = this.resolveMimeType(options.output_format);
32865
34605
  if (outputMimeType) config.outputMimeType = outputMimeType;
32866
34606
  return config;
@@ -34171,11 +35911,11 @@ const DEFAULT_FALLBACK_CHAIN = [
34171
35911
  */
34172
35912
  function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logger, excludeModelIds, preferUntriedBackend) {
34173
35913
  logger.info(`🔍 Finding automatic fallback for ${originalModel.id}`);
34174
- const hasValidKey = (m) => !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
35914
+ const hasValidKey = (m) => !m.disabled && !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
34175
35915
  const preferences = [...FALLBACK_PREFERENCES[originalModel.id] ?? []];
34176
35916
  if (preferences.length === 0) preferences.push(...DEFAULT_FALLBACK_CHAIN);
34177
35917
  if (preferUntriedBackend) {
34178
- const triedBackends = /* @__PURE__ */ new Set([originalModel.backend]);
35918
+ const triedBackends = new Set(originalModel.backend ? [originalModel.backend] : []);
34179
35919
  for (const id of excludeModelIds ?? []) {
34180
35920
  const tried = availableModels.find((m) => m.id === id);
34181
35921
  if (tried) triedBackends.add(tried.backend);
@@ -34202,7 +35942,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
34202
35942
  return model;
34203
35943
  }
34204
35944
  }
34205
- logger.error(" No suitable automatic fallback model found");
35945
+ logger.warn("⚠️ No suitable automatic fallback model found");
34206
35946
  return null;
34207
35947
  }
34208
35948
  /**
@@ -34272,6 +36012,39 @@ async function getLlmWithFallback(originalModel, fallbackModelId, availableModel
34272
36012
  return null;
34273
36013
  }
34274
36014
  /**
36015
+ * Pick a runnable substitute for a model id that is NOT in `availableModels` at all -
36016
+ * a sunset id a catalog lifecycle row has hidden, or one disabled since whatever
36017
+ * persisted it (a session pin, a stored mapping row) was written.
36018
+ *
36019
+ * `getLlmWithFallback` cannot serve this case: its `originalModel` is a `ModelInfo`,
36020
+ * which by definition does not exist here. This walks the same
36021
+ * `FALLBACK_PREFERENCES` -> `DEFAULT_FALLBACK_CHAIN` -> any-keyed-model selection
36022
+ * through the same `findAutomaticFallback`, so the substitute a hidden id lands on
36023
+ * cannot diverge from the one a listed-but-failing id would.
36024
+ *
36025
+ * Callers should run `resolveDeprecatedModelId` first: a sunset id with a known
36026
+ * successor should be forwarded to it (and counted as a `[model-sunset]`) rather
36027
+ * than treated as a model with no answer.
36028
+ */
36029
+ function findFallbackForMissingModel(missingModelId, availableModels, apiKeyTable, logger, endUserId) {
36030
+ const fallbackModel = findAutomaticFallback({ id: missingModelId }, availableModels, apiKeyTable, logger);
36031
+ if (!fallbackModel) return null;
36032
+ const backend = (0, llm_exports.getLlmByModel)(apiKeyTable, {
36033
+ modelInfo: fallbackModel,
36034
+ logger,
36035
+ endUserId
36036
+ });
36037
+ if (!backend) {
36038
+ logger.warn(`⚠️ Fallback for unavailable model ${missingModelId} failed to initialize: ${fallbackModel.id}`);
36039
+ return null;
36040
+ }
36041
+ return {
36042
+ model: fallbackModel,
36043
+ backend,
36044
+ attempt: 1
36045
+ };
36046
+ }
36047
+ /**
34275
36048
  * Tiktoken-based implementation of the tokenizer interface
34276
36049
  * Provides caching for performance and configurable logging
34277
36050
  *
@@ -34317,6 +36090,21 @@ var TiktokenTokenizer = class {
34317
36090
  return Array.from(encoder.encode_ordinary(text));
34318
36091
  }
34319
36092
  /**
36093
+ * Decode token ids back to text through the same encoder encodeTokens used, so an
36094
+ * encode -> slice -> decode round trip yields real text rather than the ids themselves.
36095
+ * @param tokens - Token ids, typically a slice of an encodeTokens result
36096
+ * @param modelId - Model ID to determine encoding (must match the one used to encode)
36097
+ * @returns Promise<string> - The decoded text
36098
+ *
36099
+ * tiktoken's wasm decode() hands back raw UTF-8 bytes. A slice that ends mid-character therefore
36100
+ * decodes to a trailing U+FFFD; callers that sliced are expected to trim it.
36101
+ */
36102
+ async decodeTokens(tokens, modelId, logger) {
36103
+ if (this.isShuttingDown) throw new Error("TiktokenTokenizer is shutting down");
36104
+ const encoder = await this.getEncoder(modelId, logger);
36105
+ return new TextDecoder().decode(encoder.decode(new Uint32Array(tokens)));
36106
+ }
36107
+ /**
34320
36108
  * Returns a lightweight ITokenizer proxy that delegates WASM encoder operations
34321
36109
  * to this instance (preserving the shared encoder cache) but routes log output
34322
36110
  * through the provided logger. Useful for attaching per-request context (e.g.
@@ -34325,7 +36113,8 @@ var TiktokenTokenizer = class {
34325
36113
  withLogger(logger) {
34326
36114
  return {
34327
36115
  countTokens: (text, modelId) => this.countTokens(text, modelId, logger),
34328
- encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger)
36116
+ encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger),
36117
+ decodeTokens: (tokens, modelId) => this.decodeTokens(tokens, modelId, logger)
34329
36118
  };
34330
36119
  }
34331
36120
  /**
@@ -34785,6 +36574,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34785
36574
  BaseStorage: () => BaseStorage,
34786
36575
  BedrockEmbeddingService: () => BedrockEmbeddingService,
34787
36576
  CONTENT_TYPE_BY_FORMAT: () => CONTENT_TYPE_BY_FORMAT,
36577
+ COSINE_SEARCH_TOP_K: () => 10,
34788
36578
  CacheKeys: () => CacheKeys,
34789
36579
  ChunkSchema: () => ChunkSchema,
34790
36580
  CircuitBreaker: () => CircuitBreaker,
@@ -34792,6 +36582,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34792
36582
  ClientMessageSender: () => ClientMessageSender,
34793
36583
  CorruptedFileError: () => CorruptedFileError,
34794
36584
  DEFAULT_FALLBACK_CHAIN: () => DEFAULT_FALLBACK_CHAIN,
36585
+ DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
34795
36586
  DEFAULT_PASSAGE_TOKEN_TARGET: () => 512,
34796
36587
  EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
34797
36588
  ElevenLabsMusicGenerator: () => ElevenLabsMusicGenerator,
@@ -34878,7 +36669,9 @@ __reExport(/* @__PURE__ */ __exportAll({
34878
36669
  checkStorageLimit: () => checkStorageLimit,
34879
36670
  checkStorageLimitForFile: () => checkStorageLimitForFile,
34880
36671
  cleanMermaidSyntax: () => cleanMermaidSyntax,
36672
+ computeContentHash: () => computeContentHash,
34881
36673
  computeCosineSimilarity: () => computeCosineSimilarity,
36674
+ computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
34882
36675
  convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
34883
36676
  createQuestPlanToolSchema: () => createQuestPlanToolSchema,
34884
36677
  createTokenizer: () => createTokenizer,
@@ -34894,6 +36687,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34894
36687
  fetchAndParseURL: () => fetchAndParseURL,
34895
36688
  fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
34896
36689
  filterRetrievalExcluded: () => filterRetrievalExcluded,
36690
+ findFallbackForMissingModel: () => findFallbackForMissingModel,
34897
36691
  formatVoiceHistory: () => formatVoiceHistory,
34898
36692
  generateSafeEmbedding: () => generateSafeEmbedding,
34899
36693
  getCachedSignedUrl: () => getCachedSignedUrl,
@@ -34947,6 +36741,7 @@ __reExport(/* @__PURE__ */ __exportAll({
34947
36741
  registerLambdaErrorHandlers: () => registerLambdaErrorHandlers,
34948
36742
  registerProcessErrorHandlers: () => registerProcessErrorHandlers,
34949
36743
  registrableDomain: () => registrableDomain,
36744
+ reservationOutputTokens: () => reservationOutputTokens,
34950
36745
  resolveEmbeddingConfig: () => resolveEmbeddingConfig,
34951
36746
  resolveSupportedMimeType: () => resolveSupportedMimeType,
34952
36747
  safeInputWindow: () => safeInputWindow,