@mars-sea/dsh-commandcode-provider 0.14.5 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -123,13 +123,15 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
123
123
  * subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
124
124
  *
125
125
  * Everything here is synced from official sources — the command-code CLI
126
- * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.86.1), the
126
+ * bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.86.3), the
127
127
  * live model catalog (`GET /provider/v1/models?view=client`, the authority for
128
128
  * capability fields since 1.82.4), and the official plan/pricing/model docs; see
129
129
  * the dsh-commandcode-upstream skill for the extraction procedures. The
130
130
  * 2026-10-21 sync moves the CLI from 1.84.0 to 1.86.1 and picks up the Ornith
131
131
  * 1.5 122B launch pair in the catalog, the ZDR exclusion-set prune and the
132
- * MiniMax M3.1 Flash promo extension. Keeping the snapshot in its own module
132
+ * MiniMax M3.1 Flash promo extension. The follow-up check later that day moves
133
+ * the pinned constant to 1.86.3 (request header only): 1.86.2/1.86.3 leave
134
+ * every snapshot here untouched, so no data in this file moved for them. Keeping the snapshot in its own module
133
135
  * confines those frequent sync diffs here: src/adapter.ts holds only the stable
134
136
  * wire/runtime logic. The read helpers live here too, for the same reason.
135
137
  *
@@ -931,7 +933,7 @@ const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
931
933
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
932
934
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
933
935
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
934
- * Provider-tier. Re-verified at command-code@1.86.1 (2026-10-21) against the
936
+ * Provider-tier. Re-verified at command-code@1.86.3 (2026-10-21) against the
935
937
  * live `?view=client` catalog: 104 ids, still a strict superset chain. Mistral
936
938
  * Large 4
937
939
  * (1.75.0) was the only addition at the 1.77.0 check; the 1.78.0/1.79.0 pair
@@ -3032,10 +3034,10 @@ function recordOrEmpty(value) {
3032
3034
  }
3033
3035
  //#endregion
3034
3036
  //#region src/cost-projection.ts
3035
- const record$3 = (v) => isRecord(v) ? v : void 0;
3037
+ const record$4 = (v) => isRecord(v) ? v : void 0;
3036
3038
  const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
