dsh-codearts 0.2.1 → 0.4.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.
@@ -1163,15 +1163,20 @@ function signRequest(req, body, cred) {
1163
1163
  var upstream_exports = /* @__PURE__ */ __exportAll({
1164
1164
  CONCURRENCY_CAP_CODE: () => CONCURRENCY_CAP_CODE,
1165
1165
  MAX_TOKENS_CEILING: () => MAX_TOKENS_CEILING,
1166
+ PARAM_INVALID_CODE: () => PARAM_INVALID_CODE,
1166
1167
  SNAP_HOST: () => SNAP_HOST,
1167
1168
  canonicalModel: () => canonicalModel,
1168
1169
  chatStream: () => chatStream,
1169
1170
  claimBenefit: () => claimBenefit,
1171
+ degradeTokenCeiling: () => degradeTokenCeiling,
1172
+ estimateTokens: () => estimateTokens,
1170
1173
  fetchCallerIdentity: () => fetchCallerIdentity,
1171
1174
  fetchModels: () => fetchModels,
1172
1175
  isConcurrencyCap: () => isConcurrencyCap,
1176
+ isParamInvalidFrame: () => isParamInvalidFrame,
1173
1177
  maxTokensCeilingFor: () => maxTokensCeilingFor,
1174
- prepareChatBody: () => prepareChatBody
1178
+ prepareChatBody: () => prepareChatBody,
1179
+ signedGet: () => signedGet
1175
1180
  });
1176
1181
  const SNAP_HOST = "https://snap-access.cn-north-4.myhuaweicloud.com";
1177
1182
  const EP_CHAT = "/api/v2/chat/completions";
@@ -1213,14 +1218,20 @@ function randomTraceId() {
1213
1218
  }
1214
1219
  /** 上游对 max_tokens 的服务端硬上限(2026-09-30 直连二分实测)。 */
1215
1220
  const MAX_TOKENS_CEILING = 65536;
1221
+ /** token 估算的余量:混排误差 + 上游自己注入的会话头,宁可少给也别整轮失败。 */
1222
+ const PROMPT_ESTIMATE_MARGIN = 2048;
1223
+ /** 短于此的生成长度就没有实际意义了,此时直接报具体错误而不是硬挤。 */
1224
+ const MIN_USEFUL_OUTPUT_TOKENS = 1024;
1216
1225
  /**
1217
1226
  * 实测过的、**按模型**的 max_tokens 上限(小写 id → 上限)。
1218
1227
  *
1219
1228
  * 这个上限是模型相关的,不能用一个全局值:
1220
1229
  * - 免费(限时福利)DeepSeek 系列:> 65536 会被流内 InferHub.001001005.400
1221
1230
  * "The request param is invalid" 拒掉,而福利网关标称的是 393216 / 384000;
1222
- * - GLM-5.2:实测 131072 可用(2026-09-30 逐值直连验证:8000/65536/131072 均正常,
1223
- * 再往上会先撞上并发上限而测不出来)。
1231
+ * - GLM-5.2:2026-09-30 逐值直连验证过 8000/65536/131072 均正常,但
1232
+ * **2026-10-10 复测 131072 已经被拒**(同一个 InferHub.001001005.400),
1233
+ * 65536 仍然正常。也就是说这条上限由服务端控制、会漂,目录里留着的旧实测值
1234
+ * 不会报错,只会让该模型的每一轮在流的第一帧就失败 —— 所以复测要按日期记。
1224
1235
  *
1225
1236
  * 没测过的模型一律退回 `MAX_TOKENS_CEILING`(保守但可用),而不是照抄标称值 ——
1226
1237
  * DSH 会把 catalog 的 maxTokens 原样当 max_tokens 发出去,照抄标称值等于让请求失败。
@@ -1232,11 +1243,63 @@ const MEASURED_MAX_TOKENS = {
1232
1243
  "deepseek-v4-pro-0813": 65536,
1233
1244
  "deepseek-v4.1-flash": 65536,
1234
1245
  "glm-5.3-flash": 65536,
1235
- "glm-5.2": 131072
1246
+ "glm-5.2": 65536
1236
1247
  };
1237
- /** 该模型可用的 max_tokens 上限(没实测过的退回全局保守值)。 */
1248
+ /** 该模型可用的 max_tokens 上限(实测值与会话内学到的更低值取小)。 */
1238
1249
  function maxTokensCeilingFor(id) {
1239
- return MEASURED_MAX_TOKENS[id.trim().toLowerCase()] ?? 65536;
1250
+ const measured = measuredCeilingFor(id);
1251
+ const learned = learnedMaxTokens.get(ceilingKey(id));
1252
+ return learned === void 0 ? measured : Math.min(measured, learned);
1253
+ }
1254
+ /**
1255
+ * 上游的「参数非法」错误码。它是**流内**帧(HTTP 仍是 200),所以既不走
1256
+ * classify(),也不会告诉我们是哪个参数错、更不区分「max_tokens 太大」和
1257
+ * 「prompt 装不下」—— 一切得我们自己判。
1258
+ */
1259
+ const PARAM_INVALID_CODE = "InferHub.001001005.400";
1260
+ /** 一帧(chatStream 已把上游 error_code 转成 OpenAI error 帧)是不是那条「参数非法」。 */
1261
+ function isParamInvalidFrame(text) {
1262
+ return text.includes("InferHub.001001005.400") || text.includes("The request param is invalid");
1263
+ }
1264
+ /**
1265
+ * 本次进程内学到的上限(小写 id → 上限)。
1266
+ *
1267
+ * 上游会把「实测过」的值悄悄调低(GLM-5.2 的 131072 于 2026-10-10 被收),
1268
+ * 手工表必然滞后。所以撞到「参数非法」时就地折半降档重试,并把结果记在这里,
1269
+ * 免得该模型后面每一轮都先白撞一次。
1270
+ */
1271
+ const learnedMaxTokens = /* @__PURE__ */ new Map();
1272
+ /** 降档下限:再低就没有可用的生成长度了,此时宁可把具体错误报给用户。 */
1273
+ const MAX_TOKENS_FLOOR = 8192;
1274
+ function ceilingKey(id) {
1275
+ return id.trim().toLowerCase();
1276
+ }
1277
+ function measuredCeilingFor(id) {
1278
+ return MEASURED_MAX_TOKENS[ceilingKey(id)] ?? 65536;
1279
+ }
1280
+ /**
1281
+ * 把该模型的上限折半降一档(收到「参数非法」后重发之前调用)。
1282
+ * @returns 新的上限;已经降到底则返回 0,表示别再试了。
1283
+ */
1284
+ function degradeTokenCeiling(id) {
1285
+ const next = Math.floor(maxTokensCeilingFor(id) / 2);
1286
+ if (next < MAX_TOKENS_FLOOR) return 0;
1287
+ learnedMaxTokens.set(ceilingKey(id), next);
1288
+ return next;
1289
+ }
1290
+ /**
1291
+ * 粗估一段文本占多少 token:CJK 每字约 1,其余每 4 字符约 1。
1292
+ *
1293
+ * 只要保守就够用 —— 它的用途是「在我们这边就把装不下的情况判掉」,
1294
+ * 而不是精确计费(上游不会告诉我们超没超,只会回一句参数非法)。
1295
+ */
1296
+ function estimateTokens(text) {
1297
+ let cjk = 0;
1298
+ for (let i = 0; i < text.length; i += 1) {
1299
+ const code = text.charCodeAt(i);
1300
+ if (code >= 12352 && code <= 40959 || code >= 44032 && code <= 64511 || code >= 65280 && code <= 65376) cjk += 1;
1301
+ }
1302
+ return Math.ceil(cjk + (text.length - cjk) / 4);
1240
1303
  }
1241
1304
  /** 上游要求 chat_id 为 32 位小写十六进制。 */
1242
1305
  const CHAT_ID_RE = /^[0-9a-f]{32}$/;
@@ -1285,16 +1348,20 @@ function sessionIdFor(chatId) {
1285
1348
  * 该端点虽是 OpenAI 兼容,但网关按自己的 schema 严格校验参数。所以这里
1286
1349
  * 不做原样透传,而是:
1287
1350
  * 1. model 走 canonicalModel(大小写敏感)
1288
- * 2. max_completion_tokens → max_tokens(华为只认后者),并钳制到
1289
- * MAX_TOKENS_CEILING(超限会流内报 InferHub.001001005.400
1290
- * "The request param is invalid, Please check it")
1351
+ * 2. max_completion_tokens → max_tokens(华为只认后者),并钳到该模型的上限;
1352
+ * 再按 contextWindow 扣掉 prompt 估算,保证 prompt + max_tokens 装得下
1353
+ * (两者任一超限,上游都只会流内回 InferHub.001001005.400
1354
+ * "The request param is invalid, Please check it",不告诉我们是哪一个)
1291
1355
  * 3. 丢弃非白名单字段(stream_options / store / logprobs 等虽实测可容忍,
1292
1356
  * 但按 codearts2api 观察到的真实流量收窄,避免网关后续收紧)
1293
1357
  * 4. tools[].function 去掉 strict(OpenAI 专有)
1294
1358
  * 5. 注入 CodeArts 特有字段:chat_id(按对话派生,见 conversationChatId)、
1295
1359
  * prompt_cache_key、tool_stream
1360
+ *
1361
+ * @param contextWindow 该模型的上下文窗口(来自 catalog)。传 0 表示不知道,
1362
+ * 此时只做上限钳制、不做「装不下」判断。
1296
1363
  */
1297
- function prepareChatBody(rawJson) {
1364
+ function prepareChatBody(rawJson, contextWindow = 0) {
1298
1365
  let parsed = {};
1299
1366
  try {
1300
1367
  parsed = JSON.parse(rawJson);
@@ -1326,7 +1393,25 @@ function prepareChatBody(rawJson) {
1326
1393
  else body.stream_options.include_usage = true;
1327
1394
  if (body.max_tokens === void 0 && typeof parsed.max_completion_tokens === "number") body.max_tokens = parsed.max_completion_tokens;
1328
1395
  const ceiling = maxTokensCeilingFor(model);
1329
- if (typeof body.max_tokens === "number" && body.max_tokens > ceiling) body.max_tokens = ceiling;
1396
+ if (typeof body.max_tokens === "number") {
1397
+ let maxTokens = Math.min(body.max_tokens, ceiling);
1398
+ if (contextWindow > 0) {
1399
+ const promptTokens = estimateTokens(`${JSON.stringify(body.messages ?? "")}${JSON.stringify(body.tools ?? "")}`);
1400
+ const room = contextWindow - promptTokens - PROMPT_ESTIMATE_MARGIN;
1401
+ if (room < MIN_USEFUL_OUTPUT_TOKENS) {
1402
+ body.max_tokens = maxTokens;
1403
+ return {
1404
+ url: `${SNAP_HOST}${EP_CHAT}`,
1405
+ body: JSON.stringify(body),
1406
+ model,
1407
+ chatId,
1408
+ error: `上下文放不下:${model} 的窗口是 ${contextWindow} tokens,本次请求(含 ${Array.isArray(body.messages) ? body.messages.length : 0} 条消息与工具定义)估算约 ${promptTokens} tokens,剩余不足 ${MIN_USEFUL_OUTPUT_TOKENS} tokens 的生成余量。请新建会话或改用上下文更大的模型。`
1409
+ };
1410
+ }
1411
+ maxTokens = Math.min(maxTokens, room);
1412
+ }
1413
+ body.max_tokens = maxTokens;
1414
+ }
1330
1415
  if (Array.isArray(body.tools)) body.tools = body.tools.map((tool) => {
1331
1416
  if (tool && typeof tool === "object" && tool.function && typeof tool.function === "object") {
1332
1417
  const fn = { ...tool.function };
@@ -1921,14 +2006,15 @@ async function refreshCatalog(store, catalog, signal) {
1921
2006
  /**
1922
2007
  * 用量账本:把聊天流里的 usage 帧累计成「今天用了多少」。
1923
2008
  *
1924
- * 为什么只能自己记:**CodeArts 没有额度/积分 API**(codearts2api 的 pool 里
1925
- * `credits` 字段恒为 0,注释写着「CodeArts 无积分字段」;本插件也没有任何余额接口
1926
- * 可用)。能拿到的真实数据只有两样:
2009
+ * 为什么还要自己记:账号总额度有积分接口了(见 credits.ts,2026-10 从官方 Agent
2010
+ * 客户端挖出并实测通过),但**本机经插件发出的请求**仍需自己累计 —— 上游不按
2011
+ * 「这台机器」维度给统计。能拿到的真实数据有三样:
1927
2012
  * 1. 每个请求的 token `usage`(上游在流末尾回一帧 `{choices:[],usage:{...}}`);
1928
- * 2. 插件自己知道的账号会话槽位占用(见 account-pool:每账号 3 个并发会话)。
2013
+ * 2. 插件自己知道的账号会话槽位占用(见 account-pool:每账号 3 个并发会话);
2014
+ * 3. 账号积分/额度(见 credits.ts)。
1929
2015
  *
1930
- * 所以这里累计的是**本机经插件发出的请求**,不是账号总额度 —— 卡片上的文案必须
1931
- * 照实说,官方额度明细在 `codearts.huaweicloud.com/portal/settings/personal-usage`。
2016
+ * 所以这里累计的是**本机经插件发出的请求**,与卡片上的积分区块互补 —— 一个是本机
2017
+ * 级、一个是账号级。官方额度明细在 `codearts.huaweicloud.com/portal/settings/personal-usage`。
1932
2018
  *
1933
2019
  * 落盘在 `$DSH_HOME/.codearts-usage.json`,按天存,保留最近 `RETENTION_DAYS` 天。
1934
2020
  *
@@ -2134,4 +2220,4 @@ function sumOf(store) {
2134
2220
  return out;
2135
2221
  }
2136
2222
  //#endregion
2137
- export { CodeArtsAccountStore as _, SNAP_HOST as a, codeartsAuthPath as b, fetchCallerIdentity as c, maxTokensCeilingFor as d, prepareChatBody as f, isBenefitModel as g, filterEnabledModels as h, refreshCatalog as i, fetchModels as l, FALLBACK_CODEARTS_MODELS as m, codeartsUsagePath as n, canonicalModel as o, CodeArtsCatalog as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, isConcurrencyCap as u, CodeArtsCredentialStore as v, startLogin as x, codeartsAccountsPath as y };
2223
+ export { codeartsAccountsPath as C, CodeArtsCredentialStore as S, startLogin as T, CodeArtsCatalog as _, SNAP_HOST as a, isBenefitModel as b, claimBenefit as c, fetchModels as d, isConcurrencyCap as f, signedGet as g, prepareChatBody as h, refreshCatalog as i, degradeTokenCeiling as l, maxTokensCeilingFor as m, codeartsUsagePath as n, canonicalModel as o, isParamInvalidFrame as p, discoverModels as r, chatStream as s, CodeArtsUsageTracker as t, fetchCallerIdentity as u, FALLBACK_CODEARTS_MODELS as v, codeartsAuthPath as w, CodeArtsAccountStore as x, filterEnabledModels as y };
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": "0.2.1",
5
+ "version": "0.4.0",
6
6
  "author": "elegant01",
7
7
  "keywords": [
8
8
  "dsh-plugin",