dsh-codearts 1.2.2 → 1.2.4

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.d.mts CHANGED
@@ -1296,12 +1296,40 @@ interface UsageTotals {
1296
1296
  promptTokens: number;
1297
1297
  completionTokens: number;
1298
1298
  totalTokens: number;
1299
+ /**
1300
+ * 命中上游 prompt 缓存的输入 token(OpenAI 兼容口径)。
1301
+ *
1302
+ * 上游若在 usage 帧里带 `prompt_tokens_details.cached_tokens`(也有平铺成
1303
+ * `cached_tokens` / `cache_tokens` / `prompt_cache_hit_tokens` 的),这里才可能
1304
+ * 是正数;不给就恒为 0。UI 据此决定显不显示「缓存」那一段 —— 不臆造。
1305
+ * 它是 `promptTokens` 的**子集**,不是额外的输入量。
1306
+ */
1307
+ cachedTokens: number;
1308
+ /**
1309
+ * 其中属于「思考」的输出 token(OpenAI 兼容口径:`completion_tokens_details.reasoning_tokens`,
1310
+ * 平铺成顶层 `reasoning_tokens` 的也认)。
1311
+ *
1312
+ * 推理模型(deepseek-v4.1-flash 这类)会把它单列出来,通常照样计费;不单独记就
1313
+ * 只能看见一个偏大的「输出」。它是 `completionTokens` 的**子集**,不是额外的输出量。
1314
+ * 上游不给就恒为 0。
1315
+ */
1316
+ reasoningTokens: number;
1299
1317
  }
1300
1318
  /** 上游 usage 帧里我们关心的字段。 */
1301
1319
  interface RawUsage {
1302
1320
  prompt_tokens?: unknown;
1303
1321
  completion_tokens?: unknown;
1304
1322
  total_tokens?: unknown;
1323
+ /** OpenAI 兼容的嵌套写法:`prompt_tokens_details: { cached_tokens }`。 */
1324
+ prompt_tokens_details?: unknown;
1325
+ /** 平铺写法,几家兼容层各用各的名字,都认。 */
1326
+ cached_tokens?: unknown;
1327
+ cache_tokens?: unknown;
1328
+ prompt_cache_hit_tokens?: unknown;
1329
+ /** 推理模型的嵌套写法:`completion_tokens_details: { reasoning_tokens }`。 */
1330
+ completion_tokens_details?: unknown;
1331
+ /** 平铺的思考 token 别名。 */
1332
+ reasoning_tokens?: unknown;
1305
1333
  }
1306
1334
  interface UsageSnapshot {
1307
1335
  /** 今天的合计。 */
package/lib/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as CodeArtsCatalog, B as codeartsAccountsPath, C as isWelfareClaimedStatus, D as rateLimitFrame, E as prepareChatBody, F as isBenefitBlocked, H as startLogin, I as isBenefitModel, L as CodeArtsAccountStore, M as clearBenefitBlocked, N as creditRatioLabel, O as signedGet, P as filterEnabledModels, R as CodeArtsCredentialStore, S as isTpmLimit, T as pickDailyWelfareCampaign, V as codeartsAuthPath, _ as isIamTokenError, a as SNAP_HOST, b as isParamInvalidFrame, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isConcurrencyCap, h as firstFrameError, i as refreshCatalog, j as FALLBACK_CODEARTS_MODELS, k as upstreamErrorMessage, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isModelNotAccessible, w as maxTokensCeilingFor, x as isQueueableRate, y as isNotMultimodal, z as MANUAL_ACCOUNT_ID } from "./usage-DztQaswb.mjs";
1
+ import { A as upstreamErrorMessage, B as modelUnavailableReason, C as isWelfareClaimedStatus, D as prepareChatBody, E as pickDailyWelfareCampaign, F as filterEnabledModels, G as codeartsAuthPath, H as CodeArtsCredentialStore, I as forgetModelUnavailable, K as startLogin, L as isBenefitBlocked, M as FALLBACK_CODEARTS_MODELS, N as clearBenefitBlocked, O as rateLimitFrame, P as creditRatioLabel, R as isBenefitModel, S as isTpmLimit, T as modelUnavailableReasonText, U as MANUAL_ACCOUNT_ID, V as CodeArtsAccountStore, W as codeartsAccountsPath, _ as isIamTokenError, a as SNAP_HOST, b as isParamInvalidFrame, c as claimDailyWelfare, d as fetchCallerIdentity, f as fetchModels, g as isConcurrencyCap, h as firstFrameError, i as refreshCatalog, j as CodeArtsCatalog, k as signedGet, l as clearQueueStatus, m as fetchWelfareCampaigns, n as codeartsUsagePath, o as canonicalModel, p as fetchQueueStatus, r as discoverModels, s as chatStream, t as CodeArtsUsageTracker, u as degradeTokenCeiling, v as isModelNotAccessible, w as maxTokensCeilingFor, x as isQueueableRate, y as isNotMultimodal, z as markModelUnavailable } from "./usage-DGIuT79_.mjs";
2
2
  import { i as lazyApi, r as createProvider } from "./models-CgKCibpk.mjs";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { randomBytes, timingSafeEqual } from "node:crypto";
