mslxdff 0.1.160 → 0.1.161

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.
Files changed (117) hide show
  1. package/bin/mslxdff.js +4 -4
  2. package/docs/cli_help_mini.md +135 -0
  3. package/package.json +1 -1
  4. package/src/auto.js +254 -254
  5. package/src/autostart.js +3 -0
  6. package/src/bench/cline-bench.js +42 -42
  7. package/src/bench/probe.js +70 -70
  8. package/src/bench/report.js +162 -162
  9. package/src/bench/runner.js +77 -77
  10. package/src/bench/via-probe.js +124 -124
  11. package/src/bench/via-routes.js +87 -87
  12. package/src/bench/workbuddy-bench.js +54 -54
  13. package/src/chat/engine.js +160 -160
  14. package/src/chat/gateway.js +163 -163
  15. package/src/chat/orchestrator.js +234 -234
  16. package/src/chat/prompt.js +70 -70
  17. package/src/chat/repl.js +88 -88
  18. package/src/chat/terminal.js +135 -135
  19. package/src/chat/tools.js +306 -306
  20. package/src/chat-pipeline/empty-turn.js +119 -0
  21. package/src/chat-pipeline/index.js +126 -123
  22. package/src/chat-pipeline/policy.js +76 -76
  23. package/src/chat-pipeline/serial-trial.js +53 -19
  24. package/src/cli/commands/daemon.js +4 -4
  25. package/src/cli/commands/group.js +249 -249
  26. package/src/cli/commands/model/list-providers.js +1 -1
  27. package/src/cli/commands/model/picks.js +50 -50
  28. package/src/cli/commands/provider/bench-via.js +247 -247
  29. package/src/cli/commands/provider/bench.js +141 -141
  30. package/src/cli/commands/provider/globalqwenwork-login.js +126 -0
  31. package/src/cli/commands/provider/index.js +126 -124
  32. package/src/cli/commands/provider/models.js +142 -139
  33. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  34. package/src/cli/commands/sync.js +232 -232
  35. package/src/cli/commands/system.js +2 -2
  36. package/src/cli/format.js +6 -0
  37. package/src/cli/policy.js +2 -2
  38. package/src/cli/provider-row.js +2 -2
  39. package/src/cli/status.js +279 -279
  40. package/src/daemon.js +101 -96
  41. package/src/logs.js +14 -1
  42. package/src/model-capabilities/enrich.js +86 -86
  43. package/src/model-capabilities/index.js +183 -183
  44. package/src/model-capabilities/parse.js +70 -70
  45. package/src/model-trace.js +5 -2
  46. package/src/models.js +225 -225
  47. package/src/providers/AGENTS.md +46 -0
  48. package/src/providers/classify.js +1 -1
  49. package/src/providers/cline/auth.js +228 -228
  50. package/src/providers/cline/chat.js +307 -307
  51. package/src/providers/cline.js +2 -2
  52. package/src/providers/globalqwenwork/account-store.js +141 -0
  53. package/src/providers/globalqwenwork/constants.js +73 -0
  54. package/src/providers/globalqwenwork/cosy.js +123 -0
  55. package/src/providers/globalqwenwork/crypto.js +220 -0
  56. package/src/providers/globalqwenwork/http.js +22 -0
  57. package/src/providers/globalqwenwork/index.js +331 -0
  58. package/src/providers/globalqwenwork/payload.js +145 -0
  59. package/src/providers/globalqwenwork/rsa.js +56 -0
  60. package/src/providers/globalqwenwork/sse.js +270 -0
  61. package/src/providers/globalqwenwork/stream.js +132 -0
  62. package/src/providers/globalqwenwork/upstream.js +122 -0
  63. package/src/providers/globalqwenwork.js +1 -0
  64. package/src/providers/keyring.js +60 -60
  65. package/src/providers/qoder/chat.js +183 -183
  66. package/src/providers/qoder/index.js +230 -230
  67. package/src/providers/qoder/sse.js +103 -103
  68. package/src/providers/qwenwork/account-store.js +133 -133
  69. package/src/providers/qwenwork/constants.js +67 -67
  70. package/src/providers/qwenwork/cosy.js +120 -120
  71. package/src/providers/qwenwork/crypto.js +218 -218
  72. package/src/providers/qwenwork/http.js +20 -20
  73. package/src/providers/qwenwork/index.js +327 -327
  74. package/src/providers/qwenwork/payload.js +142 -142
  75. package/src/providers/qwenwork/rsa.js +54 -54
  76. package/src/providers/qwenwork/sse.js +268 -268
  77. package/src/providers/qwenwork/stream.js +130 -130
  78. package/src/providers/qwenwork/upstream.js +120 -120
  79. package/src/providers/qwenwork.js +1 -1
  80. package/src/providers/registry.js +74 -66
  81. package/src/providers/workbuddy/chat.js +248 -248
  82. package/src/providers/workbuddy/reshape.js +152 -152
  83. package/src/providers/workbuddy.js +2 -2
  84. package/src/providers/zcode/sse.js +19 -2
  85. package/src/reasoning.js +32 -32
  86. package/src/routes/AGENTS.md +37 -0
  87. package/src/routes/chat/exhausted-handler.js +23 -8
  88. package/src/routes/chat/gateway.js +46 -46
  89. package/src/routes/chat/local-handler.js +3 -1
  90. package/src/routes/chat/relay-pipeline.js +276 -264
  91. package/src/routes/chat/via-route-handler.js +146 -146
  92. package/src/routes/hedge.js +255 -255
  93. package/src/routes/helpers.js +20 -4
  94. package/src/routes/models-route.js +167 -167
  95. package/src/routes/peers.js +273 -273
  96. package/src/routes/stream-hold.js +138 -0
  97. package/src/routes/stream-scan.js +192 -0
  98. package/src/routes/stream.js +386 -393
  99. package/src/runtime/bootstrap.js +45 -45
  100. package/src/runtime/lifecycle-forensics.js +116 -0
  101. package/src/runtime/lifecycle-log.js +23 -0
  102. package/src/runtime/provider-gate.js +34 -33
  103. package/src/runtime/providers-setup.js +165 -165
  104. package/src/server.js +64 -64
  105. package/src/state/schemas/allowlist.js +92 -92
  106. package/src/sync-opencode.js +280 -280
  107. package/src/talk-log.js +226 -0
  108. package/src/timeline.js +5 -2
  109. package/src/transport/index.js +244 -244
  110. package/src/transport/pool.js +56 -56
  111. package/src/transport/retry.js +24 -24
  112. package/src/transport/sse.js +93 -93
  113. package/src/upstream-probe/display.js +52 -52
  114. package/src/upstream-probe/probe.js +49 -49
  115. package/src/upstream-probe/rotate.js +110 -110
  116. package/src/upstream-probe/start.js +45 -45
  117. package/src/upstream.js +289 -289
