@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.
- package/README.md +3 -1
- package/dist/{AgentHistoryStore-BQiATPsQ.mjs → AgentHistoryStore-ucv6xXVT.mjs} +2292 -497
- package/dist/{ApiClient-BPmlalut.mjs → ApiClient-Ut1MXtOs.mjs} +5 -5
- package/dist/{ConfigStore-CNfbeaJf.mjs → ConfigStore-CoY0l0gr.mjs} +2065 -216
- package/dist/ProxyManager-B1jFWL7b.mjs +3 -0
- package/dist/{buildAgent-DwPvcTpz.mjs → buildAgent-jZhBReAr.mjs} +3 -3
- package/dist/commands/acpCommand.mjs +4 -4
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +4 -4
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +3 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +51 -61
- package/dist/{package-CxHSRXdp.mjs → package-CfIETbXd.mjs} +1 -1
- package/dist/{serve-Du3HiqAH.mjs → serve-C9UDR5px.mjs} +5 -3
- package/package.json +13 -12
- package/dist/ProxyManager-Bqr7Lmsd.mjs +0 -3
- package/dist/{ProxyManager-C5H0pUyK.mjs → ProxyManager-C1-lgzEU.mjs} +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
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
|
|
222
|
+
isDirectory
|
|
206
223
|
};
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
1960
|
+
const params = new URLSearchParams({
|
|
1920
1961
|
q: query,
|
|
1921
1962
|
format: "json",
|
|
1922
1963
|
language: "en",
|
|
1923
1964
|
safesearch: "1"
|
|
1924
|
-
})
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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,
|
|
6461
|
-
Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${
|
|
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 >
|
|
6528
|
-
Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
7654
|
-
|
|
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
|
-
|
|
7665
|
-
return
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
7700
|
-
const isIPv6Address =
|
|
8126
|
+
const isIPv4Address = /^(\d+\.){3}\d+$/.test(hostname);
|
|
8127
|
+
const isIPv6Address = hostname.includes(":");
|
|
7701
8128
|
if (!isIPv4Address && !isIPv6Address) try {
|
|
7702
|
-
const ipv4Addresses = await dnsResolve4(
|
|
7703
|
-
const ipv6Addresses = await dnsResolve6(
|
|
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
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
}
|
|
7767
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = [];
|
|
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
|
-
|
|
12664
|
-
|
|
12665
|
-
if (
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
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
|
|
13491
|
+
? __apply(__strSlice, line, [0, HARD_PER_LINE_BYTES]) + ' [...line truncated]'
|
|
12671
13492
|
: line;
|
|
12672
13493
|
}
|
|
12673
|
-
|
|
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 (
|
|
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
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15484
|
-
|
|
15485
|
-
|
|
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
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
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
|
-
|
|
15519
|
-
if (contentArray.length > 0) {
|
|
16507
|
+
if (contentArray && contentArray.length > 0) {
|
|
15520
16508
|
const lastBlock = contentArray[contentArray.length - 1];
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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:
|
|
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:
|
|
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
|
-
},
|
|
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)
|
|
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:
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
|
|
18613
|
-
|
|
18614
|
-
|
|
18615
|
-
|
|
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
|
-
|
|
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)
|
|
18679
|
-
|
|
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
|
-
|
|
18721
|
-
if (
|
|
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))
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
20871
|
-
const
|
|
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
|
|
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
|
|
20886
|
-
|
|
20887
|
-
|
|
20888
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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)
|
|
21442
|
-
|
|
21443
|
-
|
|
21444
|
-
|
|
21445
|
-
|
|
21446
|
-
|
|
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
|
-
},
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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 =
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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)
|
|
21629
|
-
|
|
21630
|
-
|
|
21631
|
-
|
|
21632
|
-
|
|
21633
|
-
|
|
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
|
-
},
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
})
|
|
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)
|
|
22018
|
-
|
|
22019
|
-
|
|
22020
|
-
|
|
22021
|
-
|
|
22022
|
-
|
|
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
|
-
/**
|
|
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
|
|
23478
|
+
return toolCalls.map((tc) => ({
|
|
22146
23479
|
name: tc.function.name,
|
|
22147
23480
|
arguments: JSON.stringify(tc.function.arguments ?? {}),
|
|
22148
|
-
id: `ollama-tool-${
|
|
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-${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
23890
|
-
|
|
23891
|
-
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
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)
|
|
24299
|
-
|
|
24300
|
-
|
|
24301
|
-
|
|
24302
|
-
|
|
24303
|
-
|
|
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
|
-
},
|
|
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)
|
|
24467
|
-
|
|
24468
|
-
|
|
24469
|
-
|
|
24470
|
-
|
|
24471
|
-
|
|
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
|
-
},
|
|
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],
|
|
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
|
-
|
|
26773
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
29433
|
-
|
|
29434
|
-
|
|
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 <
|
|
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 >
|
|
31245
|
+
if (ranked.length > 10) {
|
|
29663
31246
|
ranked.sort(compareRankedChunks);
|
|
29664
|
-
ranked.length =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
29765
|
-
|
|
29766
|
-
|
|
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
|
-
|
|
29797
|
-
|
|
29798
|
-
|
|
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
|
|
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:
|
|
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))
|
|
29844
|
-
|
|
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
|
-
|
|
29921
|
-
|
|
29922
|
-
|
|
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"))
|
|
29937
|
-
|
|
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
|
-
|
|
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
|
|
32188
|
-
if (
|
|
32189
|
-
const
|
|
32190
|
-
|
|
32191
|
-
|
|
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
|
|
32267
|
-
|
|
32268
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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,
|