@bike4mind/cli 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AgentHistoryStore-C8uUKjjC.mjs → AgentHistoryStore-T7Oh84Yn.mjs} +1454 -350
- package/dist/{ApiClient-B_CQrUiF.mjs → ApiClient-BopvMmQk.mjs} +5 -5
- package/dist/{ConfigStore-DD3DcC3-.mjs → ConfigStore-cIyF7hDg.mjs} +1048 -70
- package/dist/ProxyManager-B1jFWL7b.mjs +3 -0
- package/dist/{buildAgent-mVuXU_H4.mjs → buildAgent-P0tOMLt1.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 +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +83 -34
- package/dist/{package-BqKSCbso.mjs → package-CnVCHR3U.mjs} +1 -1
- package/dist/{serve-CuF0I5en.mjs → serve-BocVOJ3W.mjs} +2 -2
- package/package.json +9 -9
- 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 VoyageAIEmbeddingModel, A as HTTPError, At as resolveHistoryFetchLimit, B as PermissionDeniedError, Bt as isNearLimit, Ct as isRetryableError, D as FIXED_TEMPERATURE_MODELS, Dt as isZodError, E as FIELD_GROUP_OF, Et as isUserInitiatedAbort, F as ModelBackend, Ft as usdToCredits, G as SpeechToTextModels, H as REASONING_SUPPORTED_MODELS, I as NO_TEMPERATURE_MODELS, It as usdToCreditsStochastic, J as TooManyRequestsError, K as SupportedFabFileMimeTypes, L as NotFoundError, Lt as withRetry, M as ImageModels, Mt as settingsMap, N as InternalServerError, Nt as toModelInfo, O as FORMAT_PROMPT_TEMPLATE, Ot as mapMimeTypeToArtifactType, P as MODEL_INFO_FIELD_GROUP_OF, Pt as toModelRecord, Q as VideoModels, R as OllamaEmbeddingModel, Rt as buildRateLimitLogEntry, S as CorruptedFileError, St as isRenderableModelType, Tt as isUnlimitedHistory, U as REFUSAL_FALLBACK_MODELS, V as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Vt as parseRateLimitHeaders, W as RESPONSES_API_TOOL_MODELS, X as UnprocessableEntityError, Y as UnauthorizedError, Z as VIDEO_SIZE_CONSTRAINTS, _ as BadRequestError, _t as isImageServeable, at as getMcpProviderMetadata, bt as isModelDeprecated, ct as isAudioMimeType, et as WORK_ITEM_STATUSES, ft as isFieldGroup, g as BFL_SAFETY_TOLERANCE, gt as isImageAttachment, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as isGeminiModelId, it as defaultEmbeddingModelForEnv, j as HttpStatus, jt as secureParameters, k as ForbiddenError, kt as obfuscateApiKey, lt as isChunkRebuildPending, m as ApiKeyType, mt as isGPTImageModel, n as logger, nt as calculateRetryDelay, ot as getQuestErrorCode, p as ARTIFACT_ATTRS_PATTERN, pt as isGPTImage2Model, q as TTS_MAX_INPUT_CHARS, rt as dayjsConfig_default, st as getRetryAfterMs, tt as applyModelPriceCatalog, ut as isConvergencePausedNote, v as BedrockEmbeddingModel, vt as isMediaModelType, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isSupportedFabFileMimeType, x as ChatModels, xt as isPlaceholderApiKey, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, yt as isModelAccessible, z as OpenAIEmbeddingModel, zt as extractSnippetMeta } from "./ConfigStore-cIyF7hDg.mjs";
|
|
3
3
|
import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
|
|
4
4
|
import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
5
5
|
import { execFile, execFileSync, spawn } from "child_process";
|
|
@@ -37,13 +37,14 @@ import OpenAI from "openai";
|
|
|
37
37
|
import { VoyageAIClient } from "voyageai";
|
|
38
38
|
import mime from "mime-types";
|
|
39
39
|
import dns from "dns";
|
|
40
|
+
import http from "http";
|
|
41
|
+
import https, { Agent } from "https";
|
|
40
42
|
import { promisify } from "util";
|
|
41
43
|
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
42
44
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
43
45
|
import { Readable } from "stream";
|
|
44
46
|
import { fileTypeFromBuffer } from "file-type";
|
|
45
47
|
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
|
46
|
-
import { Agent } from "https";
|
|
47
48
|
import "@opensearch-project/opensearch";
|
|
48
49
|
import "@aws-sdk/credential-provider-node";
|
|
49
50
|
import "@opensearch-project/opensearch/aws-v3";
|
|
@@ -110,6 +111,18 @@ const COMPACTION_SUMMARY_MARKER = "[Previous conversation summary]";
|
|
|
110
111
|
//#endregion
|
|
111
112
|
//#region src/utils/fileSearch.ts
|
|
112
113
|
/**
|
|
114
|
+
* stat() that yields null instead of throwing. Following a symlink is best-effort here:
|
|
115
|
+
* a dangling link, or one pointing somewhere unreadable, must not take down a whole
|
|
116
|
+
* directory listing.
|
|
117
|
+
*/
|
|
118
|
+
function statOrNull(targetPath) {
|
|
119
|
+
try {
|
|
120
|
+
return fs$2.statSync(targetPath);
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
113
126
|
* Load gitignore rules from project root
|
|
114
127
|
*/
|
|
115
128
|
function loadIgnoreRules(projectRoot) {
|
|
@@ -200,13 +213,17 @@ function listAbsoluteDirectory(absolutePath, filterQuery) {
|
|
|
200
213
|
}
|
|
201
214
|
return filteredEntries.slice(0, 15).map((entry) => {
|
|
202
215
|
const fullPath = path$1.join(normalizedPath, entry.name);
|
|
216
|
+
const target = entry.isSymbolicLink() ? statOrNull(fullPath) : null;
|
|
217
|
+
const isDirectory = target ? target.isDirectory() : entry.isDirectory();
|
|
218
|
+
const isFile = target ? target.isFile() : entry.isFile();
|
|
203
219
|
const result = {
|
|
204
220
|
path: fullPath,
|
|
205
|
-
isDirectory
|
|
221
|
+
isDirectory
|
|
206
222
|
};
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
if (isFile) {
|
|
224
|
+
const fileStats = target ?? statOrNull(fullPath);
|
|
225
|
+
if (fileStats) result.size = fileStats.size;
|
|
226
|
+
}
|
|
210
227
|
return result;
|
|
211
228
|
});
|
|
212
229
|
} catch {
|
|
@@ -1555,7 +1572,7 @@ function webFetchBody(result) {
|
|
|
1555
1572
|
if (result.extractedChars === 0 && result.offset > 0) return `[web_fetch: offset ${result.offset} is at or beyond the end of the content (~${result.originalChars} chars); nothing further to read.]`;
|
|
1556
1573
|
return result.truncated ? result.markdown + truncationMarker(result) : result.markdown;
|
|
1557
1574
|
}
|
|
1558
|
-
function isPdfUrl(url) {
|
|
1575
|
+
function isPdfUrl$2(url) {
|
|
1559
1576
|
try {
|
|
1560
1577
|
const { pathname } = new URL(url);
|
|
1561
1578
|
return pathname.toLowerCase().endsWith(".pdf");
|
|
@@ -1615,7 +1632,7 @@ async function probeLlmsTxt(pageUrl) {
|
|
|
1615
1632
|
*/
|
|
1616
1633
|
async function firecrawlFetch(adapters, url, options) {
|
|
1617
1634
|
if (!/^https?:\/\/.+/i.test(url)) throw new Error(`Invalid URL format: ${url}. URL must start with http:// or https://`);
|
|
1618
|
-
const isPdf = isPdfUrl(url);
|
|
1635
|
+
const isPdf = isPdfUrl$2(url);
|
|
1619
1636
|
const desiredTimeout = isPdf ? PDF_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
1620
1637
|
const timeoutMs = options?.maxTimeoutMs ? Math.min(desiredTimeout, options.maxTimeoutMs) : desiredTimeout;
|
|
1621
1638
|
const app = createFirecrawlApp(await (0, apiKeyService_exports.getFirecrawlConfig)(adapters));
|
|
@@ -2044,7 +2061,7 @@ const webSearchTool = {
|
|
|
2044
2061
|
})
|
|
2045
2062
|
};
|
|
2046
2063
|
//#endregion
|
|
2047
|
-
//#region ../../b4m-core/services/dist/toolGenerators-
|
|
2064
|
+
//#region ../../b4m-core/services/dist/toolGenerators-D3QFkvc-.mjs
|
|
2048
2065
|
const diceRoll = async (parameters) => {
|
|
2049
2066
|
if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
|
|
2050
2067
|
return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
|
|
@@ -2731,11 +2748,12 @@ const askUserQuestionTool = {
|
|
|
2731
2748
|
* re-export them without pulling the full tool graph. `index.ts` re-exports them
|
|
2732
2749
|
* so the server barrel's public API is unchanged.
|
|
2733
2750
|
*/
|
|
2734
|
-
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
2751
|
+
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
2735
2752
|
const context = {
|
|
2736
2753
|
userId,
|
|
2737
2754
|
user,
|
|
2738
2755
|
sessionId,
|
|
2756
|
+
questId,
|
|
2739
2757
|
logger,
|
|
2740
2758
|
db,
|
|
2741
2759
|
storage,
|
|
@@ -2750,9 +2768,14 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope }, s
|
|
|
2750
2768
|
entitlementKeys,
|
|
2751
2769
|
retrievalFilter,
|
|
2752
2770
|
kbScope,
|
|
2771
|
+
inlinedAttachmentIds,
|
|
2772
|
+
fullyInlinedAttachmentIds,
|
|
2773
|
+
suppressLakeArms,
|
|
2774
|
+
sessionRetrievalTags,
|
|
2753
2775
|
codeMinifier,
|
|
2754
2776
|
availableModels,
|
|
2755
|
-
onToolLlmUsage
|
|
2777
|
+
onToolLlmUsage,
|
|
2778
|
+
getAbortSignal
|
|
2756
2779
|
};
|
|
2757
2780
|
return Object.entries(tools).reduce((acc, [key, tool]) => ({
|
|
2758
2781
|
...acc,
|
|
@@ -5436,6 +5459,61 @@ function extractCommandName(filename) {
|
|
|
5436
5459
|
return name;
|
|
5437
5460
|
}
|
|
5438
5461
|
//#endregion
|
|
5462
|
+
//#region src/utils/findMarkdownFiles.ts
|
|
5463
|
+
/**
|
|
5464
|
+
* Classify a directory entry, resolving symlinks by their target.
|
|
5465
|
+
*
|
|
5466
|
+
* `fs.readdir(withFileTypes)` builds each Dirent from the entry itself, never
|
|
5467
|
+
* the link target, so a symlink reports `isFile() === false` AND
|
|
5468
|
+
* `isDirectory() === false`. A walk keyed off those two predicates alone skips
|
|
5469
|
+
* every symlinked entry.
|
|
5470
|
+
*/
|
|
5471
|
+
async function classifyEntry(fullPath, entry) {
|
|
5472
|
+
if (!entry.isSymbolicLink()) {
|
|
5473
|
+
if (entry.isDirectory()) return "directory";
|
|
5474
|
+
return entry.isFile() ? "file" : "other";
|
|
5475
|
+
}
|
|
5476
|
+
try {
|
|
5477
|
+
const stats = await fs$1.stat(fullPath);
|
|
5478
|
+
if (stats.isDirectory()) return "directory";
|
|
5479
|
+
return stats.isFile() ? "file" : "other";
|
|
5480
|
+
} catch {
|
|
5481
|
+
return "other";
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
/**
|
|
5485
|
+
* Recursively collect every `.md` file under `directory`, following symlinks.
|
|
5486
|
+
*
|
|
5487
|
+
* Symlink support is load-bearing, not a nicety: dotfile managers (nix
|
|
5488
|
+
* home-manager, chezmoi, a plain dotfiles repo) materialize
|
|
5489
|
+
* `~/.claude/skills/<name>/SKILL.md` as a symlink into an immutable store, so a
|
|
5490
|
+
* walk that only accepts real files finds zero global skills on those machines.
|
|
5491
|
+
*
|
|
5492
|
+
* Directories are deduped by realpath so a cyclic link cannot spin forever.
|
|
5493
|
+
*/
|
|
5494
|
+
async function findMarkdownFiles(directory, visitedRealPaths = /* @__PURE__ */ new Set()) {
|
|
5495
|
+
const files = [];
|
|
5496
|
+
try {
|
|
5497
|
+
const realDirectory = await fs$1.realpath(directory);
|
|
5498
|
+
if (visitedRealPaths.has(realDirectory)) return files;
|
|
5499
|
+
visitedRealPaths.add(realDirectory);
|
|
5500
|
+
} catch {}
|
|
5501
|
+
let entries;
|
|
5502
|
+
try {
|
|
5503
|
+
entries = await fs$1.readdir(directory, { withFileTypes: true });
|
|
5504
|
+
} catch (error) {
|
|
5505
|
+
console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
|
|
5506
|
+
return files;
|
|
5507
|
+
}
|
|
5508
|
+
for (const entry of entries) {
|
|
5509
|
+
const fullPath = path.join(directory, entry.name);
|
|
5510
|
+
const kind = await classifyEntry(fullPath, entry);
|
|
5511
|
+
if (kind === "directory") files.push(...await findMarkdownFiles(fullPath, visitedRealPaths));
|
|
5512
|
+
else if (kind === "file" && entry.name.endsWith(".md")) files.push(fullPath);
|
|
5513
|
+
}
|
|
5514
|
+
return files;
|
|
5515
|
+
}
|
|
5516
|
+
//#endregion
|
|
5439
5517
|
//#region src/storage/CustomCommandStore.ts
|
|
5440
5518
|
var CustomCommandStore = class {
|
|
5441
5519
|
constructor(projectRoot, options = {}) {
|
|
@@ -5513,7 +5591,7 @@ var CustomCommandStore = class {
|
|
|
5513
5591
|
async loadCommandsFromDirectory(directory, source) {
|
|
5514
5592
|
try {
|
|
5515
5593
|
if (!(await fs$1.stat(directory)).isDirectory()) return;
|
|
5516
|
-
const commandFiles = await
|
|
5594
|
+
const commandFiles = await findMarkdownFiles(directory);
|
|
5517
5595
|
for (const filePath of commandFiles) try {
|
|
5518
5596
|
await this.loadCommandFile(filePath, source);
|
|
5519
5597
|
} catch (error) {
|
|
@@ -5524,28 +5602,6 @@ var CustomCommandStore = class {
|
|
|
5524
5602
|
}
|
|
5525
5603
|
}
|
|
5526
5604
|
/**
|
|
5527
|
-
* Recursively finds all .md files in a directory
|
|
5528
|
-
*
|
|
5529
|
-
* @param directory - Directory to search
|
|
5530
|
-
* @returns Array of full file paths to .md files
|
|
5531
|
-
*/
|
|
5532
|
-
async findCommandFiles(directory) {
|
|
5533
|
-
const files = [];
|
|
5534
|
-
try {
|
|
5535
|
-
const entries = await fs$1.readdir(directory, { withFileTypes: true });
|
|
5536
|
-
for (const entry of entries) {
|
|
5537
|
-
const fullPath = path.join(directory, entry.name);
|
|
5538
|
-
if (entry.isDirectory()) {
|
|
5539
|
-
const subFiles = await this.findCommandFiles(fullPath);
|
|
5540
|
-
files.push(...subFiles);
|
|
5541
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
|
|
5542
|
-
}
|
|
5543
|
-
} catch (error) {
|
|
5544
|
-
console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
|
|
5545
|
-
}
|
|
5546
|
-
return files;
|
|
5547
|
-
}
|
|
5548
|
-
/**
|
|
5549
5605
|
* Loads a single command file
|
|
5550
5606
|
*
|
|
5551
5607
|
* @param filePath - Full path to the command file
|
|
@@ -7000,6 +7056,41 @@ const ChunkSchema = z$1.object({
|
|
|
7000
7056
|
const isEmbeddingModel = (model, modelEnum) => {
|
|
7001
7057
|
return Object.values(modelEnum).includes(model);
|
|
7002
7058
|
};
|
|
7059
|
+
/**
|
|
7060
|
+
* The embedding model's context window in tokens - the HARD ceiling an embedding call accepts.
|
|
7061
|
+
* Extracted from SmartChunker so a caller (the chunk queue handler, #1662) can compute the same
|
|
7062
|
+
* effective passage limit for owner-altitude policy resolution and cross-lake conflict reporting
|
|
7063
|
+
* without constructing a chunker. Throws on an unsupported model, exactly as the chunker does.
|
|
7064
|
+
*/
|
|
7065
|
+
function embeddingModelContextWindow(model) {
|
|
7066
|
+
if (isEmbeddingModel(model, OpenAIEmbeddingModel)) return OPENAI_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7067
|
+
if (isEmbeddingModel(model, VoyageAIEmbeddingModel)) return VOYAGEAI_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7068
|
+
if (isEmbeddingModel(model, BedrockEmbeddingModel)) return BEDROCK_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7069
|
+
if (isEmbeddingModel(model, OllamaEmbeddingModel)) return OLLAMA_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7070
|
+
throw new Error(`Unsupported embedding model: ${model}`);
|
|
7071
|
+
}
|
|
7072
|
+
/**
|
|
7073
|
+
* The buffer subtracted from the model window to absorb cross-provider tokenizer differences
|
|
7074
|
+
* (char/4 approximations can undercount by ~8-10% vs tiktoken). A value < 1 is a percent of the
|
|
7075
|
+
* window (floored to >= 32 tokens); a value >= 1 is an absolute token count.
|
|
7076
|
+
*/
|
|
7077
|
+
function embeddingWindowBuffer(maxTokens, bufferPercentOrValue = .2) {
|
|
7078
|
+
return bufferPercentOrValue < 1 ? Math.max(Math.floor(maxTokens * bufferPercentOrValue), 32) : Math.floor(bufferPercentOrValue);
|
|
7079
|
+
}
|
|
7080
|
+
/**
|
|
7081
|
+
* The effective per-chunk token limit the chunker will actually use: the SOFT passage target
|
|
7082
|
+
* (retrieval granularity) hard-capped to the buffered model window (an oversized chunk fails the
|
|
7083
|
+
* embedding call). THE single source of truth for that clamp - SmartChunker's constructor and the
|
|
7084
|
+
* chunk handler's conflict/observability logic (#1662) both derive from it, so a resolved policy
|
|
7085
|
+
* value can never drift from the granularity it actually produces. An omitted/invalid target falls
|
|
7086
|
+
* back to DEFAULT_PASSAGE_TOKEN_TARGET; a supplied one is floored to MIN_PASSAGE_TOKEN_TARGET.
|
|
7087
|
+
*/
|
|
7088
|
+
function effectiveChunkTokenLimit(opts) {
|
|
7089
|
+
const maxTokens = embeddingModelContextWindow(opts.model);
|
|
7090
|
+
const hardLimit = maxTokens - embeddingWindowBuffer(maxTokens, opts.bufferPercentOrValue ?? .2);
|
|
7091
|
+
const { passageTokenTarget } = opts;
|
|
7092
|
+
return Math.min(hardLimit, passageTokenTarget !== void 0 && Number.isFinite(passageTokenTarget) && passageTokenTarget > 0 ? Math.max(Math.floor(passageTokenTarget), 64) : 512);
|
|
7093
|
+
}
|
|
7003
7094
|
var SmartChunker = class {
|
|
7004
7095
|
logger;
|
|
7005
7096
|
model;
|
|
@@ -7008,6 +7099,7 @@ var SmartChunker = class {
|
|
|
7008
7099
|
encoder;
|
|
7009
7100
|
storage;
|
|
7010
7101
|
bufferPercentOrValue;
|
|
7102
|
+
lastExtractedText;
|
|
7011
7103
|
/**
|
|
7012
7104
|
* @param model - The embedding model name
|
|
7013
7105
|
* @param storage - Storage instance for file content
|
|
@@ -7023,25 +7115,20 @@ var SmartChunker = class {
|
|
|
7023
7115
|
passageTokenTarget: void 0
|
|
7024
7116
|
} : options ?? {};
|
|
7025
7117
|
this.model = model;
|
|
7026
|
-
|
|
7027
|
-
else if (isEmbeddingModel(model, VoyageAIEmbeddingModel)) this.maxTokens = VOYAGEAI_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7028
|
-
else if (isEmbeddingModel(model, BedrockEmbeddingModel)) this.maxTokens = BEDROCK_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7029
|
-
else if (isEmbeddingModel(model, OllamaEmbeddingModel)) this.maxTokens = OLLAMA_EMBEDDING_MODEL_MAP[model].contextWindow;
|
|
7030
|
-
else throw new Error(`Unsupported embedding model: ${model}`);
|
|
7118
|
+
this.maxTokens = embeddingModelContextWindow(model);
|
|
7031
7119
|
this.bufferPercentOrValue = bufferPercentOrValue ?? .2;
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
this.chunkTokenLimit = Math.min(hardLimit, target);
|
|
7120
|
+
this.chunkTokenLimit = effectiveChunkTokenLimit({
|
|
7121
|
+
model,
|
|
7122
|
+
passageTokenTarget,
|
|
7123
|
+
bufferPercentOrValue: this.bufferPercentOrValue
|
|
7124
|
+
});
|
|
7038
7125
|
this.storage = storage;
|
|
7039
7126
|
this.logger.updateMetadata({
|
|
7040
7127
|
model,
|
|
7041
7128
|
maxTokens: this.maxTokens,
|
|
7042
7129
|
chunkTokenLimit: this.chunkTokenLimit,
|
|
7043
7130
|
bufferPercentOrValue: this.bufferPercentOrValue,
|
|
7044
|
-
passageTokenTarget
|
|
7131
|
+
passageTokenTarget
|
|
7045
7132
|
});
|
|
7046
7133
|
}
|
|
7047
7134
|
async initializeEncoder() {
|
|
@@ -7066,6 +7153,13 @@ var SmartChunker = class {
|
|
|
7066
7153
|
this.encoder = void 0;
|
|
7067
7154
|
}
|
|
7068
7155
|
}
|
|
7156
|
+
/**
|
|
7157
|
+
* The canonical extracted text from the most recent chunkFile() call - policy-independent, unlike
|
|
7158
|
+
* the returned chunks. Undefined when the file yielded no extractable text. See lastExtractedText.
|
|
7159
|
+
*/
|
|
7160
|
+
getExtractedText() {
|
|
7161
|
+
return this.lastExtractedText;
|
|
7162
|
+
}
|
|
7069
7163
|
async chunkFile(fileOrContent, mimeType) {
|
|
7070
7164
|
let content;
|
|
7071
7165
|
if (Buffer.isBuffer(fileOrContent)) content = fileOrContent;
|
|
@@ -7075,6 +7169,7 @@ var SmartChunker = class {
|
|
|
7075
7169
|
}
|
|
7076
7170
|
this.logger.updateMetadata({ mimeType });
|
|
7077
7171
|
this.logger.log(`Chunking file with type: ${mimeType}`);
|
|
7172
|
+
this.lastExtractedText = void 0;
|
|
7078
7173
|
if (isAudioMimeType(mimeType)) {
|
|
7079
7174
|
this.logger.log(`Skipping chunking for audio file type: ${mimeType}`);
|
|
7080
7175
|
return [];
|
|
@@ -7112,12 +7207,17 @@ var SmartChunker = class {
|
|
|
7112
7207
|
case SupportedFabFileMimeTypes.PHP:
|
|
7113
7208
|
case SupportedFabFileMimeTypes.RUBY:
|
|
7114
7209
|
case SupportedFabFileMimeTypes.SH:
|
|
7115
|
-
case SupportedFabFileMimeTypes.BASH:
|
|
7116
|
-
|
|
7210
|
+
case SupportedFabFileMimeTypes.BASH: {
|
|
7211
|
+
const textContent = content.toString();
|
|
7212
|
+
this.lastExtractedText = textContent;
|
|
7213
|
+
chunks = await this.chunkText(textContent);
|
|
7117
7214
|
break;
|
|
7215
|
+
}
|
|
7118
7216
|
default:
|
|
7119
7217
|
if (mimeType && mimeType.startsWith("text/")) {
|
|
7120
|
-
|
|
7218
|
+
const textContent = content.toString();
|
|
7219
|
+
this.lastExtractedText = textContent;
|
|
7220
|
+
chunks = await this.chunkText(textContent);
|
|
7121
7221
|
break;
|
|
7122
7222
|
}
|
|
7123
7223
|
this.logger.error(`Unsupported file type: ${mimeType}`);
|
|
@@ -7129,7 +7229,9 @@ var SmartChunker = class {
|
|
|
7129
7229
|
return await this.storage.getContentAsBuffer(file.filePath);
|
|
7130
7230
|
}
|
|
7131
7231
|
async chunkCSV(content) {
|
|
7132
|
-
const
|
|
7232
|
+
const csvString = content.toString("utf8");
|
|
7233
|
+
this.lastExtractedText = csvString;
|
|
7234
|
+
const rows = csvString.split(/\r?\n/).filter((row) => row.trim().length > 0);
|
|
7133
7235
|
const chunks = [];
|
|
7134
7236
|
let currentChunk = [];
|
|
7135
7237
|
let currentTokens = 0;
|
|
@@ -7182,6 +7284,7 @@ var SmartChunker = class {
|
|
|
7182
7284
|
async chunkPDF(content) {
|
|
7183
7285
|
const pdf = await getDocumentProxy(new Uint8Array(content));
|
|
7184
7286
|
const { text } = await extractText(pdf);
|
|
7287
|
+
this.lastExtractedText = Array.isArray(text) ? text.join("\n") : text;
|
|
7185
7288
|
if (typeof text === "string") return this.chunkText(text);
|
|
7186
7289
|
const chunks = [];
|
|
7187
7290
|
let currentChunk = "";
|
|
@@ -7213,7 +7316,9 @@ var SmartChunker = class {
|
|
|
7213
7316
|
return chunks;
|
|
7214
7317
|
}
|
|
7215
7318
|
async chunkJSON(content) {
|
|
7216
|
-
const
|
|
7319
|
+
const jsonString = content.toString();
|
|
7320
|
+
this.lastExtractedText = jsonString;
|
|
7321
|
+
const json = JSON.parse(jsonString);
|
|
7217
7322
|
return this.chunkObject(json);
|
|
7218
7323
|
}
|
|
7219
7324
|
async chunkObject(obj) {
|
|
@@ -7280,6 +7385,7 @@ var SmartChunker = class {
|
|
|
7280
7385
|
}
|
|
7281
7386
|
async chunkDOCX(content) {
|
|
7282
7387
|
const result = await mammoth.extractRawText({ buffer: content });
|
|
7388
|
+
this.lastExtractedText = result.value;
|
|
7283
7389
|
return this.chunkText(result.value);
|
|
7284
7390
|
}
|
|
7285
7391
|
async chunkPPTX(content) {
|
|
@@ -7298,6 +7404,7 @@ var SmartChunker = class {
|
|
|
7298
7404
|
this.logger.warn("PPTX contained no extractable slide text");
|
|
7299
7405
|
return [];
|
|
7300
7406
|
}
|
|
7407
|
+
this.lastExtractedText = fullText;
|
|
7301
7408
|
return this.chunkText(fullText);
|
|
7302
7409
|
}
|
|
7303
7410
|
async chunkText(content) {
|
|
@@ -7372,6 +7479,9 @@ var SmartChunker = class {
|
|
|
7372
7479
|
async chunkExcel(content) {
|
|
7373
7480
|
const { read, utils } = await import("xlsx");
|
|
7374
7481
|
const workbook = read(content, { type: "buffer" });
|
|
7482
|
+
this.lastExtractedText = workbook.SheetNames.map((sheetName) => {
|
|
7483
|
+
return `--- Sheet: ${sheetName} ---\n${utils.sheet_to_json(workbook.Sheets[sheetName], { header: 1 }).filter(Array.isArray).map((row) => JSON.stringify(row)).join("\n")}\n--- End of Sheet: ${sheetName} ---`;
|
|
7484
|
+
}).join("\n");
|
|
7375
7485
|
const chunks = [];
|
|
7376
7486
|
let currentChunk = "";
|
|
7377
7487
|
let currentTokens = 0;
|
|
@@ -7591,6 +7701,8 @@ const dnsResolve6 = promisify(dns.resolve6);
|
|
|
7591
7701
|
* Check if an IPv4 address is in a private/internal range.
|
|
7592
7702
|
*/
|
|
7593
7703
|
function isPrivateIPv4(ip) {
|
|
7704
|
+
const nonCanonical = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
7705
|
+
if (nonCanonical && nonCanonical.slice(1).some((octet) => octet.length > 1 && octet.startsWith("0"))) return true;
|
|
7594
7706
|
const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
7595
7707
|
if (!ipv4Match) return false;
|
|
7596
7708
|
const [, a, b, c] = ipv4Match.map(Number);
|
|
@@ -7611,44 +7723,153 @@ function isPrivateIPv4(ip) {
|
|
|
7611
7723
|
/**
|
|
7612
7724
|
* Check if an IPv6 address is in a private/internal range.
|
|
7613
7725
|
*/
|
|
7726
|
+
/**
|
|
7727
|
+
* Strip the brackets WHATWG URL keeps on an IPv6 hostname: `new URL('http://[::1]/').hostname` is
|
|
7728
|
+
* `'[::1]'`, not `'::1'`. Every literal check below compares against unbracketed forms, so without
|
|
7729
|
+
* this a bracketed address matched nothing and fell through as safe.
|
|
7730
|
+
*
|
|
7731
|
+
* Same treatment as the sibling guards in this repo - `ssrfGuard.ts` and `external-image.ts` both
|
|
7732
|
+
* strip brackets before their literal checks.
|
|
7733
|
+
*/
|
|
7734
|
+
function stripIpv6Brackets(hostname) {
|
|
7735
|
+
const h = hostname.toLowerCase();
|
|
7736
|
+
return h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h;
|
|
7737
|
+
}
|
|
7738
|
+
/**
|
|
7739
|
+
* Canonicalize an IPv6 literal to the RFC 5952 form - leading zeros dropped per hextet, longest run of
|
|
7740
|
+
* two or more zero hextets compressed to `::`. That is the shape both feeders already hand this module
|
|
7741
|
+
* (WHATWG `URL.hostname` at the `validateUrlForFetch` call site, and getaddrinfo answers via
|
|
7742
|
+
* `ssrfSafeLookup`), so it is the shape every prefix arm in `isPrivateIPv6` was written against.
|
|
7743
|
+
* Canonicalizing once here is what lets those arms cover a family instead of enumerating its legal
|
|
7744
|
+
* spellings: `0:0:0:0:0:ffff:127.0.0.1` arrives as `::ffff:127.0.0.1` instead of matching nothing.
|
|
7745
|
+
*
|
|
7746
|
+
* A dotted IPv4 tail is deliberately kept dotted, unlike WHATWG which hexifies it. The mapped branch
|
|
7747
|
+
* below decodes a dotted tail exactly through `isPrivateIPv4` and only blanket-refuses when the tail is
|
|
7748
|
+
* hex, so hexifying here would turn `::ffff:8.8.8.8` into an over-block.
|
|
7749
|
+
*
|
|
7750
|
+
* This cannot over-block: dropping leading zeros only shortens hextets below 0x1000, and every
|
|
7751
|
+
* globally routable address is inside 2000::/3 (first hextet 0x2000-0x3fff), which never carries one.
|
|
7752
|
+
* Input that does not parse as IPv6 is returned untouched, so it keeps whatever verdict it has today -
|
|
7753
|
+
* which is also why the redundant zero-padded arms further down (`2001:0db8:`, `2001:0000:`,
|
|
7754
|
+
* `0064:ff9b:`, `0100::`) are left in place rather than deleted.
|
|
7755
|
+
*/
|
|
7756
|
+
function normalizeIpv6(ip) {
|
|
7757
|
+
const ported = ip.match(/^(\[[^\]]*\]):\d+$/);
|
|
7758
|
+
const bare = stripIpv6Brackets(ported ? ported[1] : ip).replace(/%.*$/, "");
|
|
7759
|
+
const halves = bare.split("::");
|
|
7760
|
+
if (halves.length > 2) return bare;
|
|
7761
|
+
if (halves.length === 2 && halves[0].includes(".")) return bare;
|
|
7762
|
+
const tokens = halves.flatMap((half) => half === "" ? [] : half.split(":"));
|
|
7763
|
+
const dotted = tokens.length > 0 && tokens[tokens.length - 1].includes(".") ? tokens.pop() : void 0;
|
|
7764
|
+
if (!tokens.every((token) => /^[0-9a-f]{1,4}$/.test(token))) return bare;
|
|
7765
|
+
const compressed = halves.length === 2;
|
|
7766
|
+
const width = tokens.length + (dotted ? 2 : 0);
|
|
7767
|
+
if (compressed ? width > 8 : width !== 8) return bare;
|
|
7768
|
+
const hextets = tokens.map((token) => token.replace(/^0+(?=.)/, ""));
|
|
7769
|
+
if (compressed) {
|
|
7770
|
+
const headWidth = halves[0] === "" ? 0 : halves[0].split(":").length;
|
|
7771
|
+
hextets.splice(headWidth, 0, ...new Array(8 - width).fill("0"));
|
|
7772
|
+
}
|
|
7773
|
+
const run = {
|
|
7774
|
+
start: -1,
|
|
7775
|
+
length: 0
|
|
7776
|
+
};
|
|
7777
|
+
for (let i = 0; i < hextets.length; i++) {
|
|
7778
|
+
if (hextets[i] !== "0") continue;
|
|
7779
|
+
let end = i;
|
|
7780
|
+
while (end < hextets.length && hextets[end] === "0") end++;
|
|
7781
|
+
if (end - i > run.length) {
|
|
7782
|
+
run.start = i;
|
|
7783
|
+
run.length = end - i;
|
|
7784
|
+
}
|
|
7785
|
+
i = end;
|
|
7786
|
+
}
|
|
7787
|
+
const body = run.length >= 2 ? `${hextets.slice(0, run.start).join(":")}::${hextets.slice(run.start + run.length).join(":")}` : hextets.join(":");
|
|
7788
|
+
if (dotted === void 0) return body;
|
|
7789
|
+
return body.endsWith(":") ? `${body}${dotted}` : `${body}:${dotted}`;
|
|
7790
|
+
}
|
|
7614
7791
|
function isPrivateIPv6(ip) {
|
|
7615
|
-
const normalized = ip
|
|
7792
|
+
const normalized = normalizeIpv6(ip);
|
|
7793
|
+
if (!normalized.includes(":")) return false;
|
|
7794
|
+
if (/[[\]]/.test(normalized)) return true;
|
|
7795
|
+
if (!/^[0-9a-f:.]+$/.test(normalized)) return false;
|
|
7796
|
+
if (normalized.includes(".") && !/^::(ffff:)?\d+\.\d+\.\d+\.\d+$/.test(normalized)) return true;
|
|
7616
7797
|
if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
|
|
7617
7798
|
if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
|
|
7618
|
-
if (normalized.startsWith("
|
|
7799
|
+
if (normalized.startsWith("fe")) return true;
|
|
7619
7800
|
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
|
|
7620
7801
|
if (normalized.startsWith("ff")) return true;
|
|
7621
|
-
|
|
7622
|
-
|
|
7802
|
+
if (normalized.startsWith("::ffff:")) {
|
|
7803
|
+
const tail = normalized.slice(7);
|
|
7804
|
+
return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
|
|
7805
|
+
}
|
|
7806
|
+
if (normalized.startsWith("::")) {
|
|
7807
|
+
const tail = normalized.slice(2);
|
|
7808
|
+
return /^\d+\.\d+\.\d+\.\d+$/.test(tail) ? isPrivateIPv4(tail) : true;
|
|
7809
|
+
}
|
|
7810
|
+
if (normalized.startsWith("0:")) return true;
|
|
7811
|
+
if (normalized.startsWith("5f00:")) return true;
|
|
7812
|
+
if (normalized.startsWith("3fff:")) {
|
|
7813
|
+
const rest = normalized.slice(5);
|
|
7814
|
+
if ((rest.startsWith(":") ? 0 : parseInt(rest.split(":")[0], 16)) <= 4095) return true;
|
|
7815
|
+
}
|
|
7623
7816
|
if (normalized.startsWith("2001:db8:") || normalized.startsWith("2001:0db8:")) return true;
|
|
7817
|
+
if (normalized.startsWith("2002:")) return true;
|
|
7818
|
+
if (normalized.startsWith("2001:0:") || normalized.startsWith("2001:0000:") || normalized.startsWith("2001::")) return true;
|
|
7624
7819
|
if (normalized.startsWith("100::") || normalized.startsWith("0100::")) return true;
|
|
7625
7820
|
if (normalized.startsWith("64:ff9b:") || normalized.startsWith("0064:ff9b:")) return true;
|
|
7626
7821
|
return false;
|
|
7627
7822
|
}
|
|
7628
7823
|
/**
|
|
7824
|
+
* Strip what belongs to the interface or the transport rather than to the address: a zone index
|
|
7825
|
+
* (`fe80::1%eth0`) and, for an IPv4 literal, a trailing port (`8.8.8.8:443`).
|
|
7826
|
+
*
|
|
7827
|
+
* MUST be shared by every exported entry point. `isPrivateIP` and `isPrivateOrInternalHostname` each carry
|
|
7828
|
+
* their own family gate, and the comment on those gates says why they are kept identical: three gates
|
|
7829
|
+
* disagreeing about what counts as IPv4 is how the bracketed-IPv6 hole happened. Stripping in one of them
|
|
7830
|
+
* only reproduced exactly that - `8.8.8.8:443` came back public from one export and private from the other,
|
|
7831
|
+
* because the second missed its IPv4 branch and was then caught by the misplaced-quad arm.
|
|
7832
|
+
*
|
|
7833
|
+
* Only a DOTTED port is stripped. An unbracketed `ipv6:port` is genuinely ambiguous - a bare IPv6 address is
|
|
7834
|
+
* mostly colons - so it stays refused, and the bracketed spelling is what `normalizeIpv6` handles.
|
|
7835
|
+
*/
|
|
7836
|
+
function stripZoneAndIpv4Port(host) {
|
|
7837
|
+
const head = host.split("%")[0];
|
|
7838
|
+
const zoneless = head.includes(":") || /^(\d+\.){3}\d+$/.test(head) ? head : host;
|
|
7839
|
+
return zoneless.match(/^((?:\d+\.){3}\d+):\d+$/)?.[1] ?? zoneless;
|
|
7840
|
+
}
|
|
7841
|
+
/**
|
|
7629
7842
|
* Check if an IP address (IPv4 or IPv6) is in a private/internal range.
|
|
7630
7843
|
*/
|
|
7631
7844
|
function isPrivateIP(ip) {
|
|
7632
|
-
|
|
7633
|
-
return
|
|
7845
|
+
const address = stripZoneAndIpv4Port(ip);
|
|
7846
|
+
if (/^(\d+\.){3}\d+$/.test(address)) return isPrivateIPv4(address);
|
|
7847
|
+
return isPrivateIPv6(address);
|
|
7634
7848
|
}
|
|
7635
7849
|
/**
|
|
7636
7850
|
* Check if a hostname is known to be private/internal.
|
|
7637
7851
|
* This catches obvious cases before DNS resolution.
|
|
7638
7852
|
*/
|
|
7639
7853
|
function isPrivateOrInternalHostname(hostname) {
|
|
7640
|
-
const normalized = hostname
|
|
7854
|
+
const normalized = stripZoneAndIpv4Port(stripIpv6Brackets(hostname));
|
|
7641
7855
|
if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "0.0.0.0" || normalized.endsWith(".localhost") || normalized.endsWith(".local")) return true;
|
|
7642
7856
|
if (normalized === "169.254.169.254" || normalized === "instance-data" || normalized === "metadata.google.internal" || normalized === "metadata.internal") return true;
|
|
7643
7857
|
if (normalized.endsWith(".cluster.local") || normalized.endsWith(".svc.cluster.local") || normalized.endsWith(".pod.cluster.local")) return true;
|
|
7644
|
-
if (/^(\d
|
|
7858
|
+
if (/^(\d+\.){3}\d+$/.test(normalized)) return isPrivateIPv4(normalized);
|
|
7645
7859
|
if (normalized.includes(":")) return isPrivateIPv6(normalized);
|
|
7646
7860
|
return false;
|
|
7647
7861
|
}
|
|
7648
7862
|
/**
|
|
7649
7863
|
* Validate a URL before fetching.
|
|
7650
7864
|
* Blocks internal/private networks to prevent SSRF attacks.
|
|
7651
|
-
*
|
|
7865
|
+
*
|
|
7866
|
+
* Resolves DNS and rejects the URL if any resolved IP is private. This is a PRE-FLIGHT check, and on
|
|
7867
|
+
* its own it does NOT stop DNS rebinding: the address it validates is not the address the eventual
|
|
7868
|
+
* socket dials, because the HTTP client resolves the hostname again when it connects. A name that
|
|
7869
|
+
* answers with a public IP here and a private one microseconds later passes this check and still
|
|
7870
|
+
* reaches the internal destination. `ssrfSafeLookup` below is what closes that window; this function
|
|
7871
|
+
* exists to fail fast, to produce a specific user-facing error, and to check the things a connect-time
|
|
7872
|
+
* hook cannot see - the scheme, and the literal address the caller actually typed.
|
|
7652
7873
|
*
|
|
7653
7874
|
* @param url - The URL to validate
|
|
7654
7875
|
* @returns Object with valid flag and optional error message
|
|
@@ -7660,15 +7881,16 @@ async function validateUrlForFetch(url) {
|
|
|
7660
7881
|
valid: false,
|
|
7661
7882
|
error: "URL must use HTTP or HTTPS protocol"
|
|
7662
7883
|
};
|
|
7663
|
-
|
|
7884
|
+
const hostname = stripIpv6Brackets(parsed.hostname);
|
|
7885
|
+
if (isPrivateOrInternalHostname(hostname)) return {
|
|
7664
7886
|
valid: false,
|
|
7665
7887
|
error: "URL points to a private or internal network"
|
|
7666
7888
|
};
|
|
7667
|
-
const isIPv4Address = /^(\d
|
|
7668
|
-
const isIPv6Address =
|
|
7889
|
+
const isIPv4Address = /^(\d+\.){3}\d+$/.test(hostname);
|
|
7890
|
+
const isIPv6Address = hostname.includes(":");
|
|
7669
7891
|
if (!isIPv4Address && !isIPv6Address) try {
|
|
7670
|
-
const ipv4Addresses = await dnsResolve4(
|
|
7671
|
-
const ipv6Addresses = await dnsResolve6(
|
|
7892
|
+
const ipv4Addresses = await dnsResolve4(hostname).catch(() => []);
|
|
7893
|
+
const ipv6Addresses = await dnsResolve6(hostname).catch(() => []);
|
|
7672
7894
|
const allAddresses = [...ipv4Addresses, ...ipv6Addresses];
|
|
7673
7895
|
if (allAddresses.length === 0) return {
|
|
7674
7896
|
valid: false,
|
|
@@ -7692,6 +7914,93 @@ async function validateUrlForFetch(url) {
|
|
|
7692
7914
|
};
|
|
7693
7915
|
}
|
|
7694
7916
|
}
|
|
7917
|
+
/** Marks a refusal that came from the connect-time hook, so callers can tell it from a DNS failure. */
|
|
7918
|
+
const SSRF_BLOCKED_CODE = "ERR_SSRF_BLOCKED_ADDRESS";
|
|
7919
|
+
/**
|
|
7920
|
+
* DNS lookup that re-validates at CONNECT time. THIS is the check that stops DNS rebinding.
|
|
7921
|
+
*
|
|
7922
|
+
* The pre-flight in `validateUrlForFetch` resolves the hostname and then hands the NAME to the HTTP
|
|
7923
|
+
* client, which resolves it a second time before opening the socket. Those are two different
|
|
7924
|
+
* resolutions, so an attacker who controls the authoritative server can answer the first with a
|
|
7925
|
+
* public address and the second with `169.254.169.254` - a textbook TOCTOU, and the reason the old
|
|
7926
|
+
* "this prevents DNS rebinding attacks" comment on that function was false.
|
|
7927
|
+
*
|
|
7928
|
+
* Installing this as the agent's `lookup` removes the gap rather than narrowing it: Node passes the
|
|
7929
|
+
* address this function returns straight to `net.connect`, so the IP that gets validated is by
|
|
7930
|
+
* construction the IP the socket dials. There is no third resolution in between for a rebind to win.
|
|
7931
|
+
*
|
|
7932
|
+
* Refuses if ANY resolved address is private, matching `validateUrlForFetch` - a dual-stack host must
|
|
7933
|
+
* not become reachable just because Node happened to prefer the healthy family this time.
|
|
7934
|
+
*
|
|
7935
|
+
* The two match in POLICY but deliberately differ in RESOLVER: `validateUrlForFetch` uses
|
|
7936
|
+
* `dns.resolve4`/`resolve6` (c-ares, straight to DNS) while this uses `dns.lookup` (getaddrinfo, which
|
|
7937
|
+
* also reads `/etc/hosts` and the OS cache). They can therefore legitimately disagree - an
|
|
7938
|
+
* `/etc/hosts` entry passes the pre-flight and is refused here. That is fail-closed and the right way
|
|
7939
|
+
* round, but it means "the URL validated and then the connection was blocked" is reachable in normal
|
|
7940
|
+
* operation and is NOT evidence that the pin is broken.
|
|
7941
|
+
*/
|
|
7942
|
+
const ssrfSafeLookup = (hostname, options, callback) => {
|
|
7943
|
+
const resolveOptions = {
|
|
7944
|
+
...options,
|
|
7945
|
+
all: true
|
|
7946
|
+
};
|
|
7947
|
+
dns.lookup(hostname, resolveOptions, (err, addresses) => {
|
|
7948
|
+
if (err) {
|
|
7949
|
+
callback(err, "", 0);
|
|
7950
|
+
return;
|
|
7951
|
+
}
|
|
7952
|
+
if (!addresses || addresses.length === 0) {
|
|
7953
|
+
const empty = /* @__PURE__ */ new Error(`No addresses resolved for hostname ${hostname}`);
|
|
7954
|
+
empty.code = "ENOTFOUND";
|
|
7955
|
+
callback(empty, "", 0);
|
|
7956
|
+
return;
|
|
7957
|
+
}
|
|
7958
|
+
const privateHit = addresses.find((entry) => isPrivateIP(entry.address));
|
|
7959
|
+
if (privateHit) {
|
|
7960
|
+
const blocked = /* @__PURE__ */ new Error(`Blocked connection to private IP address (${privateHit.address}) for hostname ${hostname}`);
|
|
7961
|
+
blocked.code = SSRF_BLOCKED_CODE;
|
|
7962
|
+
callback(blocked, "", 0);
|
|
7963
|
+
return;
|
|
7964
|
+
}
|
|
7965
|
+
if (options.all) {
|
|
7966
|
+
callback(null, addresses);
|
|
7967
|
+
return;
|
|
7968
|
+
}
|
|
7969
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
7970
|
+
});
|
|
7971
|
+
};
|
|
7972
|
+
/**
|
|
7973
|
+
* Agents that pin every connection through `ssrfSafeLookup`.
|
|
7974
|
+
*
|
|
7975
|
+
* Module-level singletons so sockets and their validation are shared, and deliberately WITHOUT
|
|
7976
|
+
* `keepAlive`: a pooled socket outlives the lookup that approved it, and reusing one would skip the
|
|
7977
|
+
* connect-time check on every request after the first.
|
|
7978
|
+
*
|
|
7979
|
+
* Any caller fetching an attacker-influenced URL should pass BOTH - the scheme is not known until
|
|
7980
|
+
* after redirects, and an https URL that 302s to http would otherwise slip past a single agent.
|
|
7981
|
+
*
|
|
7982
|
+
* SCOPE - these protect callers that fetch through Node's http/https stack, which today means
|
|
7983
|
+
* `fetchAndParseURL` in `ingest.ts` and nothing else. That does NOT mean other fetchers are unpinned:
|
|
7984
|
+
* the webfetch LLM tool (`services/src/llm/tools/implementation/webfetch/plainFetch.ts`) reaches the
|
|
7985
|
+
* same guarantee by a different route, and a reader should not go looking for a gap there that is
|
|
7986
|
+
* already closed. It vets via `ssrfGuard.ts`, then for http rewrites the URL's hostname to the vetted
|
|
7987
|
+
* IP while preserving `Host`, and sets `redirect: 'error'` so a public origin cannot 302-pivot at all.
|
|
7988
|
+
* That is connect-by-IP under global `fetch` - so the technique IS available there, and an
|
|
7989
|
+
* undici `Agent` with a validating `connect` is not required to pin.
|
|
7990
|
+
*
|
|
7991
|
+
* The honest residual over there is narrower: https keeps the hostname and leans on TLS validation, so
|
|
7992
|
+
* what is left is an SYN-level probe oracle rather than a rebind to a private target. The reason to
|
|
7993
|
+
* use the agents here instead is that axios drives a manual redirect chain over an arbitrary number of
|
|
7994
|
+
* hops and schemes, where per-request agent selection is the tractable place to enforce this.
|
|
7995
|
+
*/
|
|
7996
|
+
const ssrfSafeHttpAgent = new http.Agent({
|
|
7997
|
+
lookup: ssrfSafeLookup,
|
|
7998
|
+
keepAlive: false
|
|
7999
|
+
});
|
|
8000
|
+
const ssrfSafeHttpsAgent = new https.Agent({
|
|
8001
|
+
lookup: ssrfSafeLookup,
|
|
8002
|
+
keepAlive: false
|
|
8003
|
+
});
|
|
7695
8004
|
const URL_REGEX = /https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?/gi;
|
|
7696
8005
|
function detectURLs(string) {
|
|
7697
8006
|
return string.match(URL_REGEX) || [];
|
|
@@ -7703,36 +8012,153 @@ function urlExists(stringWithPossibleUrl) {
|
|
|
7703
8012
|
return detectURLs(stringWithPossibleUrl.replace(/\n/g, " ").replace(/,/g, " "));
|
|
7704
8013
|
}
|
|
7705
8014
|
const URL_FETCH_TIMEOUT_MS = 1e4;
|
|
8015
|
+
/**
|
|
8016
|
+
* Redirect hops followed before giving up. Deliberately far below axios's own default of 21: every
|
|
8017
|
+
* hop costs a DNS resolution plus a request, and no legitimate document needs more than a couple.
|
|
8018
|
+
*/
|
|
8019
|
+
const MAX_REDIRECTS = 5;
|
|
8020
|
+
/**
|
|
8021
|
+
* Hard ceiling on a fetched body. A SAFETY NET against an unbounded response, not a policy limit -
|
|
8022
|
+
* `createFabFile` still enforces the `MaxFileSize` admin setting afterwards. Set generously (the
|
|
8023
|
+
* same 50MB as the Slack attachment ceiling) so it can never refuse something the app would accept;
|
|
8024
|
+
* without it axios defaults to `maxContentLength: -1`, i.e. buffer whatever the server sends, and
|
|
8025
|
+
* `@datalake add <link>` takes URLs from anyone who can type in a Slack channel.
|
|
8026
|
+
*/
|
|
8027
|
+
const URL_MAX_RESPONSE_BYTES = 52428800;
|
|
8028
|
+
/**
|
|
8029
|
+
* PDF test against the URL's PATH only. The previous form (`url.split('.').pop().startsWith('pdf')`)
|
|
8030
|
+
* also matched a query string, so `?doc=report.pdf` on an HTML page was fetched as a PDF.
|
|
8031
|
+
*/
|
|
8032
|
+
function isPdfUrl(url) {
|
|
8033
|
+
return new URL(url).pathname.toLowerCase().endsWith(".pdf");
|
|
8034
|
+
}
|
|
8035
|
+
/**
|
|
8036
|
+
* True when the body opens with the PDF signature.
|
|
8037
|
+
*
|
|
8038
|
+
* Closes the door `isPdfUrl` cannot reach: a download endpoint with no `.pdf` in its path, served as
|
|
8039
|
+
* `application/octet-stream`, produced neither a Content-Type signal nor an extension signal and was
|
|
8040
|
+
* decoded as text - the same `toString('utf8')` corruption the Content-Type fallback exists to
|
|
8041
|
+
* prevent, arriving through the one remaining door. `/download?id=123` and `Content-Disposition`
|
|
8042
|
+
* attachment links are exactly this shape.
|
|
8043
|
+
*
|
|
8044
|
+
* Checked at offset 0 only. The PDF spec tolerates leading bytes before the header and readers scan
|
|
8045
|
+
* ahead for it, but scanning here would mean sniffing arbitrary attacker-supplied content to
|
|
8046
|
+
* RE-CLASSIFY it, and a false positive sends a real text document into the PDF parser. The strict
|
|
8047
|
+
* check costs nothing on well-formed files, which is every file this has been observed to affect.
|
|
8048
|
+
*
|
|
8049
|
+
* Deliberately consulted ONLY on the generic-binary branch, never to override a server that stated a
|
|
8050
|
+
* type. A server declaring `text/html` while sending PDF bytes is a different (and unobserved) bug,
|
|
8051
|
+
* and overriding an explicit Content-Type is a wider behaviour change than this fix needs.
|
|
8052
|
+
*/
|
|
8053
|
+
function hasPdfMagicBytes(body) {
|
|
8054
|
+
return body.subarray(0, 5).toString("latin1") === "%PDF-";
|
|
8055
|
+
}
|
|
8056
|
+
/**
|
|
8057
|
+
* Strip embedded credentials before a URL is written to a log.
|
|
8058
|
+
*
|
|
8059
|
+
* `https://user:pass@host/doc` is a legitimate paste, and this function is reached from the Slack
|
|
8060
|
+
* `@datalake add` path and the LLM URL-fetch path - both of which take URLs from whoever can type in
|
|
8061
|
+
* a channel or a chat. The FETCH still uses the original URL; only what is recorded is redacted, and a
|
|
8062
|
+
* log line outlives the message that produced it.
|
|
8063
|
+
*
|
|
8064
|
+
* MUST STAY IN SYNC with `sanitizeUrlForRecord` in `apps/client/server/slack/dataLakeLinkIngest.ts`,
|
|
8065
|
+
* which does the same job for the PERSISTED provenance record. Deliberately duplicated rather than
|
|
8066
|
+
* shared: exporting this would change `fab-pipeline`'s public surface, which its own `index.test.ts`
|
|
8067
|
+
* pins as an explicit list of names.
|
|
8068
|
+
*/
|
|
8069
|
+
function redactUrlCredentials(raw) {
|
|
8070
|
+
try {
|
|
8071
|
+
const parsed = new URL(raw);
|
|
8072
|
+
if (!parsed.username && !parsed.password) return raw;
|
|
8073
|
+
parsed.username = "";
|
|
8074
|
+
parsed.password = "";
|
|
8075
|
+
return parsed.toString();
|
|
8076
|
+
} catch {
|
|
8077
|
+
return "[unparseable url]";
|
|
8078
|
+
}
|
|
8079
|
+
}
|
|
8080
|
+
/** Last path segment, used only as a display-name fallback when a page has no `<title>`. */
|
|
8081
|
+
function lastPathSegment(url) {
|
|
8082
|
+
try {
|
|
8083
|
+
return new URL(url).pathname.split("/").filter(Boolean).pop() ?? url;
|
|
8084
|
+
} catch {
|
|
8085
|
+
return url.split("/")?.pop() ?? url;
|
|
8086
|
+
}
|
|
8087
|
+
}
|
|
8088
|
+
/**
|
|
8089
|
+
* Fetch one URL without following redirects, so the caller can SSRF-validate each hop itself.
|
|
8090
|
+
*
|
|
8091
|
+
* SECURITY: this is why `maxRedirects: 0` is set rather than left at axios's default. Validating
|
|
8092
|
+
* only the URL the user supplied is not enough - axios would follow the redirect chain internally,
|
|
8093
|
+
* so any public host could answer `302 Location: http://169.254.169.254/latest/meta-data/` and the
|
|
8094
|
+
* guard would never see the address actually fetched.
|
|
8095
|
+
*
|
|
8096
|
+
* SECURITY: the agents are the OTHER half, and the two guard different attacks. Per-hop
|
|
8097
|
+
* `validateUrlForFetch` judges each address the chain names; the agents' `ssrfSafeLookup` judges the
|
|
8098
|
+
* IP each socket actually dials. Without the agents a hostname that passes validation and then
|
|
8099
|
+
* re-resolves to a private address on connect - DNS rebinding - reaches the internal destination with
|
|
8100
|
+
* every URL-level check having passed. Both are needed: the pre-flight sees the scheme and the typed
|
|
8101
|
+
* literal, the lookup sees the truth at connect time.
|
|
8102
|
+
*
|
|
8103
|
+
* `timeoutMs` is the budget REMAINING for the whole operation, not a fresh per-hop allowance - see
|
|
8104
|
+
* the deadline in `fetchAndParseURL`.
|
|
8105
|
+
*/
|
|
8106
|
+
async function fetchWithoutRedirects(url, timeoutMs) {
|
|
8107
|
+
return axios.get(url, {
|
|
8108
|
+
httpAgent: ssrfSafeHttpAgent,
|
|
8109
|
+
httpsAgent: ssrfSafeHttpsAgent,
|
|
8110
|
+
proxy: false,
|
|
8111
|
+
responseType: "arraybuffer",
|
|
8112
|
+
timeout: timeoutMs,
|
|
8113
|
+
maxRedirects: 0,
|
|
8114
|
+
maxContentLength: URL_MAX_RESPONSE_BYTES,
|
|
8115
|
+
maxBodyLength: URL_MAX_RESPONSE_BYTES,
|
|
8116
|
+
validateStatus: (status) => status >= 200 && status < 300 || status >= 300 && status < 400
|
|
8117
|
+
});
|
|
8118
|
+
}
|
|
7706
8119
|
async function fetchAndParseURL(url, { logger }) {
|
|
7707
8120
|
logger.updateMetadata({ failedUrl: null });
|
|
7708
8121
|
try {
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
8122
|
+
let currentUrl = url;
|
|
8123
|
+
let response = null;
|
|
8124
|
+
const deadline = Date.now() + URL_FETCH_TIMEOUT_MS;
|
|
8125
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
8126
|
+
const ssrfValidation = await validateUrlForFetch(currentUrl);
|
|
8127
|
+
if (!ssrfValidation.valid) throw new Error(`URL blocked for security reasons: ${ssrfValidation.error}`);
|
|
8128
|
+
const remainingMs = deadline - Date.now();
|
|
8129
|
+
if (remainingMs <= 0) throw new Error("Timed out while following redirects for URL");
|
|
8130
|
+
response = await fetchWithoutRedirects(currentUrl, remainingMs);
|
|
8131
|
+
if (!(response.status >= 300 && response.status < 400)) break;
|
|
8132
|
+
const location = response.headers?.location;
|
|
8133
|
+
if (typeof location !== "string" || location.length === 0) break;
|
|
8134
|
+
if (hop === MAX_REDIRECTS) throw new Error(`Too many redirects (more than ${MAX_REDIRECTS}) while fetching URL`);
|
|
8135
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
8136
|
+
}
|
|
8137
|
+
if (!response) throw new Error("URL fetch produced no response");
|
|
8138
|
+
const body = Buffer.isBuffer(response.data) ? response.data : Buffer.from(response.data);
|
|
8139
|
+
const contentType = String(response.headers?.["content-type"] ?? "").toLowerCase();
|
|
8140
|
+
const isGenericBinary = !contentType || contentType.includes("application/octet-stream") || contentType.includes("binary/octet-stream");
|
|
8141
|
+
const urlMimeType = contentType.includes("application/pdf") || isGenericBinary && (isPdfUrl(currentUrl) || hasPdfMagicBytes(body)) ? "application/pdf" : "text/plain";
|
|
8142
|
+
let title;
|
|
8143
|
+
let urlContent;
|
|
8144
|
+
if (urlMimeType === "application/pdf") {
|
|
8145
|
+
urlContent = body;
|
|
8146
|
+
title = lastPathSegment(currentUrl);
|
|
8147
|
+
} else {
|
|
8148
|
+
const cheerio = await import("cheerio");
|
|
8149
|
+
const htmlContent = body.toString("utf8");
|
|
8150
|
+
const $ = cheerio.load(htmlContent);
|
|
8151
|
+
title = $("title").text() || lastPathSegment(currentUrl);
|
|
8152
|
+
let textContent = "";
|
|
8153
|
+
$("body").find("p").each((index, element) => {
|
|
8154
|
+
textContent += $(element).text() + "\n";
|
|
8155
|
+
});
|
|
8156
|
+
urlContent = textContent || htmlContent;
|
|
7734
8157
|
}
|
|
7735
|
-
|
|
8158
|
+
const original = redactUrlCredentials(url);
|
|
8159
|
+
const final = redactUrlCredentials(currentUrl);
|
|
8160
|
+
const fetched = original === final ? original : `${original} -> ${final}`;
|
|
8161
|
+
logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
|
|
7736
8162
|
return {
|
|
7737
8163
|
title,
|
|
7738
8164
|
textContent: urlContent,
|
|
@@ -7740,7 +8166,7 @@ async function fetchAndParseURL(url, { logger }) {
|
|
|
7740
8166
|
ext: mime.extension(urlMimeType) || null
|
|
7741
8167
|
};
|
|
7742
8168
|
} catch (error) {
|
|
7743
|
-
logger.updateMetadata({ failedUrl: url });
|
|
8169
|
+
logger.updateMetadata({ failedUrl: redactUrlCredentials(url) });
|
|
7744
8170
|
logger.debug("Error fetching or parsing URL:", error);
|
|
7745
8171
|
throw error;
|
|
7746
8172
|
}
|
|
@@ -10698,7 +11124,6 @@ var ReActAgent = class extends EventEmitter {
|
|
|
10698
11124
|
this.context = {
|
|
10699
11125
|
...context,
|
|
10700
11126
|
maxIterations: context.maxIterations ?? 50,
|
|
10701
|
-
maxTokens: context.maxTokens ?? 4096,
|
|
10702
11127
|
temperature: context.temperature ?? .7
|
|
10703
11128
|
};
|
|
10704
11129
|
this.repeatedCallGuard = new RepeatedCallGuard(context.repeatedCallGuard);
|
|
@@ -10743,7 +11168,7 @@ var ReActAgent = class extends EventEmitter {
|
|
|
10743
11168
|
*/
|
|
10744
11169
|
async completeText(prompt, options = {}) {
|
|
10745
11170
|
const model = options.model ?? this.context.model;
|
|
10746
|
-
const maxTokens = options.maxTokens ?? this.context.maxTokens
|
|
11171
|
+
const maxTokens = options.maxTokens ?? this.context.maxTokens;
|
|
10747
11172
|
const temperature = options.temperature ?? this.context.temperature ?? .7;
|
|
10748
11173
|
let text = "";
|
|
10749
11174
|
await this.context.llm.complete(model, [{
|
|
@@ -10784,7 +11209,7 @@ var ReActAgent = class extends EventEmitter {
|
|
|
10784
11209
|
this.isReadOnlyToolFn = options.isReadOnlyTool ?? defaultIsReadOnlyTool;
|
|
10785
11210
|
const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
|
|
10786
11211
|
const temperature = options.temperature ?? this.context.temperature ?? .7;
|
|
10787
|
-
const maxTokens = options.maxTokens ?? this.context.maxTokens
|
|
11212
|
+
const maxTokens = options.maxTokens ?? this.context.maxTokens;
|
|
10788
11213
|
const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
|
|
10789
11214
|
const maxHistoryIterations = options.maxHistoryIterations ?? 4;
|
|
10790
11215
|
let iterations = 0;
|
|
@@ -10825,7 +11250,8 @@ var ReActAgent = class extends EventEmitter {
|
|
|
10825
11250
|
totalCacheWriteTokens: this.totalCacheWriteTokens > 0 ? this.totalCacheWriteTokens : void 0,
|
|
10826
11251
|
iterations,
|
|
10827
11252
|
toolCalls: this.toolCallCount,
|
|
10828
|
-
reachedMaxIterations: false
|
|
11253
|
+
reachedMaxIterations: false,
|
|
11254
|
+
finishReason: this.lastStopReason
|
|
10829
11255
|
}
|
|
10830
11256
|
};
|
|
10831
11257
|
this.emit("complete", result);
|
|
@@ -11059,6 +11485,7 @@ var ReActAgent = class extends EventEmitter {
|
|
|
11059
11485
|
toolCalls: this.toolCallCount,
|
|
11060
11486
|
reachedMaxIterations,
|
|
11061
11487
|
reachedMaxTotalTokens: reachedMaxTotalTokens || void 0,
|
|
11488
|
+
finishReason: this.lastStopReason,
|
|
11062
11489
|
averageConfidence: avgConfidence,
|
|
11063
11490
|
minConfidence,
|
|
11064
11491
|
confidenceLog: this.confidenceLog.length > 0 ? this.confidenceLog : void 0
|
|
@@ -11290,7 +11717,7 @@ Remember: You are an autonomous AGENT. Act independently and solve problems proa
|
|
|
11290
11717
|
async runIteration(query, options = {}) {
|
|
11291
11718
|
const maxIterations = options.maxIterations ?? this.context.maxIterations ?? 50;
|
|
11292
11719
|
const temperature = options.temperature ?? this.context.temperature ?? .7;
|
|
11293
|
-
const maxTokens = options.maxTokens ?? this.context.maxTokens
|
|
11720
|
+
const maxTokens = options.maxTokens ?? this.context.maxTokens;
|
|
11294
11721
|
const maxTotalTokens = options.maxTotalTokens ?? this.context.maxTotalTokens;
|
|
11295
11722
|
if (!this.iterationInitialized) {
|
|
11296
11723
|
if (!query) throw new Error("query is required on the first call to runIteration(). Pass the user query, or call fromCheckpoint() first to resume.");
|
|
@@ -15075,6 +15502,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
|
|
|
15075
15502
|
buildApiKeyTable: () => buildApiKeyTable,
|
|
15076
15503
|
buildSupersededIndex: () => buildSupersededIndex,
|
|
15077
15504
|
buildThinkingParams: () => buildThinkingParams,
|
|
15505
|
+
cachedTokensFromUsage: () => cachedTokensFromUsage,
|
|
15078
15506
|
catalogLifecycles: () => catalogLifecycles,
|
|
15079
15507
|
catalogSuccessors: () => catalogSuccessors,
|
|
15080
15508
|
checkStaleModelReferences: () => checkStaleModelReferences,
|
|
@@ -15110,6 +15538,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
|
|
|
15110
15538
|
resolveSuccessorChain: () => resolveSuccessorChain,
|
|
15111
15539
|
setModelCatalogProvider: () => setModelCatalogProvider,
|
|
15112
15540
|
setModelPriceRowsProvider: () => setModelPriceRowsProvider,
|
|
15541
|
+
splitCacheInclusiveInput: () => splitCacheInclusiveInput,
|
|
15113
15542
|
stripAllToolBlocks: () => stripAllToolBlocks,
|
|
15114
15543
|
stripToolDependentMessages: () => stripToolDependentMessages,
|
|
15115
15544
|
toKimiEffort: () => toKimiEffort,
|
|
@@ -15195,6 +15624,44 @@ async function runWithConcurrency(tasks, limit) {
|
|
|
15195
15624
|
return results;
|
|
15196
15625
|
}
|
|
15197
15626
|
/**
|
|
15627
|
+
* Attaches a tool call's outcome onto its `toolsUsed` entry so it survives into
|
|
15628
|
+
* `promptMeta.functionCalls.returnValue`/`.success` (see ChatCompletionProcess.ts's mapper
|
|
15629
|
+
* and utils.ts's `replayableToolCalls`, which gates a whole replay path on at least one
|
|
15630
|
+
* recorded `returnValue`). Every backend pushes a `toolsUsed` entry before executing the
|
|
15631
|
+
* tool and only learns the real outcome a few lines later - this is the merge-back.
|
|
15632
|
+
*/
|
|
15633
|
+
/**
|
|
15634
|
+
* Cap applied to a persisted `returnValue` before it reaches Mongo (chars, not bytes). Also the
|
|
15635
|
+
* cap on what a later turn replays back to the model for this call (utils.ts's Priority 2
|
|
15636
|
+
* reconstruction reads the same persisted, already-truncated value) - this is not a
|
|
15637
|
+
* persistence-only limit, it is what a continued conversation sees of an older tool result too.
|
|
15638
|
+
*/
|
|
15639
|
+
const MAX_RECORDED_TOOL_RESULT_CHARS = 8e3;
|
|
15640
|
+
const TOOL_RESULT_TRUNCATION_NOTICE = "\n[tool result truncated]";
|
|
15641
|
+
function truncateToolResult(observation) {
|
|
15642
|
+
if (observation.length <= 8e3) return observation;
|
|
15643
|
+
return observation.slice(0, MAX_RECORDED_TOOL_RESULT_CHARS) + TOOL_RESULT_TRUNCATION_NOTICE;
|
|
15644
|
+
}
|
|
15645
|
+
/**
|
|
15646
|
+
* Finds the NOT-YET-STAMPED `toolsUsed` entry for this call (id-first, falling back to the
|
|
15647
|
+
* first unstamped entry with the same name when a provider omitted an id) and attaches the
|
|
15648
|
+
* truncated result. The "not yet stamped" filter (`success === undefined`) is what keeps this
|
|
15649
|
+
* safe across recursive tool-call turns: `toolsUsed` accumulates across rounds, so a later
|
|
15650
|
+
* turn's call to the same tool must not overwrite an earlier turn's already-recorded entry.
|
|
15651
|
+
* Never throws - a call with no matching entry (e.g. one filtered out before execution) is a
|
|
15652
|
+
* silent no-op, matching the tolerance the existing normalize-by-id sites already have.
|
|
15653
|
+
*/
|
|
15654
|
+
function recordToolResult(toolsUsed, call, observation, success) {
|
|
15655
|
+
const wantId = call.id || void 0;
|
|
15656
|
+
const entry = toolsUsed.find((t) => t.success === void 0 && t.name === call.name && (wantId === void 0 || t.id === wantId));
|
|
15657
|
+
if (!entry) {
|
|
15658
|
+
Logger.globalInstance.debug(`[recordToolResult] no unstamped toolsUsed entry matched name=${call.name} id=${call.id ?? "(none)"} - result not recorded`);
|
|
15659
|
+
return;
|
|
15660
|
+
}
|
|
15661
|
+
entry.returnValue = truncateToolResult(String(observation));
|
|
15662
|
+
entry.success = success;
|
|
15663
|
+
}
|
|
15664
|
+
/**
|
|
15198
15665
|
* Helper function to handle tool result streaming for artifact-generating tools
|
|
15199
15666
|
* This ensures tools like recharts that generate artifacts are streamed immediately
|
|
15200
15667
|
* rather than waiting for recursive completion calls.
|
|
@@ -15710,6 +16177,25 @@ function getCachingAdapter(backend) {
|
|
|
15710
16177
|
return ADAPTERS[backend] || new NoOpCachingAdapter();
|
|
15711
16178
|
}
|
|
15712
16179
|
/**
|
|
16180
|
+
* Flatten a system message's content to the plain text a provider should receive.
|
|
16181
|
+
*
|
|
16182
|
+
* System content is usually a string, but the assembly pipeline can hand back an
|
|
16183
|
+
* array of content blocks. Both Anthropic-family adapters previously coerced that
|
|
16184
|
+
* array with `JSON.stringify` (or bare `String()`), which sent the model literal
|
|
16185
|
+
* JSON syntax - escaped quotes and `type`/`text` keys - in place of the prompt.
|
|
16186
|
+
*
|
|
16187
|
+
* Only `text` blocks are read: Anthropic's `system` accepts text only, so an image
|
|
16188
|
+
* or tool block there is already invalid and is dropped rather than serialized into
|
|
16189
|
+
* the prompt. Blocks are joined on a newline, matching how separate system messages
|
|
16190
|
+
* are joined by the callers.
|
|
16191
|
+
*/
|
|
16192
|
+
function systemContentToText(content) {
|
|
16193
|
+
if (content === void 0 || content === null) return "";
|
|
16194
|
+
if (typeof content === "string") return content;
|
|
16195
|
+
if (!Array.isArray(content)) return "";
|
|
16196
|
+
return content.filter((block) => block?.type === "text").map((block) => block.text ?? "").filter((text) => text.trim() !== "").join("\n");
|
|
16197
|
+
}
|
|
16198
|
+
/**
|
|
15713
16199
|
* max_tokens floor for adaptive reasoning models (Claude 4.7+/Opus 5). These
|
|
15714
16200
|
* models self-manage extended thinking *within* max_tokens, which is a ceiling
|
|
15715
16201
|
* (they stop at end_turn), not a target - so a larger floor costs nothing on
|
|
@@ -15773,8 +16259,17 @@ function reasonsWithinOutputBudget(modelInfo) {
|
|
|
15773
16259
|
* be consumed entirely by reasoning, leaving an empty visible reply.
|
|
15774
16260
|
*/
|
|
15775
16261
|
function resolveOutputMaxTokens({ requested, fallback, modelInfo, modelMaxOutputTokens }) {
|
|
15776
|
-
const preferred = requested ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
|
|
15777
|
-
|
|
16262
|
+
const preferred = usableTokenCount(requested) ?? (reasonsWithinOutputBudget(modelInfo) ? 64e3 : fallback);
|
|
16263
|
+
const cap = usableTokenCount(modelMaxOutputTokens);
|
|
16264
|
+
return cap === void 0 ? preferred : Math.min(preferred, cap);
|
|
16265
|
+
}
|
|
16266
|
+
/**
|
|
16267
|
+
* Token counts reaching this module come from catalog rows and external callers, so they are
|
|
16268
|
+
* only trustworthy when finite and positive - a zero or negative cap would clamp the budget
|
|
16269
|
+
* to an unsendable value just as surely as NaN poisons it.
|
|
16270
|
+
*/
|
|
16271
|
+
function usableTokenCount(value) {
|
|
16272
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
15778
16273
|
}
|
|
15779
16274
|
/**
|
|
15780
16275
|
* Builds the correct thinking parameters for an Anthropic API call based on
|
|
@@ -15963,6 +16458,12 @@ const SLOW_MODEL_REQUEST_TIMEOUT_MS = 12e4;
|
|
|
15963
16458
|
*/
|
|
15964
16459
|
const ANTHROPIC_NONSTREAMING_MAX_TOKENS = 21e3;
|
|
15965
16460
|
/**
|
|
16461
|
+
* Output budget used when the caller names none. Only applies to models that do NOT spend
|
|
16462
|
+
* reasoning inside the output budget - resolveOutputMaxTokens sizes the rest for the model,
|
|
16463
|
+
* so this value stays the historical one and non-reasoning behavior is unchanged.
|
|
16464
|
+
*/
|
|
16465
|
+
const DEFAULT_ANTHROPIC_MAX_TOKENS = 4096;
|
|
16466
|
+
/**
|
|
15966
16467
|
* Accumulated multi-turn cache token total. Undefined when zero so turns
|
|
15967
16468
|
* without cache activity keep the pre-cache callback shape.
|
|
15968
16469
|
*/
|
|
@@ -16573,7 +17074,8 @@ var AnthropicBackend = class {
|
|
|
16573
17074
|
const systemMessages = messages.filter((m) => m.role === "system");
|
|
16574
17075
|
const blocks = [];
|
|
16575
17076
|
for (const sm of systemMessages) {
|
|
16576
|
-
const text =
|
|
17077
|
+
const text = systemContentToText(sm.content);
|
|
17078
|
+
if (text.trim() === "") continue;
|
|
16577
17079
|
if (sm.cache === true) blocks.push({
|
|
16578
17080
|
type: "text",
|
|
16579
17081
|
text,
|
|
@@ -16612,7 +17114,11 @@ var AnthropicBackend = class {
|
|
|
16612
17114
|
let { useCount: toolUseCount, resultCount: toolResultCount } = countToolBlocks(filteredMessages);
|
|
16613
17115
|
if (toolUseCount > 0 || toolResultCount > 0) {
|
|
16614
17116
|
this.logger.debug(`[Pre-API #6181] Sending ${filteredMessages.length} messages with ${toolUseCount} tool_use and ${toolResultCount} tool_result blocks`);
|
|
16615
|
-
if (
|
|
17117
|
+
if (!options.tools?.length) {
|
|
17118
|
+
this.logger.warn(`[Pre-API #6181] Tool blocks present (tool_use: ${toolUseCount}, tool_result: ${toolResultCount}) but no tools offered this turn. Stripping all tool blocks.`);
|
|
17119
|
+
filteredMessages = stripAllToolBlocks(filteredMessages, this.logger);
|
|
17120
|
+
({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
|
|
17121
|
+
} else if (toolUseCount !== toolResultCount) {
|
|
16616
17122
|
this.logger.warn(`[Pre-API #6181] Tool block mismatch! tool_use: ${toolUseCount}, tool_result: ${toolResultCount}. Attempting auto-repair...`);
|
|
16617
17123
|
filteredMessages = ensureToolPairingIntegrity(filteredMessages, this.logger);
|
|
16618
17124
|
({useCount: toolUseCount, resultCount: toolResultCount} = countToolBlocks(filteredMessages));
|
|
@@ -16622,9 +17128,15 @@ var AnthropicBackend = class {
|
|
|
16622
17128
|
}
|
|
16623
17129
|
}
|
|
16624
17130
|
}
|
|
17131
|
+
const currentModelInfo = this.modelRecordFor(model);
|
|
16625
17132
|
const apiParams = {
|
|
16626
17133
|
model,
|
|
16627
|
-
max_tokens:
|
|
17134
|
+
max_tokens: currentModelInfo ? resolveOutputMaxTokens({
|
|
17135
|
+
requested: options.maxTokens,
|
|
17136
|
+
fallback: DEFAULT_ANTHROPIC_MAX_TOKENS,
|
|
17137
|
+
modelInfo: currentModelInfo,
|
|
17138
|
+
modelMaxOutputTokens: currentModelInfo.max_tokens
|
|
17139
|
+
}) : options.maxTokens ?? DEFAULT_ANTHROPIC_MAX_TOKENS,
|
|
16628
17140
|
messages: filteredMessages.map((m) => ({
|
|
16629
17141
|
role: m.role === "user" ? "user" : "assistant",
|
|
16630
17142
|
content: m.content
|
|
@@ -16664,7 +17176,6 @@ var AnthropicBackend = class {
|
|
|
16664
17176
|
}
|
|
16665
17177
|
}
|
|
16666
17178
|
const requestExtraHeaders = anyMessageCacheControlled ? { "anthropic-beta": "prompt-caching-2024-07-31" } : void 0;
|
|
16667
|
-
const currentModelInfo = this.modelRecordFor(model);
|
|
16668
17179
|
if (currentModelInfo?.can_think) {
|
|
16669
17180
|
const thinkingOptions = options;
|
|
16670
17181
|
const isQuestMaster = thinkingOptions.questMaster === true;
|
|
@@ -17045,6 +17556,10 @@ var AnthropicBackend = class {
|
|
|
17045
17556
|
if (!parsedParams) {
|
|
17046
17557
|
const entry = toolsUsed.find((t) => t.name === name && t.id === id);
|
|
17047
17558
|
if (entry) entry.arguments = "{}";
|
|
17559
|
+
recordToolResult(toolsUsed, {
|
|
17560
|
+
id,
|
|
17561
|
+
name
|
|
17562
|
+
}, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
|
|
17048
17563
|
continue;
|
|
17049
17564
|
}
|
|
17050
17565
|
resolvedTools.push({
|
|
@@ -17112,6 +17627,10 @@ var AnthropicBackend = class {
|
|
|
17112
17627
|
toolsUsed
|
|
17113
17628
|
});
|
|
17114
17629
|
});
|
|
17630
|
+
recordToolResult(toolsUsed, {
|
|
17631
|
+
id: outcome.id,
|
|
17632
|
+
name: outcome.name
|
|
17633
|
+
}, resultStr, true);
|
|
17115
17634
|
this.pushToolMessages(messages, {
|
|
17116
17635
|
id: toolId,
|
|
17117
17636
|
name: outcome.name,
|
|
@@ -17119,16 +17638,22 @@ var AnthropicBackend = class {
|
|
|
17119
17638
|
}, resultStr);
|
|
17120
17639
|
} else {
|
|
17121
17640
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
17641
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
17122
17642
|
this.logger.error("[Tool Execution] Tool failed", {
|
|
17123
17643
|
model,
|
|
17124
17644
|
toolName: outcome.name,
|
|
17125
|
-
error:
|
|
17645
|
+
error: errorMessage
|
|
17126
17646
|
});
|
|
17647
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
17648
|
+
recordToolResult(toolsUsed, {
|
|
17649
|
+
id: outcome.id,
|
|
17650
|
+
name: outcome.name
|
|
17651
|
+
}, observation, false);
|
|
17127
17652
|
this.pushToolMessages(messages, {
|
|
17128
17653
|
id: toolId,
|
|
17129
17654
|
name: outcome.name,
|
|
17130
17655
|
parameters: outcome.parameters
|
|
17131
|
-
},
|
|
17656
|
+
}, observation);
|
|
17132
17657
|
}
|
|
17133
17658
|
}
|
|
17134
17659
|
await cb(["\n\n"], { toolsUsed });
|
|
@@ -17265,7 +17790,13 @@ var AnthropicBackend = class {
|
|
|
17265
17790
|
isMcpTool,
|
|
17266
17791
|
streaming: false
|
|
17267
17792
|
}, messages);
|
|
17268
|
-
if (!parsedParams)
|
|
17793
|
+
if (!parsedParams) {
|
|
17794
|
+
recordToolResult(toolsUsed, {
|
|
17795
|
+
id,
|
|
17796
|
+
name
|
|
17797
|
+
}, "Error: Tool parameters were corrupted due to a stream interruption. Please retry.", false);
|
|
17798
|
+
continue;
|
|
17799
|
+
}
|
|
17269
17800
|
resolvedTools.push({
|
|
17270
17801
|
id: id ?? "",
|
|
17271
17802
|
name,
|
|
@@ -17327,6 +17858,10 @@ var AnthropicBackend = class {
|
|
|
17327
17858
|
await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
|
|
17328
17859
|
await cb(results, { toolsUsed });
|
|
17329
17860
|
});
|
|
17861
|
+
recordToolResult(toolsUsed, {
|
|
17862
|
+
id: outcome.id,
|
|
17863
|
+
name: outcome.name
|
|
17864
|
+
}, resultStr, true);
|
|
17330
17865
|
this.pushToolMessages(messages, {
|
|
17331
17866
|
id: toolId,
|
|
17332
17867
|
name: outcome.name,
|
|
@@ -17334,16 +17869,22 @@ var AnthropicBackend = class {
|
|
|
17334
17869
|
}, resultStr);
|
|
17335
17870
|
} else {
|
|
17336
17871
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
17872
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
17337
17873
|
this.logger.error("[Tool Execution] Tool failed (non-streaming)", {
|
|
17338
17874
|
model,
|
|
17339
17875
|
toolName: outcome.name,
|
|
17340
|
-
error:
|
|
17876
|
+
error: errorMessage
|
|
17341
17877
|
});
|
|
17878
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
17879
|
+
recordToolResult(toolsUsed, {
|
|
17880
|
+
id: outcome.id,
|
|
17881
|
+
name: outcome.name
|
|
17882
|
+
}, observation, false);
|
|
17342
17883
|
this.pushToolMessages(messages, {
|
|
17343
17884
|
id: toolId,
|
|
17344
17885
|
name: outcome.name,
|
|
17345
17886
|
parameters: outcome.parameters
|
|
17346
|
-
},
|
|
17887
|
+
}, observation);
|
|
17347
17888
|
}
|
|
17348
17889
|
}
|
|
17349
17890
|
this.logger.info("[Tool Execution] Making recursive call after tool execution (non-streaming)", {
|
|
@@ -17597,7 +18138,7 @@ var AnthropicBackend = class {
|
|
|
17597
18138
|
consolidateSystemMessages(messages) {
|
|
17598
18139
|
const systemMessages = messages.filter((m) => m.role === "system");
|
|
17599
18140
|
if (systemMessages.length === 0) return void 0;
|
|
17600
|
-
return systemMessages.map((m) => m.content).join("\n");
|
|
18141
|
+
return systemMessages.map((m) => systemContentToText(m.content)).filter((text) => text.trim() !== "").join("\n");
|
|
17601
18142
|
}
|
|
17602
18143
|
isToolUseEvent(event) {
|
|
17603
18144
|
return typeof event === "object" && event !== null && "type" in event && event.type === "tool_use" && "name" in event && "input" in event && "id" in event;
|
|
@@ -17740,6 +18281,15 @@ var BaseBedrockBackend = class {
|
|
|
17740
18281
|
const command = new InvokeModelWithResponseStreamCommand(input);
|
|
17741
18282
|
return this._bedrockRuntime.send(command, { abortSignal });
|
|
17742
18283
|
}
|
|
18284
|
+
/**
|
|
18285
|
+
* The reasoning blocks the just-translated assistant turn produced, cleared as they are
|
|
18286
|
+
* taken. A backend whose provider signs thinking blocks overrides this so the tool loop
|
|
18287
|
+
* below can replay them onto the assistant turns it rebuilds; providers that sign nothing
|
|
18288
|
+
* keep the default. @see AnthropicBedrockBackend.takeReasoningBlocks
|
|
18289
|
+
*/
|
|
18290
|
+
takeReasoningBlocks() {
|
|
18291
|
+
return [];
|
|
18292
|
+
}
|
|
17743
18293
|
updateClientForModel(model) {
|
|
17744
18294
|
const requiredRegion = this.getRegionForModel(model);
|
|
17745
18295
|
this._options.region = requiredRegion;
|
|
@@ -17765,8 +18315,14 @@ var BaseBedrockBackend = class {
|
|
|
17765
18315
|
}, callback, toolsUsed);
|
|
17766
18316
|
return;
|
|
17767
18317
|
}
|
|
17768
|
-
|
|
18318
|
+
let messagesWithFormat = injectJsonSchemaInstruction(messages, options.responseFormat);
|
|
17769
18319
|
const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
|
|
18320
|
+
if (!options.tools?.length) {
|
|
18321
|
+
if (messagesWithFormat.some((m) => Array.isArray(m.content) && m.content.some((b) => b.type === "tool_use" || b.type === "tool_result"))) {
|
|
18322
|
+
Logger.globalInstance.warn("[BaseBedrockBackend Pre-API #6181] Tool blocks present but no tools offered this turn. Stripping all tool blocks.");
|
|
18323
|
+
messagesWithFormat = stripAllToolBlocks(messagesWithFormat, Logger.globalInstance);
|
|
18324
|
+
}
|
|
18325
|
+
}
|
|
17770
18326
|
let formattedMessages = this.formatMessages(messagesWithFormat);
|
|
17771
18327
|
let input = this.getPayload(model, formattedMessages, options);
|
|
17772
18328
|
const contextWindow = this.getModelContextWindow(model);
|
|
@@ -17900,6 +18456,12 @@ var BaseBedrockBackend = class {
|
|
|
17900
18456
|
});
|
|
17901
18457
|
} catch {
|
|
17902
18458
|
Logger.globalInstance.warn("[BaseBedrockBackend] Tool parameter parse error, skipping tool:", name);
|
|
18459
|
+
const entry = toolsUsed.find((t) => t.name === name && t.id === id);
|
|
18460
|
+
if (entry) entry.arguments = "{}";
|
|
18461
|
+
recordToolResult(toolsUsed, {
|
|
18462
|
+
id,
|
|
18463
|
+
name
|
|
18464
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
17903
18465
|
}
|
|
17904
18466
|
}
|
|
17905
18467
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -17927,32 +18489,40 @@ var BaseBedrockBackend = class {
|
|
|
17927
18489
|
parameters: resolvedTools[i].parameters,
|
|
17928
18490
|
error: outcome.error
|
|
17929
18491
|
});
|
|
18492
|
+
const roundReasoningBlocks = this.takeReasoningBlocks();
|
|
17930
18493
|
for (const outcome of outcomes) if (outcome.ok) {
|
|
17931
18494
|
await handleToolResultStreaming(outcome.name, outcome.result, async (results) => {
|
|
17932
18495
|
await callback(results, buildCompletionInfo());
|
|
17933
18496
|
});
|
|
18497
|
+
const resultStr = outcome.result.toString();
|
|
18498
|
+
recordToolResult(toolsUsed, {
|
|
18499
|
+
id: outcome.id,
|
|
18500
|
+
name: outcome.name
|
|
18501
|
+
}, resultStr, true);
|
|
17934
18502
|
this.pushToolMessages(messages, {
|
|
17935
18503
|
id: outcome.id,
|
|
17936
18504
|
name: outcome.name,
|
|
17937
18505
|
parameters: outcome.parameters
|
|
17938
|
-
},
|
|
18506
|
+
}, resultStr, roundReasoningBlocks);
|
|
17939
18507
|
} else {
|
|
17940
18508
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
17941
18509
|
if (isAbortError(outcome.error)) throw outcome.error;
|
|
17942
18510
|
Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${outcome.name} failed:`, outcome.error instanceof Error ? outcome.error.message : String(outcome.error));
|
|
18511
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
18512
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
18513
|
+
recordToolResult(toolsUsed, {
|
|
18514
|
+
id: outcome.id,
|
|
18515
|
+
name: outcome.name
|
|
18516
|
+
}, observation, false);
|
|
17943
18517
|
this.pushToolMessages(messages, {
|
|
17944
18518
|
id: outcome.id,
|
|
17945
18519
|
name: outcome.name,
|
|
17946
18520
|
parameters: outcome.parameters
|
|
17947
|
-
},
|
|
18521
|
+
}, observation, roundReasoningBlocks);
|
|
17948
18522
|
}
|
|
17949
18523
|
await callback(["\n\n"], buildCompletionInfo());
|
|
17950
18524
|
await this.complete(model, messages, {
|
|
17951
18525
|
...options,
|
|
17952
|
-
thinking: {
|
|
17953
|
-
enabled: false,
|
|
17954
|
-
budget_tokens: 0
|
|
17955
|
-
},
|
|
17956
18526
|
tool_choice: "auto",
|
|
17957
18527
|
_internal: {
|
|
17958
18528
|
...options._internal,
|
|
@@ -17996,35 +18566,38 @@ var BaseBedrockBackend = class {
|
|
|
17996
18566
|
if (options.executeTools !== false) {
|
|
17997
18567
|
const executable = toolChoices.map((tc) => tc.tool).filter((tool) => tool.id && tool.name && options.tools?.some((o) => o.toolSchema.name === tool.name));
|
|
17998
18568
|
if (executable.length > 0) {
|
|
18569
|
+
const roundReasoningBlocks = this.takeReasoningBlocks();
|
|
17999
18570
|
for (const { id, name, parameters } of executable) {
|
|
18000
18571
|
const toolFn = options.tools?.find((o) => o.toolSchema.name === name)?.toolFn;
|
|
18001
18572
|
if (!toolFn) continue;
|
|
18002
18573
|
const safeParameters = parameters || "{}";
|
|
18003
18574
|
let result;
|
|
18575
|
+
let succeeded = true;
|
|
18004
18576
|
try {
|
|
18005
18577
|
result = await toolFn(JSON.parse(safeParameters));
|
|
18006
18578
|
} catch (err) {
|
|
18007
18579
|
if (err instanceof PermissionDeniedError) throw err;
|
|
18008
18580
|
if (isAbortError(err)) throw err;
|
|
18009
18581
|
Logger.globalInstance.error(`[BaseBedrockBackend] Tool ${name} failed:`, err instanceof Error ? err.message : String(err));
|
|
18582
|
+
succeeded = false;
|
|
18010
18583
|
result = `Error processing ${name} tool: ${err instanceof Error ? err.message : "Unknown error"}`;
|
|
18011
18584
|
}
|
|
18012
18585
|
await handleToolResultStreaming(name, result, async (results) => {
|
|
18013
18586
|
await callback(results, buildCompletionInfo());
|
|
18014
18587
|
});
|
|
18588
|
+
recordToolResult(toolsUsed, {
|
|
18589
|
+
id,
|
|
18590
|
+
name
|
|
18591
|
+
}, result.toString(), succeeded);
|
|
18015
18592
|
this.pushToolMessages(messages, {
|
|
18016
18593
|
id,
|
|
18017
18594
|
name,
|
|
18018
18595
|
parameters
|
|
18019
|
-
}, result.toString());
|
|
18596
|
+
}, result.toString(), roundReasoningBlocks);
|
|
18020
18597
|
}
|
|
18021
18598
|
await callback(["\n\n"], buildCompletionInfo());
|
|
18022
18599
|
await this.complete(model, messages, {
|
|
18023
18600
|
...options,
|
|
18024
|
-
thinking: {
|
|
18025
|
-
enabled: false,
|
|
18026
|
-
budget_tokens: 0
|
|
18027
|
-
},
|
|
18028
18601
|
tool_choice: "auto",
|
|
18029
18602
|
_internal: {
|
|
18030
18603
|
...options._internal,
|
|
@@ -18119,6 +18692,9 @@ function isToolUseContentBlock(content_block) {
|
|
|
18119
18692
|
function isThinkingContentBlock(content_block) {
|
|
18120
18693
|
return isRecord(content_block) && content_block.type === "thinking";
|
|
18121
18694
|
}
|
|
18695
|
+
function isRedactedThinkingContentBlock(content_block) {
|
|
18696
|
+
return isRecord(content_block) && content_block.type === "redacted_thinking";
|
|
18697
|
+
}
|
|
18122
18698
|
function isTextDelta(delta) {
|
|
18123
18699
|
return isRecord(delta) && delta.type === "text_delta" && "text" in delta;
|
|
18124
18700
|
}
|
|
@@ -18128,6 +18704,9 @@ function isInputJsonDelta(delta) {
|
|
|
18128
18704
|
function isThinkingDelta(delta) {
|
|
18129
18705
|
return isRecord(delta) && delta.type === "thinking_delta" && "thinking" in delta;
|
|
18130
18706
|
}
|
|
18707
|
+
function isSignatureDelta(delta) {
|
|
18708
|
+
return isRecord(delta) && delta.type === "signature_delta" && "signature" in delta;
|
|
18709
|
+
}
|
|
18131
18710
|
function isMessageStart(chunk) {
|
|
18132
18711
|
return isRecord(chunk) && chunk.type === "message_start";
|
|
18133
18712
|
}
|
|
@@ -18155,12 +18734,35 @@ const TEMPERATURE_ONLY_MODELS = [
|
|
|
18155
18734
|
];
|
|
18156
18735
|
var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
18157
18736
|
isInThinkingBlock = false;
|
|
18737
|
+
/**
|
|
18738
|
+
* Reasoning blocks of the assistant turn currently being translated, indexed by the
|
|
18739
|
+
* stream's content-block index. Reset at `message_start` and consumed by
|
|
18740
|
+
* `takeReasoningBlocks` when that turn is rebuilt for a tool continuation.
|
|
18741
|
+
*/
|
|
18742
|
+
assistantReasoningBlocks = [];
|
|
18158
18743
|
/** Catalog view of the model being completed; see DispatchModel. */
|
|
18159
18744
|
_dispatch = new DispatchModel();
|
|
18160
18745
|
setDispatchModel(info) {
|
|
18161
18746
|
this._dispatch.set(info);
|
|
18162
18747
|
}
|
|
18163
18748
|
/**
|
|
18749
|
+
* The reasoning blocks the last translated assistant turn produced, cleared as they
|
|
18750
|
+
* are taken. Anthropic requires a `tool_use` assistant turn to replay its own signed
|
|
18751
|
+
* thinking blocks whenever extended thinking is active on the turn, and an adaptive
|
|
18752
|
+
* model thinks on every turn whether or not the request asked it to - so dropping
|
|
18753
|
+
* them is what makes the synthesis round of a multi-round tool turn come back empty.
|
|
18754
|
+
*
|
|
18755
|
+
* Taken once per provider turn. base.ts takes them before its tool loop and hands the
|
|
18756
|
+
* same array to every assistant message it rebuilds for that round, because a parallel
|
|
18757
|
+
* round splits one provider turn across several synthetic turns and each of them has to
|
|
18758
|
+
* carry the reasoning.
|
|
18759
|
+
*/
|
|
18760
|
+
takeReasoningBlocks() {
|
|
18761
|
+
const blocks = this.assistantReasoningBlocks.filter((b) => b != null);
|
|
18762
|
+
this.assistantReasoningBlocks = [];
|
|
18763
|
+
return blocks;
|
|
18764
|
+
}
|
|
18765
|
+
/**
|
|
18164
18766
|
* The record the payload is shaped from: the adapter table first, then the
|
|
18165
18767
|
* catalog for a model the table never listed. Table-first keeps every
|
|
18166
18768
|
* currently-dispatched Bedrock id on exactly today's payload.
|
|
@@ -18577,11 +19179,13 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18577
19179
|
content: ""
|
|
18578
19180
|
};
|
|
18579
19181
|
}).filter((m) => m.content !== "" && (Array.isArray(m.content) ? m.content.length > 0 : true));
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
}
|
|
19182
|
+
const systemBlocks = messages.filter((m) => m.role === "system" && m.content).map((m) => ({
|
|
19183
|
+
text: systemContentToText(m.content),
|
|
19184
|
+
cache: m.cache === true
|
|
19185
|
+
})).filter((block) => block.text.trim() !== "");
|
|
19186
|
+
const identityReminder = options.omitIdentityReminder ? null : `IMPORTANT! Only when someone asks, remember that you are specifically the ${model} model.`;
|
|
19187
|
+
let systemMessage = systemBlocks.map((block) => block.text).join("\n");
|
|
19188
|
+
if (identityReminder) systemMessage = systemMessage ? `${systemMessage}\n${identityReminder}` : identityReminder;
|
|
18585
19189
|
const modelId = model.includes(":") || model.startsWith("global.") || model.startsWith("us.") || model.startsWith("anthropic.") ? model : `anthropic.${model}`;
|
|
18586
19190
|
const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : 4096;
|
|
18587
19191
|
const body = {
|
|
@@ -18589,7 +19193,27 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18589
19193
|
max_tokens: maxTokens,
|
|
18590
19194
|
messages: filteredMessages
|
|
18591
19195
|
};
|
|
18592
|
-
|
|
19196
|
+
const cacheStrategy = options.cacheStrategy;
|
|
19197
|
+
const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
|
|
19198
|
+
if (Boolean(cacheStrategy?.enableCaching) && modelSupportsCaching && systemBlocks.some((block) => block.cache)) {
|
|
19199
|
+
const cacheControl = {
|
|
19200
|
+
type: "ephemeral",
|
|
19201
|
+
...cacheStrategy?.cacheTTL === "1h" ? { ttl: cacheStrategy.cacheTTL } : {}
|
|
19202
|
+
};
|
|
19203
|
+
const blocks = systemBlocks.map((block) => block.cache ? {
|
|
19204
|
+
type: "text",
|
|
19205
|
+
text: block.text,
|
|
19206
|
+
cache_control: cacheControl
|
|
19207
|
+
} : {
|
|
19208
|
+
type: "text",
|
|
19209
|
+
text: block.text
|
|
19210
|
+
});
|
|
19211
|
+
if (identityReminder) blocks.push({
|
|
19212
|
+
type: "text",
|
|
19213
|
+
text: identityReminder
|
|
19214
|
+
});
|
|
19215
|
+
body.system = blocks;
|
|
19216
|
+
} else if (systemMessage) body.system = systemMessage;
|
|
18593
19217
|
if (options.tools?.length) body.tools = this.formatTools(options.tools);
|
|
18594
19218
|
if (typeof options.temperature === "number" && !this.omitsSamplingParams(model)) body.temperature = options.temperature;
|
|
18595
19219
|
if (typeof options.topP === "number" && !TEMPERATURE_ONLY_MODELS.includes(model) && !this.omitsSamplingParams(model)) body.top_p = options.topP;
|
|
@@ -18617,8 +19241,6 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18617
19241
|
topP: options.topP
|
|
18618
19242
|
}
|
|
18619
19243
|
}, null, 2)}`);
|
|
18620
|
-
const cacheStrategy = options.cacheStrategy;
|
|
18621
|
-
const modelSupportsCaching = !BEDROCK_NO_PROMPT_CACHING_MODELS.has(modelId);
|
|
18622
19244
|
if (cacheStrategy?.enableCaching && modelSupportsCaching) {
|
|
18623
19245
|
const cachedBody = getCachingAdapter(ModelBackend.Bedrock).applyCaching(body, cacheStrategy);
|
|
18624
19246
|
Object.assign(body, cachedBody);
|
|
@@ -18642,9 +19264,11 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18642
19264
|
formatMessages(messages) {
|
|
18643
19265
|
return messages.reduce((cur, value) => {
|
|
18644
19266
|
const previousMessage = cur[cur.length - 1];
|
|
18645
|
-
if (previousMessage && value.role === previousMessage.role) {
|
|
18646
|
-
if (previousMessage.content === value.content)
|
|
18647
|
-
|
|
19267
|
+
if (previousMessage && value.role === previousMessage.role && previousMessage.cache !== true) {
|
|
19268
|
+
if (previousMessage.content === value.content) {
|
|
19269
|
+
if (value.cache === true) previousMessage.cache = true;
|
|
19270
|
+
return cur;
|
|
19271
|
+
} else if (!Array.isArray(previousMessage.content)) {
|
|
18648
19272
|
const lastIndex = cur.length - 1;
|
|
18649
19273
|
const prevContent = typeof cur[lastIndex].content === "string" ? cur[lastIndex].content : "";
|
|
18650
19274
|
const currContent = typeof value.content === "string" ? value.content : "";
|
|
@@ -18664,6 +19288,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18664
19288
|
});
|
|
18665
19289
|
if (contentArray.length > 0) cur[lastIndex].content = contentArray;
|
|
18666
19290
|
}
|
|
19291
|
+
if (value.cache === true) cur[lastIndex].cache = true;
|
|
18667
19292
|
} else {
|
|
18668
19293
|
if (typeof value.content !== "string") {
|
|
18669
19294
|
cur.push(value);
|
|
@@ -18675,6 +19300,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18675
19300
|
type: "text",
|
|
18676
19301
|
text: textContent
|
|
18677
19302
|
}];
|
|
19303
|
+
if (value.cache === true) previousMessage.cache = true;
|
|
18678
19304
|
}
|
|
18679
19305
|
return cur;
|
|
18680
19306
|
}
|
|
@@ -18685,11 +19311,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18685
19311
|
translateChunk(model, chunk) {
|
|
18686
19312
|
try {
|
|
18687
19313
|
const response = chunk;
|
|
18688
|
-
|
|
18689
|
-
if (
|
|
18690
|
-
thinkingBlockCount: thinkingBlocks.length,
|
|
18691
|
-
thinkingLengths: thinkingBlocks.map((b) => b.thinking?.length || 0)
|
|
18692
|
-
});
|
|
19314
|
+
this.assistantReasoningBlocks = response.content.filter((c) => c.type === "thinking" || c.type === "redacted_thinking");
|
|
19315
|
+
if (this.assistantReasoningBlocks.length > 0) console.log(`[AnthropicBedrockBackend] Captured ${this.assistantReasoningBlocks.length} reasoning block(s) for tool continuation`);
|
|
18693
19316
|
const textContent = response.content.filter((item) => item.type === "text").map((item) => item.text || "").join("");
|
|
18694
19317
|
const toolUseBlocks = response.content.filter((item) => item.type === "tool_use");
|
|
18695
19318
|
let choice;
|
|
@@ -18743,6 +19366,7 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18743
19366
|
try {
|
|
18744
19367
|
if (isMessageStart(chunk)) {
|
|
18745
19368
|
this.isInThinkingBlock = false;
|
|
19369
|
+
this.assistantReasoningBlocks = [];
|
|
18746
19370
|
choice = {
|
|
18747
19371
|
chunkText: "",
|
|
18748
19372
|
usage: {
|
|
@@ -18764,8 +19388,12 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18764
19388
|
};
|
|
18765
19389
|
else if (isThinkingContentBlock(contentBlock)) {
|
|
18766
19390
|
this.isInThinkingBlock = true;
|
|
19391
|
+
this.assistantReasoningBlocks[chunk.index] = {
|
|
19392
|
+
...contentBlock,
|
|
19393
|
+
thinking: contentBlock.thinking ?? ""
|
|
19394
|
+
};
|
|
18767
19395
|
choice.chunkText = "<think>";
|
|
18768
|
-
}
|
|
19396
|
+
} else if (isRedactedThinkingContentBlock(contentBlock)) this.assistantReasoningBlocks[chunk.index] = { ...contentBlock };
|
|
18769
19397
|
} else if (isContentBlockDelta(chunk)) {
|
|
18770
19398
|
choice = {
|
|
18771
19399
|
status: "stream",
|
|
@@ -18775,7 +19403,14 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18775
19403
|
const delta = chunk.delta;
|
|
18776
19404
|
if (isTextDelta(delta)) choice.chunkText = delta.text;
|
|
18777
19405
|
else if (isInputJsonDelta(delta)) choice.chunkText = delta.partial_json;
|
|
18778
|
-
else if (isThinkingDelta(delta))
|
|
19406
|
+
else if (isThinkingDelta(delta)) {
|
|
19407
|
+
choice.chunkText = delta.thinking;
|
|
19408
|
+
const block = this.assistantReasoningBlocks[chunk.index];
|
|
19409
|
+
if (block?.type === "thinking") block.thinking += delta.thinking;
|
|
19410
|
+
} else if (isSignatureDelta(delta)) {
|
|
19411
|
+
const block = this.assistantReasoningBlocks[chunk.index];
|
|
19412
|
+
if (block?.type === "thinking") block.signature = delta.signature;
|
|
19413
|
+
}
|
|
18779
19414
|
} else if (isContentBlockStop(chunk)) {
|
|
18780
19415
|
choice = {
|
|
18781
19416
|
status: "stream",
|
|
@@ -18834,7 +19469,8 @@ var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
|
18834
19469
|
name: tool.name,
|
|
18835
19470
|
input: JSON.parse(tool.parameters || "{}")
|
|
18836
19471
|
};
|
|
18837
|
-
const
|
|
19472
|
+
const reasoningBlocks = thinkingBlocks?.length ? thinkingBlocks : this.takeReasoningBlocks();
|
|
19473
|
+
const assistantContent = reasoningBlocks.length > 0 ? [...reasoningBlocks, toolUseBlock] : [toolUseBlock];
|
|
18838
19474
|
messages.push({
|
|
18839
19475
|
role: "assistant",
|
|
18840
19476
|
content: assistantContent
|
|
@@ -19966,7 +20602,7 @@ var BFLBackend = class {
|
|
|
19966
20602
|
input: 400,
|
|
19967
20603
|
output: 400
|
|
19968
20604
|
} },
|
|
19969
|
-
description: "FLUX
|
|
20605
|
+
description: "FLUX Pro 1.1 - Professional quality image generation with strong prompt adherence and fast turnaround.",
|
|
19970
20606
|
supportsSafetyTolerance: true,
|
|
19971
20607
|
rank: 1
|
|
19972
20608
|
},
|
|
@@ -20032,7 +20668,7 @@ var BFLBackend = class {
|
|
|
20032
20668
|
input: 350,
|
|
20033
20669
|
output: 350
|
|
20034
20670
|
} },
|
|
20035
|
-
description: "FLUX Kontext
|
|
20671
|
+
description: "FLUX Kontext Pro - Advanced image-to-image transformation with high-quality output and versatile editing capabilities.",
|
|
20036
20672
|
supportsSafetyTolerance: true,
|
|
20037
20673
|
rank: 4
|
|
20038
20674
|
},
|
|
@@ -20048,7 +20684,7 @@ var BFLBackend = class {
|
|
|
20048
20684
|
input: 450,
|
|
20049
20685
|
output: 450
|
|
20050
20686
|
} },
|
|
20051
|
-
description: "
|
|
20687
|
+
description: "FLUX Kontext Max - Premium image-to-image transformation with maximum quality and capabilities.",
|
|
20052
20688
|
supportsSafetyTolerance: true,
|
|
20053
20689
|
rank: 5
|
|
20054
20690
|
}
|
|
@@ -20506,6 +21142,7 @@ var GeminiBackend = class {
|
|
|
20506
21142
|
const toolCallCount = options._internal?.toolCallCount ?? 0;
|
|
20507
21143
|
const accumInputTokens = options._internal?.accumInputTokens ?? 0;
|
|
20508
21144
|
const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
|
|
21145
|
+
const liveToolUseIds = new Set(options._internal?.liveToolUseIds ?? []);
|
|
20509
21146
|
const maxToolCalls = options._internal?.maxToolCalls ?? 10;
|
|
20510
21147
|
if (toolCallCount >= maxToolCalls && options.tools?.length) {
|
|
20511
21148
|
this.logger.warn(`[Gemini] Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
|
|
@@ -20520,7 +21157,7 @@ var GeminiBackend = class {
|
|
|
20520
21157
|
const bestEffortFormat = isBestEffortJsonSchema(options.responseFormat);
|
|
20521
21158
|
const systemInstruction = messagesWithFormat.filter((message) => message.role === "system").map((message) => message.content).join("\n");
|
|
20522
21159
|
const nonsystemMessages = messagesWithFormat.filter((message) => message.role !== "system");
|
|
20523
|
-
const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages);
|
|
21160
|
+
const contents = this.formatMessagesIntoGeminiContent(nonsystemMessages, liveToolUseIds);
|
|
20524
21161
|
const generationConfig = this.getGenerationConfig(modelInfo, options);
|
|
20525
21162
|
const tools = !options.tools?.length ? void 0 : [{ functionDeclarations: options.tools.map((tool) => {
|
|
20526
21163
|
const params = pick(tool.toolSchema.parameters, "type", "properties", "required");
|
|
@@ -20647,22 +21284,32 @@ var GeminiBackend = class {
|
|
|
20647
21284
|
await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
|
|
20648
21285
|
await callback(results, { toolsUsed });
|
|
20649
21286
|
});
|
|
21287
|
+
const resultContent = JSON.stringify({ result: outcome.result });
|
|
21288
|
+
recordToolResult(toolsUsed, {
|
|
21289
|
+
id: outcome.toolCall.id,
|
|
21290
|
+
name: outcome.toolCall.name
|
|
21291
|
+
}, resultContent, true);
|
|
20650
21292
|
messages.push({
|
|
20651
21293
|
role: "tool",
|
|
20652
21294
|
content: [{
|
|
20653
21295
|
type: "tool_result",
|
|
20654
|
-
content:
|
|
21296
|
+
content: resultContent,
|
|
20655
21297
|
tool_use_id: outcome.toolCall.id
|
|
20656
21298
|
}]
|
|
20657
21299
|
});
|
|
20658
21300
|
} else {
|
|
20659
21301
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
20660
21302
|
this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
|
|
21303
|
+
const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
|
|
21304
|
+
recordToolResult(toolsUsed, {
|
|
21305
|
+
id: outcome.toolCall.id,
|
|
21306
|
+
name: outcome.toolCall.name
|
|
21307
|
+
}, errorContent, false);
|
|
20661
21308
|
messages.push({
|
|
20662
21309
|
role: "tool",
|
|
20663
21310
|
content: [{
|
|
20664
21311
|
type: "tool_result",
|
|
20665
|
-
content:
|
|
21312
|
+
content: errorContent,
|
|
20666
21313
|
tool_use_id: outcome.toolCall.id
|
|
20667
21314
|
}]
|
|
20668
21315
|
});
|
|
@@ -20674,7 +21321,8 @@ var GeminiBackend = class {
|
|
|
20674
21321
|
...options._internal,
|
|
20675
21322
|
toolCallCount: toolCallCount + 1,
|
|
20676
21323
|
accumInputTokens: accumInputTokens + turnInputTokens,
|
|
20677
|
-
accumOutputTokens: accumOutputTokens + turnOutputTokens
|
|
21324
|
+
accumOutputTokens: accumOutputTokens + turnOutputTokens,
|
|
21325
|
+
liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
|
|
20678
21326
|
}
|
|
20679
21327
|
}, callback, toolsUsed);
|
|
20680
21328
|
} else {
|
|
@@ -20773,22 +21421,32 @@ var GeminiBackend = class {
|
|
|
20773
21421
|
await handleToolResultStreaming(outcome.toolCall.name, outcome.result, async (results) => {
|
|
20774
21422
|
await callback(results, { toolsUsed });
|
|
20775
21423
|
});
|
|
21424
|
+
const resultContent = JSON.stringify({ result: outcome.result });
|
|
21425
|
+
recordToolResult(toolsUsed, {
|
|
21426
|
+
id: outcome.toolCall.id,
|
|
21427
|
+
name: outcome.toolCall.name
|
|
21428
|
+
}, resultContent, true);
|
|
20776
21429
|
messages.push({
|
|
20777
21430
|
role: "tool",
|
|
20778
21431
|
content: [{
|
|
20779
21432
|
type: "tool_result",
|
|
20780
|
-
content:
|
|
21433
|
+
content: resultContent,
|
|
20781
21434
|
tool_use_id: outcome.toolCall.id
|
|
20782
21435
|
}]
|
|
20783
21436
|
});
|
|
20784
21437
|
} else {
|
|
20785
21438
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
20786
21439
|
this.logger.error(`[Gemini] Error executing tool ${outcome.toolCall.name}:`, outcome.error);
|
|
21440
|
+
const errorContent = JSON.stringify({ error: outcome.error instanceof Error ? outcome.error.message : "Unknown error" });
|
|
21441
|
+
recordToolResult(toolsUsed, {
|
|
21442
|
+
id: outcome.toolCall.id,
|
|
21443
|
+
name: outcome.toolCall.name
|
|
21444
|
+
}, errorContent, false);
|
|
20787
21445
|
messages.push({
|
|
20788
21446
|
role: "tool",
|
|
20789
21447
|
content: [{
|
|
20790
21448
|
type: "tool_result",
|
|
20791
|
-
content:
|
|
21449
|
+
content: errorContent,
|
|
20792
21450
|
tool_use_id: outcome.toolCall.id
|
|
20793
21451
|
}]
|
|
20794
21452
|
});
|
|
@@ -20800,7 +21458,8 @@ var GeminiBackend = class {
|
|
|
20800
21458
|
...options._internal,
|
|
20801
21459
|
toolCallCount: toolCallCount + 1,
|
|
20802
21460
|
accumInputTokens: accumInputTokens + turnInputTokens,
|
|
20803
|
-
accumOutputTokens: accumOutputTokens + turnOutputTokens
|
|
21461
|
+
accumOutputTokens: accumOutputTokens + turnOutputTokens,
|
|
21462
|
+
liveToolUseIds: [...liveToolUseIds, ...toolCalls.map((tc) => tc.id)]
|
|
20804
21463
|
}
|
|
20805
21464
|
}, callback, toolsUsed);
|
|
20806
21465
|
} else {
|
|
@@ -20809,8 +21468,9 @@ var GeminiBackend = class {
|
|
|
20809
21468
|
}
|
|
20810
21469
|
}
|
|
20811
21470
|
}
|
|
20812
|
-
formatMessagesIntoGeminiContent(messages) {
|
|
21471
|
+
formatMessagesIntoGeminiContent(messages, liveToolUseIds) {
|
|
20813
21472
|
const toolUseIdToName = /* @__PURE__ */ new Map();
|
|
21473
|
+
const droppedToolUseIds = /* @__PURE__ */ new Set();
|
|
20814
21474
|
return messages.map((message) => {
|
|
20815
21475
|
const mapRole = (role) => {
|
|
20816
21476
|
switch (role) {
|
|
@@ -20824,25 +21484,41 @@ var GeminiBackend = class {
|
|
|
20824
21484
|
role: mapRole(message.role),
|
|
20825
21485
|
parts: [{ text: message.content }]
|
|
20826
21486
|
};
|
|
20827
|
-
|
|
21487
|
+
const hasToolUse = Array.isArray(message.content) && message.content.some((item) => item.type === "tool_use");
|
|
21488
|
+
if (!hasToolUse && message.content?.[0].type === "text") return {
|
|
20828
21489
|
role: mapRole(message.role),
|
|
20829
21490
|
parts: [{ text: (message.content?.[0]).text }]
|
|
20830
21491
|
};
|
|
20831
|
-
if (message.content?.[0].type === "image") return {
|
|
21492
|
+
if (!hasToolUse && message.content?.[0].type === "image") return {
|
|
20832
21493
|
role: mapRole(message.role),
|
|
20833
21494
|
parts: [{ inlineData: {
|
|
20834
21495
|
mimeType: (message.content?.[0]).source.media_type,
|
|
20835
21496
|
data: (message.content?.[0]).source.data
|
|
20836
21497
|
} }]
|
|
20837
21498
|
};
|
|
20838
|
-
if (
|
|
20839
|
-
const
|
|
21499
|
+
if (hasToolUse) {
|
|
21500
|
+
const toolUseBlocks = message.content.filter((item) => item.type === "tool_use");
|
|
21501
|
+
const textParts = message.content.filter((item) => item.type === "text").map((item) => ({ text: item.text }));
|
|
21502
|
+
if (!toolUseBlocks.some((t) => liveToolUseIds.has(t.id)) && /^gemini-3(\D|$)/.test(this.currentModel) && !toolUseBlocks[0]?.thought_signature) {
|
|
21503
|
+
this.logger.warn("[Gemini] Dropping replayed tool_use block(s) with no thought_signature on the first call:", {
|
|
21504
|
+
names: toolUseBlocks.map((t) => t.name),
|
|
21505
|
+
messageRole: message.role
|
|
21506
|
+
});
|
|
21507
|
+
toolUseBlocks.forEach((t) => droppedToolUseIds.add(t.id));
|
|
21508
|
+
if (textParts.length === 0) return null;
|
|
21509
|
+
return {
|
|
21510
|
+
role: mapRole(message.role),
|
|
21511
|
+
parts: textParts
|
|
21512
|
+
};
|
|
21513
|
+
}
|
|
21514
|
+
const parts = [...textParts];
|
|
21515
|
+
parts.push(...toolUseBlocks.map((toolUse, index) => {
|
|
20840
21516
|
toolUseIdToName.set(toolUse.id, toolUse.name);
|
|
20841
21517
|
const part = { functionCall: {
|
|
20842
21518
|
name: toolUse.name,
|
|
20843
21519
|
args: toolUse.input
|
|
20844
21520
|
} };
|
|
20845
|
-
if (index === 0
|
|
21521
|
+
if (index === 0) if (toolUse.thought_signature) {
|
|
20846
21522
|
part.thoughtSignature = toolUse.thought_signature;
|
|
20847
21523
|
part.thought_signature = toolUse.thought_signature;
|
|
20848
21524
|
this.logger.debug("[Gemini] Including thought_signature in request (both formats):", {
|
|
@@ -20850,29 +21526,26 @@ var GeminiBackend = class {
|
|
|
20850
21526
|
id: toolUse.id,
|
|
20851
21527
|
position: "first"
|
|
20852
21528
|
});
|
|
20853
|
-
} else
|
|
20854
|
-
|
|
20855
|
-
|
|
20856
|
-
|
|
20857
|
-
messageRole: message.role
|
|
20858
|
-
});
|
|
20859
|
-
this.logger.warn("[Gemini] This may cause a 400 error with Gemini 3 Pro");
|
|
20860
|
-
}
|
|
21529
|
+
} else this.logger.warn("[Gemini] Missing thought_signature for first function call:", {
|
|
21530
|
+
name: toolUse.name,
|
|
21531
|
+
id: toolUse.id
|
|
21532
|
+
});
|
|
20861
21533
|
return part;
|
|
20862
|
-
});
|
|
21534
|
+
}));
|
|
20863
21535
|
return {
|
|
20864
21536
|
role: mapRole(message.role),
|
|
20865
21537
|
parts
|
|
20866
21538
|
};
|
|
20867
21539
|
}
|
|
20868
21540
|
if (message.content?.[0].type === "tool_result") {
|
|
20869
|
-
const
|
|
21541
|
+
const parts = message.content.filter((item) => item.type === "tool_result").filter((toolResult) => !droppedToolUseIds.has(toolResult.tool_use_id)).map((toolResult) => ({ functionResponse: {
|
|
21542
|
+
name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
|
|
21543
|
+
response: { result: toolResult.content }
|
|
21544
|
+
} }));
|
|
21545
|
+
if (parts.length === 0) return null;
|
|
20870
21546
|
return {
|
|
20871
21547
|
role: mapRole(message.role),
|
|
20872
|
-
parts
|
|
20873
|
-
name: toolUseIdToName.get(toolResult.tool_use_id) ?? toolResult.tool_use_id,
|
|
20874
|
-
response: { result: (message.content?.[0]).content }
|
|
20875
|
-
} }]
|
|
21548
|
+
parts
|
|
20876
21549
|
};
|
|
20877
21550
|
}
|
|
20878
21551
|
return null;
|
|
@@ -20959,6 +21632,54 @@ var GeminiBackend = class {
|
|
|
20959
21632
|
}
|
|
20960
21633
|
};
|
|
20961
21634
|
/**
|
|
21635
|
+
* The cache-inclusive-to-cache-exclusive conversion, shared by every adapter whose
|
|
21636
|
+
* provider reports cached tokens as a SUBSET of the prompt count.
|
|
21637
|
+
*
|
|
21638
|
+
* getTextModelCost expects Anthropic's convention: `inputTokens` counts only uncached
|
|
21639
|
+
* tokens and cache reads bill separately at their own (much cheaper) rate. Anthropic
|
|
21640
|
+
* and Claude-on-Bedrock deliver that natively. OpenAI and Moonshot do not - their
|
|
21641
|
+
* prompt total already CONTAINS the cached tokens - so those adapters must subtract
|
|
21642
|
+
* here before forwarding, or settlement double-bills the cached portion.
|
|
21643
|
+
*
|
|
21644
|
+
* Must stay in sync with the disjoint-fields assumption documented at the settlement
|
|
21645
|
+
* site in ChatCompletionProcess.
|
|
21646
|
+
*/
|
|
21647
|
+
/**
|
|
21648
|
+
* Split a cache-INCLUSIVE prompt total into the disjoint pair CompletionInfo carries.
|
|
21649
|
+
*
|
|
21650
|
+
* Forwarding the cached count without subtracting double-bills it; forwarding nothing
|
|
21651
|
+
* charges the full input rate on tokens the provider billed at a fraction of it.
|
|
21652
|
+
* Subtracting is the only split that bills what the provider actually charged.
|
|
21653
|
+
*
|
|
21654
|
+
* Clamped at zero: if a feed ever reports more cached than prompt tokens, a negative
|
|
21655
|
+
* input count would silently credit the user.
|
|
21656
|
+
*/
|
|
21657
|
+
function splitCacheInclusiveInput(totalPromptTokens, cacheReadTokens) {
|
|
21658
|
+
if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
|
|
21659
|
+
const cached = Math.min(cacheReadTokens, totalPromptTokens);
|
|
21660
|
+
return {
|
|
21661
|
+
inputTokens: Math.max(0, totalPromptTokens - cached),
|
|
21662
|
+
cacheReadInputTokens: cached
|
|
21663
|
+
};
|
|
21664
|
+
}
|
|
21665
|
+
/**
|
|
21666
|
+
* Cached prompt tokens from a raw provider usage object, across every spelling in use:
|
|
21667
|
+
* OpenAI Chat Completions nests them under `prompt_tokens_details`, the OpenAI
|
|
21668
|
+
* Responses API under `input_tokens_details`, and Moonshot publishes a flat
|
|
21669
|
+
* `cached_tokens` alongside the OpenAI-shaped nesting. Reading only one spelling
|
|
21670
|
+
* silently bills every cache hit on the other transports at the full input rate.
|
|
21671
|
+
*/
|
|
21672
|
+
function cachedTokensFromUsage(usage) {
|
|
21673
|
+
if (!usage) return 0;
|
|
21674
|
+
const candidates = [
|
|
21675
|
+
usage.cached_tokens,
|
|
21676
|
+
usage.prompt_tokens_details?.cached_tokens,
|
|
21677
|
+
usage.input_tokens_details?.cached_tokens
|
|
21678
|
+
];
|
|
21679
|
+
for (const value of candidates) if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
21680
|
+
return 0;
|
|
21681
|
+
}
|
|
21682
|
+
/**
|
|
20962
21683
|
* Request shaping for Moonshot's Kimi models. Kept separate from kimiBackend's
|
|
20963
21684
|
* transport so every "which parameter does this id accept" rule is one pure
|
|
20964
21685
|
* function with a test, rather than a conditional buried in a 400-line complete().
|
|
@@ -21347,7 +22068,7 @@ var KimiBackend = class {
|
|
|
21347
22068
|
if (!(response instanceof Stream)) {
|
|
21348
22069
|
const streamedText = [];
|
|
21349
22070
|
if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from the Moonshot API");
|
|
21350
|
-
const turnCacheReadTokens =
|
|
22071
|
+
const turnCacheReadTokens = cachedTokensFromUsage(response.usage);
|
|
21351
22072
|
for (const c of response.choices) {
|
|
21352
22073
|
if (!c.message) continue;
|
|
21353
22074
|
const reasoningContent = c.message.reasoning_content;
|
|
@@ -21379,6 +22100,10 @@ var KimiBackend = class {
|
|
|
21379
22100
|
this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
|
|
21380
22101
|
const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
|
|
21381
22102
|
if (entry) entry.arguments = "{}";
|
|
22103
|
+
recordToolResult(toolsUsed, {
|
|
22104
|
+
id: toolCall.id,
|
|
22105
|
+
name: toolCall.function.name
|
|
22106
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
21382
22107
|
}
|
|
21383
22108
|
}
|
|
21384
22109
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -21406,18 +22131,30 @@ var KimiBackend = class {
|
|
|
21406
22131
|
parameters: resolvedTools[i].parameters,
|
|
21407
22132
|
error: outcome.error
|
|
21408
22133
|
});
|
|
21409
|
-
for (const outcome of outcomes) if (outcome.ok)
|
|
21410
|
-
|
|
21411
|
-
|
|
21412
|
-
|
|
21413
|
-
|
|
21414
|
-
|
|
22134
|
+
for (const outcome of outcomes) if (outcome.ok) {
|
|
22135
|
+
const resultStr = outcome.result.toString();
|
|
22136
|
+
recordToolResult(toolsUsed, {
|
|
22137
|
+
id: outcome.id,
|
|
22138
|
+
name: outcome.name
|
|
22139
|
+
}, resultStr, true);
|
|
22140
|
+
this.pushToolMessages(messages, {
|
|
22141
|
+
id: outcome.id,
|
|
22142
|
+
name: outcome.name,
|
|
22143
|
+
parameters: outcome.parameters
|
|
22144
|
+
}, resultStr);
|
|
22145
|
+
} else {
|
|
21415
22146
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
22147
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
22148
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
22149
|
+
recordToolResult(toolsUsed, {
|
|
22150
|
+
id: outcome.id,
|
|
22151
|
+
name: outcome.name
|
|
22152
|
+
}, observation, false);
|
|
21416
22153
|
this.pushToolMessages(messages, {
|
|
21417
22154
|
id: outcome.id,
|
|
21418
22155
|
name: outcome.name,
|
|
21419
22156
|
parameters: outcome.parameters
|
|
21420
|
-
},
|
|
22157
|
+
}, observation);
|
|
21421
22158
|
}
|
|
21422
22159
|
await this.complete(model, messages, {
|
|
21423
22160
|
...options,
|
|
@@ -21433,7 +22170,7 @@ var KimiBackend = class {
|
|
|
21433
22170
|
} else {
|
|
21434
22171
|
this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
|
|
21435
22172
|
await callback([null], {
|
|
21436
|
-
...
|
|
22173
|
+
...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), accumCacheReadTokens + turnCacheReadTokens),
|
|
21437
22174
|
outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
|
|
21438
22175
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
21439
22176
|
});
|
|
@@ -21456,7 +22193,7 @@ var KimiBackend = class {
|
|
|
21456
22193
|
const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
|
|
21457
22194
|
const totalCacheReadTokens = accumCacheReadTokens + turnCacheReadTokens;
|
|
21458
22195
|
await callback(streamedText, {
|
|
21459
|
-
...
|
|
22196
|
+
...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
|
|
21460
22197
|
outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
|
|
21461
22198
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
21462
22199
|
cacheStats,
|
|
@@ -21475,7 +22212,7 @@ var KimiBackend = class {
|
|
|
21475
22212
|
if (chunk.usage) {
|
|
21476
22213
|
inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
|
|
21477
22214
|
outputTokens += chunk.usage?.completion_tokens || 0;
|
|
21478
|
-
const chunkCached =
|
|
22215
|
+
const chunkCached = cachedTokensFromUsage(chunk.usage);
|
|
21479
22216
|
if (chunkCached > 0) cachedTokensFromStream = chunkCached;
|
|
21480
22217
|
}
|
|
21481
22218
|
chunk?.choices.forEach((c) => {
|
|
@@ -21505,7 +22242,7 @@ var KimiBackend = class {
|
|
|
21505
22242
|
if (streamedText.some((t) => t)) sawAnyText = true;
|
|
21506
22243
|
const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
|
|
21507
22244
|
await callback(streamedText, {
|
|
21508
|
-
...
|
|
22245
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
21509
22246
|
outputTokens: accumOutputTokens + outputTokens,
|
|
21510
22247
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
21511
22248
|
...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
|
|
@@ -21513,7 +22250,7 @@ var KimiBackend = class {
|
|
|
21513
22250
|
}
|
|
21514
22251
|
if (isInThinkingBlock) {
|
|
21515
22252
|
await callback(["</think>"], {
|
|
21516
|
-
...
|
|
22253
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
21517
22254
|
outputTokens: accumOutputTokens + outputTokens,
|
|
21518
22255
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
21519
22256
|
});
|
|
@@ -21530,7 +22267,7 @@ var KimiBackend = class {
|
|
|
21530
22267
|
if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
|
|
21531
22268
|
}
|
|
21532
22269
|
if (nativeFormat && func.length === 0) await callback([], {
|
|
21533
|
-
...
|
|
22270
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
21534
22271
|
outputTokens: accumOutputTokens + outputTokens,
|
|
21535
22272
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
21536
22273
|
responseFormatMode: "native",
|
|
@@ -21566,6 +22303,10 @@ var KimiBackend = class {
|
|
|
21566
22303
|
this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
|
|
21567
22304
|
const entry = toolsUsed.find((t) => t.name === name && t.id === id);
|
|
21568
22305
|
if (entry) entry.arguments = "{}";
|
|
22306
|
+
recordToolResult(toolsUsed, {
|
|
22307
|
+
id,
|
|
22308
|
+
name
|
|
22309
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
21569
22310
|
}
|
|
21570
22311
|
}
|
|
21571
22312
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -21593,18 +22334,30 @@ var KimiBackend = class {
|
|
|
21593
22334
|
parameters: resolvedTools[i].parameters,
|
|
21594
22335
|
error: outcome.error
|
|
21595
22336
|
});
|
|
21596
|
-
for (const outcome of outcomes) if (outcome.ok)
|
|
21597
|
-
|
|
21598
|
-
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
22337
|
+
for (const outcome of outcomes) if (outcome.ok) {
|
|
22338
|
+
const resultStr = outcome.result.toString();
|
|
22339
|
+
recordToolResult(toolsUsed, {
|
|
22340
|
+
id: outcome.id,
|
|
22341
|
+
name: outcome.name
|
|
22342
|
+
}, resultStr, true);
|
|
22343
|
+
this.pushToolMessages(messages, {
|
|
22344
|
+
id: outcome.id,
|
|
22345
|
+
name: outcome.name,
|
|
22346
|
+
parameters: outcome.parameters
|
|
22347
|
+
}, resultStr);
|
|
22348
|
+
} else {
|
|
21602
22349
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
22350
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
22351
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
22352
|
+
recordToolResult(toolsUsed, {
|
|
22353
|
+
id: outcome.id,
|
|
22354
|
+
name: outcome.name
|
|
22355
|
+
}, observation, false);
|
|
21603
22356
|
this.pushToolMessages(messages, {
|
|
21604
22357
|
id: outcome.id,
|
|
21605
22358
|
name: outcome.name,
|
|
21606
22359
|
parameters: outcome.parameters
|
|
21607
|
-
},
|
|
22360
|
+
}, observation);
|
|
21608
22361
|
}
|
|
21609
22362
|
await this.complete(model, messages, {
|
|
21610
22363
|
...options,
|
|
@@ -21619,52 +22372,13 @@ var KimiBackend = class {
|
|
|
21619
22372
|
} else {
|
|
21620
22373
|
this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
|
|
21621
22374
|
await callback([null], {
|
|
21622
|
-
...
|
|
22375
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
21623
22376
|
outputTokens: accumOutputTokens + outputTokens,
|
|
21624
22377
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
21625
22378
|
});
|
|
21626
22379
|
}
|
|
21627
22380
|
}
|
|
21628
22381
|
}
|
|
21629
|
-
/**
|
|
21630
|
-
* Convert Moonshot's CACHE-INCLUSIVE `prompt_tokens` into the cache-EXCLUSIVE
|
|
21631
|
-
* convention getTextModelCost expects, which is Anthropic's: `inputTokens`
|
|
21632
|
-
* counts only uncached tokens and cache reads are billed separately at their
|
|
21633
|
-
* own (much cheaper) rate.
|
|
21634
|
-
*
|
|
21635
|
-
* Verified live 2026-07-28: a repeated 1220-token prompt returned
|
|
21636
|
-
* `prompt_tokens: 1220` WITH `cached_tokens: 1220` - the same tokens, reported
|
|
21637
|
-
* twice, not 1220 fresh plus 1220 cached. So passing prompt_tokens through as
|
|
21638
|
-
* inputTokens while also forwarding cacheReadInputTokens would bill the cached
|
|
21639
|
-
* portion twice; passing it through and forwarding NOTHING (the previous
|
|
21640
|
-
* behavior) charges the full input rate on tokens Moonshot billed at roughly a
|
|
21641
|
-
* sixth of it - on k2.6, $0.95/MTok against $0.16. Subtracting is the only
|
|
21642
|
-
* split that bills what the provider actually charged.
|
|
21643
|
-
*
|
|
21644
|
-
* Clamped at zero: if a feed ever reports more cached than prompt tokens, a
|
|
21645
|
-
* negative input count would silently credit the user.
|
|
21646
|
-
*/
|
|
21647
|
-
splitCachedInput(totalPromptTokens, cacheReadTokens) {
|
|
21648
|
-
if (cacheReadTokens <= 0) return { inputTokens: totalPromptTokens };
|
|
21649
|
-
const cached = Math.min(cacheReadTokens, totalPromptTokens);
|
|
21650
|
-
return {
|
|
21651
|
-
inputTokens: Math.max(0, totalPromptTokens - cached),
|
|
21652
|
-
cacheReadInputTokens: cached
|
|
21653
|
-
};
|
|
21654
|
-
}
|
|
21655
|
-
/**
|
|
21656
|
-
* Cached prompt tokens from a usage object. Moonshot publishes BOTH a flat
|
|
21657
|
-
* `usage.cached_tokens` and the nested OpenAI `prompt_tokens_details.cached_tokens`
|
|
21658
|
-
* - confirmed live, both present with the same value - so either spelling is
|
|
21659
|
-
* accepted and reading neither would bill every hit at the full input rate.
|
|
21660
|
-
*/
|
|
21661
|
-
cachedTokensOf(usage) {
|
|
21662
|
-
if (!usage) return 0;
|
|
21663
|
-
const flat = usage.cached_tokens;
|
|
21664
|
-
if (typeof flat === "number" && Number.isFinite(flat) && flat >= 0) return flat;
|
|
21665
|
-
const nested = usage.prompt_tokens_details?.cached_tokens;
|
|
21666
|
-
return typeof nested === "number" && Number.isFinite(nested) && nested >= 0 ? nested : 0;
|
|
21667
|
-
}
|
|
21668
22382
|
formatMessages(messages) {
|
|
21669
22383
|
return convertMessagesToOpenAIFormat(messages);
|
|
21670
22384
|
}
|
|
@@ -21969,7 +22683,7 @@ var OllamaBackend = class OllamaBackend {
|
|
|
21969
22683
|
name: tc.name,
|
|
21970
22684
|
parameters: tc.arguments || "{}"
|
|
21971
22685
|
}, `Error: tool "${tc.name}" is not available. Do not call it again; answer directly or use a listed tool.`);
|
|
21972
|
-
|
|
22686
|
+
const outcomes = await executeToolsBatch(resolved.map(({ tc, toolFn }) => async () => {
|
|
21973
22687
|
let params = {};
|
|
21974
22688
|
try {
|
|
21975
22689
|
params = JSON.parse(tc.arguments || "{}");
|
|
@@ -21979,17 +22693,22 @@ var OllamaBackend = class OllamaBackend {
|
|
|
21979
22693
|
}), {
|
|
21980
22694
|
parallel: options.parallelToolExecution !== false,
|
|
21981
22695
|
maxConcurrency: options.maxParallelTools
|
|
21982
|
-
})
|
|
22696
|
+
});
|
|
22697
|
+
const observations = [];
|
|
22698
|
+
outcomes.forEach((outcome, i) => {
|
|
21983
22699
|
const { tc } = resolved[i];
|
|
21984
22700
|
const params = tc.arguments || "{}";
|
|
21985
|
-
if (outcome.ok)
|
|
21986
|
-
|
|
21987
|
-
|
|
21988
|
-
|
|
21989
|
-
|
|
21990
|
-
|
|
22701
|
+
if (outcome.ok) {
|
|
22702
|
+
observations[i] = outcome.result;
|
|
22703
|
+
this.pushToolMessages(messages, {
|
|
22704
|
+
id: tc.id,
|
|
22705
|
+
name: tc.name,
|
|
22706
|
+
parameters: params
|
|
22707
|
+
}, outcome.result);
|
|
22708
|
+
} else {
|
|
21991
22709
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
21992
22710
|
const errorMsg = `Error running ${tc.name}: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
|
|
22711
|
+
observations[i] = errorMsg;
|
|
21993
22712
|
this.pushToolMessages(messages, {
|
|
21994
22713
|
id: tc.id,
|
|
21995
22714
|
name: tc.name,
|
|
@@ -21997,10 +22716,12 @@ var OllamaBackend = class OllamaBackend {
|
|
|
21997
22716
|
}, errorMsg);
|
|
21998
22717
|
}
|
|
21999
22718
|
});
|
|
22000
|
-
const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }) => ({
|
|
22719
|
+
const executedToolsUsed = [...priorToolsUsed, ...resolved.map(({ tc }, i) => ({
|
|
22001
22720
|
name: tc.name,
|
|
22002
22721
|
arguments: tc.arguments,
|
|
22003
|
-
id: tc.id
|
|
22722
|
+
id: tc.id,
|
|
22723
|
+
returnValue: truncateToolResult(String(observations[i])),
|
|
22724
|
+
success: outcomes[i].ok
|
|
22004
22725
|
}))];
|
|
22005
22726
|
if (options.abortSignal?.aborted) {
|
|
22006
22727
|
await callback([""], {
|
|
@@ -22108,12 +22829,21 @@ var OllamaBackend = class OllamaBackend {
|
|
|
22108
22829
|
}
|
|
22109
22830
|
};
|
|
22110
22831
|
}
|
|
22111
|
-
/**
|
|
22832
|
+
/**
|
|
22833
|
+
* Normalize Ollama's native tool_calls into the shared NormalizedToolCall shape.
|
|
22834
|
+
*
|
|
22835
|
+
* Ids are real uuids, not a position-derived string. A prior version keyed ids off
|
|
22836
|
+
* `accumulated-count + round-local-index`, but the accumulated count is measured AFTER
|
|
22837
|
+
* hallucinated calls are filtered out while the round-local index is assigned BEFORE that
|
|
22838
|
+
* filter runs, so the two can drift and mint the same id for two different real calls across
|
|
22839
|
+
* rounds - replayableToolCalls dedupes by id and silently drops the later one. A uuid makes
|
|
22840
|
+
* the whole collision class unrepresentable, matching how the other backends already mint ids.
|
|
22841
|
+
*/
|
|
22112
22842
|
normalizeToolCalls(toolCalls) {
|
|
22113
|
-
return toolCalls.map((tc
|
|
22843
|
+
return toolCalls.map((tc) => ({
|
|
22114
22844
|
name: tc.function.name,
|
|
22115
22845
|
arguments: JSON.stringify(tc.function.arguments ?? {}),
|
|
22116
|
-
id: `ollama-tool-${
|
|
22846
|
+
id: `ollama-tool-${v4()}`
|
|
22117
22847
|
}));
|
|
22118
22848
|
}
|
|
22119
22849
|
/**
|
|
@@ -22151,7 +22881,7 @@ var OllamaBackend = class OllamaBackend {
|
|
|
22151
22881
|
seen.add(key);
|
|
22152
22882
|
calls.push({
|
|
22153
22883
|
...call,
|
|
22154
|
-
id: `ollama-content-tool-${
|
|
22884
|
+
id: `ollama-content-tool-${v4()}`
|
|
22155
22885
|
});
|
|
22156
22886
|
}
|
|
22157
22887
|
return calls;
|
|
@@ -22400,7 +23130,8 @@ var OpenAIBackend = class {
|
|
|
22400
23130
|
can_stream: true,
|
|
22401
23131
|
pricing: { 1047576: {
|
|
22402
23132
|
input: 2 / 1e6,
|
|
22403
|
-
output: 8 / 1e6
|
|
23133
|
+
output: 8 / 1e6,
|
|
23134
|
+
cache_read: .5 / 1e6
|
|
22404
23135
|
} },
|
|
22405
23136
|
supportsVision: true,
|
|
22406
23137
|
supportsTools: true,
|
|
@@ -22420,7 +23151,8 @@ var OpenAIBackend = class {
|
|
|
22420
23151
|
can_stream: true,
|
|
22421
23152
|
pricing: { 1047576: {
|
|
22422
23153
|
input: .4 / 1e6,
|
|
22423
|
-
output: 1.6 / 1e6
|
|
23154
|
+
output: 1.6 / 1e6,
|
|
23155
|
+
cache_read: .1 / 1e6
|
|
22424
23156
|
} },
|
|
22425
23157
|
supportsVision: true,
|
|
22426
23158
|
supportsTools: true,
|
|
@@ -22440,7 +23172,8 @@ var OpenAIBackend = class {
|
|
|
22440
23172
|
can_stream: true,
|
|
22441
23173
|
pricing: { 1047576: {
|
|
22442
23174
|
input: .1 / 1e6,
|
|
22443
|
-
output: .4 / 1e6
|
|
23175
|
+
output: .4 / 1e6,
|
|
23176
|
+
cache_read: .025 / 1e6
|
|
22444
23177
|
} },
|
|
22445
23178
|
supportsVision: true,
|
|
22446
23179
|
supportsTools: true,
|
|
@@ -22503,7 +23236,8 @@ var OpenAIBackend = class {
|
|
|
22503
23236
|
can_stream: true,
|
|
22504
23237
|
pricing: { 2e5: {
|
|
22505
23238
|
input: 2 / 1e6,
|
|
22506
|
-
output: 8 / 1e6
|
|
23239
|
+
output: 8 / 1e6,
|
|
23240
|
+
cache_read: .5 / 1e6
|
|
22507
23241
|
} },
|
|
22508
23242
|
supportsVision: true,
|
|
22509
23243
|
supportsTools: true,
|
|
@@ -22589,7 +23323,8 @@ var OpenAIBackend = class {
|
|
|
22589
23323
|
can_think: true,
|
|
22590
23324
|
pricing: { 2e5: {
|
|
22591
23325
|
input: 1.1 / 1e6,
|
|
22592
|
-
output: 4.4 / 1e6
|
|
23326
|
+
output: 4.4 / 1e6,
|
|
23327
|
+
cache_read: .275 / 1e6
|
|
22593
23328
|
} },
|
|
22594
23329
|
supportsVision: true,
|
|
22595
23330
|
supportsImageVariation: false,
|
|
@@ -22965,7 +23700,8 @@ var OpenAIBackend = class {
|
|
|
22965
23700
|
can_stream: true,
|
|
22966
23701
|
pricing: { 8e3: {
|
|
22967
23702
|
input: 2.5 / 1e6,
|
|
22968
|
-
output: 10 / 1e6
|
|
23703
|
+
output: 10 / 1e6,
|
|
23704
|
+
cache_read: 1.25 / 1e6
|
|
22969
23705
|
} },
|
|
22970
23706
|
supportsVision: true,
|
|
22971
23707
|
supportsImageVariation: false,
|
|
@@ -22984,7 +23720,8 @@ var OpenAIBackend = class {
|
|
|
22984
23720
|
can_stream: true,
|
|
22985
23721
|
pricing: { 8e3: {
|
|
22986
23722
|
input: .15 / 1e6,
|
|
22987
|
-
output: .6 / 1e6
|
|
23723
|
+
output: .6 / 1e6,
|
|
23724
|
+
cache_read: .075 / 1e6
|
|
22988
23725
|
} },
|
|
22989
23726
|
supportsVision: true,
|
|
22990
23727
|
supportsImageVariation: false,
|
|
@@ -23175,6 +23912,7 @@ var OpenAIBackend = class {
|
|
|
23175
23912
|
const toolCallCount = options._internal?.toolCallCount ?? 0;
|
|
23176
23913
|
const accumInputTokens = options._internal?.accumInputTokens ?? 0;
|
|
23177
23914
|
const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
|
|
23915
|
+
const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
|
|
23178
23916
|
const maxToolCalls = options._internal?.maxToolCalls ?? 10;
|
|
23179
23917
|
if (toolCallCount >= maxToolCalls && options.tools?.length) {
|
|
23180
23918
|
this.logger.warn(`⚠️ Max tool calls limit (${maxToolCalls}) reached. Disabling tools to prevent infinite loops.`);
|
|
@@ -23264,6 +24002,7 @@ var OpenAIBackend = class {
|
|
|
23264
24002
|
let outputTokens = 0;
|
|
23265
24003
|
if (!(response instanceof Stream)) {
|
|
23266
24004
|
const streamedText = [];
|
|
24005
|
+
const totalCacheReadTokens = accumCacheReadTokens + cachedTokensFromUsage(response.usage);
|
|
23267
24006
|
if (!response.choices || response.choices.length === 0) throw new Error("No choices returned from OpenAI API");
|
|
23268
24007
|
for (const c of response.choices) {
|
|
23269
24008
|
if (!c.message) continue;
|
|
@@ -23297,6 +24036,10 @@ var OpenAIBackend = class {
|
|
|
23297
24036
|
this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
|
|
23298
24037
|
const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
|
|
23299
24038
|
if (entry) entry.arguments = "{}";
|
|
24039
|
+
recordToolResult(toolsUsed, {
|
|
24040
|
+
id: toolCall.id,
|
|
24041
|
+
name: toolCall.function.name
|
|
24042
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
23300
24043
|
}
|
|
23301
24044
|
}
|
|
23302
24045
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -23333,6 +24076,10 @@ var OpenAIBackend = class {
|
|
|
23333
24076
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
23334
24077
|
const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
|
|
23335
24078
|
streamedText[c.index] = errorMsg;
|
|
24079
|
+
recordToolResult(toolsUsed, {
|
|
24080
|
+
id: outcome.id,
|
|
24081
|
+
name: outcome.name
|
|
24082
|
+
}, errorMsg, false);
|
|
23336
24083
|
this.pushToolMessages(messages, {
|
|
23337
24084
|
id: outcome.id,
|
|
23338
24085
|
name: outcome.name,
|
|
@@ -23353,6 +24100,10 @@ var OpenAIBackend = class {
|
|
|
23353
24100
|
});
|
|
23354
24101
|
});
|
|
23355
24102
|
const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
|
|
24103
|
+
recordToolResult(toolsUsed, {
|
|
24104
|
+
id: outcome.id,
|
|
24105
|
+
name: outcome.name
|
|
24106
|
+
}, sanitizedResult, true);
|
|
23356
24107
|
this.pushToolMessages(messages, {
|
|
23357
24108
|
id: outcome.id,
|
|
23358
24109
|
name: outcome.name,
|
|
@@ -23376,7 +24127,8 @@ var OpenAIBackend = class {
|
|
|
23376
24127
|
...options._internal,
|
|
23377
24128
|
toolCallCount: toolCallCount + 1,
|
|
23378
24129
|
accumInputTokens: accumInputTokens + (response.usage?.prompt_tokens || 0),
|
|
23379
|
-
accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0)
|
|
24130
|
+
accumOutputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
|
|
24131
|
+
accumCacheReadTokens: totalCacheReadTokens
|
|
23380
24132
|
}
|
|
23381
24133
|
}, recursiveCallback, toolsUsed);
|
|
23382
24134
|
if (anyArtifactWasStreamed && recursiveBuffer) {
|
|
@@ -23387,7 +24139,7 @@ var OpenAIBackend = class {
|
|
|
23387
24139
|
} else {
|
|
23388
24140
|
this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
|
|
23389
24141
|
await callback([null], {
|
|
23390
|
-
|
|
24142
|
+
...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
|
|
23391
24143
|
outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
|
|
23392
24144
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
23393
24145
|
});
|
|
@@ -23403,7 +24155,7 @@ var OpenAIBackend = class {
|
|
|
23403
24155
|
}
|
|
23404
24156
|
const finishReason = normalizeOpenAIFinishReason(response.choices[0]?.finish_reason);
|
|
23405
24157
|
await callback(streamedText, {
|
|
23406
|
-
|
|
24158
|
+
...splitCacheInclusiveInput(accumInputTokens + (response.usage?.prompt_tokens || 0), totalCacheReadTokens),
|
|
23407
24159
|
outputTokens: accumOutputTokens + (response.usage?.completion_tokens || 0),
|
|
23408
24160
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23409
24161
|
cacheStats,
|
|
@@ -23431,7 +24183,7 @@ var OpenAIBackend = class {
|
|
|
23431
24183
|
inputTokens = Math.max(inputTokens, chunk.usage?.prompt_tokens || 0);
|
|
23432
24184
|
outputTokens += chunk.usage?.completion_tokens || 0;
|
|
23433
24185
|
if (chunk.usage.prompt_tokens_details?.cached_tokens !== void 0) {
|
|
23434
|
-
cachedTokensFromStream = chunk.usage.prompt_tokens_details.cached_tokens;
|
|
24186
|
+
cachedTokensFromStream = Math.max(cachedTokensFromStream, chunk.usage.prompt_tokens_details.cached_tokens);
|
|
23435
24187
|
if (cachedTokensFromStream > 0) this.logger.debug("[OpenAI] Captured cached tokens", { cachedTokens: cachedTokensFromStream });
|
|
23436
24188
|
else this.logger.debug("[OpenAI] No cached tokens in chunk", { note: "possible cache miss or first request" });
|
|
23437
24189
|
}
|
|
@@ -23448,7 +24200,7 @@ var OpenAIBackend = class {
|
|
|
23448
24200
|
});
|
|
23449
24201
|
const normalizedFinishReason = normalizeOpenAIFinishReason(streamFinishReason);
|
|
23450
24202
|
await callback(streamedText, {
|
|
23451
|
-
|
|
24203
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23452
24204
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23453
24205
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23454
24206
|
...normalizedFinishReason ? { stopReason: normalizedFinishReason } : {}
|
|
@@ -23478,7 +24230,7 @@ var OpenAIBackend = class {
|
|
|
23478
24230
|
if (cacheStats) logCacheStats(this.logger, cacheStats, { streaming: true });
|
|
23479
24231
|
}
|
|
23480
24232
|
if ((isO1Model || func.length === 0) && options.responseFormat?.type === "json_schema") await callback([], {
|
|
23481
|
-
|
|
24233
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23482
24234
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23483
24235
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23484
24236
|
cacheStats,
|
|
@@ -23516,6 +24268,10 @@ var OpenAIBackend = class {
|
|
|
23516
24268
|
});
|
|
23517
24269
|
const entry = toolsUsed.find((t) => t.name === name && t.id === id);
|
|
23518
24270
|
if (entry) entry.arguments = "{}";
|
|
24271
|
+
recordToolResult(toolsUsed, {
|
|
24272
|
+
id,
|
|
24273
|
+
name
|
|
24274
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
23519
24275
|
}
|
|
23520
24276
|
}
|
|
23521
24277
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -23550,11 +24306,16 @@ var OpenAIBackend = class {
|
|
|
23550
24306
|
for (const outcome of outcomes) {
|
|
23551
24307
|
if (!outcome.ok) {
|
|
23552
24308
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
24309
|
+
const errorMsg = `Error processing ${outcome.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
|
|
24310
|
+
recordToolResult(toolsUsed, {
|
|
24311
|
+
id: outcome.id,
|
|
24312
|
+
name: outcome.name
|
|
24313
|
+
}, errorMsg, false);
|
|
23553
24314
|
this.pushToolMessages(messages, {
|
|
23554
24315
|
id: outcome.id,
|
|
23555
24316
|
name: outcome.name,
|
|
23556
24317
|
parameters: outcome.parameters
|
|
23557
|
-
},
|
|
24318
|
+
}, errorMsg);
|
|
23558
24319
|
continue;
|
|
23559
24320
|
}
|
|
23560
24321
|
const resultStr = outcome.result.toString();
|
|
@@ -23564,13 +24325,17 @@ var OpenAIBackend = class {
|
|
|
23564
24325
|
thisToolHadArtifact = true;
|
|
23565
24326
|
anyArtifactWasStreamed = true;
|
|
23566
24327
|
await callback(results, {
|
|
23567
|
-
|
|
24328
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23568
24329
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23569
24330
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23570
24331
|
cacheStats
|
|
23571
24332
|
});
|
|
23572
24333
|
});
|
|
23573
24334
|
const sanitizedResult = thisToolHadArtifact ? resultStr.replace(/<artifact(?:\s[^>]*)?>[\s\S]*?<\/artifact>/gi, "[Artifact rendered and delivered to user]") : resultStr;
|
|
24335
|
+
recordToolResult(toolsUsed, {
|
|
24336
|
+
id: outcome.id,
|
|
24337
|
+
name: outcome.name
|
|
24338
|
+
}, sanitizedResult, true);
|
|
23574
24339
|
this.pushToolMessages(messages, {
|
|
23575
24340
|
id: outcome.id,
|
|
23576
24341
|
name: outcome.name,
|
|
@@ -23591,7 +24356,8 @@ var OpenAIBackend = class {
|
|
|
23591
24356
|
...options._internal,
|
|
23592
24357
|
toolCallCount: toolCallCount + 1,
|
|
23593
24358
|
accumInputTokens: accumInputTokens + inputTokens,
|
|
23594
|
-
accumOutputTokens: accumOutputTokens + outputTokens
|
|
24359
|
+
accumOutputTokens: accumOutputTokens + outputTokens,
|
|
24360
|
+
accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
|
|
23595
24361
|
}
|
|
23596
24362
|
}, async (results, meta) => {
|
|
23597
24363
|
for (const r of results) if (r != null) recursiveBuffer += r;
|
|
@@ -23607,13 +24373,14 @@ var OpenAIBackend = class {
|
|
|
23607
24373
|
...options._internal,
|
|
23608
24374
|
toolCallCount: toolCallCount + 1,
|
|
23609
24375
|
accumInputTokens: accumInputTokens + inputTokens,
|
|
23610
|
-
accumOutputTokens: accumOutputTokens + outputTokens
|
|
24376
|
+
accumOutputTokens: accumOutputTokens + outputTokens,
|
|
24377
|
+
accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
|
|
23611
24378
|
}
|
|
23612
24379
|
}, callback, toolsUsed);
|
|
23613
24380
|
} else {
|
|
23614
24381
|
this.logger.debug(`[Tool Execution] executeTools=false, passing tool calls to callback`);
|
|
23615
24382
|
await callback([null], {
|
|
23616
|
-
|
|
24383
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23617
24384
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23618
24385
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23619
24386
|
cacheStats
|
|
@@ -23755,6 +24522,7 @@ var OpenAIBackend = class {
|
|
|
23755
24522
|
const toolCallCount = options._internal?.toolCallCount ?? 0;
|
|
23756
24523
|
const accumInputTokens = options._internal?.accumInputTokens ?? 0;
|
|
23757
24524
|
const accumOutputTokens = options._internal?.accumOutputTokens ?? 0;
|
|
24525
|
+
const accumCacheReadTokens = options._internal?.accumCacheReadTokens ?? 0;
|
|
23758
24526
|
const chatMessages = this.formatMessages(messages, false, model, options);
|
|
23759
24527
|
const input = this.toResponsesInput(chatMessages);
|
|
23760
24528
|
const reasoningEffort = this.resolveReasoningEffort(model, options);
|
|
@@ -23781,8 +24549,9 @@ var OpenAIBackend = class {
|
|
|
23781
24549
|
let finalResponse;
|
|
23782
24550
|
let inputTokens = 0;
|
|
23783
24551
|
let outputTokens = 0;
|
|
24552
|
+
let cachedTokensFromStream = 0;
|
|
23784
24553
|
for await (const event of stream) if (event.type === "response.output_text.delta") await callback([event.delta], {
|
|
23785
|
-
|
|
24554
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23786
24555
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23787
24556
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
23788
24557
|
});
|
|
@@ -23792,11 +24561,12 @@ var OpenAIBackend = class {
|
|
|
23792
24561
|
if (!finalResponse) throw new Error(`OpenAI Responses stream for ${model} ended without a terminal response event`);
|
|
23793
24562
|
inputTokens = finalResponse.usage?.input_tokens ?? 0;
|
|
23794
24563
|
outputTokens = finalResponse.usage?.output_tokens ?? 0;
|
|
24564
|
+
cachedTokensFromStream = cachedTokensFromUsage(finalResponse.usage);
|
|
23795
24565
|
const functionCalls = finalResponse.output.filter((item) => item.type === "function_call");
|
|
23796
24566
|
if (functionCalls.length === 0) {
|
|
23797
24567
|
const stopReason = normalizeOpenAIResponsesStopReason(finalResponse.incomplete_details?.reason);
|
|
23798
24568
|
await callback([], {
|
|
23799
|
-
|
|
24569
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23800
24570
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23801
24571
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0,
|
|
23802
24572
|
...stopReason ? { stopReason } : {}
|
|
@@ -23810,7 +24580,7 @@ var OpenAIBackend = class {
|
|
|
23810
24580
|
});
|
|
23811
24581
|
if (options.executeTools === false) {
|
|
23812
24582
|
await callback([null], {
|
|
23813
|
-
|
|
24583
|
+
...splitCacheInclusiveInput(accumInputTokens + inputTokens, accumCacheReadTokens + cachedTokensFromStream),
|
|
23814
24584
|
outputTokens: accumOutputTokens + outputTokens,
|
|
23815
24585
|
toolsUsed: toolsUsed.length > 0 ? toolsUsed : void 0
|
|
23816
24586
|
});
|
|
@@ -23831,6 +24601,10 @@ var OpenAIBackend = class {
|
|
|
23831
24601
|
});
|
|
23832
24602
|
} catch {
|
|
23833
24603
|
this.logger.warn(`JSON parse error for ${fc.name} arguments (Responses path)`);
|
|
24604
|
+
recordToolResult(toolsUsed, {
|
|
24605
|
+
id: fc.call_id,
|
|
24606
|
+
name: fc.name
|
|
24607
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
23834
24608
|
}
|
|
23835
24609
|
}
|
|
23836
24610
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -23854,14 +24628,24 @@ var OpenAIBackend = class {
|
|
|
23854
24628
|
for (let i = 0; i < batchOutcomes.length; i++) {
|
|
23855
24629
|
const outcome = batchOutcomes[i];
|
|
23856
24630
|
const r = resolved[i];
|
|
23857
|
-
if (outcome.ok)
|
|
23858
|
-
|
|
23859
|
-
|
|
23860
|
-
|
|
23861
|
-
|
|
23862
|
-
|
|
24631
|
+
if (outcome.ok) {
|
|
24632
|
+
const resultStr = outcome.result.result.toString();
|
|
24633
|
+
recordToolResult(toolsUsed, {
|
|
24634
|
+
id: r.callId,
|
|
24635
|
+
name: r.name
|
|
24636
|
+
}, resultStr, true);
|
|
24637
|
+
this.pushToolMessages(messages, {
|
|
24638
|
+
id: r.callId,
|
|
24639
|
+
name: r.name,
|
|
24640
|
+
parameters: r.args
|
|
24641
|
+
}, resultStr);
|
|
24642
|
+
} else {
|
|
23863
24643
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
23864
24644
|
const errorMsg = `Error processing ${r.name} tool: ${outcome.error instanceof Error ? outcome.error.message : "Unknown error"}`;
|
|
24645
|
+
recordToolResult(toolsUsed, {
|
|
24646
|
+
id: r.callId,
|
|
24647
|
+
name: r.name
|
|
24648
|
+
}, errorMsg, false);
|
|
23865
24649
|
this.pushToolMessages(messages, {
|
|
23866
24650
|
id: r.callId,
|
|
23867
24651
|
name: r.name,
|
|
@@ -23878,7 +24662,8 @@ var OpenAIBackend = class {
|
|
|
23878
24662
|
...options._internal,
|
|
23879
24663
|
toolCallCount: toolCallCount + 1,
|
|
23880
24664
|
accumInputTokens: accumInputTokens + inputTokens,
|
|
23881
|
-
accumOutputTokens: accumOutputTokens + outputTokens
|
|
24665
|
+
accumOutputTokens: accumOutputTokens + outputTokens,
|
|
24666
|
+
accumCacheReadTokens: accumCacheReadTokens + cachedTokensFromStream
|
|
23882
24667
|
}
|
|
23883
24668
|
}, callback, toolsUsed);
|
|
23884
24669
|
}
|
|
@@ -24236,6 +25021,10 @@ var XAIBackend = class {
|
|
|
24236
25021
|
this.logger.warn(`JSON parse error for ${toolCall.function.name} arguments`);
|
|
24237
25022
|
const entry = toolsUsed.find((t) => t.name === toolCall.function.name && t.id === toolCall.id);
|
|
24238
25023
|
if (entry) entry.arguments = "{}";
|
|
25024
|
+
recordToolResult(toolsUsed, {
|
|
25025
|
+
id: toolCall.id,
|
|
25026
|
+
name: toolCall.function.name
|
|
25027
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
24239
25028
|
}
|
|
24240
25029
|
}
|
|
24241
25030
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -24263,18 +25052,30 @@ var XAIBackend = class {
|
|
|
24263
25052
|
parameters: resolvedTools[i].parameters,
|
|
24264
25053
|
error: outcome.error
|
|
24265
25054
|
});
|
|
24266
|
-
for (const outcome of outcomes) if (outcome.ok)
|
|
24267
|
-
|
|
24268
|
-
|
|
24269
|
-
|
|
24270
|
-
|
|
24271
|
-
|
|
25055
|
+
for (const outcome of outcomes) if (outcome.ok) {
|
|
25056
|
+
const resultStr = outcome.result.toString();
|
|
25057
|
+
recordToolResult(toolsUsed, {
|
|
25058
|
+
id: outcome.id,
|
|
25059
|
+
name: outcome.name
|
|
25060
|
+
}, resultStr, true);
|
|
25061
|
+
this.pushToolMessages(messages, {
|
|
25062
|
+
id: outcome.id,
|
|
25063
|
+
name: outcome.name,
|
|
25064
|
+
parameters: outcome.parameters
|
|
25065
|
+
}, resultStr);
|
|
25066
|
+
} else {
|
|
24272
25067
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
25068
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
25069
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
25070
|
+
recordToolResult(toolsUsed, {
|
|
25071
|
+
id: outcome.id,
|
|
25072
|
+
name: outcome.name
|
|
25073
|
+
}, observation, false);
|
|
24273
25074
|
this.pushToolMessages(messages, {
|
|
24274
25075
|
id: outcome.id,
|
|
24275
25076
|
name: outcome.name,
|
|
24276
25077
|
parameters: outcome.parameters
|
|
24277
|
-
},
|
|
25078
|
+
}, observation);
|
|
24278
25079
|
}
|
|
24279
25080
|
await this.complete(model, messages, {
|
|
24280
25081
|
...options,
|
|
@@ -24404,6 +25205,10 @@ var XAIBackend = class {
|
|
|
24404
25205
|
this.logger.warn(`JSON parse error for ${name} arguments (streaming)`);
|
|
24405
25206
|
const entry = toolsUsed.find((t) => t.name === name && t.id === id);
|
|
24406
25207
|
if (entry) entry.arguments = "{}";
|
|
25208
|
+
recordToolResult(toolsUsed, {
|
|
25209
|
+
id,
|
|
25210
|
+
name
|
|
25211
|
+
}, "Error: Tool arguments were malformed and could not be parsed.", false);
|
|
24407
25212
|
}
|
|
24408
25213
|
}
|
|
24409
25214
|
const parallelEnabled = options.parallelToolExecution !== false;
|
|
@@ -24431,18 +25236,30 @@ var XAIBackend = class {
|
|
|
24431
25236
|
parameters: resolvedTools[i].parameters,
|
|
24432
25237
|
error: outcome.error
|
|
24433
25238
|
});
|
|
24434
|
-
for (const outcome of outcomes) if (outcome.ok)
|
|
24435
|
-
|
|
24436
|
-
|
|
24437
|
-
|
|
24438
|
-
|
|
24439
|
-
|
|
25239
|
+
for (const outcome of outcomes) if (outcome.ok) {
|
|
25240
|
+
const resultStr = outcome.result.toString();
|
|
25241
|
+
recordToolResult(toolsUsed, {
|
|
25242
|
+
id: outcome.id,
|
|
25243
|
+
name: outcome.name
|
|
25244
|
+
}, resultStr, true);
|
|
25245
|
+
this.pushToolMessages(messages, {
|
|
25246
|
+
id: outcome.id,
|
|
25247
|
+
name: outcome.name,
|
|
25248
|
+
parameters: outcome.parameters
|
|
25249
|
+
}, resultStr);
|
|
25250
|
+
} else {
|
|
24440
25251
|
if (outcome.error instanceof PermissionDeniedError) throw outcome.error;
|
|
25252
|
+
const errorMessage = outcome.error instanceof Error ? outcome.error.message : "Unknown error";
|
|
25253
|
+
const observation = `Error processing ${outcome.name} tool: ${errorMessage}`;
|
|
25254
|
+
recordToolResult(toolsUsed, {
|
|
25255
|
+
id: outcome.id,
|
|
25256
|
+
name: outcome.name
|
|
25257
|
+
}, observation, false);
|
|
24441
25258
|
this.pushToolMessages(messages, {
|
|
24442
25259
|
id: outcome.id,
|
|
24443
25260
|
name: outcome.name,
|
|
24444
25261
|
parameters: outcome.parameters
|
|
24445
|
-
},
|
|
25262
|
+
}, observation);
|
|
24446
25263
|
}
|
|
24447
25264
|
await this.complete(model, messages, {
|
|
24448
25265
|
...options,
|
|
@@ -25603,6 +26420,10 @@ function checkStaleModelReferences(input) {
|
|
|
25603
26420
|
for (const referencedId of chain) check("fallback-chain", chainKey, referencedId);
|
|
25604
26421
|
}
|
|
25605
26422
|
for (const referencedId of input.defaultChain ?? []) check("fallback-default", "default", referencedId);
|
|
26423
|
+
for (const [agentName, decl] of Object.entries(input.agentModels ?? {})) {
|
|
26424
|
+
if (decl.model) check("agent-model", agentName, decl.model);
|
|
26425
|
+
for (const referencedId of decl.fallbackModels ?? []) check("agent-fallback", agentName, referencedId);
|
|
26426
|
+
}
|
|
25606
26427
|
for (const [source, target] of Object.entries(DEPRECATED_MODEL_MAP)) check("deprecated-model-map", source, target);
|
|
25607
26428
|
for (const [source, target] of replacedByOverlayEntries()) check("replaced-by-overlay", source, target);
|
|
25608
26429
|
return found.sort((a, b) => a.surface.localeCompare(b.surface) || a.key.localeCompare(b.key) || a.referencedId.localeCompare(b.referencedId));
|
|
@@ -26067,13 +26888,19 @@ const toolUseSchema = z$1.object({
|
|
|
26067
26888
|
* Discriminated union of streaming events. `thinking` blocks are opaque
|
|
26068
26889
|
* provider-shaped objects (Anthropic extended thinking) replayed verbatim into
|
|
26069
26890
|
* the next request, so they stay `unknown[]` rather than being modeled.
|
|
26891
|
+
*
|
|
26892
|
+
* `stopReason` must be declared to survive: this schema strips unknown keys, so a
|
|
26893
|
+
* field absent here is dropped at the boundary no matter what the server sends.
|
|
26894
|
+
* 'max_tokens' is the one value with user-visible consequences - it means the reply
|
|
26895
|
+
* was cut off rather than finished.
|
|
26070
26896
|
*/
|
|
26071
26897
|
const streamEventSchema = z$1.discriminatedUnion("type", [
|
|
26072
26898
|
z$1.object({
|
|
26073
26899
|
type: z$1.literal("content"),
|
|
26074
26900
|
text: z$1.string().optional(),
|
|
26075
26901
|
usage: usageSchema.optional(),
|
|
26076
|
-
credits: creditsSchema.optional()
|
|
26902
|
+
credits: creditsSchema.optional(),
|
|
26903
|
+
stopReason: z$1.string().optional()
|
|
26077
26904
|
}),
|
|
26078
26905
|
z$1.object({
|
|
26079
26906
|
type: z$1.literal("tool_use"),
|
|
@@ -26081,7 +26908,8 @@ const streamEventSchema = z$1.discriminatedUnion("type", [
|
|
|
26081
26908
|
tools: z$1.array(toolUseSchema).optional(),
|
|
26082
26909
|
thinking: z$1.array(z$1.unknown()).optional(),
|
|
26083
26910
|
usage: usageSchema.optional(),
|
|
26084
|
-
credits: creditsSchema.optional()
|
|
26911
|
+
credits: creditsSchema.optional(),
|
|
26912
|
+
stopReason: z$1.string().optional()
|
|
26085
26913
|
}),
|
|
26086
26914
|
z$1.object({
|
|
26087
26915
|
type: z$1.literal("error"),
|
|
@@ -26150,12 +26978,14 @@ var StreamAccumulator = class {
|
|
|
26150
26978
|
case "content":
|
|
26151
26979
|
this.accumulatedText += event.text ?? "";
|
|
26152
26980
|
if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
|
|
26981
|
+
if (event.stopReason) this.stopReason = event.stopReason;
|
|
26153
26982
|
break;
|
|
26154
26983
|
case "tool_use":
|
|
26155
26984
|
if (event.text) this.accumulatedText += event.text;
|
|
26156
26985
|
if (event.tools && event.tools.length > 0) this.toolsUsed = event.tools;
|
|
26157
26986
|
if (event.thinking && event.thinking.length > 0) this.thinkingBlocks = event.thinking;
|
|
26158
26987
|
if (event.usage || event.credits) this.lastUsageInfo = extractUsageInfo(event);
|
|
26988
|
+
if (event.stopReason) this.stopReason = event.stopReason;
|
|
26159
26989
|
}
|
|
26160
26990
|
}
|
|
26161
26991
|
/** True when neither text nor tools have been accumulated (stream produced nothing useful). */
|
|
@@ -26182,10 +27012,14 @@ var StreamAccumulator = class {
|
|
|
26182
27012
|
const info = {
|
|
26183
27013
|
toolsUsed: this.toolsUsed,
|
|
26184
27014
|
thinking: this.thinkingBlocks.length > 0 ? this.thinkingBlocks : void 0,
|
|
26185
|
-
...this.lastUsageInfo
|
|
27015
|
+
...this.lastUsageInfo,
|
|
27016
|
+
stopReason: this.stopReason
|
|
26186
27017
|
};
|
|
26187
27018
|
await callback([cleanedText], info);
|
|
26188
|
-
} else if (cleanedText) await callback([cleanedText],
|
|
27019
|
+
} else if (cleanedText) await callback([cleanedText], {
|
|
27020
|
+
...this.lastUsageInfo,
|
|
27021
|
+
stopReason: this.stopReason
|
|
27022
|
+
});
|
|
26189
27023
|
}
|
|
26190
27024
|
};
|
|
26191
27025
|
//#endregion
|
|
@@ -27296,7 +28130,7 @@ var AgentStore = class {
|
|
|
27296
28130
|
async loadAgentsFromDirectory(directory, source) {
|
|
27297
28131
|
try {
|
|
27298
28132
|
if (!(await fs$1.stat(directory)).isDirectory()) return;
|
|
27299
|
-
const files = await
|
|
28133
|
+
const files = await findMarkdownFiles(directory);
|
|
27300
28134
|
for (const filePath of files) try {
|
|
27301
28135
|
const agent = await this.parseAgentFile(filePath, source);
|
|
27302
28136
|
this.agents.set(agent.name, agent);
|
|
@@ -27308,25 +28142,6 @@ var AgentStore = class {
|
|
|
27308
28142
|
}
|
|
27309
28143
|
}
|
|
27310
28144
|
/**
|
|
27311
|
-
* Recursively find all .md files in directory
|
|
27312
|
-
*/
|
|
27313
|
-
async findAgentFiles(directory) {
|
|
27314
|
-
const files = [];
|
|
27315
|
-
try {
|
|
27316
|
-
const entries = await fs$1.readdir(directory, { withFileTypes: true });
|
|
27317
|
-
for (const entry of entries) {
|
|
27318
|
-
const fullPath = path.join(directory, entry.name);
|
|
27319
|
-
if (entry.isDirectory()) {
|
|
27320
|
-
const subFiles = await this.findAgentFiles(fullPath);
|
|
27321
|
-
files.push(...subFiles);
|
|
27322
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) files.push(fullPath);
|
|
27323
|
-
}
|
|
27324
|
-
} catch (error) {
|
|
27325
|
-
console.warn(`Error reading directory ${directory}:`, error instanceof Error ? error.message : String(error));
|
|
27326
|
-
}
|
|
27327
|
-
return files;
|
|
27328
|
-
}
|
|
27329
|
-
/**
|
|
27330
28145
|
* Parse a single agent markdown file
|
|
27331
28146
|
*/
|
|
27332
28147
|
async parseAgentFile(filePath, source) {
|
|
@@ -27643,7 +28458,8 @@ function buildFilenameMarkerRegex(markers) {
|
|
|
27643
28458
|
* of the best-effort DB pre-filter. Fail-closed by design.
|
|
27644
28459
|
*/
|
|
27645
28460
|
function isRetrievalExcluded(file, opts) {
|
|
27646
|
-
|
|
28461
|
+
const stalledByConvergence = isConvergencePausedNote(file.notes) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
|
|
28462
|
+
if (opts.vectorizedOnly && !file.vectorized && !stalledByConvergence) return true;
|
|
27647
28463
|
const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
|
|
27648
28464
|
return !!re && re.test((file.fileName ?? "").toLowerCase());
|
|
27649
28465
|
}
|
|
@@ -28153,6 +28969,41 @@ function attachedContentExtractionBudget(maxSafeInputTokens, systemPromptReserve
|
|
|
28153
28969
|
const boundedReserve = Math.min(systemPromptReserve, Math.floor(maxSafeInputTokens * EXTRACTION_SYSTEM_RESERVE_MAX_SHARE));
|
|
28154
28970
|
return Math.max(0, Math.max(Math.floor(maxSafeInputTokens * MIN_ATTACHED_CONTENT_EXTRACTION_SHARE), Math.floor((maxSafeInputTokens - boundedReserve) * ATTACHED_CONTENT_EXTRACTION_SHARE)));
|
|
28155
28971
|
}
|
|
28972
|
+
/**
|
|
28973
|
+
* Output budget used when a caller supplies no max_tokens. Within supported output limits for
|
|
28974
|
+
* every configured non-reasoning model; models that reason inside the output budget default to
|
|
28975
|
+
* ADAPTIVE_THINKING_MAX_TOKENS_FLOOR instead, since their reasoning would otherwise consume this
|
|
28976
|
+
* whole budget (see resolveOutputMaxTokens/reasonsWithinOutputBudget for which those are).
|
|
28977
|
+
* Distinct from the catalog's DEFAULT_MAX_OUTPUT_TOKENS, which fills in a model's *capability*
|
|
28978
|
+
* when its record omits one.
|
|
28979
|
+
*/
|
|
28980
|
+
const DEFAULT_OUTPUT_MAX_TOKENS = 4096;
|
|
28981
|
+
/**
|
|
28982
|
+
* Verbatim-history token budget shared by every caller that sizes one: how much of the model's
|
|
28983
|
+
* safe input window recent conversation turns may claim before older ones fall out to
|
|
28984
|
+
* contextSummary. Pulled into one place because ChatCompletionProcess.ts and
|
|
28985
|
+
* ChatCompletionFeatures.ts (QuestMaster) both need it and must not drift apart - the first
|
|
28986
|
+
* version of the QuestMaster call site hand-copied the formula and got the window wrong for
|
|
28987
|
+
* every model below the unknown-model floor.
|
|
28988
|
+
*
|
|
28989
|
+
* Always returns at least 1, even on a window so small or a message so long that the raw
|
|
28990
|
+
* subtraction goes to zero or negative: `Math.max(0, budget) && budget > 0` downstream treats a
|
|
28991
|
+
* falsy budget as "no budget given" and skips trimming entirely, which is the exact unbounded
|
|
28992
|
+
* history this function exists to prevent. A budget of 1 still trims to the single most recent
|
|
28993
|
+
* turn instead of disabling the trim.
|
|
28994
|
+
*/
|
|
28995
|
+
function computeVerbatimTokenBudget(modelInfo, requestedMaxTokens, opts) {
|
|
28996
|
+
const modelMaxOutput = modelInfo.max_tokens ?? 16384;
|
|
28997
|
+
const safeMaxTokens = resolveOutputMaxTokens({
|
|
28998
|
+
requested: requestedMaxTokens,
|
|
28999
|
+
fallback: DEFAULT_OUTPUT_MAX_TOKENS,
|
|
29000
|
+
modelInfo,
|
|
29001
|
+
modelMaxOutputTokens: modelMaxOutput
|
|
29002
|
+
});
|
|
29003
|
+
const safeInputTokens = Math.max(0, safeInputWindow(modelInfo, safeMaxTokens));
|
|
29004
|
+
const availableForVerbatim = Math.max(0, safeInputTokens - opts.nonHistoryOverheadTokens);
|
|
29005
|
+
return Math.max(1, Math.floor(availableForVerbatim * opts.verbatimWindowFraction));
|
|
29006
|
+
}
|
|
28156
29007
|
/** The buffer buildAndSortMessages holds back before dividing the input window. */
|
|
28157
29008
|
function assemblyTokenBuffer(maxInputTokens) {
|
|
28158
29009
|
return Math.max(MIN_TOKEN_BUFFER, Math.floor(maxInputTokens * TOKEN_BUFFER_PERCENTAGE));
|
|
@@ -28418,7 +29269,125 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
28418
29269
|
this.logger.info("🛑 Admin settings cache shutdown complete");
|
|
28419
29270
|
}
|
|
28420
29271
|
};
|
|
29272
|
+
/** Address of one cached override, shared by the cache and its callers so lookups are consistent. */
|
|
29273
|
+
function scopedOverrideKey(scopeLevel, scopeId, settingName) {
|
|
29274
|
+
return JSON.stringify([
|
|
29275
|
+
scopeLevel,
|
|
29276
|
+
scopeId,
|
|
29277
|
+
settingName
|
|
29278
|
+
]);
|
|
29279
|
+
}
|
|
29280
|
+
function scopeKey(scopeLevel, scopeId) {
|
|
29281
|
+
return JSON.stringify([scopeLevel, scopeId]);
|
|
29282
|
+
}
|
|
29283
|
+
/**
|
|
29284
|
+
* In-memory cache for scoped setting OVERRIDES, sibling to `AdminSettingsCache` (which caches the
|
|
29285
|
+
* flat platform table). Unlike the platform cache it cannot hold one "all settings" blob - overrides
|
|
29286
|
+
* are per (rung, setting) and can be many (a value per lake), so each address is cached individually
|
|
29287
|
+
* and negatively (a confirmed absence is cached too, so an un-overridden setting does not re-query
|
|
29288
|
+
* every resolve).
|
|
29289
|
+
*
|
|
29290
|
+
* Entries are nested one level per rung address so a whole rung invalidates in one delete (the shape
|
|
29291
|
+
* a future scoped-override writer needs). The cache key carries the scope rung, which is the whole
|
|
29292
|
+
* reason this is separate from `AdminSettingsCache`: that one keys by bare `settingName`, so scoped
|
|
29293
|
+
* values would collide there. See #1660.
|
|
29294
|
+
*/
|
|
29295
|
+
var ScopedSettingsCache = class ScopedSettingsCache {
|
|
29296
|
+
cache = /* @__PURE__ */ new Map();
|
|
29297
|
+
logger;
|
|
29298
|
+
maxScopes = 5e3;
|
|
29299
|
+
static DEFAULT_TTL = 3e5;
|
|
29300
|
+
static DEVELOPMENT_TTL = 3e4;
|
|
29301
|
+
constructor(logger) {
|
|
29302
|
+
this.logger = logger;
|
|
29303
|
+
}
|
|
29304
|
+
getTTL() {
|
|
29305
|
+
return process.env.NODE_ENV === "development" ? ScopedSettingsCache.DEVELOPMENT_TTL : ScopedSettingsCache.DEFAULT_TTL;
|
|
29306
|
+
}
|
|
29307
|
+
isValid(entry) {
|
|
29308
|
+
return Date.now() - entry.timestamp < entry.ttl;
|
|
29309
|
+
}
|
|
29310
|
+
/**
|
|
29311
|
+
* Resolve override values for the given rungs and setting names, reading through the cache. Returns
|
|
29312
|
+
* a map keyed by `scopedOverrideKey(level,id,name)` -> value|null for EVERY (scope, name) pair
|
|
29313
|
+
* requested, so the resolver can look up any rung deterministically. One DB query covers all misses.
|
|
29314
|
+
*/
|
|
29315
|
+
async getOverrides(scopes, settingNames, db) {
|
|
29316
|
+
const out = /* @__PURE__ */ new Map();
|
|
29317
|
+
if (scopes.length === 0 || settingNames.length === 0) return out;
|
|
29318
|
+
const missingScopeRefs = /* @__PURE__ */ new Map();
|
|
29319
|
+
const missingNames = /* @__PURE__ */ new Set();
|
|
29320
|
+
for (const scope of scopes) {
|
|
29321
|
+
const inner = this.cache.get(scopeKey(scope.scopeLevel, scope.scopeId));
|
|
29322
|
+
for (const name of settingNames) {
|
|
29323
|
+
const outKey = scopedOverrideKey(scope.scopeLevel, scope.scopeId, name);
|
|
29324
|
+
const cached = inner?.get(name);
|
|
29325
|
+
if (cached && this.isValid(cached)) out.set(outKey, cached.value);
|
|
29326
|
+
else {
|
|
29327
|
+
if (cached) inner.delete(name);
|
|
29328
|
+
out.set(outKey, null);
|
|
29329
|
+
missingScopeRefs.set(scopeKey(scope.scopeLevel, scope.scopeId), scope);
|
|
29330
|
+
missingNames.add(name);
|
|
29331
|
+
}
|
|
29332
|
+
}
|
|
29333
|
+
}
|
|
29334
|
+
if (missingScopeRefs.size === 0) return out;
|
|
29335
|
+
const rows = await db.scopedSettings.findOverrides(Array.from(missingScopeRefs.values()), Array.from(missingNames));
|
|
29336
|
+
const ttl = this.getTTL();
|
|
29337
|
+
const now = Date.now();
|
|
29338
|
+
const found = /* @__PURE__ */ new Set();
|
|
29339
|
+
for (const row of rows) {
|
|
29340
|
+
out.set(scopedOverrideKey(row.scopeLevel, row.scopeId, row.settingName), row.settingValue);
|
|
29341
|
+
this.put(row.scopeLevel, row.scopeId, row.settingName, row.settingValue, now, ttl);
|
|
29342
|
+
found.add(scopedOverrideKey(row.scopeLevel, row.scopeId, row.settingName));
|
|
29343
|
+
}
|
|
29344
|
+
for (const scope of missingScopeRefs.values()) for (const name of missingNames) if (!found.has(scopedOverrideKey(scope.scopeLevel, scope.scopeId, name))) this.put(scope.scopeLevel, scope.scopeId, name, null, now, ttl);
|
|
29345
|
+
if (this.cache.size > this.maxScopes) this.evictOldestScopes();
|
|
29346
|
+
return out;
|
|
29347
|
+
}
|
|
29348
|
+
put(scopeLevel, scopeId, name, value, now, ttl) {
|
|
29349
|
+
const sk = scopeKey(scopeLevel, scopeId);
|
|
29350
|
+
let inner = this.cache.get(sk);
|
|
29351
|
+
if (!inner) {
|
|
29352
|
+
inner = /* @__PURE__ */ new Map();
|
|
29353
|
+
this.cache.set(sk, inner);
|
|
29354
|
+
}
|
|
29355
|
+
inner.set(name, {
|
|
29356
|
+
value,
|
|
29357
|
+
timestamp: now,
|
|
29358
|
+
ttl
|
|
29359
|
+
});
|
|
29360
|
+
}
|
|
29361
|
+
evictOldestScopes() {
|
|
29362
|
+
const freshness = (inner) => Math.max(...Array.from(inner.values()).map((e) => e.timestamp));
|
|
29363
|
+
const scopesByAge = Array.from(this.cache.entries()).sort((a, b) => freshness(a[1]) - freshness(b[1]));
|
|
29364
|
+
const toRemove = this.cache.size - this.maxScopes;
|
|
29365
|
+
for (let i = 0; i < toRemove; i++) this.cache.delete(scopesByAge[i][0]);
|
|
29366
|
+
this.logger.warn(`ScopedSettingsCache evicted ${toRemove} oldest rungs (size limit)`);
|
|
29367
|
+
}
|
|
29368
|
+
/** Invalidate every cached value for one rung address (call this from a future scoped-override writer). */
|
|
29369
|
+
invalidateScope(scopeLevel, scopeId) {
|
|
29370
|
+
this.cache.delete(scopeKey(scopeLevel, scopeId));
|
|
29371
|
+
}
|
|
29372
|
+
invalidateAll() {
|
|
29373
|
+
this.cache.clear();
|
|
29374
|
+
}
|
|
29375
|
+
/** Snapshot for monitoring, parity with AdminSettingsCache.getStats(). */
|
|
29376
|
+
getStats() {
|
|
29377
|
+
let entries = 0;
|
|
29378
|
+
for (const inner of this.cache.values()) entries += inner.size;
|
|
29379
|
+
return {
|
|
29380
|
+
scopes: this.cache.size,
|
|
29381
|
+
entries
|
|
29382
|
+
};
|
|
29383
|
+
}
|
|
29384
|
+
/** Graceful shutdown - no timers to stop (unlike AdminSettingsCache), just drop the map. */
|
|
29385
|
+
shutdown() {
|
|
29386
|
+
this.cache.clear();
|
|
29387
|
+
}
|
|
29388
|
+
};
|
|
28421
29389
|
let globalSettingsCache = null;
|
|
29390
|
+
let globalScopedSettingsCache = null;
|
|
28422
29391
|
/**
|
|
28423
29392
|
* Get or create the global settings cache instance
|
|
28424
29393
|
*/
|
|
@@ -28426,6 +29395,32 @@ function getSettingsCache(logger) {
|
|
|
28426
29395
|
if (!globalSettingsCache) globalSettingsCache = new AdminSettingsCache(logger || new Logger());
|
|
28427
29396
|
return globalSettingsCache;
|
|
28428
29397
|
}
|
|
29398
|
+
function getScopedSettingsCache(logger) {
|
|
29399
|
+
if (!globalScopedSettingsCache) globalScopedSettingsCache = new ScopedSettingsCache(logger || new Logger());
|
|
29400
|
+
return globalScopedSettingsCache;
|
|
29401
|
+
}
|
|
29402
|
+
/**
|
|
29403
|
+
* Read org/owner/lake OVERRIDE values for a set of rungs and setting names, through the scoped cache.
|
|
29404
|
+
* Returns a map keyed by `scopedOverrideKey(level,id,name)` -> value|null (see ScopedSettingsCache).
|
|
29405
|
+
* Platform values are NOT here - the resolver layers these over the platform read. Passing no rungs
|
|
29406
|
+
* (a platform-altitude resolve) returns an empty map and touches neither cache nor DB.
|
|
29407
|
+
*/
|
|
29408
|
+
async function getScopedOverrides(scopes, settingNames, db, options) {
|
|
29409
|
+
return getScopedSettingsCache(options?.logger).getOverrides(scopes, settingNames, db);
|
|
29410
|
+
}
|
|
29411
|
+
/**
|
|
29412
|
+
* Invalidate cached overrides for one rung address, or all of them. Call from a scoped-override writer.
|
|
29413
|
+
*
|
|
29414
|
+
* In-process only: this clears the calling process's map. With negative caching and the 5-minute TTL,
|
|
29415
|
+
* a newly-written override can stay invisible on other Lambda/container instances for up to one TTL.
|
|
29416
|
+
* That is acceptable while no writer ships (this PR), but a future write path needs a cross-instance
|
|
29417
|
+
* invalidation story (shared cache or short TTL) rather than relying on this alone.
|
|
29418
|
+
*/
|
|
29419
|
+
function invalidateScopedSettingsCache(scope) {
|
|
29420
|
+
if (!globalScopedSettingsCache) return;
|
|
29421
|
+
if (scope) globalScopedSettingsCache.invalidateScope(scope.scopeLevel, scope.scopeId);
|
|
29422
|
+
else globalScopedSettingsCache.invalidateAll();
|
|
29423
|
+
}
|
|
28429
29424
|
function getSettingsValue(key, settings, defaultValue) {
|
|
28430
29425
|
const settingConfig = settingsMap[key];
|
|
28431
29426
|
if (!settingConfig) {
|
|
@@ -28478,7 +29473,7 @@ async function getSettingsByNames(settingNames, db, options) {
|
|
|
28478
29473
|
const allSettings = await getSettingsCache(logger).getSettingsMap(db);
|
|
28479
29474
|
const result = {};
|
|
28480
29475
|
settingNames.forEach((name) => {
|
|
28481
|
-
result[name] = allSettings[name]
|
|
29476
|
+
result[name] = allSettings[name] ?? null;
|
|
28482
29477
|
});
|
|
28483
29478
|
return result;
|
|
28484
29479
|
}
|
|
@@ -28503,6 +29498,10 @@ function shutdownSettingsCache() {
|
|
|
28503
29498
|
globalSettingsCache.shutdown();
|
|
28504
29499
|
globalSettingsCache = null;
|
|
28505
29500
|
}
|
|
29501
|
+
if (globalScopedSettingsCache) {
|
|
29502
|
+
globalScopedSettingsCache.shutdown();
|
|
29503
|
+
globalScopedSettingsCache = null;
|
|
29504
|
+
}
|
|
28506
29505
|
}
|
|
28507
29506
|
/**
|
|
28508
29507
|
* Get cache statistics for monitoring
|
|
@@ -28860,6 +29859,12 @@ const EMBEDDING_TOKEN_LIMITS = {
|
|
|
28860
29859
|
*/
|
|
28861
29860
|
const KNOWLEDGE_FILE_TOKEN_ALLOCATION = .7;
|
|
28862
29861
|
/**
|
|
29862
|
+
* The two always-on blocks this function injects itself, downstream of the caller's own
|
|
29863
|
+
* systemPromptDetails assembly - so a caller reporting per-source token telemetry (see
|
|
29864
|
+
* ChatCompletionProcess) cannot see them without reading this list back off the return value.
|
|
29865
|
+
*/
|
|
29866
|
+
const BUILDER_INJECTED_BLOCK_IDS = ["formatPrompt", "imagePrompt"];
|
|
29867
|
+
/**
|
|
28863
29868
|
* Rounds the final safety pass may spend shrinking the payload. It has to re-measure between rounds
|
|
28864
29869
|
* (see the pass for why one shot overshoots), and each round costs two real tokenizer calls, so this
|
|
28865
29870
|
* bounds the work. Converges in one or two rounds in practice.
|
|
@@ -29005,12 +30010,12 @@ const estimateMessagesTokens = (messages) => messages.reduce((sum, message) => s
|
|
|
29005
30010
|
* staying synchronous (no N async tokenizer calls over a long history). Mirrors
|
|
29006
30011
|
* the fields the conversion below actually emits into the prompt.
|
|
29007
30012
|
*/
|
|
29008
|
-
function estimateQuestTokenLength(item) {
|
|
30013
|
+
function estimateQuestTokenLength(item, disableToolReplay = false) {
|
|
29009
30014
|
const parts = [item.prompt ?? ""];
|
|
29010
30015
|
if (item.structuredReplies?.length) parts.push(JSON.stringify(item.structuredReplies));
|
|
29011
30016
|
else if (item.replies?.length) parts.push(item.replies.join("\n"));
|
|
29012
30017
|
if (item.toolResults?.length) parts.push(JSON.stringify(item.toolResults));
|
|
29013
|
-
if (!item.structuredReplies?.length) {
|
|
30018
|
+
if (!item.structuredReplies?.length && !disableToolReplay) {
|
|
29014
30019
|
const toolCalls = replayableToolCalls(item.promptMeta?.functionCalls);
|
|
29015
30020
|
if (toolCalls.length) parts.push(JSON.stringify(toolCalls));
|
|
29016
30021
|
}
|
|
@@ -29090,20 +30095,21 @@ async function generateSafeEmbedding(embeddingService, text, logger) {
|
|
|
29090
30095
|
* history at all, and UNLIMITED_HISTORY_COUNT means no window (which still pages, since the
|
|
29091
30096
|
* fetch needs some limit).
|
|
29092
30097
|
*/
|
|
29093
|
-
async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget }) {
|
|
30098
|
+
async function fetchAndProcessPreviousMessages(session, historyCount = null, { db, verbatimTokenBudget, excludeCurrentPrompt = false, model }) {
|
|
29094
30099
|
if (!isUnlimitedHistory(historyCount) && historyCount !== null && historyCount <= 0) return [
|
|
29095
30100
|
[],
|
|
29096
30101
|
0,
|
|
29097
30102
|
{ cacheHit: false }
|
|
29098
30103
|
];
|
|
29099
30104
|
const limit = resolveHistoryFetchLimit(historyCount);
|
|
30105
|
+
const disableToolReplay = !!model && isGeminiModelId(model);
|
|
29100
30106
|
const startTime = Date.now();
|
|
29101
30107
|
const chatHistoryItems = await db.quests.getMostRecentChatHistory(session.id, limit + 1);
|
|
29102
30108
|
const fetchTime = Date.now() - startTime;
|
|
29103
30109
|
const cacheIndicator = fetchTime < 50 ? "CACHE_HIT" : "CACHE_MISS";
|
|
29104
30110
|
Logger.globalInstance.log(`⚡ Message History ${cacheIndicator}: ${fetchTime}ms for session ${session.id.slice(-8)} (${chatHistoryItems.length} items)`);
|
|
29105
30111
|
chatHistoryItems.reverse();
|
|
29106
|
-
if (chatHistoryItems.length > 1) chatHistoryItems.pop();
|
|
30112
|
+
if (chatHistoryItems.length > 1 || excludeCurrentPrompt) chatHistoryItems.pop();
|
|
29107
30113
|
if (session.contextSummaryUpToQuestId) {
|
|
29108
30114
|
const boundary = session.contextSummaryUpToQuestId;
|
|
29109
30115
|
const filtered = chatHistoryItems.filter((item) => item.id > boundary);
|
|
@@ -29114,7 +30120,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
|
|
|
29114
30120
|
let usedTokens = 0;
|
|
29115
30121
|
let keepFromIndex = 0;
|
|
29116
30122
|
for (let i = chatHistoryItems.length - 1; i >= 0; i--) {
|
|
29117
|
-
usedTokens += estimateQuestTokenLength(chatHistoryItems[i]);
|
|
30123
|
+
usedTokens += estimateQuestTokenLength(chatHistoryItems[i], disableToolReplay);
|
|
29118
30124
|
if (usedTokens > verbatimTokenBudget && i < chatHistoryItems.length - 1) {
|
|
29119
30125
|
keepFromIndex = i + 1;
|
|
29120
30126
|
break;
|
|
@@ -29146,7 +30152,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
|
|
|
29146
30152
|
is_error: tr.is_error
|
|
29147
30153
|
}))
|
|
29148
30154
|
});
|
|
29149
|
-
} else if (toolCalls.length > 0) {
|
|
30155
|
+
} else if (toolCalls.length > 0 && !disableToolReplay) {
|
|
29150
30156
|
const textReply = cur.replies?.find((reply) => !reply.trim().startsWith("<think>")) || "";
|
|
29151
30157
|
const assistantContent = [];
|
|
29152
30158
|
if (textReply) assistantContent.push({
|
|
@@ -29194,6 +30200,7 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
|
|
|
29194
30200
|
});
|
|
29195
30201
|
}
|
|
29196
30202
|
}
|
|
30203
|
+
const priorToolNames = chatHistoryItems.flatMap((item) => (item.promptMeta?.functionCalls ?? []).map((fc) => fc.name)).filter((name) => Boolean(name));
|
|
29197
30204
|
return [
|
|
29198
30205
|
convertedMessages,
|
|
29199
30206
|
chatHistoryItems.length,
|
|
@@ -29203,7 +30210,8 @@ async function fetchAndProcessPreviousMessages(session, historyCount = null, { d
|
|
|
29203
30210
|
itemCount: chatHistoryItems.length,
|
|
29204
30211
|
oldestIncludedQuestId,
|
|
29205
30212
|
excludedOlderQuestCount,
|
|
29206
|
-
recentGeneratedImages
|
|
30213
|
+
recentGeneratedImages,
|
|
30214
|
+
priorToolNames
|
|
29207
30215
|
}
|
|
29208
30216
|
];
|
|
29209
30217
|
}
|
|
@@ -29495,12 +30503,16 @@ const noopResize = async (imageBuffer) => imageBuffer;
|
|
|
29495
30503
|
async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, attachedContentTokenBudget, modelInfo, sendStatusUpdate, { logger, storage, db, resizeImageForModel = noopResize }, progressCallback) {
|
|
29496
30504
|
if (!fabFiles || fabFiles.length === 0) return {
|
|
29497
30505
|
userMessages: [],
|
|
29498
|
-
errorMessages: []
|
|
30506
|
+
errorMessages: [],
|
|
30507
|
+
deliveredFileIds: [],
|
|
30508
|
+
fullyDeliveredFileIds: []
|
|
29499
30509
|
};
|
|
29500
30510
|
const fileProcessingStartTime = Date.now();
|
|
29501
30511
|
let systemContent = "";
|
|
29502
30512
|
const userMessages = [];
|
|
29503
30513
|
const errorMessages = [];
|
|
30514
|
+
const deliveredFileIds = /* @__PURE__ */ new Set();
|
|
30515
|
+
const fullyDeliveredFileIds = /* @__PURE__ */ new Set();
|
|
29504
30516
|
const contextFiles = [];
|
|
29505
30517
|
const supportsVision = modelInfo?.supportsVision ?? false;
|
|
29506
30518
|
if (fabFiles.length > 0) sendStatusUpdate("Munching attached files...");
|
|
@@ -29524,6 +30536,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29524
30536
|
const maxTokens = Math.max(1, Math.floor(attachedContentTokenBudget / textFileCount));
|
|
29525
30537
|
const fileContentCache = /* @__PURE__ */ new Map();
|
|
29526
30538
|
const processFileInParallel = async (file) => {
|
|
30539
|
+
let delivered = false;
|
|
30540
|
+
let fullyDelivered = false;
|
|
29527
30541
|
try {
|
|
29528
30542
|
if (isAudioMimeType(file.mimeType)) {
|
|
29529
30543
|
logger.warn(`[processFabFilesServer] Skipping audio file ${file.fileName} — audio is not attachable to an LLM.`);
|
|
@@ -29552,6 +30566,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29552
30566
|
type: "text",
|
|
29553
30567
|
text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
|
|
29554
30568
|
});
|
|
30569
|
+
delivered = true;
|
|
30570
|
+
fullyDelivered = true;
|
|
29555
30571
|
break;
|
|
29556
30572
|
}
|
|
29557
30573
|
case ModelBackend.Anthropic:
|
|
@@ -29586,6 +30602,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29586
30602
|
type: "text",
|
|
29587
30603
|
text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
|
|
29588
30604
|
});
|
|
30605
|
+
delivered = true;
|
|
30606
|
+
fullyDelivered = true;
|
|
29589
30607
|
} else if (modelInfo.id.startsWith("moonshot")) {
|
|
29590
30608
|
const moonshotBuffer = await resizeImageForModel(await storage.download(file.filePath), void 0, logger);
|
|
29591
30609
|
const { mime: moonshotMimeType } = await getFileType(moonshotBuffer, file.fileName, file.mimeType);
|
|
@@ -29609,6 +30627,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29609
30627
|
type: "text",
|
|
29610
30628
|
text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}" — do not rename based on image content.`
|
|
29611
30629
|
});
|
|
30630
|
+
delivered = true;
|
|
30631
|
+
fullyDelivered = true;
|
|
29612
30632
|
} else logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
|
|
29613
30633
|
break;
|
|
29614
30634
|
case ModelBackend.Ollama: {
|
|
@@ -29634,6 +30654,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29634
30654
|
type: "text",
|
|
29635
30655
|
text: `Image URL: ${fileUrl}\nFile: "${file.fileName}" (fabFileId: ${file.id})\nWhen referencing this file, use the exact filename "${file.fileName}". Do not rename based on image content.`
|
|
29636
30656
|
});
|
|
30657
|
+
delivered = true;
|
|
30658
|
+
fullyDelivered = true;
|
|
29637
30659
|
break;
|
|
29638
30660
|
}
|
|
29639
30661
|
default: logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
|
|
@@ -29680,7 +30702,9 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29680
30702
|
}
|
|
29681
30703
|
if (truncatedResults.length > 0) {
|
|
29682
30704
|
deliveredViaCosine = true;
|
|
30705
|
+
delivered = true;
|
|
29683
30706
|
const deliveredEveryChunk = !scanTruncated && totalChunks === truncatedResults.length;
|
|
30707
|
+
fullyDelivered = deliveredEveryChunk && !anyChunkCut;
|
|
29684
30708
|
let notice = "";
|
|
29685
30709
|
if (!deliveredEveryChunk) notice = excerptNotice(file.fileName);
|
|
29686
30710
|
else if (anyChunkCut) notice = CONTENT_TRUNCATION_NOTICE;
|
|
@@ -29707,6 +30731,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29707
30731
|
const finalMaxFileSize = maxTokens > 0 ? maxSizeBasedonMaxTokens : MAX_FILE_SIZE;
|
|
29708
30732
|
logger.log(`[processFabFilesServer] Final max file size: ${finalMaxFileSize}`);
|
|
29709
30733
|
sendStatusUpdate("Adding file content to prompt...");
|
|
30734
|
+
fullyDelivered = fabContent.length <= finalMaxFileSize;
|
|
29710
30735
|
if (fabContent.length > finalMaxFileSize) {
|
|
29711
30736
|
await sendStatusUpdate("File is too large, truncating...");
|
|
29712
30737
|
const originalFileSize = fabContent.length;
|
|
@@ -29717,6 +30742,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29717
30742
|
content: errorMsg
|
|
29718
30743
|
});
|
|
29719
30744
|
} else errorMsg = null;
|
|
30745
|
+
delivered = true;
|
|
29720
30746
|
if (file.system) systemContent += fabContent;
|
|
29721
30747
|
else contextFiles.push({
|
|
29722
30748
|
fileName: file.fileName,
|
|
@@ -29746,6 +30772,8 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29746
30772
|
}
|
|
29747
30773
|
}
|
|
29748
30774
|
}
|
|
30775
|
+
if (delivered) deliveredFileIds.add(file.id);
|
|
30776
|
+
if (fullyDelivered) fullyDeliveredFileIds.add(file.id);
|
|
29749
30777
|
} catch (error) {
|
|
29750
30778
|
logger.updateMetadata({ fileId: file.id });
|
|
29751
30779
|
logger.error(`🕐 [processFabFilesServer] Error processing file ${file.fileName}: ${error}`);
|
|
@@ -29784,7 +30812,9 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
29784
30812
|
logger.info(`📁 File processing completed in ${fileProcessingTime}ms for ${fabFiles.length} files`);
|
|
29785
30813
|
return {
|
|
29786
30814
|
userMessages,
|
|
29787
|
-
errorMessages
|
|
30815
|
+
errorMessages,
|
|
30816
|
+
deliveredFileIds: Array.from(deliveredFileIds),
|
|
30817
|
+
fullyDeliveredFileIds: Array.from(fullyDeliveredFileIds)
|
|
29788
30818
|
};
|
|
29789
30819
|
}
|
|
29790
30820
|
/**
|
|
@@ -29950,7 +30980,11 @@ const processMessages = (messages, tokenBudget, { truncationNotice } = {}) => {
|
|
|
29950
30980
|
async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, maxInputTokens, settings, historyCount = 0, logger, tokenizer, options = { verbose: false }) {
|
|
29951
30981
|
if (!(maxInputTokens > 0)) {
|
|
29952
30982
|
logger.error(`Invalid maxInputTokens: ${maxInputTokens}. Must be greater than 0.`);
|
|
29953
|
-
return
|
|
30983
|
+
return {
|
|
30984
|
+
messages: [],
|
|
30985
|
+
messageTruncation: null,
|
|
30986
|
+
injectedBlocks: []
|
|
30987
|
+
};
|
|
29954
30988
|
}
|
|
29955
30989
|
if (process.env.VERBOSE_CHAT_CONTEXT !== "false") {
|
|
29956
30990
|
if (options.verbose) {
|
|
@@ -29990,18 +31024,28 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
|
|
|
29990
31024
|
const systemMessages = [];
|
|
29991
31025
|
let systemTokenCount = 0;
|
|
29992
31026
|
const builderInjectedPriorities = /* @__PURE__ */ new Map();
|
|
29993
|
-
|
|
31027
|
+
const injectedBlockGateResults = /* @__PURE__ */ new Map();
|
|
31028
|
+
if (options.skipAdminPromptTemplates) {
|
|
31029
|
+
injectedBlockGateResults.set("formatPrompt", { reason: "mode_skipped" });
|
|
31030
|
+
injectedBlockGateResults.set("imagePrompt", { reason: "mode_skipped" });
|
|
31031
|
+
} else {
|
|
29994
31032
|
if (getSettingsValue("UseFormatPrompt", settings)) {
|
|
29995
31033
|
const formatPromptTemplate = settings.FormatPromptTemplate;
|
|
29996
31034
|
const withFormatPrompt = includeHardcodedSystemMessage(fabMessages, formatPromptTemplate);
|
|
29997
|
-
if (withFormatPrompt.length > fabMessages.length)
|
|
31035
|
+
if (withFormatPrompt.length > fabMessages.length) {
|
|
31036
|
+
builderInjectedPriorities.set(withFormatPrompt[0], 60);
|
|
31037
|
+
injectedBlockGateResults.set("formatPrompt", { message: withFormatPrompt[0] });
|
|
31038
|
+
} else injectedBlockGateResults.set("formatPrompt", { reason: "not_triggered" });
|
|
29998
31039
|
fabMessages = withFormatPrompt;
|
|
29999
|
-
}
|
|
31040
|
+
} else injectedBlockGateResults.set("formatPrompt", { reason: "setting_disabled" });
|
|
30000
31041
|
if (getSettingsValue("UseImagePrompt", settings)) {
|
|
30001
31042
|
const withImagePrompt = includeImagePromptSystemMessage(fabMessages, userPromptContent, options.imageGenerationAvailable ?? false);
|
|
30002
|
-
if (withImagePrompt.length > fabMessages.length)
|
|
31043
|
+
if (withImagePrompt.length > fabMessages.length) {
|
|
31044
|
+
builderInjectedPriorities.set(withImagePrompt[0], 50);
|
|
31045
|
+
injectedBlockGateResults.set("imagePrompt", { message: withImagePrompt[0] });
|
|
31046
|
+
} else injectedBlockGateResults.set("imagePrompt", { reason: "not_triggered" });
|
|
30003
31047
|
fabMessages = withImagePrompt;
|
|
30004
|
-
}
|
|
31048
|
+
} else injectedBlockGateResults.set("imagePrompt", { reason: "setting_disabled" });
|
|
30005
31049
|
}
|
|
30006
31050
|
const imageMessages = [];
|
|
30007
31051
|
const nonImageMessages = [];
|
|
@@ -30067,6 +31111,21 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
|
|
|
30067
31111
|
admittedSystemMessages.add(message);
|
|
30068
31112
|
}
|
|
30069
31113
|
systemMessages.push(...systemCandidates.filter((message) => admittedSystemMessages.has(message)));
|
|
31114
|
+
const injectedBlocks = BUILDER_INJECTED_BLOCK_IDS.map((id) => {
|
|
31115
|
+
const gateResult = injectedBlockGateResults.get(id);
|
|
31116
|
+
if (!gateResult?.message) return {
|
|
31117
|
+
id,
|
|
31118
|
+
injected: false,
|
|
31119
|
+
delivered: false,
|
|
31120
|
+
...gateResult?.reason ? { reason: gateResult.reason } : {}
|
|
31121
|
+
};
|
|
31122
|
+
return {
|
|
31123
|
+
id,
|
|
31124
|
+
injected: true,
|
|
31125
|
+
delivered: admittedSystemMessages.has(gateResult.message),
|
|
31126
|
+
...typeof gateResult.message.content === "string" ? { content: gateResult.message.content } : {}
|
|
31127
|
+
};
|
|
31128
|
+
});
|
|
30070
31129
|
if (systemCandidates.length > 0 && systemMessages.length === 0) logger.warn(`No system instructions fit the budget: ${systemCandidates.length} message(s) dropped, cap ${systemTokenCap} of ${preSystemBudget} est. tokens (${Math.round(65)}% after the attached-content reserve of ${contentReserve}). Smallest was ${Math.min(...systemCandidates.map(systemMessageTokens))} est. tokens.`);
|
|
30071
31130
|
tokenBudget -= systemTokenCount;
|
|
30072
31131
|
const historyMessages = isUnlimitedHistory(historyCount) ? previousMessages : historyCount > 0 ? previousMessages.slice(-historyCount * 2) : [];
|
|
@@ -30197,16 +31256,16 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
|
|
|
30197
31256
|
];
|
|
30198
31257
|
};
|
|
30199
31258
|
const messages = assemble(processedContentMessages, processedPreviousMessages);
|
|
30200
|
-
const
|
|
31259
|
+
const buildDebugInfo = (finalContentMessages) => {
|
|
30201
31260
|
const historyWindowed = previousMessages.length > historyMessages.length;
|
|
30202
31261
|
const budgetTruncated = allRemovedMessages.length > 0 || contentSqueezed || historyCutMidMessage;
|
|
30203
|
-
|
|
31262
|
+
return {
|
|
30204
31263
|
wasTruncated: budgetTruncated,
|
|
30205
31264
|
originalMessageCount: originalTotalMessageCount,
|
|
30206
31265
|
truncatedMessageCount: processedPreviousMessages.length + finalContentMessages.length,
|
|
30207
31266
|
truncationMethod: budgetTruncated ? "token-budget" : historyWindowed ? "history-limit" : void 0,
|
|
30208
31267
|
removedMessages: allRemovedMessages.length > 0 ? allRemovedMessages : void 0
|
|
30209
|
-
}
|
|
31268
|
+
};
|
|
30210
31269
|
};
|
|
30211
31270
|
const finalTokenCount = await calculateTotalTokenLength(messages, {
|
|
30212
31271
|
estimateOnly: false,
|
|
@@ -30251,8 +31310,11 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
|
|
|
30251
31310
|
else break;
|
|
30252
31311
|
}
|
|
30253
31312
|
if (currentTokenCount > maxInputTokens) logger.warn(`Final safety pass could not bring the payload under maxInputTokens (${currentTokenCount} > ${maxInputTokens}). Remaining: ${systemMessages.length} system, ${processedPreviousMessages.length} history, ${reducedContentMessages.length} content, ${imageMessages.length} image message(s) plus the user prompt` + (lastRatio === null ? ", with nothing shrinkable to measure." : `, at ${lastRatio.toFixed(2)} real tokens per estimated token.`));
|
|
30254
|
-
|
|
30255
|
-
|
|
31313
|
+
return {
|
|
31314
|
+
messages: ensureToolPairingIntegrity(assemble(reducedContentMessages, processedPreviousMessages), logger),
|
|
31315
|
+
messageTruncation: buildDebugInfo(reducedContentMessages),
|
|
31316
|
+
injectedBlocks
|
|
31317
|
+
};
|
|
30256
31318
|
}
|
|
30257
31319
|
if (process.env.VERBOSE_MESSAGE_BUILDING === "true") {
|
|
30258
31320
|
logger.log("=== Verbose Message Building Log ===");
|
|
@@ -30265,18 +31327,17 @@ async function buildAndSortMessages(previousMessages, fabMessages, userPrompt, m
|
|
|
30265
31327
|
logger.log(`\nTotal messages: ${messages.length}`);
|
|
30266
31328
|
logger.log("=== End of Verbose Message Building Log ===");
|
|
30267
31329
|
}
|
|
30268
|
-
|
|
30269
|
-
|
|
30270
|
-
|
|
30271
|
-
|
|
30272
|
-
|
|
30273
|
-
*/
|
|
30274
|
-
function getLastBuildDebugInfo() {
|
|
30275
|
-
return buildAndSortMessages.lastDebugInfo?.messageTruncation || null;
|
|
31330
|
+
return {
|
|
31331
|
+
messages: ensureToolPairingIntegrity(messages, logger),
|
|
31332
|
+
messageTruncation: buildDebugInfo(processedContentMessages),
|
|
31333
|
+
injectedBlocks
|
|
31334
|
+
};
|
|
30276
31335
|
}
|
|
30277
31336
|
var llm_exports = /* @__PURE__ */ __exportAll({
|
|
30278
31337
|
ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
|
|
30279
31338
|
ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
|
|
31339
|
+
BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
|
|
31340
|
+
DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
|
|
30280
31341
|
EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
|
|
30281
31342
|
FORMAT_PROMPT_PRIORITY: () => 60,
|
|
30282
31343
|
IMAGE_PROMPT_PRIORITY: () => 50,
|
|
@@ -30292,13 +31353,13 @@ var llm_exports = /* @__PURE__ */ __exportAll({
|
|
|
30292
31353
|
buildAndSortMessages: () => buildAndSortMessages,
|
|
30293
31354
|
calculateTotalTokenLength: () => calculateTotalTokenLength,
|
|
30294
31355
|
computeCosineSimilarity: () => computeCosineSimilarity,
|
|
31356
|
+
computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
|
|
30295
31357
|
effectiveContextWindow: () => effectiveContextWindow,
|
|
30296
31358
|
fetchAgentConversationHistory: () => fetchAgentConversationHistory,
|
|
30297
31359
|
fetchAndConvertFabFiles: () => fetchAndConvertFabFiles,
|
|
30298
31360
|
fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
|
|
30299
31361
|
generateSafeEmbedding: () => generateSafeEmbedding,
|
|
30300
31362
|
getCachedSignedUrl: () => getCachedSignedUrl,
|
|
30301
|
-
getLastBuildDebugInfo: () => getLastBuildDebugInfo,
|
|
30302
31363
|
includeHardcodedSystemMessage: () => includeHardcodedSystemMessage,
|
|
30303
31364
|
includeImagePromptSystemMessage: () => includeImagePromptSystemMessage,
|
|
30304
31365
|
processFabFilesServer: () => processFabFilesServer,
|
|
@@ -32198,7 +33259,12 @@ var BFLImageService = class extends AIImageService {
|
|
|
32198
33259
|
if (height) requestBody.height = height;
|
|
32199
33260
|
}
|
|
32200
33261
|
const cleanedBody = this.stripNullFields(requestBody);
|
|
32201
|
-
|
|
33262
|
+
const safeRequestBody = {
|
|
33263
|
+
...cleanedBody,
|
|
33264
|
+
prompt: prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt,
|
|
33265
|
+
...cleanedBody.image_prompt ? { image_prompt: `[BASE64_DATA_${cleanedBody.image_prompt.length}_CHARS]` } : {}
|
|
33266
|
+
};
|
|
33267
|
+
Logger.globalInstance.log("BFL Image generation request body:", safeRequestBody);
|
|
32202
33268
|
const submitResponse = await axios.post(`${this.baseUrl}/${model}`, cleanedBody, { headers: {
|
|
32203
33269
|
accept: "application/json",
|
|
32204
33270
|
"x-key": this.apiKey,
|
|
@@ -32211,7 +33277,7 @@ var BFLImageService = class extends AIImageService {
|
|
|
32211
33277
|
pollingUrl,
|
|
32212
33278
|
responseData: submitResponse.data,
|
|
32213
33279
|
endpoint: `${this.baseUrl}/${model}`,
|
|
32214
|
-
requestBody
|
|
33280
|
+
requestBody: safeRequestBody
|
|
32215
33281
|
});
|
|
32216
33282
|
const imageUrl = await this.pollForResult(requestId, pollingUrl);
|
|
32217
33283
|
Logger.globalInstance.debug("[DEBUG] Received BFL image URL:", {
|
|
@@ -32620,8 +33686,6 @@ var GeminiImageService = class extends AIImageService {
|
|
|
32620
33686
|
const aspectRatio = this.resolveAspectRatio(options);
|
|
32621
33687
|
if (aspectRatio) config.aspectRatio = aspectRatio;
|
|
32622
33688
|
if (options.guidance !== null && options.guidance !== void 0) config.guidanceScale = options.guidance;
|
|
32623
|
-
if (options.prompt_upsampling !== void 0) config.enhancePrompt = options.prompt_upsampling;
|
|
32624
|
-
if (options.seed !== null && options.seed !== void 0) config.seed = options.seed;
|
|
32625
33689
|
const outputMimeType = this.resolveMimeType(options.output_format);
|
|
32626
33690
|
if (outputMimeType) config.outputMimeType = outputMimeType;
|
|
32627
33691
|
return config;
|
|
@@ -33932,11 +34996,11 @@ const DEFAULT_FALLBACK_CHAIN = [
|
|
|
33932
34996
|
*/
|
|
33933
34997
|
function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logger, excludeModelIds, preferUntriedBackend) {
|
|
33934
34998
|
logger.info(`🔍 Finding automatic fallback for ${originalModel.id}`);
|
|
33935
|
-
const hasValidKey = (m) => !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
|
|
34999
|
+
const hasValidKey = (m) => !m.disabled && !!apiKeyTable[m.backend] && apiKeyTable[m.backend] !== "expired";
|
|
33936
35000
|
const preferences = [...FALLBACK_PREFERENCES[originalModel.id] ?? []];
|
|
33937
35001
|
if (preferences.length === 0) preferences.push(...DEFAULT_FALLBACK_CHAIN);
|
|
33938
35002
|
if (preferUntriedBackend) {
|
|
33939
|
-
const triedBackends =
|
|
35003
|
+
const triedBackends = new Set(originalModel.backend ? [originalModel.backend] : []);
|
|
33940
35004
|
for (const id of excludeModelIds ?? []) {
|
|
33941
35005
|
const tried = availableModels.find((m) => m.id === id);
|
|
33942
35006
|
if (tried) triedBackends.add(tried.backend);
|
|
@@ -33963,7 +35027,7 @@ function findAutomaticFallback(originalModel, availableModels, apiKeyTable, logg
|
|
|
33963
35027
|
return model;
|
|
33964
35028
|
}
|
|
33965
35029
|
}
|
|
33966
|
-
logger.
|
|
35030
|
+
logger.warn("⚠️ No suitable automatic fallback model found");
|
|
33967
35031
|
return null;
|
|
33968
35032
|
}
|
|
33969
35033
|
/**
|
|
@@ -34033,6 +35097,39 @@ async function getLlmWithFallback(originalModel, fallbackModelId, availableModel
|
|
|
34033
35097
|
return null;
|
|
34034
35098
|
}
|
|
34035
35099
|
/**
|
|
35100
|
+
* Pick a runnable substitute for a model id that is NOT in `availableModels` at all -
|
|
35101
|
+
* a sunset id a catalog lifecycle row has hidden, or one disabled since whatever
|
|
35102
|
+
* persisted it (a session pin, a stored mapping row) was written.
|
|
35103
|
+
*
|
|
35104
|
+
* `getLlmWithFallback` cannot serve this case: its `originalModel` is a `ModelInfo`,
|
|
35105
|
+
* which by definition does not exist here. This walks the same
|
|
35106
|
+
* `FALLBACK_PREFERENCES` -> `DEFAULT_FALLBACK_CHAIN` -> any-keyed-model selection
|
|
35107
|
+
* through the same `findAutomaticFallback`, so the substitute a hidden id lands on
|
|
35108
|
+
* cannot diverge from the one a listed-but-failing id would.
|
|
35109
|
+
*
|
|
35110
|
+
* Callers should run `resolveDeprecatedModelId` first: a sunset id with a known
|
|
35111
|
+
* successor should be forwarded to it (and counted as a `[model-sunset]`) rather
|
|
35112
|
+
* than treated as a model with no answer.
|
|
35113
|
+
*/
|
|
35114
|
+
function findFallbackForMissingModel(missingModelId, availableModels, apiKeyTable, logger, endUserId) {
|
|
35115
|
+
const fallbackModel = findAutomaticFallback({ id: missingModelId }, availableModels, apiKeyTable, logger);
|
|
35116
|
+
if (!fallbackModel) return null;
|
|
35117
|
+
const backend = (0, llm_exports.getLlmByModel)(apiKeyTable, {
|
|
35118
|
+
modelInfo: fallbackModel,
|
|
35119
|
+
logger,
|
|
35120
|
+
endUserId
|
|
35121
|
+
});
|
|
35122
|
+
if (!backend) {
|
|
35123
|
+
logger.warn(`⚠️ Fallback for unavailable model ${missingModelId} failed to initialize: ${fallbackModel.id}`);
|
|
35124
|
+
return null;
|
|
35125
|
+
}
|
|
35126
|
+
return {
|
|
35127
|
+
model: fallbackModel,
|
|
35128
|
+
backend,
|
|
35129
|
+
attempt: 1
|
|
35130
|
+
};
|
|
35131
|
+
}
|
|
35132
|
+
/**
|
|
34036
35133
|
* Tiktoken-based implementation of the tokenizer interface
|
|
34037
35134
|
* Provides caching for performance and configurable logging
|
|
34038
35135
|
*
|
|
@@ -34541,6 +35638,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34541
35638
|
AdminSettingsCache: () => AdminSettingsCache,
|
|
34542
35639
|
BEDROCK_EMBEDDING_MODEL_MAP: () => BEDROCK_EMBEDDING_MODEL_MAP,
|
|
34543
35640
|
BFLImageService: () => BFLImageService,
|
|
35641
|
+
BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
|
|
34544
35642
|
BadRequestError: () => BadRequestError,
|
|
34545
35643
|
BaseStorage: () => BaseStorage,
|
|
34546
35644
|
BedrockEmbeddingService: () => BedrockEmbeddingService,
|
|
@@ -34552,6 +35650,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34552
35650
|
ClientMessageSender: () => ClientMessageSender,
|
|
34553
35651
|
CorruptedFileError: () => CorruptedFileError,
|
|
34554
35652
|
DEFAULT_FALLBACK_CHAIN: () => DEFAULT_FALLBACK_CHAIN,
|
|
35653
|
+
DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
|
|
34555
35654
|
DEFAULT_PASSAGE_TOKEN_TARGET: () => 512,
|
|
34556
35655
|
EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
|
|
34557
35656
|
ElevenLabsMusicGenerator: () => ElevenLabsMusicGenerator,
|
|
@@ -34602,6 +35701,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34602
35701
|
RapidReplyMappingsCache: () => RapidReplyMappingsCache,
|
|
34603
35702
|
S3Storage: () => S3Storage,
|
|
34604
35703
|
SQSService: () => SQSService,
|
|
35704
|
+
ScopedSettingsCache: () => ScopedSettingsCache,
|
|
34605
35705
|
SmartChunker: () => SmartChunker,
|
|
34606
35706
|
TOKEN_BUFFER_PERCENTAGE: () => TOKEN_BUFFER_PERCENTAGE,
|
|
34607
35707
|
TOOL_RESULT_NOT_RECORDED: () => TOOL_RESULT_NOT_RECORDED,
|
|
@@ -34638,6 +35738,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34638
35738
|
checkStorageLimitForFile: () => checkStorageLimitForFile,
|
|
34639
35739
|
cleanMermaidSyntax: () => cleanMermaidSyntax,
|
|
34640
35740
|
computeCosineSimilarity: () => computeCosineSimilarity,
|
|
35741
|
+
computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
|
|
34641
35742
|
convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
|
|
34642
35743
|
createQuestPlanToolSchema: () => createQuestPlanToolSchema,
|
|
34643
35744
|
createTokenizer: () => createTokenizer,
|
|
@@ -34653,18 +35754,19 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34653
35754
|
fetchAndParseURL: () => fetchAndParseURL,
|
|
34654
35755
|
fetchAndProcessPreviousMessages: () => fetchAndProcessPreviousMessages,
|
|
34655
35756
|
filterRetrievalExcluded: () => filterRetrievalExcluded,
|
|
35757
|
+
findFallbackForMissingModel: () => findFallbackForMissingModel,
|
|
34656
35758
|
formatVoiceHistory: () => formatVoiceHistory,
|
|
34657
35759
|
generateSafeEmbedding: () => generateSafeEmbedding,
|
|
34658
35760
|
getCachedSignedUrl: () => getCachedSignedUrl,
|
|
34659
35761
|
getFileContent: () => getFileContent,
|
|
34660
35762
|
getFileExtension: () => getFileExtension,
|
|
34661
35763
|
getFileType: () => getFileType,
|
|
34662
|
-
getLastBuildDebugInfo: () => getLastBuildDebugInfo,
|
|
34663
35764
|
getLlmWithFallback: () => getLlmWithFallback,
|
|
34664
35765
|
getMimeTypeByExtension: () => getMimeTypeByExtension,
|
|
34665
35766
|
getNotificationDeduplicator: () => getNotificationDeduplicator,
|
|
34666
35767
|
getProviderFromModel: () => getProviderFromModel,
|
|
34667
35768
|
getRetryAfterMs: () => getRetryAfterMs,
|
|
35769
|
+
getScopedOverrides: () => getScopedOverrides,
|
|
34668
35770
|
getSettingByName: () => getSettingByName,
|
|
34669
35771
|
getSettingsByNames: () => getSettingsByNames,
|
|
34670
35772
|
getSettingsCacheStats: () => getSettingsCacheStats,
|
|
@@ -34674,6 +35776,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34674
35776
|
includeHardcodedSystemMessage: () => includeHardcodedSystemMessage,
|
|
34675
35777
|
includeImagePromptSystemMessage: () => includeImagePromptSystemMessage,
|
|
34676
35778
|
initializeConfig: () => initializeConfig,
|
|
35779
|
+
invalidateScopedSettingsCache: () => invalidateScopedSettingsCache,
|
|
34677
35780
|
invalidateSettingsCache: () => invalidateSettingsCache,
|
|
34678
35781
|
isAiEditableOfficeMime: () => isAiEditableOfficeMime,
|
|
34679
35782
|
isGPT5ModelWithToolSupport: () => isGPT5ModelWithToolSupport,
|
|
@@ -34708,6 +35811,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
34708
35811
|
resolveEmbeddingConfig: () => resolveEmbeddingConfig,
|
|
34709
35812
|
resolveSupportedMimeType: () => resolveSupportedMimeType,
|
|
34710
35813
|
safeInputWindow: () => safeInputWindow,
|
|
35814
|
+
scopedOverrideKey: () => scopedOverrideKey,
|
|
34711
35815
|
secureParameters: () => secureParameters,
|
|
34712
35816
|
sendToConnection: () => sendToConnection,
|
|
34713
35817
|
shouldTriggerFallback: () => shouldTriggerFallback,
|