@bike4mind/cli 0.20.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/{AgentHistoryStore-T7Oh84Yn.mjs → AgentHistoryStore-ucv6xXVT.mjs} +1127 -193
- package/dist/{ApiClient-BopvMmQk.mjs → ApiClient-Ut1MXtOs.mjs} +2 -2
- package/dist/{ConfigStore-cIyF7hDg.mjs → ConfigStore-CoY0l0gr.mjs} +1393 -187
- package/dist/{buildAgent-P0tOMLt1.mjs → buildAgent-jZhBReAr.mjs} +2 -2
- 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 +3 -3
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +3 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +13 -54
- package/dist/{package-CnVCHR3U.mjs → package-CfIETbXd.mjs} +1 -1
- package/dist/{serve-BocVOJ3W.mjs → serve-C9UDR5px.mjs} +5 -3
- package/package.json +13 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as SupportedFabFileMimeTypes, $t as buildRateLimitLogEntry, A as HTTPError, At as isRenderableModelType, B as OPENAI_GPT_IMAGE_1_IMAGE_SIZES, Bt as resolveHistoryFetchLimit, Ct as isGeminiModelId, D as FIXED_TEMPERATURE_MODELS, Dt as isModelAccessible, E as FIELD_GROUP_OF, Et as isMediaModelType, F as MODEL_INFO_FIELD_GROUP_OF, Ft as isZodError, G as PermissionDeniedError, Gt as usdToCredits, H as OllamaEmbeddingModel, Ht as settingsMap, I as McpServerName, It as mapMimeTypeToArtifactType, J as REFUSAL_FALLBACK_MODELS, K as REASONING_EFFORT_INCOMPATIBLE_WITH_TOOLS_MODELS, Kt as usdToCreditsStochastic, L as ModelBackend, Lt as obfuscateApiKey, M as IMAGE_SIZE_CONSTRAINTS, Mt as isSupportedFabFileMimeType, N as ImageModels, Nt as isUnlimitedHistory, O as FORMAT_PROMPT_TEMPLATE, Ot as isModelDeprecated, P as InternalServerError, Pt as isUserInitiatedAbort, Q as SpeechToTextModels, R as NO_TEMPERATURE_MODELS, Rt as parseEmbeddingRateLimitHeaders, S as CorruptedFileError, St as isGPTImageModel, Tt as isImageServeable, U as OpenAIEmbeddingModel, Ut as toModelInfo, V as OPENAI_GPT_IMAGE_2_IMAGE_SIZES, Vt as secureParameters, Wt as toModelRecord, Y as RESPONSES_API_TOOL_MODELS, _ as BadRequestError, _t as isChunkRebuildPending, at as VideoModels, bt as isFieldGroup, ct as applyModelPriceCatalog, dt as defaultEmbeddingModelForEnv, en as extractSnippetMeta, et as TTS_MAX_INPUT_CHARS, ft as getMcpProviderMetadata, g as BFL_SAFETY_TOLERANCE, gt as isAudioMimeType, h as BEDROCK_NO_PROMPT_CACHING_MODELS, ht as hasUsableLimits, it as VIDEO_SIZE_CONSTRAINTS, j as HttpStatus, jt as isRetryableError, k as ForbiddenError, kt as isPlaceholderApiKey, lt as calculateRetryDelay, m as ApiKeyType, mt as getRetryAfterMs, n as logger, nn as parseRateLimitHeaders, nt as UnauthorizedError, ot as VoyageAIEmbeddingModel, p as ARTIFACT_ATTRS_PATTERN, pt as getQuestErrorCode, q as REASONING_SUPPORTED_MODELS, qt as withRetry, rt as UnprocessableEntityError, st as WORK_ITEM_STATUSES, tn as isNearLimit, tt as TooManyRequestsError, ut as dayjsConfig_default, v as BedrockEmbeddingModel, vt as isChunkStalledFile, w as DEFAULT_UNKNOWN_CONTEXT_WINDOW, wt as isImageAttachment, x as ChatModels, xt as isGPTImage2Model, y as CONTEXT_WINDOW_SAFETY_BUFFER_TOKENS, z as NotFoundError, zt as reservationOutputTokens } from "./ConfigStore-CoY0l0gr.mjs";
|
|
3
3
|
import { n as isPathAllowed, t as assertPathAllowed } from "./pathValidation-D8tjkQXE-1HwvsuYT.mjs";
|
|
4
4
|
import { n as isTerminalShellStatus, t as getShellSessionManager } from "./ShellSessionManager-6o8KZzl1-vrbPAUTq.mjs";
|
|
5
5
|
import { execFile, execFileSync, spawn } from "child_process";
|
|
@@ -52,9 +52,9 @@ import mongoose from "mongoose";
|
|
|
52
52
|
import { parse } from "shell-quote";
|
|
53
53
|
import { homedir as homedir$1 } from "node:os";
|
|
54
54
|
import { EventEmitter } from "events";
|
|
55
|
+
import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
|
|
55
56
|
import { fileURLToPath } from "url";
|
|
56
57
|
import { Anthropic, RateLimitError } from "@anthropic-ai/sdk";
|
|
57
|
-
import { CloudWatchClient, PutMetricDataCommand, StandardUnit } from "@aws-sdk/client-cloudwatch";
|
|
58
58
|
import { GoogleGenAI } from "@google/genai";
|
|
59
59
|
import pick from "lodash/pick.js";
|
|
60
60
|
import { Stream } from "openai/streaming";
|
|
@@ -66,6 +66,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|
|
66
66
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
67
67
|
import { getDomain } from "tldts";
|
|
68
68
|
import * as dotenv from "dotenv";
|
|
69
|
+
import { createHash as createHash$1 } from "node:crypto";
|
|
69
70
|
import invert from "lodash/invert.js";
|
|
70
71
|
import * as util from "node:util";
|
|
71
72
|
import * as zlib from "node:zlib";
|
|
@@ -1836,7 +1837,27 @@ const webFetchTool = {
|
|
|
1836
1837
|
})
|
|
1837
1838
|
};
|
|
1838
1839
|
//#endregion
|
|
1839
|
-
//#region ../../b4m-core/services/dist/websearch-
|
|
1840
|
+
//#region ../../b4m-core/services/dist/websearch-BLmQCbHG.mjs
|
|
1841
|
+
/**
|
|
1842
|
+
* The coarse recency bucket both providers speak, as the smallest one containing `recencyDays`.
|
|
1843
|
+
* Null when there is no constraint, or when the window is wider than the widest bucket - a
|
|
1844
|
+
* "within 10 years" filter is not a filter, and sending one would exclude undated pages for nothing.
|
|
1845
|
+
*/
|
|
1846
|
+
function recencyBucket(recencyDays) {
|
|
1847
|
+
if (typeof recencyDays !== "number" || !Number.isFinite(recencyDays) || recencyDays <= 0) return null;
|
|
1848
|
+
if (recencyDays <= 1) return "day";
|
|
1849
|
+
if (recencyDays <= 7) return "week";
|
|
1850
|
+
if (recencyDays <= 31) return "month";
|
|
1851
|
+
if (recencyDays <= 366) return "year";
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
/** SerpAPI spells the buckets `qdr:d|w|m|y` on the `tbs` parameter. */
|
|
1855
|
+
const SERPAPI_QDR = {
|
|
1856
|
+
day: "qdr:d",
|
|
1857
|
+
week: "qdr:w",
|
|
1858
|
+
month: "qdr:m",
|
|
1859
|
+
year: "qdr:y"
|
|
1860
|
+
};
|
|
1840
1861
|
const DEFAULT_NUM_RESULTS = 3;
|
|
1841
1862
|
const SEARCH_TIMEOUT_MS = 6e4;
|
|
1842
1863
|
/**
|
|
@@ -1845,14 +1866,14 @@ const SEARCH_TIMEOUT_MS = 6e4;
|
|
|
1845
1866
|
* on a non-OK response so the tool surfaces the failure. Exported (re-exported from index) for the
|
|
1846
1867
|
* REST endpoint and existing tests.
|
|
1847
1868
|
*/
|
|
1848
|
-
async function serpApiSearch(adapters, query, num_results) {
|
|
1869
|
+
async function serpApiSearch(adapters, query, num_results, options) {
|
|
1849
1870
|
const apiKey = await (0, apiKeyService_exports.getSerperKey)(adapters);
|
|
1850
1871
|
const url = new URL("https://serpapi.com/search");
|
|
1851
1872
|
if (!apiKey) {
|
|
1852
1873
|
Logger.globalInstance.error("❌ WebSearch Tool: No API key configured. Skipping search.");
|
|
1853
1874
|
return { organic_results: [] };
|
|
1854
1875
|
}
|
|
1855
|
-
|
|
1876
|
+
const searchParams = new URLSearchParams({
|
|
1856
1877
|
engine: "google",
|
|
1857
1878
|
api_key: apiKey,
|
|
1858
1879
|
q: query,
|
|
@@ -1861,7 +1882,10 @@ async function serpApiSearch(adapters, query, num_results) {
|
|
|
1861
1882
|
gl: "us",
|
|
1862
1883
|
hl: "en",
|
|
1863
1884
|
num: (num_results || DEFAULT_NUM_RESULTS).toString()
|
|
1864
|
-
})
|
|
1885
|
+
});
|
|
1886
|
+
const bucket = recencyBucket(options?.recencyDays);
|
|
1887
|
+
if (bucket) searchParams.set("tbs", SERPAPI_QDR[bucket]);
|
|
1888
|
+
url.search = searchParams.toString();
|
|
1865
1889
|
const controller = new AbortController();
|
|
1866
1890
|
const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
|
|
1867
1891
|
let response;
|
|
@@ -1889,8 +1913,8 @@ async function serpApiSearch(adapters, query, num_results) {
|
|
|
1889
1913
|
function createSerpApiProvider(adapters) {
|
|
1890
1914
|
return {
|
|
1891
1915
|
name: "serpapi",
|
|
1892
|
-
async search(query, numResults) {
|
|
1893
|
-
const data = await serpApiSearch(adapters, query, numResults);
|
|
1916
|
+
async search(query, numResults, options) {
|
|
1917
|
+
const data = await serpApiSearch(adapters, query, numResults, options);
|
|
1894
1918
|
return (Array.isArray(data.organic_results) ? data.organic_results : []).filter((r) => !!r && typeof r.link === "string").map((r) => ({
|
|
1895
1919
|
title: r.title ?? r.link,
|
|
1896
1920
|
url: r.link,
|
|
@@ -1929,16 +1953,19 @@ function parseSearxngResults(data, numResults) {
|
|
|
1929
1953
|
function createSearxngProvider(baseUrl) {
|
|
1930
1954
|
return {
|
|
1931
1955
|
name: "searxng",
|
|
1932
|
-
async search(query, numResults) {
|
|
1956
|
+
async search(query, numResults, options) {
|
|
1933
1957
|
const limit = numResults && numResults > 0 ? numResults : DEFAULT_NUM_RESULTS;
|
|
1934
1958
|
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
1935
1959
|
const url = new URL(`${trimmed}/search`);
|
|
1936
|
-
|
|
1960
|
+
const params = new URLSearchParams({
|
|
1937
1961
|
q: query,
|
|
1938
1962
|
format: "json",
|
|
1939
1963
|
language: "en",
|
|
1940
1964
|
safesearch: "1"
|
|
1941
|
-
})
|
|
1965
|
+
});
|
|
1966
|
+
const bucket = recencyBucket(options?.recencyDays);
|
|
1967
|
+
if (bucket) params.set("time_range", bucket);
|
|
1968
|
+
url.search = params.toString();
|
|
1942
1969
|
const controller = new AbortController();
|
|
1943
1970
|
const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
|
|
1944
1971
|
try {
|
|
@@ -2061,7 +2088,7 @@ const webSearchTool = {
|
|
|
2061
2088
|
})
|
|
2062
2089
|
};
|
|
2063
2090
|
//#endregion
|
|
2064
|
-
//#region ../../b4m-core/services/dist/toolGenerators-
|
|
2091
|
+
//#region ../../b4m-core/services/dist/toolGenerators-hk-Robqc.mjs
|
|
2065
2092
|
const diceRoll = async (parameters) => {
|
|
2066
2093
|
if (!parameters?.sides || !parameters?.times) throw new Error("Tool dice roll: Missing required parameters");
|
|
2067
2094
|
return sum(times(parameters.times, () => random(1, parameters.sides))).toString();
|
|
@@ -2748,7 +2775,7 @@ const askUserQuestionTool = {
|
|
|
2748
2775
|
* re-export them without pulling the full tool graph. `index.ts` re-exports them
|
|
2749
2776
|
* so the server barrel's public API is unchanged.
|
|
2750
2777
|
*/
|
|
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) => {
|
|
2778
|
+
const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inlinedAttachmentIds, fullyInlinedAttachmentIds, suppressLakeArms, sessionRetrievalTags, sessionPreauthorizedLakeIds, questId, getAbortSignal }, storage, imageGenerateStorage, statusUpdate, onStart, onFinish, llm, config, model, imageProcessorLambdaName, tools, allowedDirectories, entitlementKeys = [], sessionId, codeMinifier, availableModels, onToolLlmUsage) => {
|
|
2752
2779
|
const context = {
|
|
2753
2780
|
userId,
|
|
2754
2781
|
user,
|
|
@@ -2772,6 +2799,7 @@ const generateTools = (userId, user, logger, { db, retrievalFilter, kbScope, inl
|
|
|
2772
2799
|
fullyInlinedAttachmentIds,
|
|
2773
2800
|
suppressLakeArms,
|
|
2774
2801
|
sessionRetrievalTags,
|
|
2802
|
+
sessionPreauthorizedLakeIds,
|
|
2775
2803
|
codeMinifier,
|
|
2776
2804
|
availableModels,
|
|
2777
2805
|
onToolLlmUsage,
|
|
@@ -4242,7 +4270,7 @@ const latticeAddEntityTool = {
|
|
|
4242
4270
|
};
|
|
4243
4271
|
if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
|
|
4244
4272
|
const model = await context.db.latticeModels.findById(modelId);
|
|
4245
|
-
if (model) {
|
|
4273
|
+
if (model && model.userId === context.userId) {
|
|
4246
4274
|
const existingIndex = model.data.entities.findIndex((e) => e.id === entityId);
|
|
4247
4275
|
if (existingIndex >= 0) model.data.entities[existingIndex] = entityData;
|
|
4248
4276
|
else model.data.entities.push(entityData);
|
|
@@ -4252,7 +4280,23 @@ const latticeAddEntityTool = {
|
|
|
4252
4280
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4253
4281
|
});
|
|
4254
4282
|
context.logger.info(`[Lattice] Added entity ${entityId} to model ${modelId}`);
|
|
4255
|
-
} else
|
|
4283
|
+
} else if (model) {
|
|
4284
|
+
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
4285
|
+
return JSON.stringify({
|
|
4286
|
+
success: false,
|
|
4287
|
+
action: "ADD_ENTITY",
|
|
4288
|
+
modelId,
|
|
4289
|
+
error: `Access denied: you do not have permission to modify model ${modelId}`
|
|
4290
|
+
});
|
|
4291
|
+
} else {
|
|
4292
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
|
|
4293
|
+
return JSON.stringify({
|
|
4294
|
+
success: false,
|
|
4295
|
+
action: "ADD_ENTITY",
|
|
4296
|
+
modelId,
|
|
4297
|
+
error: `Model ${modelId} not found`
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4256
4300
|
} catch (error) {
|
|
4257
4301
|
context.logger.error(`[Lattice] Failed to persist entity to database:`, error);
|
|
4258
4302
|
}
|
|
@@ -4370,7 +4414,7 @@ const latticeSetValueTool = {
|
|
|
4370
4414
|
const entityId = entityName.toLowerCase().replace(/\s+/g, "_");
|
|
4371
4415
|
if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
|
|
4372
4416
|
const model = await context.db.latticeModels.findById(modelId);
|
|
4373
|
-
if (model) {
|
|
4417
|
+
if (model && model.userId === context.userId) {
|
|
4374
4418
|
const entity = model.data.entities.find((e) => e.id === entityId || e.name === entityName);
|
|
4375
4419
|
if (entity) {
|
|
4376
4420
|
const attrIndex = entity.attributes.findIndex((a) => a.key === attributeKey);
|
|
@@ -4390,7 +4434,23 @@ const latticeSetValueTool = {
|
|
|
4390
4434
|
});
|
|
4391
4435
|
context.logger.info(`[Lattice] Set ${entityId}.${attributeKey} = ${value} in model ${modelId}`);
|
|
4392
4436
|
} else context.logger.warn(`[Lattice] Entity ${entityName} not found in model ${modelId}`);
|
|
4393
|
-
} else
|
|
4437
|
+
} else if (model) {
|
|
4438
|
+
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
4439
|
+
return JSON.stringify({
|
|
4440
|
+
success: false,
|
|
4441
|
+
action: "SET_VALUE",
|
|
4442
|
+
modelId,
|
|
4443
|
+
error: `Access denied: you do not have permission to modify model ${modelId}`
|
|
4444
|
+
});
|
|
4445
|
+
} else {
|
|
4446
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
|
|
4447
|
+
return JSON.stringify({
|
|
4448
|
+
success: false,
|
|
4449
|
+
action: "SET_VALUE",
|
|
4450
|
+
modelId,
|
|
4451
|
+
error: `Model ${modelId} not found`
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4394
4454
|
} catch (error) {
|
|
4395
4455
|
context.logger.error(`[Lattice] Failed to persist value to database:`, error);
|
|
4396
4456
|
}
|
|
@@ -4489,7 +4549,7 @@ const latticeCreateRuleTool = {
|
|
|
4489
4549
|
let entityCreatedMessage = "";
|
|
4490
4550
|
if (context.db.latticeModels && modelId && /^[a-f0-9]{24}$/.test(modelId)) try {
|
|
4491
4551
|
const model = await context.db.latticeModels.findById(modelId);
|
|
4492
|
-
if (model) {
|
|
4552
|
+
if (model && model.userId === context.userId) {
|
|
4493
4553
|
if (!model.data.entities.some((e) => e.id === outputEntityId || e.name.toLowerCase() === parsedRule.outputEntity.toLowerCase()) && parsedRule.outputEntity !== "unknown") {
|
|
4494
4554
|
const now = /* @__PURE__ */ new Date();
|
|
4495
4555
|
const newEntity = {
|
|
@@ -4526,7 +4586,23 @@ const latticeCreateRuleTool = {
|
|
|
4526
4586
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4527
4587
|
});
|
|
4528
4588
|
context.logger.info(`[Lattice] Created rule ${ruleId} in model ${modelId}`);
|
|
4529
|
-
} else
|
|
4589
|
+
} else if (model) {
|
|
4590
|
+
context.logger.warn(`[Lattice] Access denied: caller does not own model ${modelId}`);
|
|
4591
|
+
return JSON.stringify({
|
|
4592
|
+
success: false,
|
|
4593
|
+
action: "CREATE_RULE",
|
|
4594
|
+
modelId,
|
|
4595
|
+
error: `Access denied: you do not have permission to modify model ${modelId}`
|
|
4596
|
+
});
|
|
4597
|
+
} else {
|
|
4598
|
+
context.logger.warn(`[Lattice] Model ${modelId} not found in database`);
|
|
4599
|
+
return JSON.stringify({
|
|
4600
|
+
success: false,
|
|
4601
|
+
action: "CREATE_RULE",
|
|
4602
|
+
modelId,
|
|
4603
|
+
error: `Model ${modelId} not found`
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4530
4606
|
} catch (error) {
|
|
4531
4607
|
context.logger.error(`[Lattice] Failed to persist rule to database:`, error);
|
|
4532
4608
|
}
|
|
@@ -6413,6 +6489,130 @@ var EmbeddingAuthError = class extends Error {
|
|
|
6413
6489
|
this.provider = provider;
|
|
6414
6490
|
}
|
|
6415
6491
|
};
|
|
6492
|
+
/**
|
|
6493
|
+
* Passive reporting of embedding-provider rate-limit ceilings.
|
|
6494
|
+
*
|
|
6495
|
+
* A rate limit belongs to the provider organization behind the key, and every embedding response
|
|
6496
|
+
* already carries the ceiling in its headers, so reading them costs no extra request and no extra
|
|
6497
|
+
* tokens. Providers that do not report them (Bedrock, Ollama) simply never produce an observation.
|
|
6498
|
+
*
|
|
6499
|
+
* This module owns the "what did the provider say" half only. It has no opinion about data lakes
|
|
6500
|
+
* or about the throughput levers configured against these numbers; interpreting a ceiling against
|
|
6501
|
+
* a lever belongs to the layer that knows what the levers are.
|
|
6502
|
+
*/
|
|
6503
|
+
/**
|
|
6504
|
+
* Remaining/limit ratio at or below which the provider counts as under pressure. A bulk re-index
|
|
6505
|
+
* draws its window down steadily, so this sits low enough that ordinary throughput does not trip
|
|
6506
|
+
* it and only genuine starvation does.
|
|
6507
|
+
*/
|
|
6508
|
+
const PRESSURE_RATIO = .1;
|
|
6509
|
+
/**
|
|
6510
|
+
* Pressure lasts as long as the window does, and every call in that window reports it. Throttle to
|
|
6511
|
+
* one line per interval so a starved ingest leaves a readable trace instead of flooding the log.
|
|
6512
|
+
*/
|
|
6513
|
+
const PRESSURE_LOG_INTERVAL_MS = 6e4;
|
|
6514
|
+
/**
|
|
6515
|
+
* Process-local, and deliberately so: a cold start re-reports what it measures rather than leaving
|
|
6516
|
+
* a gap shared storage would have to close. Keyed by provider+model+account, so the map is bounded
|
|
6517
|
+
* by the model list times the number of distinct credentials the process serves.
|
|
6518
|
+
*/
|
|
6519
|
+
const stateByKey = /* @__PURE__ */ new Map();
|
|
6520
|
+
/**
|
|
6521
|
+
* A broken reporter is indistinguishable from a steady ceiling - both are silence - so the first
|
|
6522
|
+
* fault has to be loud. Subsequent ones drop to debug: whatever breaks here breaks on every
|
|
6523
|
+
* embedding call, and a bulk re-index would drown the log in it.
|
|
6524
|
+
*/
|
|
6525
|
+
let hasReportedFailure = false;
|
|
6526
|
+
const keyFor = (provider, model, account) => `${provider}:${model}:${account}`;
|
|
6527
|
+
const ceilingChanged = (previous, next) => previous.limitTokens !== next.limitTokens || previous.limitRequests !== next.limitRequests;
|
|
6528
|
+
const describeCeiling = (snapshot) => `${snapshot.limitTokens ?? "unreported"} tokens/min, ${snapshot.limitRequests ?? "unreported"} requests/min`;
|
|
6529
|
+
/** Ratio of the window still available, or null when the provider did not report that dimension. */
|
|
6530
|
+
const remainingRatio = (remaining, limit) => {
|
|
6531
|
+
if (remaining === null || limit === null || limit <= 0) return null;
|
|
6532
|
+
return remaining / limit;
|
|
6533
|
+
};
|
|
6534
|
+
const pressuredDimensions = (snapshot) => {
|
|
6535
|
+
const tokens = remainingRatio(snapshot.remainingTokens, snapshot.limitTokens);
|
|
6536
|
+
const requests = remainingRatio(snapshot.remainingRequests, snapshot.limitRequests);
|
|
6537
|
+
const dimensions = [];
|
|
6538
|
+
if (tokens !== null && tokens <= PRESSURE_RATIO) dimensions.push("tokens");
|
|
6539
|
+
if (requests !== null && requests <= PRESSURE_RATIO) dimensions.push("requests");
|
|
6540
|
+
return dimensions;
|
|
6541
|
+
};
|
|
6542
|
+
/**
|
|
6543
|
+
* Read the rate-limit headers off an embedding response and report the ceiling when it is worth
|
|
6544
|
+
* reporting: the first sighting in this process, a change since the last sighting, or the window
|
|
6545
|
+
* running down. Returns the observation when the provider reported a usable ceiling, else null.
|
|
6546
|
+
*
|
|
6547
|
+
* `account` identifies the provider account the reading belongs to and is part of the memo key,
|
|
6548
|
+
* not just the log line. The credential is resolved per user - a stored personal key beats the
|
|
6549
|
+
* platform key in `getEffectiveLLMApiKeys` - so one process can see several accounts on the same
|
|
6550
|
+
* provider+model. Without the discriminator their readings would collapse into one entry that
|
|
6551
|
+
* flaps between unrelated ceilings and attributes each figure to whoever reads the log next. The
|
|
6552
|
+
* caller supplies it; it must never be key material.
|
|
6553
|
+
*
|
|
6554
|
+
* Never throws. This hangs off the hot path of every embedding call, and a reporting fault must
|
|
6555
|
+
* not be able to fail an embedding that otherwise succeeded.
|
|
6556
|
+
*/
|
|
6557
|
+
function recordEmbeddingRateLimitHeaders(provider, model, account, headers, now = Date.now()) {
|
|
6558
|
+
try {
|
|
6559
|
+
const snapshot = parseEmbeddingRateLimitHeaders(headers);
|
|
6560
|
+
if (!hasUsableLimits(snapshot)) return null;
|
|
6561
|
+
const key = keyFor(provider, model, account);
|
|
6562
|
+
const previous = stateByKey.get(key);
|
|
6563
|
+
const observation = {
|
|
6564
|
+
provider,
|
|
6565
|
+
model,
|
|
6566
|
+
account,
|
|
6567
|
+
snapshot,
|
|
6568
|
+
observedAt: now
|
|
6569
|
+
};
|
|
6570
|
+
const subject = `${provider} ${model} (account ${account})`;
|
|
6571
|
+
if (!previous) Logger.globalInstance.info(`[embedding-limits] ${subject} ceiling measured: ${describeCeiling(snapshot)}`, {
|
|
6572
|
+
provider,
|
|
6573
|
+
model,
|
|
6574
|
+
account,
|
|
6575
|
+
limitTokens: snapshot.limitTokens,
|
|
6576
|
+
limitRequests: snapshot.limitRequests
|
|
6577
|
+
});
|
|
6578
|
+
else if (ceilingChanged(previous.last.snapshot, snapshot)) Logger.globalInstance.warn(`[embedding-limits] ${subject} ceiling CHANGED: was ${describeCeiling(previous.last.snapshot)}, now ${describeCeiling(snapshot)}. Reconcile any throughput lever governed by this account against the new figure.`, {
|
|
6579
|
+
provider,
|
|
6580
|
+
model,
|
|
6581
|
+
account,
|
|
6582
|
+
previousLimitTokens: previous.last.snapshot.limitTokens,
|
|
6583
|
+
previousLimitRequests: previous.last.snapshot.limitRequests,
|
|
6584
|
+
limitTokens: snapshot.limitTokens,
|
|
6585
|
+
limitRequests: snapshot.limitRequests
|
|
6586
|
+
});
|
|
6587
|
+
const pressured = pressuredDimensions(snapshot);
|
|
6588
|
+
const dueForPressureLog = previous?.lastPressureLogAt == null || now - previous.lastPressureLogAt >= PRESSURE_LOG_INTERVAL_MS;
|
|
6589
|
+
const logPressure = pressured.length > 0 && dueForPressureLog;
|
|
6590
|
+
if (logPressure) Logger.globalInstance.warn(`[embedding-limits] ${subject} is at or below ${PRESSURE_RATIO * 100}% of its ${pressured.join(" and ")} window`, {
|
|
6591
|
+
provider,
|
|
6592
|
+
model,
|
|
6593
|
+
account,
|
|
6594
|
+
remainingTokens: snapshot.remainingTokens,
|
|
6595
|
+
remainingRequests: snapshot.remainingRequests,
|
|
6596
|
+
limitTokens: snapshot.limitTokens,
|
|
6597
|
+
limitRequests: snapshot.limitRequests,
|
|
6598
|
+
resetTokensMs: snapshot.resetTokensMs,
|
|
6599
|
+
resetRequestsMs: snapshot.resetRequestsMs
|
|
6600
|
+
});
|
|
6601
|
+
stateByKey.set(key, {
|
|
6602
|
+
last: observation,
|
|
6603
|
+
lastPressureLogAt: logPressure ? now : previous?.lastPressureLogAt ?? null
|
|
6604
|
+
});
|
|
6605
|
+
return observation;
|
|
6606
|
+
} catch (error) {
|
|
6607
|
+
const message = `[embedding-limits] failed to record rate-limit headers: ${error}`;
|
|
6608
|
+
if (hasReportedFailure) Logger.globalInstance.debug(message);
|
|
6609
|
+
else {
|
|
6610
|
+
hasReportedFailure = true;
|
|
6611
|
+
Logger.globalInstance.warn(message);
|
|
6612
|
+
}
|
|
6613
|
+
return null;
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6416
6616
|
const OPENAI_EMBEDDING_MODEL_MAP = {
|
|
6417
6617
|
[OpenAIEmbeddingModel.TEXT_EMBEDDING_3_SMALL]: {
|
|
6418
6618
|
provider: "OpenAI",
|
|
@@ -6433,34 +6633,72 @@ const OPENAI_EMBEDDING_MODEL_MAP = {
|
|
|
6433
6633
|
dimensions: [1536]
|
|
6434
6634
|
}
|
|
6435
6635
|
};
|
|
6436
|
-
|
|
6636
|
+
/**
|
|
6637
|
+
* Non-reversible stand-in for a credential, for use where two accounts have to be told apart in a
|
|
6638
|
+
* log. Same construction as the API-key logging hash in the request middleware. Never emit the key.
|
|
6639
|
+
*/
|
|
6640
|
+
const fingerprintCredential = (apiKey) => `key:${createHash("sha256").update(apiKey).digest("hex").slice(0, 16)}`;
|
|
6641
|
+
/**
|
|
6642
|
+
* Total by construction. The only caller runs inside processSingleBatch's classifying try, where a
|
|
6643
|
+
* throw would be misread as a provider error and re-issue the batch.
|
|
6644
|
+
*/
|
|
6645
|
+
const headerOrNull = (httpResponse, name) => {
|
|
6646
|
+
try {
|
|
6647
|
+
return httpResponse.headers?.get(name) ?? null;
|
|
6648
|
+
} catch {
|
|
6649
|
+
return null;
|
|
6650
|
+
}
|
|
6651
|
+
};
|
|
6652
|
+
/**
|
|
6653
|
+
* The ceilings `generateEmbeddingBatch` splits on, at module scope and exported because a cost
|
|
6654
|
+
* PREFLIGHT has to model the same split before it spends (packages/scripts/retrieval/capturePlan.ts).
|
|
6655
|
+
* A second copy of these numbers in a script cannot track a provider change.
|
|
6656
|
+
*/
|
|
6657
|
+
const OPENAI_MAX_INPUTS_PER_REQUEST = 2048;
|
|
6658
|
+
const OPENAI_MAX_TOKENS_PER_INPUT = 8192;
|
|
6659
|
+
/**
|
|
6660
|
+
* Effective token limit with a 10% safety buffer.
|
|
6661
|
+
* The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
|
|
6662
|
+
* but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
|
|
6663
|
+
* that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
|
|
6664
|
+
*/
|
|
6665
|
+
const OPENAI_EFFECTIVE_TOKEN_LIMIT = Math.floor(27e4);
|
|
6666
|
+
var OpenAIEmbeddingService = class {
|
|
6437
6667
|
client;
|
|
6438
6668
|
model;
|
|
6439
|
-
|
|
6440
|
-
static MAX_TOKENS_PER_REQUEST = 3e5;
|
|
6441
|
-
/**
|
|
6442
|
-
* Effective token limit with a 10% safety buffer.
|
|
6443
|
-
* The tiktoken fallback (text.length/3) deliberately overestimates to be safe,
|
|
6444
|
-
* but DB token counts may have been produced by a different tokenizer (Bedrock, Voyage)
|
|
6445
|
-
* that underestimates. The buffer keeps us clear of the hard limit under tokenizer variance.
|
|
6446
|
-
*/
|
|
6447
|
-
static EFFECTIVE_TOKEN_LIMIT = Math.floor(OpenAIEmbeddingService.MAX_TOKENS_PER_REQUEST * .9);
|
|
6669
|
+
credentialFingerprint;
|
|
6448
6670
|
constructor(apiKey, model = OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002) {
|
|
6449
6671
|
this.client = new OpenAI({ apiKey });
|
|
6450
6672
|
this.validateModel(model);
|
|
6451
6673
|
this.model = model;
|
|
6674
|
+
this.credentialFingerprint = fingerprintCredential(apiKey);
|
|
6675
|
+
}
|
|
6676
|
+
/**
|
|
6677
|
+
* Report the provider ceiling carried on a response we already received. Covers ingest and
|
|
6678
|
+
* query alike: both reach OpenAI through this class, so neither needs its own sampling point.
|
|
6679
|
+
*
|
|
6680
|
+
* The ceiling belongs to the organization behind the key, and the key is resolved per user
|
|
6681
|
+
* (getEffectiveLLMApiKeys prefers a stored personal key over the platform one), so the reading
|
|
6682
|
+
* has to say whose it is. `openai-organization` is the provider's own answer to that; the
|
|
6683
|
+
* credential fingerprint covers the case where the response omits it, and still keeps two
|
|
6684
|
+
* distinct keys as two readings rather than one that flaps between them.
|
|
6685
|
+
*/
|
|
6686
|
+
recordRateLimit(httpResponse) {
|
|
6687
|
+
const account = headerOrNull(httpResponse, "openai-organization") || this.credentialFingerprint;
|
|
6688
|
+
recordEmbeddingRateLimitHeaders("OpenAI", this.model, account, httpResponse.headers);
|
|
6452
6689
|
}
|
|
6453
6690
|
validateModel(model) {
|
|
6454
6691
|
if (!OPENAI_EMBEDDING_MODEL_MAP[model]) throw new Error(`Invalid OpenAI embedding model: ${model}`);
|
|
6455
6692
|
}
|
|
6456
6693
|
async generateEmbedding(text) {
|
|
6457
|
-
const response = await this.client.embeddings.create({
|
|
6694
|
+
const { data: body, response: httpResponse } = await this.client.embeddings.create({
|
|
6458
6695
|
model: this.model,
|
|
6459
6696
|
input: text
|
|
6460
|
-
}).catch((error) => {
|
|
6697
|
+
}).withResponse().catch((error) => {
|
|
6461
6698
|
throw this.toActionableAuthError(error);
|
|
6462
6699
|
});
|
|
6463
|
-
|
|
6700
|
+
this.recordRateLimit(httpResponse);
|
|
6701
|
+
if (body.data && body.data.length > 0) return body.data[0].embedding;
|
|
6464
6702
|
throw new Error("No embedding data received from OpenAI");
|
|
6465
6703
|
}
|
|
6466
6704
|
/**
|
|
@@ -6491,8 +6729,6 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
|
|
|
6491
6729
|
*/
|
|
6492
6730
|
async generateEmbeddingBatch(texts, tokenCounts) {
|
|
6493
6731
|
if (texts.length === 0) return [];
|
|
6494
|
-
const MAX_INPUTS_PER_REQUEST = 2048;
|
|
6495
|
-
const MAX_TOKENS_PER_INPUT = 8192;
|
|
6496
6732
|
let tokens;
|
|
6497
6733
|
let needsRecalculation = false;
|
|
6498
6734
|
if (!tokenCounts || tokenCounts.length !== texts.length) needsRecalculation = true;
|
|
@@ -6507,12 +6743,12 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
|
|
|
6507
6743
|
let totalTokens = 0;
|
|
6508
6744
|
for (let i = 0; i < texts.length; i++) {
|
|
6509
6745
|
const tokenCount = tokens[i];
|
|
6510
|
-
if (tokenCount >
|
|
6746
|
+
if (tokenCount > 8192) throw new Error(`Input at index ${i} exceeds ${OPENAI_MAX_TOKENS_PER_INPUT} token limit (${tokenCount} tokens)`);
|
|
6511
6747
|
totalTokens += tokenCount;
|
|
6512
6748
|
}
|
|
6513
6749
|
Logger.globalInstance.debug(`[OpenAI] Batch embedding: ${texts.length} inputs, ${totalTokens} total tokens`);
|
|
6514
|
-
const batches = this.createBatches(texts, tokens,
|
|
6515
|
-
Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${
|
|
6750
|
+
const batches = this.createBatches(texts, tokens, OPENAI_MAX_INPUTS_PER_REQUEST, OPENAI_EFFECTIVE_TOKEN_LIMIT);
|
|
6751
|
+
Logger.globalInstance.debug(`[OpenAI] Split into ${batches.length} batch(es) (effective limit: ${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens)`);
|
|
6516
6752
|
if (batches.length === 1) return await this.processSingleBatch(batches[0].texts);
|
|
6517
6753
|
const allEmbeddings = new Array(texts.length);
|
|
6518
6754
|
for (const batch of batches) (await this.processSingleBatch(batch.texts)).forEach((embedding, batchIndex) => {
|
|
@@ -6578,8 +6814,8 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
|
|
|
6578
6814
|
async processSingleBatch(texts, preCalculatedTokens) {
|
|
6579
6815
|
const tokenCounts = preCalculatedTokens || await this.calculateTokenCounts(texts);
|
|
6580
6816
|
const batchTokens = tokenCounts.reduce((sum, count) => sum + count, 0);
|
|
6581
|
-
if (batchTokens >
|
|
6582
|
-
Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${
|
|
6817
|
+
if (batchTokens > 27e4) {
|
|
6818
|
+
Logger.globalInstance.warn(`[OpenAI] Batch exceeds effective token limit (${batchTokens}/${OPENAI_EFFECTIVE_TOKEN_LIMIT} tokens), splitting recursively`);
|
|
6583
6819
|
const mid = Math.ceil(texts.length / 2);
|
|
6584
6820
|
const firstHalf = texts.slice(0, mid);
|
|
6585
6821
|
const secondHalf = texts.slice(mid);
|
|
@@ -6588,13 +6824,14 @@ var OpenAIEmbeddingService = class OpenAIEmbeddingService {
|
|
|
6588
6824
|
const [firstEmbeddings, secondEmbeddings] = await Promise.all([this.processSingleBatch(firstHalf, firstTokens), this.processSingleBatch(secondHalf, secondTokens)]);
|
|
6589
6825
|
return [...firstEmbeddings, ...secondEmbeddings];
|
|
6590
6826
|
}
|
|
6591
|
-
for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's
|
|
6827
|
+
for (let i = 0; i < tokenCounts.length; i++) if (tokenCounts[i] > 8192) throw new Error(`Text at index ${i} exceeds OpenAI's ${OPENAI_MAX_TOKENS_PER_INPUT} token limit per input (${tokenCounts[i]} tokens). This indicates a data integrity issue - chunk should have been smaller. This chunk cannot be processed and the entire batch must fail.`);
|
|
6592
6828
|
try {
|
|
6593
|
-
const response = await this.client.embeddings.create({
|
|
6829
|
+
const { data: body, response: httpResponse } = await this.client.embeddings.create({
|
|
6594
6830
|
model: this.model,
|
|
6595
6831
|
input: texts
|
|
6596
|
-
});
|
|
6597
|
-
|
|
6832
|
+
}).withResponse();
|
|
6833
|
+
this.recordRateLimit(httpResponse);
|
|
6834
|
+
if (body.data && body.data.length > 0) return body.data.sort((a, b) => a.index - b.index).map((item) => item.embedding);
|
|
6598
6835
|
else throw new Error("No embedding data received from OpenAI");
|
|
6599
6836
|
} catch (error) {
|
|
6600
6837
|
if (error instanceof OpenAI.AuthenticationError) throw this.toActionableAuthError(error);
|
|
@@ -8116,6 +8353,50 @@ async function fetchWithoutRedirects(url, timeoutMs) {
|
|
|
8116
8353
|
validateStatus: (status) => status >= 200 && status < 300 || status >= 300 && status < 400
|
|
8117
8354
|
});
|
|
8118
8355
|
}
|
|
8356
|
+
const BLOCK_LEVEL_SELECTOR = `*:not(${"a, span, em, strong, b, i, u, code, kbd, samp, var, sub, sup, small, abbr, cite, q, time, mark, s, del, ins, bdi, bdo, wbr, ruby, rt, rp".split(", ").join("):not(")}):not(td):not(th)`;
|
|
8357
|
+
/**
|
|
8358
|
+
* Extract readable text from the WHOLE document, not just `<p>` elements. The single collector
|
|
8359
|
+
* this replaced was `<p>`-only and fell back to the raw HTML when it found none: on a page whose
|
|
8360
|
+
* content isn't inside `<p>` (an RFC page using `<pre>`) that meant the fallback fired and stored
|
|
8361
|
+
* markup verbatim; on a page with real substance in headings, list items, table cells or code
|
|
8362
|
+
* blocks alongside its `<p>`s, that content was silently dropped.
|
|
8363
|
+
*
|
|
8364
|
+
* `head` (title/meta/script/style all live there, and the caller already reads `<title>`
|
|
8365
|
+
* separately) plus any stray `script`/`style`/`noscript` outside it are removed before extraction,
|
|
8366
|
+
* so none of that reaches what gets embedded. `<pre>` content is pulled out and stashed BEFORE the
|
|
8367
|
+
* rest of the document is collapsed, and spliced back in verbatim afterward - it needs to skip the
|
|
8368
|
+
* whitespace-collapse below (a code block's leading-space indentation is meaningful, unlike prose
|
|
8369
|
+
* whitespace) but still needs to land in the right place relative to everything else. Table cells
|
|
8370
|
+
* get a trailing space (still the same row, but no longer jammed into the next cell's word); every
|
|
8371
|
+
* other block-level element gets a trailing newline; runs of whitespace and blank lines are then
|
|
8372
|
+
* collapsed. Returns `''` when nothing extractable was found, so the caller stores nothing rather
|
|
8373
|
+
* than falling back to raw HTML.
|
|
8374
|
+
*/
|
|
8375
|
+
function extractReadableText($) {
|
|
8376
|
+
$("head, script, style, noscript").remove();
|
|
8377
|
+
$("br").replaceWith("\n");
|
|
8378
|
+
const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
8379
|
+
const markerFor = (index) => `\uE000PRE${nonce}_${index}\uE000`;
|
|
8380
|
+
const markerPattern = new RegExp(`\\uE000PRE${nonce}_(\\d+)\\uE000`, "g");
|
|
8381
|
+
const preBlocks = [];
|
|
8382
|
+
$("pre").each((_index, element) => {
|
|
8383
|
+
const text = $(element).text();
|
|
8384
|
+
if (text) {
|
|
8385
|
+
preBlocks.push(text);
|
|
8386
|
+
$(element).replaceWith(`${markerFor(preBlocks.length - 1)}\n`);
|
|
8387
|
+
} else $(element).remove();
|
|
8388
|
+
});
|
|
8389
|
+
$("td, th").each((_index, cell) => {
|
|
8390
|
+
$(cell).after(" ");
|
|
8391
|
+
});
|
|
8392
|
+
$(BLOCK_LEVEL_SELECTOR).each((_index, element) => {
|
|
8393
|
+
$(element).after("\n");
|
|
8394
|
+
});
|
|
8395
|
+
return $.root().text().split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter(Boolean).join("\n").replace(markerPattern, (match, indexStr) => {
|
|
8396
|
+
const index = Number(indexStr);
|
|
8397
|
+
return index >= 0 && index < preBlocks.length ? preBlocks[index] : match;
|
|
8398
|
+
});
|
|
8399
|
+
}
|
|
8119
8400
|
async function fetchAndParseURL(url, { logger }) {
|
|
8120
8401
|
logger.updateMetadata({ failedUrl: null });
|
|
8121
8402
|
try {
|
|
@@ -8149,16 +8430,13 @@ async function fetchAndParseURL(url, { logger }) {
|
|
|
8149
8430
|
const htmlContent = body.toString("utf8");
|
|
8150
8431
|
const $ = cheerio.load(htmlContent);
|
|
8151
8432
|
title = $("title").text() || lastPathSegment(currentUrl);
|
|
8152
|
-
|
|
8153
|
-
$("body").find("p").each((index, element) => {
|
|
8154
|
-
textContent += $(element).text() + "\n";
|
|
8155
|
-
});
|
|
8156
|
-
urlContent = textContent || htmlContent;
|
|
8433
|
+
urlContent = extractReadableText($);
|
|
8157
8434
|
}
|
|
8158
8435
|
const original = redactUrlCredentials(url);
|
|
8159
8436
|
const final = redactUrlCredentials(currentUrl);
|
|
8160
8437
|
const fetched = original === final ? original : `${original} -> ${final}`;
|
|
8161
|
-
logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
|
|
8438
|
+
if (urlContent === "") logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}, but no extractable text was found`);
|
|
8439
|
+
else logger.log(`Fetched ${title} with mimetype ${urlMimeType} and parsed ${fetched}`);
|
|
8162
8440
|
return {
|
|
8163
8441
|
title,
|
|
8164
8442
|
textContent: urlContent,
|
|
@@ -12901,10 +13179,117 @@ const vm = require('node:vm');
|
|
|
12901
13179
|
const STDOUT_HEAD_BYTES = ${5e3};
|
|
12902
13180
|
const STDOUT_TAIL_BYTES = ${2e3};
|
|
12903
13181
|
const HARD_PER_LINE_BYTES = ${5e4};
|
|
13182
|
+
const MIRROR_TAIL_FLUSH_MS = ${100};
|
|
12904
13183
|
|
|
12905
13184
|
let stdoutChunks = [];
|
|
12906
|
-
let stdoutBytes = 0;
|
|
12907
13185
|
let truncated = false;
|
|
13186
|
+
// --- Mirror state --------------------------------------------------------
|
|
13187
|
+
// Mirrors this run's stdout to the main thread as it is produced, in the same
|
|
13188
|
+
// head + marker + tail shape collectStdout() produces, so a retired run and a
|
|
13189
|
+
// completed one report the same thing by the same rule.
|
|
13190
|
+
//
|
|
13191
|
+
// The two halves are cost-bounded differently. The head is mirrored line by
|
|
13192
|
+
// line, so a chatty loop stops paying per line once the head is full. Past
|
|
13193
|
+
// that the tail is kept locally in a rolling window and posted on a timer, so
|
|
13194
|
+
// the message rate stops tracking the line rate entirely.
|
|
13195
|
+
let currentRunId = null;
|
|
13196
|
+
let mirroredHeadBytes = 0;
|
|
13197
|
+
let headMirrorFull = false;
|
|
13198
|
+
let tailChunks = [];
|
|
13199
|
+
let tailBytes = 0;
|
|
13200
|
+
let elidedBytes = 0;
|
|
13201
|
+
let tailFlushTimer = null;
|
|
13202
|
+
|
|
13203
|
+
/**
|
|
13204
|
+
* What the rolling tail may hold: whatever the head did not use of the same
|
|
13205
|
+
* HEAD + TAIL total collectStdout() reports within. A fixed TAIL budget made
|
|
13206
|
+
* the two disagree whenever the head came up short - a single 6KB first line
|
|
13207
|
+
* does not fit the head, so the mirror would have kept 2KB of a run that
|
|
13208
|
+
* collectStdout() reports whole, and called it truncated. mirroredHeadBytes
|
|
13209
|
+
* is frozen once the head is full, so this is stable for the rest of the run.
|
|
13210
|
+
*/
|
|
13211
|
+
function tailBudget() {
|
|
13212
|
+
return STDOUT_HEAD_BYTES + STDOUT_TAIL_BYTES - mirroredHeadBytes;
|
|
13213
|
+
}
|
|
13214
|
+
|
|
13215
|
+
function postToMain(msg) {
|
|
13216
|
+
try { parentPort.postMessage(msg); } catch { /* worker being torn down; nothing to preserve */ }
|
|
13217
|
+
}
|
|
13218
|
+
function cancelTailFlush() {
|
|
13219
|
+
if (tailFlushTimer === null) return;
|
|
13220
|
+
clearTimeout(tailFlushTimer);
|
|
13221
|
+
tailFlushTimer = null;
|
|
13222
|
+
}
|
|
13223
|
+
function flushTail() {
|
|
13224
|
+
if (currentRunId === null || !headMirrorFull) return;
|
|
13225
|
+
const joined = tailChunks.join('\n');
|
|
13226
|
+
// The rolling window is trimmed line by line, so it can only exceed the
|
|
13227
|
+
// budget by holding ONE line longer than the whole budget. Slice to the
|
|
13228
|
+
// same last-N-chars rule collectStdout() uses, which both matches that
|
|
13229
|
+
// path and keeps the flush payload bounded - a guest printing 50KB lines
|
|
13230
|
+
// would otherwise re-send 50KB on every tick.
|
|
13231
|
+
const overflow = Math.max(0, joined.length - tailBudget());
|
|
13232
|
+
postToMain({
|
|
13233
|
+
type: 'stdoutTail',
|
|
13234
|
+
id: currentRunId,
|
|
13235
|
+
tail: overflow > 0 ? joined.slice(overflow) : joined,
|
|
13236
|
+
// Counted from what was actually DROPPED - lines the rolling window
|
|
13237
|
+
// evicted, plus whatever this payload's own slice cuts - rather than
|
|
13238
|
+
// derived from the byte totals. The derived form read zero on the first
|
|
13239
|
+
// flush by construction (every line was still in the head, so the
|
|
13240
|
+
// subtraction cancelled), which made "truncated" unreportable on exactly
|
|
13241
|
+
// the run the mirror exists for.
|
|
13242
|
+
elidedBytes: elidedBytes + overflow,
|
|
13243
|
+
});
|
|
13244
|
+
}
|
|
13245
|
+
function scheduleTailFlush() {
|
|
13246
|
+
if (tailFlushTimer !== null) return;
|
|
13247
|
+
tailFlushTimer = setTimeout(() => {
|
|
13248
|
+
tailFlushTimer = null;
|
|
13249
|
+
flushTail();
|
|
13250
|
+
}, MIRROR_TAIL_FLUSH_MS);
|
|
13251
|
+
}
|
|
13252
|
+
function mirrorLine(capped) {
|
|
13253
|
+
if (currentRunId === null) return;
|
|
13254
|
+
if (!headMirrorFull) {
|
|
13255
|
+
// Does THIS line fit, rather than "is the running total already over".
|
|
13256
|
+
// Both of the orderings tried before this were wrong in one direction
|
|
13257
|
+
// each: gating on the running total let one line of up to
|
|
13258
|
+
// HARD_PER_LINE_BYTES past a 5KB budget (mirrored head ~55KB, disagreeing
|
|
13259
|
+
// with collectStdout's head and with the "~7K chars" codeExecuteTool
|
|
13260
|
+
// advertises to the model), while adding first and checking after moved
|
|
13261
|
+
// the boundary but kept the crossing line in the head - so the tail was
|
|
13262
|
+
// still empty at the immediate flush below and a run killed right there
|
|
13263
|
+
// dropped the last line before the hang and reported itself complete.
|
|
13264
|
+
//
|
|
13265
|
+
// A fit check does both: the head stops at STDOUT_HEAD_BYTES exactly, and
|
|
13266
|
+
// the line that did not fit STARTS the tail, so the flush that fires on
|
|
13267
|
+
// this same call carries it.
|
|
13268
|
+
if (mirroredHeadBytes + capped.length + 1 <= STDOUT_HEAD_BYTES) {
|
|
13269
|
+
mirroredHeadBytes += capped.length + 1;
|
|
13270
|
+
postToMain({ type: 'stdout', id: currentRunId, chunk: capped });
|
|
13271
|
+
return;
|
|
13272
|
+
}
|
|
13273
|
+
headMirrorFull = true;
|
|
13274
|
+
tailChunks.push(capped);
|
|
13275
|
+
tailBytes += capped.length + 1;
|
|
13276
|
+
// Post once immediately: a run killed before the first timed flush would
|
|
13277
|
+
// otherwise report a short mirror as if it were complete.
|
|
13278
|
+
flushTail();
|
|
13279
|
+
return;
|
|
13280
|
+
}
|
|
13281
|
+
tailChunks.push(capped);
|
|
13282
|
+
tailBytes += capped.length + 1;
|
|
13283
|
+
// Never evict the only line held: a line larger than the whole budget is
|
|
13284
|
+
// still the last thing the run printed, which is what the mirror is for.
|
|
13285
|
+
const budget = tailBudget();
|
|
13286
|
+
while (tailBytes > budget && tailChunks.length > 1) {
|
|
13287
|
+
const dropped = tailChunks.shift();
|
|
13288
|
+
tailBytes -= dropped.length + 1;
|
|
13289
|
+
elidedBytes += dropped.length + 1;
|
|
13290
|
+
}
|
|
13291
|
+
scheduleTailFlush();
|
|
13292
|
+
}
|
|
12908
13293
|
function captureLine(args) {
|
|
12909
13294
|
const line = args.map(a => {
|
|
12910
13295
|
if (typeof a === 'string') return a;
|
|
@@ -12916,7 +13301,7 @@ function captureLine(args) {
|
|
|
12916
13301
|
? line.slice(0, HARD_PER_LINE_BYTES) + ' [...line truncated]'
|
|
12917
13302
|
: line;
|
|
12918
13303
|
stdoutChunks.push(capped);
|
|
12919
|
-
|
|
13304
|
+
mirrorLine(capped);
|
|
12920
13305
|
}
|
|
12921
13306
|
function jsonReplacer(_k, v) {
|
|
12922
13307
|
if (v instanceof Error) return { name: v.name, message: v.message };
|
|
@@ -13017,7 +13402,11 @@ parentPort.on('message', async (msg) => {
|
|
|
13017
13402
|
}
|
|
13018
13403
|
if (msg.type === 'runCode') {
|
|
13019
13404
|
const t0 = Date.now();
|
|
13020
|
-
stdoutChunks = [];
|
|
13405
|
+
stdoutChunks = []; truncated = false;
|
|
13406
|
+
cancelTailFlush();
|
|
13407
|
+
currentRunId = msg.id;
|
|
13408
|
+
mirroredHeadBytes = 0; headMirrorFull = false;
|
|
13409
|
+
tailChunks = []; tailBytes = 0; elidedBytes = 0;
|
|
13021
13410
|
let error = null;
|
|
13022
13411
|
const wrapped = '(async () => {\n' + msg.code + '\n})()';
|
|
13023
13412
|
try {
|
|
@@ -13029,6 +13418,11 @@ parentPort.on('message', async (msg) => {
|
|
|
13029
13418
|
} catch (e) {
|
|
13030
13419
|
error = serializeError(e);
|
|
13031
13420
|
}
|
|
13421
|
+
// Stop mirroring before the authoritative result goes out, so a late
|
|
13422
|
+
// console.log from an abandoned continuation cannot attach to this run,
|
|
13423
|
+
// and a pending tail flush cannot land after it.
|
|
13424
|
+
currentRunId = null;
|
|
13425
|
+
cancelTailFlush();
|
|
13032
13426
|
parentPort.postMessage({
|
|
13033
13427
|
type: 'runResult',
|
|
13034
13428
|
id: msg.id,
|
|
@@ -13042,6 +13436,17 @@ parentPort.on('message', async (msg) => {
|
|
|
13042
13436
|
});
|
|
13043
13437
|
`;
|
|
13044
13438
|
String.raw`
|
|
13439
|
+
// Wrapped in an IIFE deliberately. A script's top-level const/let bind into the
|
|
13440
|
+
// context's SHARED global lexical scope (and its function declarations become
|
|
13441
|
+
// globalThis properties), so without this wrapper every bootstrap-local name is
|
|
13442
|
+
// directly referenceable by LLM-authored code run later in the same context:
|
|
13443
|
+
// __RealFunction('...')() walks straight around the codegen block below, and
|
|
13444
|
+
// __cap.applySync(...) / __cap.release() forges or permanently kills stdout
|
|
13445
|
+
// capture. Function scope keeps them unreachable. Note the leak is invisible to
|
|
13446
|
+
// listGlobals(), which reads Object.getOwnPropertyNames(globalThis) and never
|
|
13447
|
+
// saw the lexical bindings - so RESERVED_GLOBAL_NAMES cannot backstop it either.
|
|
13448
|
+
// Anything guest code IS meant to see is assigned onto globalThis explicitly.
|
|
13449
|
+
(function () {
|
|
13045
13450
|
const __cap = _captureLine;
|
|
13046
13451
|
const __callTool = _callTool;
|
|
13047
13452
|
delete globalThis._captureLine;
|
|
@@ -13049,28 +13454,68 @@ delete globalThis._callTool;
|
|
|
13049
13454
|
|
|
13050
13455
|
const HARD_PER_LINE_BYTES = ${5e4};
|
|
13051
13456
|
|
|
13457
|
+
// Every intrinsic the formatter below reaches for is captured HERE, while the
|
|
13458
|
+
// context is still pristine. Resolving \`args.map\` / \`.join\` / \`line.slice\`
|
|
13459
|
+
// at CALL time walks a prototype chain the guest owns, so one
|
|
13460
|
+
// \`Array.prototype.join = () => 'X'\` - deliberate, or an innocent polyfill -
|
|
13461
|
+
// forges every stdout line for the rest of the session, and the run still
|
|
13462
|
+
// reports error=null / truncated=false. That is the same integrity failure the
|
|
13463
|
+
// frozen \`console\` below exists to prevent, one level down: freezing the
|
|
13464
|
+
// binding is worthless if the formatter behind it is guest-reachable.
|
|
13465
|
+
const __stringify = JSON.stringify;
|
|
13466
|
+
const __String = String;
|
|
13467
|
+
const __apply = Reflect.apply;
|
|
13468
|
+
const __strSlice = String.prototype.slice;
|
|
13469
|
+
|
|
13052
13470
|
function __jsonReplacer(_k, v) {
|
|
13053
13471
|
if (v instanceof Error) return { name: v.name, message: v.message };
|
|
13054
13472
|
if (typeof v === 'bigint') return v.toString() + 'n';
|
|
13055
13473
|
return v;
|
|
13056
13474
|
}
|
|
13475
|
+
// Indexed loop and \`+=\` rather than map/join: string concatenation is an
|
|
13476
|
+
// operator, not a lookup, so there is nothing here for the guest to replace.
|
|
13477
|
+
// What a guest CAN still steer is how its own values render - a \`toJSON\` or
|
|
13478
|
+
// \`toString\` on the object it passed - which is content it already owns, not
|
|
13479
|
+
// the channel.
|
|
13057
13480
|
function __formatLine(args) {
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
if (
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13481
|
+
let line = '';
|
|
13482
|
+
for (let i = 0; i < args.length; i++) {
|
|
13483
|
+
if (i > 0) line += ' ';
|
|
13484
|
+
const a = args[i];
|
|
13485
|
+
if (typeof a === 'string') { line += a; continue; }
|
|
13486
|
+
if (a === undefined) { line += 'undefined'; continue; }
|
|
13487
|
+
if (a === null) { line += 'null'; continue; }
|
|
13488
|
+
try { line += __stringify(a, __jsonReplacer, 2); } catch { line += __String(a); }
|
|
13489
|
+
}
|
|
13064
13490
|
return line.length > HARD_PER_LINE_BYTES
|
|
13065
|
-
? line
|
|
13491
|
+
? __apply(__strSlice, line, [0, HARD_PER_LINE_BYTES]) + ' [...line truncated]'
|
|
13066
13492
|
: line;
|
|
13067
13493
|
}
|
|
13068
|
-
|
|
13494
|
+
// stdout is the channel the HOST reports back as the run's observation, so its
|
|
13495
|
+
// integrity is ours, not the guest's. A plain assignment left \`console\`
|
|
13496
|
+
// writable and configurable: guest code could set globalThis.console = {log(){}}
|
|
13497
|
+
// (or just reassign console.log) and every later run in the session would come
|
|
13498
|
+
// back with stdout="" or forged lines, error=null, and a clean listGlobals().
|
|
13499
|
+
// Frozen object + non-writable, non-configurable property: the guest's
|
|
13500
|
+
// assignment is a silent no-op in sloppy mode and a TypeError under 'use
|
|
13501
|
+
// strict', and either way capture keeps working.
|
|
13502
|
+
//
|
|
13503
|
+
// The BINDING is what this protects, and the binding is only half of it: a
|
|
13504
|
+
// frozen console whose formatter resolved its intrinsics at call time would
|
|
13505
|
+
// still hand the guest every line. That half is closed above, where
|
|
13506
|
+
// __formatLine captures what it needs.
|
|
13507
|
+
const __console = Object.freeze({
|
|
13069
13508
|
log: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
|
|
13070
13509
|
warn: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
|
|
13071
13510
|
error: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
|
|
13072
13511
|
info: (...a) => __cap.applySync(undefined, [__formatLine(a)], { arguments: { copy: true } }),
|
|
13073
|
-
};
|
|
13512
|
+
});
|
|
13513
|
+
Object.defineProperty(globalThis, 'console', {
|
|
13514
|
+
value: __console,
|
|
13515
|
+
writable: false,
|
|
13516
|
+
configurable: false,
|
|
13517
|
+
enumerable: true,
|
|
13518
|
+
});
|
|
13074
13519
|
|
|
13075
13520
|
// A bare isolate has no structuredClone (it's a host/web API, not a V8
|
|
13076
13521
|
// intrinsic). The in-process + worker backends expose the *host's* real
|
|
@@ -13158,11 +13603,33 @@ for (const __Ctor of [__RealFunction, __AsyncFunction, __GeneratorFunction, __As
|
|
|
13158
13603
|
globalThis.eval = __blockCodegen;
|
|
13159
13604
|
globalThis.Function = __blockCodegen;
|
|
13160
13605
|
|
|
13606
|
+
// WebAssembly is removed, not stubbed. Its compile/instantiate promises never
|
|
13607
|
+
// settle inside an isolated-vm isolate (there is no host task runner to drive
|
|
13608
|
+
// them), so \`await WebAssembly.instantiate(...)\` is a one-line way for guest
|
|
13609
|
+
// code to park a run until the host deadline fires - and that deadline kills
|
|
13610
|
+
// the isolate, costing the whole session its sandbox. Deleting it turns that
|
|
13611
|
+
// into an immediate ReferenceError. It is also codegen-from-bytes, so it
|
|
13612
|
+
// belongs on the same side of the line as eval / Function anyway.
|
|
13613
|
+
delete globalThis.WebAssembly;
|
|
13614
|
+
|
|
13161
13615
|
// Tool-stub registry. Each registered tool becomes a top-level async
|
|
13162
13616
|
// function that round-trips through the host dispatcher and re-throws on
|
|
13163
13617
|
// the { ok:false } envelope.
|
|
13618
|
+
//
|
|
13619
|
+
// Assigned to globalThis only so the constructor can lift a Reference to it;
|
|
13620
|
+
// the constructor deletes the global immediately afterwards and calls it
|
|
13621
|
+
// through that Reference forever after. It must NOT stay guest-reachable: a
|
|
13622
|
+
// guest could call __registerTools(['console']) to overwrite the frozen
|
|
13623
|
+
// console binding with a tool stub, or \`delete\` it and make the host's next
|
|
13624
|
+
// setTools() throw.
|
|
13625
|
+
//
|
|
13626
|
+
// Indexed loop, not for..of, deliberately: the host calls this with a copied
|
|
13627
|
+
// array whose iterator comes from the GUEST's Array.prototype, so an
|
|
13628
|
+
// overridden Symbol.iterator would let guest code hang or hijack a host-side
|
|
13629
|
+
// setTools() call. Indexing touches only the copy's own properties.
|
|
13164
13630
|
globalThis.__registerTools = function (names) {
|
|
13165
|
-
for (
|
|
13631
|
+
for (let i = 0; i < names.length; i++) {
|
|
13632
|
+
const name = names[i];
|
|
13166
13633
|
globalThis[name] = async (...args) => {
|
|
13167
13634
|
const envJson = await __callTool.apply(
|
|
13168
13635
|
undefined,
|
|
@@ -13175,6 +13642,7 @@ globalThis.__registerTools = function (names) {
|
|
|
13175
13642
|
};
|
|
13176
13643
|
}
|
|
13177
13644
|
};
|
|
13645
|
+
})();
|
|
13178
13646
|
`;
|
|
13179
13647
|
z$1.object({
|
|
13180
13648
|
reflection: z$1.string().min(1),
|
|
@@ -15467,6 +15935,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
|
|
|
15467
15935
|
AnthropicBackend: () => AnthropicBackend,
|
|
15468
15936
|
AnthropicBatchService: () => AnthropicBatchService,
|
|
15469
15937
|
AnthropicBedrockBackend: () => AnthropicBedrockBackend,
|
|
15938
|
+
BEDROCK_REQUEST_HANDLER: () => BEDROCK_REQUEST_HANDLER,
|
|
15470
15939
|
BFLBackend: () => BFLBackend,
|
|
15471
15940
|
BaseBedrockBackend: () => BaseBedrockBackend,
|
|
15472
15941
|
ChoiceEndReason: () => ChoiceEndReason,
|
|
@@ -15524,6 +15993,7 @@ var dist_exports = /* @__PURE__ */ __exportAll$3({
|
|
|
15524
15993
|
logExpiringModels: () => logExpiringModels,
|
|
15525
15994
|
mergeCatalog: () => mergeCatalog,
|
|
15526
15995
|
mergeCatalogWithDrops: () => mergeCatalogWithDrops,
|
|
15996
|
+
normalizeToolUseInputs: () => normalizeToolUseInputs,
|
|
15527
15997
|
reasonsWithinOutputBudget: () => reasonsWithinOutputBudget,
|
|
15528
15998
|
recordDeprecatedModelRequest: () => recordDeprecatedModelRequest,
|
|
15529
15999
|
replaceLastToolResultObservationCanonical: () => replaceLastToolResultObservationCanonical,
|
|
@@ -15898,27 +16368,112 @@ const stripAllToolBlocks = (messages, logger) => {
|
|
|
15898
16368
|
return result;
|
|
15899
16369
|
};
|
|
15900
16370
|
/**
|
|
16371
|
+
* Restores `input: {}` on any `tool_use` block that reached us without one.
|
|
16372
|
+
*
|
|
16373
|
+
* `MessageContentToolUse.input` is non-optional in the type system, so nothing upstream checks it -
|
|
16374
|
+
* but a message that round-trips through a persistence or serialization layer can lose it. The
|
|
16375
|
+
* known offender is Mongoose's default `minimize`, which deletes empty objects on
|
|
16376
|
+
* `toObject()`/`toJSON()`: a zero-argument tool call (`current_datetime`, `mission_status`, ...)
|
|
16377
|
+
* stores `input: {}` and reads back with the key gone. Anthropic then rejects the whole request
|
|
16378
|
+
* with "messages.N.content.M.tool_use.input: Field required", killing a resumed agent run or a
|
|
16379
|
+
* chat turn that replays history.
|
|
16380
|
+
*
|
|
16381
|
+
* The schema that caused it is fixed at the source (`minimize: false` on AgentExecutionModel), so
|
|
16382
|
+
* this is the last line of defense for every other store that replays blocks verbatim -
|
|
16383
|
+
* `QuestModel.structuredReplies[].content` is the same Mixed-under-default-minimize shape and is
|
|
16384
|
+
* deliberately covered here rather than by widening that hot collection's schema. The cost of a
|
|
16385
|
+
* miss is a hard 400, and `{}` is the only value a zero-argument call could have had.
|
|
16386
|
+
*
|
|
16387
|
+
* Returns the input array unchanged (same reference) when nothing needed repair.
|
|
16388
|
+
*/
|
|
16389
|
+
const normalizeToolUseInputs = (messages, logger) => {
|
|
16390
|
+
let repaired = 0;
|
|
16391
|
+
const result = messages.map((message) => {
|
|
16392
|
+
if (!Array.isArray(message.content)) return message;
|
|
16393
|
+
let messageChanged = false;
|
|
16394
|
+
const content = message.content.map((block) => {
|
|
16395
|
+
if (block.type !== "tool_use") return block;
|
|
16396
|
+
const toolUse = block;
|
|
16397
|
+
if (toolUse.input !== null && typeof toolUse.input === "object") return block;
|
|
16398
|
+
repaired++;
|
|
16399
|
+
messageChanged = true;
|
|
16400
|
+
return {
|
|
16401
|
+
...toolUse,
|
|
16402
|
+
input: {}
|
|
16403
|
+
};
|
|
16404
|
+
});
|
|
16405
|
+
return messageChanged ? {
|
|
16406
|
+
...message,
|
|
16407
|
+
content
|
|
16408
|
+
} : message;
|
|
16409
|
+
});
|
|
16410
|
+
if (repaired === 0) return messages;
|
|
16411
|
+
logger?.warn(`[Tool Input Repair] Restored empty input on ${repaired} tool_use block(s) that lost it in serialization`);
|
|
16412
|
+
return result;
|
|
16413
|
+
};
|
|
16414
|
+
/**
|
|
16415
|
+
* Anthropic's hard ceiling on `cache_control` markers per request. Exceeding it fails the
|
|
16416
|
+
* WHOLE request with `ValidationException: A maximum of 4 blocks with cache_control may be
|
|
16417
|
+
* provided`, which is non-retryable - so an over-budget request loses the turn outright,
|
|
16418
|
+
* after the user has already waited for it.
|
|
16419
|
+
*/
|
|
16420
|
+
const MAX_CACHE_CONTROL_BLOCKS = 4;
|
|
16421
|
+
/**
|
|
16422
|
+
* Does this block already carry a marker? Re-marking one costs no budget.
|
|
16423
|
+
*
|
|
16424
|
+
* Tests the VALUE, not just key presence: a block carrying an explicit
|
|
16425
|
+
* `cache_control: undefined` is not a marker as far as the provider is concerned, and counting
|
|
16426
|
+
* it would spend budget on nothing and drop a breakpoint we could have kept.
|
|
16427
|
+
*/
|
|
16428
|
+
function hasMarker(block) {
|
|
16429
|
+
return !!block && typeof block === "object" && !!block.cache_control;
|
|
16430
|
+
}
|
|
16431
|
+
/**
|
|
16432
|
+
* Markers already on the request. Callers upstream attach their own before this runs -
|
|
16433
|
+
* `bedrockBackend/anthropic.ts` marks each system block flagged `cache: true` (the mid-stack
|
|
16434
|
+
* shareable-prefix breakpoint) - so this adapter's budget is whatever they left, not the full four.
|
|
16435
|
+
*/
|
|
16436
|
+
function censusMarkers(params) {
|
|
16437
|
+
const tools = Array.isArray(params.tools) ? params.tools.filter(hasMarker).length : 0;
|
|
16438
|
+
const system = Array.isArray(params.system) ? params.system.filter(hasMarker).length : 0;
|
|
16439
|
+
let messages = 0;
|
|
16440
|
+
if (Array.isArray(params.messages)) for (const message of params.messages) {
|
|
16441
|
+
const content = message?.content;
|
|
16442
|
+
if (Array.isArray(content)) messages += content.filter(hasMarker).length;
|
|
16443
|
+
}
|
|
16444
|
+
return {
|
|
16445
|
+
tools,
|
|
16446
|
+
system,
|
|
16447
|
+
messages,
|
|
16448
|
+
total: tools + system + messages
|
|
16449
|
+
};
|
|
16450
|
+
}
|
|
16451
|
+
/**
|
|
15901
16452
|
* Anthropic-specific caching adapter
|
|
15902
16453
|
* Adds explicit cache_control markers to content blocks
|
|
15903
16454
|
*/
|
|
15904
16455
|
var AnthropicCachingAdapter = class {
|
|
15905
|
-
applyCaching(apiParams, strategy) {
|
|
16456
|
+
applyCaching(apiParams, strategy, logger) {
|
|
15906
16457
|
if (!strategy.enableCaching) return apiParams;
|
|
15907
16458
|
const ttl = strategy.cacheTTL ?? "5m";
|
|
15908
16459
|
const modifiedParams = { ...apiParams };
|
|
15909
|
-
const
|
|
15910
|
-
|
|
15911
|
-
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
16460
|
+
const cacheControl = {
|
|
16461
|
+
type: "ephemeral",
|
|
16462
|
+
...ttl === "1h" ? { ttl } : {}
|
|
16463
|
+
};
|
|
16464
|
+
const inbound = censusMarkers(modifiedParams);
|
|
16465
|
+
let budget = MAX_CACHE_CONTROL_BLOCKS - inbound.total;
|
|
16466
|
+
const dropped = [];
|
|
16467
|
+
/** Claim one marker slot, or record the miss. Re-marking a marked block is free. */
|
|
16468
|
+
const claim = (name, alreadyMarked) => {
|
|
16469
|
+
if (alreadyMarked) return true;
|
|
16470
|
+
if (budget <= 0) {
|
|
16471
|
+
dropped.push(name);
|
|
16472
|
+
return false;
|
|
16473
|
+
}
|
|
16474
|
+
budget -= 1;
|
|
16475
|
+
return true;
|
|
16476
|
+
};
|
|
15922
16477
|
const systemParam = modifiedParams.system;
|
|
15923
16478
|
if (strategy.cacheSystemPrompt && systemParam) {
|
|
15924
16479
|
const systemArray = Array.isArray(systemParam) ? [...systemParam] : [{
|
|
@@ -15927,14 +16482,13 @@ var AnthropicCachingAdapter = class {
|
|
|
15927
16482
|
}];
|
|
15928
16483
|
if (systemArray.length > 0) {
|
|
15929
16484
|
const lastBlock = systemArray[systemArray.length - 1];
|
|
15930
|
-
|
|
15931
|
-
|
|
15932
|
-
|
|
15933
|
-
|
|
15934
|
-
|
|
15935
|
-
|
|
15936
|
-
}
|
|
15937
|
-
modifiedParams.system = systemArray;
|
|
16485
|
+
if (claim("system", hasMarker(lastBlock))) {
|
|
16486
|
+
systemArray[systemArray.length - 1] = {
|
|
16487
|
+
...lastBlock,
|
|
16488
|
+
cache_control: cacheControl
|
|
16489
|
+
};
|
|
16490
|
+
modifiedParams.system = systemArray;
|
|
16491
|
+
}
|
|
15938
16492
|
}
|
|
15939
16493
|
}
|
|
15940
16494
|
const messagesParam = modifiedParams.messages;
|
|
@@ -15950,24 +16504,65 @@ var AnthropicCachingAdapter = class {
|
|
|
15950
16504
|
text: msgContent
|
|
15951
16505
|
}];
|
|
15952
16506
|
else if (Array.isArray(msgContent)) contentArray = [...msgContent];
|
|
15953
|
-
|
|
15954
|
-
if (contentArray.length > 0) {
|
|
16507
|
+
if (contentArray && contentArray.length > 0) {
|
|
15955
16508
|
const lastBlock = contentArray[contentArray.length - 1];
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
}
|
|
15967
|
-
modifiedParams.messages = messages;
|
|
16509
|
+
if (claim("history", hasMarker(lastBlock))) {
|
|
16510
|
+
contentArray[contentArray.length - 1] = {
|
|
16511
|
+
...lastBlock,
|
|
16512
|
+
cache_control: cacheControl
|
|
16513
|
+
};
|
|
16514
|
+
messages[anchorIndex] = {
|
|
16515
|
+
...anchorMsg,
|
|
16516
|
+
content: contentArray
|
|
16517
|
+
};
|
|
16518
|
+
modifiedParams.messages = messages;
|
|
16519
|
+
}
|
|
15968
16520
|
}
|
|
15969
16521
|
}
|
|
15970
16522
|
}
|
|
16523
|
+
const tools = modifiedParams.tools;
|
|
16524
|
+
if (strategy.cacheTools && Array.isArray(tools) && tools.length > 0) {
|
|
16525
|
+
const toolsCopy = [...tools];
|
|
16526
|
+
const lastTool = toolsCopy[toolsCopy.length - 1];
|
|
16527
|
+
if (claim("tools", hasMarker(lastTool))) {
|
|
16528
|
+
toolsCopy[toolsCopy.length - 1] = {
|
|
16529
|
+
...lastTool,
|
|
16530
|
+
cache_control: cacheControl
|
|
16531
|
+
};
|
|
16532
|
+
modifiedParams.tools = toolsCopy;
|
|
16533
|
+
}
|
|
16534
|
+
}
|
|
16535
|
+
const outbound = censusMarkers(modifiedParams);
|
|
16536
|
+
const census = {
|
|
16537
|
+
inbound,
|
|
16538
|
+
outbound,
|
|
16539
|
+
limit: MAX_CACHE_CONTROL_BLOCKS
|
|
16540
|
+
};
|
|
16541
|
+
if (outbound.total >= MAX_CACHE_CONTROL_BLOCKS) {
|
|
16542
|
+
const message = "[PromptCache] cache_control census at the ceiling";
|
|
16543
|
+
if (logger) logger.info(message, census);
|
|
16544
|
+
else console.info(message, JSON.stringify(census));
|
|
16545
|
+
} else if (logger) logger.debug("[PromptCache] cache_control census", census);
|
|
16546
|
+
if (outbound.total > MAX_CACHE_CONTROL_BLOCKS) {
|
|
16547
|
+
const message = `[PromptCache] request exceeds the ${MAX_CACHE_CONTROL_BLOCKS}-block cache_control limit on arrival (${outbound.total}); the provider will reject it`;
|
|
16548
|
+
const detail = {
|
|
16549
|
+
inbound,
|
|
16550
|
+
outbound,
|
|
16551
|
+
limit: MAX_CACHE_CONTROL_BLOCKS
|
|
16552
|
+
};
|
|
16553
|
+
if (logger) logger.error(message, detail);
|
|
16554
|
+
else console.error(message, JSON.stringify(detail));
|
|
16555
|
+
} else if (dropped.length > 0) {
|
|
16556
|
+
const message = `[PromptCache] cache_control budget exhausted (limit ${MAX_CACHE_CONTROL_BLOCKS}); skipped breakpoints: ${dropped.join(", ")}`;
|
|
16557
|
+
const detail = {
|
|
16558
|
+
dropped,
|
|
16559
|
+
inbound,
|
|
16560
|
+
outbound,
|
|
16561
|
+
limit: MAX_CACHE_CONTROL_BLOCKS
|
|
16562
|
+
};
|
|
16563
|
+
if (logger) logger.warn(message, detail);
|
|
16564
|
+
else console.warn(message, JSON.stringify(detail));
|
|
16565
|
+
}
|
|
15971
16566
|
return modifiedParams;
|
|
15972
16567
|
}
|
|
15973
16568
|
extractCacheStats(response, model) {
|
|
@@ -17095,7 +17690,7 @@ var AnthropicBackend = class {
|
|
|
17095
17690
|
const parts = [this.consolidateSystemMessages(messages), identityReminder].filter(Boolean);
|
|
17096
17691
|
system = parts.length > 0 ? parts.join("\n") : void 0;
|
|
17097
17692
|
}
|
|
17098
|
-
let filteredMessages = ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger);
|
|
17693
|
+
let filteredMessages = normalizeToolUseInputs(ensureToolPairingIntegrity(this.filterRelevantMessages(cacheStampedMessages), this.logger), this.logger);
|
|
17099
17694
|
const countToolBlocks = (msgs) => {
|
|
17100
17695
|
let useCount = 0;
|
|
17101
17696
|
let resultCount = 0;
|
|
@@ -17195,7 +17790,7 @@ var AnthropicBackend = class {
|
|
|
17195
17790
|
} else this.isThinkingEnabled = false;
|
|
17196
17791
|
const cacheStrategy = options.cacheStrategy;
|
|
17197
17792
|
if (cacheStrategy?.enableCaching) {
|
|
17198
|
-
const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy);
|
|
17793
|
+
const cachedParams = getCachingAdapter(ModelBackend.Anthropic).applyCaching(apiParams, cacheStrategy, this.logger);
|
|
17199
17794
|
Object.assign(apiParams, cachedParams);
|
|
17200
17795
|
this.logger.debug("[Anthropic] Applying cache control", {
|
|
17201
17796
|
cacheSystemPrompt: cacheStrategy.cacheSystemPrompt,
|
|
@@ -18219,6 +18814,11 @@ const BEDROCK_RETRY_CONFIG = {
|
|
|
18219
18814
|
maxAttempts: 6,
|
|
18220
18815
|
retryMode: "adaptive"
|
|
18221
18816
|
};
|
|
18817
|
+
const BEDROCK_REQUEST_HANDLER = {
|
|
18818
|
+
requestTimeout: 12e4,
|
|
18819
|
+
sessionTimeout: 13e4,
|
|
18820
|
+
disableConcurrentStreams: true
|
|
18821
|
+
};
|
|
18222
18822
|
/**
|
|
18223
18823
|
* Detect cancellation errors so they propagate past tool-error containment to
|
|
18224
18824
|
* the outer catch (which has dedicated abort handling). Without this, aborts
|
|
@@ -18260,7 +18860,8 @@ var BaseBedrockBackend = class {
|
|
|
18260
18860
|
};
|
|
18261
18861
|
this._bedrockRuntime = new BedrockRuntimeClient({
|
|
18262
18862
|
region: this._options.region,
|
|
18263
|
-
...BEDROCK_RETRY_CONFIG
|
|
18863
|
+
...BEDROCK_RETRY_CONFIG,
|
|
18864
|
+
requestHandler: BEDROCK_REQUEST_HANDLER
|
|
18264
18865
|
});
|
|
18265
18866
|
}
|
|
18266
18867
|
getRegionForModel(model) {
|
|
@@ -18290,12 +18891,31 @@ var BaseBedrockBackend = class {
|
|
|
18290
18891
|
takeReasoningBlocks() {
|
|
18291
18892
|
return [];
|
|
18292
18893
|
}
|
|
18894
|
+
/**
|
|
18895
|
+
* Whether this adapter's `translateStreamChunk` reports `done: true` ONLY on the provider's
|
|
18896
|
+
* terminal event. When true, complete() treats a stream that produced output but never
|
|
18897
|
+
* reported done as a TRUNCATED response and throws instead of returning the partial text.
|
|
18898
|
+
*
|
|
18899
|
+
* Opt-in rather than the default because "reports done terminally" is a per-adapter contract
|
|
18900
|
+
* the base class cannot infer, and getting it wrong turns every healthy completion into an
|
|
18901
|
+
* error. Three groups exist today:
|
|
18902
|
+
* - terminal-only, so they override this to true: anthropic, deepseek, llama, jurassicTwo
|
|
18903
|
+
* - `done: true` on EVERY content chunk, so the check would be inert: titan, moonshot
|
|
18904
|
+
* (the better fix for those is a stopReason passthrough, as moonshot.ts already does)
|
|
18905
|
+
* - never report done, incl. the test doubles in this directory: left false
|
|
18906
|
+
*
|
|
18907
|
+
* A new streaming backend must opt in deliberately; silence keeps the old behaviour.
|
|
18908
|
+
*/
|
|
18909
|
+
get signalsStreamTermination() {
|
|
18910
|
+
return false;
|
|
18911
|
+
}
|
|
18293
18912
|
updateClientForModel(model) {
|
|
18294
18913
|
const requiredRegion = this.getRegionForModel(model);
|
|
18295
18914
|
this._options.region = requiredRegion;
|
|
18296
18915
|
this._bedrockRuntime = new BedrockRuntimeClient({
|
|
18297
18916
|
region: this._options.region,
|
|
18298
|
-
...BEDROCK_RETRY_CONFIG
|
|
18917
|
+
...BEDROCK_RETRY_CONFIG,
|
|
18918
|
+
requestHandler: BEDROCK_REQUEST_HANDLER
|
|
18299
18919
|
});
|
|
18300
18920
|
}
|
|
18301
18921
|
async complete(model, messages, options, callback, toolsUsed = []) {
|
|
@@ -18403,9 +19023,11 @@ var BaseBedrockBackend = class {
|
|
|
18403
19023
|
if (!response.body) throw new Error("No response body");
|
|
18404
19024
|
const func = [];
|
|
18405
19025
|
let emittedTextChars = 0;
|
|
19026
|
+
let sawTerminalEvent = false;
|
|
18406
19027
|
for await (const streamEvent of response.body) if (streamEvent.chunk?.bytes) {
|
|
18407
19028
|
const json = new TextDecoder().decode(streamEvent.chunk.bytes);
|
|
18408
|
-
const { chunk } = this.translateStreamChunk(model, JSON.parse(json));
|
|
19029
|
+
const { done, chunk } = this.translateStreamChunk(model, JSON.parse(json));
|
|
19030
|
+
sawTerminalEvent ||= done;
|
|
18409
19031
|
if (chunk?.stopReason) stopReason = chunk.stopReason;
|
|
18410
19032
|
chunk?.choices?.forEach((choice) => {
|
|
18411
19033
|
func[choice.index] ||= {};
|
|
@@ -18429,6 +19051,7 @@ var BaseBedrockBackend = class {
|
|
|
18429
19051
|
await callback(streamedText, buildCompletionInfo());
|
|
18430
19052
|
}
|
|
18431
19053
|
if (emittedTextChars === 0 && !func.some((f) => f.name)) throw new Error(`[BaseBedrockBackend] model "${model}" returned an EMPTY response in region ${this._options.region} (no text, no tool call, no output tokens). A "global." cross-region inference profile served from a region that does not host it does exactly this - try the "us." variant, or confirm the model/profile is granted in ${this._options.region}.`);
|
|
19054
|
+
if (this.signalsStreamTermination && !sawTerminalEvent && !options.abortSignal?.aborted) throw new Error(`[BaseBedrockBackend] stream timeout - model "${model}" in region ${this._options.region} ended after ${emittedTextChars} chars without a terminal event, so the response is TRUNCATED. Usually a stalled Bedrock socket cut the stream short; the partial text is withheld deliberately rather than returned as a finished answer.`);
|
|
18432
19055
|
if (func.some((f) => f.name)) {
|
|
18433
19056
|
for await (const tool of func) {
|
|
18434
19057
|
const { id, name, parameters } = tool;
|
|
@@ -18733,6 +19356,10 @@ const TEMPERATURE_ONLY_MODELS = [
|
|
|
18733
19356
|
ChatModels.CLAUDE_4_6_OPUS_BEDROCK
|
|
18734
19357
|
];
|
|
18735
19358
|
var AnthropicBedrockBackend = class extends BaseBedrockBackend {
|
|
19359
|
+
/** Reports done only on message_stop (anthropic.ts translateStreamChunk), so a missing terminal event means a truncated stream. */
|
|
19360
|
+
get signalsStreamTermination() {
|
|
19361
|
+
return true;
|
|
19362
|
+
}
|
|
18736
19363
|
isInThinkingBlock = false;
|
|
18737
19364
|
/**
|
|
18738
19365
|
* Reasoning blocks of the assistant turn currently being translated, indexed by the
|
|
@@ -19495,6 +20122,10 @@ function isReasoningBlock(block) {
|
|
|
19495
20122
|
return "reasoningContent" in block;
|
|
19496
20123
|
}
|
|
19497
20124
|
var DeepSeekBedrockBackend = class extends BaseBedrockBackend {
|
|
20125
|
+
/** Reports done only on event.messageStop, so a missing terminal event means a truncated stream. */
|
|
20126
|
+
get signalsStreamTermination() {
|
|
20127
|
+
return true;
|
|
20128
|
+
}
|
|
19498
20129
|
/** Suppresses reasoning/thinking output for summary and title generation calls. */
|
|
19499
20130
|
isSpecialTask = false;
|
|
19500
20131
|
/** Tracks whether the stream is currently inside a reasoning span, to emit one <think>/</think> pair per span. */
|
|
@@ -19839,6 +20470,10 @@ var JurassicTwoBedrockBackend = class extends BaseBedrockBackend {
|
|
|
19839
20470
|
}
|
|
19840
20471
|
};
|
|
19841
20472
|
var LlamaBedrockBackend = class extends BaseBedrockBackend {
|
|
20473
|
+
/** Reports done only on response.stop_reason on the terminal chunk, so a missing terminal event means a truncated stream. */
|
|
20474
|
+
get signalsStreamTermination() {
|
|
20475
|
+
return true;
|
|
20476
|
+
}
|
|
19842
20477
|
async getModelInfo() {
|
|
19843
20478
|
return [
|
|
19844
20479
|
{
|
|
@@ -20984,7 +21619,7 @@ var GeminiBackend = class {
|
|
|
20984
21619
|
rank: 8,
|
|
20985
21620
|
trainingCutoff: "2025-01-31",
|
|
20986
21621
|
releaseDate: "2025-06-01",
|
|
20987
|
-
deprecationDate: "2026-
|
|
21622
|
+
deprecationDate: "2026-09-02",
|
|
20988
21623
|
description: "Google's Gemini 2.5 Flash, offering well-rounded price-performance. Best for large scale processing, low-latency, high volume tasks that require thinking, and agentic use cases"
|
|
20989
21624
|
},
|
|
20990
21625
|
{
|
|
@@ -25949,6 +26584,7 @@ const DEPRECATED_MODEL_MAP = {
|
|
|
25949
26584
|
"claude-3-haiku-20240307": "claude-haiku-4-5-20251001",
|
|
25950
26585
|
"gpt-5-chat-latest": "gpt-5.5",
|
|
25951
26586
|
"gpt-5.1-chat-latest": "gpt-5.5",
|
|
26587
|
+
"gemini-2.5-flash": "gemini-3.1-flash-lite",
|
|
25952
26588
|
"grok-3": "grok-4.5",
|
|
25953
26589
|
"grok-3-fast": "grok-4.5",
|
|
25954
26590
|
"grok-2-1212": "grok-4.5",
|
|
@@ -27522,6 +28158,109 @@ async function createSseBackend(input, deps = defaultSseTransportDeps) {
|
|
|
27522
28158
|
}
|
|
27523
28159
|
//#endregion
|
|
27524
28160
|
//#region ../../b4m-core/mcp/dist/index.mjs
|
|
28161
|
+
/**
|
|
28162
|
+
* Environment construction for the MCP stdio child process.
|
|
28163
|
+
*
|
|
28164
|
+
* The child is spawned from a process that also holds platform credentials - provider API keys,
|
|
28165
|
+
* database URIs, signing secrets - so what it inherits is a trust decision, not a convenience.
|
|
28166
|
+
* Two rules follow:
|
|
28167
|
+
*
|
|
28168
|
+
* 1. The child environment is built from an allowlist, never spread from `process.env`. The base
|
|
28169
|
+
* layer is the MCP SDK's own `getDefaultEnvironment()`, which the stdio transport merges
|
|
28170
|
+
* underneath whatever we pass (PATH, HOME, SHELL, TERM, USER on POSIX; the equivalent set on
|
|
28171
|
+
* Windows). Everything above that base comes from the table below.
|
|
28172
|
+
* 2. A stored variable is provider data, never runtime configuration. The child is a Node
|
|
28173
|
+
* process, so a key like NODE_OPTIONS is applied by the runtime before a single line of
|
|
28174
|
+
* server code loads: `--require /tmp/x.js` would turn a credential field into arbitrary code
|
|
28175
|
+
* execution inside the child. Those keys are refused rather than dropped quietly.
|
|
28176
|
+
*
|
|
28177
|
+
* MUST STAY IN SYNC with the `process.env` reads under each server directory
|
|
28178
|
+
* (`github/config.ts`, `notion/config.ts`, `atlassian/config.ts`, `linkedin/index.ts`). A
|
|
28179
|
+
* variable a server reads but this table omits arrives `undefined`, so add it here in the same
|
|
28180
|
+
* change. `childEnv.test.ts` pins that both ways.
|
|
28181
|
+
*/
|
|
28182
|
+
const MCP_SERVER_ENV_KEYS = {
|
|
28183
|
+
[McpServerName.LinkedIn]: ["LINKEDIN_ACCESS_TOKEN", "COMPANY_NAME"],
|
|
28184
|
+
[McpServerName.Github]: ["GITHUB_ACCESS_TOKEN"],
|
|
28185
|
+
[McpServerName.Atlassian]: [
|
|
28186
|
+
"ATLASSIAN_ACCESS_TOKEN",
|
|
28187
|
+
"ATLASSIAN_CLOUD_ID",
|
|
28188
|
+
"ATLASSIAN_SITE_URL"
|
|
28189
|
+
],
|
|
28190
|
+
[McpServerName.Notion]: [
|
|
28191
|
+
"NOTION_ACCESS_TOKEN",
|
|
28192
|
+
"NOTION_WORKSPACE_ID",
|
|
28193
|
+
"NOTION_WRITE_ENABLED",
|
|
28194
|
+
"NOTION_ROOT_PAGE_ID",
|
|
28195
|
+
"NOTION_ACCESS_MODE",
|
|
28196
|
+
"NOTION_ALLOWED_PAGES",
|
|
28197
|
+
"NOTION_EXCLUDED_PAGE_IDS",
|
|
28198
|
+
"NOTION_DEBUG"
|
|
28199
|
+
]
|
|
28200
|
+
};
|
|
28201
|
+
/**
|
|
28202
|
+
* Keys that make the runtime execute caller-chosen code before the server's entry point runs:
|
|
28203
|
+
* NODE_OPTIONS can `--require` a file, the loader variables preload a shared object, and
|
|
28204
|
+
* ELECTRON_RUN_AS_NODE changes what the binary is. Matching is case-insensitive because Windows
|
|
28205
|
+
* environment names are.
|
|
28206
|
+
*/
|
|
28207
|
+
const CODE_INJECTING_ENV_KEY_PATTERNS = [
|
|
28208
|
+
/^NODE_/i,
|
|
28209
|
+
/^ELECTRON_RUN_AS_NODE$/i,
|
|
28210
|
+
/^LD_/i,
|
|
28211
|
+
/^DYLD_/i
|
|
28212
|
+
];
|
|
28213
|
+
/**
|
|
28214
|
+
* Keys that steer where the child resolves things rather than what it executes: npm_* redirects
|
|
28215
|
+
* package resolution, PATH decides which binary a bare command name finds, and the proxy
|
|
28216
|
+
* variables redirect outbound traffic.
|
|
28217
|
+
*/
|
|
28218
|
+
const RESOLUTION_STEERING_ENV_KEY_PATTERNS = [
|
|
28219
|
+
/^npm_/i,
|
|
28220
|
+
/^PATH$/i,
|
|
28221
|
+
/^PATHEXT$/i,
|
|
28222
|
+
/^(HTTP|HTTPS|ALL|NO|FTP)_PROXY$/i,
|
|
28223
|
+
/^GLOBAL_AGENT_/i
|
|
28224
|
+
];
|
|
28225
|
+
[...CODE_INJECTING_ENV_KEY_PATTERNS, ...RESOLUTION_STEERING_ENV_KEY_PATTERNS];
|
|
28226
|
+
const matchesAny = (patterns, key) => {
|
|
28227
|
+
const normalized = key.trim();
|
|
28228
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
28229
|
+
};
|
|
28230
|
+
/** True when `key` would have the runtime load caller-chosen code before the server starts. */
|
|
28231
|
+
function isCodeInjectingMcpEnvKey(key) {
|
|
28232
|
+
return matchesAny(CODE_INJECTING_ENV_KEY_PATTERNS, key);
|
|
28233
|
+
}
|
|
28234
|
+
/**
|
|
28235
|
+
* Build the environment for a stdio MCP child.
|
|
28236
|
+
*
|
|
28237
|
+
* A bundled server gets exactly its declared variables - the allowlist decides, and the denylist
|
|
28238
|
+
* above is never consulted.
|
|
28239
|
+
*
|
|
28240
|
+
* A caller-defined command has no declared contract to check against, so it gets everything
|
|
28241
|
+
* except the code-injecting keys. Only the `b4m` CLI config reaches this branch, and that file
|
|
28242
|
+
* already lets its owner set `command` and `args` to any binary - so withholding PATH or a proxy
|
|
28243
|
+
* variable from them protects nobody while breaking a wrapper script or a corporate proxy, and
|
|
28244
|
+
* the warning that says so goes to a stderr the TUI hides. The code-injecting half stays because
|
|
28245
|
+
* an env-only `--require` is the one lever that is easy to set by accident.
|
|
28246
|
+
*/
|
|
28247
|
+
function buildMcpChildEnv({ serverName, envVariables, hasCustomCommand = false }) {
|
|
28248
|
+
const declaredKeys = hasCustomCommand ? void 0 : MCP_SERVER_ENV_KEYS[serverName];
|
|
28249
|
+
const isAllowed = declaredKeys ? (key) => declaredKeys.includes(key) : (key) => !isCodeInjectingMcpEnvKey(key);
|
|
28250
|
+
const env = {};
|
|
28251
|
+
const droppedKeys = [];
|
|
28252
|
+
for (const { key, value } of envVariables) {
|
|
28253
|
+
if (!isAllowed(key)) {
|
|
28254
|
+
droppedKeys.push(key);
|
|
28255
|
+
continue;
|
|
28256
|
+
}
|
|
28257
|
+
env[key] = value;
|
|
28258
|
+
}
|
|
28259
|
+
return {
|
|
28260
|
+
env,
|
|
28261
|
+
droppedKeys
|
|
28262
|
+
};
|
|
28263
|
+
}
|
|
27525
28264
|
var MCPClient = class {
|
|
27526
28265
|
mcp;
|
|
27527
28266
|
transport = null;
|
|
@@ -27565,14 +28304,11 @@ var MCPClient = class {
|
|
|
27565
28304
|
}));
|
|
27566
28305
|
return;
|
|
27567
28306
|
}
|
|
27568
|
-
const envVarsObject = this.envVariables.reduce((acc, env) => ({
|
|
27569
|
-
...acc,
|
|
27570
|
-
[env.key]: env.value
|
|
27571
|
-
}), {});
|
|
27572
28307
|
let command;
|
|
27573
28308
|
let args;
|
|
27574
|
-
|
|
27575
|
-
|
|
28309
|
+
const customCommand = this.customCommand?.trim() ? this.customCommand : void 0;
|
|
28310
|
+
if (customCommand) {
|
|
28311
|
+
command = customCommand;
|
|
27576
28312
|
args = this.customArgs ?? [];
|
|
27577
28313
|
} else {
|
|
27578
28314
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -27589,13 +28325,16 @@ var MCPClient = class {
|
|
|
27589
28325
|
console.log(`[MCP] Using server: ${this.serverName} at ${serverScriptPath}`);
|
|
27590
28326
|
}
|
|
27591
28327
|
const stderrMode = this.suppressStderr ? "ignore" : this.onStderrLine ? "pipe" : void 0;
|
|
28328
|
+
const { env, droppedKeys } = buildMcpChildEnv({
|
|
28329
|
+
serverName: this.serverName,
|
|
28330
|
+
envVariables: this.envVariables,
|
|
28331
|
+
hasCustomCommand: Boolean(customCommand)
|
|
28332
|
+
});
|
|
28333
|
+
if (droppedKeys.length > 0) console.warn(`[MCP] Withheld ${droppedKeys.length} undeclared env variable(s) from ${this.serverName}: ${droppedKeys.join(", ")}`);
|
|
27592
28334
|
const transportConfig = {
|
|
27593
28335
|
command,
|
|
27594
28336
|
args,
|
|
27595
|
-
env
|
|
27596
|
-
...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0)),
|
|
27597
|
-
...envVarsObject
|
|
27598
|
-
},
|
|
28337
|
+
env,
|
|
27599
28338
|
...stderrMode && { stderr: stderrMode }
|
|
27600
28339
|
};
|
|
27601
28340
|
const stdioTransport = new StdioClientTransport(transportConfig);
|
|
@@ -28023,7 +28762,7 @@ const MODEL_ALIASES = {
|
|
|
28023
28762
|
"o4-mini": ChatModels.O4_MINI,
|
|
28024
28763
|
gemini: ChatModels.GEMINI_2_5_PRO,
|
|
28025
28764
|
"gemini-pro": ChatModels.GEMINI_2_5_PRO,
|
|
28026
|
-
"gemini-flash": ChatModels.
|
|
28765
|
+
"gemini-flash": ChatModels.GEMINI_3_5_FLASH,
|
|
28027
28766
|
"gemini-flash-lite": ChatModels.GEMINI_2_5_FLASH_LITE,
|
|
28028
28767
|
"gemini-3": ChatModels.GEMINI_3_PRO_PREVIEW,
|
|
28029
28768
|
"gemini-3-pro": ChatModels.GEMINI_3_PRO_PREVIEW,
|
|
@@ -28458,7 +29197,7 @@ function buildFilenameMarkerRegex(markers) {
|
|
|
28458
29197
|
* of the best-effort DB pre-filter. Fail-closed by design.
|
|
28459
29198
|
*/
|
|
28460
29199
|
function isRetrievalExcluded(file, opts) {
|
|
28461
|
-
const stalledByConvergence =
|
|
29200
|
+
const stalledByConvergence = isChunkStalledFile(file) || isChunkRebuildPending(file.chunkRebuildRequestedAt);
|
|
28462
29201
|
if (opts.vectorizedOnly && !file.vectorized && !stalledByConvergence) return true;
|
|
28463
29202
|
const re = buildFilenameMarkerRegex(opts.excludeFilenameMarkers);
|
|
28464
29203
|
return !!re && re.test((file.fileName ?? "").toLowerCase());
|
|
@@ -29042,6 +29781,18 @@ function attachedContentBudgetsAgree(maxSafeInputTokens, systemPromptReserve) {
|
|
|
29042
29781
|
var AdminSettingsCache = class AdminSettingsCache {
|
|
29043
29782
|
cache = /* @__PURE__ */ new Map();
|
|
29044
29783
|
individualCache = /* @__PURE__ */ new Map();
|
|
29784
|
+
/**
|
|
29785
|
+
* Every call through this field is optional-chained (`this.logger.debug?.()`).
|
|
29786
|
+
*
|
|
29787
|
+
* A cache must not throw because it could not log, and this one is exposed to that: it is a
|
|
29788
|
+
* process-wide singleton created with whichever logger happens to reach `getSettingsCache` first.
|
|
29789
|
+
* What each caller then does with a throw varies, and it is mostly NOT a degrade-to-defaults
|
|
29790
|
+
* guard: `getSettingsByNames` has none at all, the scoped resolver guards one layer out in
|
|
29791
|
+
* `resolveAll`, and `resolveSpendLevers` deliberately rethrows to halt spend. So a logger missing
|
|
29792
|
+
* a quieter level could surface as a silent wrong VALUE, as an unhandled rejection, or as a hard
|
|
29793
|
+
* fail-closed, depending on who asked. `ScopedSettingsCache` is built by the same factory pair
|
|
29794
|
+
* and still has one unguarded call - the same hazard, not a solved one.
|
|
29795
|
+
*/
|
|
29045
29796
|
logger;
|
|
29046
29797
|
cleanupInterval = null;
|
|
29047
29798
|
maxCacheSize = 1e3;
|
|
@@ -29057,13 +29808,13 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29057
29808
|
*/
|
|
29058
29809
|
startCleanupTimer() {
|
|
29059
29810
|
if (process.env.NODE_ENV !== "production" || process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME) {
|
|
29060
|
-
this.logger.debug("Skipping cleanup timer in serverless environment");
|
|
29811
|
+
this.logger.debug?.("Skipping cleanup timer in serverless environment");
|
|
29061
29812
|
return;
|
|
29062
29813
|
}
|
|
29063
29814
|
this.cleanupInterval = setInterval(() => {
|
|
29064
29815
|
this.performCleanup();
|
|
29065
29816
|
}, AdminSettingsCache.CLEANUP_INTERVAL);
|
|
29066
|
-
this.logger.debug("Started cache cleanup timer");
|
|
29817
|
+
this.logger.debug?.("Started cache cleanup timer");
|
|
29067
29818
|
}
|
|
29068
29819
|
/**
|
|
29069
29820
|
* Stop cleanup timer (for graceful shutdown)
|
|
@@ -29072,7 +29823,7 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29072
29823
|
if (this.cleanupInterval) {
|
|
29073
29824
|
clearInterval(this.cleanupInterval);
|
|
29074
29825
|
this.cleanupInterval = null;
|
|
29075
|
-
this.logger.debug("Stopped cache cleanup timer");
|
|
29826
|
+
this.logger.debug?.("Stopped cache cleanup timer");
|
|
29076
29827
|
}
|
|
29077
29828
|
}
|
|
29078
29829
|
/**
|
|
@@ -29097,9 +29848,9 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29097
29848
|
this.individualCache.delete(entries[i][0]);
|
|
29098
29849
|
removedCount++;
|
|
29099
29850
|
}
|
|
29100
|
-
this.logger.warn(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
|
|
29851
|
+
this.logger.warn?.(`Emergency cache cleanup: removed ${toRemove} entries due to size limit`);
|
|
29101
29852
|
}
|
|
29102
|
-
if (removedCount > 0) this.logger.debug(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
|
|
29853
|
+
if (removedCount > 0) this.logger.debug?.(`Cache cleanup removed ${removedCount} expired entries (${beforeSize} → ${this.cache.size + this.individualCache.size})`);
|
|
29103
29854
|
}
|
|
29104
29855
|
/**
|
|
29105
29856
|
* Get TTL based on environment
|
|
@@ -29120,18 +29871,18 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29120
29871
|
const cacheKey = "all_settings";
|
|
29121
29872
|
const cached = this.cache.get(cacheKey);
|
|
29122
29873
|
if (cached && this.isValid(cached.timestamp, cached.ttl)) {
|
|
29123
|
-
this.logger.debug("📦 Admin settings cache HIT");
|
|
29874
|
+
this.logger.debug?.("📦 Admin settings cache HIT");
|
|
29124
29875
|
return cached.data;
|
|
29125
29876
|
}
|
|
29126
29877
|
if (cached) this.cache.delete(cacheKey);
|
|
29127
|
-
this.logger.debug("🔍 Admin settings cache MISS - fetching from database");
|
|
29878
|
+
this.logger.debug?.("🔍 Admin settings cache MISS - fetching from database");
|
|
29128
29879
|
const fetchStart = Date.now();
|
|
29129
29880
|
const settingsMap = (await db.adminSettings.findAll()).reduce((out, s) => {
|
|
29130
29881
|
out[s.settingName] = s.settingValue;
|
|
29131
29882
|
return out;
|
|
29132
29883
|
}, {});
|
|
29133
29884
|
const fetchTime = Date.now() - fetchStart;
|
|
29134
|
-
this.logger.info(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
|
|
29885
|
+
this.logger.info?.(`📦 Cached ${Object.keys(settingsMap).length} admin settings in ${fetchTime}ms`);
|
|
29135
29886
|
const ttl = this.getTTL();
|
|
29136
29887
|
this.cache.set(cacheKey, {
|
|
29137
29888
|
data: settingsMap,
|
|
@@ -29153,15 +29904,15 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29153
29904
|
async getSettingByName(settingName, db) {
|
|
29154
29905
|
const cached = this.individualCache.get(settingName);
|
|
29155
29906
|
if (cached && this.isValid(cached.timestamp, cached.ttl)) {
|
|
29156
|
-
this.logger.debug(`📦 Individual setting '${settingName}' cache HIT`);
|
|
29907
|
+
this.logger.debug?.(`📦 Individual setting '${settingName}' cache HIT`);
|
|
29157
29908
|
return cached.value;
|
|
29158
29909
|
}
|
|
29159
29910
|
if (cached) this.individualCache.delete(settingName);
|
|
29160
|
-
this.logger.debug(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
|
|
29911
|
+
this.logger.debug?.(`🔍 Individual setting '${settingName}' cache MISS - fetching from database`);
|
|
29161
29912
|
const fetchStart = Date.now();
|
|
29162
29913
|
const value = (await db.adminSettings.findBySettingName(settingName))?.settingValue ?? null;
|
|
29163
29914
|
const fetchTime = Date.now() - fetchStart;
|
|
29164
|
-
this.logger.debug(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
|
|
29915
|
+
this.logger.debug?.(`📦 Cached individual setting '${settingName}' in ${fetchTime}ms`);
|
|
29165
29916
|
this.individualCache.set(settingName, {
|
|
29166
29917
|
value,
|
|
29167
29918
|
timestamp: Date.now(),
|
|
@@ -29185,11 +29936,11 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29185
29936
|
}
|
|
29186
29937
|
}
|
|
29187
29938
|
if (uncachedSettings.length > 0) {
|
|
29188
|
-
this.logger.debug(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
|
|
29939
|
+
this.logger.debug?.(`🔍 Batch fetching ${uncachedSettings.length} uncached settings: ${uncachedSettings.join(", ")}`);
|
|
29189
29940
|
const fetchStart = Date.now();
|
|
29190
29941
|
const settings = await db.adminSettings.findBySettingNames(uncachedSettings);
|
|
29191
29942
|
const fetchTime = Date.now() - fetchStart;
|
|
29192
|
-
this.logger.debug(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
|
|
29943
|
+
this.logger.debug?.(`📦 Batch fetched ${settings.length} settings in ${fetchTime}ms`);
|
|
29193
29944
|
const ttl = this.getTTL();
|
|
29194
29945
|
settings.forEach((setting) => {
|
|
29195
29946
|
result[setting.settingName] = setting.settingValue;
|
|
@@ -29207,7 +29958,7 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29207
29958
|
});
|
|
29208
29959
|
});
|
|
29209
29960
|
}
|
|
29210
|
-
this.logger.debug(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
|
|
29961
|
+
this.logger.debug?.(`📦 Returned ${Object.keys(result).length} settings (${settingNames.length - uncachedSettings.length} from cache, ${uncachedSettings.length} from DB)`);
|
|
29211
29962
|
return result;
|
|
29212
29963
|
}
|
|
29213
29964
|
/**
|
|
@@ -29216,7 +29967,7 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29216
29967
|
invalidateSetting(settingName) {
|
|
29217
29968
|
this.individualCache.delete(settingName);
|
|
29218
29969
|
this.cache.delete("all_settings");
|
|
29219
|
-
this.logger.info(`🗑️ Invalidated cache for setting: ${settingName}`);
|
|
29970
|
+
this.logger.info?.(`🗑️ Invalidated cache for setting: ${settingName}`);
|
|
29220
29971
|
}
|
|
29221
29972
|
/**
|
|
29222
29973
|
* Invalidate all cached admin settings
|
|
@@ -29224,7 +29975,7 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29224
29975
|
invalidateAll() {
|
|
29225
29976
|
this.cache.clear();
|
|
29226
29977
|
this.individualCache.clear();
|
|
29227
|
-
this.logger.info("🗑️ Invalidated all admin settings cache");
|
|
29978
|
+
this.logger.info?.("🗑️ Invalidated all admin settings cache");
|
|
29228
29979
|
}
|
|
29229
29980
|
/**
|
|
29230
29981
|
* Get cache statistics for monitoring
|
|
@@ -29257,16 +30008,16 @@ var AdminSettingsCache = class AdminSettingsCache {
|
|
|
29257
30008
|
* Warm up the cache by fetching all settings
|
|
29258
30009
|
*/
|
|
29259
30010
|
async warmUp(db) {
|
|
29260
|
-
this.logger.info("🔥 Warming up admin settings cache...");
|
|
30011
|
+
this.logger.info?.("🔥 Warming up admin settings cache...");
|
|
29261
30012
|
await this.getSettingsMap(db);
|
|
29262
|
-
this.logger.info("✅ Admin settings cache warmed up");
|
|
30013
|
+
this.logger.info?.("✅ Admin settings cache warmed up");
|
|
29263
30014
|
}
|
|
29264
30015
|
/**
|
|
29265
30016
|
* Graceful shutdown - cleanup timers
|
|
29266
30017
|
*/
|
|
29267
30018
|
shutdown() {
|
|
29268
30019
|
this.stopCleanupTimer();
|
|
29269
|
-
this.logger.info("🛑 Admin settings cache shutdown complete");
|
|
30020
|
+
this.logger.info?.("🛑 Admin settings cache shutdown complete");
|
|
29270
30021
|
}
|
|
29271
30022
|
};
|
|
29272
30023
|
/** Address of one cached override, shared by the cache and its callers so lookups are consistent. */
|
|
@@ -29615,6 +30366,17 @@ const getFileContent = async (fabFile, { storage, logger }) => {
|
|
|
29615
30366
|
}
|
|
29616
30367
|
return content;
|
|
29617
30368
|
};
|
|
30369
|
+
/**
|
|
30370
|
+
* Content hash for per-lake FabFile dedup (`findByContentHashesInDataLake`). Shared by every
|
|
30371
|
+
* ingest path that needs to hash bytes before creating a FabFile - the Slack attachment path
|
|
30372
|
+
* (raw downloaded buffer) and the URL/link path (`fetchAndParseURL`'s extracted `textContent`) -
|
|
30373
|
+
* so at least the HASHING ITSELF cannot drift between two copies of the same algorithm.
|
|
30374
|
+
*
|
|
30375
|
+
* This does NOT make `contentHash` one hash domain: the two callers feed it different inputs
|
|
30376
|
+
* (raw bytes vs. extracted text), so the same document added once as an attachment and once as a
|
|
30377
|
+
* link produces two different hashes and is not caught as a duplicate by this field.
|
|
30378
|
+
*/
|
|
30379
|
+
const computeContentHash = (content) => createHash$1("sha256").update(content).digest("hex");
|
|
29618
30380
|
/** The next 1-based version number given the existing (possibly absent) version history. */
|
|
29619
30381
|
const nextVersionNumber = (versions) => {
|
|
29620
30382
|
if (!versions || versions.length === 0) return 1;
|
|
@@ -29825,19 +30587,6 @@ const EDITABLE_IMAGE_KEY_RE = /\.(jpe?g|png|webp|gif)$/i;
|
|
|
29825
30587
|
const PREVIEW_CHUNK = 700;
|
|
29826
30588
|
const CHARS_PER_TOKEN = 3.5;
|
|
29827
30589
|
/**
|
|
29828
|
-
* Chunks per attached file that cosine retrieval feeds to the model. Three starved small embedders: a
|
|
29829
|
-
* chunk is the embedding model's context window less a 20% buffer (see SmartChunker), so three chunks
|
|
29830
|
-
* is roughly 69k chars on an 8192-token embedder but only 4.3k on a 512-token one, which answers a
|
|
29831
|
-
* question about a 200-row table from 43 rows without saying so.
|
|
29832
|
-
*
|
|
29833
|
-
* 10 is borrowed from rankChunksForFiles' topK default, but note the two caps differ in shape: that
|
|
29834
|
-
* one is global across every file in the search, this one is PER FILE, so a multi-file attachment can
|
|
29835
|
-
* yield more chunks here. What bounds the payload is the per-file character budget applied to these
|
|
29836
|
-
* results (maxChars in processFabFilesServer), not this count - and that budget now derives from the
|
|
29837
|
-
* model's input window rather than its output limit; see attachedContentExtractionBudget.
|
|
29838
|
-
*/
|
|
29839
|
-
const COSINE_SEARCH_TOP_K = 10;
|
|
29840
|
-
/**
|
|
29841
30590
|
* How much of one attached file the cosine scan will read, and in what size pages.
|
|
29842
30591
|
*
|
|
29843
30592
|
* Module constants rather than admin settings: unlike a data lake, an attachment is one file the
|
|
@@ -30249,14 +30998,28 @@ async function fetchAgentConversationHistory(session, questCount, { db }) {
|
|
|
30249
30998
|
return acc;
|
|
30250
30999
|
}, new Array());
|
|
30251
31000
|
}
|
|
30252
|
-
|
|
30253
|
-
|
|
30254
|
-
|
|
31001
|
+
/**
|
|
31002
|
+
* Resolves attachment ids to documents, and reports the ones it could NOT resolve. The missing set
|
|
31003
|
+
* is the point: `getAccessibleFiles` applies a permission scope and simply omits what it rejects, so
|
|
31004
|
+
* an id dropped by the scope filter or by a delete/upload race used to leave no trace anywhere - the
|
|
31005
|
+
* turn ran as though the file had never been attached (#2228). Callers report `missingIds` through
|
|
31006
|
+
* the same channel as the per-file notices rather than inferring the drop from a shorter array.
|
|
31007
|
+
*/
|
|
31008
|
+
async function fetchAndConvertFabFiles(fabFileIds, { scope, lakeAccess }, { db, storage, logger }) {
|
|
31009
|
+
const fabFiles = await db.fabfiles.getAccessibleFiles(fabFileIds, scope, lakeAccess);
|
|
31010
|
+
const files = await Promise.all(fabFiles.map(async (file) => {
|
|
30255
31011
|
return {
|
|
30256
31012
|
...file,
|
|
30257
31013
|
userId: file.userId.toString()
|
|
30258
31014
|
};
|
|
30259
31015
|
}));
|
|
31016
|
+
const returnedIds = new Set(files.map((file) => String(file.id)));
|
|
31017
|
+
const missingIds = Array.from(new Set(fabFileIds)).filter((id) => !returnedIds.has(String(id)));
|
|
31018
|
+
if (missingIds.length > 0) logger?.warn(`[fetchAndConvertFabFiles] ${missingIds.length} of ${fabFileIds.length} requested file id(s) were not returned by getAccessibleFiles and contribute nothing to this turn: ${missingIds.join(", ")}`);
|
|
31019
|
+
return {
|
|
31020
|
+
files,
|
|
31021
|
+
missingIds
|
|
31022
|
+
};
|
|
30260
31023
|
}
|
|
30261
31024
|
async function getCachedSignedUrl(filePath, storage, db) {
|
|
30262
31025
|
const key = `cachedSignedUrl:${filePath}`;
|
|
@@ -30458,7 +31221,7 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
|
|
|
30458
31221
|
for (const chunk of usable) {
|
|
30459
31222
|
const position = scanned;
|
|
30460
31223
|
scanned++;
|
|
30461
|
-
if (head.length <
|
|
31224
|
+
if (head.length < 10) head.push({
|
|
30462
31225
|
chunkId: chunk.id,
|
|
30463
31226
|
content: chunk.text,
|
|
30464
31227
|
score: 0
|
|
@@ -30479,9 +31242,9 @@ async function cosineSearch(file, userPromptVector, { db, logger }) {
|
|
|
30479
31242
|
position
|
|
30480
31243
|
});
|
|
30481
31244
|
}
|
|
30482
|
-
if (ranked.length >
|
|
31245
|
+
if (ranked.length > 10) {
|
|
30483
31246
|
ranked.sort(compareRankedChunks);
|
|
30484
|
-
ranked.length =
|
|
31247
|
+
ranked.length = 10;
|
|
30485
31248
|
}
|
|
30486
31249
|
if (!moreExist) break;
|
|
30487
31250
|
}
|
|
@@ -30503,14 +31266,14 @@ const noopResize = async (imageBuffer) => imageBuffer;
|
|
|
30503
31266
|
async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, attachedContentTokenBudget, modelInfo, sendStatusUpdate, { logger, storage, db, resizeImageForModel = noopResize }, progressCallback) {
|
|
30504
31267
|
if (!fabFiles || fabFiles.length === 0) return {
|
|
30505
31268
|
userMessages: [],
|
|
30506
|
-
|
|
31269
|
+
fileNotices: [],
|
|
30507
31270
|
deliveredFileIds: [],
|
|
30508
31271
|
fullyDeliveredFileIds: []
|
|
30509
31272
|
};
|
|
30510
31273
|
const fileProcessingStartTime = Date.now();
|
|
30511
31274
|
let systemContent = "";
|
|
30512
31275
|
const userMessages = [];
|
|
30513
|
-
const
|
|
31276
|
+
const fileNotices = [];
|
|
30514
31277
|
const deliveredFileIds = /* @__PURE__ */ new Set();
|
|
30515
31278
|
const fullyDeliveredFileIds = /* @__PURE__ */ new Set();
|
|
30516
31279
|
const contextFiles = [];
|
|
@@ -30541,11 +31304,25 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30541
31304
|
try {
|
|
30542
31305
|
if (isAudioMimeType(file.mimeType)) {
|
|
30543
31306
|
logger.warn(`[processFabFilesServer] Skipping audio file ${file.fileName} — audio is not attachable to an LLM.`);
|
|
31307
|
+
fileNotices.push({
|
|
31308
|
+
fabFileId: file.id,
|
|
31309
|
+
fileName: file.fileName,
|
|
31310
|
+
band: "audio",
|
|
31311
|
+
message: `"${noticeFileName(file.fileName)}" is an audio file and was not sent: no model accepts audio as input.`,
|
|
31312
|
+
delivered: false
|
|
31313
|
+
});
|
|
30544
31314
|
return;
|
|
30545
31315
|
}
|
|
30546
31316
|
if (supportsVision && isImageAttachment(file.mimeType)) {
|
|
30547
31317
|
if (!isImageServeable(file)) {
|
|
30548
31318
|
logger.warn(`[processFabFilesServer] Skipping image file ${file.fileName} — held pending moderation or blocked (#9776 Q2b).`);
|
|
31319
|
+
fileNotices.push({
|
|
31320
|
+
fabFileId: file.id,
|
|
31321
|
+
fileName: file.fileName,
|
|
31322
|
+
band: "image_not_serveable",
|
|
31323
|
+
message: `Image "${noticeFileName(file.fileName)}" was not sent: it is held pending moderation or has been blocked.`,
|
|
31324
|
+
delivered: false
|
|
31325
|
+
});
|
|
30549
31326
|
return;
|
|
30550
31327
|
}
|
|
30551
31328
|
sendStatusUpdate(`Processing image file ${file.fileName}...`);
|
|
@@ -30581,9 +31358,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30581
31358
|
const errorMsg = `⚠️ Image "${file.fileName}" (${fileSizeMB.toFixed(1)}MB) is too large for ${backendName}. Max: ${MAX_IMAGE_SIZE_MB}MB. Please delete this file and re-upload to auto-resize.`;
|
|
30582
31359
|
logger.warn(errorMsg);
|
|
30583
31360
|
await sendStatusUpdate(errorMsg);
|
|
30584
|
-
|
|
30585
|
-
|
|
30586
|
-
|
|
31361
|
+
fileNotices.push({
|
|
31362
|
+
fabFileId: file.id,
|
|
31363
|
+
fileName: file.fileName,
|
|
31364
|
+
band: "image_too_large",
|
|
31365
|
+
message: errorMsg,
|
|
31366
|
+
delivered: false
|
|
30587
31367
|
});
|
|
30588
31368
|
return;
|
|
30589
31369
|
}
|
|
@@ -30613,9 +31393,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30613
31393
|
const errorMsg = `⚠️ Image "${file.fileName}" (${encodedMB}MB encoded) is too large for ${modelInfo.name}. Max ~3MB. Please delete this file and re-upload a smaller image.`;
|
|
30614
31394
|
logger.warn(errorMsg);
|
|
30615
31395
|
await sendStatusUpdate(errorMsg);
|
|
30616
|
-
|
|
30617
|
-
|
|
30618
|
-
|
|
31396
|
+
fileNotices.push({
|
|
31397
|
+
fabFileId: file.id,
|
|
31398
|
+
fileName: file.fileName,
|
|
31399
|
+
band: "image_too_large",
|
|
31400
|
+
message: errorMsg,
|
|
31401
|
+
delivered: false
|
|
30619
31402
|
});
|
|
30620
31403
|
return;
|
|
30621
31404
|
}
|
|
@@ -30629,7 +31412,16 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30629
31412
|
});
|
|
30630
31413
|
delivered = true;
|
|
30631
31414
|
fullyDelivered = true;
|
|
30632
|
-
} else
|
|
31415
|
+
} else {
|
|
31416
|
+
logger.warn(`Vision support for the model ${modelInfo.id} is not implemented. Skipping image processing.`);
|
|
31417
|
+
fileNotices.push({
|
|
31418
|
+
fabFileId: file.id,
|
|
31419
|
+
fileName: file.fileName,
|
|
31420
|
+
band: "vision_unsupported",
|
|
31421
|
+
message: `Image "${noticeFileName(file.fileName)}" was not sent: image input is not implemented for ${modelInfo.name ?? modelInfo.id}.`,
|
|
31422
|
+
delivered: false
|
|
31423
|
+
});
|
|
31424
|
+
}
|
|
30633
31425
|
break;
|
|
30634
31426
|
case ModelBackend.Ollama: {
|
|
30635
31427
|
const imageBuffer = await resizeImageForModel(await storage.download(file.filePath), void 0, logger);
|
|
@@ -30658,10 +31450,26 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30658
31450
|
fullyDelivered = true;
|
|
30659
31451
|
break;
|
|
30660
31452
|
}
|
|
30661
|
-
default:
|
|
31453
|
+
default:
|
|
31454
|
+
logger.error(`Unsupported backend for model ${modelInfo.id} backend ${modelInfo?.backend ?? "undefined"}`);
|
|
31455
|
+
fileNotices.push({
|
|
31456
|
+
fabFileId: file.id,
|
|
31457
|
+
fileName: file.fileName,
|
|
31458
|
+
band: "unsupported_backend",
|
|
31459
|
+
message: `Image "${noticeFileName(file.fileName)}" was not sent: this model's backend does not accept image attachments.`,
|
|
31460
|
+
delivered: false
|
|
31461
|
+
});
|
|
30662
31462
|
}
|
|
30663
|
-
} else if (!supportsVision && isImageAttachment(file.mimeType))
|
|
30664
|
-
|
|
31463
|
+
} else if (!supportsVision && isImageAttachment(file.mimeType)) {
|
|
31464
|
+
logger.warn(`File ${file.fileName} is an image but model does not support vision. Skipping...`);
|
|
31465
|
+
fileNotices.push({
|
|
31466
|
+
fabFileId: file.id,
|
|
31467
|
+
fileName: file.fileName,
|
|
31468
|
+
band: "vision_unsupported",
|
|
31469
|
+
message: `Image "${noticeFileName(file.fileName)}" was not sent: ${modelInfo?.name ?? modelInfo?.id ?? "this model"} cannot read images.`,
|
|
31470
|
+
delivered: false
|
|
31471
|
+
});
|
|
31472
|
+
} else {
|
|
30665
31473
|
const embeddingModel = file.embeddingModel ?? OpenAIEmbeddingModel.TEXT_EMBEDDING_ADA_002;
|
|
30666
31474
|
const userVector = userVectorPrompt[embeddingModel];
|
|
30667
31475
|
const canCosineSearch = file.vectorized && !!userVector && userVector.length > 0;
|
|
@@ -30737,9 +31545,12 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30737
31545
|
const originalFileSize = fabContent.length;
|
|
30738
31546
|
fabContent = fabContent.substring(0, finalMaxFileSize ?? PREVIEW_CHUNK) + CONTENT_TRUNCATION_NOTICE;
|
|
30739
31547
|
errorMsg = `Knowledge in the workbench with the fileName ${file.fileName} is ${originalFileSize} long which exceeds ${finalMaxFileSize}. ` + (canCosineSearch ? "None of its vectorized chunks could be searched with this turn's embedding model, so it was sent as raw text and truncated. Re-vectorize it under the current embedding model, or select a model with a higher context window." : "Vectorize your large file or select a model with higher context window.");
|
|
30740
|
-
|
|
30741
|
-
|
|
30742
|
-
|
|
31548
|
+
fileNotices.push({
|
|
31549
|
+
fabFileId: file.id,
|
|
31550
|
+
fileName: file.fileName,
|
|
31551
|
+
band: "truncated",
|
|
31552
|
+
message: `"${noticeFileName(file.fileName)}" was too large to send whole; only the first ${Math.floor(finalMaxFileSize)} characters of ${originalFileSize} reached this conversation.`,
|
|
31553
|
+
delivered: true
|
|
30743
31554
|
});
|
|
30744
31555
|
} else errorMsg = null;
|
|
30745
31556
|
delivered = true;
|
|
@@ -30753,19 +31564,41 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30753
31564
|
error: errorMsg
|
|
30754
31565
|
});
|
|
30755
31566
|
} catch (e) {
|
|
30756
|
-
if (e instanceof BadRequestError && e.message.includes("Unsupported file type"))
|
|
30757
|
-
|
|
31567
|
+
if (e instanceof BadRequestError && e.message.includes("Unsupported file type")) {
|
|
31568
|
+
logger.warn(`Unsupported file type: ${file.fileName}`);
|
|
31569
|
+
fileNotices.push({
|
|
31570
|
+
fabFileId: file.id,
|
|
31571
|
+
fileName: file.fileName,
|
|
31572
|
+
band: "unsupported_type",
|
|
31573
|
+
message: `"${noticeFileName(file.fileName)}" was not sent: its file type (${file.mimeType}) cannot be read as text.`,
|
|
31574
|
+
delivered: false
|
|
31575
|
+
});
|
|
31576
|
+
} else if (isAxiosError(e) && e.response?.status === 404) {
|
|
30758
31577
|
await sendStatusUpdate(`Skipping file ${file.fileName}. File might be corrupted or deleted`);
|
|
30759
31578
|
await db.fabfiles.update({
|
|
30760
31579
|
id: file.id,
|
|
30761
31580
|
error: "This file appears to be corrupted or may have been deleted. Please try uploading the file again."
|
|
30762
31581
|
});
|
|
31582
|
+
fileNotices.push({
|
|
31583
|
+
fabFileId: file.id,
|
|
31584
|
+
fileName: file.fileName,
|
|
31585
|
+
band: "read_failed",
|
|
31586
|
+
message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: it appears to be corrupted or deleted. Try uploading it again.`,
|
|
31587
|
+
delivered: false
|
|
31588
|
+
});
|
|
30763
31589
|
} else if (e instanceof CorruptedFileError) {
|
|
30764
31590
|
await sendStatusUpdate(`Skipping corrupted file ${file.fileName}. Please try re-uploading`);
|
|
30765
31591
|
await db.fabfiles.update({
|
|
30766
31592
|
id: file.id,
|
|
30767
31593
|
error: e.message
|
|
30768
31594
|
});
|
|
31595
|
+
fileNotices.push({
|
|
31596
|
+
fabFileId: file.id,
|
|
31597
|
+
fileName: file.fileName,
|
|
31598
|
+
band: "read_failed",
|
|
31599
|
+
message: `"${noticeFileName(file.fileName)}" could not be read and was not sent: ${e.message}`,
|
|
31600
|
+
delivered: false
|
|
31601
|
+
});
|
|
30769
31602
|
} else {
|
|
30770
31603
|
logger.updateMetadata({ filePath: file.filePath });
|
|
30771
31604
|
throw e;
|
|
@@ -30785,6 +31618,18 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30785
31618
|
processedFiles++;
|
|
30786
31619
|
if (progressCallback) await progressCallback(processedFiles, totalFiles);
|
|
30787
31620
|
}))));
|
|
31621
|
+
const noticedFileIds = new Set(fileNotices.map((notice) => notice.fabFileId));
|
|
31622
|
+
for (const file of fabFiles) {
|
|
31623
|
+
if (deliveredFileIds.has(file.id) || noticedFileIds.has(file.id)) continue;
|
|
31624
|
+
logger.warn(`[processFabFilesServer] "${file.fileName}" (${file.id}) contributed no content and produced no notice; reporting it as undelivered.`);
|
|
31625
|
+
fileNotices.push({
|
|
31626
|
+
fabFileId: file.id,
|
|
31627
|
+
fileName: file.fileName,
|
|
31628
|
+
band: "no_readable_content",
|
|
31629
|
+
message: `"${noticeFileName(file.fileName)}" was not sent: no readable content could be extracted from it.`,
|
|
31630
|
+
delivered: false
|
|
31631
|
+
});
|
|
31632
|
+
}
|
|
30788
31633
|
if (imageContent.length > 0) userMessages.push({
|
|
30789
31634
|
role: "user",
|
|
30790
31635
|
content: imageContent
|
|
@@ -30812,7 +31657,7 @@ async function processFabFilesServer(embeddingFactory, fabFiles, userPrompt, att
|
|
|
30812
31657
|
logger.info(`📁 File processing completed in ${fileProcessingTime}ms for ${fabFiles.length} files`);
|
|
30813
31658
|
return {
|
|
30814
31659
|
userMessages,
|
|
30815
|
-
|
|
31660
|
+
fileNotices,
|
|
30816
31661
|
deliveredFileIds: Array.from(deliveredFileIds),
|
|
30817
31662
|
fullyDeliveredFileIds: Array.from(fullyDeliveredFileIds)
|
|
30818
31663
|
};
|
|
@@ -31337,6 +32182,7 @@ var llm_exports = /* @__PURE__ */ __exportAll({
|
|
|
31337
32182
|
ATTACHED_CONTENT_EXTRACTION_SHARE: () => ATTACHED_CONTENT_EXTRACTION_SHARE,
|
|
31338
32183
|
ATTACHMENT_DELIVERED_NOTICE: () => ATTACHMENT_DELIVERED_NOTICE,
|
|
31339
32184
|
BUILDER_INJECTED_BLOCK_IDS: () => BUILDER_INJECTED_BLOCK_IDS,
|
|
32185
|
+
COSINE_SEARCH_TOP_K: () => 10,
|
|
31340
32186
|
DEFAULT_OUTPUT_MAX_TOKENS: () => DEFAULT_OUTPUT_MAX_TOKENS,
|
|
31341
32187
|
EXTRACTION_SYSTEM_RESERVE_MAX_SHARE: () => EXTRACTION_SYSTEM_RESERVE_MAX_SHARE,
|
|
31342
32188
|
FORMAT_PROMPT_PRIORITY: () => 60,
|
|
@@ -32955,6 +33801,7 @@ const OPENAI_IMAGE_CLIENT_OPTS = {
|
|
|
32955
33801
|
maxRetries: 0
|
|
32956
33802
|
};
|
|
32957
33803
|
const ALTERNATIVE_IMAGE_MODELS = "Flux Pro, Flux Dev, or Grok";
|
|
33804
|
+
const truncatePromptForLog = (prompt) => prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
|
|
32958
33805
|
/**
|
|
32959
33806
|
* Builds a user-friendly error when OpenAI's safety system blocks an image
|
|
32960
33807
|
* request, guiding the user to rephrase or switch to an alternative model.
|
|
@@ -32982,6 +33829,53 @@ function buildModerationBlockedError(error) {
|
|
|
32982
33829
|
|
|
32983
33830
|
Tip: Switch to an alternative model with different content policies — e.g. ${ALTERNATIVE_IMAGE_MODELS} — which may accept this prompt.\n\nIf you believe this is an error, you can report it to OpenAI with request ID: ${requestId}`);
|
|
32984
33831
|
}
|
|
33832
|
+
/**
|
|
33833
|
+
* Splits a WIDTHxHEIGHT size into its two edges, or null when the value is not a
|
|
33834
|
+
* pair of non-zero numbers (e.g. 'auto', '', 'wide'). Null means "not a custom
|
|
33835
|
+
* resolution" rather than "invalid": generate() has always left such values
|
|
33836
|
+
* untouched, and that behaviour is preserved.
|
|
33837
|
+
*/
|
|
33838
|
+
function parseSizeEdges(size) {
|
|
33839
|
+
if (typeof size !== "string") return null;
|
|
33840
|
+
const [width, height] = size.split("x").map(Number);
|
|
33841
|
+
if (!width || !height) return null;
|
|
33842
|
+
return {
|
|
33843
|
+
width,
|
|
33844
|
+
height
|
|
33845
|
+
};
|
|
33846
|
+
}
|
|
33847
|
+
/**
|
|
33848
|
+
* True when a custom gpt-image-2 resolution meets OpenAI's documented limits.
|
|
33849
|
+
* gpt-image-2 accepts any resolution satisfying these, not only the presets in
|
|
33850
|
+
* OPENAI_GPT_IMAGE_2_IMAGE_SIZES, so a flat preset check would reject valid
|
|
33851
|
+
* custom sizes. Must stay the single source of this rule for generate() and edit().
|
|
33852
|
+
*/
|
|
33853
|
+
function satisfiesGptImage2Constraints({ width, height }) {
|
|
33854
|
+
const { maxEdge, minTotalPixels, maxTotalPixels, edgeMultiple, maxAspectRatio } = IMAGE_SIZE_CONSTRAINTS.GPT_IMAGE_2.constraints;
|
|
33855
|
+
const longEdge = Math.max(width, height);
|
|
33856
|
+
const shortEdge = Math.min(width, height);
|
|
33857
|
+
const totalPixels = width * height;
|
|
33858
|
+
return longEdge <= maxEdge && width % edgeMultiple === 0 && height % edgeMultiple === 0 && longEdge / shortEdge <= maxAspectRatio && totalPixels >= minTotalPixels && totalPixels <= maxTotalPixels;
|
|
33859
|
+
}
|
|
33860
|
+
/**
|
|
33861
|
+
* True when `size` may be forwarded to images.edit for `model`. gpt-image-2 takes
|
|
33862
|
+
* its presets (including 'auto') or any custom WIDTHxHEIGHT meeting the same
|
|
33863
|
+
* constraints generate() enforces; the gpt-image-1 family is limited to its three
|
|
33864
|
+
* fixed sizes. An unsupported size is dropped by the caller so OpenAI applies its
|
|
33865
|
+
* own default instead of rejecting the whole request with a 400.
|
|
33866
|
+
*
|
|
33867
|
+
* GPT-Image tiers only: dall-e-2 has its own size list and passes size through
|
|
33868
|
+
* untouched, so do not route that model here.
|
|
33869
|
+
*/
|
|
33870
|
+
function isSupportedEditSize(model, size) {
|
|
33871
|
+
if (typeof size !== "string") return false;
|
|
33872
|
+
if (isGPTImage2Model(model)) {
|
|
33873
|
+
if (OPENAI_GPT_IMAGE_2_IMAGE_SIZES.includes(size)) return true;
|
|
33874
|
+
const edges = parseSizeEdges(size);
|
|
33875
|
+
return edges !== null && satisfiesGptImage2Constraints(edges);
|
|
33876
|
+
}
|
|
33877
|
+
return OPENAI_GPT_IMAGE_1_IMAGE_SIZES.includes(size);
|
|
33878
|
+
}
|
|
32985
33879
|
var OpenAIImageService = class extends AIImageService {
|
|
32986
33880
|
async generate(prompt, options) {
|
|
32987
33881
|
const openai = new OpenAI({
|
|
@@ -33006,16 +33900,11 @@ var OpenAIImageService = class extends AIImageService {
|
|
|
33006
33900
|
}
|
|
33007
33901
|
if (isGPTImage2Model(options.model)) {
|
|
33008
33902
|
if (openaiOptions.size && openaiOptions.size !== "auto") {
|
|
33009
|
-
const
|
|
33010
|
-
if (
|
|
33011
|
-
const
|
|
33012
|
-
|
|
33013
|
-
|
|
33014
|
-
if (maxEdge > 3840 || w % 16 !== 0 || h % 16 !== 0 || maxEdge / minEdge > 3 || totalPixels < 655360 || totalPixels > 8294400) {
|
|
33015
|
-
const originalSize = openaiOptions.size;
|
|
33016
|
-
openaiOptions.size = "1024x1024";
|
|
33017
|
-
parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
|
|
33018
|
-
}
|
|
33903
|
+
const edges = parseSizeEdges(openaiOptions.size);
|
|
33904
|
+
if (edges && !satisfiesGptImage2Constraints(edges)) {
|
|
33905
|
+
const originalSize = openaiOptions.size;
|
|
33906
|
+
openaiOptions.size = "1024x1024";
|
|
33907
|
+
parameterWarnings.push(`Size '${originalSize}' violates gpt-image-2 constraints, changed to '1024x1024'`);
|
|
33019
33908
|
}
|
|
33020
33909
|
} else if (!openaiOptions.size) openaiOptions.size = "auto";
|
|
33021
33910
|
} else {
|
|
@@ -33068,6 +33957,10 @@ var OpenAIImageService = class extends AIImageService {
|
|
|
33068
33957
|
const imageFile = new File([pngBuffer], "image.png", { type: "image/png" });
|
|
33069
33958
|
if (isGPTImageModel(options.model)) {
|
|
33070
33959
|
const editModel = options.model || ImageModels.GPT_IMAGE_2;
|
|
33960
|
+
this.logger.log("OpenAI image generation request (edit endpoint, image-to-image):", {
|
|
33961
|
+
model: editModel,
|
|
33962
|
+
prompt: truncatePromptForLog(prompt)
|
|
33963
|
+
});
|
|
33071
33964
|
result = await openai.images.edit({
|
|
33072
33965
|
model: editModel,
|
|
33073
33966
|
image: [imageFile],
|
|
@@ -33075,20 +33968,31 @@ var OpenAIImageService = class extends AIImageService {
|
|
|
33075
33968
|
});
|
|
33076
33969
|
} else {
|
|
33077
33970
|
const { style, quality, model, ...opts } = openaiOptions;
|
|
33971
|
+
const variationSize = [
|
|
33972
|
+
"256x256",
|
|
33973
|
+
"512x512",
|
|
33974
|
+
"1024x1024"
|
|
33975
|
+
].find((s) => s === openaiOptions.size);
|
|
33976
|
+
this.logger.log("OpenAI image generation request (variation endpoint):", {
|
|
33977
|
+
...opts,
|
|
33978
|
+
size: variationSize
|
|
33979
|
+
});
|
|
33078
33980
|
result = await openai.images.createVariation({
|
|
33079
33981
|
...opts,
|
|
33080
33982
|
image: imageFile,
|
|
33081
|
-
size:
|
|
33082
|
-
"256x256",
|
|
33083
|
-
"512x512",
|
|
33084
|
-
"1024x1024"
|
|
33085
|
-
].find((s) => s === openaiOptions.size)
|
|
33983
|
+
size: variationSize
|
|
33086
33984
|
});
|
|
33087
33985
|
}
|
|
33088
|
-
} else
|
|
33089
|
-
|
|
33090
|
-
|
|
33091
|
-
|
|
33986
|
+
} else {
|
|
33987
|
+
this.logger.log("OpenAI image generation request:", {
|
|
33988
|
+
prompt: truncatePromptForLog(prompt),
|
|
33989
|
+
...openaiOptions
|
|
33990
|
+
});
|
|
33991
|
+
result = await openai.images.generate({
|
|
33992
|
+
prompt,
|
|
33993
|
+
...openaiOptions
|
|
33994
|
+
});
|
|
33995
|
+
}
|
|
33092
33996
|
images = this.imageResponseToUrl(result);
|
|
33093
33997
|
return images;
|
|
33094
33998
|
} catch (error) {
|
|
@@ -33145,10 +34049,21 @@ var OpenAIImageService = class extends AIImageService {
|
|
|
33145
34049
|
Logger.globalInstance.debug(`[DEBUG] ⚠️ Edit endpoint doesn't support ${model}, defaulting to gpt-image-2`);
|
|
33146
34050
|
editModel = ImageModels.GPT_IMAGE_2;
|
|
33147
34051
|
}
|
|
34052
|
+
const forwardSize = isSupportedEditSize(editModel, size);
|
|
34053
|
+
this.logger.log("OpenAI image edit request:", {
|
|
34054
|
+
model: editModel,
|
|
34055
|
+
prompt: truncatePromptForLog(prompt),
|
|
34056
|
+
hasMask: !!maskFile,
|
|
34057
|
+
n,
|
|
34058
|
+
size,
|
|
34059
|
+
response_format
|
|
34060
|
+
});
|
|
33148
34061
|
const response = await openai.images.edit(isGPTImageModel(editModel) ? {
|
|
33149
34062
|
model: editModel,
|
|
33150
34063
|
image: [imageFile],
|
|
33151
|
-
prompt
|
|
34064
|
+
prompt,
|
|
34065
|
+
...forwardSize ? { size } : {},
|
|
34066
|
+
...maskFile ? { mask: maskFile } : {}
|
|
33152
34067
|
} : {
|
|
33153
34068
|
model: editModel,
|
|
33154
34069
|
image: imageFile,
|
|
@@ -35175,6 +36090,21 @@ var TiktokenTokenizer = class {
|
|
|
35175
36090
|
return Array.from(encoder.encode_ordinary(text));
|
|
35176
36091
|
}
|
|
35177
36092
|
/**
|
|
36093
|
+
* Decode token ids back to text through the same encoder encodeTokens used, so an
|
|
36094
|
+
* encode -> slice -> decode round trip yields real text rather than the ids themselves.
|
|
36095
|
+
* @param tokens - Token ids, typically a slice of an encodeTokens result
|
|
36096
|
+
* @param modelId - Model ID to determine encoding (must match the one used to encode)
|
|
36097
|
+
* @returns Promise<string> - The decoded text
|
|
36098
|
+
*
|
|
36099
|
+
* tiktoken's wasm decode() hands back raw UTF-8 bytes. A slice that ends mid-character therefore
|
|
36100
|
+
* decodes to a trailing U+FFFD; callers that sliced are expected to trim it.
|
|
36101
|
+
*/
|
|
36102
|
+
async decodeTokens(tokens, modelId, logger) {
|
|
36103
|
+
if (this.isShuttingDown) throw new Error("TiktokenTokenizer is shutting down");
|
|
36104
|
+
const encoder = await this.getEncoder(modelId, logger);
|
|
36105
|
+
return new TextDecoder().decode(encoder.decode(new Uint32Array(tokens)));
|
|
36106
|
+
}
|
|
36107
|
+
/**
|
|
35178
36108
|
* Returns a lightweight ITokenizer proxy that delegates WASM encoder operations
|
|
35179
36109
|
* to this instance (preserving the shared encoder cache) but routes log output
|
|
35180
36110
|
* through the provided logger. Useful for attaching per-request context (e.g.
|
|
@@ -35183,7 +36113,8 @@ var TiktokenTokenizer = class {
|
|
|
35183
36113
|
withLogger(logger) {
|
|
35184
36114
|
return {
|
|
35185
36115
|
countTokens: (text, modelId) => this.countTokens(text, modelId, logger),
|
|
35186
|
-
encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger)
|
|
36116
|
+
encodeTokens: (text, modelId) => this.encodeTokens(text, modelId, logger),
|
|
36117
|
+
decodeTokens: (tokens, modelId) => this.decodeTokens(tokens, modelId, logger)
|
|
35187
36118
|
};
|
|
35188
36119
|
}
|
|
35189
36120
|
/**
|
|
@@ -35643,6 +36574,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
35643
36574
|
BaseStorage: () => BaseStorage,
|
|
35644
36575
|
BedrockEmbeddingService: () => BedrockEmbeddingService,
|
|
35645
36576
|
CONTENT_TYPE_BY_FORMAT: () => CONTENT_TYPE_BY_FORMAT,
|
|
36577
|
+
COSINE_SEARCH_TOP_K: () => 10,
|
|
35646
36578
|
CacheKeys: () => CacheKeys,
|
|
35647
36579
|
ChunkSchema: () => ChunkSchema,
|
|
35648
36580
|
CircuitBreaker: () => CircuitBreaker,
|
|
@@ -35737,6 +36669,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
35737
36669
|
checkStorageLimit: () => checkStorageLimit,
|
|
35738
36670
|
checkStorageLimitForFile: () => checkStorageLimitForFile,
|
|
35739
36671
|
cleanMermaidSyntax: () => cleanMermaidSyntax,
|
|
36672
|
+
computeContentHash: () => computeContentHash,
|
|
35740
36673
|
computeCosineSimilarity: () => computeCosineSimilarity,
|
|
35741
36674
|
computeVerbatimTokenBudget: () => computeVerbatimTokenBudget,
|
|
35742
36675
|
convertCodeBlocksToArtifacts: () => convertCodeBlocksToArtifacts,
|
|
@@ -35808,6 +36741,7 @@ __reExport(/* @__PURE__ */ __exportAll({
|
|
|
35808
36741
|
registerLambdaErrorHandlers: () => registerLambdaErrorHandlers,
|
|
35809
36742
|
registerProcessErrorHandlers: () => registerProcessErrorHandlers,
|
|
35810
36743
|
registrableDomain: () => registrableDomain,
|
|
36744
|
+
reservationOutputTokens: () => reservationOutputTokens,
|
|
35811
36745
|
resolveEmbeddingConfig: () => resolveEmbeddingConfig,
|
|
35812
36746
|
resolveSupportedMimeType: () => resolveSupportedMimeType,
|
|
35813
36747
|
safeInputWindow: () => safeInputWindow,
|