mslxdff 0.1.231 → 0.1.233

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 +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/index.js +155 -145
  19. package/src/chat-pipeline/policy.js +84 -84
  20. package/src/cli/commands/group.js +266 -266
  21. package/src/cli/commands/model/picks.js +50 -50
  22. package/src/cli/commands/provider/bench-via.js +247 -247
  23. package/src/cli/commands/provider/bench.js +141 -141
  24. package/src/cli/commands/provider/index.js +139 -139
  25. package/src/cli/commands/provider/models.js +149 -149
  26. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  27. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  28. package/src/cli/commands/sync.js +310 -310
  29. package/src/cli/status.js +279 -279
  30. package/src/daemon.js +246 -246
  31. package/src/model-capabilities/enrich.js +86 -86
  32. package/src/model-capabilities/index.js +183 -183
  33. package/src/model-capabilities/parse.js +70 -70
  34. package/src/model-trace.js +10 -2
  35. package/src/models.js +292 -225
  36. package/src/peers.js +345 -345
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/account-store.js +18 -0
  53. package/src/providers/qoder/chat.js +292 -203
  54. package/src/providers/qoder/checkin.js +25 -12
  55. package/src/providers/qoder/index.js +311 -249
  56. package/src/providers/qoder/models.js +64 -2
  57. package/src/providers/qoder/oauth.js +42 -1
  58. package/src/providers/qoder/payload.js +116 -80
  59. package/src/providers/qoder/request.js +7 -2
  60. package/src/providers/qoder/session.js +26 -10
  61. package/src/providers/qoder/sse.js +233 -145
  62. package/src/providers/qoder/sticky.js +5 -2
  63. package/src/providers/qoder/wire-messages.js +242 -0
  64. package/src/providers/qwenwork/account-store.js +133 -133
  65. package/src/providers/qwenwork/constants.js +67 -67
  66. package/src/providers/qwenwork/cosy.js +120 -120
  67. package/src/providers/qwenwork/crypto.js +218 -218
  68. package/src/providers/qwenwork/http.js +20 -20
  69. package/src/providers/qwenwork/index.js +337 -337
  70. package/src/providers/qwenwork/payload.js +144 -144
  71. package/src/providers/qwenwork/rsa.js +54 -54
  72. package/src/providers/qwenwork/sse.js +268 -268
  73. package/src/providers/qwenwork/stream.js +130 -130
  74. package/src/providers/qwenwork/upstream.js +120 -120
  75. package/src/providers/qwenwork.js +1 -1
  76. package/src/providers/raccoon/chat.js +1 -1
  77. package/src/providers/raccoon/const.js +219 -202
  78. package/src/providers/raccoon/credits.js +285 -285
  79. package/src/providers/raccoon/index.js +15 -2
  80. package/src/providers/raccoon/models.js +68 -17
  81. package/src/providers/registry.js +89 -89
  82. package/src/providers/workbuddy/auth.js +16 -0
  83. package/src/providers/workbuddy/chat.js +298 -264
  84. package/src/providers/workbuddy/client-headers.js +148 -0
  85. package/src/providers/workbuddy/desktop-info.js +17 -1
  86. package/src/providers/workbuddy/models.js +8 -20
  87. package/src/providers/workbuddy/reshape.js +152 -152
  88. package/src/providers/workbuddy.js +2 -2
  89. package/src/reasoning.js +32 -32
  90. package/src/routes/chat/broadband-handler.js +80 -80
  91. package/src/routes/chat/gateway.js +46 -46
  92. package/src/routes/chat/relay-pipeline.js +282 -282
  93. package/src/routes/chat/via-route-handler.js +156 -156
  94. package/src/routes/hedge.js +260 -260
  95. package/src/routes/models-route.js +167 -167
  96. package/src/routes/peers.js +373 -373
  97. package/src/routes/stream.js +364 -347
  98. package/src/runtime/bootstrap.js +75 -75
  99. package/src/runtime/keep-warm.js +135 -135
  100. package/src/runtime/provider-gate.js +75 -75
  101. package/src/runtime/providers-setup.js +187 -187
  102. package/src/runtime/raccoon-checkin.js +175 -175
  103. package/src/server.js +85 -85
  104. package/src/state/schemas/allowlist.js +92 -92
  105. package/src/sync-opencode.js +280 -280
  106. package/src/transport/index.js +244 -244
  107. package/src/transport/pool.js +56 -56
  108. package/src/transport/retry.js +24 -24
  109. package/src/transport/sse.js +93 -93
  110. package/src/upstream-probe/display.js +52 -52
  111. package/src/upstream-probe/probe.js +49 -49
  112. package/src/upstream-probe/rotate.js +110 -110
  113. package/src/upstream-probe/start.js +45 -45
  114. package/src/upstream.js +355 -355
  115. package/docs/cli_help_mini.md +0 -148
  116. package/src/providers/AGENTS.md +0 -49
  117. package/src/routes/AGENTS.md +0 -37
@@ -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";