mslxdff 0.1.232 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.232",
3
+ "version": "0.1.233",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auto.js CHANGED
@@ -176,13 +176,21 @@ export function createAutoSelector({
176
176
 
177
177
  async function candidatesFor(requested) {
178
178
  if (!requested) return candidates();
179
+ // 分段计时取证(2026-10-22:真机 candidatesFor 烧 20-26s,源码只 await loadList 与 persistPicks
180
+ // 两口——不测还是猜。>500ms 才留痕,正常毫秒级零噪音)。
181
+ const tL = Date.now();
179
182
  const list = await loadList();
183
+ const loadMs = Date.now() - tL;
180
184
  // 显式指定某模型 = 认可它,自动加入勾选集(仅当它是真实上游 free 模型时,避免垃圾 id 污染)
181
185
  const picks = loadPicks();
186
+ const tP = Date.now();
182
187
  if (list.includes(requested) && !picks.includes(requested) && persistPicks) {
183
188
  await persistPicks([...picks, requested]);
184
189
  }
190
+ const persistMs = Date.now() - tP;
191
+ if (loadMs > 500 || persistMs > 500) console.log(`[auto-slow] candidatesFor model=${requested} loadList=${loadMs}ms persistPicks=${persistMs}ms listSize=${list.length}`);
185
192
  const all = list.includes(requested) ? list : [requested, ...list];
193
+ const tR = Date.now();
186
194
  const others = rankModels(all.filter((id) => id !== requested), lastErrorAt, {
187
195
  now: now(),
188
196
  cooldownMs,
@@ -190,6 +198,8 @@ export function createAutoSelector({
190
198
  latencies,
191
199
  preferred: getPreferredModel({ file: file ?? undefined }),
192
200
  });
201
+ const rankMs = Date.now() - tR;
202
+ if (rankMs > 500) console.log(`[auto-slow] candidatesFor.rank model=${requested} rank=${rankMs}ms`);
193
203
  // 显式指定模型:严格优先,永不因冷却被挤到最后(原设计:A deepseek 失败 → B/D deepseek 并发 → 都失败才 fallback)
194
204
  // 冷却仅影响 auto 的择优,不影响指定模型的“很难被更改”语义
195
205
  return [requested, ...others];
@@ -94,8 +94,18 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
94
94
  // 指定模型 = 死锁单模型:额度耗尽只换组员/组长(同模型换机器),**绝不换模型**。
95
95
  // candidatesFor 的持久化副作用(把显式指定的模型加入勾选集,供日后 auto 用)必须保留——
96
96
  // 见 auto.js:180「显式指定某模型 = 认可它,自动加入勾选集」,历史上删它会丢勾选记录。
97
+ // 但**不 await**(2026-10-22 日志定罪:loadList 拉全量模型目录烧 24s,写盘 persistPicks 仅 0ms)。
98
+ // 此路径 order 已定死 [requested],candidatesFor 返回值根本不被消费,只为触发"加入勾选集"副作用——
99
+ // 为副作用让主请求干等 24s 是自伤。改后台 fire-and-forget:勾选照样持久化,主请求零阻塞。
97
100
  if (auto && requested) {
98
- try { await auto.candidatesFor(requested); } catch {}
101
+ const tCf = performance.now();
102
+ Promise.resolve()
103
+ .then(() => auto.candidatesFor(requested))
104
+ .catch(() => {})
105
+ .finally(() => {
106
+ const ms = Math.round(performance.now() - tCf);
107
+ if (ms > 5000) console.log(`[pipeline-bg] candidatesFor 后台耗时 model=${requested} ms=${ms}(未阻塞主请求)`);
108
+ });
99
109
  }
100
110
  order = [requested];
101
111
  }
@@ -84,6 +84,8 @@ const DECISION_FIELDS = [
84
84
  "latencyMs", "ttfMs", "hedged", "hops", "hasShare", "elapsedMs", "threshold",
85
85
  "stallHits", "maxGapMs", "interrupted", "providers", "via",
86
86
  "retry", "step", "max", "delayMs", "upstream", "account", "pick", "cooled", "queued", "nudged", "retries", "waitedMs", "raiseFrom", "raiseTo",
87
+ // qoder 判决定点日志(2026-10-22):login/额度/排队判决各走各的路径,没有这三项,403 盲飞无从分辨
88
+ "login", "nocapacity", "retryAfter",
87
89
  "heldOpen", "terminalForm", "requestWaitedMs", "budgetMs", "budgetOut",
88
90
  // 终局判决(ADR-0058):取证时第一眼要看的三件——判的哪种、命中哪条判据、是否因此没问组员
89
91
  "kind", "marker", "peersSkipped",
@@ -103,8 +105,10 @@ function decisionKv(data = {}) {
103
105
  return out.join(" ");
104
106
  }
105
107
 
106
- // provider 回显头 → 日志字段(谁上的 / 打哪个站 / 为什么选它 / 是否被冷却 / 是否被判排队):
108
+ // provider 回显头 → 日志字段(谁上的 / 打哪个站 / 为什么选它 / 是否被冷却 / 是否被判排队 / 判决命中哪种):
107
109
  // 单一来源,防各处重复写法漂移;取不到时返回空对象(其它供应商零变化)。
110
+ // login/nocapacity/retryAfter(2026-10-22 补):qoder 判决链曾全程盲飞——403 直出日志里只有状态码,
111
+ // 看不出内部是 login 误伤、queue 漏检还是根本没命中,三轮排查全靠猜(用户实证批评:没多打日志)。
108
112
  export function upstreamEcho(res) {
109
113
  const h = res?.headers;
110
114
  if (typeof h?.get !== "function") return {};
@@ -115,6 +119,10 @@ export function upstreamEcho(res) {
115
119
  pick: pick("x-mslxdff-qoder-account"),
116
120
  cooled: pick("x-mslxdff-qoder-cooldown"),
117
121
  queued: pick("x-mslxdff-qoder-queued"),
122
+ login: pick("x-mslxdff-qoder-login"),
123
+ nocapacity: pick("x-mslxdff-qoder-nocapacity"),
124
+ verdict: pick("x-mslxdff-qoder-verdict"),
125
+ qoderRetry: pick("x-mslxdff-qoder-retry-after"),
118
126
  };
119
127
  }
120
128
 
@@ -128,7 +136,7 @@ export function formatModelTrace({ type, reqId, model, data = {}, request = null
128
136
  else if (type === "alias") detail = `rawModel=${data.rawModel || "-"} requested=${data.requested || model || "-"}`;
129
137
  else if (type === "exhausted-local" || type === "exhausted-all") detail = `last=${data.lastModel || model || "-"} status=${data.lastStatus ?? "-"} order=${(data.order || []).join(" | ")}`;
130
138
  else if (type === "upstream-try") detail = `target=${data.model || model || "-"} attempt=${data.attempt ?? "-"} payload=${kv(data.payload || {})}`;
131
- else if (type === "upstream-done" || type === "upstream-error") detail = `upstream status=${data.status ?? "-"} timing=${data.timing?.totalMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.cooled ? ` cooled=${data.cooled}` : ""}${data.queued ? ` queued=1` : ""} ${safeText(data.message || data.error || "")}`;
139
+ else if (type === "upstream-done" || type === "upstream-error") detail = `upstream status=${data.status ?? "-"} timing=${data.timing?.totalMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.cooled ? ` cooled=${data.cooled}` : ""}${data.queued ? ` queued=1` : ""}${data.login ? ` login=1` : ""}${data.nocapacity ? ` nocapacity=1` : ""}${data.verdict ? ` verdict=${data.verdict}` : ""}${data.qoderRetry ? ` retry=${data.qoderRetry}s` : ""} ${safeText(data.message || data.error || "")}`;
132
140
  else if (type === "empty-turn-retry") detail = `空转重试:retry ${data.retry ?? "-"}/${data.max ?? "-"}${data.step != null ? ` step=${data.step}` : ""} delay=${data.delayMs ?? "-"}ms${data.waitedMs ? ` waited=${data.waitedMs}ms` : ""}${data.requestWaitedMs != null ? ` 请求级累计=${data.requestWaitedMs}ms/预算${data.budgetMs ?? "-"}ms` : ""}${data.raiseTo ? ` max_tokens ${data.raiseFrom}→${data.raiseTo}` : ""}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.nudged ? ` nudged=1` : ""} reason=empty turn${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
133
141
  else if (type === "empty-turn-recovered") detail = `空转重试成功:第 ${data.retries ?? "-"}/${data.max ?? "-"} 次重拉拿到真实输出,本轮共白等 ${data.waitedMs ?? "-"}ms${data.upstream ? ` via=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}`;
134
142
  else if (type === "empty-turn-exhausted") detail = `空转重试已用尽:重试 ${data.retries ?? 0}/${data.max ?? "-"} 次仍零输出(累计等待 ${data.waitedMs ?? 0}ms${data.budgetOut ? `,请求级等待预算 ${data.budgetMs ?? "-"}ms 用尽` : ""}),交回换候选${data.heldOpen ? "(响应仍开着,换过去的正文还能送达)" : "(响应已封口,重拉只为省额度)"}${data.upstream ? ` after=${data.upstream}` : ""}${data.account ? ` account=${data.account}` : ""}${data.pick ? ` pick=${data.pick}` : ""}${data.reason ? ` :: ${safeText(data.reason, 120)}` : ""}`;
package/src/models.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
2
2
  const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
3
3
  const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
4
- import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { mkdirSync, writeFileSync, readFileSync } from "node:fs";
5
5
  import { dirname } from "node:path";
6
6
  import { compatFetch } from "./compat.js";
7
7
 
@@ -28,7 +28,21 @@ export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, re
28
28
  if (providers?.length) {
29
29
  let aggregate = null;
30
30
  let aggFetchedAt = 0;
31
+ let aggInflight = null;
32
+ let aggTimer = null;
31
33
  let lastAllowlistKey = "";
34
+ // 冷启动预热:读上一次 persist 的磁盘目录当首值(真机取证:全目录拉取烧 20-26s,
35
+ // 首个 candidatesFor 冷等全程)。aggFetchedAt 用磁盘记录的真实时间;get 对
36
+ // 「有旧值」的情形一律 stale-while-revalidate(立即返回 + 后台刷),主请求零等待。
37
+ if (cacheFile) {
38
+ try {
39
+ const raw = JSON.parse(readFileSync(cacheFile, "utf8"));
40
+ if (Array.isArray(raw?.data)) {
41
+ aggregate = { object: "list", data: raw.data };
42
+ aggFetchedAt = Number(raw.cachedAt) || 0;
43
+ }
44
+ } catch { /* 无缓存/坏档:按冷处理 */ }
45
+ }
32
46
  async function currentAllowlistKey() {
33
47
  try {
34
48
  const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
@@ -42,6 +56,15 @@ export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, re
42
56
  }
43
57
  async function aggLoad() {
44
58
  const all = [];
59
+ // 单供应商拉取失败时沿用旧缓存里它的段(取证教训:此前失败=整段消失,
60
+ // 且 2h TTL 内无人再拉(startAutoRefresh 曾是空函数),目录会长期缺一大块)。
61
+ const prevByProvider = new Map();
62
+ for (const m of aggregate?.data || []) {
63
+ const pid = typeof m?.id === "string" ? m.id.split("/")[0] : "";
64
+ if (!pid) continue;
65
+ if (!prevByProvider.has(pid)) prevByProvider.set(pid, []);
66
+ prevByProvider.get(pid).push(m);
67
+ }
45
68
  for (const p of providers) {
46
69
  try {
47
70
  let list = (await p.listModels?.()) ?? [];
@@ -66,7 +89,9 @@ export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, re
66
89
  } catch {}
67
90
  all.push(...list);
68
91
  } catch {
69
- // 单供应商取数失败不拖垮整体
92
+ // 单供应商取数失败不拖垮整体:有旧数据则保留旧数据
93
+ const prev = prevByProvider.get(p.id);
94
+ if (prev?.length) all.push(...prev);
70
95
  }
71
96
  }
72
97
  aggregate = { object: "list", data: all };
@@ -76,42 +101,84 @@ export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, re
76
101
  if (cacheFile) persistModels(aggregate, cacheFile);
77
102
  return aggregate;
78
103
  }
104
+ // 对当前 aggregate(含磁盘 stale 值)做白名单二次过滤后返回;过滤失败回原值。
105
+ async function aggSnapshot() {
106
+ try {
107
+ const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
108
+ const { splitModelId } = await import("./providers/model-id.js");
109
+ const filteredData = aggregate.data.filter((m) => {
110
+ if (!m || !m.id) return false;
111
+ const { provider, raw } = splitModelId(m.id, providers.map((x) => x.id));
112
+ const allowed = loadProviderAllowedModels(provider);
113
+ const allowAny = loadProviderAllowAnyModels(provider);
114
+ if (!allowed.length && !allowAny) return false;
115
+ if (!allowed.length) return true;
116
+ return allowed.includes(String(raw || "").trim());
117
+ });
118
+ if (filteredData.length !== aggregate.data.length) return { ...aggregate, data: filteredData };
119
+ return aggregate;
120
+ } catch {
121
+ return aggregate;
122
+ }
123
+ }
124
+ function aggRefreshInBackground() {
125
+ void aggLoadShared().then(() => { aggFetchedAt = Date.now(); }).catch(() => {});
126
+ }
79
127
  async function get() {
80
128
  const now = Date.now();
81
- if (aggregate && now - aggFetchedAt < ttlMs) {
129
+ const fresh = aggregate && now - aggFetchedAt < ttlMs;
130
+ if (fresh) {
82
131
  // 热更新白名单:allowlist 变化时,即使命中 TTL 也要重载(否则 clear 后仍返回旧过滤结果)
83
132
  try {
84
133
  const curKey = await currentAllowlistKey();
85
134
  if (curKey !== lastAllowlistKey) {
86
- const out = await aggLoad();
135
+ const out = await aggLoadShared();
87
136
  aggFetchedAt = Date.now();
88
137
  return out;
89
138
  }
90
139
  } catch {}
91
- // 否则尝试在缓存上二次过滤(处理 allowlist 从空变非空等未触发重载的场景)
92
- try {
93
- const { loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("./state.js");
94
- const { splitModelId } = await import("./providers/model-id.js");
95
- const filteredData = aggregate.data.filter((m) => {
96
- if (!m || !m.id) return false;
97
- const { provider, raw } = splitModelId(m.id, providers.map((x) => x.id));
98
- const allowed = loadProviderAllowedModels(provider);
99
- const allowAny = loadProviderAllowAnyModels(provider);
100
- if (!allowed.length && !allowAny) return false;
101
- if (!allowed.length) return true;
102
- return allowed.includes(String(raw || "").trim());
103
- });
104
- if (filteredData.length !== aggregate.data.length) return { ...aggregate, data: filteredData };
105
- return aggregate;
106
- } catch {
107
- return aggregate;
108
- }
140
+ return aggSnapshot();
109
141
  }
110
- const out = await aggLoad();
142
+ // 有旧值(含冷启动磁盘 stale):立即返回 + 后台刷新,主请求零等待。
143
+ // 取证教训:全目录拉取 20-26s,此前冷路径同步阻塞整个首请求。
144
+ if (aggregate) {
145
+ aggRefreshInBackground();
146
+ return aggSnapshot();
147
+ }
148
+ // 完全无值(首次冷跑):只能等这一趟。
149
+ const out = await aggLoadShared();
111
150
  aggFetchedAt = Date.now();
112
151
  return out;
113
152
  }
114
- return { get, startAutoRefresh: () => {}, stopAutoRefresh: () => {} };
153
+ // 并发去重:aggLoad 要遍历全部供应商拉目录(真机实测 20-26s),
154
+ // 无去重时赛马/后台任务并发触发就各烧各的(真机日志实证同窗口重复 3 次)。
155
+ function aggLoadShared() {
156
+ if (aggInflight) return aggInflight;
157
+ aggInflight = (async () => {
158
+ try {
159
+ return await aggLoad();
160
+ } catch (e) {
161
+ // 失败保旧:有旧聚合就回退旧值,绝不让半次刷新把目录清空
162
+ if (aggregate) return aggregate;
163
+ throw e;
164
+ } finally {
165
+ aggInflight = null;
166
+ }
167
+ })();
168
+ return aggInflight;
169
+ }
170
+ function aggStartAutoRefresh(intervalMs = refreshMs) {
171
+ if (aggTimer) return aggStopAutoRefresh;
172
+ aggTimer = setInterval(() => {
173
+ void aggLoadShared().then(() => { aggFetchedAt = Date.now(); }).catch(() => {});
174
+ }, intervalMs);
175
+ aggTimer.unref?.();
176
+ return aggStopAutoRefresh;
177
+ }
178
+ function aggStopAutoRefresh() {
179
+ if (aggTimer) { clearInterval(aggTimer); aggTimer = null; }
180
+ }
181
+ return { get, startAutoRefresh: aggStartAutoRefresh, stopAutoRefresh: aggStopAutoRefresh };
115
182
  }
116
183
  let cache = null;
117
184
  let fetchedAt = 0;
@@ -96,6 +96,24 @@ export async function saveQoderAccount({ uid, deviceToken, refreshToken = "", re
96
96
  return { file: fp, accounts: st.keys.length, updated: st.updated > 0 };
97
97
  }
98
98
 
99
+ // 刷新成功后按 uid 原位更新令牌对(deviceToken 时效短,105 过期即刷)。
100
+ // auth 文档保留原 region/name;state 行按 uid 覆盖(persistAccounts 命中既有 uid 行即替换)。
101
+ export async function updateQoderAccountTokens({ uid, deviceToken, refreshToken, file } = {}) {
102
+ if (!uid || !deviceToken) throw new Error("updateQoderAccountTokens: 缺少 uid/deviceToken");
103
+ const docs = listAccountDocs();
104
+ const found = docs.find((d) => String(d.uid) === String(uid));
105
+ const a = found?.doc?.auth || {};
106
+ const acc = found?.doc?.account || {};
107
+ writeAccountFile({
108
+ uid, deviceToken, refreshToken,
109
+ region: a.region || "global",
110
+ name: acc.name || "", email: acc.email || "", userType: acc.userType || "personal_standard",
111
+ organizationId: acc.organizationId || "", organizationName: acc.organizationName || "",
112
+ });
113
+ await persistAccounts([{ uid, deviceToken, refreshToken, region: a.region || "global", name: acc.name || "", domain: "qoder.com" }], file);
114
+ return { updated: Boolean(found) };
115
+ }
116
+
99
117
  // 重建 provider 的行级 auth:由 keys[i] blob + auths[i] 还原 deviceToken。
100
118
  export function accountFromBlob(blob) {
101
119
  try {
@@ -2,12 +2,12 @@
2
2
  // false → 聚合回 JSON。装配走 request.js,帧格式走 sse.js,两条管线共享。
3
3
  import { compatFetch, timeoutSignal } from "../../compat.js";
4
4
  import { mapModel } from "./payload.js";
5
- import { effortFromBody, isReasoningEnabled } from "../../thinking-effort.js";
5
+ import { effortFromBody, effortLevelFromBody, isReasoningEnabled } from "../../thinking-effort.js";
6
6
  import { normalizeRegion } from "./constants.js";
7
7
  import { buildUpstreamRequest } from "./request.js";
8
8
  import { reshapeQoderStream } from "./stream.js";
9
9
  import { aggregateQoderStream, toCompletionJson } from "./aggregate.js";
10
- import { errorStatus, extractDelta } from "./sse.js";
10
+ import { errorStatus, extractDelta, findQuotaSignal, findLoginSignal, findQueueSignal, retryAfterFrom } from "./sse.js";
11
11
 
12
12
  function hostOf(url) {
13
13
  try { return new URL(String(url)).host; } catch { return "-"; }
@@ -25,21 +25,39 @@ function mapUpstreamError(status, detail) {
25
25
  // 又不会把首字节延迟拖到不可控。超窗未得判决即放弃(退化为无冷却的流内 error,见 SPEC 已知上限)。
26
26
  const PEEK_MAX_FRAMES = 8;
27
27
 
28
+ // 判决旗标(统一出口):登录过期/额度/排队(含无容量子型)+ retryAfter 秒数。
29
+ // 非 200 直出与 200 信封裹身两条路径共用同一解析口,防两处漂移。
30
+ function verdictFlagsFrom(objOrText) {
31
+ const v = { login: false, quota: false, queued: false, noCapacity: false, retryAfter: 0 };
32
+ if (findLoginSignal(objOrText)) v.login = true;
33
+ else if (findQuotaSignal(objOrText)) v.quota = true;
34
+ else {
35
+ const q = findQueueSignal(objOrText);
36
+ if (q) { v.queued = true; v.noCapacity = q.noCapacity === true; }
37
+ }
38
+ const ra = retryAfterFrom(typeof objOrText === "string" ? objOrText : JSON.stringify(objOrText ?? ""));
39
+ if (ra.retryAfter) v.retryAfter = ra.retryAfter;
40
+ return v;
41
+ }
42
+
28
43
  // 预读上游首帧只为取「流内判决」:qoder 把限流/鉴权判决裹在 HTTP 200 的 SSE 信封里
29
44
  //(statusCodeValue:403 + serviceAvailable:false),upRes.status 恒 200,判决只有读完帧才知道。
30
45
  // 不预读则状态码永远出不来流式路径 → 门面不冷却该号(ADR-0036 的粘号会把重试继续粘在它上面)。
31
46
  // 返回 { res, status, frames }:res 是把预读帧回灌后的等价上游响应,frames 交回灌用。
32
47
  // 任何异常都降级为「无判决」(status=0):排障手段不得变成故障源。
33
48
  async function peekEnvelopeVerdict(upRes, cdbg) {
34
- if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, frames: [] };
49
+ if (!upRes?.body) return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] };
35
50
  let reader;
36
- try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, frames: [] }; }
51
+ try { reader = upRes.body.getReader(); } catch { return { res: upRes, status: 0, quota: false, queued: false, noCapacity: false, retryAfter: 0, login: false, frames: [] }; }
37
52
  const dec = new TextDecoder();
38
53
  const frames = [];
39
54
  let text = "";
40
55
  let status = 0;
41
56
  let quota = false;
42
57
  let queued = false;
58
+ let noCapacity = false;
59
+ let retryAfter = 0;
60
+ let login = false;
43
61
  let decided = false;
44
62
  try {
45
63
  for (let i = 0; i < PEEK_MAX_FRAMES && !decided; i++) {
@@ -53,14 +71,22 @@ async function peekEnvelopeVerdict(upRes, cdbg) {
53
71
  const p = line.slice(5).trim();
54
72
  if (!p || p === "[DONE]") continue;
55
73
  const d = extractDelta(p);
56
- if (d.err) { status = Number(d.err.status) || 0; quota = d.err.kind === "quota"; queued = d.err.queued === true; decided = true; break; }
74
+ if (d.err) {
75
+ status = Number(d.err.status) || 0;
76
+ quota = d.err.kind === "quota";
77
+ queued = d.err.queued === true;
78
+ noCapacity = d.err.noCapacity === true;
79
+ retryAfter = Number(d.err.retryAfter) || 0;
80
+ login = d.err.loginExpired === true;
81
+ decided = true; break;
82
+ }
57
83
  if (d.content || d.reasoning || d.toolCalls || d.usageIn || d.usageOut) { decided = true; break; }
58
84
  }
59
85
  }
60
86
  } catch (e) {
61
87
  cdbg(`[peek-throw] ${String(e?.message || e).slice(0, 160)}`);
62
88
  }
63
- cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued}`);
89
+ cdbg(`[peek] frames=${frames.length} verdict=${status || "none(正常流)"} quota=${quota} queued=${queued} noCapacity=${noCapacity} ra=${retryAfter} login=${login}`);
64
90
  // 回灌:已读帧先按原样吐出,再接续同一个 reader 的剩余部分
65
91
  const body = new ReadableStream({
66
92
  start(ctrl) {
@@ -77,7 +103,7 @@ async function peekEnvelopeVerdict(upRes, cdbg) {
77
103
  })();
78
104
  },
79
105
  });
80
- return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, frames };
106
+ return { res: new Response(body, { status: upRes.status, headers: upRes.headers }), status, quota, queued, noCapacity, retryAfter, login, frames };
81
107
  }
82
108
  export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModelMeta } = {}) {
83
109
  if (!fetchImpl) fetchImpl = compatFetch;
@@ -114,6 +140,31 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
114
140
  try { res.headers.set("x-mslxdff-qoder-queued", "1"); } catch {}
115
141
  return res;
116
142
  };
143
+ // 无容量旗标:10605 且 queueCount:0 + serviceAvailable:false = 整池共享同一 p3 队列、
144
+ // 换号救不了,只能等 Retry-After。门面据此**不换号轮询**,带 429+Retry-After 如实透给客户端。
145
+ const withNoCapacity = (res, retryAfter) => {
146
+ try {
147
+ res.headers.set("x-mslxdff-qoder-nocapacity", "1");
148
+ if (retryAfter) {
149
+ res.headers.set("x-mslxdff-qoder-retry-after", String(retryAfter));
150
+ res.headers.set("Retry-After", String(retryAfter)); // 对客户端的标准交代(relay 层原样转发响应头)
151
+ }
152
+ } catch {}
153
+ return res;
154
+ };
155
+ // 登录过期标记(内部交接,不落日志):门面据此走「刷新令牌→原地重发」,不当普通坏号冷却轮询。
156
+ const withLogin = (res) => {
157
+ try { res.headers.set("x-mslxdff-qoder-login", "1"); } catch {}
158
+ return res;
159
+ };
160
+ // 判决结果钉头(2026-10-22 用户实证「没多打日志→排查全靠猜」后补):verdict 取值
161
+ // login/quota/queue/queue-nocapacity/none,由 model-trace.upstreamEcho 落进模型日志。
162
+ // 关键在 none 也要显式钉——「压根没命中」与「命中但走别的分支」在 403 盲视图里必须分得开。
163
+ const verdictName = (v) => (v.login ? "login" : v.quota ? "quota" : v.queued ? (v.noCapacity ? "queue-nocapacity" : "queue") : "none");
164
+ const withVerdict = (res, v) => {
165
+ try { res.headers.set("x-mslxdff-qoder-verdict", verdictName(v)); } catch {}
166
+ return res;
167
+ };
117
168
 
118
169
  async function runChat(body, sess, region = "global", pick = "") {
119
170
  const stream = body?.stream !== false;
@@ -122,14 +173,20 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
122
173
  const tools = Array.isArray(body?.tools) && body.tools.length ? body.tools : null;
123
174
  const messages = Array.isArray(body?.messages) ? body.messages : [];
124
175
  const maxTokens = Number(body?.max_tokens) || 0;
125
- // is_reasoning 由模型目录供数(index 注入 getModelMeta,只读 peekModels 快照缓存,chat 路径恒 0 上游调用);
126
- // 冷缓存/缺失一律降级 false(= 旧行为,不阻断对话)—— P0:推理标志此前从未出门。
176
+ // is_reasoning 与能力元数据(窗口/档位)由模型目录一次供数(index 注入 getModelMeta,
177
+ // 只读 peekModels 快照缓存,chat 路径恒 0 额外上游调用;preheat 负责灌缓存)。
178
+ // 冷缓存/缺失一律降级:isReasoning=false、无 meta(= 旧行为,不阻断对话)。
179
+ let meta = null;
127
180
  let isReasoning = false;
128
181
  try {
129
- if (getModelMeta) isReasoning = (await getModelMeta(sess, normalizeRegion(region), mapModel(model)))?.is_reasoning === true;
182
+ if (getModelMeta) {
183
+ meta = (await getModelMeta(sess, normalizeRegion(region), mapModel(model))) || null;
184
+ isReasoning = meta?.is_reasoning === true;
185
+ }
130
186
  } catch {}
131
187
  // 客户端显式档位优先于目录推断(grill-decisions Q3):关断→false、其余非空→true;无档位→沿用目录结论。
132
188
  isReasoning = isReasoningEnabled(effortFromBody(body), isReasoning);
189
+ const effortLevel = effortLevelFromBody(body); // 真档位串(low/medium/high…),透传给装配口对齐模型档位
133
190
 
134
191
  let upRes; let reqUrl = "";
135
192
  const echo = (res) => withEcho(res, reqUrl, region, pick);
@@ -137,7 +194,7 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
137
194
  const cdbg = (...a) => { if (process.env.QODER_DEBUG_STREAM === "1") console.log("[qoder-chat]", ...a); };
138
195
  let upMs = 0;
139
196
  try {
140
- const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning });
197
+ const req = buildUpstreamRequest({ sess, region: normalizeRegion(region), model, messages, tools, maxTokens, isReasoning, meta, effort: effortLevel });
141
198
  reqUrl = req.url;
142
199
  cdbg(`[req] url=${hostOf(req.url)} region=${normalizeRegion(region)} model=${model} msgs=${messages.length} tools=${tools?.length ?? 0} maxTokens=${maxTokens} stream=${stream} bodyBytes=${req.bodyStr?.length ?? 0}`);
143
200
  const t0 = Date.now();
@@ -159,7 +216,15 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
159
216
  if (upRes.status !== 200) {
160
217
  const detail = await upRes.text().catch(() => "");
161
218
  cdbg(`[res-non200] status=${upRes.status} body=${detail.slice(0, 300)}`);
162
- const e = mapUpstreamError(upRes.status, detail);
219
+ // 真 HTTP 403 也可能直出 10605 排队/110 额度/105 登录(不只裹 200 信封)。
220
+ // 旧版一律 mapUpstreamError(403)→auth→冷却换号,把「整池共享的 p3 无容量」当死号轮询白烧。
221
+ // 先解析 body 定旗标:无容量 → 不换号、带 Retry-After 如实透;额度/登录 → 交门面对应处理。
222
+ const vf = verdictFlagsFrom(detail);
223
+ cdbg(`[verdict] status=${upRes.status} login=${vf.login} quota=${vf.quota} queued=${vf.queued} noCapacity=${vf.noCapacity} ra=${vf.retryAfter} bodyLen=${detail.length} body=${detail.slice(0, 300)}`);
224
+ const e = vf.login ? { kind: "auth", status: 401, detail }
225
+ : vf.quota ? { kind: "quota", status: 429, detail }
226
+ : vf.queued ? { kind: "upstream", status: 429, detail }
227
+ : mapUpstreamError(upRes.status, detail);
163
228
  if (stream) {
164
229
  // 首包错误也走流内 error 事件(与旧 reshapeStream catch 语义一致:200 + event:error + DONE)
165
230
  const enc = new TextEncoder();
@@ -171,21 +236,36 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
171
236
  },
172
237
  });
173
238
  // 对外仍是 200(契约不变),真实状态码经回显头交门面决定是否冷却
174
- return withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
239
+ let shaped = withUpstreamStatus(echo(new Response(s, { status: 200, headers: { "Content-Type": "text/event-stream" } })), upRes.status);
240
+ shaped = withVerdict(shaped, vf);
241
+ if (vf.quota) shaped = withQuota(shaped);
242
+ if (vf.login) shaped = withLogin(shaped);
243
+ if (vf.queued) shaped = vf.noCapacity ? withNoCapacity(shaped, vf.retryAfter) : withQueued(shaped);
244
+ return shaped;
175
245
  }
176
- return withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
246
+ let out = withUpstreamStatus(echo(errRes(errorStatus(e), e.detail, e.kind)), upRes.status);
247
+ out = withVerdict(out, vf);
248
+ if (vf.quota) out = withQuota(out);
249
+ if (vf.login) out = withLogin(out);
250
+ if (vf.queued) out = vf.noCapacity ? withNoCapacity(out, vf.retryAfter) : withQueued(out);
251
+ return out;
177
252
  }
178
253
  if (stream) {
179
254
  // 预读首帧取流内判决(qoder 把 403/429 裹在 200 的信封里),预读帧原样回灌给整形器。
180
255
  // 判决状态码经 withUpstreamStatus 出门面 → index.js 据此 ring.onError 冷却该号 →
181
256
  // 粘号选择器下次因 isCooling 为真而换号。对外仍恒 200(契约不变)。
182
257
  const peek = await peekEnvelopeVerdict(upRes, cdbg);
258
+ const pvf = { login: peek.login, quota: peek.quota, queued: peek.queued, noCapacity: peek.noCapacity };
183
259
  let shaped = echo(reshapeQoderStream(peek.res, { model, chatId, prefetched: peek.frames }));
184
260
  // 只在真有判决时挂状态码:正常流不写这个头(index.js 靠它区分"要不要冷却")
185
261
  if (peek.status) shaped = withUpstreamStatus(shaped, peek.status);
262
+ // verdict 钉头同理只钉有判决的流——正常流不写,避免成功请求日志被 verdict=none 刷屏
263
+ if (peek.status) shaped = withVerdict(shaped, pvf);
186
264
  if (peek.quota) shaped = withQuota(shaped);
187
- // 排队旗标(内部交接):门面据此走队列档冷却并在同一请求内换号,别把排队号送回客户端
188
- if (peek.queued) shaped = withQueued(shaped);
265
+ // 排队旗标(内部交接):真排队(有人等位)→ 门面换号碰别的号;无容量 → 门面不换号透 Retry-After
266
+ if (peek.queued) shaped = peek.noCapacity ? withNoCapacity(shaped, peek.retryAfter) : withQueued(shaped);
267
+ // 登录过期旗标:门面据此刷新令牌原地重发(不冷却换号轮询整池过期号)
268
+ if (peek.login) shaped = withLogin(shaped);
189
269
  return shaped;
190
270
  }
191
271
  try {
@@ -193,9 +273,18 @@ export function createChatService({ id = "qoder", fetchImpl, timeoutMs, getModel
193
273
  const out = toCompletionJson({ model, chatId, ...agg });
194
274
  return echo(new Response(JSON.stringify(out), { status: 200, headers: { "Content-Type": "application/json" } }));
195
275
  } catch (e) {
196
- const out = echo(errRes(errorStatus(e), String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
197
- const marked = e?.kind === "quota" ? withQuota(out) : out;
198
- return e?.queued ? withQueued(marked) : marked;
276
+ // 聚合出口的判决折态(2026-10-22 真机实锤 req=mvj6u7wj:HTTP 恒 200、403 裹在 SSE 信封
277
+ // statusCodeValue 里,extractDelta 的 err.status=403 原样 errRes 出去 → nocapacity 头挂了、
278
+ // 状态却还是 403。无容量档必须与非 200 直出路径同口径折 429,否则门面 break 后
279
+ // lastRes 透给客户端仍是 403,Retry-After 语义全丢)。
280
+ const cvf = { login: e?.loginExpired === true, quota: e?.kind === "quota", queued: e?.queued === true, noCapacity: e?.noCapacity === true };
281
+ const aggStatus = cvf.noCapacity ? 429 : errorStatus(e);
282
+ let out = echo(errRes(aggStatus, String(e?.detail || e?.message || e).slice(0, 300), e?.kind || "upstream_error"));
283
+ out = withVerdict(out, cvf);
284
+ if (cvf.noCapacity && e?.retryAfter) out = withNoCapacity(out, e.retryAfter);
285
+ let marked = cvf.quota ? withQuota(out) : out;
286
+ if (cvf.queued && !cvf.noCapacity) marked = withQueued(marked);
287
+ return cvf.login ? withLogin(marked) : marked;
199
288
  }
200
289
  }
201
290
 
@@ -54,18 +54,27 @@ export function dailyCheckinClaim({ deviceToken, region, fetchImpl = compatFetch
54
54
  return call(fetchImpl, region, EP_CHECKIN_CLAIM, { method: "POST", deviceToken, body: {}, timeoutMs });
55
55
  }
56
56
 
57
- // 余额:/api/v2/quota/usage → userQuota{total,used,remaining,unit} + isQuotaExceeded
57
+ // 余额:/api/v2/quota/usage → 基础池 userQuota + 奖励池 addOnQuota(签到/活动领的额度)。
58
+ // ⚠ 取证教训(2026-10-22):旧版只读 userQuota,漏了 addOnQuota —— Free 号 userQuota 恒 0/0,
59
+ // 但领过签到奖励的号 addOnQuota.remaining=100 仍有额度,被误报成「已耗尽」。
60
+ // exhausted 以接口综合判断 isQuotaExceeded 为权威(两池都用完才 true),不自行推断。
58
61
  export async function fetchQuota({ deviceToken, region, fetchImpl = compatFetch, timeoutMs } = {}) {
59
62
  const r = await call(fetchImpl, region, EP_QUOTA, { deviceToken, timeoutMs });
60
63
  if (r.status !== 200) return { ok: false, status: r.status, error: r.error || r.text.slice(0, 160) };
61
- const q = r.json?.userQuota || {};
64
+ const num = (v) => Number(v) || 0;
65
+ const q = r.json?.userQuota;
66
+ const add = r.json?.addOnQuota;
67
+ const base = { total: num(q?.total), used: num(q?.used), remaining: num(q?.remaining) };
68
+ const addOn = add ? { total: num(add.total), used: num(add.used), remaining: num(add.remaining) } : null;
62
69
  return {
63
70
  ok: true,
64
- total: Number(q.total) || 0,
65
- used: Number(q.used) || 0,
66
- remaining: Number(q.remaining) || 0,
67
- unit: String(q.unit || "credits"),
71
+ total: base.total + (addOn?.total ?? 0),
72
+ used: base.used + (addOn?.used ?? 0),
73
+ remaining: base.remaining + (addOn?.remaining ?? 0),
74
+ unit: String(q?.unit || add?.unit || "credits"),
68
75
  exhausted: Boolean(r.json?.isQuotaExceeded),
76
+ base,
77
+ addOn,
69
78
  };
70
79
  }
71
80
 
@@ -102,15 +111,19 @@ export async function runCheckin({ deviceToken, region, fetchImpl = compatFetch,
102
111
  }
103
112
  if (st.status === 401) { r.message = `device token 失效(HTTP 401):请重新 mslxdff -provider qoder login`; return r; }
104
113
 
105
- // 干跑:只报状态,不发任何 claim 请求
106
- if (probeOnly && st.status === 200 && st.json?.status) {
114
+ // 取证教训(2026-10-22):上游 cn 老入口 daily-check-in 已 DISABLED,但字段还在返回。
115
+ // 旧代码把 DISABLED 当活跃态去 claim,拿到 409 就报「今日已领取」直接 return ——
116
+ // 永远走不到 campaigns 兜底,天天错过真积分活动(act-20261020-993,每日 10:00 UTC+8 刷新)。
117
+ // 修法:DISABLED 唯一豁免——不碰老入口直接落 campaigns;其余状态保持原行为(试领,409=已领)。
118
+ const legacy = st.status === 200 ? String(st.json?.status || "") : "";
119
+ const legacyActive = st.status === 200 && st.json?.status && legacy !== "DISABLED";
120
+ if (probeOnly && legacyActive) {
107
121
  r.ok = true;
108
- r.status = st.json.status === "CLAIMED" ? "already" : "claimable";
109
- r.message = `干跑:status=${st.json.status} 每日奖励 ${r.reward}`;
122
+ r.status = legacy === "CLAIMED" ? "already" : "claimable";
123
+ r.message = `干跑:status=${legacy} 每日奖励 ${r.reward}`;
110
124
  return r;
111
125
  }
112
- // 主流程:daily-check-in(国内站)
113
- if (st.status === 200 && st.json?.status) {
126
+ if (legacyActive) {
114
127
  const claim = await dailyCheckinClaim({ deviceToken, region: reg, fetchImpl, timeoutMs });
115
128
  if (claim.status === 409) { r.ok = true; r.status = "already"; r.message = "今日已领取"; return r; }
116
129
  if (claim.status === 200) {