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