@@ -1,307 +1,307 @@
1
- import { joinUrl, sleep } from "../base.js";
2
- import { appendEvent } from "../../logs.js";
3
- import { createHash } from "node:crypto";
4
- import { clineHeaders } from "./headers.js";
5
- import { createTransport } from "../../transport/index.js";
6
- import { normalizeProviderId } from "../model-id.js";
7
- import { recordLimit, recordOutput, exactOutputTokens } from "./usage.js";
8
- import { createSdkDispatch } from "../../upstream-engine/sdk/dispatch.js";
9
-
10
- function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
11
-
12
- // 只记哈希标识,不落邮箱 / refreshToken(events.log 可读不可泄)
13
- function acctId(account) {
14
- const seed = String(account?.refreshToken || "");
15
- if (!seed) return "slot-unknown";
16
- return `acct_${createHash("sha256").update(seed).digest("hex").slice(0, 8)}`;
17
- }
18
- // 池子快照:ready/cooling 按「该模型」口径统计(ready=对该模型可用的号数)。
19
- // isReady 由调用方注入 authPool.accountAvailable(a, model),避免与 auth 逻辑分叉。
20
- function poolStat(pool, isReady = () => true) {
21
- let cooling = 0;
22
- let dead = 0;
23
- for (const a of pool || []) {
24
- if (a.dead) dead++;
25
- else if (!isReady(a)) cooling++;
26
- }
27
- const total = (pool || []).length;
28
- return { total, cooling, dead, ready: total - cooling - dead };
29
- }
30
-
31
- // 429 错误体自带权威模型名("...Daily free limit reached on model meta/muse-spark-1.3-contributor")。
32
- function limitModelOf(bodyText) {
33
- // 模型名可含 / . - _ : 字母数字(如 meta/muse-spark-1.3-contributor),
34
- // 遇空格/引号/逗号/句号结尾或 "Try again" 前的空白即停。
35
- const m = String(bodyText || "").match(/on model\s+([A-Za-z0-9/._:-]+?)(?=[\s"',]|\.\s|$)/i);
36
- return m ? m[1] : "";
37
- }
38
-
39
- // stripProviderPrefix 只剥「本供应商」前缀(cline/<裸 id> → 裸 id),非本前缀(含上游自带多段 id
40
- // 如 meta/vendor/x)原样透传,不误削首段(旧实现按段数剥曾是潜在 bug)。
41
- function stripProviderPrefix(model, providerId) {
42
- const s = String(model || "");
43
- const i = s.indexOf("/");
44
- if (i <= 0) return s;
45
- const low = (v) => normalizeProviderId(String(v || "").toLowerCase());
46
- return low(s.slice(0, i)) === low(providerId) ? s.slice(i + 1) : s;
47
- }
48
- function unwrapData(obj) {
49
- if (obj && obj.data && typeof obj.data === "object") {
50
- const d = obj.data;
51
- if (d.choices || d.id || d.usage || d.output) return d;
52
- }
53
- return obj;
54
- }
55
-
56
- async function streamToNonStream(upstream, track = null) {
57
- let content = "";
58
- let reasoning = "";
59
- let finishReason = null;
60
- let model = "";
61
- let id = "";
62
- let usage = null;
63
- for await (const ev of upstream.stream()) {
64
- if (!ev || ev === "[DONE]") continue;
65
- try {
66
- const obj = JSON.parse(ev);
67
- const normalized = unwrapData(obj);
68
- const choice = normalized?.choices?.[0];
69
- if (!choice) continue;
70
- const delta = choice.delta || {};
71
- if (delta.content) content += delta.content;
72
- if (delta.reasoning) reasoning += delta.reasoning;
73
- if (choice.finish_reason) finishReason = choice.finish_reason;
74
- if (normalized.id) id = normalized.id;
75
- if (normalized.model) model = normalized.model;
76
- if (normalized.usage) usage = normalized.usage;
77
- } catch {}
78
- }
79
- const msg = { role: "assistant", content };
80
- if (reasoning) msg.reasoning = reasoning;
81
- if (!content && reasoning) { msg.content = reasoning; msg.reasoning_used_as_content = true; }
82
- if (track) {
83
- const exact = exactOutputTokens(usage);
84
- if (exact) track({ tokens: exact, estimated: false });
85
- else if (content || reasoning) track({ tokens: Math.ceil((content.length + reasoning.length) / 4), estimated: true });
86
- }
87
- return {
88
- id: id || `gen_${Date.now()}`,
89
- object: "chat.completion",
90
- created: Math.floor(Date.now() / 1000),
91
- model: model || "",
92
- choices: [{ index: 0, message: msg, finish_reason: finishReason || "stop", logprobs: null, native_finish_reason: finishReason || "stop" }],
93
- usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
94
- };
95
- }
96
-
97
- export function createChatService({
98
- id,
99
- baseUrl,
100
- chatPath,
101
- fetchImpl,
102
- dispatcher,
103
- authPool,
104
- connectTimeoutMs = 30_000,
105
- } = {}) {
106
- const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
107
- const resolvedChat = chatPath || (String(resolvedBase).includes("/api/v1") ? "/chat/completions" : "/api/v1/chat/completions");
108
- const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
109
- const sdk = createSdkDispatch({ id, providerName: id || "cline" });
110
-
111
- async function clineFetch(body, sessionId, allowSdk = false) {
112
- const token = await authPool.getAccessToken(body?.model);
113
- appendEvent({ type: "cline-account-state", provider: id || "cline", state: "selected", accountId: acctId(authPool.getCurrentAccount()), model: body?.model, pool: poolStat(authPool.getAccounts(), (a) => authPool.accountAvailable(a, body?.model)) });
114
- const headers = clineHeaders(sessionId, token);
115
- const finalUrl = joinUrl(resolvedBase, resolvedChat);
116
- const isStream = body?.stream === true;
117
- // SDK 通道(缺省):仅客户端显式流式;forceStream 聚合与非流式保持原生(避免丢 nonStreamWithContentCheck)。
118
- if (allowSdk && sdk.enabled) {
119
- const r = await sdk.trySdk({ url: finalUrl, body, headers, fetchImpl });
120
- if (r) return r;
121
- }
122
- return transport.request({ url: finalUrl, headers, body, stream: isStream, timeoutMs: connectTimeoutMs });
123
- }
124
- // 记账回调:捕获当前账号哈希,成功输出进当前额度周期
125
- const track = (model) => ({ tokens, estimated }) => recordOutput({ accountId: acctId(authPool.getCurrentAccount()), model, tokens, estimated }).catch(() => {});
126
-
127
- function isLimitHit(status, bodyText) {
128
- if (status === 429) return true;
129
- if (status >= 500 && String(bodyText).includes("empty response content")) return true;
130
- return false;
131
- }
132
-
133
- async function clineFetchWithRetry(body, sessionId, allowSdk = false) {
134
- const maxRetries = 4;
135
- let lastResp = null;
136
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
137
- const resp0 = await authPool.enqueue(() => clineFetch(body, sessionId, allowSdk));
138
- let resp = resp0;
139
- let bodyText = "";
140
- if (resp.status !== 200) {
141
- try { bodyText = await resp.text(); } catch {}
142
- // 错误响应体已在此读走:原生 transport 的 text() 有缓存,而 SDK Response 的 body 一次性,
143
- // 重建以便上层/客户端仍能读到错误详情(如 429 的 INFERENCE_CAP_ERROR)。
144
- try {
145
- resp = new Response(bodyText, { status: resp0.status, statusText: resp0.statusText, headers: new Headers(resp0.headers) });
146
- } catch {}
147
- }
148
- lastResp = resp;
149
- const hit = isLimitHit(resp.status, bodyText);
150
- if (hit) {
151
- const { parseCooldown } = await import("./auth.js");
152
- const cooldownMs = parseCooldown(bodyText, resp.status);
153
- const cur = authPool.getCurrentAccount();
154
- // 免费额度按「账号 × 模型」计:只把该模型挂到这个号上(limits[model]),
155
- // 不动账号级 cooldownUntil、不清 accessToken——同号其它模型照常可用。
156
- authPool.markLimit(cur, body?.model, cooldownMs);
157
- const limitedModel = limitModelOf(bodyText);
158
- if (limitedModel && limitedModel !== body?.model) authPool.markLimit(cur, limitedModel, cooldownMs);
159
- const cycle = await recordLimit({ accountId: acctId(cur), model: body?.model, reason: resp.status === 429 ? "daily_limit" : "empty_response", status: resp.status, cooldownMs });
160
- const pool = authPool.getAccounts();
161
- const ready = (a) => authPool.accountAvailable(a, body?.model);
162
- const hasOther = pool.some(ready);
163
- // 限流是「账号 × 模型」维度:429 只说明当前账号在这个模型上额度用尽,
164
- // 换模型(如 muse-spark → deepseek)往往仍可用,故日志必须点名模型。
165
- appendEvent({
166
- type: "cline-account-state",
167
- provider: id || "cline",
168
- state: hasOther ? "switch" : "pool-exhausted",
169
- accountId: acctId(cur),
170
- model: body?.model,
171
- limitedModel: limitedModel || undefined,
172
- scope: "model",
173
- reason: resp.status === 429 ? "daily_limit" : "empty_response",
174
- status: resp.status,
175
- cooldownMs,
176
- pool: poolStat(pool, ready),
177
- cycleOutputTokens: cycle?.cycleOutputTokens ?? 0,
178
- cycleCount: cycle?.cycleCount ?? 0,
179
- });
180
- if (!hasOther) return resp;
181
- await sleep(500 + Math.floor(Math.random() * 500));
182
- continue;
183
- }
184
- if (resp.ok) return resp;
185
- return resp;
186
- }
187
- return lastResp;
188
- }
189
-
190
- async function nonStreamWithContentCheck(body, sessionId, firstResp) {
191
- const maxAttempts = 3;
192
- let lastData = null;
193
- let resp = firstResp;
194
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
195
- if (!resp) resp = await clineFetchWithRetry(body, sessionId);
196
- if (!resp.ok) {
197
- const errText = await resp.text().catch(() => "");
198
- const hdrs = new Headers(resp.headers);
199
- const errBody = JSON.stringify({ error: { message: errText.slice(0, 500), type: "api_error" } });
200
- return { error: new Response(errBody, { status: resp.status, headers: hdrs }) };
201
- }
202
- const ct = resp.headers.get("content-type") || "";
203
- let normalized = null;
204
- if (ct.includes("text/event-stream")) normalized = await streamToNonStream(resp, track(body?.model));
205
- else {
206
- const raw = await resp.json().catch(() => null);
207
- if (raw) normalized = unwrapData(raw);
208
- }
209
- if (!normalized) return { error: new Response(JSON.stringify({ error: { message: "upstream returned non-SSE body", type: "api_error" } }), { status: 502 }) };
210
- lastData = normalized;
211
- const msg = normalized?.choices?.[0]?.message || {};
212
- const content = String(msg.content || "").trim();
213
- const reasoning = String(msg.reasoning || "").trim();
214
- const isFallback = msg.reasoning_used_as_content === true;
215
- if (content && !isFallback) return { data: normalized };
216
- if (reasoning || isFallback) {
217
- const cur = authPool.getCurrentAccount();
218
- // 空响应是「模型 × 通道」形态问题(deepseek 非流式特性),同样只挡该模型。
219
- authPool.markLimit(cur, body?.model, 30 * 1000);
220
- appendEvent({ type: "cline-account-state", provider: id || "cline", state: "cooldown", accountId: acctId(cur), model: body?.model, reason: "empty_response", cooldownMs: 30000, scope: "model", pool: poolStat(authPool.getAccounts(), (a) => authPool.accountAvailable(a, body?.model)) });
221
- await sleep(300 + Math.floor(Math.random() * 300));
222
- resp = null;
223
- continue;
224
- }
225
- await sleep(300 + Math.floor(Math.random() * 300));
226
- resp = null;
227
- }
228
- return { data: lastData };
229
- }
230
-
231
- async function runChat(body, ring, sourceKey) {
232
- const model = body?.model || "deepseek/deepseek-v4-flash";
233
- const sessionId = genSessionId();
234
- const isStream = body?.stream === true;
235
- const upstreamModel = stripProviderPrefix(model, id);
236
- // token 上限对标 dsh-cline-pass(DEFAULT_MAX_TOKENS=32000):客户端显式 max 优先,
237
- // 缺省给 32000 而非 4096——旧 4096 默认会在长文处 finish=length 拦腰截断(2026-09-23 实测)。
238
- // reasoning_effort 缺省不发(dsh 同款:调用方自选 none→max,不替上游做 high 假设;
239
- // 高推理+小预算双挤是超大上下文秒回超短答的推手之一);max_completion_tokens 双写保留
240
- //(只发 max_tokens 会被部分通道拒,历史教训)。
241
- const tokLimit = body?.max_tokens || body?.max_completion_tokens || 32000;
242
- const upstreamBody = {
243
- model: upstreamModel,
244
- max_tokens: tokLimit,
245
- max_completion_tokens: tokLimit,
246
- session_id: sessionId,
247
- messages: body?.messages || [],
248
- };
249
- const effort = body?.reasoning_effort || body?.reasoningEffort;
250
- if (effort) upstreamBody.reasoning_effort = effort;
251
- // 部分免费通道(deepseek 家族,含 cline-free/deepseek-*)原生非流式不可靠
252
- //(500 empty response):这类模型在内部走 stream+聚合成 JSON,对外仍按请求方
253
- // stream 标志返回(true 直透,false 聚合)。其它模型完全尊重请求方,不强制。
254
- const forceStream = !isStream && String(upstreamModel).toLowerCase().includes("deepseek");
255
- if (isStream || forceStream) upstreamBody.stream = true;
256
- for (const k of ["temperature", "top_p", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"]) {
257
- if (body[k] !== undefined) upstreamBody[k] = body[k];
258
- }
259
- // Gemini 3.x Flash 经 Vertex 的 maxOutputTokens 上限 65536:客户端(PI-Desktop 默认
260
- // 128000)原样透传即 400 invalid argument(网关 SSE 内 detail:
261
- // "Unable to submit request because it has a maxOutputTokens va...")。
262
- // 2026-09-25 直连实测:max 128000 必 400,65536/32000/500 全 200;
263
- // reasoning_effort=high 单独不触发,仅夹上限。只夹 gemini,其他通道不动。
264
- if (/gemini/i.test(String(upstreamModel || "")) && tokLimit > 65536) {
265
- upstreamBody.max_tokens = 65536;
266
- upstreamBody.max_completion_tokens = 65536;
267
- }
268
- for (let netAttempt = 0; netAttempt < 3; netAttempt++) {
269
- try {
270
- const resp = await clineFetchWithRetry(upstreamBody, sessionId, isStream);
271
- if (!resp) throw new Error("empty response");
272
- if (!resp.ok) return resp;
273
- if (isStream) {
274
- // 旁路观测:把本次选中的账号哈希与裸 model 挂到透传 Response 上,供 relay 记账用。
275
- // 转发链路不读这两个字段;记账失败也不影响返回。见 .agents/notes/implemented/feature/2026-09-24-cline-usage-dualwindow.md
276
- try { resp.clineAccountId = acctId(authPool.getCurrentAccount()); resp.clineModel = model; } catch {}
277
- return resp;
278
- }
279
- if (forceStream) {
280
- const ret = await nonStreamWithContentCheck(upstreamBody, sessionId, resp);
281
- if (ret.error) return ret.error;
282
- ret.data.model = model;
283
- const hdrs = new Headers({ "Content-Type": "application/json" });
284
- return new Response(JSON.stringify(ret.data), { status: 200, headers: hdrs });
285
- }
286
- const raw = await resp.json().catch(() => null);
287
- if (!raw || typeof raw !== "object") {
288
- if (!raw) return resp;
289
- return new Response(JSON.stringify({ error: { message: "upstream returned non-JSON body", type: "api_error" } }), { status: 502, headers: { "Content-Type": "application/json" } });
290
- }
291
- const normalized = unwrapData(raw);
292
- normalized.model = model;
293
- // 非流式终点:正文估算记账(上游有 usage 时 streamToNonStream 路径才拿得到精确值;此处 raw 无流)
294
- const m2 = normalized?.choices?.[0]?.message || {};
295
- const outLen2 = String(m2.content || "").length + String(m2.reasoning || "").length;
296
- if (outLen2 > 0) track(model)({ tokens: Math.ceil(outLen2 / 4), estimated: true });
297
- return new Response(JSON.stringify(normalized), { status: 200, headers: { "Content-Type": "application/json" } });
298
- } catch (err) {
299
- if (netAttempt < 2 && String(err?.message || "").toLowerCase().includes("timed out")) { await sleep(300); continue; }
300
- throw err;
301
- }
302
- }
303
- throw new Error("cline chat failed after retries");
304
- }
305
-
306
- return { runChat, streamToNonStream, _clineFetch: clineFetch };
307
- }
1
+ import { joinUrl, sleep } from "../base.js";
2
+ import { appendEvent } from "../../logs.js";
3
+ import { createHash } from "node:crypto";
4
+ import { clineHeaders } from "./headers.js";
5
+ import { createTransport } from "../../transport/index.js";
6
+ import { normalizeProviderId } from "../model-id.js";
7
+ import { recordLimit, recordOutput, exactOutputTokens } from "./usage.js";
8
+ import { createSdkDispatch } from "../../upstream-engine/sdk/dispatch.js";
9
+
10
+ function genSessionId() { return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
11
+
12
+ // 只记哈希标识,不落邮箱 / refreshToken(events.log 可读不可泄)
13
+ function acctId(account) {
14
+ const seed = String(account?.refreshToken || "");
15
+ if (!seed) return "slot-unknown";
16
+ return `acct_${createHash("sha256").update(seed).digest("hex").slice(0, 8)}`;
17
+ }
18
+ // 池子快照:ready/cooling 按「该模型」口径统计(ready=对该模型可用的号数)。
19
+ // isReady 由调用方注入 authPool.accountAvailable(a, model),避免与 auth 逻辑分叉。
20
+ function poolStat(pool, isReady = () => true) {
21
+ let cooling = 0;
22
+ let dead = 0;
23
+ for (const a of pool || []) {
24
+ if (a.dead) dead++;
25
+ else if (!isReady(a)) cooling++;
26
+ }
27
+ const total = (pool || []).length;
28
+ return { total, cooling, dead, ready: total - cooling - dead };
29
+ }
30
+
31
+ // 429 错误体自带权威模型名("...Daily free limit reached on model meta/muse-spark-1.3-contributor")。
32
+ function limitModelOf(bodyText) {
33
+ // 模型名可含 / . - _ : 字母数字(如 meta/muse-spark-1.3-contributor),
34
+ // 遇空格/引号/逗号/句号结尾或 "Try again" 前的空白即停。
35
+ const m = String(bodyText || "").match(/on model\s+([A-Za-z0-9/._:-]+?)(?=[\s"',]|\.\s|$)/i);
36
+ return m ? m[1] : "";
37
+ }
38
+
39
+ // stripProviderPrefix 只剥「本供应商」前缀(cline/<裸 id> → 裸 id),非本前缀(含上游自带多段 id
40
+ // 如 meta/vendor/x)原样透传,不误削首段(旧实现按段数剥曾是潜在 bug)。
41
+ function stripProviderPrefix(model, providerId) {
42
+ const s = String(model || "");
43
+ const i = s.indexOf("/");
44
+ if (i <= 0) return s;
45
+ const low = (v) => normalizeProviderId(String(v || "").toLowerCase());
46
+ return low(s.slice(0, i)) === low(providerId) ? s.slice(i + 1) : s;
47
+ }
48
+ function unwrapData(obj) {
49
+ if (obj && obj.data && typeof obj.data === "object") {
50
+ const d = obj.data;
51
+ if (d.choices || d.id || d.usage || d.output) return d;
52
+ }
53
+ return obj;
54
+ }
55
+
56
+ async function streamToNonStream(upstream, track = null) {
57
+ let content = "";
58
+ let reasoning = "";
59
+ let finishReason = null;
60
+ let model = "";
61
+ let id = "";
62
+ let usage = null;
63
+ for await (const ev of upstream.stream()) {
64
+ if (!ev || ev === "[DONE]") continue;
65
+ try {
66
+ const obj = JSON.parse(ev);
67
+ const normalized = unwrapData(obj);
68
+ const choice = normalized?.choices?.[0];
69
+ if (!choice) continue;
70
+ const delta = choice.delta || {};
71
+ if (delta.content) content += delta.content;
72
+ if (delta.reasoning) reasoning += delta.reasoning;
73
+ if (choice.finish_reason) finishReason = choice.finish_reason;
74
+ if (normalized.id) id = normalized.id;
75
+ if (normalized.model) model = normalized.model;
76
+ if (normalized.usage) usage = normalized.usage;
77
+ } catch {}
78
+ }
79
+ const msg = { role: "assistant", content };
80
+ if (reasoning) msg.reasoning = reasoning;
81
+ if (!content && reasoning) { msg.content = reasoning; msg.reasoning_used_as_content = true; }
82
+ if (track) {
83
+ const exact = exactOutputTokens(usage);
84
+ if (exact) track({ tokens: exact, estimated: false });
85
+ else if (content || reasoning) track({ tokens: Math.ceil((content.length + reasoning.length) / 4), estimated: true });
86
+ }
87
+ return {
88
+ id: id || `gen_${Date.now()}`,
89
+ object: "chat.completion",
90
+ created: Math.floor(Date.now() / 1000),
91
+ model: model || "",
92
+ choices: [{ index: 0, message: msg, finish_reason: finishReason || "stop", logprobs: null, native_finish_reason: finishReason || "stop" }],
93
+ usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
94
+ };
95
+ }
96
+
97
+ export function createChatService({
98
+ id,
99
+ baseUrl,
100
+ chatPath,
101
+ fetchImpl,
102
+ dispatcher,
103
+ authPool,
104
+ connectTimeoutMs = 30_000,
105
+ } = {}) {
106
+ const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
107
+ const resolvedChat = chatPath || (String(resolvedBase).includes("/api/v1") ? "/chat/completions" : "/api/v1/chat/completions");
108
+ const transport = createTransport({ fetchImpl, dispatcher, keepAlive: !!dispatcher, timeoutMs: connectTimeoutMs, retry: {} });
109
+ const sdk = createSdkDispatch({ id, providerName: id || "cline" });
110
+
111
+ async function clineFetch(body, sessionId, allowSdk = false) {
112
+ const token = await authPool.getAccessToken(body?.model);
113
+ appendEvent({ type: "cline-account-state", provider: id || "cline", state: "selected", accountId: acctId(authPool.getCurrentAccount()), model: body?.model, pool: poolStat(authPool.getAccounts(), (a) => authPool.accountAvailable(a, body?.model)) });
114
+ const headers = clineHeaders(sessionId, token);
115
+ const finalUrl = joinUrl(resolvedBase, resolvedChat);
116
+ const isStream = body?.stream === true;
117
+ // SDK 通道(缺省):仅客户端显式流式;forceStream 聚合与非流式保持原生(避免丢 nonStreamWithContentCheck)。
118
+ if (allowSdk && sdk.enabled) {
119
+ const r = await sdk.trySdk({ url: finalUrl, body, headers, fetchImpl });
120
+ if (r) return r;
121
+ }
122
+ return transport.request({ url: finalUrl, headers, body, stream: isStream, timeoutMs: connectTimeoutMs });
123
+ }
124
+ // 记账回调:捕获当前账号哈希,成功输出进当前额度周期
125
+ const track = (model) => ({ tokens, estimated }) => recordOutput({ accountId: acctId(authPool.getCurrentAccount()), model, tokens, estimated }).catch(() => {});
126
+
127
+ function isLimitHit(status, bodyText) {
128
+ if (status === 429) return true;
129
+ if (status >= 500 && String(bodyText).includes("empty response content")) return true;
130
+ return false;
131
+ }
132
+
133
+ async function clineFetchWithRetry(body, sessionId, allowSdk = false) {
134
+ const maxRetries = 4;
135
+ let lastResp = null;
136
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
137
+ const resp0 = await authPool.enqueue(() => clineFetch(body, sessionId, allowSdk));
138
+ let resp = resp0;
139
+ let bodyText = "";
140
+ if (resp.status !== 200) {
141
+ try { bodyText = await resp.text(); } catch {}
142
+ // 错误响应体已在此读走:原生 transport 的 text() 有缓存,而 SDK Response 的 body 一次性,
143
+ // 重建以便上层/客户端仍能读到错误详情(如 429 的 INFERENCE_CAP_ERROR)。
144
+ try {
145
+ resp = new Response(bodyText, { status: resp0.status, statusText: resp0.statusText, headers: new Headers(resp0.headers) });
146
+ } catch {}
147
+ }
148
+ lastResp = resp;
149
+ const hit = isLimitHit(resp.status, bodyText);
150
+ if (hit) {
151
+ const { parseCooldown } = await import("./auth.js");
152
+ const cooldownMs = parseCooldown(bodyText, resp.status);
153
+ const cur = authPool.getCurrentAccount();
154
+ // 免费额度按「账号 × 模型」计:只把该模型挂到这个号上(limits[model]),
155
+ // 不动账号级 cooldownUntil、不清 accessToken——同号其它模型照常可用。
156
+ authPool.markLimit(cur, body?.model, cooldownMs);
157
+ const limitedModel = limitModelOf(bodyText);
158
+ if (limitedModel && limitedModel !== body?.model) authPool.markLimit(cur, limitedModel, cooldownMs);
159
+ const cycle = await recordLimit({ accountId: acctId(cur), model: body?.model, reason: resp.status === 429 ? "daily_limit" : "empty_response", status: resp.status, cooldownMs });
160
+ const pool = authPool.getAccounts();
161
+ const ready = (a) => authPool.accountAvailable(a, body?.model);
162
+ const hasOther = pool.some(ready);
163
+ // 限流是「账号 × 模型」维度:429 只说明当前账号在这个模型上额度用尽,
164
+ // 换模型(如 muse-spark → deepseek)往往仍可用,故日志必须点名模型。
165
+ appendEvent({
166
+ type: "cline-account-state",
167
+ provider: id || "cline",
168
+ state: hasOther ? "switch" : "pool-exhausted",
169
+ accountId: acctId(cur),
170
+ model: body?.model,
171
+ limitedModel: limitedModel || undefined,
172
+ scope: "model",
173
+ reason: resp.status === 429 ? "daily_limit" : "empty_response",
174
+ status: resp.status,
175
+ cooldownMs,
176
+ pool: poolStat(pool, ready),
177
+ cycleOutputTokens: cycle?.cycleOutputTokens ?? 0,
178
+ cycleCount: cycle?.cycleCount ?? 0,
179
+ });
180
+ if (!hasOther) return resp;
181
+ await sleep(500 + Math.floor(Math.random() * 500));
182
+ continue;
183
+ }
184
+ if (resp.ok) return resp;
185
+ return resp;
186
+ }
187
+ return lastResp;
188
+ }
189
+
190
+ async function nonStreamWithContentCheck(body, sessionId, firstResp) {
191
+ const maxAttempts = 3;
192
+ let lastData = null;
193
+ let resp = firstResp;
194
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
195
+ if (!resp) resp = await clineFetchWithRetry(body, sessionId);
196
+ if (!resp.ok) {
197
+ const errText = await resp.text().catch(() => "");
198
+ const hdrs = new Headers(resp.headers);
199
+ const errBody = JSON.stringify({ error: { message: errText.slice(0, 500), type: "api_error" } });
200
+ return { error: new Response(errBody, { status: resp.status, headers: hdrs }) };
201
+ }
202
+ const ct = resp.headers.get("content-type") || "";
203
+ let normalized = null;
204
+ if (ct.includes("text/event-stream")) normalized = await streamToNonStream(resp, track(body?.model));
205
+ else {
206
+ const raw = await resp.json().catch(() => null);
207
+ if (raw) normalized = unwrapData(raw);
208
+ }
209
+ if (!normalized) return { error: new Response(JSON.stringify({ error: { message: "upstream returned non-SSE body", type: "api_error" } }), { status: 502 }) };
210
+ lastData = normalized;
211
+ const msg = normalized?.choices?.[0]?.message || {};
212
+ const content = String(msg.content || "").trim();
213
+ const reasoning = String(msg.reasoning || "").trim();
214
+ const isFallback = msg.reasoning_used_as_content === true;
215
+ if (content && !isFallback) return { data: normalized };
216
+ if (reasoning || isFallback) {
217
+ const cur = authPool.getCurrentAccount();
218
+ // 空响应是「模型 × 通道」形态问题(deepseek 非流式特性),同样只挡该模型。
219
+ authPool.markLimit(cur, body?.model, 30 * 1000);
220
+ appendEvent({ type: "cline-account-state", provider: id || "cline", state: "cooldown", accountId: acctId(cur), model: body?.model, reason: "empty_response", cooldownMs: 30000, scope: "model", pool: poolStat(authPool.getAccounts(), (a) => authPool.accountAvailable(a, body?.model)) });
221
+ await sleep(300 + Math.floor(Math.random() * 300));
222
+ resp = null;
223
+ continue;
224
+ }
225
+ await sleep(300 + Math.floor(Math.random() * 300));
226
+ resp = null;
227
+ }
228
+ return { data: lastData };
229
+ }
230
+
231
+ async function runChat(body, ring, sourceKey) {
232
+ const model = body?.model || "deepseek/deepseek-v4-flash";
233
+ const sessionId = genSessionId();
234
+ const isStream = body?.stream === true;
235
+ const upstreamModel = stripProviderPrefix(model, id);
236
+ // token 上限对标 dsh-cline-pass(DEFAULT_MAX_TOKENS=32000):客户端显式 max 优先,
237
+ // 缺省给 32000 而非 4096——旧 4096 默认会在长文处 finish=length 拦腰截断(2026-09-23 实测)。
238
+ // reasoning_effort 缺省不发(dsh 同款:调用方自选 none→max,不替上游做 high 假设;
239
+ // 高推理+小预算双挤是超大上下文秒回超短答的推手之一);max_completion_tokens 双写保留
240
+ //(只发 max_tokens 会被部分通道拒,历史教训)。
241
+ const tokLimit = body?.max_tokens || body?.max_completion_tokens || 32000;
242
+ const upstreamBody = {
243
+ model: upstreamModel,
244
+ max_tokens: tokLimit,
245
+ max_completion_tokens: tokLimit,
246
+ session_id: sessionId,
247
+ messages: body?.messages || [],
248
+ };
249
+ const effort = body?.reasoning_effort || body?.reasoningEffort;
250
+ if (effort) upstreamBody.reasoning_effort = effort;
251
+ // 部分免费通道(deepseek 家族,含 cline-free/deepseek-*)原生非流式不可靠
252
+ //(500 empty response):这类模型在内部走 stream+聚合成 JSON,对外仍按请求方
253
+ // stream 标志返回(true 直透,false 聚合)。其它模型完全尊重请求方,不强制。
254
+ const forceStream = !isStream && String(upstreamModel).toLowerCase().includes("deepseek");
255
+ if (isStream || forceStream) upstreamBody.stream = true;
256
+ for (const k of ["temperature", "top_p", "tools", "tool_choice", "stop", "presence_penalty", "frequency_penalty", "response_format", "user", "n", "seed"]) {
257
+ if (body[k] !== undefined) upstreamBody[k] = body[k];
258
+ }
259
+ // Gemini 3.x Flash 经 Vertex 的 maxOutputTokens 上限 65536:客户端(PI-Desktop 默认
260
+ // 128000)原样透传即 400 invalid argument(网关 SSE 内 detail:
261
+ // "Unable to submit request because it has a maxOutputTokens va...")。
262
+ // 2026-09-25 直连实测:max 128000 必 400,65536/32000/500 全 200;
263
+ // reasoning_effort=high 单独不触发,仅夹上限。只夹 gemini,其他通道不动。
264
+ if (/gemini/i.test(String(upstreamModel || "")) && tokLimit > 65536) {
265
+ upstreamBody.max_tokens = 65536;
266
+ upstreamBody.max_completion_tokens = 65536;
267
+ }
268
+ for (let netAttempt = 0; netAttempt < 3; netAttempt++) {
269
+ try {
270
+ const resp = await clineFetchWithRetry(upstreamBody, sessionId, isStream);
271
+ if (!resp) throw new Error("empty response");
272
+ if (!resp.ok) return resp;
273
+ if (isStream) {
274
+ // 旁路观测:把本次选中的账号哈希与裸 model 挂到透传 Response 上,供 relay 记账用。
275
+ // 转发链路不读这两个字段;记账失败也不影响返回。见 .agents/notes/implemented/feature/2026-09-24-cline-usage-dualwindow.md
276
+ try { resp.clineAccountId = acctId(authPool.getCurrentAccount()); resp.clineModel = model; } catch {}
277
+ return resp;
278
+ }
279
+ if (forceStream) {
280
+ const ret = await nonStreamWithContentCheck(upstreamBody, sessionId, resp);
281
+ if (ret.error) return ret.error;
282
+ ret.data.model = model;
283
+ const hdrs = new Headers({ "Content-Type": "application/json" });
284
+ return new Response(JSON.stringify(ret.data), { status: 200, headers: hdrs });
285
+ }
286
+ const raw = await resp.json().catch(() => null);
287
+ if (!raw || typeof raw !== "object") {
288
+ if (!raw) return resp;
289
+ return new Response(JSON.stringify({ error: { message: "upstream returned non-JSON body", type: "api_error" } }), { status: 502, headers: { "Content-Type": "application/json" } });
290
+ }
291
+ const normalized = unwrapData(raw);
292
+ normalized.model = model;
293
+ // 非流式终点:正文估算记账(上游有 usage 时 streamToNonStream 路径才拿得到精确值;此处 raw 无流)
294
+ const m2 = normalized?.choices?.[0]?.message || {};
295
+ const outLen2 = String(m2.content || "").length + String(m2.reasoning || "").length;
296
+ if (outLen2 > 0) track(model)({ tokens: Math.ceil(outLen2 / 4), estimated: true });
297
+ return new Response(JSON.stringify(normalized), { status: 200, headers: { "Content-Type": "application/json" } });
298
+ } catch (err) {
299
+ if (netAttempt < 2 && String(err?.message || "").toLowerCase().includes("timed out")) { await sleep(300); continue; }
300
+ throw err;
301
+ }
302
+ }
303
+ throw new Error("cline chat failed after retries");
304
+ }
305
+
306
+ return { runChat, streamToNonStream, _clineFetch: clineFetch };
307
+ }
@@ -1,2 +1,2 @@
1
- // Shim — 保持历史导入路径兼容,真实实现下沉至 ./cline/ 深模块
2
- export * from "./cline/index.js";
1
+ // Shim — 保持历史导入路径兼容,真实实现下沉至 ./cline/ 深模块
2
+ export * from "./cline/index.js";