3037
3039
  function tokensOf$1(value) {
3038
- const u = record$3(value);
3040
+ const u = record$4(value);
3039
3041
  if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
3040
3042
  if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
3041
3043
  return {
@@ -3047,14 +3049,14 @@ function tokensOf$1(value) {
3047
3049
  }
3048
3050
  /** The four buckets of an already-parsed record, or undefined when one is missing. */
3049
3051
  function costTokensOf(value) {
3050
- const t = record$3(value);
3052
+ const t = record$4(value);
3051
3053
  if (!t || !COST_TOKEN_KEYS.every((k) => finite(t[k]))) return void 0;
3052
3054
  return t;
3053
3055
  }
3054
3056
  /** Current v2 settlements store the last usage in their stream; v1 stores chunk events. */
3055
3057
  function usageOf(type, data) {
3056
3058
  if (type === "assistant/chunk") {
3057
- const chunk = record$3(data.chunk);
3059
+ const chunk = record$4(data.chunk);
3058
3060
  return chunk?.type === "usage" ? tokensOf$1(chunk.usage) : void 0;
3059
3061
  }
3060
3062
  if (type !== "assistant/message" && type !== "assistant/attempt") return void 0;
@@ -3062,19 +3064,19 @@ function usageOf(type, data) {
3062
3064
  if (direct !== void 0) return direct;
3063
3065
  if (!Array.isArray(data.stream)) return void 0;
3064
3066
  for (let i = data.stream.length - 1; i >= 0; i--) {
3065
- const item = record$3(data.stream[i]);
3066
- const chunk = item?.type === "chunk" ? record$3(item.chunk) : void 0;
3067
+ const item = record$4(data.stream[i]);
3068
+ const chunk = item?.type === "chunk" ? record$4(item.chunk) : void 0;
3067
3069
  if (chunk?.type === "usage") return tokensOf$1(chunk.usage);
3068
3070
  }
3069
3071
  }
3070
3072
  function parseState(value) {
3071
- const s = record$3(value), facts = record$3(s?.facts);
3073
+ const s = record$4(value), facts = record$4(s?.facts);
3072
3074
  if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
3073
3075
  const group = (v) => {
3074
- const g = record$3(v);
3076
+ const g = record$4(v);
3075
3077
  return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && costTokensOf(g.tokens) !== void 0;
3076
3078
  };
3077
- const sel = record$3(s.selection), last = record$3(s.last);
3079
+ const sel = record$4(s.selection), last = record$4(s.last);
3078
3080
  const summaryTokens = facts.summaryTokens === void 0 ? zeroCostTokens() : costTokensOf(facts.summaryTokens);
3079
3081
  if (summaryTokens === void 0 || !(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
3080
3082
  return {
@@ -3131,10 +3133,10 @@ function createCostProjection(table = modelPriceTable()) {
3131
3133
  }
3132
3134
  }),
3133
3135
  apply(state, event) {
3134
- const data = record$3(event.data);
3136
+ const data = record$4(event.data);
3135
3137
  if (!data) return state;
3136
3138
  if (event.type === "request/header") {
3137
- const config = record$3(record$3(data.header)?.config);
3139
+ const config = record$4(record$4(data.header)?.config);
3138
3140
  return {
3139
3141
  ...state,
3140
3142
  selection: config && typeof config.provider === "string" && typeof config.model === "string" ? {
@@ -3226,10 +3228,10 @@ function installCostProjection(ctx) {
3226
3228
  //#endregion
3227
3229
  //#region src/analytics.ts
3228
3230
  /** 在宿主内折叠原始历史;每次请求保留计价时间和上下文,禁止使用已合并的会话成本桶。 */
3229
- const record$2 = (v) => isRecord(v) ? v : void 0;
3231
+ const record$3 = (v) => isRecord(v) ? v : void 0;
3230
3232
  const number = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
3231
3233
  function tokensOf(v) {
3232
- const u = record$2(v);
3234
+ const u = record$3(v);
3233
3235
  if (!u || !number(u.inputTokens) || !number(u.outputTokens)) return void 0;
3234
3236
  if (u.cacheReadTokens !== void 0 && !number(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !number(u.cacheWriteTokens)) return void 0;
3235
3237
  return {
@@ -3259,10 +3261,10 @@ function foldRequests(session, log) {
3259
3261
  if (listed) facts.push(current);
3260
3262
  };
3261
3263
  for (const event of log.events) {
3262
- const d = record$2(event.data);
3264
+ const d = record$3(event.data);
3263
3265
  if (!d) continue;
3264
3266
  if (event.type === "request/header") {
3265
- const config = record$2(record$2(d.header)?.config);
3267
+ const config = record$3(record$3(d.header)?.config);
3266
3268
  provider = typeof config?.provider === "string" ? config.provider : "";
3267
3269
  model = typeof config?.model === "string" ? config.model : "";
3268
3270
  if (current && current.outcome === "unknown") {
@@ -3306,7 +3308,7 @@ function foldRequests(session, log) {
3306
3308
  }
3307
3309
  if (!current) continue;
3308
3310
  if (event.type === "turn/end" && d.turn === turn) {
3309
- const reason = record$2(d.reason)?.kind;
3311
+ const reason = record$3(d.reason)?.kind;
3310
3312
  if (current.outcome === "unknown") {
3311
3313
  if (reason === "error") current.outcome = "failed";
3312
3314
  else if (reason === "aborted") current.outcome = "cancelled";
@@ -3324,11 +3326,11 @@ function foldRequests(session, log) {
3324
3326
  "assistant/attempt",
3325
3327
  "assistant/chunk"
3326
3328
  ].includes(event.type)) continue;
3327
- const chunk = record$2(d.chunk);
3329
+ const chunk = record$3(d.chunk);
3328
3330
  let tokens = tokensOf(d.usage);
3329
3331
  if (event.type === "assistant/chunk" && chunk?.type === "usage") tokens = tokensOf(chunk.usage);
3330
3332
  if (tokens === void 0 && Array.isArray(d.stream)) for (let i = d.stream.length - 1; i >= 0; i--) {
3331
- const item = record$2(d.stream[i]), c = item?.type === "chunk" ? record$2(item.chunk) : void 0;
3333
+ const item = record$3(d.stream[i]), c = item?.type === "chunk" ? record$3(item.chunk) : void 0;
3332
3334
  if (c?.type === "usage") {
3333
3335
  tokens = tokensOf(c.usage);
3334
3336
  if (tokens) break;
@@ -3981,6 +3983,29 @@ const RETRY_MAX_DELAY_MS = 9e5;
3981
3983
  * once per interval per key bounds a probe endpoint that keeps failing.
3982
3984
  */
3983
3985
  const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
3986
+ /**
3987
+ * How long a model-scoped `unavailable` rejection demotes an account.
3988
+ *
3989
+ * `unavailable` (no credits, or a model outside this account's plan) is a fact
3990
+ * about the balance or the model, NOT about the key — a credits-empty account
3991
+ * still serves `:free` models, which is why it must never become a permanent
3992
+ * account mark. But not remembering it at all made EVERY later request pick
3993
+ * the same doomed account first: one wasted round trip per step, and in a pool
3994
+ * where that account was the fallback, the user's error became "insufficient
3995
+ * credits" from an account they never chose (issue #51's remaining half,
3996
+ * live-probed 2026-10-21: a $0.0874 balance rejects every paid model with
3997
+ * `400 insufficient credits` while free models still answer 200).
3998
+ *
3999
+ * So the mark is a PREFERENCE, not a ban, and it is scoped per model to keep
4000
+ * the free-model escape hatch intact. Five minutes is long enough to stop a
4001
+ * single turn's steps from re-hitting the same rejection and short enough that
4002
+ * a top-up takes effect without a restart.
4003
+ */
4004
+ const MODEL_UNAVAILABLE_TTL_MS = 3e5;
4005
+ /** One model-scoped soft mark's Map key (NUL keeps the two halves unambiguous). */
4006
+ function modelUnavailableKey(apiKey, model) {
4007
+ return `${apiKey}\u0000${model}`;
4008
+ }
3984
4009
  /** A labeled, human-readable clock reading for error messages. */
3985
4010
  function clockLabel(ms) {
3986
4011
  return new Date(ms).toLocaleString();
@@ -4051,6 +4076,11 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4051
4076
  states = /* @__PURE__ */ new Map();
4052
4077
  /** 当前网关与密钥的显式探测时间,限制失败探测的频率。 */
4053
4078
  explicitProbes = /* @__PURE__ */ new Map();
4079
+ /**
4080
+ * 模型级 `unavailable` 软标记:`key\0model` → 过期时刻(毫秒)。
4081
+ * 只影响选择顺序,从不参与 {@link accountUsable},所以免费模型仍可用。
4082
+ */
4083
+ modelUnavailable = /* @__PURE__ */ new Map();
4054
4084
  scopedStates = /* @__PURE__ */ new Map();
4055
4085
  constructor(deps) {
4056
4086
  this.deps = deps;
@@ -4062,7 +4092,8 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4062
4092
  if (shared === void 0) {
4063
4093
  shared = {
4064
4094
  states: /* @__PURE__ */ new Map(),
4065
- probes: /* @__PURE__ */ new Map()
4095
+ probes: /* @__PURE__ */ new Map(),
4096
+ modelUnavailable: /* @__PURE__ */ new Map()
4066
4097
  };
4067
4098
  this.scopedStates.set(context.apiBase, shared);
4068
4099
  }
@@ -4075,6 +4106,7 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4075
4106
  });
4076
4107
  scoped.states = shared.states;
4077
4108
  scoped.explicitProbes = shared.probes;
4109
+ scoped.modelUnavailable = shared.modelUnavailable;
4078
4110
  scoped.scopedStates = this.scopedStates;
4079
4111
  return scoped;
4080
4112
  }
@@ -4141,11 +4173,13 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4141
4173
  if (selectActiveAccount(accounts, preferred) !== void 0) return void 0;
4142
4174
  throw allAccountsUnusable(accounts);
4143
4175
  }
4144
- const routed = selectAccountForModel(available, options?.model ?? "", this.deps.modelAccountRules?.());
4176
+ const model = options?.model ?? "";
4177
+ const serveable = this.withoutModelUnavailable(available, model);
4178
+ const routed = selectAccountForModel(serveable, model, this.deps.modelAccountRules?.());
4145
4179
  if (routed !== void 0) return this.pick(routed);
4146
- const chosen = selectActiveAccount(available, preferred);
4180
+ const chosen = selectActiveAccount(serveable, preferred);
4147
4181
  if (chosen !== void 0) {
4148
- const revived = await this.reviveExplicit(available, options?.model ?? "", preferred, chosen);
4182
+ const revived = await this.reviveExplicit(available, model, preferred, chosen);
4149
4183
  return this.pick(revived ?? chosen);
4150
4184
  }
4151
4185
  await this.probeAllMarked(available);
@@ -4198,13 +4232,15 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4198
4232
  * keeps its window unknown on purpose, so a probe can still find out.
4199
4233
  */
4200
4234
  markRejected(apiKey, rejection, resetAtMs) {
4201
- if (rejection === "invalid-credential") this.states.set(apiKey, {
4202
- kind: "disabled",
4203
- cause: "auth",
4204
- reason: "invalid API key (401)",
4205
- until: 0
4206
- });
4207
- else if (rejection === "throttled") this.states.set(apiKey, {
4235
+ if (rejection === "invalid-credential") {
4236
+ this.states.set(apiKey, {
4237
+ kind: "disabled",
4238
+ cause: "auth",
4239
+ reason: "invalid API key (401)",
4240
+ until: 0
4241
+ });
4242
+ this.clearModelUnavailable(apiKey);
4243
+ } else if (rejection === "throttled") this.states.set(apiKey, {
4208
4244
  kind: "unknown",
4209
4245
  cause: "throttle",
4210
4246
  reason: "rate limited (429)",
@@ -4224,6 +4260,46 @@ var CommandCodeAccountPool = class CommandCodeAccountPool {
4224
4260
  });
4225
4261
  }
4226
4262
  /**
4263
+ * Record a model-scoped `unavailable` rejection (no credits, or a model
4264
+ * outside this account's plan) as a SHORT-LIVED PREFERENCE demotion.
4265
+ *
4266
+ * 职责:让同一模型的后续请求先跳过这个账号,而不是每一步都重撞一次
4267
+ * `400 insufficient credits`;同时刻意不写进 `states`,所以
4268
+ * {@link accountUsable} 与 {@link activeAccount} 都不受影响——余额为空的
4269
+ * 账号仍然能服务 `:free` 模型,账号卡片也不会把它标成"不可用"。
4270
+ *
4271
+ * 为什么按时限而非永久:余额/套餐是可以不换密钥就改变的事实(充值、升级),
4272
+ * 而进程内没有任何事件能通知这改变,所以只能靠时钟自行恢复。
4273
+ */
4274
+ markModelUnavailable(apiKey, model) {
4275
+ if (model === "") return;
4276
+ const now = this.now();
4277
+ this.modelUnavailable.set(modelUnavailableKey(apiKey, model), now + MODEL_UNAVAILABLE_TTL_MS);
4278
+ for (const [key, until] of this.modelUnavailable) if (until <= now) this.modelUnavailable.delete(key);
4279
+ }
4280
+ /** Drop every model-scoped mark for one key (a new credential invalidates them). */
4281
+ clearModelUnavailable(apiKey) {
4282
+ const prefix = `${apiKey}\u0000`;
4283
+ for (const key of this.modelUnavailable.keys()) if (key.startsWith(prefix)) this.modelUnavailable.delete(key);
4284
+ }
4285
+ /**
4286
+ * The candidates for THIS model, in preference order: accounts without a live
4287
+ * model-scoped mark first, everything else after them.
4288
+ *
4289
+ * 为什么要"排后"而不是"剔除":软标记只是偏好(见
4290
+ * {@link markModelUnavailable})。全部候选都被标记时必须退回全量,否则唯一
4291
+ * 账号的池子会变成"没有可用账号",把一次余额不足误报成额度耗尽。
4292
+ */
4293
+ withoutModelUnavailable(accounts, model) {
4294
+ if (model === "") return accounts;
4295
+ const now = this.now();
4296
+ const healthy = accounts.filter((account) => {
4297
+ const until = this.modelUnavailable.get(modelUnavailableKey(account.key, model));
4298
+ return until === void 0 || until <= now;
4299
+ });
4300
+ return healthy.length > 0 ? healthy : accounts;
4301
+ }
4302
+ /**
4227
4303
  * One account's key: literal → credential seam → auth file (default slot).
4228
4304
  *
4229
4305
  * Every source is normalized here, at the single point where a slot's key
@@ -4495,6 +4571,18 @@ function parseOutputCeiling(detail) {
4495
4571
  return Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : void 0;
4496
4572
  }
4497
4573
  /**
4574
+ * The gateway's cap on the request's JSON STRUCTURE: `JSON node count
4575
+ * cannot exceed 32768` (issue #96). A structural limit, not a token one —
4576
+ * a session heavy in messages, tool calls and images trips it while well
4577
+ * inside the context window, so it needs its own wording: reported as the
4578
+ * generic refusal, the one sentence naming the actual cap was dropped and
4579
+ * the user had no lever at all; reported as a context overflow, the
4580
+ * harness would compact tokens and resend a structure that still exceeds
4581
+ * the cap. The stated limit is captured because the gateway owns the
4582
+ * number and it can move.
4583
+ */
4584
+ const JSON_NODE_LIMIT_REJECTION = /JSON node count cannot exceed\s+(\d+)/i;
4585
+ /**
4498
4586
  * Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
4499
4587
  * provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
4500
4588
  * `CMD_ZDR_NO_PROVIDERS` in plain-text ones (the official CLI's own
@@ -4635,6 +4723,8 @@ function generateHttpError(status, errText, retryAfterMs, parsed = parseProvider
4635
4723
  return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error ${status}: this request's max_tokens is not valid for this model` + (stated ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code API 返回 ${status}:本次请求的 max_tokens 对该模型不合法` + (stated ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", { status });
4636
4724
  }
4637
4725
  if (status < 500 && isContextOverflowDetail(overflowDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried`, `Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, { status });
4726
+ const nodeLimit = status < 500 ? JSON_NODE_LIMIT_REJECTION.exec(overflowDetail)?.[1] : void 0;
4727
+ if (nodeLimit !== void 0) return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error ${status}: the request exceeds the gateway's JSON node limit (node count cannot exceed ${nodeLimit}) — the cap is on the request's structure, not its token length, so compacting the session usually will not be enough; start a new session, or reduce the messages and images in this one`, `Command Code API 返回 ${status}:请求超过网关的 JSON 节点数上限(节点数不能超过 ${nodeLimit})——限制的是请求结构,不是 token 长度,压缩上下文通常不足以降到该上限以内;请新建会话,或减少本会话中的消息与图片`, { status });
4638
4728
  if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model)`, "Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型", { status: 422 });
4639
4729
  if (status === 401) return bilingual("INVALID_CREDENTIAL", `Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file`, "Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件", { status: 401 });
4640
4730
  if (status === 413) return bilingual("PROVIDER_HTTP_ERROR", "Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry", "Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", { status: 413 });
@@ -6393,7 +6483,7 @@ var GatewayFacts = class {
6393
6483
  * per-request options thunk and an API-key resolver from the plugin entry
6394
6484
  * (src/index.ts), so a settings change reaches the very next request.
6395
6485
  */
6396
- const COMMAND_CODE_CLI_VERSION = "1.86.1";
6486
+ const COMMAND_CODE_CLI_VERSION = "1.86.3";
6397
6487
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
6398
6488
  /**
6399
6489
  * The output budget this bundle asks for, and the ceiling it will never exceed.
@@ -8996,7 +9086,7 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
8996
9086
  * The shared read/validate helpers for the usage report endpoint, prefixed
8997
9087
  * so rejection messages name the offending boundary.
8998
9088
  */
8999
- const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
9089
+ const { reject: reject$2, record: record$2, stringField: stringField$2, numberField: numberField$1, booleanField } = makeBoundaryValidator("commandcode/report result:");
9000
9090
  /**
9001
9091
  * Parse one quota window, or undefined when the frame carries no such window.
9002
9092
  *
@@ -9007,12 +9097,12 @@ const { reject: reject$1, record: record$1, stringField: stringField$1, numberFi
9007
9097
  */
9008
9098
  function windowLimit(value, field) {
9009
9099
  if (value === void 0) return void 0;
9010
- const source = record$1(value, field);
9100
+ const source = record$2(value, field);
9011
9101
  return {
9012
- used: numberField(source, "used", `${field}.used`),
9013
- cap: numberField(source, "cap", `${field}.cap`),
9102
+ used: numberField$1(source, "used", `${field}.used`),
9103
+ cap: numberField$1(source, "cap", `${field}.cap`),
9014
9104
  exceeded: booleanField(source, "exceeded", `${field}.exceeded`),
9015
- resetAt: numberField(source, "resetAt", `${field}.resetAt`)
9105
+ resetAt: numberField$1(source, "resetAt", `${field}.resetAt`)
9016
9106
  };
9017
9107
  }
9018
9108
  /**
@@ -9021,44 +9111,44 @@ function windowLimit(value, field) {
9021
9111
  * malformed frame fails the boundary instead of rendering garbage.
9022
9112
  */
9023
9113
  function parseUsageReport(value) {
9024
- const source = record$1(value, "report");
9114
+ const source = record$2(value, "report");
9025
9115
  const failures = source.failures;
9026
- if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$1("failures");
9116
+ if (!Array.isArray(failures) || failures.some((entry) => typeof entry !== "string")) reject$2("failures");
9027
9117
  const report = { failures };
9028
9118
  if (source.blocked !== void 0) {
9029
9119
  const blocked = source.blocked;
9030
9120
  if (blocked === "invalid-key" || blocked === "service-unavailable" || blocked === "invalid-response" || blocked === "network") report.blocked = blocked;
9031
- else reject$1("blocked");
9121
+ else reject$2("blocked");
9032
9122
  }
9033
9123
  if (source.account !== void 0) {
9034
- const account = record$1(source.account, "account");
9124
+ const account = record$2(source.account, "account");
9035
9125
  report.account = {
9036
- id: stringField$1(account, "id", "account.id"),
9037
- name: stringField$1(account, "name", "account.name"),
9038
- userName: stringField$1(account, "userName", "account.userName")
9126
+ id: stringField$2(account, "id", "account.id"),
9127
+ name: stringField$2(account, "name", "account.name"),
9128
+ userName: stringField$2(account, "userName", "account.userName")
9039
9129
  };
9040
9130
  }
9041
9131
  if (source.usage !== void 0) {
9042
- const usage = record$1(source.usage, "usage");
9132
+ const usage = record$2(source.usage, "usage");
9043
9133
  report.usage = {
9044
- totalCount: numberField(usage, "totalCount", "usage.totalCount"),
9045
- totalCost: numberField(usage, "totalCost", "usage.totalCost"),
9046
- successRate: numberField(usage, "successRate", "usage.successRate"),
9047
- completedCount: numberField(usage, "completedCount", "usage.completedCount"),
9048
- failedCount: numberField(usage, "failedCount", "usage.failedCount"),
9049
- totalTokensIn: numberField(usage, "totalTokensIn", "usage.totalTokensIn"),
9050
- totalTokensOut: numberField(usage, "totalTokensOut", "usage.totalTokensOut"),
9051
- totalCredits: numberField(usage, "totalCredits", "usage.totalCredits"),
9052
- periodBasis: stringField$1(usage, "periodBasis", "usage.periodBasis")
9134
+ totalCount: numberField$1(usage, "totalCount", "usage.totalCount"),
9135
+ totalCost: numberField$1(usage, "totalCost", "usage.totalCost"),
9136
+ successRate: numberField$1(usage, "successRate", "usage.successRate"),
9137
+ completedCount: numberField$1(usage, "completedCount", "usage.completedCount"),
9138
+ failedCount: numberField$1(usage, "failedCount", "usage.failedCount"),
9139
+ totalTokensIn: numberField$1(usage, "totalTokensIn", "usage.totalTokensIn"),
9140
+ totalTokensOut: numberField$1(usage, "totalTokensOut", "usage.totalTokensOut"),
9141
+ totalCredits: numberField$1(usage, "totalCredits", "usage.totalCredits"),
9142
+ periodBasis: stringField$2(usage, "periodBasis", "usage.periodBasis")
9053
9143
  };
9054
- if (usage.periodStart !== void 0) report.usage.periodStart = numberField(usage, "periodStart", "usage.periodStart");
9144
+ if (usage.periodStart !== void 0) report.usage.periodStart = numberField$1(usage, "periodStart", "usage.periodStart");
9055
9145
  }
9056
9146
  if (source.credits !== void 0) {
9057
- const credits = record$1(source.credits, "credits");
9147
+ const credits = record$2(source.credits, "credits");
9058
9148
  const parsed = {
9059
- monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
9060
- purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
9061
- freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
9149
+ monthlyCredits: numberField$1(credits, "monthlyCredits", "credits.monthlyCredits"),
9150
+ purchasedCredits: numberField$1(credits, "purchasedCredits", "credits.purchasedCredits"),
9151
+ freeCredits: numberField$1(credits, "freeCredits", "credits.freeCredits")
9062
9152
  };
9063
9153
  if (credits.monthlyReported !== void 0) parsed.monthlyReported = booleanField(credits, "monthlyReported", "credits.monthlyReported");
9064
9154
  for (const flag of ["purchasedReported", "freeReported"]) if (credits[flag] !== void 0) parsed[flag] = booleanField(credits, flag, `credits.${flag}`);
@@ -9069,38 +9159,38 @@ function parseUsageReport(value) {
9069
9159
  report.credits = parsed;
9070
9160
  }
9071
9161
  if (source.plan !== void 0) {
9072
- const plan = record$1(source.plan, "plan");
9162
+ const plan = record$2(source.plan, "plan");
9073
9163
  const monthly = plan.monthlyCredits;
9074
- if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$1("plan.monthlyCredits");
9164
+ if (monthly !== null && (typeof monthly !== "number" || !Number.isFinite(monthly))) reject$2("plan.monthlyCredits");
9075
9165
  report.plan = {
9076
- planId: stringField$1(plan, "planId", "plan.planId"),
9077
- name: stringField$1(plan, "name", "plan.name"),
9078
- status: stringField$1(plan, "status", "plan.status"),
9166
+ planId: stringField$2(plan, "planId", "plan.planId"),
9167
+ name: stringField$2(plan, "name", "plan.name"),
9168
+ status: stringField$2(plan, "status", "plan.status"),
9079
9169
  monthlyCredits: monthly,
9080
- currentPeriodEnd: numberField(plan, "currentPeriodEnd", "plan.currentPeriodEnd")
9170
+ currentPeriodEnd: numberField$1(plan, "currentPeriodEnd", "plan.currentPeriodEnd")
9081
9171
  };
9082
- if (plan.currentPeriodStart !== void 0) report.plan.currentPeriodStart = numberField(plan, "currentPeriodStart", "plan.currentPeriodStart");
9172
+ if (plan.currentPeriodStart !== void 0) report.plan.currentPeriodStart = numberField$1(plan, "currentPeriodStart", "plan.currentPeriodStart");
9083
9173
  }
9084
9174
  return report;
9085
9175
  }
9086
9176
  /** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
9087
9177
  function parseAccountUsage(value) {
9088
- const source = record$1(value, "account");
9178
+ const source = record$2(value, "account");
9089
9179
  return {
9090
- id: stringField$1(source, "id", "account.id"),
9091
- label: stringField$1(source, "label", "account.label"),
9180
+ id: stringField$2(source, "id", "account.id"),
9181
+ label: stringField$2(source, "label", "account.label"),
9092
9182
  configured: booleanField(source, "configured", "account.configured"),
9093
9183
  active: booleanField(source, "active", "account.active"),
9094
- mark: stringField$1(source, "mark", "account.mark"),
9095
- cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
9184
+ mark: stringField$2(source, "mark", "account.mark"),
9185
+ cooldownUntil: numberField$1(source, "cooldownUntil", "account.cooldownUntil"),
9096
9186
  report: parseUsageReport(source.report)
9097
9187
  };
9098
9188
  }
9099
9189
  /** Parse the wire result into a {@link CommandCodeAccountsReport}. */
9100
9190
  function parseAccountsReport(value) {
9101
- const accounts = record$1(value, "result").accounts;
9191
+ const accounts = record$2(value, "result").accounts;
9102
9192
  if (Array.isArray(accounts)) return { accounts: accounts.map(parseAccountUsage) };
9103
- return reject$1("accounts");
9193
+ return reject$2("accounts");
9104
9194
  }
9105
9195
  /**
9106
9196
  * The strict result codec both halves attach to the descriptor. Hand-rolled:
@@ -9233,36 +9323,36 @@ const REASONS = [
9233
9323
  ];
9234
9324
  /** The shared read/validate helpers, prefixed with the login endpoint so
9235
9325
  * rejection messages name the offending boundary. */
9236
- const { reject, record, stringField } = makeBoundaryValidator("commandcode/login result:");
9326
+ const { reject: reject$1, record: record$1, stringField: stringField$1 } = makeBoundaryValidator("commandcode/login result:");
9237
9327
  /**
9238
9328
  * Parse one untrusted boundary value into a {@link CommandCodeLoginStatus}.
9239
9329
  * Every field is shape-checked so a malformed frame fails the boundary
9240
9330
  * instead of leaking into the page.
9241
9331
  */
9242
9332
  function parseLoginStatus(value) {
9243
- const source = record(value, "status");
9333
+ const source = record$1(value, "status");
9244
9334
  const state = source.state;
9245
9335
  if (state === "idle" || state === "waiting" || state === "success" || state === "failed") {
9246
9336
  const status = { state };
9247
- if (source.authUrl !== void 0) status.authUrl = stringField(source, "authUrl", "authUrl");
9248
- if (source.userName !== void 0) status.userName = stringField(source, "userName", "userName");
9249
- if (source.keyName !== void 0) status.keyName = stringField(source, "keyName", "keyName");
9337
+ if (source.authUrl !== void 0) status.authUrl = stringField$1(source, "authUrl", "authUrl");
9338
+ if (source.userName !== void 0) status.userName = stringField$1(source, "userName", "userName");
9339
+ if (source.keyName !== void 0) status.keyName = stringField$1(source, "keyName", "keyName");
9250
9340
  if (source.reason !== void 0) {
9251
9341
  const reason = source.reason;
9252
9342
  if (typeof reason === "string" && REASONS.includes(reason)) status.reason = reason;
9253
- else reject("reason");
9343
+ else reject$1("reason");
9254
9344
  }
9255
- if (source.message !== void 0) status.message = stringField(source, "message", "message");
9345
+ if (source.message !== void 0) status.message = stringField$1(source, "message", "message");
9256
9346
  return status;
9257
9347
  }
9258
- return reject("state");
9348
+ return reject$1("state");
9259
9349
  }
9260
9350
  /** The strict result codec shared by all three login endpoints. */
9261
9351
  const loginStatusSchema = { parse: parseLoginStatus };
9262
9352
  /** Optional account target; the Host still checks it against saved slots. */
9263
9353
  const loginTargetSchema = { parse(value) {
9264
9354
  if (value === void 0 || typeof value === "string") return value;
9265
- return reject("targetRef");
9355
+ return reject$1("targetRef");
9266
9356
  } };
9267
9357
  /** Build one login invocation descriptor. Only begin accepts an account ref. */
9268
9358
  function loginDescriptor(endpoint, method) {
@@ -9421,6 +9511,31 @@ const ENROLLMENT_DESCRIPTORS = [
9421
9511
  }
9422
9512
  ];
9423
9513
  //#endregion
9514
+ //#region src/service-probe-wire.ts
9515
+ /** Canonical `<namespace>/<method>` endpoint of the probe Remote. */
9516
+ const PROBE_ENDPOINT = "commandcode/probe";
9517
+ const OUTCOMES = [
9518
+ "ok",
9519
+ "http-error",
9520
+ "timeout",
9521
+ "unreachable"
9522
+ ];
9523
+ const { reject, record, stringField, numberField } = makeBoundaryValidator("commandcode/probe result:");
9524
+ /** Strict result codec, shared verbatim by the Host registration and Client mount. */
9525
+ function parseProbeResult(value) {
9526
+ const source = record(value, "result");
9527
+ const outcome = stringField(source, "outcome", "outcome");
9528
+ if (!OUTCOMES.includes(outcome)) reject("outcome");
9529
+ return {
9530
+ outcome,
9531
+ status: numberField(source, "status", "status"),
9532
+ latencyMs: numberField(source, "latencyMs", "latencyMs"),
9533
+ checkedAt: numberField(source, "checkedAt", "checkedAt")
9534
+ };
9535
+ }
9536
+ /** The one invocation descriptor, shared by the Host registration and the Client mount. */
9537
+ const PROBE_DESCRIPTOR = makeRemoteDescriptor(PROBE_ENDPOINT, "probe", `${REMOTE_PACKAGE}#CommandCodeProbeResult`, { parse: parseProbeResult });
9538
+ //#endregion
9424
9539
  //#region src/usage-remote.ts
9425
9540
  /**
9426
9541
  * The Remote receiver: a Cordis service the Gateway resolves by key
@@ -9471,6 +9586,17 @@ var CommandCodeUsageService = class extends TypertRemoteService {
9471
9586
  async prices() {
9472
9587
  return (this.deps.prices ?? modelPriceTable)();
9473
9588
  }
9589
+ /**
9590
+ * One live connectivity probe of the configured API base, run on this Host.
9591
+ * The status page's own data lags by design (Upptime publishes on change or
9592
+ * once a day), so this is the only real-time half of the status row. The
9593
+ * plugin entry wires the probe; a programmatic setup without it gets an
9594
+ * explicit failure, which the row renders as unknown.
9595
+ */
9596
+ async probe() {
9597
+ if (this.deps.probe === void 0) throw new Error("宿主未提供连通性探测");
9598
+ return this.deps.probe();
9599
+ }
9474
9600
  async analytics(query) {
9475
9601
  if (!this.deps.analytics) throw new Error("宿主不支持模型分析,请更新插件");
9476
9602
  return this.deps.analytics(query, this.ctx.invocation?.signal);
@@ -9565,6 +9691,7 @@ function applyUsageRemote(ctx, deps) {
9565
9691
  MODELS_DESCRIPTOR,
9566
9692
  PRICES_DESCRIPTOR,
9567
9693
  ANALYTICS_DESCRIPTOR,
9694
+ PROBE_DESCRIPTOR,
9568
9695
  ...LOGIN_HOST_CONTRIBUTION.invocations,
9569
9696
  ...ENROLLMENT_DESCRIPTORS
9570
9697
  ]
@@ -9572,6 +9699,93 @@ function applyUsageRemote(ctx, deps) {
9572
9699
  remoteCtx.effect(() => () => void unregister(), "dsh-commandcode-provider: usage remote");
9573
9700
  });
9574
9701
  }
9702
+ /**
9703
+ * HTTP statuses Upptime's default monitor accepts as `up` (from
9704
+ * `uptime-monitor`'s `expectedStatusCodes` fallback). Kept in sync with the
9705
+ * page's monitor rather than invented here: a 401 or 404 means the endpoint
9706
+ * answered but is not the surface the official check calls healthy.
9707
+ */
9708
+ const UP_STATUSES = /* @__PURE__ */ new Set([
9709
+ 200,
9710
+ 201,
9711
+ 202,
9712
+ 203,
9713
+ 204,
9714
+ 205,
9715
+ 206,
9716
+ 207,
9717
+ 208,
9718
+ 226,
9719
+ 300,
9720
+ 301,
9721
+ 302,
9722
+ 303,
9723
+ 304,
9724
+ 305,
9725
+ 306,
9726
+ 307,
9727
+ 308
9728
+ ]);
9729
+ /** Turn the response status into the outcome the row renders. */
9730
+ function probeOutcomeForStatus(status) {
9731
+ return UP_STATUSES.has(status) ? "ok" : "http-error";
9732
+ }
9733
+ /**
9734
+ * Probe the configured API base once. Never rejects: an invalid base, a
9735
+ * timeout and a transport failure all resolve to an outcome.
9736
+ *
9737
+ * `redirect: 'manual'` keeps the classification on the FIRST response, exactly
9738
+ * like the monitor's HTTP client; following a redirect would report the final
9739
+ * destination's status as if it were the API's own.
9740
+ */
9741
+ async function probeCommandCode(options) {
9742
+ const now = options.now ?? Date.now;
9743
+ let url;
9744
+ try {
9745
+ url = new URL("/", options.apiBase).toString();
9746
+ } catch {
9747
+ return {
9748
+ outcome: "unreachable",
9749
+ status: 0,
9750
+ latencyMs: 0,
9751
+ checkedAt: now()
9752
+ };
9753
+ }
9754
+ const controller = new AbortController();
9755
+ let timedOut = false;
9756
+ const timer = setTimeout(() => {
9757
+ timedOut = true;
9758
+ controller.abort();
9759
+ }, options.timeoutMs ?? 5e3);
9760
+ const started = Date.now();
9761
+ try {
9762
+ const response = await (options.fetchImpl ?? fetch)(url, {
9763
+ method: "GET",
9764
+ redirect: "manual",
9765
+ signal: controller.signal,
9766
+ headers: { accept: "*/*" }
9767
+ });
9768
+ const latencyMs = Math.max(0, Date.now() - started);
9769
+ try {
9770
+ await response.body?.cancel();
9771
+ } catch {}
9772
+ return {
9773
+ outcome: probeOutcomeForStatus(response.status),
9774
+ status: response.status,
9775
+ latencyMs,
9776
+ checkedAt: now()
9777
+ };
9778
+ } catch {
9779
+ return {
9780
+ outcome: timedOut ? "timeout" : "unreachable",
9781
+ status: 0,
9782
+ latencyMs: 0,
9783
+ checkedAt: now()
9784
+ };
9785
+ } finally {
9786
+ clearTimeout(timer);
9787
+ }
9788
+ }
9575
9789
  //#endregion
9576
9790
  //#region src/login.ts
9577
9791
  /**
@@ -11694,6 +11908,7 @@ function apply(ctx, config) {
11694
11908
  rotateApiKey: async (rejectedKey, rejection, connection, model, rotation) => {
11695
11909
  const accounts = scopedPool(connection);
11696
11910
  if (rejection !== "unavailable") accounts.markRejected(rejectedKey, rejection, rotation?.resetAtMs);
11911
+ else if (model !== void 0) accounts.markModelUnavailable(rejectedKey, model);
11697
11912
  const tried = rotation?.tried?.length ? rotation.tried : [rejectedKey];
11698
11913
  const resolved = await accounts.resolveKey(model === void 0 ? { tried } : {
11699
11914
  tried,
@@ -11848,7 +12063,8 @@ function apply(ctx, config) {
11848
12063
  login: loginFlow,
11849
12064
  enrollment,
11850
12065
  listModels: catalogForEditors,
11851
- analytics: createAnalyticsReader(ctx, adapter)
12066
+ analytics: createAnalyticsReader(ctx, adapter),
12067
+ probe: () => probeCommandCode({ apiBase: options().apiBase })
11852
12068
  });
11853
12069
  const selectionLogger = {
11854
12070
  warn: (message) => {