@eddyskywalker/dsh-chatgpt-subscription 0.45.0 → 0.46.1
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 +20 -0
- package/lib/client.js +19 -0
- package/lib/client.js.map +1 -1
- package/lib/index.js +883 -153
- 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/host/responses-client.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.1";
|
|
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`;
|
|
@@ -11585,6 +11585,13 @@ function record$3(value) {
|
|
|
11585
11585
|
//#endregion
|
|
11586
11586
|
//#region src/host/responses-client.ts
|
|
11587
11587
|
const MAX_VISIBLE_REASONING_CHARS = 12e3;
|
|
11588
|
+
/**
|
|
11589
|
+
* A handshake/connect failure happens before any request bytes reach ChatGPT,
|
|
11590
|
+
* so only that narrow family is safe to replay here. Two short retries ride out
|
|
11591
|
+
* the intermittent proxy/VPN paths that otherwise fail a whole model request.
|
|
11592
|
+
*/
|
|
11593
|
+
const PRE_CONNECTION_RETRIES = 2;
|
|
11594
|
+
const PRE_CONNECTION_RETRY_BASE_MS = 400;
|
|
11588
11595
|
/** Cooldown a rotated account takes when the provider states no retry delay. */
|
|
11589
11596
|
const DEFAULT_POOL_COOLDOWN_MS = 15 * 6e4;
|
|
11590
11597
|
const REASONING_DELTA_FLUSH_CHARS = 768;
|
|
@@ -11802,7 +11809,7 @@ var ResponsesClient = class {
|
|
|
11802
11809
|
}
|
|
11803
11810
|
}
|
|
11804
11811
|
async request(payload, credentials, sessionId, turnKey, signal) {
|
|
11805
|
-
try {
|
|
11812
|
+
for (let attempt = 0;; attempt += 1) try {
|
|
11806
11813
|
return await this.fetchFn(CODEX_RESPONSES_URL, {
|
|
11807
11814
|
method: "POST",
|
|
11808
11815
|
headers: {
|
|
@@ -11815,12 +11822,30 @@ var ResponsesClient = class {
|
|
|
11815
11822
|
});
|
|
11816
11823
|
} catch (cause) {
|
|
11817
11824
|
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
11825
|
+
if (attempt < PRE_CONNECTION_RETRIES && isPreConnectionFailure$2(cause)) {
|
|
11826
|
+
try {
|
|
11827
|
+
await setTimeout$1(PRE_CONNECTION_RETRY_BASE_MS * (attempt + 1), void 0, { signal });
|
|
11828
|
+
} catch (waitError) {
|
|
11829
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
11830
|
+
throw waitError;
|
|
11831
|
+
}
|
|
11832
|
+
continue;
|
|
11833
|
+
}
|
|
11818
11834
|
const chain = errorChain(cause).trim();
|
|
11819
|
-
throw new LlmError(`Codex request failed: ${chain === "" ? "the connection failed before any response arrived" : chain.slice(0, 500)}`, "NETWORK", { cause });
|
|
11835
|
+
throw new LlmError(`Codex request failed: ${chain === "" ? "the connection failed before any response arrived" : chain.slice(0, 500)}${isPreConnectionFailure$2(cause) ? " The request never reached ChatGPT. Check the network, proxy/VPN/TUN, or the plugin proxy settings." : ""}`, "NETWORK", { cause });
|
|
11820
11836
|
}
|
|
11821
11837
|
}
|
|
11822
11838
|
};
|
|
11823
11839
|
/**
|
|
11840
|
+
* Whether the transport failed before the request could reach ChatGPT.
|
|
11841
|
+
*
|
|
11842
|
+
* Deliberately exclude `ECONNRESET` and generic socket deaths: either can happen
|
|
11843
|
+
* after the server accepted the body, where replaying a POST would be ambiguous.
|
|
11844
|
+
*/
|
|
11845
|
+
function isPreConnectionFailure$2(error) {
|
|
11846
|
+
return /Client network socket disconnected before secure TLS connection|Connect Timeout Error|UND_ERR_CONNECT_TIMEOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(errorChain(error));
|
|
11847
|
+
}
|
|
11848
|
+
/**
|
|
11824
11849
|
* The failure a stream that died mid-body is reported as.
|
|
11825
11850
|
*
|
|
11826
11851
|
* Everything above the response body is already typed — the request path raises
|
|
@@ -24710,6 +24735,19 @@ const CACHE_WARMUP_DEFAULTS = {
|
|
|
24710
24735
|
hitRatioThreshold: .97,
|
|
24711
24736
|
/** 退出预热期需要连续命中的报告条数。 */
|
|
24712
24737
|
consecutiveHitsRequired: 2,
|
|
24738
|
+
/**
|
|
24739
|
+
* 稳态第二轨(短会话分母效应兜底):已命中的缓存前缀至少达到该 token 规模,
|
|
24740
|
+
* 且相比上一条报告一字未掉(`cacheReadTokens >= prev.cacheReadTokens`),
|
|
24741
|
+
* 同时本轮新增未命中部分不超过 `retainedPrefixMaxFreshTokens` 时,同样计为一次有效命中。
|
|
24742
|
+
*/
|
|
24743
|
+
retainedPrefixMinCachedTokens: 1024,
|
|
24744
|
+
/** 稳态第二轨允许的单轮最大新增未命中 token 数(与 Kimi 256-token 缓存块对齐)。 */
|
|
24745
|
+
retainedPrefixMaxFreshTokens: 256,
|
|
24746
|
+
/**
|
|
24747
|
+
* 缓存存活窗口(毫秒,25 分钟,与 Kimi 实测全热上限一致)。
|
|
24748
|
+
* 距离最近一次报告超过该时长时,预算计算自动预判缓存已冷、退回预热期保守预算。
|
|
24749
|
+
*/
|
|
24750
|
+
cacheTtlMs: 1500 * 1e3,
|
|
24713
24751
|
/** 单结果原文上限(字节)。 */
|
|
24714
24752
|
maxResultBytes: 8 * 1024 * 1024,
|
|
24715
24753
|
/** 每 session 原文总量上限(字节)。 */
|
|
@@ -24773,12 +24811,13 @@ function cellOf(value) {
|
|
|
24773
24811
|
return "";
|
|
24774
24812
|
}
|
|
24775
24813
|
}
|
|
24776
|
-
/** JSON Lines
|
|
24814
|
+
/** JSON Lines:每一行都是一个结构一致的表格式对象,且至少 5 行(防止 2~3 个异构 tool 结果对象被当成一张表压掉)。 */
|
|
24777
24815
|
function parseJsonLines(lines) {
|
|
24778
|
-
if (lines.length <
|
|
24816
|
+
if (lines.length < 5) return void 0;
|
|
24779
24817
|
const fields = [];
|
|
24780
24818
|
const seen = /* @__PURE__ */ new Set();
|
|
24781
24819
|
const rows = [];
|
|
24820
|
+
let firstKeySet;
|
|
24782
24821
|
for (const line of lines) {
|
|
24783
24822
|
let parsed;
|
|
24784
24823
|
try {
|
|
@@ -24788,6 +24827,10 @@ function parseJsonLines(lines) {
|
|
|
24788
24827
|
}
|
|
24789
24828
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
24790
24829
|
const record = parsed;
|
|
24830
|
+
const keys = Object.keys(record);
|
|
24831
|
+
if (keys.length === 0) return void 0;
|
|
24832
|
+
if (firstKeySet === void 0) firstKeySet = new Set(keys);
|
|
24833
|
+
else if (!keys.some((k) => firstKeySet.has(k))) return void 0;
|
|
24791
24834
|
const row = {};
|
|
24792
24835
|
for (const [key, value] of Object.entries(record)) {
|
|
24793
24836
|
if (!seen.has(key)) {
|
|
@@ -24804,7 +24847,7 @@ function parseJsonLines(lines) {
|
|
|
24804
24847
|
rows
|
|
24805
24848
|
};
|
|
24806
24849
|
}
|
|
24807
|
-
/**
|
|
24850
|
+
/** 分隔符表格:首行合法表头(标识符列名,非 grep 路径行号或代码行)+ 至少两行同列数的数据。 */
|
|
24808
24851
|
function parseDelimitedRows(lines) {
|
|
24809
24852
|
if (lines.length < 3) return void 0;
|
|
24810
24853
|
const header = lines[0];
|
|
@@ -24812,6 +24855,7 @@ function parseDelimitedRows(lines) {
|
|
|
24812
24855
|
if (delimiter === void 0) return void 0;
|
|
24813
24856
|
const fields = splitDelimitedLine(header, delimiter).map((cell) => cell.trim());
|
|
24814
24857
|
if (fields.length < 2 || fields.some((field) => field === "")) return void 0;
|
|
24858
|
+
if (fields.some((field) => field.length > 64 || /[:;{}()[\]\\/*'"`]/.test(field) || /^\d+$/.test(field))) return;
|
|
24815
24859
|
if (new Set(fields).size !== fields.length) return void 0;
|
|
24816
24860
|
const rows = [];
|
|
24817
24861
|
for (const line of lines.slice(1)) {
|
|
@@ -24916,12 +24960,40 @@ function clampMaxBytes(maxBytes) {
|
|
|
24916
24960
|
if (rounded < 1) return 1;
|
|
24917
24961
|
return Math.min(rounded, CACHE_WARMUP_DEFAULTS.pageMaxBytesMax);
|
|
24918
24962
|
}
|
|
24919
|
-
/**
|
|
24920
|
-
|
|
24963
|
+
/**
|
|
24964
|
+
* 判断一段文本是否为不含真实换行(或几乎全在一行内)的 JSON 转义文本。
|
|
24965
|
+
*
|
|
24966
|
+
* 当 `run_code` 返回 `JSON.stringify({ stdout: "a\nb\nc..." })` 时,原文逐字保存在
|
|
24967
|
+
* 存储里(不改任何字节),但整份文本里没有真 `\n`,只有字面量两字符序列 `\n`。
|
|
24968
|
+
* 若只数真 `\n`,`limit: 64` 行会退化成一次吐完 `maxBytes`。识别出这种形态后,
|
|
24969
|
+
* `pageText` 和 `countLines` 把未转义的 `\n`(即前面有奇数个 `\` 的 `n`)也算作逻辑换行,
|
|
24970
|
+
* 既 100% 保持「翻页拼起来逐字节等于原文」,又让 `limit` 行上限对单行 JSON 同样生效。
|
|
24971
|
+
*/
|
|
24972
|
+
function usesEscapedNewlines(text) {
|
|
24973
|
+
if (text.length < 4) return false;
|
|
24974
|
+
let realNewlines = 0;
|
|
24975
|
+
for (let i = 0; i < text.length; i += 1) if (text.charCodeAt(i) === 10) {
|
|
24976
|
+
realNewlines += 1;
|
|
24977
|
+
if (realNewlines > 2) return false;
|
|
24978
|
+
}
|
|
24979
|
+
return text.includes("\\n");
|
|
24980
|
+
}
|
|
24981
|
+
/** 判断 `text[index]`(已知为 `'n'`)是否构成一个 JSON 转义换行 `\n`(前面紧邻奇数个 `\`)。 */
|
|
24982
|
+
function isEscapedNewlineAt(text, index) {
|
|
24983
|
+
let backslashes = 0;
|
|
24984
|
+
for (let cursor = index - 1; cursor >= 0 && text.charCodeAt(cursor) === 92; cursor -= 1) backslashes += 1;
|
|
24985
|
+
return (backslashes & 1) === 1;
|
|
24986
|
+
}
|
|
24987
|
+
/** 一段文本的行数(末尾没有换行也算一行;支持单行 JSON 内的转义 `\n`)。 */
|
|
24988
|
+
function countLines(text, escapedMode = usesEscapedNewlines(text)) {
|
|
24921
24989
|
if (text === "") return 0;
|
|
24922
24990
|
let lines = 0;
|
|
24923
|
-
for (
|
|
24924
|
-
|
|
24991
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
24992
|
+
const code = text.charCodeAt(index);
|
|
24993
|
+
if (code === 10) lines += 1;
|
|
24994
|
+
else if (escapedMode && code === 110 && isEscapedNewlineAt(text, index)) lines += 1;
|
|
24995
|
+
}
|
|
24996
|
+
return text.endsWith("\n") || escapedMode && text.endsWith("n") && isEscapedNewlineAt(text, text.length - 1) ? lines : lines + 1;
|
|
24925
24997
|
}
|
|
24926
24998
|
/**
|
|
24927
24999
|
* 按 UTF-8 字节安全分页。
|
|
@@ -24937,6 +25009,7 @@ function pageText(args) {
|
|
|
24937
25009
|
if (!Number.isInteger(offset) || offset < 0) throw new CacheWarmupStoreError("offset must be a non-negative integer byte offset", "OFFSET_OUT_OF_RANGE");
|
|
24938
25010
|
if (offset > totalBytes) throw new CacheWarmupStoreError(`offset ${offset} is past the ${totalBytes}-byte result`, "OFFSET_OUT_OF_RANGE");
|
|
24939
25011
|
if (offset < totalBytes && (buffer[offset] & 192) === 128) throw new CacheWarmupStoreError("offset is not on a UTF-8 character boundary", "OFFSET_INVALID");
|
|
25012
|
+
const escapedMode = usesEscapedNewlines(text);
|
|
24940
25013
|
const rest = offset === 0 ? text : new TextDecoder("utf-8").decode(buffer.subarray(offset));
|
|
24941
25014
|
let content = "";
|
|
24942
25015
|
let contentBytes = 0;
|
|
@@ -24960,6 +25033,7 @@ function pageText(args) {
|
|
|
24960
25033
|
content += char;
|
|
24961
25034
|
contentBytes += charBytes;
|
|
24962
25035
|
if (char === "\n") lines += 1;
|
|
25036
|
+
else if (escapedMode && char === "n" && isEscapedNewlineAt(content, content.length - 1)) lines += 1;
|
|
24963
25037
|
}
|
|
24964
25038
|
const nextOffset = offset + Buffer.byteLength(content, "utf8");
|
|
24965
25039
|
const complete = nextOffset >= totalBytes;
|
|
@@ -24975,7 +25049,7 @@ function pageText(args) {
|
|
|
24975
25049
|
omitted: {
|
|
24976
25050
|
reason: complete ? "none" : stoppedByBytes ? "bytes" : stoppedByLines ? "lines" : "bytes",
|
|
24977
25051
|
chars: remaining.length,
|
|
24978
|
-
lines: countLines(remaining),
|
|
25052
|
+
lines: countLines(remaining, escapedMode),
|
|
24979
25053
|
rows: 0,
|
|
24980
25054
|
bytes: Buffer.byteLength(remaining, "utf8")
|
|
24981
25055
|
},
|
|
@@ -25309,6 +25383,28 @@ function cacheWarmupHitRatio(usage) {
|
|
|
25309
25383
|
*/
|
|
25310
25384
|
/** Kimi 对每个请求都会报告的生命周期状态;只有其余值才表示真实前缀漂移。 */
|
|
25311
25385
|
const BENIGN_LIFECYCLE_CAUSES = /* @__PURE__ */ new Set(["stable", "first-request"]);
|
|
25386
|
+
/**
|
|
25387
|
+
* 判断一条报告是否算作「预热命中」(双轨判据):
|
|
25388
|
+
*
|
|
25389
|
+
* 1. **第一轨(比例轨)**:`hitRatio >= 0.97`;
|
|
25390
|
+
* 2. **第二轨(前缀完整延续轨)**:解决短会话的分母效应(如 `1792 cached + 84 input = 95.52%`)。
|
|
25391
|
+
* 当本轮 `cacheReadTokens >= 1024`(已形成实质缓存前缀)、存在紧邻的上一条无真实漂移报告、
|
|
25392
|
+
* 本轮命中缓存没有比上一轮缩水(`cacheReadTokens >= previous.cacheReadTokens`),
|
|
25393
|
+
* 且本轮新增未命中部分 `inputTokens <= 256`(一个缓存块的正常尾巴)时,同样视为有效命中。
|
|
25394
|
+
*/
|
|
25395
|
+
function isWarmupHitReport(report, previous) {
|
|
25396
|
+
if (cacheWarmupHitRatio(report) >= CACHE_WARMUP_DEFAULTS.hitRatioThreshold) return true;
|
|
25397
|
+
if (previous === void 0 || previous === null) return false;
|
|
25398
|
+
const prevDrift = previous.driftCause;
|
|
25399
|
+
if (typeof prevDrift === "string" && prevDrift.trim() !== "" && !BENIGN_LIFECYCLE_CAUSES.has(prevDrift)) return false;
|
|
25400
|
+
const cached = finiteCount(report.cacheReadTokens);
|
|
25401
|
+
const prevCached = finiteCount(previous.cacheReadTokens);
|
|
25402
|
+
const fresh = finiteCount(report.inputTokens);
|
|
25403
|
+
if (cached < CACHE_WARMUP_DEFAULTS.retainedPrefixMinCachedTokens) return false;
|
|
25404
|
+
if (prevCached < CACHE_WARMUP_DEFAULTS.retainedPrefixMinCachedTokens) return false;
|
|
25405
|
+
if (cached < prevCached) return false;
|
|
25406
|
+
return fresh <= CACHE_WARMUP_DEFAULTS.retainedPrefixMaxFreshTokens;
|
|
25407
|
+
}
|
|
25312
25408
|
function consecutiveWarmupHits(reports) {
|
|
25313
25409
|
try {
|
|
25314
25410
|
if (!Array.isArray(reports)) return 0;
|
|
@@ -25318,7 +25414,7 @@ function consecutiveWarmupHits(reports) {
|
|
|
25318
25414
|
if (report === void 0 || report === null) break;
|
|
25319
25415
|
const drift = report.driftCause;
|
|
25320
25416
|
if (typeof drift === "string" && drift.trim() !== "" && !BENIGN_LIFECYCLE_CAUSES.has(drift)) break;
|
|
25321
|
-
if (
|
|
25417
|
+
if (!isWarmupHitReport(report, index > 0 ? reports[index - 1] : void 0)) break;
|
|
25322
25418
|
hits += 1;
|
|
25323
25419
|
}
|
|
25324
25420
|
return hits;
|
|
@@ -25447,21 +25543,41 @@ const WARMUP_BUDGET = Object.freeze({
|
|
|
25447
25543
|
* 当前会话的预览预算。
|
|
25448
25544
|
*
|
|
25449
25545
|
* 规则(约束 4):预热期 `maxChars = 1200`(约 600-750 tokens);连续
|
|
25450
|
-
* `consecutiveHitsRequired`(2)
|
|
25451
|
-
* `maxChars = 8000`。任何一次 drift
|
|
25546
|
+
* `consecutiveHitsRequired`(2) 次命中(比例 ≥ 0.97 或前缀完整延续且新增 ≤ 256 tokens)
|
|
25547
|
+
* 且期间没有 drift,才进入稳态 `maxChars = 8000`。任何一次 drift 都清零连续计数,
|
|
25548
|
+
* 于是下一条请求立刻回落到预热预算。
|
|
25549
|
+
*
|
|
25550
|
+
* 空闲 TTL 预判:若距离最近一条报告已超过 `cacheTtlMs`(25 分钟,Kimi 实测缓存
|
|
25551
|
+
* 全热上限),说明上游缓存大概率已冷,无需等下一轮请求失败,当场退回预热期保守预算。
|
|
25452
25552
|
*
|
|
25453
25553
|
* **不抛出**:没有会话、没有报告、登记表异常,一律返回预热期的保守预算。
|
|
25454
25554
|
* @param sessionId - 会话 id;不传则按最保守的预热预算处理。
|
|
25555
|
+
* @param now - 当前时间戳(毫秒),默认 `Date.now()`;测试可注入。
|
|
25556
|
+
* @param durable - 进程重启后尚无内存报告时的持久化回退摘要(可选)。
|
|
25455
25557
|
* @returns 本次投影可用的预算。
|
|
25456
25558
|
*/
|
|
25457
|
-
function cacheWarmupBudget(sessionId) {
|
|
25559
|
+
function cacheWarmupBudget(sessionId, now = Date.now(), durable) {
|
|
25458
25560
|
try {
|
|
25459
25561
|
if (typeof sessionId !== "string" || sessionId.trim() === "") return { ...WARMUP_BUDGET };
|
|
25460
25562
|
const reports = cacheWarmupUsageReports(sessionId);
|
|
25461
|
-
if (reports.length === 0)
|
|
25462
|
-
|
|
25563
|
+
if (reports.length === 0) {
|
|
25564
|
+
if (durable !== void 0 && durable !== null && typeof durable === "object") {
|
|
25565
|
+
const lastAt = durable.lastActiveAt;
|
|
25566
|
+
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;
|
|
25567
|
+
const hitRatio = typeof durable.hitRatio === "number" && Number.isFinite(durable.hitRatio) && durable.hitRatio >= 0 ? Math.min(1, durable.hitRatio) : 0;
|
|
25568
|
+
const warmup = hits < CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired;
|
|
25569
|
+
return {
|
|
25570
|
+
maxChars: warmup ? CACHE_WARMUP_DEFAULTS.warmupMaxChars : CACHE_WARMUP_DEFAULTS.steadyStateMaxChars,
|
|
25571
|
+
warmup,
|
|
25572
|
+
consecutiveHits: hits,
|
|
25573
|
+
hitRatio
|
|
25574
|
+
};
|
|
25575
|
+
}
|
|
25576
|
+
return { ...WARMUP_BUDGET };
|
|
25577
|
+
}
|
|
25463
25578
|
const last = reports[reports.length - 1];
|
|
25464
25579
|
const hitRatio = last === void 0 ? 0 : cacheWarmupHitRatio(last);
|
|
25580
|
+
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
25581
|
const warmup = consecutiveHits < CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired;
|
|
25466
25582
|
return {
|
|
25467
25583
|
maxChars: warmup ? CACHE_WARMUP_DEFAULTS.warmupMaxChars : CACHE_WARMUP_DEFAULTS.steadyStateMaxChars,
|
|
@@ -25476,9 +25592,10 @@ function cacheWarmupBudget(sessionId) {
|
|
|
25476
25592
|
/**
|
|
25477
25593
|
* 一份可读快照。**不抛出**,且完全无副作用(只读登记表)。
|
|
25478
25594
|
* @param sessionId - 会话 id;不传则给全局汇总。
|
|
25595
|
+
* @param now - 当前时间戳(毫秒),默认 `Date.now()`。
|
|
25479
25596
|
* @returns 快照。
|
|
25480
25597
|
*/
|
|
25481
|
-
function cacheWarmupSnapshot(sessionId) {
|
|
25598
|
+
function cacheWarmupSnapshot(sessionId, now = Date.now()) {
|
|
25482
25599
|
const fallback = {
|
|
25483
25600
|
budget: { ...WARMUP_BUDGET },
|
|
25484
25601
|
defaults: CACHE_WARMUP_DEFAULTS,
|
|
@@ -25496,7 +25613,7 @@ function cacheWarmupSnapshot(sessionId) {
|
|
|
25496
25613
|
sessions: 0
|
|
25497
25614
|
};
|
|
25498
25615
|
try {
|
|
25499
|
-
const budget = cacheWarmupBudget(sessionId);
|
|
25616
|
+
const budget = cacheWarmupBudget(sessionId, now);
|
|
25500
25617
|
const reports = typeof sessionId === "string" && sessionId.trim() !== "" ? cacheWarmupUsageReports(sessionId) : [];
|
|
25501
25618
|
const totals = {
|
|
25502
25619
|
inputTokens: 0,
|
|
@@ -25559,26 +25676,47 @@ function clip(text, max) {
|
|
|
25559
25676
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
25560
25677
|
}
|
|
25561
25678
|
/**
|
|
25679
|
+
* 剥离终端 ANSI/CSI 转义序列(如 `\x1b[31mFAIL\x1b[0m`)与回车符 `\r`。
|
|
25680
|
+
*
|
|
25681
|
+
* JSON 解码后,原先源码/日志里的 `\u001b[31m` 会还原成控制字符 `\x1b`;
|
|
25682
|
+
* 如果不剥离,`^FAIL` 锚点匹配会被颜色码挡住,而再次 `JSON.stringify` 时每个
|
|
25683
|
+
* `\x1b` 又会膨胀成 6 个字符 `\u001b`。
|
|
25684
|
+
*/
|
|
25685
|
+
const ANSI_ESCAPE = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
25686
|
+
function stripAnsi(text) {
|
|
25687
|
+
return text.replace(ANSI_ESCAPE, "").replace(/\r/g, "");
|
|
25688
|
+
}
|
|
25689
|
+
/**
|
|
25690
|
+
* 计算一行文本在最终输出里占用的精确字符宽度:
|
|
25691
|
+
* - 普通纯文本模式:`line.length + 1`(含换行符 `\n`);
|
|
25692
|
+
* - JSON 字符串内嵌模式:`JSON.stringify(line).length`(去首尾引号后 `+ 2` 个字符的 `\\n` 换行转义,
|
|
25693
|
+
* 恰好等于 `JSON.stringify(line).length`,对 `"`、`\`、`\t`、`\u00xx` 的转义膨胀 100% 精确)。
|
|
25694
|
+
*/
|
|
25695
|
+
function lineCost(line, jsonEscaped) {
|
|
25696
|
+
return jsonEscaped ? JSON.stringify(line).length : line.length + 1;
|
|
25697
|
+
}
|
|
25698
|
+
/**
|
|
25562
25699
|
* 按预算装配预览正文。
|
|
25563
25700
|
*
|
|
25564
25701
|
* 必需段落要么整段进来,要么整体装不下就返回 undefined(调用方据此保持原文);
|
|
25565
25702
|
* 可选段落逐段尝试,装不下就整段跳过。这样任何输出都不会出现「半截失败清单」。
|
|
25566
25703
|
*/
|
|
25567
|
-
function fitSections(sections, maxChars) {
|
|
25704
|
+
function fitSections(sections, maxChars, jsonEscaped = false) {
|
|
25568
25705
|
const lines = [];
|
|
25569
25706
|
let used = 0;
|
|
25707
|
+
const trailingNewlineAllowance = jsonEscaped ? 2 : 1;
|
|
25570
25708
|
for (const section of sections) {
|
|
25571
25709
|
if (section.optional === true) continue;
|
|
25572
25710
|
for (const line of section.lines) {
|
|
25573
|
-
used += line
|
|
25711
|
+
used += lineCost(line, jsonEscaped);
|
|
25574
25712
|
lines.push(line);
|
|
25575
25713
|
}
|
|
25576
25714
|
}
|
|
25577
|
-
if (used > maxChars +
|
|
25715
|
+
if (used > maxChars + trailingNewlineAllowance) return void 0;
|
|
25578
25716
|
for (const section of sections) {
|
|
25579
25717
|
if (section.optional !== true) continue;
|
|
25580
|
-
const size = section.lines.reduce((sum, line) => sum + line
|
|
25581
|
-
if (used + size > maxChars +
|
|
25718
|
+
const size = section.lines.reduce((sum, line) => sum + lineCost(line, jsonEscaped), 0);
|
|
25719
|
+
if (used + size > maxChars + trailingNewlineAllowance) continue;
|
|
25582
25720
|
for (const line of section.lines) lines.push(line);
|
|
25583
25721
|
used += size;
|
|
25584
25722
|
}
|
|
@@ -25613,51 +25751,140 @@ function buildTestSections(evidence) {
|
|
|
25613
25751
|
}
|
|
25614
25752
|
/** 一行 shell 错误。用于「完整错误集」,所以匹配宁可宽一点。 */
|
|
25615
25753
|
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
|
-
/**
|
|
25754
|
+
/** TypeScript 编译器报错首行:`path(line,col): error TSxxxx:` 或 `path:line:col - error TSxxxx:`。 */
|
|
25755
|
+
const TS_DIAGNOSTIC_HEADER = /^(?:.+?\(\d+,\d+\):\s+error\s+TS\d+:|.+?:\d+:\d+\s+-\s+error\s+TS\d+:)/;
|
|
25756
|
+
/**
|
|
25757
|
+
* 紧跟在 TypeScript 诊断首行后面的续行(缩进的类型比对、代码高亮行、波浪线指示行),
|
|
25758
|
+
* 最多连带收集 6 行,使模型无需补读就能看全 `Type A is not assignable to Type B` 的完整原因。
|
|
25759
|
+
*/
|
|
25760
|
+
function isTsDiagnosticDetailLine(raw) {
|
|
25761
|
+
if (/^\s{2,}\S/.test(raw)) return true;
|
|
25762
|
+
if (/^\d+\s+\|/.test(raw.trimStart())) return true;
|
|
25763
|
+
if (/^~+$/.test(raw.trim())) return true;
|
|
25764
|
+
return false;
|
|
25765
|
+
}
|
|
25766
|
+
/** 一行 glob 结果的判定:无空白、像文件路径(至少 80% 含路径分隔符或文件扩展名,不含引号/括号/冒号行号)。 */
|
|
25617
25767
|
function looksLikeGlobResult(text) {
|
|
25618
25768
|
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
25619
25769
|
if (lines.length < 10) return false;
|
|
25620
25770
|
let paths = 0;
|
|
25771
|
+
let pathWithSepOrExt = 0;
|
|
25621
25772
|
for (const line of lines) {
|
|
25622
25773
|
const trimmed = line.trim();
|
|
25623
|
-
if (trimmed.length === 0 || trimmed.length > 400)
|
|
25774
|
+
if (trimmed.length === 0 || trimmed.length > 400) return false;
|
|
25775
|
+
if (GREP_LINE.test(trimmed)) return false;
|
|
25776
|
+
if (/^===.*===$/.test(trimmed)) return false;
|
|
25777
|
+
if (/['"[\]{}()]/.test(trimmed)) return false;
|
|
25624
25778
|
if (/\s/.test(trimmed)) continue;
|
|
25625
25779
|
if (/^[-*+]$/.test(trimmed)) continue;
|
|
25626
25780
|
paths += 1;
|
|
25781
|
+
if (/[\\/]/.test(trimmed) || /\.[A-Za-z0-9]{1,8}$/.test(trimmed)) pathWithSepOrExt += 1;
|
|
25627
25782
|
}
|
|
25628
|
-
return paths / lines.length >= .9;
|
|
25783
|
+
return paths / lines.length >= .9 && pathWithSepOrExt / lines.length >= .8;
|
|
25784
|
+
}
|
|
25785
|
+
/** 判断一行是否只是单纯的文件路径(含可选引号/逗号,如 `"test\\stream-error.test.ts",`),而非报错消息。 */
|
|
25786
|
+
function isBareFilePathLine(trimmed) {
|
|
25787
|
+
const unquoted = trimmed.replace(/^['"]|['"],?$/g, "").trim();
|
|
25788
|
+
if (unquoted.length === 0 || unquoted.length > 300 || /\s/.test(unquoted)) return false;
|
|
25789
|
+
return /[\\/]/.test(unquoted) && /\.[A-Za-z0-9]{1,8}$/.test(unquoted) && !/:(\d+)/.test(unquoted);
|
|
25629
25790
|
}
|
|
25630
|
-
/**
|
|
25791
|
+
/**
|
|
25792
|
+
* 判断文本是否为混合证据(如包含已投影的 `[cache-warmup preview]`、内嵌大型 JSON 对象行、
|
|
25793
|
+
* 多行 pretty-printed JSON 块、`util.inspect` 字符串拼接对象、分节头等)。
|
|
25794
|
+
*/
|
|
25795
|
+
function isMixedEvidenceOutput(text) {
|
|
25796
|
+
if (text.includes("[cache-warmup preview]")) return true;
|
|
25797
|
+
if (/^(?:===|---)\s+.+?(?:\s+(?:===|---))?$/m.test(text) || /^###\s+\S/m.test(text) || /^--\s+.+\s+--$/m.test(text)) return true;
|
|
25798
|
+
const lines = text.split("\n");
|
|
25799
|
+
let inspectStringConcat = 0;
|
|
25800
|
+
let jsonPropertyLines = 0;
|
|
25801
|
+
for (const raw of lines.slice(0, 200)) {
|
|
25802
|
+
const trimmed = raw.trim();
|
|
25803
|
+
if (trimmed === "") continue;
|
|
25804
|
+
if (trimmed.length > 80 && (trimmed.startsWith("{\"") && (trimmed.endsWith("}") || trimmed.includes("\":")) || trimmed.startsWith("[{\"") && trimmed.includes("\":"))) return true;
|
|
25805
|
+
if (/^"[A-Za-z0-9_$-]+"\s*:/.test(trimmed)) {
|
|
25806
|
+
jsonPropertyLines += 1;
|
|
25807
|
+
if (jsonPropertyLines >= 2) return true;
|
|
25808
|
+
}
|
|
25809
|
+
if (/^['"].*\\n['"]\s*\+$/.test(trimmed)) {
|
|
25810
|
+
inspectStringConcat += 1;
|
|
25811
|
+
if (inspectStringConcat >= 2) return true;
|
|
25812
|
+
}
|
|
25813
|
+
}
|
|
25814
|
+
return false;
|
|
25815
|
+
}
|
|
25816
|
+
/**
|
|
25817
|
+
* shell 输出判定:
|
|
25818
|
+
* - 排除混合证据输出(内嵌 JSON 行、`[cache-warmup preview]`、分节头、`util.inspect` 对象);
|
|
25819
|
+
* - 有明确的退出码标记(`[exit code: N]`、`exit code: N`、`EXIT=N`);
|
|
25820
|
+
* - 有 Python `Traceback (most recent call last):` 或 JS 异常头 + 堆栈行(`^\s+at\s+`);
|
|
25821
|
+
* - 若没有任何退出码或堆栈,只有纯文本行时,要求不存在 grep 行,且错误行不是单纯的文件名。
|
|
25822
|
+
*/
|
|
25631
25823
|
function looksLikeShellOutput(text) {
|
|
25824
|
+
if (isMixedEvidenceOutput(text)) return false;
|
|
25632
25825
|
if (/(?:exit code|exited with code|process exited with code)[:=\s]+-?\d+/i.test(text)) return true;
|
|
25633
25826
|
if (/\[exit code: -?\d+\]/i.test(text)) return true;
|
|
25827
|
+
if (/^EXIT=-?\d+$/m.test(text)) return true;
|
|
25634
25828
|
if (/^Traceback \(most recent call last\):/m.test(text)) return true;
|
|
25829
|
+
if (/^(?:[A-Za-z]*Error|Exception): /m.test(text) && /^\s+at\s+\S/m.test(text)) return true;
|
|
25635
25830
|
const lines = text.split("\n");
|
|
25831
|
+
let grepLikes = 0;
|
|
25636
25832
|
let errors = 0;
|
|
25637
25833
|
for (const line of lines) {
|
|
25638
|
-
|
|
25639
|
-
if (
|
|
25834
|
+
const trimmed = line.trim();
|
|
25835
|
+
if (trimmed === "") continue;
|
|
25836
|
+
if (/^(?:PASS\b|[✓✔]|ok\s+\d+\b)/.test(trimmed)) continue;
|
|
25837
|
+
if (/^stdout\s+\|\s+/.test(trimmed)) continue;
|
|
25838
|
+
if (/^\(node:\d+\)\s+\[\w+\]\s+Warning:/.test(trimmed)) continue;
|
|
25839
|
+
if (isBareFilePathLine(trimmed)) continue;
|
|
25840
|
+
if (GREP_LINE.test(trimmed) && !TS_DIAGNOSTIC_HEADER.test(trimmed)) {
|
|
25841
|
+
grepLikes += 1;
|
|
25842
|
+
if (grepLikes >= 2) return false;
|
|
25843
|
+
}
|
|
25844
|
+
if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(trimmed)) continue;
|
|
25640
25845
|
if (SHELL_ERROR_LINE.test(line)) errors += 1;
|
|
25641
|
-
if (errors >= 1) return true;
|
|
25642
25846
|
}
|
|
25643
|
-
return
|
|
25847
|
+
return errors >= 1;
|
|
25644
25848
|
}
|
|
25645
|
-
/** shell 预览:退出码 +
|
|
25849
|
+
/** shell 预览:退出码 + 完整错误集(必需,含 TS 多行诊断上下文),首尾片段(可选)。 */
|
|
25646
25850
|
function buildShellSections(text) {
|
|
25647
25851
|
const lines = text.split("\n");
|
|
25648
25852
|
const errors = [];
|
|
25649
|
-
|
|
25650
|
-
|
|
25853
|
+
let errorCount = 0;
|
|
25854
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
25855
|
+
const trimmed = lines[index].trim();
|
|
25651
25856
|
if (trimmed === "") continue;
|
|
25652
25857
|
if (/^\[exit code: -?\d+\]/i.test(trimmed)) continue;
|
|
25653
25858
|
if (/^\s*(?:0|no)\s+(?:errors?|failures?|failed)\b/i.test(trimmed)) continue;
|
|
25654
|
-
if (
|
|
25859
|
+
if (/^(?:PASS\b|[✓✔]|ok\s+\d+\b)/.test(trimmed)) continue;
|
|
25860
|
+
if (/^stdout\s+\|\s+/.test(trimmed)) continue;
|
|
25861
|
+
if (/^\(node:\d+\)\s+\[\w+\]\s+Warning:/.test(trimmed)) continue;
|
|
25862
|
+
if (isBareFilePathLine(trimmed)) continue;
|
|
25863
|
+
if (TS_DIAGNOSTIC_HEADER.test(trimmed)) {
|
|
25864
|
+
errorCount += 1;
|
|
25865
|
+
errors.push(` ${clip(trimmed, 240)}`);
|
|
25866
|
+
let details = 0;
|
|
25867
|
+
while (index + 1 < lines.length && details < 6) {
|
|
25868
|
+
const nextRaw = lines[index + 1];
|
|
25869
|
+
const nextTrimmed = nextRaw.trim();
|
|
25870
|
+
if (nextTrimmed === "" || TS_DIAGNOSTIC_HEADER.test(nextTrimmed) || /^\[exit code: -?\d+\]/i.test(nextTrimmed)) break;
|
|
25871
|
+
if (!isTsDiagnosticDetailLine(nextRaw)) break;
|
|
25872
|
+
errors.push(` ${clip(nextTrimmed, 240)}`);
|
|
25873
|
+
index += 1;
|
|
25874
|
+
details += 1;
|
|
25875
|
+
}
|
|
25876
|
+
continue;
|
|
25877
|
+
}
|
|
25878
|
+
if (SHELL_ERROR_LINE.test(trimmed)) {
|
|
25879
|
+
errorCount += 1;
|
|
25880
|
+
errors.push(` ${clip(trimmed, 360)}`);
|
|
25881
|
+
}
|
|
25655
25882
|
}
|
|
25656
|
-
if (errors.length >
|
|
25883
|
+
if (errorCount > 300 || errors.length > 600) return void 0;
|
|
25657
25884
|
const exitMatch = /(?:exit code|exited with code|process exited with code)[:=\s]+(-?\d+)/i.exec(text) ?? /\[exit code: (-?\d+)\]/i.exec(text);
|
|
25658
25885
|
const sections = [];
|
|
25659
25886
|
if (exitMatch !== null) sections.push({ lines: [`exit code: ${exitMatch[1]}`] });
|
|
25660
|
-
sections.push({ lines: [`errors: ${
|
|
25887
|
+
sections.push({ lines: [`errors: ${errorCount} (complete set)`] });
|
|
25661
25888
|
if (errors.length > 0) sections.push({ lines: errors });
|
|
25662
25889
|
const nonEmpty = lines.map((line) => line.trimEnd()).filter((line) => line.trim() !== "");
|
|
25663
25890
|
const head = nonEmpty.slice(0, 5).map((line) => ` ${clip(line.trim(), 160)}`);
|
|
@@ -25672,8 +25899,8 @@ function buildShellSections(text) {
|
|
|
25672
25899
|
});
|
|
25673
25900
|
return sections;
|
|
25674
25901
|
}
|
|
25675
|
-
/** 一行 grep 结果:`<path>:<line>[:<column>]:<text>`。 */
|
|
25676
|
-
const GREP_LINE = /^(.+?):(\d+)
|
|
25902
|
+
/** 一行 grep 结果:`<path>:<line>[:<column>]:<text>` 或 `<path>:<line>(?:\t|\||\s{2,})<text>`。 */
|
|
25903
|
+
const GREP_LINE = /^(.+?):(\d+)(?::(?:\d+:)?|\t|\|\s?|\s{2,})(.*)$/;
|
|
25677
25904
|
/** 抽取 grep 证据;命中线占比不到 80% 就当它不是 grep 输出。 */
|
|
25678
25905
|
function extractGrepEvidence(text) {
|
|
25679
25906
|
const lines = text.split("\n").filter((line) => line.trim() !== "");
|
|
@@ -25742,36 +25969,58 @@ function buildStructuredSections(text) {
|
|
|
25742
25969
|
optional: true
|
|
25743
25970
|
}];
|
|
25744
25971
|
}
|
|
25972
|
+
/** 剥离 `read` 工具或 `grep -C` 常见的行号前缀(如 `1: `、`1- `、`1\t`、`1| `、` 12→`)。 */
|
|
25973
|
+
const LINE_NUMBER_PREFIX = /^\s*\d+(?::(?:\s|$)|-\s{2,}|\t|\|(?:\s|$)|→\s?)/;
|
|
25974
|
+
function stripLineNumberPrefix(line) {
|
|
25975
|
+
return line.replace(LINE_NUMBER_PREFIX, "");
|
|
25976
|
+
}
|
|
25745
25977
|
/** 契约/类型定义:命中两行以上声明就当它是契约,保持原样。 */
|
|
25746
25978
|
function looksLikeContract(text) {
|
|
25747
25979
|
let declarations = 0;
|
|
25748
|
-
for (const
|
|
25980
|
+
for (const raw of text.split("\n").slice(0, 800)) {
|
|
25981
|
+
const line = stripLineNumberPrefix(raw);
|
|
25749
25982
|
if (/^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type)\s+[A-Za-z_$][\w$]*/.test(line)) declarations += 1;
|
|
25750
25983
|
if (declarations >= 2) return true;
|
|
25751
25984
|
}
|
|
25752
25985
|
return false;
|
|
25753
25986
|
}
|
|
25754
|
-
/**
|
|
25987
|
+
/**
|
|
25988
|
+
* 源码或混合证据判定:
|
|
25989
|
+
* - 含多处 markdown 代码围栏;
|
|
25990
|
+
* - 含 `git diff` 补丁块(`diff --git` / `@@ -x,y +x,y @@`);
|
|
25991
|
+
* - 含至少 5 行带行号的源码切片(`1: ...` 或 `1\t...`),说明是单文件或多文件混合读取输出;
|
|
25992
|
+
* - 代码行占比 >= 45%。
|
|
25993
|
+
*/
|
|
25755
25994
|
function looksLikeSourceCode(text) {
|
|
25995
|
+
if (/^diff --git\s+/m.test(text) || /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/m.test(text)) return true;
|
|
25756
25996
|
let fences = 0;
|
|
25757
|
-
for (const line of text.split("\n").slice(0, 600)) if (line.trim().startsWith("```")) fences += 1;
|
|
25997
|
+
for (const line of text.split("\n").slice(0, 600)) if (stripLineNumberPrefix(line).trim().startsWith("```")) fences += 1;
|
|
25758
25998
|
if (fences >= 4) return true;
|
|
25759
25999
|
let code = 0;
|
|
26000
|
+
let numbered = 0;
|
|
25760
26001
|
let nonEmpty = 0;
|
|
25761
26002
|
for (const raw of text.split("\n").slice(0, 600)) {
|
|
25762
|
-
|
|
26003
|
+
if (raw.trim() === "") continue;
|
|
26004
|
+
if (LINE_NUMBER_PREFIX.test(raw)) numbered += 1;
|
|
26005
|
+
const trimmed = stripLineNumberPrefix(raw).trim();
|
|
25763
26006
|
if (trimmed === "") continue;
|
|
25764
26007
|
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;
|
|
26008
|
+
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
26009
|
}
|
|
26010
|
+
if (numbered >= 5) return true;
|
|
25767
26011
|
return nonEmpty >= 10 && code / nonEmpty >= .45;
|
|
25768
26012
|
}
|
|
25769
|
-
/** 完整 JSON
|
|
26013
|
+
/** 完整 JSON 文档(对象、数组、JSON 字符串)或被截断的 JSON 对象:不按裸日志猜测。 */
|
|
25770
26014
|
function isJsonDocument(text) {
|
|
25771
26015
|
const trimmed = text.trim();
|
|
25772
26016
|
if (trimmed.length < 2) return false;
|
|
25773
26017
|
const first = trimmed[0];
|
|
25774
26018
|
const last = trimmed[trimmed.length - 1];
|
|
26019
|
+
if (first === "\"" && last === "\"") try {
|
|
26020
|
+
return typeof JSON.parse(trimmed) === "string";
|
|
26021
|
+
} catch {
|
|
26022
|
+
return false;
|
|
26023
|
+
}
|
|
25775
26024
|
if (first !== "{" && first !== "[") return false;
|
|
25776
26025
|
if (first === "{" && last === "}" || first === "[" && last === "]") try {
|
|
25777
26026
|
const value = JSON.parse(trimmed);
|
|
@@ -25784,36 +26033,117 @@ function isJsonDocument(text) {
|
|
|
25784
26033
|
}
|
|
25785
26034
|
return false;
|
|
25786
26035
|
}
|
|
26036
|
+
/** PTC `run_code` JSON envelope 中允许独立投影日志的字段白名单。 */
|
|
26037
|
+
const JSON_ENVELOPE_LOG_KEYS = [
|
|
26038
|
+
"stdout",
|
|
26039
|
+
"stderr",
|
|
26040
|
+
"output",
|
|
26041
|
+
"logs",
|
|
26042
|
+
"log"
|
|
26043
|
+
];
|
|
26044
|
+
/** JSON envelope 其余字段允许占用的最大序列化字符数(防止把含批量源码的混合 envelope 压掉)。 */
|
|
26045
|
+
const JSON_ENVELOPE_MAX_OTHER_CHARS = 800;
|
|
25787
26046
|
/**
|
|
25788
|
-
*
|
|
26047
|
+
* 解开一层或两层 JSON 包装(支持顶层 JSON 字符串 `"..."` 或双重序列化的 `"{...}"`)。
|
|
26048
|
+
*/
|
|
26049
|
+
function parseJsonCandidate(text) {
|
|
26050
|
+
const trimmed = text.trim();
|
|
26051
|
+
if (trimmed.length < 2) return void 0;
|
|
26052
|
+
const first = trimmed[0];
|
|
26053
|
+
const last = trimmed[trimmed.length - 1];
|
|
26054
|
+
if (first === "\"" && last === "\"") try {
|
|
26055
|
+
const inner = JSON.parse(trimmed);
|
|
26056
|
+
if (typeof inner !== "string") return void 0;
|
|
26057
|
+
const innerTrimmed = inner.trim();
|
|
26058
|
+
if (innerTrimmed.startsWith("{") && innerTrimmed.endsWith("}")) try {
|
|
26059
|
+
const innerObj = JSON.parse(innerTrimmed);
|
|
26060
|
+
if (innerObj !== null && typeof innerObj === "object" && !Array.isArray(innerObj)) return {
|
|
26061
|
+
kind: "object",
|
|
26062
|
+
record: innerObj,
|
|
26063
|
+
wrappedInString: true
|
|
26064
|
+
};
|
|
26065
|
+
} catch {}
|
|
26066
|
+
return {
|
|
26067
|
+
kind: "string",
|
|
26068
|
+
decoded: inner
|
|
26069
|
+
};
|
|
26070
|
+
} catch {
|
|
26071
|
+
return;
|
|
26072
|
+
}
|
|
26073
|
+
if (first === "{" && last === "}") try {
|
|
26074
|
+
const parsed = JSON.parse(trimmed);
|
|
26075
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return {
|
|
26076
|
+
kind: "object",
|
|
26077
|
+
record: parsed,
|
|
26078
|
+
wrappedInString: false
|
|
26079
|
+
};
|
|
26080
|
+
} catch {
|
|
26081
|
+
return;
|
|
26082
|
+
}
|
|
26083
|
+
}
|
|
26084
|
+
/**
|
|
26085
|
+
* 尝试从完整 JSON(顶层 JSON 字符串或对象 envelope)中解出可结构化投影的日志正文。
|
|
25789
26086
|
*
|
|
25790
|
-
*
|
|
25791
|
-
*
|
|
25792
|
-
*
|
|
26087
|
+
* 规则刻意保守:
|
|
26088
|
+
* - 截断 JSON、数组一律返回 undefined;
|
|
26089
|
+
* - 对象 envelope 除白名单字段外,其余字段序列化后的总长度不超过 `JSON_ENVELOPE_MAX_OTHER_CHARS`;
|
|
26090
|
+
* - 解码并剥离 ANSI 后的日志文本自身必须能通过非源码/非契约的结构化日志投影计划;
|
|
26091
|
+
* - 返回的 `storeContent` 把解码后的多行日志直接展开(对象 envelope 还会把其余元数据缩进在头部),
|
|
26092
|
+
* 彻底避免单行 minified JSON 因没有真实 `\n` 导致 `store.page` 的按行分页失效。
|
|
25793
26093
|
*/
|
|
25794
|
-
function
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
26094
|
+
function extractJsonEnvelopeLogField(text, toolName) {
|
|
26095
|
+
const candidate = parseJsonCandidate(text);
|
|
26096
|
+
if (candidate === void 0) return void 0;
|
|
26097
|
+
if (candidate.kind === "string") {
|
|
26098
|
+
const clean = stripAnsi(candidate.decoded);
|
|
26099
|
+
if (isJsonDocument(clean) || isMixedEvidenceOutput(clean)) return void 0;
|
|
26100
|
+
const plan = planPlainTextProjection(clean, toolName);
|
|
26101
|
+
if (plan.kind !== "preview") return void 0;
|
|
26102
|
+
return { plan };
|
|
26103
|
+
}
|
|
26104
|
+
const { record } = candidate;
|
|
26105
|
+
let candidateKey;
|
|
26106
|
+
let candidateText = "";
|
|
26107
|
+
for (const key of JSON_ENVELOPE_LOG_KEYS) {
|
|
26108
|
+
const val = record[key];
|
|
26109
|
+
if (typeof val === "string" && val.length > candidateText.length) {
|
|
26110
|
+
candidateKey = key;
|
|
26111
|
+
candidateText = val;
|
|
26112
|
+
}
|
|
26113
|
+
}
|
|
26114
|
+
if (candidateKey === void 0 || candidateText.length === 0) return void 0;
|
|
26115
|
+
const rest = {};
|
|
26116
|
+
for (const [key, value] of Object.entries(record)) {
|
|
26117
|
+
if (key === candidateKey) continue;
|
|
26118
|
+
if (typeof value === "string" && (value.length > 400 || looksLikeSourceCode(value) || looksLikeContract(value))) return;
|
|
26119
|
+
rest[key] = value;
|
|
26120
|
+
}
|
|
26121
|
+
if (JSON.stringify(rest).length > JSON_ENVELOPE_MAX_OTHER_CHARS) return void 0;
|
|
26122
|
+
const cleanField = stripAnsi(candidateText);
|
|
26123
|
+
if (isJsonDocument(cleanField) || isMixedEvidenceOutput(cleanField)) return void 0;
|
|
26124
|
+
const plan = planPlainTextProjection(cleanField, toolName);
|
|
26125
|
+
if (plan.kind !== "preview") return void 0;
|
|
26126
|
+
return {
|
|
26127
|
+
envelope: {
|
|
26128
|
+
record,
|
|
26129
|
+
key: candidateKey
|
|
26130
|
+
},
|
|
26131
|
+
plan
|
|
25802
26132
|
};
|
|
26133
|
+
}
|
|
26134
|
+
/** 对一段纯文本(非 JSON 文档)做投影计划判定。 */
|
|
26135
|
+
function planPlainTextProjection(rawText, toolName) {
|
|
26136
|
+
const text = stripAnsi(rawText);
|
|
25803
26137
|
if (looksLikeContract(text)) return {
|
|
25804
26138
|
kind: "verbatim",
|
|
25805
26139
|
strategy: "verbatim-contract"
|
|
25806
26140
|
};
|
|
25807
|
-
if (isJsonDocument(text)) return {
|
|
25808
|
-
kind: "verbatim",
|
|
25809
|
-
strategy: "verbatim-unparsed"
|
|
25810
|
-
};
|
|
25811
26141
|
if (looksLikeSourceCode(text)) return {
|
|
25812
26142
|
kind: "verbatim",
|
|
25813
26143
|
strategy: "verbatim-source-code"
|
|
25814
26144
|
};
|
|
25815
26145
|
const test = extractTestEvidence(text);
|
|
25816
|
-
if (test !== void 0) return {
|
|
26146
|
+
if (test !== void 0 && test.failures.length > 0) return {
|
|
25817
26147
|
kind: "preview",
|
|
25818
26148
|
strategy: "test-failure",
|
|
25819
26149
|
sections: buildTestSections(test)
|
|
@@ -25852,6 +26182,42 @@ function planProjection(text, toolName) {
|
|
|
25852
26182
|
strategy: "verbatim-unparsed"
|
|
25853
26183
|
};
|
|
25854
26184
|
}
|
|
26185
|
+
/**
|
|
26186
|
+
* 决定这一段文本该怎么处理。
|
|
26187
|
+
*
|
|
26188
|
+
* 顺序就是「可证明性」的顺序:子代理报告/源码工具/契约先被判成原样,
|
|
26189
|
+
* 然后是 JSON envelope(只允许纯日志字段投影,混合证据一律原样),
|
|
26190
|
+
* 接着是测试失败集、grep、结构化、glob、shell 这些有明确形状的,
|
|
26191
|
+
* 最后落到「无法解析 → 原样」。
|
|
26192
|
+
*/
|
|
26193
|
+
function planProjection(text, toolName) {
|
|
26194
|
+
if (TOOL_SUBAGENT.test(toolName)) return {
|
|
26195
|
+
kind: "verbatim",
|
|
26196
|
+
strategy: "verbatim-subagent-report"
|
|
26197
|
+
};
|
|
26198
|
+
if (TOOL_SOURCE.test(toolName)) return {
|
|
26199
|
+
kind: "verbatim",
|
|
26200
|
+
strategy: "verbatim-source-code"
|
|
26201
|
+
};
|
|
26202
|
+
if (looksLikeContract(text)) return {
|
|
26203
|
+
kind: "verbatim",
|
|
26204
|
+
strategy: "verbatim-contract"
|
|
26205
|
+
};
|
|
26206
|
+
if (isJsonDocument(text)) {
|
|
26207
|
+
const extracted = extractJsonEnvelopeLogField(text, toolName);
|
|
26208
|
+
if (extracted !== void 0) return {
|
|
26209
|
+
kind: "preview",
|
|
26210
|
+
strategy: extracted.plan.strategy,
|
|
26211
|
+
sections: extracted.plan.sections,
|
|
26212
|
+
...extracted.envelope === void 0 ? {} : { envelope: extracted.envelope }
|
|
26213
|
+
};
|
|
26214
|
+
return {
|
|
26215
|
+
kind: "verbatim",
|
|
26216
|
+
strategy: "verbatim-unparsed"
|
|
26217
|
+
};
|
|
26218
|
+
}
|
|
26219
|
+
return planPlainTextProjection(text, toolName);
|
|
26220
|
+
}
|
|
25855
26221
|
/** 预览尾部:把「怎么读全文」写死在预览里,模型不用猜。 */
|
|
25856
26222
|
function footerFor(refId) {
|
|
25857
26223
|
return `[full result: ${refId} · page with offset/limit(default 64 lines)/maxBytes(default 16384 bytes)]`;
|
|
@@ -25870,12 +26236,22 @@ async function decide(args) {
|
|
|
25870
26236
|
if (input.isError === true && plan.strategy !== "test-failure" && plan.strategy !== "shell-output") return { reason: "verbatim-unparsed" };
|
|
25871
26237
|
const header = `[cache-warmup preview] strategy=${plan.strategy} input=${text.length}ch`;
|
|
25872
26238
|
const footerBudget = footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`).length;
|
|
25873
|
-
const
|
|
26239
|
+
const jsonEscaped = plan.envelope !== void 0;
|
|
26240
|
+
const envelopeShellLength = plan.envelope === void 0 ? 0 : JSON.stringify({
|
|
26241
|
+
...plan.envelope.record,
|
|
26242
|
+
[plan.envelope.key]: ""
|
|
26243
|
+
}).length;
|
|
26244
|
+
const bodyBudget = budget.maxChars - header.length - footerBudget - envelopeShellLength - (jsonEscaped ? 4 : 2);
|
|
25874
26245
|
if (bodyBudget <= 0) return { reason: "verbatim-budget" };
|
|
25875
|
-
const body = fitSections(plan.sections, bodyBudget);
|
|
26246
|
+
const body = fitSections(plan.sections, bodyBudget, jsonEscaped);
|
|
25876
26247
|
if (body === void 0) return { reason: "verbatim-budget" };
|
|
25877
26248
|
const store = input.store;
|
|
25878
26249
|
if (store === void 0) return { reason: "store-unavailable" };
|
|
26250
|
+
const placeholderInner = `${header}\n${body}\n${footerFor(`ref://${input.sessionId}/${"0".repeat(64)}`)}`;
|
|
26251
|
+
if ((plan.envelope === void 0 ? placeholderInner : JSON.stringify({
|
|
26252
|
+
...plan.envelope.record,
|
|
26253
|
+
[plan.envelope.key]: placeholderInner
|
|
26254
|
+
})).length > budget.maxChars) return { reason: "verbatim-budget" };
|
|
25879
26255
|
let reference;
|
|
25880
26256
|
try {
|
|
25881
26257
|
reference = await store.put({
|
|
@@ -25887,7 +26263,11 @@ async function decide(args) {
|
|
|
25887
26263
|
} catch {
|
|
25888
26264
|
return { reason: "store-unavailable" };
|
|
25889
26265
|
}
|
|
25890
|
-
const
|
|
26266
|
+
const innerPreview = `${header}\n${body}\n${footerFor(reference.refId)}`;
|
|
26267
|
+
const preview = plan.envelope === void 0 ? innerPreview : JSON.stringify({
|
|
26268
|
+
...plan.envelope.record,
|
|
26269
|
+
[plan.envelope.key]: innerPreview
|
|
26270
|
+
});
|
|
25891
26271
|
if (preview.length > budget.maxChars) return { reason: "verbatim-budget" };
|
|
25892
26272
|
return {
|
|
25893
26273
|
preview,
|
|
@@ -30195,7 +30575,7 @@ function messageToolsOf(message) {
|
|
|
30195
30575
|
* lives in the request's declaration list, which is why the two are resolved
|
|
30196
30576
|
* together rather than the block carrying a schema of its own.
|
|
30197
30577
|
*/
|
|
30198
|
-
function toolAdditionNames(message) {
|
|
30578
|
+
function toolAdditionNames$1(message) {
|
|
30199
30579
|
const names = [];
|
|
30200
30580
|
for (const block of message.content) {
|
|
30201
30581
|
if (block.type !== "tool-addition") continue;
|
|
@@ -30212,7 +30592,7 @@ function toolAdditionNames(message) {
|
|
|
30212
30592
|
* `addition-only` never sees one, because the harness removes the declaration
|
|
30213
30593
|
* from the list instead.
|
|
30214
30594
|
*/
|
|
30215
|
-
function toolRemovalNames(message) {
|
|
30595
|
+
function toolRemovalNames$1(message) {
|
|
30216
30596
|
const names = [];
|
|
30217
30597
|
for (const block of message.content) {
|
|
30218
30598
|
if (block.type !== "tool-removal") continue;
|
|
@@ -30229,7 +30609,7 @@ function toolRemovalNames(message) {
|
|
|
30229
30609
|
* list (that is what keeps the prefix intact), so the only thing standing
|
|
30230
30610
|
* between the model and a call that would fail is this sentence.
|
|
30231
30611
|
*/
|
|
30232
|
-
function removalNotice(names) {
|
|
30612
|
+
function removalNotice$1(names) {
|
|
30233
30613
|
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
30234
30614
|
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
30615
|
}
|
|
@@ -30373,7 +30753,7 @@ function declarationSlots(options, media) {
|
|
|
30373
30753
|
let beforeIndex = 0;
|
|
30374
30754
|
for (const message of options.messages) {
|
|
30375
30755
|
if (message.role === "developer") {
|
|
30376
|
-
const additions = declarationsFor(toolAdditionNames(message), options.tools);
|
|
30756
|
+
const additions = declarationsFor(toolAdditionNames$1(message), options.tools);
|
|
30377
30757
|
const entries = [];
|
|
30378
30758
|
if (additions.length > 0) entries.push(media.messageTools === true ? {
|
|
30379
30759
|
role: "system",
|
|
@@ -30382,10 +30762,10 @@ function declarationSlots(options, media) {
|
|
|
30382
30762
|
role: "system",
|
|
30383
30763
|
content: declarationNotice(options.model, additions.length, "capability")
|
|
30384
30764
|
});
|
|
30385
|
-
const removals = toolRemovalNames(message);
|
|
30765
|
+
const removals = toolRemovalNames$1(message);
|
|
30386
30766
|
if (removals.length > 0) entries.push({
|
|
30387
30767
|
role: "system",
|
|
30388
|
-
content: removalNotice(removals)
|
|
30768
|
+
content: removalNotice$1(removals)
|
|
30389
30769
|
});
|
|
30390
30770
|
if (entries.length > 0) slots.push({
|
|
30391
30771
|
beforeIndex,
|
|
@@ -31622,6 +32002,27 @@ var SessionCacheStateStore = class {
|
|
|
31622
32002
|
this.schedule();
|
|
31623
32003
|
}
|
|
31624
32004
|
/**
|
|
32005
|
+
* Update the warmup consecutive-hit streak on an already-written session record
|
|
32006
|
+
* in place, without issuing a second `write()` call.
|
|
32007
|
+
*/
|
|
32008
|
+
noteWarmupStreak(sessionId, consecutiveHits) {
|
|
32009
|
+
const key = sessionId.trim();
|
|
32010
|
+
if (key === "") return;
|
|
32011
|
+
const document = this.load();
|
|
32012
|
+
const existing = document.sessions[key];
|
|
32013
|
+
if (existing === void 0) return;
|
|
32014
|
+
const normalized = count$1(consecutiveHits);
|
|
32015
|
+
if (normalized > 0) document.sessions[key] = {
|
|
32016
|
+
...existing,
|
|
32017
|
+
warmupConsecutiveHits: normalized
|
|
32018
|
+
};
|
|
32019
|
+
else if (existing.warmupConsecutiveHits !== void 0) {
|
|
32020
|
+
const { warmupConsecutiveHits: _dropped, ...rest } = existing;
|
|
32021
|
+
document.sessions[key] = rest;
|
|
32022
|
+
} else return;
|
|
32023
|
+
this.schedule();
|
|
32024
|
+
}
|
|
32025
|
+
/**
|
|
31625
32026
|
* Write the document now, synchronously.
|
|
31626
32027
|
*
|
|
31627
32028
|
* Exposed for tests and for a caller that is about to lose the process; the
|
|
@@ -31698,6 +32099,7 @@ function normalizeRecord(value) {
|
|
|
31698
32099
|
const updatedAt = finite(value.updatedAt);
|
|
31699
32100
|
if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
|
|
31700
32101
|
const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
|
|
32102
|
+
const warmupStreak = count$1(value.warmupConsecutiveHits);
|
|
31701
32103
|
return {
|
|
31702
32104
|
lastActiveAt,
|
|
31703
32105
|
updatedAt,
|
|
@@ -31709,7 +32111,8 @@ function normalizeRecord(value) {
|
|
|
31709
32111
|
cacheWriteTokens: count$1(value.cacheWriteTokens),
|
|
31710
32112
|
cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
|
|
31711
32113
|
cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
|
|
31712
|
-
...drift === void 0 ? {} : { lastDrift: drift }
|
|
32114
|
+
...drift === void 0 ? {} : { lastDrift: drift },
|
|
32115
|
+
...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {}
|
|
31713
32116
|
};
|
|
31714
32117
|
}
|
|
31715
32118
|
/**
|
|
@@ -32809,7 +33212,7 @@ const KIMI_CODE_REQUEST_IMAGE_MAX_EDGE = 2e3;
|
|
|
32809
33212
|
* @param catalog - live catalog, possibly empty when offline.
|
|
32810
33213
|
* @returns the tool-update mode, or undefined when this route must resend everything.
|
|
32811
33214
|
*/
|
|
32812
|
-
function toolUpdateMode(modelId, catalog) {
|
|
33215
|
+
function toolUpdateMode$1(modelId, catalog) {
|
|
32813
33216
|
if (!dynamicToolsForEntry(modelId, catalog)) return void 0;
|
|
32814
33217
|
if (wireForCatalogEntry(modelId, catalog) !== "openai") return void 0;
|
|
32815
33218
|
const raw = (process.env.DSH_KIMI_CODE_TOOL_UPDATE ?? "").trim().toLowerCase();
|
|
@@ -32860,7 +33263,7 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
32860
33263
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
32861
33264
|
updatedAt: now
|
|
32862
33265
|
});
|
|
32863
|
-
recordCacheWarmupUsage({
|
|
33266
|
+
const warmupState = recordCacheWarmupUsage({
|
|
32864
33267
|
sessionId,
|
|
32865
33268
|
inputTokens: state.inputTokens,
|
|
32866
33269
|
cacheReadTokens: state.cacheReadTokens,
|
|
@@ -32869,6 +33272,7 @@ function noteSessionCacheState(sessionId, state) {
|
|
|
32869
33272
|
at: now,
|
|
32870
33273
|
...lastDrift === void 0 ? {} : { driftCause: lastDrift }
|
|
32871
33274
|
});
|
|
33275
|
+
sessionCacheState.noteWarmupStreak(sessionId, warmupState.consecutiveHits);
|
|
32872
33276
|
}
|
|
32873
33277
|
var KimiCodeAdapter = class extends LlmAdapter {
|
|
32874
33278
|
store;
|
|
@@ -33052,7 +33456,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
|
|
|
33052
33456
|
id: modelId,
|
|
33053
33457
|
name: entry?.name ?? modelId,
|
|
33054
33458
|
inputModalities: inputModalitiesForEntry(modelId, catalog),
|
|
33055
|
-
...toolUpdateMode(modelId, catalog) === void 0 ? {} : { toolUpdate: toolUpdateMode(modelId, catalog) },
|
|
33459
|
+
...toolUpdateMode$1(modelId, catalog) === void 0 ? {} : { toolUpdate: toolUpdateMode$1(modelId, catalog) },
|
|
33056
33460
|
context: { contextWindow: this.contextWindowFor(modelId, entry, settings.contextWindowOverrides) },
|
|
33057
33461
|
...efforts.length === 0 ? {} : { reasoning: {
|
|
33058
33462
|
efforts: efforts.map((effort) => ({
|
|
@@ -33849,13 +34253,32 @@ async function getKimiCodeWebStatus(store, modelSettings, preferences, options =
|
|
|
33849
34253
|
* answers with the process aggregate, which is the right answer for a settings
|
|
33850
34254
|
* card that is not showing a specific conversation.
|
|
33851
34255
|
*/
|
|
34256
|
+
function warmupStatusOf(sessionId) {
|
|
34257
|
+
const targetSession = sessionId?.trim() || cacheWarmupUsageSessions()[0]?.sessionId;
|
|
34258
|
+
const durable = sessionCacheState.read(targetSession);
|
|
34259
|
+
const hint = durable === void 0 ? void 0 : {
|
|
34260
|
+
lastActiveAt: durable.lastActiveAt,
|
|
34261
|
+
consecutiveHits: durable.warmupConsecutiveHits ?? 0,
|
|
34262
|
+
hitRatio: durable.cachedTokens + durable.freshTokens > 0 ? durable.cachedTokens / (durable.cachedTokens + durable.freshTokens) : 0
|
|
34263
|
+
};
|
|
34264
|
+
if (targetSession === void 0 && hint === void 0) return void 0;
|
|
34265
|
+
const budget = cacheWarmupBudget(targetSession ?? "durable-latest", Date.now(), hint);
|
|
34266
|
+
return {
|
|
34267
|
+
warmup: budget.warmup,
|
|
34268
|
+
maxChars: budget.maxChars,
|
|
34269
|
+
consecutiveHits: budget.consecutiveHits,
|
|
34270
|
+
requiredHits: CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired
|
|
34271
|
+
};
|
|
34272
|
+
}
|
|
33852
34273
|
function cacheStatsOrNull(sessionId) {
|
|
33853
34274
|
const stats = getCacheStats(sessionId);
|
|
33854
34275
|
if (stats.requests === 0) return durableCacheStats(sessionId);
|
|
33855
34276
|
const lastDrift = getLastDriftCause(sessionId);
|
|
34277
|
+
const warmup = warmupStatusOf(sessionId);
|
|
33856
34278
|
return {
|
|
33857
34279
|
...stats,
|
|
33858
34280
|
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34281
|
+
...warmup === void 0 ? {} : { warmup },
|
|
33859
34282
|
cacheWriteTokens: stats.cacheWriteTokens,
|
|
33860
34283
|
cacheWrite5mTokens: stats.cacheWrite5mTokens,
|
|
33861
34284
|
cacheWrite1hTokens: stats.cacheWrite1hTokens
|
|
@@ -33897,6 +34320,7 @@ function durableCacheStats(sessionId) {
|
|
|
33897
34320
|
const durable = sessionCacheState.read(sessionId);
|
|
33898
34321
|
if (durable === void 0 || durable.requests === 0) return null;
|
|
33899
34322
|
const lastDrift = driftCauseOf(durable.lastDrift);
|
|
34323
|
+
const warmup = warmupStatusOf(sessionId);
|
|
33900
34324
|
return {
|
|
33901
34325
|
requests: durable.requests,
|
|
33902
34326
|
cachedTokens: durable.cachedTokens,
|
|
@@ -33906,7 +34330,8 @@ function durableCacheStats(sessionId) {
|
|
|
33906
34330
|
cacheWriteTokens: durable.cacheWriteTokens,
|
|
33907
34331
|
cacheWrite5mTokens: durable.cacheWrite5mTokens,
|
|
33908
34332
|
cacheWrite1hTokens: durable.cacheWrite1hTokens,
|
|
33909
|
-
...lastDrift === void 0 ? {} : { lastDrift }
|
|
34333
|
+
...lastDrift === void 0 ? {} : { lastDrift },
|
|
34334
|
+
...warmup === void 0 ? {} : { warmup }
|
|
33910
34335
|
};
|
|
33911
34336
|
}
|
|
33912
34337
|
/**
|
|
@@ -64949,6 +65374,52 @@ function imagePart$1(block, images) {
|
|
|
64949
65374
|
image_url: { url: `data:${inline.mediaType};base64,${inline.data}` }
|
|
64950
65375
|
};
|
|
64951
65376
|
}
|
|
65377
|
+
/**
|
|
65378
|
+
* Whether one model id names a DeepSeek model.
|
|
65379
|
+
*
|
|
65380
|
+
* DeepSeek's own Chat Completions contract carries a disabled reasoning level as
|
|
65381
|
+
* `thinking: { type: 'disabled' }` rather than as a `reasoning_effort` value, so
|
|
65382
|
+
* the translation below is scoped to this model family. Read from the model id's
|
|
65383
|
+
* own segment so a provider-prefixed spelling (`cline-pass/...`, or a canonical
|
|
65384
|
+
* `deepseek/...` slug) is recognised either way.
|
|
65385
|
+
*/
|
|
65386
|
+
function isDeepSeekModel$1(modelId) {
|
|
65387
|
+
const bare = modelId.slice(modelId.lastIndexOf("/") + 1).trim();
|
|
65388
|
+
return /^deepseek/i.test(bare);
|
|
65389
|
+
}
|
|
65390
|
+
/** The names one developer message declares or revokes, in history order. */
|
|
65391
|
+
function toolNamesOf(blocks, type) {
|
|
65392
|
+
const names = [];
|
|
65393
|
+
for (const block of blocks ?? []) {
|
|
65394
|
+
const record = block;
|
|
65395
|
+
if (record.type !== type) continue;
|
|
65396
|
+
const name = record.toolName;
|
|
65397
|
+
if (typeof name !== "string" || name.trim().length === 0) continue;
|
|
65398
|
+
names.push(name.trim());
|
|
65399
|
+
}
|
|
65400
|
+
return names;
|
|
65401
|
+
}
|
|
65402
|
+
/** One quoted, comma-joined list of tool names. */
|
|
65403
|
+
function quotedList(names) {
|
|
65404
|
+
return names.map((name) => "\"" + name + "\"").join(", ");
|
|
65405
|
+
}
|
|
65406
|
+
/**
|
|
65407
|
+
* The model-facing text of a mid-conversation tool change, or '' when there is none.
|
|
65408
|
+
*
|
|
65409
|
+
* `in-history` means the harness stopped rewriting the declaration list: an
|
|
65410
|
+
* added tool still travels in `tools`, and a removed one keeps its declaration
|
|
65411
|
+
* so the prefix survives. The history note is therefore the only thing that
|
|
65412
|
+
* tells the model a removal happened, which is why the removal sentence is
|
|
65413
|
+
* imperative rather than descriptive.
|
|
65414
|
+
*/
|
|
65415
|
+
function toolUpdateNotice(blocks) {
|
|
65416
|
+
const added = toolNamesOf(blocks, "tool-addition");
|
|
65417
|
+
const removed = toolNamesOf(blocks, "tool-removal");
|
|
65418
|
+
const notices = [];
|
|
65419
|
+
if (added.length > 0) notices.push(added.length === 1 ? "[The tool " + quotedList(added) + " is now available.]" : "[The tools " + quotedList(added) + " are now available.]");
|
|
65420
|
+
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.]");
|
|
65421
|
+
return notices.join("\n");
|
|
65422
|
+
}
|
|
64952
65423
|
/** Recursively collect image parts from blocks, including nested tool results. */
|
|
64953
65424
|
function collectImageParts$1(blocks, images, target = []) {
|
|
64954
65425
|
if (!Array.isArray(blocks)) return target;
|
|
@@ -65001,8 +65472,17 @@ function serializeMessages(messages, images) {
|
|
|
65001
65472
|
pendingToolImages = [];
|
|
65002
65473
|
emit(message);
|
|
65003
65474
|
};
|
|
65004
|
-
/**
|
|
65475
|
+
/**
|
|
65476
|
+
* A tool answer: it closes its own id, and the group when it was the last.
|
|
65477
|
+
*
|
|
65478
|
+
* An answer whose call id has no pending `tool_call` is refused here, before
|
|
65479
|
+
* anything is sent. Forwarding it produces an OpenAI message sequence the
|
|
65480
|
+
* gateway rejects (a `tool` message with no declaration in front of it), and
|
|
65481
|
+
* the gateway's 400 names neither the call nor the turn, so the failure is
|
|
65482
|
+
* raised locally with the id that has no question.
|
|
65483
|
+
*/
|
|
65005
65484
|
const answer = (callId, content) => {
|
|
65485
|
+
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
65486
|
wire.push({
|
|
65007
65487
|
role: "tool",
|
|
65008
65488
|
tool_call_id: callId,
|
|
@@ -65015,9 +65495,11 @@ function serializeMessages(messages, images) {
|
|
|
65015
65495
|
for (const message of messages) {
|
|
65016
65496
|
if (message.role === "system" || message.role === "developer") {
|
|
65017
65497
|
flushToolImages();
|
|
65498
|
+
const content = [toolUpdateNotice(message.content), flattenText$1(message.content)].filter((part) => part.length > 0).join("\n");
|
|
65499
|
+
if (content.length === 0) continue;
|
|
65018
65500
|
emit({
|
|
65019
65501
|
role: "system",
|
|
65020
|
-
content
|
|
65502
|
+
content
|
|
65021
65503
|
});
|
|
65022
65504
|
continue;
|
|
65023
65505
|
}
|
|
@@ -65037,7 +65519,7 @@ function serializeMessages(messages, images) {
|
|
|
65037
65519
|
else if (block.type === "image") parts.push(imagePart$1(block, images));
|
|
65038
65520
|
const hasImage = parts.some((part) => part.type === "image_url");
|
|
65039
65521
|
const text = parts.filter((part) => part.type === "text").map((part) => String(part.text)).join("");
|
|
65040
|
-
if (hasImage || text.length > 0
|
|
65522
|
+
if (hasImage || text.length > 0) {
|
|
65041
65523
|
flushToolImages();
|
|
65042
65524
|
emit({
|
|
65043
65525
|
role: "user",
|
|
@@ -65072,6 +65554,7 @@ function buildClinePassBody(options, connection, images) {
|
|
|
65072
65554
|
}
|
|
65073
65555
|
}));
|
|
65074
65556
|
const maxTokens = options.maxTokens ?? connection.maxTokens;
|
|
65557
|
+
const thinkingOff = options.reasoningEffort !== void 0 && String(options.reasoningEffort) === "none" && isDeepSeekModel$1(options.model);
|
|
65075
65558
|
return {
|
|
65076
65559
|
model: options.model,
|
|
65077
65560
|
messages,
|
|
@@ -65081,7 +65564,8 @@ function buildClinePassBody(options, connection, images) {
|
|
|
65081
65564
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
65082
65565
|
...maxTokens === void 0 || maxTokens <= 0 ? {} : { max_tokens: maxTokens },
|
|
65083
65566
|
...options.stop === void 0 ? {} : { stop: options.stop },
|
|
65084
|
-
...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
|
|
65567
|
+
...options.reasoningEffort === void 0 || thinkingOff ? {} : { reasoning_effort: String(options.reasoningEffort) },
|
|
65568
|
+
...thinkingOff ? { thinking: { type: "disabled" } } : {}
|
|
65085
65569
|
};
|
|
65086
65570
|
}
|
|
65087
65571
|
/** Map an OpenAI `finish_reason` to the harness finish reason. */
|
|
@@ -65242,12 +65726,47 @@ function closeBlock$1(block) {
|
|
|
65242
65726
|
}
|
|
65243
65727
|
}
|
|
65244
65728
|
/**
|
|
65729
|
+
* Whether one failure is the SSE parser's own "the body ended without [DONE]".
|
|
65730
|
+
*
|
|
65731
|
+
* Structural rather than `instanceof`: what has to match is the code the parser
|
|
65732
|
+
* assigns, and a second copy of this module (or a harness generation that ships
|
|
65733
|
+
* its own LlmError class) would fail an identity test on a value that is
|
|
65734
|
+
* otherwise exactly the signal this decision is about.
|
|
65735
|
+
*/
|
|
65736
|
+
function isMissingSentinel$1(error) {
|
|
65737
|
+
return typeof error === "object" && error !== null && error.code === "STREAM_CLOSED";
|
|
65738
|
+
}
|
|
65739
|
+
/**
|
|
65740
|
+
* End the payload iteration instead of throwing when a body stops cleanly.
|
|
65741
|
+
*
|
|
65742
|
+
* The parser can only report "no [DONE] arrived" by throwing, and that is NOT a
|
|
65743
|
+
* failure on its own: the gateway sometimes closes the response right after the
|
|
65744
|
+
* terminal frame. Whether the response actually completed is decided by the
|
|
65745
|
+
* frames that were seen, which is knowledge this generator does not have — so it
|
|
65746
|
+
* only removes the transport signal and leaves the verdict to {@link translate}.
|
|
65747
|
+
* Every other failure passes through untouched, including the error frames a 200
|
|
65748
|
+
* body can carry.
|
|
65749
|
+
*/
|
|
65750
|
+
async function* tolerateMissingSentinel$1(payloads) {
|
|
65751
|
+
try {
|
|
65752
|
+
for await (const payload of payloads) yield payload;
|
|
65753
|
+
} catch (error) {
|
|
65754
|
+
if (!isMissingSentinel$1(error)) throw error;
|
|
65755
|
+
}
|
|
65756
|
+
}
|
|
65757
|
+
/**
|
|
65245
65758
|
* Translate decoded SSE payloads into harness stream chunks.
|
|
65246
65759
|
*
|
|
65247
65760
|
* Deltas stream through as they arrive; block ends, usage and the finish reason
|
|
65248
65761
|
* are all deferred to the `[DONE]` sentinel, so no chunk ever follows `finish`.
|
|
65249
65762
|
* A completion with no content at all becomes an `EMPTY_RESPONSE` error finish
|
|
65250
65763
|
* rather than a silent empty message.
|
|
65764
|
+
*
|
|
65765
|
+
* A missing `[DONE]` is not a failure by itself. Once a valid `finish_reason`
|
|
65766
|
+
* has been seen the response IS complete, and it closes exactly as a `[DONE]`
|
|
65767
|
+
* one does — block ends, usage and finish all preserved. A body that carried
|
|
65768
|
+
* content but never named a finish reason is a truncation, and still fails with
|
|
65769
|
+
* `STREAM_CLOSED`.
|
|
65251
65770
|
*/
|
|
65252
65771
|
async function* translate(payloads) {
|
|
65253
65772
|
let nextIndex = 0;
|
|
@@ -65274,38 +65793,48 @@ async function* translate(payloads) {
|
|
|
65274
65793
|
order.push(block);
|
|
65275
65794
|
return block;
|
|
65276
65795
|
};
|
|
65277
|
-
|
|
65796
|
+
/**
|
|
65797
|
+
* The chunks that close a completed response: every open block, then usage,
|
|
65798
|
+
* then the single finish. Both terminal paths use this one assembly, so a
|
|
65799
|
+
* response read from a clean EOF and one read from `[DONE]` cannot drift.
|
|
65800
|
+
*/
|
|
65801
|
+
const closing = () => {
|
|
65802
|
+
const chunks = [];
|
|
65803
|
+
for (const block of order) chunks.push({
|
|
65804
|
+
type: "block-end",
|
|
65805
|
+
index: block.index,
|
|
65806
|
+
block: closeBlock$1(block)
|
|
65807
|
+
});
|
|
65808
|
+
if (pendingUsage !== void 0) chunks.push({
|
|
65809
|
+
type: "usage",
|
|
65810
|
+
usage: pendingUsage
|
|
65811
|
+
});
|
|
65812
|
+
const reason = pendingFinish ?? {
|
|
65813
|
+
type: "finish",
|
|
65814
|
+
reason: { kind: "stop" }
|
|
65815
|
+
};
|
|
65816
|
+
const hasVisibleAnswer = toolBlocks.size > 0 || textBlock !== void 0 && textBlock.text.trim() !== "";
|
|
65817
|
+
chunks.push({
|
|
65818
|
+
type: "finish",
|
|
65819
|
+
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
65820
|
+
kind: "error",
|
|
65821
|
+
failure: {
|
|
65822
|
+
message: "model returned a completed response with no content",
|
|
65823
|
+
code: EMPTY_RESPONSE_CODE
|
|
65824
|
+
}
|
|
65825
|
+
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
65826
|
+
kind: "error",
|
|
65827
|
+
failure: {
|
|
65828
|
+
message: "Cline Pass ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
65829
|
+
code: "PROVIDER_ERROR"
|
|
65830
|
+
}
|
|
65831
|
+
} : reason.reason
|
|
65832
|
+
});
|
|
65833
|
+
return chunks;
|
|
65834
|
+
};
|
|
65835
|
+
for await (const payload of tolerateMissingSentinel$1(payloads)) {
|
|
65278
65836
|
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
|
-
};
|
|
65837
|
+
for (const chunk of closing()) yield chunk;
|
|
65309
65838
|
return;
|
|
65310
65839
|
}
|
|
65311
65840
|
const chunk = unwrapEnvelope(payload);
|
|
@@ -65379,6 +65908,10 @@ async function* translate(payloads) {
|
|
|
65379
65908
|
}
|
|
65380
65909
|
if (isRecord$11(chunk.usage)) pendingUsage = mapUsage$1(chunk.usage);
|
|
65381
65910
|
}
|
|
65911
|
+
if (pendingFinish !== void 0) {
|
|
65912
|
+
for (const chunk of closing()) yield chunk;
|
|
65913
|
+
return;
|
|
65914
|
+
}
|
|
65382
65915
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
65383
65916
|
}
|
|
65384
65917
|
//#endregion
|
|
@@ -65515,6 +66048,7 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
65515
66048
|
id: modelId,
|
|
65516
66049
|
name: info.displayName,
|
|
65517
66050
|
inputModalities: info.input,
|
|
66051
|
+
toolUpdate: "in-history",
|
|
65518
66052
|
context: { contextWindow: info.contextWindow },
|
|
65519
66053
|
...outputReservation(info.contextWindow, info.maxTokens),
|
|
65520
66054
|
...info.reasoning ? { reasoning: { efforts: info.efforts.map((effort) => ({
|
|
@@ -65535,11 +66069,12 @@ var ClinePassAdapter = class extends LlmAdapter {
|
|
|
65535
66069
|
}
|
|
65536
66070
|
async *requestStream(rawOptions, settings, signal, activity) {
|
|
65537
66071
|
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(
|
|
66072
|
+
const normalized = normalizeGenerateOptions(rawOptions);
|
|
66073
|
+
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal).catch((error) => {
|
|
65540
66074
|
if (signal.aborted) throw new LlmError("Cline Pass request aborted by caller", "ABORTED", { cause: error });
|
|
65541
66075
|
return NO_RESOLVED_IMAGES$3;
|
|
65542
66076
|
});
|
|
66077
|
+
const options = offloadOldestRequestImages$1(normalized, void 0, images);
|
|
65543
66078
|
const body = buildClinePassBody(options, {
|
|
65544
66079
|
baseURL: settings.baseURL,
|
|
65545
66080
|
displayName: PROVIDER_NAME$4,
|
|
@@ -76386,6 +76921,52 @@ function collectImageParts(blocks, images, target = []) {
|
|
|
76386
76921
|
}
|
|
76387
76922
|
return target;
|
|
76388
76923
|
}
|
|
76924
|
+
/** Names of the tools one developer message adds at its own history position. */
|
|
76925
|
+
function toolAdditionNames(message) {
|
|
76926
|
+
const names = [];
|
|
76927
|
+
for (const block of message.content) {
|
|
76928
|
+
if (block.type !== "tool-addition") continue;
|
|
76929
|
+
const name = block.toolName;
|
|
76930
|
+
if (typeof name !== "string" || name.trim() === "") continue;
|
|
76931
|
+
names.push(name.trim());
|
|
76932
|
+
}
|
|
76933
|
+
return names;
|
|
76934
|
+
}
|
|
76935
|
+
/** Names of the tools one developer message revokes at its own history position. */
|
|
76936
|
+
function toolRemovalNames(message) {
|
|
76937
|
+
const names = [];
|
|
76938
|
+
for (const block of message.content) {
|
|
76939
|
+
if (block.type !== "tool-removal") continue;
|
|
76940
|
+
const name = block.toolName;
|
|
76941
|
+
if (typeof name !== "string" || name.trim() === "") continue;
|
|
76942
|
+
names.push(name.trim());
|
|
76943
|
+
}
|
|
76944
|
+
return names;
|
|
76945
|
+
}
|
|
76946
|
+
/**
|
|
76947
|
+
* The model-facing note that announces tools a conversation just gained.
|
|
76948
|
+
*
|
|
76949
|
+
* This route declares `toolUpdate: 'in-history'`, so the harness states an
|
|
76950
|
+
* addition as a block at its own history position instead of rewriting the whole
|
|
76951
|
+
* request's tool list. OpenAI has no message-level tool declaration, so the
|
|
76952
|
+
* change travels as this sentence: it costs one short system turn and leaves the
|
|
76953
|
+
* top-level `tools` array — and therefore the prefix cache — intact.
|
|
76954
|
+
*/
|
|
76955
|
+
function additionNotice(names) {
|
|
76956
|
+
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
76957
|
+
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.]";
|
|
76958
|
+
}
|
|
76959
|
+
/**
|
|
76960
|
+
* The model-facing note that revokes one or more tools.
|
|
76961
|
+
*
|
|
76962
|
+
* Imperative and unambiguous on purpose: the tool is still in the declaration
|
|
76963
|
+
* list, so this sentence is the only thing between the model and a call that
|
|
76964
|
+
* would fail.
|
|
76965
|
+
*/
|
|
76966
|
+
function removalNotice(names) {
|
|
76967
|
+
const list = names.map((name) => "\"" + name + "\"").join(", ");
|
|
76968
|
+
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.]";
|
|
76969
|
+
}
|
|
76389
76970
|
/**
|
|
76390
76971
|
* Convert the harness conversation to wire messages.
|
|
76391
76972
|
*
|
|
@@ -76397,7 +76978,15 @@ function collectImageParts(blocks, images, target = []) {
|
|
|
76397
76978
|
* message keeps a parallel group's answers contiguous.
|
|
76398
76979
|
*
|
|
76399
76980
|
* `developer` becomes `system`: the newer OpenAI spelling is not accepted by
|
|
76400
|
-
* every OpenAI-compatible gateway, while `system` is.
|
|
76981
|
+
* every OpenAI-compatible gateway, while `system` is. A tool addition or
|
|
76982
|
+
* removal carried by such a message is replayed as its own system sentence —
|
|
76983
|
+
* the `in-history` shape this route declares — and ordinary text is kept
|
|
76984
|
+
* beside it. A message that contributes neither is dropped rather than sent as
|
|
76985
|
+
* an empty system turn.
|
|
76986
|
+
*
|
|
76987
|
+
* A user message that carries no text and no image contributes nothing to a
|
|
76988
|
+
* chat request, so it is dropped too; a pure tool-result message never took that
|
|
76989
|
+
* path, because its answers travel as `tool` messages.
|
|
76401
76990
|
*/
|
|
76402
76991
|
function serializeMessages$1(messages, images) {
|
|
76403
76992
|
const wire = [];
|
|
@@ -76430,6 +77019,7 @@ function serializeMessages$1(messages, images) {
|
|
|
76430
77019
|
};
|
|
76431
77020
|
/** A tool answer: it closes its own id, and the group when it was the last. */
|
|
76432
77021
|
const answer = (callId, content) => {
|
|
77022
|
+
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
77023
|
wire.push({
|
|
76434
77024
|
role: "tool",
|
|
76435
77025
|
tool_call_id: callId,
|
|
@@ -76442,9 +77032,20 @@ function serializeMessages$1(messages, images) {
|
|
|
76442
77032
|
for (const message of messages) {
|
|
76443
77033
|
if (message.role === "system" || message.role === "developer") {
|
|
76444
77034
|
flushToolImages();
|
|
76445
|
-
|
|
77035
|
+
const text = flattenText(message.content);
|
|
77036
|
+
if (text.length > 0) emit({
|
|
77037
|
+
role: "system",
|
|
77038
|
+
content: text
|
|
77039
|
+
});
|
|
77040
|
+
const additions = toolAdditionNames(message);
|
|
77041
|
+
if (additions.length > 0) emit({
|
|
77042
|
+
role: "system",
|
|
77043
|
+
content: additionNotice(additions)
|
|
77044
|
+
});
|
|
77045
|
+
const removals = toolRemovalNames(message);
|
|
77046
|
+
if (removals.length > 0) emit({
|
|
76446
77047
|
role: "system",
|
|
76447
|
-
content:
|
|
77048
|
+
content: removalNotice(removals)
|
|
76448
77049
|
});
|
|
76449
77050
|
continue;
|
|
76450
77051
|
}
|
|
@@ -76464,7 +77065,7 @@ function serializeMessages$1(messages, images) {
|
|
|
76464
77065
|
else if (block.type === "image") parts.push(imagePart(block, images));
|
|
76465
77066
|
const hasImage = parts.some((part) => part.type === "image_url");
|
|
76466
77067
|
const text = parts.filter((part) => part.type === "text").map((part) => String(part.text)).join("");
|
|
76467
|
-
if (hasImage || text.length > 0
|
|
77068
|
+
if (hasImage || text.length > 0) {
|
|
76468
77069
|
flushToolImages();
|
|
76469
77070
|
emit({
|
|
76470
77071
|
role: "user",
|
|
@@ -76478,6 +77079,21 @@ function serializeMessages$1(messages, images) {
|
|
|
76478
77079
|
return wire;
|
|
76479
77080
|
}
|
|
76480
77081
|
/**
|
|
77082
|
+
* Whether one model id is a DeepSeek model.
|
|
77083
|
+
*
|
|
77084
|
+
* The vendor prefix is stripped before the compare, so the bare wire id this
|
|
77085
|
+
* line ships (`deepseek-v4.1-flash`) and a vendor-qualified spelling
|
|
77086
|
+
* (`deepseek/deepseek-v4-pro`, as a sibling registry writes it) both match,
|
|
77087
|
+
* while an id that merely mentions the word — a gateway alias such as
|
|
77088
|
+
* `my-deepseek-proxy` — does not. Only DeepSeek's own chat endpoint documents
|
|
77089
|
+
* the `thinking` field, which is why the translation below is scoped to it
|
|
77090
|
+
* rather than applied to every model that offers a `none` level.
|
|
77091
|
+
*/
|
|
77092
|
+
function isDeepSeekModel(model) {
|
|
77093
|
+
const id = String(model ?? "").trim().toLowerCase();
|
|
77094
|
+
return id.slice(Math.max(id.lastIndexOf("/"), id.lastIndexOf(":")) + 1).startsWith("deepseek");
|
|
77095
|
+
}
|
|
77096
|
+
/**
|
|
76481
77097
|
* Build the wire request for one harness call.
|
|
76482
77098
|
* @param options - the harness request (model, system, history, tools, sampling).
|
|
76483
77099
|
* @param connection - resolved connection facts (default output budget).
|
|
@@ -76499,6 +77115,8 @@ function buildOpenDesignBody(options, connection, images) {
|
|
|
76499
77115
|
}
|
|
76500
77116
|
}));
|
|
76501
77117
|
const maxTokens = options.maxTokens ?? connection.maxTokens;
|
|
77118
|
+
const reasoningEffort = options.reasoningEffort === void 0 ? void 0 : String(options.reasoningEffort);
|
|
77119
|
+
const thinkingOff = reasoningEffort !== void 0 && reasoningEffort.toLowerCase() === "none" && isDeepSeekModel(options.model);
|
|
76502
77120
|
return {
|
|
76503
77121
|
model: options.model,
|
|
76504
77122
|
messages,
|
|
@@ -76508,7 +77126,8 @@ function buildOpenDesignBody(options, connection, images) {
|
|
|
76508
77126
|
...options.temperature === void 0 ? {} : { temperature: options.temperature },
|
|
76509
77127
|
...maxTokens === void 0 || maxTokens <= 0 ? {} : { max_tokens: maxTokens },
|
|
76510
77128
|
...options.stop === void 0 ? {} : { stop: options.stop },
|
|
76511
|
-
...
|
|
77129
|
+
...thinkingOff || reasoningEffort === void 0 ? {} : { reasoning_effort: reasoningEffort },
|
|
77130
|
+
...thinkingOff ? { thinking: { type: "disabled" } } : {}
|
|
76512
77131
|
};
|
|
76513
77132
|
}
|
|
76514
77133
|
/** Map an OpenAI `finish_reason` to the harness finish reason. */
|
|
@@ -76627,8 +77246,16 @@ function streamErrorCode(message, status) {
|
|
|
76627
77246
|
}
|
|
76628
77247
|
/**
|
|
76629
77248
|
* Parse an SSE body into decoded JSON frames.
|
|
77249
|
+
*
|
|
77250
|
+
* The parser's contract is the sibling line's and stays unsoftened: a body that
|
|
77251
|
+
* ends without `[DONE]` is reported by THROWING `STREAM_CLOSED` after its last
|
|
77252
|
+
* frame. An SSE parser cannot tell a gateway that closes politely after the
|
|
77253
|
+
* terminal frame from a truncated response, because it never interprets
|
|
77254
|
+
* `finish_reason` — so it must not decide that question for its callers either.
|
|
77255
|
+
* That verdict belongs to {@link translate}, which has the frames, and absorbs
|
|
77256
|
+
* this one code from its input.
|
|
76630
77257
|
* @param body - a web ReadableStream of UTF-8 bytes.
|
|
76631
|
-
* @returns every `data:` payload up to `[DONE]`.
|
|
77258
|
+
* @returns every `data:` payload up to `[DONE]`, then throws `STREAM_CLOSED`.
|
|
76632
77259
|
*/
|
|
76633
77260
|
async function* parseServerSentEvents(body) {
|
|
76634
77261
|
const decoder = new TextDecoder();
|
|
@@ -76674,6 +77301,36 @@ async function* parseServerSentEvents(body) {
|
|
|
76674
77301
|
}
|
|
76675
77302
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
76676
77303
|
}
|
|
77304
|
+
/**
|
|
77305
|
+
* Whether one failure is the SSE parser's own "the body ended without [DONE]".
|
|
77306
|
+
*
|
|
77307
|
+
* Structural rather than `instanceof`: what has to match is the CODE the parser
|
|
77308
|
+
* assigns, and a second copy of this module (or a harness generation that ships
|
|
77309
|
+
* its own LlmError class) would fail an identity test on a value that is
|
|
77310
|
+
* otherwise exactly the signal this decision is about.
|
|
77311
|
+
*/
|
|
77312
|
+
function isMissingSentinel(error) {
|
|
77313
|
+
return typeof error === "object" && error !== null && error.code === "STREAM_CLOSED";
|
|
77314
|
+
}
|
|
77315
|
+
/**
|
|
77316
|
+
* End the payload iteration instead of throwing when a body stops without the
|
|
77317
|
+
* sentinel.
|
|
77318
|
+
*
|
|
77319
|
+
* The parser can only report "no [DONE] arrived" by throwing, and that is NOT a
|
|
77320
|
+
* failure on its own: the gateway sometimes closes the response right after the
|
|
77321
|
+
* terminal frame. Whether the response actually completed is decided by the
|
|
77322
|
+
* frames that were seen, which is knowledge this generator does not have — so it
|
|
77323
|
+
* only removes the transport signal and leaves the verdict to {@link translate}.
|
|
77324
|
+
* Every other failure passes through untouched, including the error frames a 200
|
|
77325
|
+
* body can carry.
|
|
77326
|
+
*/
|
|
77327
|
+
async function* tolerateMissingSentinel(payloads) {
|
|
77328
|
+
try {
|
|
77329
|
+
for await (const payload of payloads) yield payload;
|
|
77330
|
+
} catch (error) {
|
|
77331
|
+
if (!isMissingSentinel(error)) throw error;
|
|
77332
|
+
}
|
|
77333
|
+
}
|
|
76677
77334
|
/** Assemble the terminal content block for one open block. */
|
|
76678
77335
|
function closeBlock(block) {
|
|
76679
77336
|
switch (block.kind) {
|
|
@@ -76697,9 +77354,21 @@ function closeBlock(block) {
|
|
|
76697
77354
|
* Translate decoded SSE payloads into harness stream chunks.
|
|
76698
77355
|
*
|
|
76699
77356
|
* Deltas stream through as they arrive; block ends, usage and the finish reason
|
|
76700
|
-
* are all deferred to the
|
|
77357
|
+
* are all deferred to the end of the answer, so no chunk ever follows `finish`.
|
|
76701
77358
|
* A completion with no content at all becomes an `EMPTY_RESPONSE` error finish
|
|
76702
77359
|
* rather than a silent empty message.
|
|
77360
|
+
*
|
|
77361
|
+
* The end of the answer is either the `[DONE]` sentinel or the end of the body
|
|
77362
|
+
* AFTER a terminal `finish_reason` arrived. Some gateways close the socket
|
|
77363
|
+
* instead of writing the sentinel, and failing such an answer threw away content
|
|
77364
|
+
* the caller had already received.
|
|
77365
|
+
*
|
|
77366
|
+
* The parser still raises `STREAM_CLOSED` for a body with no sentinel — that is
|
|
77367
|
+
* its contract and it is not widened here. The signal is absorbed where the
|
|
77368
|
+
* frames are available instead (see {@link tolerateMissingSentinel}), and only
|
|
77369
|
+
* then does the finish reason decide: a body that stated one closes like any
|
|
77370
|
+
* other, and a body that never stated one is truncation and still fails with
|
|
77371
|
+
* `STREAM_CLOSED`. No other input failure is absorbed.
|
|
76703
77372
|
*/
|
|
76704
77373
|
async function* translate$1(payloads) {
|
|
76705
77374
|
let nextIndex = 0;
|
|
@@ -76726,38 +77395,44 @@ async function* translate$1(payloads) {
|
|
|
76726
77395
|
order.push(block);
|
|
76727
77396
|
return block;
|
|
76728
77397
|
};
|
|
76729
|
-
|
|
77398
|
+
/** The terminal chunks of one completed answer, in the order the harness accepts. */
|
|
77399
|
+
const terminal = (reason) => {
|
|
77400
|
+
const chunks = [];
|
|
77401
|
+
for (const block of order) chunks.push({
|
|
77402
|
+
type: "block-end",
|
|
77403
|
+
index: block.index,
|
|
77404
|
+
block: closeBlock(block)
|
|
77405
|
+
});
|
|
77406
|
+
if (pendingUsage !== void 0) chunks.push({
|
|
77407
|
+
type: "usage",
|
|
77408
|
+
usage: pendingUsage
|
|
77409
|
+
});
|
|
77410
|
+
const hasVisibleAnswer = toolBlocks.size > 0 || textBlock !== void 0 && textBlock.text.trim() !== "";
|
|
77411
|
+
chunks.push({
|
|
77412
|
+
type: "finish",
|
|
77413
|
+
reason: reason.reason.kind === "stop" && order.length === 0 ? {
|
|
77414
|
+
kind: "error",
|
|
77415
|
+
failure: {
|
|
77416
|
+
message: "model returned a completed response with no content",
|
|
77417
|
+
code: EMPTY_RESPONSE_CODE
|
|
77418
|
+
}
|
|
77419
|
+
} : reason.reason.kind === "stop" && !hasVisibleAnswer ? {
|
|
77420
|
+
kind: "error",
|
|
77421
|
+
failure: {
|
|
77422
|
+
message: "OpenDesign ended the turn without an answer or a tool call — the response stopped before it did anything",
|
|
77423
|
+
code: "PROVIDER_ERROR"
|
|
77424
|
+
}
|
|
77425
|
+
} : reason.reason
|
|
77426
|
+
});
|
|
77427
|
+
return chunks;
|
|
77428
|
+
};
|
|
77429
|
+
for await (const payload of tolerateMissingSentinel(payloads)) {
|
|
76730
77430
|
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
77431
|
const reason = pendingFinish ?? {
|
|
76741
77432
|
type: "finish",
|
|
76742
77433
|
reason: { kind: "stop" }
|
|
76743
77434
|
};
|
|
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
|
-
};
|
|
77435
|
+
for (const chunk of terminal(reason)) yield chunk;
|
|
76761
77436
|
return;
|
|
76762
77437
|
}
|
|
76763
77438
|
if (isErrorPayload(payload)) throw new LlmError(errorText(payload.error), streamErrorCode(errorText(payload.error)));
|
|
@@ -76831,6 +77506,10 @@ async function* translate$1(payloads) {
|
|
|
76831
77506
|
}
|
|
76832
77507
|
if (isRecord$2(chunk.usage)) pendingUsage = mapUsage(chunk.usage);
|
|
76833
77508
|
}
|
|
77509
|
+
if (pendingFinish !== void 0) {
|
|
77510
|
+
for (const chunk of terminal(pendingFinish)) yield chunk;
|
|
77511
|
+
return;
|
|
77512
|
+
}
|
|
76834
77513
|
throw new LlmError("stream ended without [DONE]", "STREAM_CLOSED");
|
|
76835
77514
|
}
|
|
76836
77515
|
//#endregion
|
|
@@ -76891,6 +77570,23 @@ const TRANSPORT_RETRY_BASE_MS = 400;
|
|
|
76891
77570
|
* but the ladder still prefers published data for anything the tables know.
|
|
76892
77571
|
*/
|
|
76893
77572
|
const REASONING_EFFORT_IDS = DEFAULT_EFFORTS;
|
|
77573
|
+
/**
|
|
77574
|
+
* How this route carries a mid-conversation tool change, or undefined for none.
|
|
77575
|
+
*
|
|
77576
|
+
* `in-history`, not `addition-only`: both append an addition, but only
|
|
77577
|
+
* `in-history` can also express a REMOVAL. Under `addition-only` a revoked
|
|
77578
|
+
* tool is deleted from the declaration list, the top-level `tools` array
|
|
77579
|
+
* shrinks, and the gateway's prefix match is thrown away for the whole prompt.
|
|
77580
|
+
*
|
|
77581
|
+
* The field is spread into the descriptor only when this route wants one, so a
|
|
77582
|
+
* harness generation that does not know it simply never reads it — no version
|
|
77583
|
+
* check, and no change to the message shape either way: mapper.ts renders each
|
|
77584
|
+
* change as a system sentence and never introduces a native dynamic-tool
|
|
77585
|
+
* declaration, which an OpenAI-compatible gateway has no field for.
|
|
77586
|
+
*/
|
|
77587
|
+
function toolUpdateMode() {
|
|
77588
|
+
return "in-history";
|
|
77589
|
+
}
|
|
76894
77590
|
var OpenDesignAdapter = class extends LlmAdapter {
|
|
76895
77591
|
modelSettings;
|
|
76896
77592
|
options;
|
|
@@ -76949,6 +77645,7 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
76949
77645
|
id: modelId,
|
|
76950
77646
|
name: info.displayName,
|
|
76951
77647
|
inputModalities: info.input,
|
|
77648
|
+
...toolUpdateMode() === void 0 ? {} : { toolUpdate: toolUpdateMode() },
|
|
76952
77649
|
context: { contextWindow: info.contextWindow },
|
|
76953
77650
|
...outputReservation(info.contextWindow, info.maxTokens),
|
|
76954
77651
|
...info.reasoning && info.efforts.length > 0 ? { reasoning: {
|
|
@@ -76976,11 +77673,12 @@ var OpenDesignAdapter = class extends LlmAdapter {
|
|
|
76976
77673
|
}
|
|
76977
77674
|
async *requestStream(rawOptions, settings, signal, activity) {
|
|
76978
77675
|
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(
|
|
77676
|
+
const normalized = normalizeGenerateOptions(rawOptions);
|
|
77677
|
+
const images = await resolveRequestImages$1(normalized, this.options.attachments, signal).catch((error) => {
|
|
76981
77678
|
if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
|
|
76982
77679
|
return NO_RESOLVED_IMAGES$3;
|
|
76983
77680
|
});
|
|
77681
|
+
const options = offloadOldestRequestImages$1(normalized, MAX_REQUEST_IMAGE_BYTES$1, images);
|
|
76984
77682
|
const body = buildOpenDesignBody(options, { maxTokens: (await this.modelInfo(options.model)).maxTokens }, images);
|
|
76985
77683
|
const refused = /* @__PURE__ */ new Set();
|
|
76986
77684
|
let lastError = null;
|
|
@@ -82700,6 +83398,20 @@ function createCacheWarmupResultTool(deps) {
|
|
|
82700
83398
|
* 预览只有坏处没有好处,所以这里写死一个 provider 名而不是做成开关。
|
|
82701
83399
|
*/
|
|
82702
83400
|
const CACHE_WARMUP_PROVIDER = "kimi-code";
|
|
83401
|
+
/**
|
|
83402
|
+
* 允许进入结果投影的工具集合。
|
|
83403
|
+
*
|
|
83404
|
+
* 除了 PTC 的 `run_code`,宿主的独立 Shell 与后台任务输出工具(`win_bash`、
|
|
83405
|
+
* `bash`、`pwsh`、`job_output`)同样经常吐出整屏测试/构建日志。源码读取/编辑类
|
|
83406
|
+
* 工具(`read`、`edit`、`grep`、`glob`、CodeGraph 等)不在此列,保持 100% 原样。
|
|
83407
|
+
*/
|
|
83408
|
+
const CACHE_WARMUP_SOURCE_TOOLS = /* @__PURE__ */ new Set([
|
|
83409
|
+
"run_code",
|
|
83410
|
+
"win_bash",
|
|
83411
|
+
"bash",
|
|
83412
|
+
"pwsh",
|
|
83413
|
+
"job_output"
|
|
83414
|
+
]);
|
|
82703
83415
|
/** 提示词分节名,全局唯一(重复注册会抛错)。 */
|
|
82704
83416
|
const CACHE_WARMUP_SECTION_NAME = "plugin:cache-warmup";
|
|
82705
83417
|
/**
|
|
@@ -82943,7 +83655,7 @@ function installCacheWarmup(ctx, options) {
|
|
|
82943
83655
|
const projectDecision = async (exec, result, decision) => {
|
|
82944
83656
|
if (decision.kind !== "accept") return decision;
|
|
82945
83657
|
if (decision.value !== void 0) return decision;
|
|
82946
|
-
if (exec.name
|
|
83658
|
+
if (!CACHE_WARMUP_SOURCE_TOOLS.has(exec.name)) return decision;
|
|
82947
83659
|
if (!options.settings().enabled) return decision;
|
|
82948
83660
|
const route = cacheWarmupRouteOf(exec.agent);
|
|
82949
83661
|
if (route.provider !== "kimi-code") return decision;
|
|
@@ -82954,7 +83666,13 @@ function installCacheWarmup(ctx, options) {
|
|
|
82954
83666
|
const blocks = decision.content ?? result.content;
|
|
82955
83667
|
if (blocks.length === 0) return decision;
|
|
82956
83668
|
const content = blocks;
|
|
82957
|
-
|
|
83669
|
+
let durable;
|
|
83670
|
+
try {
|
|
83671
|
+
durable = options.durableHint?.(sessionId);
|
|
83672
|
+
} catch {
|
|
83673
|
+
durable = void 0;
|
|
83674
|
+
}
|
|
83675
|
+
const budget = durable === void 0 ? cacheWarmupBudget(sessionId) : cacheWarmupBudget(sessionId, Date.now(), durable);
|
|
82958
83676
|
const projected = await projectToolResult({
|
|
82959
83677
|
toolName: exec.name,
|
|
82960
83678
|
provider: route.provider,
|
|
@@ -83191,7 +83909,19 @@ function apply(ctx, pluginConfig = {}) {
|
|
|
83191
83909
|
return dispose;
|
|
83192
83910
|
}, "dsh-chatgpt-subscription: no-answer resume");
|
|
83193
83911
|
}
|
|
83194
|
-
ctx.effect(() => installCacheWarmup(ctx, {
|
|
83912
|
+
ctx.effect(() => installCacheWarmup(ctx, {
|
|
83913
|
+
settings: () => resolveCacheWarmupSettings(pluginConfig),
|
|
83914
|
+
durableHint: (sessionId) => {
|
|
83915
|
+
const record = sessionCacheState.read(sessionId);
|
|
83916
|
+
if (record === void 0) return void 0;
|
|
83917
|
+
const total = record.cachedTokens + record.freshTokens;
|
|
83918
|
+
return {
|
|
83919
|
+
lastActiveAt: record.lastActiveAt,
|
|
83920
|
+
consecutiveHits: record.warmupConsecutiveHits ?? 0,
|
|
83921
|
+
hitRatio: total > 0 ? record.cachedTokens / total : 0
|
|
83922
|
+
};
|
|
83923
|
+
}
|
|
83924
|
+
}), "dsh-chatgpt-subscription: cache warmup");
|
|
83195
83925
|
const probeEnvFile = relayProbeEnvFile();
|
|
83196
83926
|
if (relayProbeEnabled(process.env, { envFile: probeEnvFile })) {
|
|
83197
83927
|
const probePath = relayProbeLogPath(process.env, { envFile: probeEnvFile });
|