@eddyskywalker/dsh-chatgpt-subscription 0.45.0 → 0.46.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/CHANGELOG.md +12 -0
- package/lib/client.js +19 -0
- package/lib/client.js.map +1 -1
- package/lib/index.js +856 -151
- package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
- package/lib/types/client/kimi-code/locales.d.ts +11 -2
- package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
- package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
- package/lib/types/compat.d.ts +1 -1
- package/lib/types/host/cache-warmup/budget.d.ts +19 -4
- package/lib/types/host/cache-warmup/budget.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/contracts.d.ts +13 -0
- package/lib/types/host/cache-warmup/contracts.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/index.d.ts +2 -2
- package/lib/types/host/cache-warmup/index.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/install.d.ts +14 -6
- package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/store.d.ts.map +1 -1
- package/lib/types/host/cache-warmup/usage.d.ts +10 -0
- package/lib/types/host/cache-warmup/usage.d.ts.map +1 -1
- package/lib/types/host/cline-pass/adapter.d.ts.map +1 -1
- package/lib/types/host/cline-pass/mapper.d.ts +10 -0
- package/lib/types/host/cline-pass/mapper.d.ts.map +1 -1
- package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
- package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
- package/lib/types/host/kimi-code/session-cache-state.d.ts +12 -0
- package/lib/types/host/kimi-code/session-cache-state.d.ts.map +1 -1
- package/lib/types/host/opendesign/adapter.d.ts +15 -0
- package/lib/types/host/opendesign/adapter.d.ts.map +1 -1
- package/lib/types/host/opendesign/mapper.d.ts +50 -3
- package/lib/types/host/opendesign/mapper.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/kimi-code-contracts.d.ts +13 -0
- package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -141,7 +141,7 @@ const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
|
|
|
141
141
|
* `test/responses-client.test.ts` asserts the two agree, so a release that
|
|
142
142
|
* forgets this constant fails the suite instead of shipping quietly.
|
|
143
143
|
*/
|
|
144
|
-
const PLUGIN_VERSION = "0.
|
|
144
|
+
const PLUGIN_VERSION = "0.46.0";
|
|
145
145
|
const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
|
|
146
146
|
const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
|
|
147
147
|
const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
|
|
@@ -24710,6 +24710,19 @@ const CACHE_WARMUP_DEFAULTS = {
|
|
|
24710
24710
|
hitRatioThreshold: .97,
|
|
24711
24711
|
/** 退出预热期需要连续命中的报告条数。 */
|
|
24712
24712
|
consecutiveHitsRequired: 2,
|
|
24713
|
+
/**
|
|
24714
|
+
* 稳态第二轨(短会话分母效应兜底):已命中的缓存前缀至少达到该 token 规模,
|
|
24715
|
+
* 且相比上一条报告一字未掉(`cacheReadTokens >= prev.cacheReadTokens`),
|
|
24716
|
+
* 同时本轮新增未命中部分不超过 `retainedPrefixMaxFreshTokens` 时,同样计为一次有效命中。
|
|
24717
|
+
*/
|
|
24718
|
+
retainedPrefixMinCachedTokens: 1024,
|
|
24719
|
+
/** 稳态第二轨允许的单轮最大新增未命中 token 数(与 Kimi 256-token 缓存块对齐)。 */
|
|
24720
|
+
retainedPrefixMaxFreshTokens: 256,
|
|
24721
|
+
/**
|
|
24722
|
+
* 缓存存活窗口(毫秒,25 分钟,与 Kimi 实测全热上限一致)。
|
|
24723
|
+
* 距离最近一次报告超过该时长时,预算计算自动预判缓存已冷、退回预热期保守预算。
|
|
24724
|
+
*/
|
|
24725
|
+
cacheTtlMs: 1500 * 1e3,
|
|
24713
24726
|
/** 单结果原文上限(字节)。 */
|
|
24714
24727
|
maxResultBytes: 8 * 1024 * 1024,
|
|
24715
24728
|
/** 每 session 原文总量上限(字节)。 */
|
|
@@ -24773,12 +24786,13 @@ function cellOf(value) {
|
|
|
24773
24786
|
return "";
|
|
24774
24787
|
}
|
|
24775
24788
|
}
|
|
24776
|
-
/** JSON Lines
|
|
24789
|
+
/** JSON Lines:每一行都是一个结构一致的表格式对象,且至少 5 行(防止 2~3 个异构 tool 结果对象被当成一张表压掉)。 */
|
|
24777
24790
|
function parseJsonLines(lines) {
|
|
24778
|
-
if (lines.length <
|
|
24791
|
+
if (lines.length < 5) return void 0;
|
|
24779
24792
|
const fields = [];
|
|
24780
24793
|
const seen = /* @__PURE__ */ new Set();
|
|
24781
24794
|
const rows = [];
|
|
24795
|
+
let firstKeySet;
|
|
24782
24796
|
for (const line of lines) {
|
|
24783
24797
|
let parsed;
|
|
24784
24798
|
try {
|
|
@@ -24788,6 +24802,10 @@ function parseJsonLines(lines) {
|
|
|
24788
24802
|
}
|
|
24789
24803
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
24790
24804
|
const record = parsed;
|
|
24805
|
+
const keys = Object.keys(record);
|
|
24806
|
+
if (keys.length === 0) return void 0;
|
|
24807
|
+
if (firstKeySet === void 0) firstKeySet = new Set(keys);
|
|
24808
|
+
else if (!keys.some((k) => firstKeySet.has(k))) return void 0;
|
|
24791
24809
|
const row = {};
|
|
24792
24810
|
for (const [key, value] of Object.entries(record)) {
|
|
24793
24811
|
if (!seen.has(key)) {
|
|
@@ -24804,7 +24822,7 @@ function parseJsonLines(lines) {
|
|
|
24804
24822
|
rows
|
|
24805
24823
|
};
|
|
24806
24824
|
}
|
|
24807
|
-
/**
|
|
24825
|
+
/** 分隔符表格:首行合法表头(标识符列名,非 grep 路径行号或代码行)+ 至少两行同列数的数据。 */
|
|
24808
24826
|
function parseDelimitedRows(lines) {
|
|
24809
24827
|
if (lines.length < 3) return void 0;
|
|
24810
24828
|
const header = lines[0];
|
|
@@ -24812,6 +24830,7 @@ function parseDelimitedRows(lines) {
|
|
|
24812
24830
|
if (delimiter === void 0) return void 0;
|
|
24813
24831
|
const fields = splitDelimitedLine(header, delimiter).map((cell) => cell.trim());
|
|
24814
24832
|
if (fields.length < 2 || fields.some((field) => field === "")) return void 0;
|
|
24833
|
+
if (fields.some((field) => field.length > 64 || /[:;{}()[\]\\/*'"`]/.test(field) || /^\d+$/.test(field))) return;
|
|
24815
24834
|
if (new Set(fields).size !== fields.length) return void 0;
|
|
24816
24835
|
const rows = [];
|
|
24817
24836
|
for (const line of lines.slice(1)) {
|
|
@@ -24916,12 +24935,40 @@ function clampMaxBytes(maxBytes) {
|
|
|
24916
24935
|
if (rounded < 1) return 1;
|
|
24917
24936
|
return Math.min(rounded, CACHE_WARMUP_DEFAULTS.pageMaxBytesMax);
|
|
24918
24937
|
}
|
|
24919
|
-
/**
|
|
24920
|
-
|
|
24938
|
+
/**
|
|
24939
|
+
* 判断一段文本是否为不含真实换行(或几乎全在一行内)的 JSON 转义文本。
|
|
24940
|
+
*
|
|
24941
|
+
* 当 `run_code` 返回 `JSON.stringify({ stdout: "a\nb\nc..." })` 时,原文逐字保存在
|
|
24942
|
+
* 存储里(不改任何字节),但整份文本里没有真 `\n`,只有字面量两字符序列 `\n`。
|
|
24943
|
+
* 若只数真 `\n`,`limit: 64` 行会退化成一次吐完 `maxBytes`。识别出这种形态后,
|
|
24944
|
+
* `pageText` 和 `countLines` 把未转义的 `\n`(即前面有奇数个 `\` 的 `n`)也算作逻辑换行,
|
|
24945
|
+
* 既 100% 保持「翻页拼起来逐字节等于原文」,又让 `limit` 行上限对单行 JSON 同样生效。
|
|
24946
|
+
*/
|
|
24947
|
+
function usesEscapedNewlines(text) {
|
|
24948
|
+
if (text.length < 4) return false;
|
|
24949
|
+
let realNewlines = 0;
|
|
24950
|
+
for (let i = 0; i < text.length; i += 1) if (text.charCodeAt(i) === 10) {
|
|
24951
|
+
realNewlines += 1;
|
|
24952
|
+
if (realNewlines > 2) return false;
|
|
24953
|
+
}
|
|
24954
|
+
return text.includes("\\n");
|
|
24955
|
+
}
|
|
24956
|
+
/** 判断 `text[index]`(已知为 `'n'`)是否构成一个 JSON 转义换行 `\n`(前面紧邻奇数个 `\`)。 */
|
|
24957
|
+
function isEscapedNewlineAt(text, index) {
|
|
24958
|
+
let backslashes = 0;
|
|
24959
|
+
for (let cursor = index - 1; cursor >= 0 && text.charCodeAt(cursor) === 92; cursor -= 1) backslashes += 1;
|
|
24960
|
+
return (backslashes & 1) === 1;
|
|
24961
|
+
}
|
|
24962
|
+
/** 一段文本的行数(末尾没有换行也算一行;支持单行 JSON 内的转义 `\n`)。 */
|
|
24963
|
+
function countLines(text, escapedMode = usesEscapedNewlines(text)) {
|
|
24921
24964
|
if (text === "") return 0;
|
|
24922
24965
|
let lines = 0;
|
|
24923
|
-
for (
|
|
24924
|
-
|
|
24966
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
24967
|
+
const code = text.charCodeAt(index);
|
|
24968
|
+
if (code === 10) lines += 1;
|
|
24969
|
+
else if (escapedMode && code === 110 && isEscapedNewlineAt(text, index)) lines += 1;
|
|
24970
|
+
}
|
|
24971
|
+
return text.endsWith("\n") || escapedMode && text.endsWith("n") && isEscapedNewlineAt(text, text.length - 1) ? lines : lines + 1;
|
|
24925
24972
|
}
|
|
24926
24973
|
/**
|
|
24927
24974
|
* 按 UTF-8 字节安全分页。
|
|
@@ -24937,6 +24984,7 @@ function pageText(args) {
|
|
|
24937
24984
|
if (!Number.isInteger(offset) || offset < 0) throw new CacheWarmupStoreError("offset must be a non-negative integer byte offset", "OFFSET_OUT_OF_RANGE");
|
|
24938
24985
|
if (offset > totalBytes) throw new CacheWarmupStoreError(`offset ${offset} is past the ${totalBytes}-byte result`, "OFFSET_OUT_OF_RANGE");
|
|
24939
24986
|
if (offset < totalBytes && (buffer[offset] & 192) === 128) throw new CacheWarmupStoreError("offset is not on a UTF-8 character boundary", "OFFSET_INVALID");
|
|
24987
|
+
const escapedMode = usesEscapedNewlines(text);
|
|
24940
24988
|
const rest = offset === 0 ? text : new TextDecoder("utf-8").decode(buffer.subarray(offset));
|
|
24941
24989
|
let content = "";
|
|
24942
24990
|
let contentBytes = 0;
|
|
@@ -24960,6 +25008,7 @@ function pageText(args) {
|
|
|
24960
25008
|
content += char;
|
|
24961
25009
|
contentBytes += charBytes;
|
|
24962
25010
|
if (char === "\n") lines += 1;
|
|
25011
|
+
else if (escapedMode && char === "n" && isEscapedNewlineAt(content, content.length - 1)) lines += 1;
|
|
24963
25012
|
}
|
|
24964
25013
|
const nextOffset = offset + Buffer.byteLength(content, "utf8");
|
|
24965
25014
|
const complete = nextOffset >= totalBytes;
|
|
@@ -24975,7 +25024,7 @@ function pageText(args) {
|
|
|
24975
25024
|
omitted: {
|
|
24976
25025
|
reason: complete ? "none" : stoppedByBytes ? "bytes" : stoppedByLines ? "lines" : "bytes",
|
|
24977
25026
|
chars: remaining.length,
|
|
24978
|
-
lines: countLines(remaining),
|
|
25027
|
+
lines: countLines(remaining, escapedMode),
|
|
24979
25028
|
rows: 0,
|
|
24980
25029
|
bytes: Buffer.byteLength(remaining, "utf8")
|
|
24981
25030
|
},
|
|
@@ -25309,6 +25358,28 @@ function cacheWarmupHitRatio(usage) {
|
|
|
25309
25358
|
*/
|
|
25310
25359
|
/** Kimi 对每个请求都会报告的生命周期状态;只有其余值才表示真实前缀漂移。 */
|
|
25311
25360
|
const BENIGN_LIFECYCLE_CAUSES = /* @__PURE__ */ new Set(["stable", "first-request"]);
|
|
25361
|
+
/**
|
|
25362
|
+
* 判断一条报告是否算作「预热命中」(双轨判据):
|
|
25363
|
+
*
|
|
25364
|
+
* 1. **第一轨(比例轨)**:`hitRatio >= 0.97`;
|
|
25365
|
+
* 2. **第二轨(前缀完整延续轨)**:解决短会话的分母效应(如 `1792 cached + 84 input = 95.52%`)。
|
|
25366
|
+
* 当本轮 `cacheReadTokens >= 1024`(已形成实质缓存前缀)、存在紧邻的上一条无真实漂移报告、
|
|
25367
|
+
* 本轮命中缓存没有比上一轮缩水(`cacheReadTokens >= previous.cacheReadTokens`),
|
|
25368
|
+
* 且本轮新增未命中部分 `inputTokens <= 256`(一个缓存块的正常尾巴)时,同样视为有效命中。
|
|
25369
|
+
*/
|
|
25370
|
+
function isWarmupHitReport(report, previous) {
|
|
25371
|
+
if (cacheWarmupHitRatio(report) >= CACHE_WARMUP_DEFAULTS.hitRatioThreshold) return true;
|
|
25372
|
+
if (previous === void 0 || previous === null) return false;
|
|
25373
|
+
const prevDrift = previous.driftCause;
|
|
25374
|
+
if (typeof prevDrift === "string" && prevDrift.trim() !== "" && !BENIGN_LIFECYCLE_CAUSES.has(prevDrift)) return false;
|
|
25375
|
+
const cached = finiteCount(report.cacheReadTokens);
|
|
25376
|
+
const prevCached = finiteCount(previous.cacheReadTokens);
|
|
25377
|
+
const fresh = finiteCount(report.inputTokens);
|
|
25378
|
+
if (cached < CACHE_WARMUP_DEFAULTS.retainedPrefixMinCachedTokens) return false;
|
|
25379
|
+
if (prevCached < CACHE_WARMUP_DEFAULTS.retainedPrefixMinCachedTokens) return false;
|
|
25380
|
+
if (cached < prevCached) return false;
|
|
25381
|
+
return fresh <= CACHE_WARMUP_DEFAULTS.retainedPrefixMaxFreshTokens;
|
|
25382
|
+
}
|
|
25312
25383
|
function consecutiveWarmupHits(reports) {
|
|
25313
25384
|
try {
|
|
25314
25385
|
if (!Array.isArray(reports)) return 0;
|
|
@@ -25318,7 +25389,7 @@ function consecutiveWarmupHits(reports) {
|
|
|
25318
25389
|
if (report === void 0 || report === null) break;
|
|
25319
25390
|
const drift = report.driftCause;
|
|
25320
25391
|
if (typeof drift === "string" && drift.trim() !== "" && !BENIGN_LIFECYCLE_CAUSES.has(drift)) break;
|
|
25321
|
-
if (
|
|
25392
|
+
if (!isWarmupHitReport(report, index > 0 ? reports[index - 1] : void 0)) break;
|
|
25322
25393
|
hits += 1;
|
|
25323
25394
|
}
|
|
25324
25395
|
return hits;
|
|
@@ -25447,21 +25518,41 @@ const WARMUP_BUDGET = Object.freeze({
|
|
|
25447
25518
|
* 当前会话的预览预算。
|
|
25448
25519
|
*
|
|
25449
25520
|
* 规则(约束 4):预热期 `maxChars = 1200`(约 600-750 tokens);连续
|
|
25450
|
-
* `consecutiveHitsRequired`(2)
|
|
25451
|
-
* `maxChars = 8000`。任何一次 drift
|
|
25521
|
+
* `consecutiveHitsRequired`(2) 次命中(比例 ≥ 0.97 或前缀完整延续且新增 ≤ 256 tokens)
|
|
25522
|
+
* 且期间没有 drift,才进入稳态 `maxChars = 8000`。任何一次 drift 都清零连续计数,
|
|
25523
|
+
* 于是下一条请求立刻回落到预热预算。
|
|
25524
|
+
*
|
|
25525
|
+
* 空闲 TTL 预判:若距离最近一条报告已超过 `cacheTtlMs`(25 分钟,Kimi 实测缓存
|
|
25526
|
+
* 全热上限),说明上游缓存大概率已冷,无需等下一轮请求失败,当场退回预热期保守预算。
|
|
25452
25527
|
*
|
|
25453
25528
|
* **不抛出**:没有会话、没有报告、登记表异常,一律返回预热期的保守预算。
|
|
25454
25529
|
* @param sessionId - 会话 id;不传则按最保守的预热预算处理。
|
|
25530
|
+
* @param now - 当前时间戳(毫秒),默认 `Date.now()`;测试可注入。
|
|
25531
|
+
* @param durable - 进程重启后尚无内存报告时的持久化回退摘要(可选)。
|
|
25455
25532
|
* @returns 本次投影可用的预算。
|
|
25456
25533
|
*/
|
|
25457
|
-
function cacheWarmupBudget(sessionId) {
|
|
25534
|
+
function cacheWarmupBudget(sessionId, now = Date.now(), durable) {
|
|
25458
25535
|
try {
|
|
25459
25536
|
if (typeof sessionId !== "string" || sessionId.trim() === "") return { ...WARMUP_BUDGET };
|
|
25460
25537
|
const reports = cacheWarmupUsageReports(sessionId);
|
|
25461
|
-
if (reports.length === 0)
|
|
25462
|
-
|
|
25538
|
+
if (reports.length === 0) {
|
|
25539
|
+
if (durable !== void 0 && durable !== null && typeof durable === "object") {
|
|
25540
|
+
const lastAt = durable.lastActiveAt;
|
|
25541
|
+
const hits = typeof lastAt === "number" && Number.isFinite(lastAt) && Number.isFinite(now) && now - lastAt <= CACHE_WARMUP_DEFAULTS.cacheTtlMs && typeof durable.consecutiveHits === "number" && Number.isFinite(durable.consecutiveHits) && durable.consecutiveHits > 0 ? Math.floor(durable.consecutiveHits) : 0;
|
|
25542
|
+
const hitRatio = typeof durable.hitRatio === "number" && Number.isFinite(durable.hitRatio) && durable.hitRatio >= 0 ? Math.min(1, durable.hitRatio) : 0;
|
|
25543
|
+
const warmup = hits < CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired;
|
|
25544
|
+
return {
|
|
25545
|
+
maxChars: warmup ? CACHE_WARMUP_DEFAULTS.warmupMaxChars : CACHE_WARMUP_DEFAULTS.steadyStateMaxChars,
|
|
25546
|
+
warmup,
|
|
25547
|
+
consecutiveHits: hits,
|
|
25548
|
+
hitRatio
|
|
25549
|
+
};
|
|
25550
|
+
}
|
|
25551
|
+
return { ...WARMUP_BUDGET };
|
|
25552
|
+
}
|
|
25463
25553
|
const last = reports[reports.length - 1];
|
|
25464
25554
|
const hitRatio = last === void 0 ? 0 : cacheWarmupHitRatio(last);
|
|
25555
|
+
const consecutiveHits = last !== void 0 && typeof last.at === "number" && Number.isFinite(last.at) && Number.isFinite(now) && now - last.at > CACHE_WARMUP_DEFAULTS.cacheTtlMs ? 0 : consecutiveWarmupHits(reports);
|
|
25465
25556
|
const warmup = consecutiveHits < CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired;
|
|
25466
25557
|
return {
|
|
25467
25558
|
maxChars: warmup ? CACHE_WARMUP_DEFAULTS.warmupMaxChars : CACHE_WARMUP_DEFAULTS.steadyStateMaxChars,
|
|
@@ -25476,9 +25567,10 @@ function cacheWarmupBudget(sessionId) {
|
|
|
25476
25567
|
/**
|
|
25477
25568
|
* 一份可读快照。**不抛出**,且完全无副作用(只读登记表)。
|
|
25478
25569
|
* @param sessionId - 会话 id;不传则给全局汇总。
|
|
25570
|
+
* @param now - 当前时间戳(毫秒),默认 `Date.now()`。
|
|
25479
25571
|
* @returns 快照。
|
|
25480
25572
|
*/
|
|
25481
|
-
function cacheWarmupSnapshot(sessionId) {
|
|
25573
|
+
function cacheWarmupSnapshot(sessionId, now = Date.now()) {
|
|
25482
25574
|
const fallback = {
|
|
25483
25575
|
budget: { ...WARMUP_BUDGET },
|
|
25484
25576
|
defaults: CACHE_WARMUP_DEFAULTS,
|
|
@@ -25496,7 +25588,7 @@ function cacheWarmupSnapshot(sessionId) {
|
|
|
25496
25588
|
sessions: 0
|
|
25497
25589
|
};
|
|
25498
25590
|
try {
|
|
25499
|
-
const budget = cacheWarmupBudget(sessionId);
|
|
25591
|
+
const budget = cacheWarmupBudget(sessionId, now);
|
|
25500
25592
|
const reports = typeof sessionId === "string" && sessionId.trim() !== "" ? cacheWarmupUsageReports(sessionId) : [];
|
|
25501
25593
|
const totals = {
|
|
25502
25594
|
inputTokens: 0,
|
|
@@ -25559,26 +25651,47 @@ function clip(text, max) {
|
|
|
25559
25651
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
25560
25652
|
}
|
|
25561
25653
|
/**
|
|
25654
|
+
* 剥离终端 ANSI/CSI 转义序列(如 `\x1b[31mFAIL\x1b[0m`)与回车符 `\r`。
|
|
25655
|
+
*
|
|
25656
|
+
* JSON 解码后,原先源码/日志里的 `\u001b[31m` 会还原成控制字符 `\x1b`;
|
|
25657
|
+
* 如果不剥离,`^FAIL` 锚点匹配会被颜色码挡住,而再次 `JSON.stringify` 时每个
|
|
25658
|
+
* `\x1b` 又会膨胀成 6 个字符 `\u001b`。
|
|
25659
|
+
*/
|
|
25660
|
+
const ANSI_ESCAPE = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
25661
|
+
function stripAnsi(text) {
|
|
25662
|
+
return text.replace(ANSI_ESCAPE, "").replace(/\r/g, "");
|
|
25663
|
+
}
|
|
25664
|
+
/**
|
|
25665
|
+
* 计算一行文本在最终输出里占用的精确字符宽度:
|
|
25666
|
+
* - 普通纯文本模式:`line.length + 1`(含换行符 `\n`);
|
|
25667
|
+
* - JSON 字符串内嵌模式:`JSON.stringify(line).length`(去首尾引号后 `+ 2` 个字符的 `\\n` 换行转义,
|
|
25668
|
+
* 恰好等于 `JSON.stringify(line).length`,对 `"`、`\`、`\t`、`\u00xx` 的转义膨胀 100% 精确)。
|
|
25669
|
+
*/
|
|
25670
|
+
function lineCost(line, jsonEscaped) {
|
|
25671
|
+
return jsonEscaped ? JSON.stringify(line).length : line.length + 1;
|
|
25672
|
+
}
|
|
25673
|
+
/**
|
|
25562
25674
|
* 按预算装配预览正文。
|
|
25563
25675
|
*
|
|
25564
25676
|
* 必需段落要么整段进来,要么整体装不下就返回 undefined(调用方据此保持原文);
|
|
25565
25677
|
* 可选段落逐段尝试,装不下就整段跳过。这样任何输出都不会出现「半截失败清单」。
|
|
25566
25678
|
*/
|
|
25567
|
-
function fitSections(sections, maxChars) {
|
|
25679
|
+
function fitSections(sections, maxChars, jsonEscaped = false) {
|
|
25568
25680
|
const lines = [];
|
|
25569
25681
|
let used = 0;
|
|
25682
|
+
const trailingNewlineAllowance = jsonEscaped ? 2 : 1;
|
|
25570
25683
|
for (const section of sections) {
|
|
25571
25684
|
if (section.optional === true) continue;
|
|
25572
25685
|
for (const line of section.lines) {
|
|
25573
|
-
used += line
|
|
25686
|
+
used += lineCost(line, jsonEscaped);
|
|
25574
25687
|
lines.push(line);
|
|
25575
25688
|
}
|
|
25576
25689
|
}
|
|
25577
|
-
if (used > maxChars +
|
|
25690
|
+
if (used > maxChars + trailingNewlineAllowance) return void 0;
|
|
25578
25691
|
for (const section of sections) {
|
|
25579
25692
|
if (section.optional !== true) continue;
|
|
25580
|
-
const size = section.lines.reduce((sum, line) => sum + line
|
|
25581
|
-
if (used + size > maxChars +
|
|
25693
|
+
const size = section.lines.reduce((sum, line) => sum + lineCost(line, jsonEscaped), 0);
|
|
25694
|
+
if (used + size > maxChars + trailingNewlineAllowance) continue;
|
|
25582
25695
|
for (const line of section.lines) lines.push(line);
|
|
25583
25696
|
used += size;
|
|
25584
25697
|
}
|
|
@@ -25613,51 +25726,140 @@ function buildTestSections(evidence) {
|
|
|
25613
25726
|
}
|
|
25614
25727
|
/** 一行 shell 错误。用于「完整错误集」,所以匹配宁可宽一点。 */
|
|
25615
25728
|
const SHELL_ERROR_LINE = /(?:error|fatal|panic|traceback|exception|EACCES|EPERM|ENOENT|EEXIST|ELIFECYCLE|ERR!|FAILED|permission denied|cannot |could not |not found)/i;
|
|
25616
|
-
/**
|
|
25729
|
+
/** TypeScript 编译器报错首行:`path(line,col): error TSxxxx:` 或 `path:line:col - error TSxxxx:`。 */
|
|
25730
|
+
const TS_DIAGNOSTIC_HEADER = /^(?:.+?\(\d+,\d+\):\s+error\s+TS\d+:|.+?:\d+:\d+\s+-\s+error\s+TS\d+:)/;
|
|
25731
|
+
/**
|
|
25732
|
+
* 紧跟在 TypeScript 诊断首行后面的续行(缩进的类型比对、代码高亮行、波浪线指示行),
|
|
25733
|
+
* 最多连带收集 6 行,使模型无需补读就能看全 `Type A is not assignable to Type B` 的完整原因。
|
|
25734
|
+
*/
|
|
25735
|
+
function isTsDiagnosticDetailLine(raw) {
|
|
25736
|
+
if (/^\s{2,}\S/.test(raw)) return true;
|
|
25737
|
+
if (/^\d+\s+\|/.test(raw.trimStart())) return true;
|
|
25738
|
+
if (/^~+$/.test(raw.trim())) return true;
|
|
25739
|
+
return false;
|
|
25740
|
+
}
|
|
25741
|
+
/** 一行 glob 结果的判定:无空白、像文件路径(至少 80% 含路径分隔符或文件扩展名,不含引号/括号/冒号行号)。 */
|
|
25617
25742
|
function looksLikeGlobResult(text) {
|
|
25618
25743
|
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
25619
25744
|
if (lines.length < 10) return false;
|
|
25620
25745
|
let paths = 0;
|
|
25746
|
+
let pathWithSepOrExt = 0;
|
|
25621
25747
|
for (const line of lines) {
|
|
25622
25748
|
const trimmed = line.trim();
|
|
25623
|
-
if (trimmed.length === 0 || trimmed.length > 400)
|
|
25749
|
+
if (trimmed.length === 0 || trimmed.length > 400) return false;
|
|
25750
|
+
if (GREP_LINE.test(trimmed)) return false;
|
|
25751
|
+
if (/^===.*===$/.test(trimmed)) return false;
|
|
25752
|
+
if (/['"[\]{}()]/.test(trimmed)) return false;
|
|
25624
25753
|
if (/\s/.test(trimmed)) continue;
|
|
25625
25754
|
if (/^[-*+]$/.test(trimmed)) continue;
|
|
25626
25755
|
paths += 1;
|
|
25756
|
+
if (/[\\/]/.test(trimmed) || /\.[A-Za-z0-9]{1,8}$/.test(trimmed)) pathWithSepOrExt += 1;
|
|
25757
|
+
}
|
|
25758
|
+
return paths / lines.length >= .9 && pathWithSepOrExt / lines.length >= .8;
|
|
25759
|
+
}
|
|
25760
|
+
/** 判断一行是否只是单纯的文件路径(含可选引号/逗号,如 `"test\\stream-error.test.ts",`),而非报错消息。 */
|
|
25761
|
+
function isBareFilePathLine(trimmed) {
|
|
25762
|
+
const unquoted = trimmed.replace(/^['"]|['"],?$/g, "").trim();
|
|
25763
|
+
if (unquoted.length === 0 || unquoted.length > 300 || /\s/.test(unquoted)) return false;
|
|
25764
|
+
return /[\\/]/.test(unquoted) && /\.[A-Za-z0-9]{1,8}$/.test(unquoted) && !/:(\d+)/.test(unquoted);
|
|
25765
|
+
}
|
|
25766
|
+
/**
|
|
25767
|
+
* 判断文本是否为混合证据(如包含已投影的 `[cache-warmup preview]`、内嵌大型 JSON 对象行、
|
|
25768
|
+
* 多行 pretty-printed JSON 块、`util.inspect` 字符串拼接对象、分节头等)。
|
|
25769
|
+
*/
|
|
25770
|
+
function isMixedEvidenceOutput(text) {
|
|
25771
|
+
if (text.includes("[cache-warmup preview]")) return true;
|
|
25772
|
+
if (/^(?:===|---)\s+.+?(?:\s+(?:===|---))?$/m.test(text) || /^###\s+\S/m.test(text) || /^--\s+.+\s+--$/m.test(text)) return true;
|
|
25773
|
+
const lines = text.split("\n");
|
|
25774
|
+
let inspectStringConcat = 0;
|
|
25775
|
+
let jsonPropertyLines = 0;
|
|
25776
|
+
for (const raw of lines.slice(0, 200)) {
|
|
25777
|
+
const trimmed = raw.trim();
|
|
25778
|
+
if (trimmed === "") continue;
|
|
25779
|
+
if (trimmed.length > 80 && (trimmed.startsWith("{\"") && (trimmed.endsWith("}") || trimmed.includes("\":")) || trimmed.startsWith("[{\"") && trimmed.includes("\":"))) return true;
|
|
25780
|
+
if (/^"[A-Za-z0-9_$-]+"\s*:/.test(trimmed)) {
|
|
25781
|
+
jsonPropertyLines += 1;
|
|
25782
|
+
if (jsonPropertyLines >= 2) return true;
|
|
25783
|
+
}
|
|
25784
|
+
if (/^['"].*\\n['"]\s*\+$/.test(trimmed)) {
|
|
25785
|
+
inspectStringConcat += 1;
|
|
25786
|
+
if (inspectStringConcat >= 2) return true;
|
|
25787
|
+
}
|
|
25627
25788
|
}
|
|
25628
|
-
return
|
|
25789
|
+
return false;
|
|
25629
25790
|
}
|
|
25630
|
-
/**
|
|
25791
|
+
/**
|
|
25792
|
+
* shell 输出判定:
|
|
25793
|
+
* - 排除混合证据输出(内嵌 JSON 行、`[cache-warmup preview]`、分节头、`util.inspect` 对象);
|
|
25794
|
+
* - 有明确的退出码标记(`[exit code: N]`、`exit code: N`、`EXIT=N`);
|
|
25795
|
+
* - 有 Python `Traceback (most recent call last):` 或 JS 异常头 + 堆栈行(`^\s+at\s+`);
|
|
25796
|
+
* - 若没有任何退出码或堆栈,只有纯文本行时,要求不存在 grep 行,且错误行不是单纯的文件名。
|
|
25797
|
+
*/
|
|
25631
25798
|
function looksLikeShellOutput(text) {
|
|
25799
|
+
if (isMixedEvidenceOutput(text)) return false;
|
|
25632
25800
|
if (/(?:exit code|exited with code|process exited with code)[:=\s]+-?\d+/i.test(text)) return true;
|
|
25633
25801
|
if (/\[exit code: -?\d+\]/i.test(text)) return true;
|
|
25802
|
+
if (/^EXIT=-?\d+$/m.test(text)) return true;
|
|
25634
25803
|
if (/^Traceback \(most recent call last\):/m.test(text)) return true;
|
|
25804
|
+
if (/^(?:[A-Za-z]*Error|Exception): /m.test(text) && /^\s+at\s+\S/m.test(text)) return true;
|
|
25635
25805
|
const lines = text.split("\n");
|
|
25806
|
+
let grepLikes = 0;
|
|
25636
25807
|
let errors = 0;
|
|
25637
25808
|
for (const line of lines) {
|
|
25638
|
-
|
|
25639
|
-
if (
|
|
25809
|
+
const trimmed = line.trim();
|
|
25810
|
+
if (trimmed === "") continue;
|
|
25811
|
+
if (/^(?:PASS\b|[✓✔]|ok\s+\d+\b)/.test(trimmed)) continue;
|
|
25812
|
+
if (/^stdout\s+\|\s+/.test(trimmed)) continue;
|
|
25813
|
+
if (/^\(node:\d+\)\s+\[\w+\]\s+Warning:/.test(trimmed)) continue;
|
|
25814
|
+
if (isBareFilePathLine(trimmed)) continue;
|
|
25815
|
+
if (GREP_LINE.test(trimmed) && !TS_DIAGNOSTIC_HEADER.test(trimmed)) {
|
|
25816
|
+
grepLikes += 1;
|
|
25817
|
+
if (grepLikes >= 2) return false;
|
|
25818
|
+
}
|
|
25819
|
+
if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(trimmed)) continue;
|
|
25640
25820
|
if (SHELL_ERROR_LINE.test(line)) errors += 1;
|
|
25641
|
-
if (errors >= 1) return true;
|
|
25642
25821
|
}
|
|
25643
|
-
return
|
|
25822
|
+
return errors >= 1;
|
|
25644
25823
|
}
|
|
25645
|
-
/** shell 预览:退出码 +
|
|
25824
|
+
/** shell 预览:退出码 + 完整错误集(必需,含 TS 多行诊断上下文),首尾片段(可选)。 */
|
|
25646
25825
|
function buildShellSections(text) {
|
|
25647
25826
|
const lines = text.split("\n");
|
|
25648
25827
|
const errors = [];
|
|
25649
|
-
|
|
25650
|
-
|
|
25828
|
+
let errorCount = 0;
|
|
25829
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
25830
|
+
const trimmed = lines[index].trim();
|
|
25651
25831
|
if (trimmed === "") continue;
|
|
25652
25832
|
if (/^\[exit code: -?\d+\]/i.test(trimmed)) continue;
|
|
25653
25833
|
if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(trimmed)) continue;
|
|
25654
|
-
if (
|
|
25834
|
+
if (/^(?:PASS\b|[✓✔]|ok\s+\d+\b)/.test(trimmed)) continue;
|
|
25835
|
+
if (/^stdout\s+\|\s+/.test(trimmed)) continue;
|
|
25836
|
+
if (/^\(node:\d+\)\s+\[\w+\]\s+Warning:/.test(trimmed)) continue;
|
|
25837
|
+
if (isBareFilePathLine(trimmed)) continue;
|
|
25838
|
+
if (TS_DIAGNOSTIC_HEADER.test(trimmed)) {
|
|
25839
|
+
errorCount += 1;
|
|
25840
|
+
errors.push(` ${clip(trimmed, 240)}`);
|
|
25841
|
+
let details = 0;
|
|
25842
|
+
while (index + 1 < lines.length && details < 6) {
|
|
25843
|
+
const nextRaw = lines[index + 1];
|
|
25844
|
+
const nextTrimmed = nextRaw.trim();
|
|
25845
|
+
if (nextTrimmed === "" || TS_DIAGNOSTIC_HEADER.test(nextTrimmed) || /^\[exit code: -?\d+\]/i.test(nextTrimmed)) break;
|
|
25846
|
+
if (!isTsDiagnosticDetailLine(nextRaw)) break;
|
|
25847
|
+
errors.push(` ${clip(nextTrimmed, 240)}`);
|
|
25848
|
+
index += 1;
|
|
25849
|
+
details += 1;
|
|
25850
|
+
}
|
|
25851
|
+
continue;
|
|
25852
|
+
}
|
|
25853
|
+
if (SHELL_ERROR_LINE.test(trimmed)) {
|
|
25854
|
+
errorCount += 1;
|
|
25855
|
+
errors.push(` ${clip(trimmed, 360)}`);
|
|
25856
|
+
}
|
|
25655
25857
|
}
|
|
25656
|
-
if (errors.length >
|
|
25858
|
+
if (errorCount > 300 || errors.length > 600) return void 0;
|
|
25657
25859
|
const exitMatch = /(?:exit code|exited with code|process exited with code)[:=\s]+(-?\d+)/i.exec(text) ?? /\[exit code: (-?\d+)\]/i.exec(text);
|
|
25658
25860
|
const sections = [];
|
|
25659
25861
|
if (exitMatch !== null) sections.push({ lines: [`exit code: ${exitMatch[1]}`] });
|
|
25660
|
-
sections.push({ lines: [`errors: ${
|
|
25862
|
+
sections.push({ lines: [`errors: ${errorCount} (complete set)`] });
|
|
25661
25863
|
if (errors.length > 0) sections.push({ lines: errors });
|
|
25662
25864
|
const nonEmpty = lines.map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
|
|
25663
25865
|
const head = nonEmpty.slice(0, 5).map((line) => ` ${clip(line.trim(), 160)}`);
|
|
@@ -25672,8 +25874,8 @@ function buildShellSections(text) {
|
|
|
25672
25874
|
});
|
|
25673
25875
|
return sections;
|
|
25674
25876
|
}
|
|
25675
|
-
/** 一行 grep 结果:`<path>:<line>[:<column>]:<text>`。 */
|
|
25676
|
-
const GREP_LINE = /^(.+?):(\d+)
|
|
25877
|
+
/** 一行 grep 结果:`<path>:<line>[:<column>]:<text>` 或 `<path>:<line>(?:\t|\||\s{2,})<text>`。 */
|
|
25878
|
+
const GREP_LINE = /^(.+?):(\d+)(?::(?:\d+:)?|\t|\|\s?|\s{2,})(.*)$/;
|
|
25677
25879
|
/** 抽取 grep 证据;命中线占比不到 80% 就当它不是 grep 输出。 */
|
|
25678
25880
|
function extractGrepEvidence(text) {
|
|
25679
25881
|
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
@@ -25742,36 +25944,58 @@ function buildStructuredSections(text) {
|
|
|
25742
25944
|
optional: true
|
|
25743
25945
|
}];
|
|
25744
25946
|
}
|
|
25947
|
+
/** 剥离 `read` 工具或 `grep -C` 常见的行号前缀(如 `1: `、`1- `、`1\t`、`1| `、` 12→`)。 */
|
|
25948
|
+
const LINE_NUMBER_PREFIX = /^\s*\d+(?::(?:\s|$)|-\s{2,}|\t|\|(?:\s|$)|→\s?)/;
|
|
25949
|
+
function stripLineNumberPrefix(line) {
|
|
25950
|
+
return line.replace(LINE_NUMBER_PREFIX, "");
|
|
25951
|
+
}
|
|
25745
25952
|
/** 契约/类型定义:命中两行以上声明就当它是契约,保持原样。 */
|
|
25746
25953
|
function looksLikeContract(text) {
|
|
25747
25954
|
let declarations = 0;
|
|
25748
|
-
for (const
|
|
25955
|
+
for (const raw of text.split("\n").slice(0, 800)) {
|
|
25956
|
+
const line = stripLineNumberPrefix(raw);
|
|
25749
25957
|
if (/^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type)\s+[A-Za-z_$][\w$]*/.test(line)) declarations += 1;
|
|
25750
25958
|
if (declarations >= 2) return true;
|
|
25751
25959
|
}
|
|
25752
25960
|
return false;
|
|
25753
25961
|
}
|
|
25754
|
-
/**
|
|
25962
|
+
/**
|
|
25963
|
+
* 源码或混合证据判定:
|
|
25964
|
+
* - 含多处 markdown 代码围栏;
|
|
25965
|
+
* - 含 `git diff` 补丁块(`diff --git` / `@@ -x,y +x,y @@`);
|
|
25966
|
+
* - 含至少 5 行带行号的源码切片(`1: ...` 或 `1\t...`),说明是单文件或多文件混合读取输出;
|
|
25967
|
+
* - 代码行占比 >= 45%。
|
|
25968
|
+
*/
|
|
25755
25969
|
function looksLikeSourceCode(text) {
|
|
25970
|
+
if (/^diff --git\s+/m.test(text) || /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/m.test(text)) return true;
|
|
25756
25971
|
let fences = 0;
|
|
25757
|
-
for (const line of text.split("\n").slice(0, 600)) if (line.trim().startsWith("```")) fences += 1;
|
|
25972
|
+
for (const line of text.split("\n").slice(0, 600)) if (stripLineNumberPrefix(line).trim().startsWith("```")) fences += 1;
|
|
25758
25973
|
if (fences >= 4) return true;
|
|
25759
25974
|
let code = 0;
|
|
25975
|
+
let numbered = 0;
|
|
25760
25976
|
let nonEmpty = 0;
|
|
25761
25977
|
for (const raw of text.split("\n").slice(0, 600)) {
|
|
25762
|
-
|
|
25978
|
+
if (raw.trim() === "") continue;
|
|
25979
|
+
if (LINE_NUMBER_PREFIX.test(raw)) numbered += 1;
|
|
25980
|
+
const trimmed = stripLineNumberPrefix(raw).trim();
|
|
25763
25981
|
if (trimmed === "") continue;
|
|
25764
25982
|
nonEmpty += 1;
|
|
25765
|
-
if (/^(?:import|export|from|const|let|var|function|class|interface|type|enum|async|await|return|if|for|while|switch|case|break|continue|try|catch|finally|throw|new|public|private|protected|static|readonly|def|using|namespace|package|#include|#pragma)\b/.test(trimmed) || /^[})\];]/.test(trimmed) || /^(?:\/\/|\/\*|\*)/.test(trimmed)) code += 1;
|
|
25983
|
+
if (/^(?:import|export|from|const|let|var|function|class|interface|type|enum|async|await|return|if|for|while|switch|case|break|continue|try|catch|finally|throw|new|public|private|protected|static|readonly|def|using|namespace|package|#include|#pragma)\b/.test(trimmed) || /^[})\];]/.test(trimmed) || /^(?:\/\/|\/\*|\*)/.test(trimmed) || /^<\/?[A-Za-z][\w.-]*/.test(trimmed)) code += 1;
|
|
25766
25984
|
}
|
|
25985
|
+
if (numbered >= 5) return true;
|
|
25767
25986
|
return nonEmpty >= 10 && code / nonEmpty >= .45;
|
|
25768
25987
|
}
|
|
25769
|
-
/** 完整 JSON
|
|
25988
|
+
/** 完整 JSON 文档(对象、数组、JSON 字符串)或被截断的 JSON 对象:不按裸日志猜测。 */
|
|
25770
25989
|
function isJsonDocument(text) {
|
|
25771
25990
|
const trimmed = text.trim();
|
|
25772
25991
|
if (trimmed.length < 2) return false;
|
|
25773
25992
|
const first = trimmed[0];
|
|
25774
25993
|
const last = trimmed[trimmed.length - 1];
|
|
25994
|
+
if (first === "\"" && last === "\"") try {
|
|
25995
|
+
return typeof JSON.parse(trimmed) === "string";
|
|
25996
|
+
} catch {
|
|
25997
|
+
return false;
|
|
25998
|
+
}
|
|
25775
25999
|
if (first !== "{" && first !== "[") return false;
|
|
25776
26000
|
if (first === "{" && last === "}" || first === "[" && last === "]") try {
|
|
25777
26001
|
const value = JSON.parse(trimmed);
|
|
@@ -25784,36 +26008,117 @@ function isJsonDocument(text) {
|
|
|
25784
26008
|
}
|
|
25785
26009
|
return false;
|
|
25786
26010
|
}
|
|
26011
|
+
/** PTC `run_code` JSON envelope 中允许独立投影日志的字段白名单。 */
|
|
26012
|
+
const JSON_ENVELOPE_LOG_KEYS = [
|
|
26013
|
+
"stdout",
|
|
26014
|
+
"stderr",
|
|
26015
|
+
"output",
|
|
26016
|
+
"logs",
|
|
26017
|
+
"log"
|
|
26018
|
+
];
|
|
26019
|
+
/** JSON envelope 其余字段允许占用的最大序列化字符数(防止把含批量源码的混合 envelope 压掉)。 */
|
|
26020
|
+
const JSON_ENVELOPE_MAX_OTHER_CHARS = 800;
|
|
25787
26021
|
/**
|
|
25788
|
-
*
|
|
26022
|
+
* 解开一层或两层 JSON 包装(支持顶层 JSON 字符串 `"..."` 或双重序列化的 `"{...}"`)。
|
|
26023
|
+
*/
|
|
26024
|
+
function parseJsonCandidate(text) {
|
|
26025
|
+
const trimmed = text.trim();
|
|
26026
|
+
if (trimmed.length < 2) return void 0;
|
|
26027
|
+
const first = trimmed[0];
|
|
26028
|
+
const last = trimmed[trimmed.length - 1];
|
|
26029
|
+
if (first === "\"" && last === "\"") try {
|
|
26030
|
+
const inner = JSON.parse(trimmed);
|
|
26031
|
+
if (typeof inner !== "string") return void 0;
|
|
26032
|
+
const innerTrimmed = inner.trim();
|
|
26033
|
+
if (innerTrimmed.startsWith("{") && innerTrimmed.endsWith("}")) try {
|
|
26034
|
+
const innerObj = JSON.parse(innerTrimmed);
|
|
26035
|
+
if (innerObj !== null && typeof innerObj === "object" && !Array.isArray(innerObj)) return {
|
|
26036
|
+
kind: "object",
|
|
26037
|
+
record: innerObj,
|
|
26038
|
+
wrappedInString: true
|
|
26039
|
+
};
|
|
26040
|
+
} catch {}
|
|
26041
|
+
return {
|
|
26042
|
+
kind: "string",
|
|
26043
|
+
decoded: inner
|
|
26044
|
+
};
|
|
26045
|
+
} catch {
|
|
26046
|
+
return;
|
|
26047
|
+
}
|
|
26048
|
+
if (first === "{" && last === "}") try {
|
|
26049
|
+
const parsed = JSON.parse(trimmed);
|
|
26050
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return {
|
|
26051
|
+
kind: "object",
|
|
26052
|
+
record: parsed,
|
|
26053
|
+
wrappedInString: false
|
|
26054
|
+
};
|
|
26055
|
+
} catch {
|
|
26056
|
+
return;
|
|
26057
|
+
}
|
|
26058
|
+
}
|
|
26059
|
+
/**
|
|
26060
|
+
* 尝试从完整 JSON(顶层 JSON 字符串或对象 envelope)中解出可结构化投影的日志正文。
|
|
25789
26061
|
*
|
|
25790
|
-
*
|
|
25791
|
-
*
|
|
25792
|
-
*
|
|
26062
|
+
* 规则刻意保守:
|
|
26063
|
+
* - 截断 JSON、数组一律返回 undefined;
|
|
26064
|
+
* - 对象 envelope 除白名单字段外,其余字段序列化后的总长度不超过 `JSON_ENVELOPE_MAX_OTHER_CHARS`;
|
|
26065
|
+
* - 解码并剥离 ANSI 后的日志文本自身必须能通过非源码/非契约的结构化日志投影计划;
|
|
26066
|
+
* - 返回的 `storeContent` 把解码后的多行日志直接展开(对象 envelope 还会把其余元数据缩进在头部),
|
|
26067
|
+
* 彻底避免单行 minified JSON 因没有真实 `\n` 导致 `store.page` 的按行分页失效。
|
|
25793
26068
|
*/
|
|
25794
|
-
function
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
26069
|
+
function extractJsonEnvelopeLogField(text, toolName) {
|
|
26070
|
+
const candidate = parseJsonCandidate(text);
|
|
26071
|
+
if (candidate === void 0) return void 0;
|
|
26072
|
+
if (candidate.kind === "string") {
|
|
26073
|
+
const clean = stripAnsi(candidate.decoded);
|
|
26074
|
+
if (isJsonDocument(clean) || isMixedEvidenceOutput(clean)) return void 0;
|
|
26075
|
+
const plan = planPlainTextProjection(clean, toolName);
|
|
26076
|
+
if (plan.kind !== "preview") return void 0;
|
|
26077
|
+
return { plan };
|
|
26078
|
+
}
|
|
26079
|
+
const { record } = candidate;
|
|
26080
|
+
let candidateKey;
|
|
26081
|
+
let candidateText = "";
|
|
26082
|
+
for (const key of JSON_ENVELOPE_LOG_KEYS) {
|
|
26083
|
+
const val = record[key];
|
|
26084
|
+
if (typeof val === "string" && val.length > candidateText.length) {
|
|
26085
|
+
candidateKey = key;
|
|
26086
|
+
candidateText = val;
|
|
26087
|
+
}
|
|
26088
|
+
}
|
|
26089
|
+
if (candidateKey === void 0 || candidateText.length === 0) return void 0;
|
|
26090
|
+
const rest = {};
|
|
26091
|
+
for (const [key, value] of Object.entries(record)) {
|
|
26092
|
+
if (key === candidateKey) continue;
|
|
26093
|
+
if (typeof value === "string" && (value.length > 400 || looksLikeSourceCode(value) || looksLikeContract(value))) return;
|
|
26094
|
+
rest[key] = value;
|
|
26095
|
+
}
|
|
26096
|
+
if (JSON.stringify(rest).length > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
|
|
26097
|
+
const cleanField = stripAnsi(candidateText);
|
|
26098
|
+
if (isJsonDocument(cleanField) || isMixedEvidenceOutput(cleanField)) return void 0;
|
|
26099
|
+
const plan = planPlainTextProjection(cleanField, toolName);
|
|
26100
|
+
if (plan.kind !== "preview") return void 0;
|
|
26101
|
+
return {
|
|
26102
|
+
envelope: {
|
|
26103
|
+
record,
|
|
26104
|
+
key: candidateKey
|
|
26105
|
+
},
|
|
26106
|
+
plan
|
|
25802
26107
|
};
|
|
26108
|
+
}
|
|
26109
|
+
/** 对一段纯文本(非 JSON 文档)做投影计划判定。 */
|
|
26110
|
+
function planPlainTextProjection(rawText, toolName) {
|
|
26111
|
+
const text = stripAnsi(rawText);
|
|
25803
26112
|
if (looksLikeContract(text)) return {
|
|
25804
26113
|
kind: "verbatim",
|
|
25805
26114
|
strategy: "verbatim-contract"
|
|
25806
26115
|
};
|
|
25807
|
-
if (isJsonDocument(text)) return {
|
|
25808
|
-
kind: "verbatim",
|
|
25809
|
-
strategy: "verbatim-unparsed"
|
|
25810
|
-
};
|
|
25811
26116
|
if (looksLikeSourceCode(text)) return {
|
|
25812
26117
|
kind: "verbatim",
|
|
25813
26118
|
strategy: "verbatim-source-code"
|
|
25814
26119
|
};
|
|
25815
26120
|
const test = extractTestEvidence(text);
|
|
25816
|
-
if (test !== void 0) return {
|
|
26121
|
+
if (test !== void 0 && test.failures.length > 0) return {
|
|
25817
26122
|
kind: "preview",
|
|
25818
26123
|
strategy: "test-failure",
|
|
25819
26124
|
sections: buildTestSections(test)
|
|
@@ -25852,6 +26157,42 @@ function planProjection(text, toolName) {
|
|
|
25852
26157
|
strategy: "verbatim-unparsed"
|
|
25853
26158
|
};
|
|
25854
26159
|
}
|
|
26160
|
+
/**
|
|
26161
|
+
* 决定这一段文本该怎么处理。
|
|
26162
|
+
*
|
|
26163
|
+
* 顺序就是「可证明性」的顺序:子代理报告/源码工具/契约先被判成原样,
|
|
26164
|
+
* 然后是 JSON envelope(只允许纯日志字段投影,混合证据一律原样),
|
|
26165
|
+
* 接着是测试失败集、grep、结构化、glob、shell 这些有明确形状的,
|
|
26166
|
+
* 最后落到「无法解析 → 原样」。
|
|
26167
|
+
*/
|
|
26168
|
+
function planProjection(text, toolName) {
|
|
26169
|
+
if (TOOL_SUBAGENT.test(toolName)) return {
|
|
26170
|
+
kind: "verbatim",
|
|
26171
|
+
strategy: "verbatim-subagent-report"
|
|
26172
|
+
};
|
|
26173
|
+
if (TOOL_SOURCE.test(toolName)) return {
|
|
26174
|
+
kind: "verbatim",
|
|
26175
|
+
strategy: "verbatim-source-code"
|
|
26176
|
+
};
|
|
26177
|
+
if (looksLikeContract(text)) return {
|
|
26178
|
+
kind: "verbatim",
|
|
26179
|
+
strategy: "verbatim-contract"
|
|
26180
|
+
};
|
|
26181
|
+
if (isJsonDocument(text)) {
|
|
26182
|
+
const extracted = extractJsonEnvelopeLogField(text, toolName);
|
|
26183
|
+
if (extracted !== void 0) return {
|
|
26184
|
+
kind: "preview",
|
|
26185
|
+
strategy: extracted.plan.strategy,
|
|
26186
|
+
sections: extracted.plan.sections,
|
|
26187
|
+
...extracted.envelope === void 0 ? {} : { envelope: extracted.envelope }
|
|
26188
|
+
};
|
|
26189
|
+
return {
|
|
26190
|
+
kind: "verbatim",
|
|
26191
|
+
strategy: "verbatim-unparsed"
|
|
26192
|
+
};
|
|
26193
|
+
}
|
|
26194
|
+
return planPlainTextProjection(text, toolName);
|
|
26195
|
+
}
|
|
25855
26196
|
/** 预览尾部:把「怎么读全文」写死在预览里,模型不用猜。 */
|
|
25856
26197
|
function footerFor(refId) {
|
|
25857
26198
|
return `[full result: ${refId} · page with offset/limit(default 64 lines)/maxBytes(default 16384 bytes)]`;
|
|
@@ -25870,12 +26211,22 @@ async function decide(args) {
|
|
|
25870
26211
|
if (input.isError === true && plan.strategy !== "test-failure" && plan.strategy !== "shell-output") return { reason: "verbatim-unparsed" };
|
|
25871
26212
|
const header = `[cache-warmup preview] strategy=${plan.strategy} input=${text.length}ch`;
|
|
25872
26213
|
const footerBudget = footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`).length;
|
|
25873
|
-
const
|
|
26214
|
+
const jsonEscaped = plan.envelope !== void 0;
|
|
26215
|
+
const envelopeShellLength = plan.envelope === void 0 ? 0 : JSON.stringify({
|
|
26216
|
+
...plan.envelope.record,
|
|
26217
|
+
[plan.envelope.key]: ""
|
|
26218
|
+
}).length;
|
|
26219
|
+
const bodyBudget = budget.maxChars - header.length - footerBudget - envelopeShellLength - (jsonEscaped ? 4 : 2);
|
|
25874
26220
|
if (bodyBudget <= 0) return { reason: "verbatim-budget" };
|
|
25875
|
-
const body = fitSections(plan.sections, bodyBudget);
|
|
26221
|
+
const body = fitSections(plan.sections, bodyBudget, jsonEscaped);
|
|
25876
26222
|
if (body === void 0) return { reason: "verbatim-budget" };
|
|
25877
26223
|
const store = input.store;
|
|
25878
26224
|
if (store === void 0) return { reason: "store-unavailable" };
|
|
26225
|
+
const placeholderInner = `${header}\n${body}\n${footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`)}`;
|
|
26226
|
+
if ((plan.envelope === void 0 ? placeholderInner : JSON.stringify({
|
|
26227
|
+
...plan.envelope.record,
|
|
26228
|
+
[plan.envelope.key]: placeholderInner
|
|
26229
|
+
})).length > budget.maxChars) return { reason: "verbatim-budget" };
|
|
25879
26230
|
let reference;
|
|
25880
26231
|
try {
|
|
25881
26232
|
reference = await store.put({
|
|
@@ -25887,7 +26238,11 @@ async function decide(args) {
|
|
|
25887
26238
|
} catch {
|
|
25888
26239
|
return { reason: "store-unavailable" };
|
|
25889
26240
|
}
|
|
25890
|
-
const
|
|
26241
|
+
const innerPreview = `${header}\n${body}\n${footerFor(reference.refId)}`;
|
|
26242
|
+
const preview = plan.envelope === void 0 ? innerPreview : JSON.stringify({
|
|
26243
|
+
...plan.envelope.record,
|
|
26244
|
+
[plan.envelope.key]: innerPreview
|
|
26245
|
+
});
|
|
25891
26246
|
if (preview.length > budget.maxChars) return { reason: "verbatim-budget" };
|
|
25892
26247
|
return {
|
|
25893
26248
|
preview,
|
|
@@ -30195,7 +30550,7 @@ function messageToolsOf(message) {
|
|
|
30195
30550
|
* lives in the request's declaration list, which is why the two are resolved
|
|
30196
30551
|
* together rather than the block carrying a schema of its own.
|
|
30197
30552
|
*/
|
|
30198
|
-
function toolAdditionNames(message) {
|
|
30553
|
+
function toolAdditionNames$1(message) {
|
|
30199
30554
|
const names = [];
|
|
30200
30555
|
for (const block of message.content) {
|
|
30201
30556
|
if (block.type !== "tool-addition") continue;
|
|
@@ -30212,7 +30567,7 @@ function toolAdditionNames(message) {
|
|
|
30212
30567
|
* `addition-only` never sees one, because the harness removes the declaration
|
|
30213
30568
|
* from the list instead.
|
|
30214
30569
|
*/
|
|
30215
|
-
function toolRemovalNames(message) {
|
|
30570
|
+
function toolRemovalNames$1(message) {
|
|
30216
30571
|
const names = [];
|
|
30217
30572
|
for (const block of message.content) {
|
|
30218
30573
|
if (block.type !== "tool-removal") continue;
|
|
@@ -30229,7 +30584,7 @@ function toolRemovalNames(message) {
|
|
|
30229
30584
|
* list (that is what keeps the prefix intact), so the only thing standing
|
|
30230
30585
|
* between the model and a call that would fail is this sentence.
|
|
30231
30586
|
*/
|
|
30232
|
-
function removalNotice(names) {
|
|
30587
|
+
function removalNotice$1(names) {
|
|
30233
30588
|
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
30234
30589
|
return names.length === 1 ? "[" + list + " is no longer available. Do not call it again: any call will fail.]" : "[" + list + " are no longer available. Do not call them again: any call will fail.]";
|
|
30235
30590
|
}
|
|
@@ -30373,7 +30728,7 @@ function declarationSlots(options, media) {
|
|
|
30373
30728
|
let beforeIndex = 0;
|
|
30374
30729
|
for (const message of options.messages) {
|
|
30375
30730
|
if (message.role === "developer") {
|
|
30376
|
-
const additions = declarationsFor(toolAdditionNames(message), options.tools);
|
|
30731
|
+
const additions = declarationsFor(toolAdditionNames$1(message), options.tools);
|
|
30377
30732
|
const entries = [];
|
|
30378
30733
|
if (additions.length > 0) entries.push(media.messageTools === true ? {
|
|
30379
30734
|
role: "system",
|
|
@@ -30382,10 +30737,10 @@ function declarationSlots(options, media) {
|
|
|
30382
30737
|
role: "system",
|
|
30383
30738
|
content: declarationNotice(options.model, additions.length, "capability")
|
|
30384
30739
|
});
|
|
30385
|
-
const removals = toolRemovalNames(message);
|
|
30740
|
+
const removals = toolRemovalNames$1(message);
|
|
30386
30741
|
if (removals.length > 0) entries.push({
|
|
30387
30742
|
role: "system",
|
|
30388
|
-
content: removalNotice(removals)
|
|
30743
|
+
content: removalNotice$1(removals)
|
|
30389
30744
|
});
|
|
30390
30745
|
if (entries.length > 0) slots.push({
|
|
30391
30746
|
beforeIndex,
|
|
@@ -31622,6 +31977,27 @@ var SessionCacheStateStore = class {
|
|
|
31622
31977
|
this.schedule();
|
|
31623
31978
|
}
|
|
31624
31979
|
/**
|
|
31980
|
+
* Update the warmup consecutive-hit streak on an already-written session record
|
|
31981
|
+
* in place, without issuing a second `write()` call.
|
|
31982
|
+
*/
|
|
31983
|
+
noteWarmupStreak(sessionId, consecutiveHits) {
|
|
31984
|
+
const key = sessionId.trim();
|
|
31985
|
+
if (key === "") return;
|
|
31986
|
+
const document = this.load();
|
|
31987
|
+
const existing = document.sessions[key];
|
|
31988
|
+
if (existing === void 0) return;
|
|
31989
|
+
const normalized = count$1(consecutiveHits);
|
|
31990
|
+
if (normalized > 0) document.sessions[key] = {
|
|
31991
|
+
...existing,
|
|
31992
|
+
warmupConsecutiveHits: normalized
|
|
31993
|
+
};
|
|
31994
|
+
else if (existing.warmupConsecutiveHits !== void 0) {
|
|
31995
|
+
const { warmupConsecutiveHits: _dropped, ...rest } = existing;
|
|
31996
|
+
document.sessions[key] = rest;
|
|
31997
|
+
} else return;
|
|
31998
|
+
this.schedule();
|
|
31999
|
+
}
|
|
32000
|
+
/**
|
|
31625
32001
|
* Write the document now, synchronously.
|
|
31626
32002
|
*
|
|
31627
32003
|
* Exposed for tests and for a caller that is about to lose the process; the
|
|
@@ -31698,6 +32074,7 @@ function normalizeRecord(value) {
|
|
|
31698
32074
|
const updatedAt = finite(value.updatedAt);
|
|
31699
32075
|
if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
|
|
31700
32076
|
const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
|
|
32077
|
+
const warmupStreak = count$1(value.warmupConsecutiveHits);
|
|
31701
32078
|
return {
|
|
31702
32079
|
lastActiveAt,
|
|
31703
32080
|
updatedAt,
|
|
@@ -31709,7 +32086,8 @@ function normalizeRecord(value) {
|
|
|
31709
32086
|
cacheWriteTokens: count$1(value.cacheWriteTokens),
|
|
31710
32087
|
cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
|
|
31711
32088
|
cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
|
|
31712
|
-
...drift === void 0 ? {} : { lastDrift: drift }
|
|
32089
|
+
...drift === void 0 ? {} : { lastDrift: drift },
|
|
32090
|
+
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {}
|
|
31713
32091
|
};
|
|
31714
32092
|
}
|
|
31715
32093
|
/**
|
|
@@ -32809,7 +33187,7 @@ const KIMI_CODE_REQUEST_IMAGE_MAX_EDGE = 2e3;
|
|
|
32809
33187
|
* @param catalog - live catalog, possibly empty when offline.
|
|
32810
33188
|
* @returns the tool-update mode, or undefined when this route must resend everything.
|
|
32811
33189
|
*/
|
|
32812
|
-
function toolUpdateMode(modelId, catalog) {
|
|
33190
|
+
function toolUpdateMode$1(modelId, catalog) {
|
|
32813
33191
|
if (!dynamicToolsForEntry(modelId, catalog)) return void 0;
|
|
32814
33192
|
if (wireForCatalogEntry(modelId, catalog) !== "openai") return void 0;
|
|
32815
33193
|
const raw = (process.env.DSH_KIMI_CODE_TOOL_UPDATE ?? "").trim().toLowerCase();
|
|
@@ -32860,7 +33238,7 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
32860
33238
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
32861
33239
|
updatedAt: now
|
|
32862
33240
|
});
|
|
32863
|
-
recordCacheWarmupUsage({
|
|
33241
|
+
const warmupState = recordCacheWarmupUsage({
|
|
32864
33242
|
sessionId,
|
|
32865
33243
|
inputTokens: state.inputTokens,
|
|
32866
33244
|
cacheReadTokens: state.cacheReadTokens,
|
|
@@ -32869,6 +33247,7 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
32869
33247
|
at: now,
|
|
32870
33248
|
...lastDrift === void 0 ? {} : { driftCause: lastDrift }
|
|
32871
33249
|
});
|
|
33250
|
+
sessionCacheState.noteWarmupStreak(sessionId, warmupState.consecutiveHits);
|
|
32872
33251
|
}
|
|
32873
33252
|
var KimiCodeAdapter = class extends LlmAdapter {
|
|
32874
33253
|
store;
|
|
@@ -33052,7 +33431,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33052
33431
|
id: modelId,
|
|
33053
33432
|
name: entry?.name ?? modelId,
|
|
33054
33433
|
inputModalities: inputModalitiesForEntry(modelId, catalog),
|
|
33055
|
-
...toolUpdateMode(modelId, catalog) === void 0 ? {} : { toolUpdate: toolUpdateMode(modelId, catalog) },
|
|
33434
|
+
...toolUpdateMode$1(modelId, catalog) === void 0 ? {} : { toolUpdate: toolUpdateMode$1(modelId, catalog) },
|
|
33056
33435
|
context: { contextWindow: this.contextWindowFor(modelId, entry, settings.contextWindowOverrides) },
|
|
33057
33436
|
...efforts.length === 0 ? {} : { reasoning: {
|
|
33058
33437
|
efforts: efforts.map((effort) => ({
|
|
@@ -33849,13 +34228,32 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
|
|
|
33849
34228
|
* answers with the process aggregate, which is the right answer for a settings
|
|
33850
34229
|
* card that is not showing a specific conversation.
|
|
33851
34230
|
*/
|
|
34231
|
+
function warmupStatusOf(sessionId) {
|
|
34232
|
+
const targetSession = sessionId?.trim() || cacheWarmupUsageSessions()[0]?.sessionId;
|
|
34233
|
+
const durable = sessionCacheState.read(targetSession);
|
|
34234
|
+
const hint = durable === void 0 ? void 0 : {
|
|
34235
|
+
lastActiveAt: durable.lastActiveAt,
|
|
34236
|
+
consecutiveHits: durable.warmupConsecutiveHits ?? 0,
|
|
34237
|
+
hitRatio: durable.cachedTokens + durable.freshTokens > 0 ? durable.cachedTokens / (durable.cachedTokens + durable.freshTokens) : 0
|
|
34238
|
+
};
|
|
34239
|
+
if (targetSession === void 0 && hint === void 0) return void 0;
|
|
34240
|
+
const budget = cacheWarmupBudget(targetSession ?? "durable-latest", Date.now(), hint);
|
|
34241
|
+
return {
|
|
34242
|
+
warmup: budget.warmup,
|
|
34243
|
+
maxChars: budget.maxChars,
|
|
34244
|
+
consecutiveHits: budget.consecutiveHits,
|
|
34245
|
+
requiredHits: CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired
|
|
34246
|
+
};
|
|
34247
|
+
}
|
|
33852
34248
|
function cacheStatsOrNull(sessionId) {
|
|
33853
34249
|
const stats = getCacheStats(sessionId);
|
|
33854
34250
|
if (stats.requests === 0) return durableCacheStats(sessionId);
|
|
33855
34251
|
const lastDrift = getLastDriftCause(sessionId);
|
|
34252
|
+
const warmup = warmupStatusOf(sessionId);
|
|
33856
34253
|
return {
|
|
33857
34254
|
...stats,
|
|
33858
34255
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34256
|
+
...warmup === void 0 ? {} : { warmup },
|
|
33859
34257
|
cacheWriteTokens: stats.cacheWriteTokens,
|
|
33860
34258
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
33861
34259
|
cacheWrite1hTokens: stats.cacheWrite1hTokens
|
|
@@ -33897,6 +34295,7 @@ function durableCacheStats(sessionId) {
|
|
|
33897
34295
|
const durable = sessionCacheState.read(sessionId);
|
|
33898
34296
|
if (durable === void 0 || durable.requests === 0) return null;
|
|
33899
34297
|
const lastDrift = driftCauseOf(durable.lastDrift);
|
|
34298
|
+
const warmup = warmupStatusOf(sessionId);
|
|
33900
34299
|
return {
|
|
33901
34300
|
requests: durable.requests,
|
|
33902
34301
|
cachedTokens: durable.cachedTokens,
|
|
@@ -33906,7 +34305,8 @@ function durableCacheStats(sessionId) {
|
|
|
33906
34305
|
cacheWriteTokens: durable.cacheWriteTokens,
|
|
33907
34306
|
cacheWrite5mTokens: durable.cacheWrite5mTokens,
|
|
33908
34307
|
cacheWrite1hTokens: durable.cacheWrite1hTokens,
|
|
33909
|
-
...lastDrift === void 0 ? {} : { lastDrift }
|
|
34308
|
+
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34309
|
+
...warmup === void 0 ? {} : { warmup }
|
|
33910
34310
|
};
|
|
33911
34311
|
}
|
|
33912
34312
|
/**
|
|
@@ -64949,6 +65349,52 @@ function imagePart$1(block, images) {
|
|
|
64949
65349
|
image_url: { url: `data:${inline.mediaType};base64,${inline.data}` }
|
|
64950
65350
|
};
|
|
64951
65351
|
}
|
|
65352
|
+
/**
|
|
65353
|
+
* Whether one model id names a DeepSeek model.
|
|
65354
|
+
*
|
|
65355
|
+
* DeepSeek's own Chat Completions contract carries a disabled reasoning level as
|
|
65356
|
+
* `thinking: { type: 'disabled' }` rather than as a `reasoning_effort` value, so
|
|
65357
|
+
* the translation below is scoped to this model family. Read from the model id's
|
|
65358
|
+
* own segment so a provider-prefixed spelling (`cline-pass/...`, or a canonical
|
|
65359
|
+
* `deepseek/...` slug) is recognised either way.
|
|
65360
|
+
*/
|
|
65361
|
+
function isDeepSeekModel$1(modelId) {
|
|
65362
|
+
const bare = modelId.slice(modelId.lastIndexOf("/") + 1).trim();
|
|
65363
|
+
return /^deepseek/i.test(bare);
|
|
65364
|
+
}
|
|
65365
|
+
/** The names one developer message declares or revokes, in history order. */
|
|
65366
|
+
function toolNamesOf(blocks, type) {
|
|
65367
|
+
const names = [];
|
|
65368
|
+
for (const block of blocks ?? []) {
|
|
65369
|
+
const record = block;
|
|
65370
|
+
if (record.type !== type) continue;
|
|
65371
|
+
const name = record.toolName;
|
|
65372
|
+
if (typeof name !== "string" || name.trim().length === 0) continue;
|
|
65373
|
+
names.push(name.trim());
|
|
65374
|
+
}
|
|
65375
|
+
return names;
|
|
65376
|
+
}
|
|
65377
|
+
/** One quoted, comma-joined list of tool names. */
|
|
65378
|
+
function quotedList(names) {
|
|
65379
|
+
return names.map((name) => "\"" + name + "\"").join(", ");
|
|
65380
|
+
}
|
|
65381
|
+
/**
|
|
65382
|
+
* The model-facing text of a mid-conversation tool change, or '' when there is none.
|
|
65383
|
+
*
|
|
65384
|
+
* `in-history` means the harness stopped rewriting the declaration list: an
|
|
65385
|
+
* added tool still travels in `tools`, and a removed one keeps its declaration
|
|
65386
|
+
* so the prefix survives. The history note is therefore the only thing that
|
|
65387
|
+
* tells the model a removal happened, which is why the removal sentence is
|
|
65388
|
+
* imperative rather than descriptive.
|
|
65389
|
+
*/
|
|
65390
|
+
function toolUpdateNotice(blocks) {
|
|
65391
|
+
const added = toolNamesOf(blocks, "tool-addition");
|
|
65392
|
+
const removed = toolNamesOf(blocks, "tool-removal");
|
|
65393
|
+
const notices = [];
|
|
65394
|
+
if (added.length > 0) notices.push(added.length === 1 ? "[The tool " + quotedList(added) + " is now available.]" : "[The tools " + quotedList(added) + " are now available.]");
|
|
65395
|
+
if (removed.length > 0) notices.push(removed.length === 1 ? "[" + quotedList(removed) + " is no longer available. Do not call it again: any call will fail.]" : "[" + quotedList(removed) + " are no longer available. Do not call them again: any call will fail.]");
|
|
65396
|
+
return notices.join("\n");
|
|
65397
|
+
}
|
|
64952
65398
|
/** Recursively collect image parts from blocks, including nested tool results. */
|
|
64953
65399
|
function collectImageParts$1(blocks, images, target = []) {
|
|
64954
65400
|
if (!Array.isArray(blocks)) return target;
|
|
@@ -65001,8 +65447,17 @@ function serializeMessages(messages, images) {
|
|
|
65001
65447
|
pendingToolImages = [];
|
|
65002
65448
|
emit(message);
|
|
65003
65449
|
};
|
|
65004
|
-
/**
|
|
65450
|
+
/**
|
|
65451
|
+
* A tool answer: it closes its own id, and the group when it was the last.
|
|
65452
|
+
*
|
|
65453
|
+
* An answer whose call id has no pending `tool_call` is refused here, before
|
|
65454
|
+
* anything is sent. Forwarding it produces an OpenAI message sequence the
|
|
65455
|
+
* gateway rejects (a `tool` message with no declaration in front of it), and
|
|
65456
|
+
* the gateway's 400 names neither the call nor the turn, so the failure is
|
|
65457
|
+
* raised locally with the id that has no question.
|
|
65458
|
+
*/
|
|
65005
65459
|
const answer = (callId, content) => {
|
|
65460
|
+
if (!awaiting.includes(callId)) throw new LlmError(`tool result for call "${callId}" has no tool_call awaiting it: the conversation cannot be expressed as a valid OpenAI message sequence`, "INVALID_REQUEST");
|
|
65006
65461
|
wire.push({
|
|
65007
65462
|
role: "tool",
|
|
65008
65463
|
tool_call_id: callId,
|
|
@@ -65015,9 +65470,11 @@ function serializeMessages(messages, images) {
|
|
|
65015
65470
|
for (const message of messages) {
|
|
65016
65471
|
if (message.role === "system" || message.role === "developer") {
|
|
65017
65472
|
flushToolImages();
|
|
65473
|
+
const content = [toolUpdateNotice(message.content), flattenText$1(message.content)].filter((part) => part.length > 0).join("\n");
|
|
65474
|
+
if (content.length === 0) continue;
|
|
65018
65475
|
emit({
|
|
65019
65476
|
role: "system",
|
|
65020
|
-
content
|
|
65477
|
+
content
|
|
65021
65478
|
});
|
|
65022
65479
|
continue;
|
|
65023
65480
|
}
|
|
@@ -65037,7 +65494,7 @@ function serializeMessages(messages, images) {
|
|
|
65037
65494
|
else if (block.type === "image") parts.push(imagePart$1(block, images));
|
|
65038
65495
|
const hasImage = parts.some((part) => part.type === "image_url");
|
|
65039
65496
|
const text = parts.filter((part) => part.type === "text").map((part) => String(part.text)).join("");
|
|
65040
|
-
if (hasImage || text.length > 0
|
|
65497
|
+
if (hasImage || text.length > 0) {
|
|
65041
65498
|
flushToolImages();
|
|
65042
65499
|
emit({
|
|
65043
65500
|
role: "user",
|
|
@@ -65072,6 +65529,7 @@ function buildClinePassBody(options, connection, images) {
|
|
|
65072
65529
|
}
|
|
65073
65530
|
}));
|
|
65074
65531
|
const maxTokens = options.maxTokens ?? connection.maxTokens;
|
|
65532
|
+
const thinkingOff = options.reasoningEffort !== void 0 && String(options.reasoningEffort) === "none" && isDeepSeekModel$1(options.model);
|
|
65075
65533
|
return {
|
|
65076
65534
|
model: options.model,
|
|
65077
65535
|
messages,
|
|
@@ -65081,7 +65539,8 @@ function buildClinePassBody(options, connection, images) {
|
|
|
65081
65539
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
65082
65540
|
...maxTokens === void 0 || maxTokens <= 0 ? {} : { max_tokens: maxTokens },
|
|
65083
65541
|
...options.stop === void 0 ? {} : { stop: options.stop },
|
|
65084
|
-
...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
|
|
65542
|
+
...options.reasoningEffort === void 0 || thinkingOff ? {} : { reasoning_effort: String(options.reasoningEffort) },
|
|
65543
|
+
...thinkingOff ? { thinking: { type: "disabled" } } : {}
|
|
65085
65544
|
};
|
|
65086
65545
|
}
|
|
65087
65546
|
/** Map an OpenAI `finish_reason` to the harness finish reason. */
|
|
@@ -65242,12 +65701,47 @@ function closeBlock$1(block) {
|
|
|
65242
65701
|
}
|
|
65243
65702
|
}
|
|
65244
65703
|
/**
|
|
65704
|
+
* Whether one failure is the SSE parser's own "the body ended without [DONE]".
|
|
65705
|
+
*
|
|
65706
|
+
* Structural rather than `instanceof`: what has to match is the code the parser
|
|
65707
|
+
* assigns, and a second copy of this module (or a harness generation that ships
|
|
65708
|
+
* its own LlmError class) would fail an identity test on a value that is
|
|
65709
|
+
* otherwise exactly the signal this decision is about.
|
|
65710
|
+
*/
|
|
65711
|
+
function isMissingSentinel$1(error) {
|
|
65712
|
+
return typeof error === "object" && error !== null && error.code === "STREAM_CLOSED";
|
|
65713
|
+
}
|
|
65714
|
+
/**
|
|
65715
|
+
* End the payload iteration instead of throwing when a body stops cleanly.
|
|
65716
|
+
*
|
|
65717
|
+
* The parser can only report "no [DONE] arrived" by throwing, and that is NOT a
|
|
65718
|
+
* failure on its own: the gateway sometimes closes the response right after the
|
|
65719
|
+
* terminal frame. Whether the response actually completed is decided by the
|
|
65720
|
+
* frames that were seen, which is knowledge this generator does not have — so it
|
|
65721
|
+
* only removes the transport signal and leaves the verdict to {@link translate}.
|
|
65722
|
+
* Every other failure passes through untouched, including the error frames a 200
|
|
65723
|
+
* body can carry.
|
|
65724
|
+
*/
|
|
65725
|
+
async function* tolerateMissingSentinel$1(payloads) {
|
|
65726
|
+
try {
|
|
65727
|
+
for await (const payload of payloads) yield payload;
|
|
65728
|
+
} catch (error) {
|
|
65729
|
+
if (!isMissingSentinel$1(error)) throw error;
|
|
65730
|
+
}
|
|
65731
|
+
}
|
|
65732
|
+
/**
|
|
65245
65733
|
* Translate decoded SSE payloads into harness stream chunks.
|
|
65246
65734
|
*
|
|
65247
65735
|
* Deltas stream through as they arrive; block ends, usage and the finish reason
|
|
65248
65736
|
* are all deferred to the `[DONE]` sentinel, so no chunk ever follows `finish`.
|
|
65249
65737
|
* A completion with no content at all becomes an `EMPTY_RESPONSE` error finish
|
|
65250
65738
|
* rather than a silent empty message.
|
|
65739
|
+
*
|
|
65740
|
+
* A missing `[DONE]` is not a failure by itself. Once a valid `finish_reason`
|
|
65741
|
+
* has been seen the response IS complete, and it closes exactly as a `[DONE]`
|
|
65742
|
+
* one does — block ends, usage and finish all preserved. A body that carried
|
|
65743
|
+
* content but never named a finish reason is a truncation, and still fails with
|
|
65744
|
+
* `STREAM_CLOSED`.
|
|
65251
65745
|
*/
|
|
65252
65746
|
async function* translate(payloads) {
|
|
65253
65747
|
let nextIndex = 0;
|
|
@@ -65274,38 +65768,48 @@ async function* translate(payloads) {
|
|
|
65274
65768
|
order.push(block);
|
|
65275
65769
|
return block;
|
|
65276
65770
|
};
|
|
65277
|
-
|
|
65771
|
+
/**
|
|
65772
|
+
* The chunks that close a completed response: every open block, then usage,
|
|
65773
|
+
* then the single finish. Both terminal paths use this one assembly, so a
|
|
65774
|
+
* response read from a clean EOF and one read from `[DONE]` cannot drift.
|
|
65775
|
+
*/
|
|
65776
|
+
const closing = () => {
|
|
65777
|
+
const chunks = [];
|
|
65778
|
+
for (const block of order) chunks.push({
|
|
65779
|
+
type: "block-end",
|
|
65780
|
+
index: block.index,
|
|
65781
|
+
block: closeBlock$1(block)
|
|
65782
|
+
});
|
|
65783
|
+
if (pendingUsage !== void 0) chunks.push({
|
|
65784
|
+
type: "usage",
|
|
65785
|
+
usage: pendingUsage
|
|
65786
|
+
});
|
|
65787
|
+
const reason = pendingFinish ?? {
|
|
65788
|
+
type: "finish",
|
|
65789
|
+
reason: { kind: "stop" }
|
|
65790
|
+
};
|
|
65791
|
+
const hasVisibleAnswer = toolBlocks.size > 0 || textBlock !== void 0 && textBlock.text.trim() !== "";
|
|
65792
|
+
chunks.push({
|
|
65793
|
+
type: "finish",
|
|
65794
|
+
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
65795
|
+
kind: "error",
|
|
65796
|
+
failure: {
|
|
65797
|
+
message: "model returned a completed response with no content",
|
|
65798
|
+
code: EMPTY_RESPONSE_CODE
|
|
65799
|
+
}
|
|
65800
|
+
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
65801
|
+
kind: "error",
|
|
65802
|
+
failure: {
|
|
65803
|
+
message: "Cline Pass ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
65804
|
+
code: "PROVIDER_ERROR"
|
|
65805
|
+
}
|
|
65806
|
+
} : reason.reason
|
|
65807
|
+
});
|
|
65808
|
+
return chunks;
|
|
65809
|
+
};
|
|
65810
|
+
for await (const payload of tolerateMissingSentinel$1(payloads)) {
|
|
65278
65811
|
if (payload === "[DONE]") {
|
|
65279
|
-
for (const
|
|
65280
|
-
type: "block-end",
|
|
65281
|
-
index: block.index,
|
|
65282
|
-
block: closeBlock$1(block)
|
|
65283
|
-
};
|
|
65284
|
-
if (pendingUsage !== void 0) yield {
|
|
65285
|
-
type: "usage",
|
|
65286
|
-
usage: pendingUsage
|
|
65287
|
-
};
|
|
65288
|
-
const reason = pendingFinish ?? {
|
|
65289
|
-
type: "finish",
|
|
65290
|
-
reason: { kind: "stop" }
|
|
65291
|
-
};
|
|
65292
|
-
const hasVisibleAnswer = toolBlocks.size > 0 || textBlock !== void 0 && textBlock.text.trim() !== "";
|
|
65293
|
-
yield {
|
|
65294
|
-
type: "finish",
|
|
65295
|
-
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
65296
|
-
kind: "error",
|
|
65297
|
-
failure: {
|
|
65298
|
-
message: "model returned a completed response with no content",
|
|
65299
|
-
code: EMPTY_RESPONSE_CODE
|
|
65300
|
-
}
|
|
65301
|
-
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
65302
|
-
kind: "error",
|
|
65303
|
-
failure: {
|
|
65304
|
-
message: "Cline Pass ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
65305
|
-
code: "PROVIDER_ERROR"
|
|
65306
|
-
}
|
|
65307
|
-
} : reason.reason
|
|
65308
|
-
};
|
|
65812
|
+
for (const chunk of closing()) yield chunk;
|
|
65309
65813
|
return;
|
|
65310
65814
|
}
|
|
65311
65815
|
const chunk = unwrapEnvelope(payload);
|
|
@@ -65379,6 +65883,10 @@ async function* translate(payloads) {
|
|
|
65379
65883
|
}
|
|
65380
65884
|
if (isRecord$11(chunk.usage)) pendingUsage = mapUsage$1(chunk.usage);
|
|
65381
65885
|
}
|
|
65886
|
+
if (pendingFinish !== void 0) {
|
|
65887
|
+
for (const chunk of closing()) yield chunk;
|
|
65888
|
+
return;
|
|
65889
|
+
}
|
|
65382
65890
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
65383
65891
|
}
|
|
65384
65892
|
//#endregion
|
|
@@ -65515,6 +66023,7 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
65515
66023
|
id: modelId,
|
|
65516
66024
|
name: info.displayName,
|
|
65517
66025
|
inputModalities: info.input,
|
|
66026
|
+
toolUpdate: "in-history",
|
|
65518
66027
|
context: { contextWindow: info.contextWindow },
|
|
65519
66028
|
...outputReservation(info.contextWindow, info.maxTokens),
|
|
65520
66029
|
...info.reasoning ? { reasoning: { efforts: info.efforts.map((effort) => ({
|
|
@@ -65535,11 +66044,12 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
65535
66044
|
}
|
|
65536
66045
|
async *requestStream(rawOptions, settings, signal, activity) {
|
|
65537
66046
|
if (settings.enabled === false) throw new LlmError(`${PROVIDER_NAME$4} is disabled in Settings > Subscription. Enable the line there, then retry.`, "INVALID_REQUEST");
|
|
65538
|
-
const
|
|
65539
|
-
const images = await resolveRequestImages$1(
|
|
66047
|
+
const normalized = normalizeGenerateOptions(rawOptions);
|
|
66048
|
+
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal).catch((error) => {
|
|
65540
66049
|
if (signal.aborted) throw new LlmError("Cline Pass request aborted by caller", "ABORTED", { cause: error });
|
|
65541
66050
|
return NO_RESOLVED_IMAGES$3;
|
|
65542
66051
|
});
|
|
66052
|
+
const options = offloadOldestRequestImages$1(normalized, void 0, images);
|
|
65543
66053
|
const body = buildClinePassBody(options, {
|
|
65544
66054
|
baseURL: settings.baseURL,
|
|
65545
66055
|
displayName: PROVIDER_NAME$4,
|
|
@@ -76386,6 +76896,52 @@ function collectImageParts(blocks, images, target = []) {
|
|
|
76386
76896
|
}
|
|
76387
76897
|
return target;
|
|
76388
76898
|
}
|
|
76899
|
+
/** Names of the tools one developer message adds at its own history position. */
|
|
76900
|
+
function toolAdditionNames(message) {
|
|
76901
|
+
const names = [];
|
|
76902
|
+
for (const block of message.content) {
|
|
76903
|
+
if (block.type !== "tool-addition") continue;
|
|
76904
|
+
const name = block.toolName;
|
|
76905
|
+
if (typeof name !== "string" || name.trim() === "") continue;
|
|
76906
|
+
names.push(name.trim());
|
|
76907
|
+
}
|
|
76908
|
+
return names;
|
|
76909
|
+
}
|
|
76910
|
+
/** Names of the tools one developer message revokes at its own history position. */
|
|
76911
|
+
function toolRemovalNames(message) {
|
|
76912
|
+
const names = [];
|
|
76913
|
+
for (const block of message.content) {
|
|
76914
|
+
if (block.type !== "tool-removal") continue;
|
|
76915
|
+
const name = block.toolName;
|
|
76916
|
+
if (typeof name !== "string" || name.trim() === "") continue;
|
|
76917
|
+
names.push(name.trim());
|
|
76918
|
+
}
|
|
76919
|
+
return names;
|
|
76920
|
+
}
|
|
76921
|
+
/**
|
|
76922
|
+
* The model-facing note that announces tools a conversation just gained.
|
|
76923
|
+
*
|
|
76924
|
+
* This route declares `toolUpdate: 'in-history'`, so the harness states an
|
|
76925
|
+
* addition as a block at its own history position instead of rewriting the whole
|
|
76926
|
+
* request's tool list. OpenAI has no message-level tool declaration, so the
|
|
76927
|
+
* change travels as this sentence: it costs one short system turn and leaves the
|
|
76928
|
+
* top-level `tools` array — and therefore the prefix cache — intact.
|
|
76929
|
+
*/
|
|
76930
|
+
function additionNotice(names) {
|
|
76931
|
+
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
76932
|
+
return names.length === 1 ? "[" + list + " is now available. Call it when it helps with the task.]" : "[" + list + " are now available. Call them when they help with the task.]";
|
|
76933
|
+
}
|
|
76934
|
+
/**
|
|
76935
|
+
* The model-facing note that revokes one or more tools.
|
|
76936
|
+
*
|
|
76937
|
+
* Imperative and unambiguous on purpose: the tool is still in the declaration
|
|
76938
|
+
* list, so this sentence is the only thing between the model and a call that
|
|
76939
|
+
* would fail.
|
|
76940
|
+
*/
|
|
76941
|
+
function removalNotice(names) {
|
|
76942
|
+
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
76943
|
+
return names.length === 1 ? "[" + list + " is no longer available. Do not call it again: any call will fail.]" : "[" + list + " are no longer available. Do not call them again: any call will fail.]";
|
|
76944
|
+
}
|
|
76389
76945
|
/**
|
|
76390
76946
|
* Convert the harness conversation to wire messages.
|
|
76391
76947
|
*
|
|
@@ -76397,7 +76953,15 @@ function collectImageParts(blocks, images, target = []) {
|
|
|
76397
76953
|
* message keeps a parallel group's answers contiguous.
|
|
76398
76954
|
*
|
|
76399
76955
|
* `developer` becomes `system`: the newer OpenAI spelling is not accepted by
|
|
76400
|
-
* every OpenAI-compatible gateway, while `system` is.
|
|
76956
|
+
* every OpenAI-compatible gateway, while `system` is. A tool addition or
|
|
76957
|
+
* removal carried by such a message is replayed as its own system sentence —
|
|
76958
|
+
* the `in-history` shape this route declares — and ordinary text is kept
|
|
76959
|
+
* beside it. A message that contributes neither is dropped rather than sent as
|
|
76960
|
+
* an empty system turn.
|
|
76961
|
+
*
|
|
76962
|
+
* A user message that carries no text and no image contributes nothing to a
|
|
76963
|
+
* chat request, so it is dropped too; a pure tool-result message never took that
|
|
76964
|
+
* path, because its answers travel as `tool` messages.
|
|
76401
76965
|
*/
|
|
76402
76966
|
function serializeMessages$1(messages, images) {
|
|
76403
76967
|
const wire = [];
|
|
@@ -76430,6 +76994,7 @@ function serializeMessages$1(messages, images) {
|
|
|
76430
76994
|
};
|
|
76431
76995
|
/** A tool answer: it closes its own id, and the group when it was the last. */
|
|
76432
76996
|
const answer = (callId, content) => {
|
|
76997
|
+
if (!awaiting.includes(callId)) throw new LlmError(`OpenDesign request is invalid: the tool result for call id "${callId}" has no pending tool call to answer, so the message list would carry a tool message that no assistant tool_calls message introduced. Nothing was sent.`, "INVALID_REQUEST");
|
|
76433
76998
|
wire.push({
|
|
76434
76999
|
role: "tool",
|
|
76435
77000
|
tool_call_id: callId,
|
|
@@ -76442,9 +77007,20 @@ function serializeMessages$1(messages, images) {
|
|
|
76442
77007
|
for (const message of messages) {
|
|
76443
77008
|
if (message.role === "system" || message.role === "developer") {
|
|
76444
77009
|
flushToolImages();
|
|
76445
|
-
|
|
77010
|
+
const text = flattenText(message.content);
|
|
77011
|
+
if (text.length > 0) emit({
|
|
77012
|
+
role: "system",
|
|
77013
|
+
content: text
|
|
77014
|
+
});
|
|
77015
|
+
const additions = toolAdditionNames(message);
|
|
77016
|
+
if (additions.length > 0) emit({
|
|
77017
|
+
role: "system",
|
|
77018
|
+
content: additionNotice(additions)
|
|
77019
|
+
});
|
|
77020
|
+
const removals = toolRemovalNames(message);
|
|
77021
|
+
if (removals.length > 0) emit({
|
|
76446
77022
|
role: "system",
|
|
76447
|
-
content:
|
|
77023
|
+
content: removalNotice(removals)
|
|
76448
77024
|
});
|
|
76449
77025
|
continue;
|
|
76450
77026
|
}
|
|
@@ -76464,7 +77040,7 @@ function serializeMessages$1(messages, images) {
|
|
|
76464
77040
|
else if (block.type === "image") parts.push(imagePart(block, images));
|
|
76465
77041
|
const hasImage = parts.some((part) => part.type === "image_url");
|
|
76466
77042
|
const text = parts.filter((part) => part.type === "text").map((part) => String(part.text)).join("");
|
|
76467
|
-
if (hasImage || text.length > 0
|
|
77043
|
+
if (hasImage || text.length > 0) {
|
|
76468
77044
|
flushToolImages();
|
|
76469
77045
|
emit({
|
|
76470
77046
|
role: "user",
|
|
@@ -76478,6 +77054,21 @@ function serializeMessages$1(messages, images) {
|
|
|
76478
77054
|
return wire;
|
|
76479
77055
|
}
|
|
76480
77056
|
/**
|
|
77057
|
+
* Whether one model id is a DeepSeek model.
|
|
77058
|
+
*
|
|
77059
|
+
* The vendor prefix is stripped before the compare, so the bare wire id this
|
|
77060
|
+
* line ships (`deepseek-v4.1-flash`) and a vendor-qualified spelling
|
|
77061
|
+
* (`deepseek/deepseek-v4-pro`, as a sibling registry writes it) both match,
|
|
77062
|
+
* while an id that merely mentions the word — a gateway alias such as
|
|
77063
|
+
* `my-deepseek-proxy` — does not. Only DeepSeek's own chat endpoint documents
|
|
77064
|
+
* the `thinking` field, which is why the translation below is scoped to it
|
|
77065
|
+
* rather than applied to every model that offers a `none` level.
|
|
77066
|
+
*/
|
|
77067
|
+
function isDeepSeekModel(model) {
|
|
77068
|
+
const id = String(model ?? "").trim().toLowerCase();
|
|
77069
|
+
return id.slice(Math.max(id.lastIndexOf("/"), id.lastIndexOf(":")) + 1).startsWith("deepseek");
|
|
77070
|
+
}
|
|
77071
|
+
/**
|
|
76481
77072
|
* Build the wire request for one harness call.
|
|
76482
77073
|
* @param options - the harness request (model, system, history, tools, sampling).
|
|
76483
77074
|
* @param connection - resolved connection facts (default output budget).
|
|
@@ -76499,6 +77090,8 @@ function buildOpenDesignBody(options, connection, images) {
|
|
|
76499
77090
|
}
|
|
76500
77091
|
}));
|
|
76501
77092
|
const maxTokens = options.maxTokens ?? connection.maxTokens;
|
|
77093
|
+
const reasoningEffort = options.reasoningEffort === void 0 ? void 0 : String(options.reasoningEffort);
|
|
77094
|
+
const thinkingOff = reasoningEffort !== void 0 && reasoningEffort.toLowerCase() === "none" && isDeepSeekModel(options.model);
|
|
76502
77095
|
return {
|
|
76503
77096
|
model: options.model,
|
|
76504
77097
|
messages,
|
|
@@ -76508,7 +77101,8 @@ function buildOpenDesignBody(options, connection, images) {
|
|
|
76508
77101
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
76509
77102
|
...maxTokens === void 0 || maxTokens <= 0 ? {} : { max_tokens: maxTokens },
|
|
76510
77103
|
...options.stop === void 0 ? {} : { stop: options.stop },
|
|
76511
|
-
...
|
|
77104
|
+
...thinkingOff || reasoningEffort === void 0 ? {} : { reasoning_effort: reasoningEffort },
|
|
77105
|
+
...thinkingOff ? { thinking: { type: "disabled" } } : {}
|
|
76512
77106
|
};
|
|
76513
77107
|
}
|
|
76514
77108
|
/** Map an OpenAI `finish_reason` to the harness finish reason. */
|
|
@@ -76627,8 +77221,16 @@ function streamErrorCode(message, status) {
|
|
|
76627
77221
|
}
|
|
76628
77222
|
/**
|
|
76629
77223
|
* Parse an SSE body into decoded JSON frames.
|
|
77224
|
+
*
|
|
77225
|
+
* The parser's contract is the sibling line's and stays unsoftened: a body that
|
|
77226
|
+
* ends without `[DONE]` is reported by THROWING `STREAM_CLOSED` after its last
|
|
77227
|
+
* frame. An SSE parser cannot tell a gateway that closes politely after the
|
|
77228
|
+
* terminal frame from a truncated response, because it never interprets
|
|
77229
|
+
* `finish_reason` — so it must not decide that question for its callers either.
|
|
77230
|
+
* That verdict belongs to {@link translate}, which has the frames, and absorbs
|
|
77231
|
+
* this one code from its input.
|
|
76630
77232
|
* @param body - a web ReadableStream of UTF-8 bytes.
|
|
76631
|
-
* @returns every `data:` payload up to `[DONE]`.
|
|
77233
|
+
* @returns every `data:` payload up to `[DONE]`, then throws `STREAM_CLOSED`.
|
|
76632
77234
|
*/
|
|
76633
77235
|
async function* parseServerSentEvents(body) {
|
|
76634
77236
|
const decoder = new TextDecoder();
|
|
@@ -76674,6 +77276,36 @@ async function* parseServerSentEvents(body) {
|
|
|
76674
77276
|
}
|
|
76675
77277
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
76676
77278
|
}
|
|
77279
|
+
/**
|
|
77280
|
+
* Whether one failure is the SSE parser's own "the body ended without [DONE]".
|
|
77281
|
+
*
|
|
77282
|
+
* Structural rather than `instanceof`: what has to match is the CODE the parser
|
|
77283
|
+
* assigns, and a second copy of this module (or a harness generation that ships
|
|
77284
|
+
* its own LlmError class) would fail an identity test on a value that is
|
|
77285
|
+
* otherwise exactly the signal this decision is about.
|
|
77286
|
+
*/
|
|
77287
|
+
function isMissingSentinel(error) {
|
|
77288
|
+
return typeof error === "object" && error !== null && error.code === "STREAM_CLOSED";
|
|
77289
|
+
}
|
|
77290
|
+
/**
|
|
77291
|
+
* End the payload iteration instead of throwing when a body stops without the
|
|
77292
|
+
* sentinel.
|
|
77293
|
+
*
|
|
77294
|
+
* The parser can only report "no [DONE] arrived" by throwing, and that is NOT a
|
|
77295
|
+
* failure on its own: the gateway sometimes closes the response right after the
|
|
77296
|
+
* terminal frame. Whether the response actually completed is decided by the
|
|
77297
|
+
* frames that were seen, which is knowledge this generator does not have — so it
|
|
77298
|
+
* only removes the transport signal and leaves the verdict to {@link translate}.
|
|
77299
|
+
* Every other failure passes through untouched, including the error frames a 200
|
|
77300
|
+
* body can carry.
|
|
77301
|
+
*/
|
|
77302
|
+
async function* tolerateMissingSentinel(payloads) {
|
|
77303
|
+
try {
|
|
77304
|
+
for await (const payload of payloads) yield payload;
|
|
77305
|
+
} catch (error) {
|
|
77306
|
+
if (!isMissingSentinel(error)) throw error;
|
|
77307
|
+
}
|
|
77308
|
+
}
|
|
76677
77309
|
/** Assemble the terminal content block for one open block. */
|
|
76678
77310
|
function closeBlock(block) {
|
|
76679
77311
|
switch (block.kind) {
|
|
@@ -76697,9 +77329,21 @@ function closeBlock(block) {
|
|
|
76697
77329
|
* Translate decoded SSE payloads into harness stream chunks.
|
|
76698
77330
|
*
|
|
76699
77331
|
* Deltas stream through as they arrive; block ends, usage and the finish reason
|
|
76700
|
-
* are all deferred to the
|
|
77332
|
+
* are all deferred to the end of the answer, so no chunk ever follows `finish`.
|
|
76701
77333
|
* A completion with no content at all becomes an `EMPTY_RESPONSE` error finish
|
|
76702
77334
|
* rather than a silent empty message.
|
|
77335
|
+
*
|
|
77336
|
+
* The end of the answer is either the `[DONE]` sentinel or the end of the body
|
|
77337
|
+
* AFTER a terminal `finish_reason` arrived. Some gateways close the socket
|
|
77338
|
+
* instead of writing the sentinel, and failing such an answer threw away content
|
|
77339
|
+
* the caller had already received.
|
|
77340
|
+
*
|
|
77341
|
+
* The parser still raises `STREAM_CLOSED` for a body with no sentinel — that is
|
|
77342
|
+
* its contract and it is not widened here. The signal is absorbed where the
|
|
77343
|
+
* frames are available instead (see {@link tolerateMissingSentinel}), and only
|
|
77344
|
+
* then does the finish reason decide: a body that stated one closes like any
|
|
77345
|
+
* other, and a body that never stated one is truncation and still fails with
|
|
77346
|
+
* `STREAM_CLOSED`. No other input failure is absorbed.
|
|
76703
77347
|
*/
|
|
76704
77348
|
async function* translate$1(payloads) {
|
|
76705
77349
|
let nextIndex = 0;
|
|
@@ -76726,38 +77370,44 @@ async function* translate$1(payloads) {
|
|
|
76726
77370
|
order.push(block);
|
|
76727
77371
|
return block;
|
|
76728
77372
|
};
|
|
76729
|
-
|
|
77373
|
+
/** The terminal chunks of one completed answer, in the order the harness accepts. */
|
|
77374
|
+
const terminal = (reason) => {
|
|
77375
|
+
const chunks = [];
|
|
77376
|
+
for (const block of order) chunks.push({
|
|
77377
|
+
type: "block-end",
|
|
77378
|
+
index: block.index,
|
|
77379
|
+
block: closeBlock(block)
|
|
77380
|
+
});
|
|
77381
|
+
if (pendingUsage !== void 0) chunks.push({
|
|
77382
|
+
type: "usage",
|
|
77383
|
+
usage: pendingUsage
|
|
77384
|
+
});
|
|
77385
|
+
const hasVisibleAnswer = toolBlocks.size > 0 || textBlock !== void 0 && textBlock.text.trim() !== "";
|
|
77386
|
+
chunks.push({
|
|
77387
|
+
type: "finish",
|
|
77388
|
+
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
77389
|
+
kind: "error",
|
|
77390
|
+
failure: {
|
|
77391
|
+
message: "model returned a completed response with no content",
|
|
77392
|
+
code: EMPTY_RESPONSE_CODE
|
|
77393
|
+
}
|
|
77394
|
+
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
77395
|
+
kind: "error",
|
|
77396
|
+
failure: {
|
|
77397
|
+
message: "OpenDesign ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
77398
|
+
code: "PROVIDER_ERROR"
|
|
77399
|
+
}
|
|
77400
|
+
} : reason.reason
|
|
77401
|
+
});
|
|
77402
|
+
return chunks;
|
|
77403
|
+
};
|
|
77404
|
+
for await (const payload of tolerateMissingSentinel(payloads)) {
|
|
76730
77405
|
if (payload === "[DONE]") {
|
|
76731
|
-
for (const block of order) yield {
|
|
76732
|
-
type: "block-end",
|
|
76733
|
-
index: block.index,
|
|
76734
|
-
block: closeBlock(block)
|
|
76735
|
-
};
|
|
76736
|
-
if (pendingUsage !== void 0) yield {
|
|
76737
|
-
type: "usage",
|
|
76738
|
-
usage: pendingUsage
|
|
76739
|
-
};
|
|
76740
77406
|
const reason = pendingFinish ?? {
|
|
76741
77407
|
type: "finish",
|
|
76742
77408
|
reason: { kind: "stop" }
|
|
76743
77409
|
};
|
|
76744
|
-
const
|
|
76745
|
-
yield {
|
|
76746
|
-
type: "finish",
|
|
76747
|
-
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
76748
|
-
kind: "error",
|
|
76749
|
-
failure: {
|
|
76750
|
-
message: "model returned a completed response with no content",
|
|
76751
|
-
code: EMPTY_RESPONSE_CODE
|
|
76752
|
-
}
|
|
76753
|
-
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
76754
|
-
kind: "error",
|
|
76755
|
-
failure: {
|
|
76756
|
-
message: "OpenDesign ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
76757
|
-
code: "PROVIDER_ERROR"
|
|
76758
|
-
}
|
|
76759
|
-
} : reason.reason
|
|
76760
|
-
};
|
|
77410
|
+
for (const chunk of terminal(reason)) yield chunk;
|
|
76761
77411
|
return;
|
|
76762
77412
|
}
|
|
76763
77413
|
if (isErrorPayload(payload)) throw new LlmError(errorText(payload.error), streamErrorCode(errorText(payload.error)));
|
|
@@ -76831,6 +77481,10 @@ async function* translate$1(payloads) {
|
|
|
76831
77481
|
}
|
|
76832
77482
|
if (isRecord$2(chunk.usage)) pendingUsage = mapUsage(chunk.usage);
|
|
76833
77483
|
}
|
|
77484
|
+
if (pendingFinish !== void 0) {
|
|
77485
|
+
for (const chunk of terminal(pendingFinish)) yield chunk;
|
|
77486
|
+
return;
|
|
77487
|
+
}
|
|
76834
77488
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
76835
77489
|
}
|
|
76836
77490
|
//#endregion
|
|
@@ -76891,6 +77545,23 @@ const TRANSPORT_RETRY_BASE_MS = 400;
|
|
|
76891
77545
|
* but the ladder still prefers published data for anything the tables know.
|
|
76892
77546
|
*/
|
|
76893
77547
|
const REASONING_EFFORT_IDS = DEFAULT_EFFORTS;
|
|
77548
|
+
/**
|
|
77549
|
+
* How this route carries a mid-conversation tool change, or undefined for none.
|
|
77550
|
+
*
|
|
77551
|
+
* `in-history`, not `addition-only`: both append an addition, but only
|
|
77552
|
+
* `in-history` can also express a REMOVAL. Under `addition-only` a revoked
|
|
77553
|
+
* tool is deleted from the declaration list, the top-level `tools` array
|
|
77554
|
+
* shrinks, and the gateway's prefix match is thrown away for the whole prompt.
|
|
77555
|
+
*
|
|
77556
|
+
* The field is spread into the descriptor only when this route wants one, so a
|
|
77557
|
+
* harness generation that does not know it simply never reads it — no version
|
|
77558
|
+
* check, and no change to the message shape either way: mapper.ts renders each
|
|
77559
|
+
* change as a system sentence and never introduces a native dynamic-tool
|
|
77560
|
+
* declaration, which an OpenAI-compatible gateway has no field for.
|
|
77561
|
+
*/
|
|
77562
|
+
function toolUpdateMode() {
|
|
77563
|
+
return "in-history";
|
|
77564
|
+
}
|
|
76894
77565
|
var OpenDesignAdapter = class extends LlmAdapter {
|
|
76895
77566
|
modelSettings;
|
|
76896
77567
|
options;
|
|
@@ -76949,6 +77620,7 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
76949
77620
|
id: modelId,
|
|
76950
77621
|
name: info.displayName,
|
|
76951
77622
|
inputModalities: info.input,
|
|
77623
|
+
...toolUpdateMode() === void 0 ? {} : { toolUpdate: toolUpdateMode() },
|
|
76952
77624
|
context: { contextWindow: info.contextWindow },
|
|
76953
77625
|
...outputReservation(info.contextWindow, info.maxTokens),
|
|
76954
77626
|
...info.reasoning && info.efforts.length > 0 ? { reasoning: {
|
|
@@ -76976,11 +77648,12 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
76976
77648
|
}
|
|
76977
77649
|
async *requestStream(rawOptions, settings, signal, activity) {
|
|
76978
77650
|
if (settings.enabled === false) throw new LlmError(`${PROVIDER_NAME$2} is disabled in Settings > Subscription. Enable the line there, then retry.`, "INVALID_REQUEST");
|
|
76979
|
-
const
|
|
76980
|
-
const images = await resolveRequestImages$1(
|
|
77651
|
+
const normalized = normalizeGenerateOptions(rawOptions);
|
|
77652
|
+
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal).catch((error) => {
|
|
76981
77653
|
if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
|
|
76982
77654
|
return NO_RESOLVED_IMAGES$3;
|
|
76983
77655
|
});
|
|
77656
|
+
const options = offloadOldestRequestImages$1(normalized, MAX_REQUEST_IMAGE_BYTES$1, images);
|
|
76984
77657
|
const body = buildOpenDesignBody(options, { maxTokens: (await this.modelInfo(options.model)).maxTokens }, images);
|
|
76985
77658
|
const refused = /* @__PURE__ */ new Set();
|
|
76986
77659
|
let lastError = null;
|
|
@@ -82700,6 +83373,20 @@ function createCacheWarmupResultTool(deps) {
|
|
|
82700
83373
|
* 预览只有坏处没有好处,所以这里写死一个 provider 名而不是做成开关。
|
|
82701
83374
|
*/
|
|
82702
83375
|
const CACHE_WARMUP_PROVIDER = "kimi-code";
|
|
83376
|
+
/**
|
|
83377
|
+
* 允许进入结果投影的工具集合。
|
|
83378
|
+
*
|
|
83379
|
+
* 除了 PTC 的 `run_code`,宿主的独立 Shell 与后台任务输出工具(`win_bash`、
|
|
83380
|
+
* `bash`、`pwsh`、`job_output`)同样经常吐出整屏测试/构建日志。源码读取/编辑类
|
|
83381
|
+
* 工具(`read`、`edit`、`grep`、`glob`、CodeGraph 等)不在此列,保持 100% 原样。
|
|
83382
|
+
*/
|
|
83383
|
+
const CACHE_WARMUP_SOURCE_TOOLS = /* @__PURE__ */ new Set([
|
|
83384
|
+
"run_code",
|
|
83385
|
+
"win_bash",
|
|
83386
|
+
"bash",
|
|
83387
|
+
"pwsh",
|
|
83388
|
+
"job_output"
|
|
83389
|
+
]);
|
|
82703
83390
|
/** 提示词分节名,全局唯一(重复注册会抛错)。 */
|
|
82704
83391
|
const CACHE_WARMUP_SECTION_NAME = "plugin:cache-warmup";
|
|
82705
83392
|
/**
|
|
@@ -82943,7 +83630,7 @@ function installCacheWarmup(ctx, options) {
|
|
|
82943
83630
|
const projectDecision = async (exec, result, decision) => {
|
|
82944
83631
|
if (decision.kind !== "accept") return decision;
|
|
82945
83632
|
if (decision.value !== void 0) return decision;
|
|
82946
|
-
if (exec.name
|
|
83633
|
+
if (!CACHE_WARMUP_SOURCE_TOOLS.has(exec.name)) return decision;
|
|
82947
83634
|
if (!options.settings().enabled) return decision;
|
|
82948
83635
|
const route = cacheWarmupRouteOf(exec.agent);
|
|
82949
83636
|
if (route.provider !== "kimi-code") return decision;
|
|
@@ -82954,7 +83641,13 @@ function installCacheWarmup(ctx, options) {
|
|
|
82954
83641
|
const blocks = decision.content ?? result.content;
|
|
82955
83642
|
if (blocks.length === 0) return decision;
|
|
82956
83643
|
const content = blocks;
|
|
82957
|
-
|
|
83644
|
+
let durable;
|
|
83645
|
+
try {
|
|
83646
|
+
durable = options.durableHint?.(sessionId);
|
|
83647
|
+
} catch {
|
|
83648
|
+
durable = void 0;
|
|
83649
|
+
}
|
|
83650
|
+
const budget = durable === void 0 ? cacheWarmupBudget(sessionId) : cacheWarmupBudget(sessionId, Date.now(), durable);
|
|
82958
83651
|
const projected = await projectToolResult({
|
|
82959
83652
|
toolName: exec.name,
|
|
82960
83653
|
provider: route.provider,
|
|
@@ -83191,7 +83884,19 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
83191
83884
|
return dispose;
|
|
83192
83885
|
}, "dsh-chatgpt-subscription: no-answer resume");
|
|
83193
83886
|
}
|
|
83194
|
-
ctx.effect(() => installCacheWarmup(ctx, {
|
|
83887
|
+
ctx.effect(() => installCacheWarmup(ctx, {
|
|
83888
|
+
settings: () => resolveCacheWarmupSettings(pluginConfig),
|
|
83889
|
+
durableHint: (sessionId) => {
|
|
83890
|
+
const record = sessionCacheState.read(sessionId);
|
|
83891
|
+
if (record === void 0) return void 0;
|
|
83892
|
+
const total = record.cachedTokens + record.freshTokens;
|
|
83893
|
+
return {
|
|
83894
|
+
lastActiveAt: record.lastActiveAt,
|
|
83895
|
+
consecutiveHits: record.warmupConsecutiveHits ?? 0,
|
|
83896
|
+
hitRatio: total > 0 ? record.cachedTokens / total : 0
|
|
83897
|
+
};
|
|
83898
|
+
}
|
|
83899
|
+
}), "dsh-chatgpt-subscription: cache warmup");
|
|
83195
83900
|
const probeEnvFile = relayProbeEnvFile();
|
|
83196
83901
|
if (relayProbeEnabled(process.env, { envFile: probeEnvFile })) {
|
|
83197
83902
|
const probePath = relayProbeLogPath(process.env, { envFile: probeEnvFile });
|