@@ -962,6 +962,13 @@ const recentShapes = [];
962
962
  function recentRequestShapes() {
963
963
  return [...recentShapes].reverse();
964
964
  }
965
+ const recentUsages = [];
966
+ /** 探针只留最近 5 帧:够看形状,又不至于把内存堆成日志。 */
967
+ const USAGE_PROBE_LIMIT = 5;
968
+ /** 最近的 usage 原帧,最新在前。 */
969
+ function recentUsageShapes() {
970
+ return [...recentUsages].reverse();
971
+ }
965
972
  function noteRequestShape(raw, model) {
966
973
  let parsed;
967
974
  try {
@@ -1115,6 +1122,7 @@ function createCodeArtsShim(options) {
1115
1122
  reader.releaseLock();
1116
1123
  await result.stream.cancel().catch(() => {});
1117
1124
  logger?.warn(`dsh-codearts: ${prepared.model} 首帧报上游错误(${detail.slice(0, 160)})`);
1125
+ if (isModelNotAccessible(detail)) markModelUnavailable(prepared.model, modelUnavailableReasonText(detail));
1118
1126
  const kind = isIamTokenError(detail) ? "iam_stale" : isModelNotAccessible(detail) ? "not_found" : "client";
1119
1127
  return {
1120
1128
  prepared,
@@ -1405,7 +1413,16 @@ function createCodeArtsShim(options) {
1405
1413
  if (payload === "" || payload === "[DONE]") continue;
1406
1414
  try {
1407
1415
  const parsed = JSON.parse(payload);
1408
- if (parsed.usage !== void 0 && parsed.usage !== null) usage.noteUsage(attribution.accountId, attribution.model, parsed.usage);
1416
+ if (parsed.usage !== void 0 && parsed.usage !== null) {
1417
+ usage.noteUsage(attribution.accountId, attribution.model, parsed.usage);
1418
+ recentUsages.push({
1419
+ at: Date.now(),
1420
+ accountId: attribution.accountId,
1421
+ model: attribution.model,
1422
+ usage: parsed.usage
1423
+ });
1424
+ while (recentUsages.length > USAGE_PROBE_LIMIT) recentUsages.shift();
1425
+ }
1409
1426
  } catch {}
1410
1427
  }
1411
1428
  }
@@ -1437,6 +1454,13 @@ const CODEARTS_ACCOUNTS_PATH = "/api/codearts/accounts";
1437
1454
  const CODEARTS_CLAIM_PATH = "/api/codearts/claim";
1438
1455
  /** POST /api/codearts/refresh-models — 立刻重拉一次模型目录,并清掉「福利今日不可用」标记。 */
1439
1456
  const CODEARTS_REFRESH_MODELS_PATH = "/api/codearts/refresh-models";
1457
+ /**
1458
+ * POST /api/codearts/test-model — 用最小请求实测某个模型能不能调通。
1459
+ *
1460
+ * 存在理由:上游的模型清单会列出调不动的模型(`InferHub.ModelArts.81003.401`),
1461
+ * 刷新救不了;只有真发一次请求才知道。成功会清掉该模型的「异常」标记。
1462
+ */
1463
+ const CODEARTS_TEST_MODEL_PATH = "/api/codearts/test-model";
1440
1464
  /** GET /api/codearts/queue — 上游排队位置(只在等开流期间有值,底部积分条旁边展示)。 */
1441
1465
  const CODEARTS_QUEUE_PATH = "/api/codearts/queue";
1442
1466
  /**
@@ -1463,6 +1487,7 @@ function selectionOf(models, enabled, writable) {
1463
1487
  blocked: m.blocked,
1464
1488
  ratio: m.ratio,
1465
1489
  supportsImages: m.supportsImages,
1490
+ ...m.error === void 0 ? {} : { error: m.error },
1466
1491
  enabled: restricted ? allowed.has(m.id) : true
1467
1492
  })),
1468
1493
  restricted,
@@ -1474,13 +1499,15 @@ function choicesOf(deps) {
1474
1499
  return deps.models().map((m) => {
1475
1500
  const benefit = isBenefitModel(m.id);
1476
1501
  const ratio = creditRatioLabel(m.ratio, benefit);
1502
+ const error = modelUnavailableReason(m.id);
1477
1503
  return {
1478
1504
  id: m.id,
1479
1505
  name: m.name,
1480
1506
  benefit,
1481
1507
  blocked: isBenefitBlocked(m.id),
1482
1508
  ...ratio === void 0 ? {} : { ratio },
1483
- ...m.supportsImages === true ? { supportsImages: true } : {}
1509
+ ...m.supportsImages === true ? { supportsImages: true } : {},
1510
+ ...error === void 0 ? {} : { error }
1484
1511
  };
1485
1512
  });
1486
1513
  }
@@ -1608,7 +1635,9 @@ function usageOf(deps) {
1608
1635
  requests: 0,
1609
1636
  promptTokens: 0,
1610
1637
  completionTokens: 0,
1611
- totalTokens: 0
1638
+ totalTokens: 0,
1639
+ cachedTokens: 0,
1640
+ reasoningTokens: 0
1612
1641
  };
1613
1642
  return {
1614
1643
  today: snapshot?.today ?? noTotals,
@@ -1968,6 +1997,131 @@ function refreshModelsHandler(deps) {
1968
1997
  }
1969
1998
  };
1970
1999
  }
2000
+ /**
2001
+ * 逐账号发一次最小聊天,验证模型**真的能调用**(清单里有 ≠ 能调)。
2002
+ *
2003
+ * 只发一句 ping、max_tokens 16:目的是让上游过一遍权限与路由检查,不是拿内容。
2004
+ * 必须逐账号试 —— 池里的正常请求也是这么轮的;只测第一个号会把「这个号没权限」
2005
+ * 误报成「模型不可用」。
2006
+ */
2007
+ async function probeModel(deps, id, signal) {
2008
+ const ids = await deps.store.ids();
2009
+ if (ids.length === 0) return {
2010
+ ok: false,
2011
+ message: "没有已登录的账号,请先在设置卡片登录。"
2012
+ };
2013
+ const raw = JSON.stringify({
2014
+ model: id,
2015
+ stream: true,
2016
+ max_tokens: 16,
2017
+ messages: [{
2018
+ role: "user",
2019
+ content: "ping"
2020
+ }]
2021
+ });
2022
+ let last = "没有可用的账号";
2023
+ for (const accountId of ids.slice(0, 3)) {
2024
+ if (signal.aborted) break;
2025
+ const credential = await deps.store.resolve(accountId);
2026
+ if (credential === void 0) {
2027
+ last = "凭据不可用,请重新登录";
2028
+ continue;
2029
+ }
2030
+ const result = await chatStream(credential, prepareChatBody(raw), signal);
2031
+ if (!result.ok) {
2032
+ last = upstreamErrorMessage(result.message, result.status);
2033
+ if (result.kind === "iam_stale") await deps.store.forceRefresh(accountId, credential.token).catch(() => void 0);
2034
+ continue;
2035
+ }
2036
+ const reader = result.stream.getReader();
2037
+ try {
2038
+ for (let frame = 0; frame < 6; frame += 1) {
2039
+ const next = await reader.read();
2040
+ if (next.done) return {
2041
+ ok: true,
2042
+ message: "上游已响应",
2043
+ accountId
2044
+ };
2045
+ const text = new TextDecoder().decode(next.value);
2046
+ const error = firstFrameError(text);
2047
+ if (error !== void 0) {
2048
+ const detail = `${error.code}: ${error.message}`.replace(/^: /, "");
2049
+ if (isModelNotAccessible(detail)) markModelUnavailable(id, modelUnavailableReasonText(detail));
2050
+ return {
2051
+ ok: false,
2052
+ message: upstreamErrorMessage(detail),
2053
+ accountId
2054
+ };
2055
+ }
2056
+ if (text.includes("\"choices\"")) return {
2057
+ ok: true,
2058
+ message: "上游正常响应",
2059
+ accountId
2060
+ };
2061
+ }
2062
+ return {
2063
+ ok: true,
2064
+ message: "上游已开流",
2065
+ accountId
2066
+ };
2067
+ } finally {
2068
+ await reader.cancel().catch(() => void 0);
2069
+ }
2070
+ }
2071
+ return {
2072
+ ok: false,
2073
+ message: last
2074
+ };
2075
+ }
2076
+ /**
2077
+ * POST /api/codearts/test-model — 实测一个模型现在能不能调用。
2078
+ *
2079
+ * 成功会清掉它的「异常」标记(测试就是重新验证一次);`81003.401` 会重新记上。
2080
+ * 失败也可能只是撞了并发上限/限流,所以除模型级错误外不动标记。
2081
+ */
2082
+ function testModelHandler(deps) {
2083
+ return async (req, res) => {
2084
+ if (req.method !== "POST") {
2085
+ json(res, 405, { error: "method not allowed" });
2086
+ return;
2087
+ }
2088
+ if (!loopbackRequest(req)) {
2089
+ json(res, 403, { error: "request-not-trusted" });
2090
+ return;
2091
+ }
2092
+ let id = "";
2093
+ try {
2094
+ const body = JSON.parse(await readBody(req));
2095
+ if (typeof body.id === "string") id = body.id.trim();
2096
+ } catch {
2097
+ json(res, 400, { error: "expected {\"id\": string}" });
2098
+ return;
2099
+ }
2100
+ if (id === "") {
2101
+ json(res, 400, { error: "expected {\"id\": string}" });
2102
+ return;
2103
+ }
2104
+ if (!deps.models().some((model) => model.id === id)) {
2105
+ json(res, 404, { error: `unknown model: ${id}` });
2106
+ return;
2107
+ }
2108
+ const started = Date.now();
2109
+ try {
2110
+ const result = await probeModel(deps, id, AbortSignal.timeout(45e3));
2111
+ if (result.ok) forgetModelUnavailable(id);
2112
+ json(res, 200, {
2113
+ ...result,
2114
+ ms: Date.now() - started
2115
+ });
2116
+ } catch (error) {
2117
+ json(res, 200, {
2118
+ ok: false,
2119
+ message: safeMessage(error),
2120
+ ms: Date.now() - started
2121
+ });
2122
+ }
2123
+ };
2124
+ }
1971
2125
  /** GET /api/codearts/queue — 排队位置快照(无跟踪器/无排队时给空数组)。 */
1972
2126
  function queueHandler(deps) {
1973
2127
  return async (req, res) => {
@@ -1993,7 +2147,10 @@ function debugHandler(deps) {
1993
2147
  json(res, 403, { error: "request-not-trusted" });
1994
2148
  return;
1995
2149
  }
1996
- json(res, 200, { requests: deps.recentRequests?.() ?? [] });
2150
+ json(res, 200, {
2151
+ requests: deps.recentRequests?.() ?? [],
2152
+ usages: deps.recentUsages?.() ?? []
2153
+ });
1997
2154
  };
1998
2155
  }
1999
2156
  /** Mount the status / models / token routes. */
@@ -2047,6 +2204,11 @@ function registerCodeArtsStatusRoute(ctx, deps) {
2047
2204
  path: CODEARTS_REFRESH_MODELS_PATH,
2048
2205
  handler: refreshModelsHandler(deps)
2049
2206
  });
2207
+ const disposeTestModel = ctx.webServer.register({
2208
+ kind: "exact",
2209
+ path: CODEARTS_TEST_MODEL_PATH,
2210
+ handler: testModelHandler(deps)
2211
+ });
2050
2212
  const disposeQueue = ctx.webServer.register({
2051
2213
  kind: "exact",
2052
2214
  path: CODEARTS_QUEUE_PATH,
@@ -2072,6 +2234,7 @@ function registerCodeArtsStatusRoute(ctx, deps) {
2072
2234
  disposeAccounts();
2073
2235
  disposeClaim();
2074
2236
  disposeRefresh();
2237
+ disposeTestModel();
2075
2238
  disposeQueue();
2076
2239
  disposeDock();
2077
2240
  disposeDebug();
@@ -2339,6 +2502,7 @@ function apply(ctx, rawConfig) {
2339
2502
  usage: () => usage.snapshot(),
2340
2503
  queue: () => queue.snapshot(),
2341
2504
  recentRequests: () => recentRequestShapes(),
2505
+ recentUsages: () => recentUsageShapes(),
2342
2506
  credits: (signal) => loadCredits(signal),
2343
2507
  refreshCredits: (signal) => {
2344
2508
  credits.invalidate();
@@ -207,6 +207,41 @@ function parseToken(json, dpopJWK, clientId, prev) {
207
207
  dpopPrivateKey: dpopJWK
208
208
  };
209
209
  }
210
+ /**
211
+ * 华为登录后浏览器落回本机回调的落地页。
212
+ *
213
+ * 这一页本身没有任何后续逻辑:`code` 在响应发出的同一时刻就已交给等待队列
214
+ * (见 startLogin 里的 finish),token 交换完全在服务端做,页面关不关都不影响
215
+ * 登录结果。它只是 redirect_uri 必然要渲染的一个落点 —— 华为侧没有「登录成功」
216
+ * 页可停留,成功标志就是这一次 302 到 127.0.0.1。
217
+ *
218
+ * 卡片用 `window.open` 打开授权页,所以这个标签页能自己关掉;用户从卡片的
219
+ * 手动链接打开、或浏览器拦脚本关窗时 close 会失败,1.2s 后把提示换成「可关闭」。
220
+ */
221
+ const CALLBACK_HTML = `<!doctype html>
222
+ <html lang="zh-CN">
223
+ <head>
224
+ <meta charset="utf-8">
225
+ <meta name="viewport" content="width=device-width,initial-scale=1">
226
+ <title>CodeArts 登录</title>
227
+ <style>
228
+ html,body{height:100%}
229
+ body{margin:0;display:flex;align-items:center;justify-content:center;background:#fff;color:#1f2329;
230
+ font:15px/1.6 system-ui,-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
231
+ p{margin:0;padding:24px;text-align:center}
232
+ </style>
233
+ </head>
234
+ <body>
235
+ <p id="msg">正在返回 DeepSeek Harness…</p>
236
+ <script>
237
+ setTimeout(function () { try { window.close() } catch (e) {} }, 300)
238
+ setTimeout(function () {
239
+ var el = document.getElementById('msg')
240
+ if (el) el.textContent = 'CodeArts 登录成功,本页可关闭,请回到 DeepSeek Harness。'
241
+ }, 1200)
242
+ <\/script>
243
+ </body>
244
+ </html>`;
210
245
  async function startLogin(cfg = defaultLoginConfig()) {
211
246
  const ticketId = randomHex(16);
212
247
  const { verifier, challenge, method } = pkce();
@@ -236,8 +271,11 @@ async function startLogin(cfg = defaultLoginConfig()) {
236
271
  }
237
272
  finish(code);
238
273
  function finish(c) {
239
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
240
- res.end("<h3>CodeArts 登录成功,可关闭此页面。</h3>");
274
+ res.writeHead(200, {
275
+ "Content-Type": "text/html; charset=utf-8",
276
+ "Cache-Control": "no-store"
277
+ });
278
+ res.end(CALLBACK_HTML);
241
279
  if (c) for (const w of waiters.splice(0)) w(c);
242
280
  }
243
281
  });
@@ -1068,6 +1106,37 @@ function clearBenefitBlocked() {
1068
1106
  benefitBlocked.clear();
1069
1107
  }
1070
1108
  /**
1109
+ * 今天被上游明确拒绝「模型不存在 / 本账号无权限」的模型(小写 id -> 原因)。
1110
+ *
1111
+ * 判据只有聊天回的 `InferHub.ModelArts.81003.401`。上游的模型发现**照样列出它**
1112
+ * (2026-11-02 实测 `deepseek-v4.1-flash` 两路都在清单里),所以刷新模型列表救不了 ——
1113
+ * 只能等上游修好,或第二天再看。这也是「刷新清单 ≠ 验证可调用」的落点。
1114
+ *
1115
+ * 与福利缺失标记同口径:**只标记,不替用户取消勾选**。多账号下这个错误只有在
1116
+ * 所有号都试过之后才会浮上来,所以按模型记全局(不按账号)。
1117
+ */
1118
+ const unavailableModels = /* @__PURE__ */ new Map();
1119
+ /** 记一次「上游判定这个模型不可用」。 */
1120
+ function markModelUnavailable(id, reason) {
1121
+ const key = id.trim().toLowerCase();
1122
+ if (key !== "") unavailableModels.set(key, {
1123
+ day: localDayKey(),
1124
+ reason
1125
+ });
1126
+ }
1127
+ /** 该模型今天是否已被上游判定不可用;是则返回原因(卡片悬浮显示)。 */
1128
+ function modelUnavailableReason(id) {
1129
+ const key = id.trim().toLowerCase();
1130
+ const entry = unavailableModels.get(key);
1131
+ if (entry === void 0) return void 0;
1132
+ if (entry.day === localDayKey()) return entry.reason;
1133
+ unavailableModels.delete(key);
1134
+ }
1135
+ /** 单独清掉一个模型的不可用标记(手动测试成功后调)。 */
1136
+ function forgetModelUnavailable(id) {
1137
+ unavailableModels.delete(id.trim().toLowerCase());
1138
+ }
1139
+ /**
1071
1140
  * 合并多路来源的模型清单(内置 / 限时福利 / agent-center)。
1072
1141
  *
1073
1142
  * 规则对齐 codearts2api 的 `MergeModels`:
@@ -1276,6 +1345,7 @@ var upstream_exports = /* @__PURE__ */ __exportAll({
1276
1345
  isTpmLimit: () => isTpmLimit,
1277
1346
  isWelfareClaimedStatus: () => isWelfareClaimedStatus,
1278
1347
  maxTokensCeilingFor: () => maxTokensCeilingFor,
1348
+ modelUnavailableReasonText: () => modelUnavailableReasonText,
1279
1349
  pickDailyWelfareCampaign: () => pickDailyWelfareCampaign,
1280
1350
  prepareChatBody: () => prepareChatBody,
1281
1351
  rateLimitFrame: () => rateLimitFrame,
@@ -1671,6 +1741,12 @@ function isModelNotAccessible(message) {
1671
1741
  function isNotMultimodal(message) {
1672
1742
  return message.includes("InferHub.001001020.406") || message.toLowerCase().includes("the request model is not multimodal");
1673
1743
  }
1744
+ /** 记进目录的原因文案(卡片「异常」Tag 的悬浮说明)。 */
1745
+ function modelUnavailableReasonText(detail) {
1746
+ const trimmed = detail.trim();
1747
+ const prefix = "上游判定该模型当前不可用(模型不存在或当前账号没有权限,换号也无效)";
1748
+ return trimmed === "" ? prefix : `${prefix}:${trimmed}`;
1749
+ }
1674
1750
  /**
1675
1751
  * 从**首帧**(已转成 OpenAI 形状)里取出上游错误码与文本;没有错误返回 undefined。
1676
1752
  *
@@ -1815,6 +1891,7 @@ async function chatStream(credential, prepared, signal) {
1815
1891
  if (!resp.ok) {
1816
1892
  const text = await resp.text().catch(() => "");
1817
1893
  if (isBenefitMissing("", text)) markBenefitBlocked(prepared.model);
1894
+ if (isModelNotAccessible(text)) markModelUnavailable(prepared.model, modelUnavailableReasonText(text.slice(0, 200)));
1818
1895
  return {
1819
1896
  ok: false,
1820
1897
  kind: classify(resp.status, text),
@@ -1874,6 +1951,8 @@ async function chatStream(credential, prepared, signal) {
1874
1951
  }
1875
1952
  if (ev.error_code && ev.error_code !== "0" && ev.error_code !== 0) {
1876
1953
  if (isBenefitMissing(String(ev.error_code), String(ev.error_msg ?? ""))) markBenefitBlocked(model);
1954
+ const errorDetail = `${String(ev.error_code)}: ${String(ev.error_msg ?? "upstream error")}`;
1955
+ if (isModelNotAccessible(errorDetail)) markModelUnavailable(model, modelUnavailableReasonText(errorDetail));
1877
1956
  controller.enqueue(openAIData({
1878
1957
  choices: [{
1879
1958
  delta: { content: "" },
@@ -2668,7 +2747,9 @@ function emptyTotals() {
2668
2747
  requests: 0,
2669
2748
  promptTokens: 0,
2670
2749
  completionTokens: 0,
2671
- totalTokens: 0
2750
+ totalTokens: 0,
2751
+ cachedTokens: 0,
2752
+ reasoningTokens: 0
2672
2753
  };
2673
2754
  }
2674
2755
  function addTotals(into, add) {
@@ -2676,6 +2757,8 @@ function addTotals(into, add) {
2676
2757
  into.promptTokens += add.promptTokens ?? 0;
2677
2758
  into.completionTokens += add.completionTokens ?? 0;
2678
2759
  into.totalTokens += add.totalTokens ?? 0;
2760
+ into.cachedTokens += add.cachedTokens ?? 0;
2761
+ into.reasoningTokens += add.reasoningTokens ?? 0;
2679
2762
  }
2680
2763
  function mergeTotals(into, from) {
2681
2764
  addTotals(into, from);
@@ -2689,12 +2772,41 @@ function localDateKey(at = Date.now()) {
2689
2772
  function positiveInt(value) {
2690
2773
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2691
2774
  }
2692
- /** 从上游 usage 帧里取三个计数字段(缺哪个算 0,total 缺失时用 prompt+completion 兜底)。 */
2775
+ /**
2776
+ * 从上游 usage 帧里取缓存命中数。
2777
+ *
2778
+ * 位置见过两种:OpenAI 的 `prompt_tokens_details.cached_tokens`,以及兼容层直接
2779
+ * 平铺在顶层的几种别名。取第一个能读到的。缓存输入必然是输入的子集,所以比
2780
+ * `promptTokens` 还大的脏数据直接钳掉 —— 否则界面上会出现 >100% 的命中率。
2781
+ */
2782
+ function cachedTokensOf(raw, promptTokens) {
2783
+ if (promptTokens <= 0) return 0;
2784
+ const cached = positiveInt(nestedField(raw.prompt_tokens_details, "cached_tokens") ?? raw.cached_tokens ?? raw.cache_tokens ?? raw.prompt_cache_hit_tokens);
2785
+ return Math.min(cached, promptTokens);
2786
+ }
2787
+ /**
2788
+ * 从上游 usage 帧里取思考 token。
2789
+ *
2790
+ * 口径与 {@link cachedTokensOf} 对称:嵌套 `completion_tokens_details.reasoning_tokens`
2791
+ * 优先,顶层 `reasoning_tokens` 兜底,超过输出总量就钳掉(它含在输出里)。
2792
+ */
2793
+ function reasoningTokensOf(raw, completionTokens) {
2794
+ if (completionTokens <= 0) return 0;
2795
+ const reasoning = positiveInt(nestedField(raw.completion_tokens_details, "reasoning_tokens") ?? raw.reasoning_tokens);
2796
+ return Math.min(reasoning, completionTokens);
2797
+ }
2798
+ /** 读 `*_tokens_details` 这类嵌套对象里的一个字段;不是对象(或缺字段)给 undefined。 */
2799
+ function nestedField(details, key) {
2800
+ return typeof details === "object" && details !== null ? details[key] : void 0;
2801
+ }
2802
+ /** 从上游 usage 帧里取计数(缺哪个算 0,total 缺失时用 prompt+completion 兜底)。 */
2693
2803
  function normalizeUsage(raw) {
2694
2804
  if (raw === void 0) return {
2695
2805
  promptTokens: 0,
2696
2806
  completionTokens: 0,
2697
- totalTokens: 0
2807
+ totalTokens: 0,
2808
+ cachedTokens: 0,
2809
+ reasoningTokens: 0
2698
2810
  };
2699
2811
  const promptTokens = positiveInt(raw.prompt_tokens);
2700
2812
  const completionTokens = positiveInt(raw.completion_tokens);
@@ -2702,9 +2814,31 @@ function normalizeUsage(raw) {
2702
2814
  return {
2703
2815
  promptTokens,
2704
2816
  completionTokens,
2705
- totalTokens: total > 0 ? total : promptTokens + completionTokens
2817
+ totalTokens: total > 0 ? total : promptTokens + completionTokens,
2818
+ cachedTokens: cachedTokensOf(raw, promptTokens),
2819
+ reasoningTokens: reasoningTokensOf(raw, completionTokens)
2820
+ };
2821
+ }
2822
+ /** 读一项累计值,缺字段(旧账本没有 cachedTokens / reasoningTokens)就按 0 补齐。 */
2823
+ function normalizeTotals(raw) {
2824
+ return {
2825
+ requests: positiveInt(raw?.requests),
2826
+ promptTokens: positiveInt(raw?.promptTokens),
2827
+ completionTokens: positiveInt(raw?.completionTokens),
2828
+ totalTokens: positiveInt(raw?.totalTokens),
2829
+ cachedTokens: positiveInt(raw?.cachedTokens),
2830
+ reasoningTokens: positiveInt(raw?.reasoningTokens)
2706
2831
  };
2707
2832
  }
2833
+ /** 整张「模型/账号 -> 累计」表补齐字段。 */
2834
+ function normalizeStore(store) {
2835
+ const out = {};
2836
+ for (const [key, totals] of Object.entries(store ?? {})) {
2837
+ if (typeof totals !== "object" || totals === null) continue;
2838
+ out[key] = normalizeTotals(totals);
2839
+ }
2840
+ return out;
2841
+ }
2708
2842
  /**
2709
2843
  * 用量账本。
2710
2844
  *
@@ -2743,8 +2877,8 @@ var CodeArtsUsageTracker = class {
2743
2877
  if (parsed.version !== 1 || typeof parsed.days !== "object" || parsed.days === null) return;
2744
2878
  for (const [date, day] of Object.entries(parsed.days)) {
2745
2879
  if (typeof day !== "object" || day === null) continue;
2746
- const models = day.models ?? {};
2747
- const accounts = day.accounts ?? {};
2880
+ const models = normalizeStore(day.models);
2881
+ const accounts = normalizeStore(day.accounts);
2748
2882
  this.days.set(date, {
2749
2883
  models,
2750
2884
  accounts
@@ -2864,4 +2998,4 @@ function sumOf(store) {
2864
2998
  return out;
2865
2999
  }
2866
3000
  //#endregion
2867
- export { CodeArtsCatalog as A, codeartsAccountsPath as B, isWelfareClaimedStatus as C, rateLimitFrame as D, prepareChatBody as E, isBenefitBlocked as F, startLogin as H, isBenefitModel as I, CodeArtsAccountStore as L, clearBenefitBlocked as M, creditRatioLabel as N, signedGet as O, filterEnabledModels as P, CodeArtsCredentialStore as R, isTpmLimit as S, pickDailyWelfareCampaign as T, codeartsAuthPath as V, isIamTokenError as _, SNAP_HOST as a, isParamInvalidFrame as b, claimDailyWelfare as c, fetchCallerIdentity as d, fetchModels as f, isConcurrencyCap as g, firstFrameError as h, refreshCatalog as i, FALLBACK_CODEARTS_MODELS as j, upstreamErrorMessage as k, clearQueueStatus as l, fetchWelfareCampaigns as m, codeartsUsagePath as n, canonicalModel as o, fetchQueueStatus as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, degradeTokenCeiling as u, isModelNotAccessible as v, maxTokensCeilingFor as w, isQueueableRate as x, isNotMultimodal as y, MANUAL_ACCOUNT_ID as z };
3001
+ export { upstreamErrorMessage as A, modelUnavailableReason as B, isWelfareClaimedStatus as C, prepareChatBody as D, pickDailyWelfareCampaign as E, filterEnabledModels as F, codeartsAuthPath as G, CodeArtsCredentialStore as H, forgetModelUnavailable as I, startLogin as K, isBenefitBlocked as L, FALLBACK_CODEARTS_MODELS as M, clearBenefitBlocked as N, rateLimitFrame as O, creditRatioLabel as P, isBenefitModel as R, isTpmLimit as S, modelUnavailableReasonText as T, MANUAL_ACCOUNT_ID as U, CodeArtsAccountStore as V, codeartsAccountsPath as W, isIamTokenError as _, SNAP_HOST as a, isParamInvalidFrame as b, claimDailyWelfare as c, fetchCallerIdentity as d, fetchModels as f, isConcurrencyCap as g, firstFrameError as h, refreshCatalog as i, CodeArtsCatalog as j, signedGet as k, clearQueueStatus as l, fetchWelfareCampaigns as m, codeartsUsagePath as n, canonicalModel as o, fetchQueueStatus as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, degradeTokenCeiling as u, isModelNotAccessible as v, maxTokensCeilingFor as w, isQueueableRate as x, isNotMultimodal as y, markModelUnavailable as z };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-codearts",
3
3
  "displayName": "DSH CodeArts Connect",
4
4
  "description": "把华为云 CodeArts 接入 DeepSeek Harness —— 内置 OAuth2 登录(PKCE + DPoP),登录后即可在 DSH 对话窗口使用 CodeArts 的模型。",
5
- "version": "1.2.2",
5
+ "version": "1.2.4",
6
6
  "packageManager": "pnpm@11.24.0",
7
7
  "author": "elegant01",
8
8
  "keywords": [