dsh-llm-adapters 1.2.1 → 1.2.2

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
@@ -788,6 +788,7 @@ function codeartsRecord(value) {
788
788
  ...text$5(raw.codeVerifier) === void 0 ? {} : { codeVerifier: text$5(raw.codeVerifier) },
789
789
  ...key === void 0 ? {} : { dpopPrivateJwk: key },
790
790
  ...text$5(raw.userName) === void 0 ? {} : { userName: text$5(raw.userName) },
791
+ ...text$5(raw.otSessionId) === void 0 ? {} : { otSessionId: text$5(raw.otSessionId) },
791
792
  ...noBenefit.length === 0 ? {} : { noBenefit }
792
793
  };
793
794
  }
@@ -1425,6 +1426,18 @@ function readUserProfile(token) {
1425
1426
  ...domainId === void 0 ? {} : { domainId }
1426
1427
  };
1427
1428
  }
1429
+ /**
1430
+ * The session id this account should chat under, derived from who the account is.
1431
+ *
1432
+ * 32 lowercase hex, the shape the IDE's own kernel sends. Nothing but stability
1433
+ * matters here — the gateway uses the value as a key, not as an identity — so it
1434
+ * is hashed from the principal id rather than kept in a file.
1435
+ */
1436
+ function otSessionIdOf(profile) {
1437
+ const seed = profile?.userId ?? profile?.userName;
1438
+ if (seed === void 0) return void 0;
1439
+ return createHash("sha256").update(`codearts-ot-session:${seed}`).digest("hex").slice(0, 32);
1440
+ }
1428
1441
  /** The portal URL the browser opens; the callback is on loopback. */
1429
1442
  function buildAuthorizeUrl(port, pkce, ticketId) {
1430
1443
  return `${CODEARTS_PORTAL_AUTHORIZE}?theme=2&locale=zh-cn&uri_scheme=${CODEARTS_CLIENT_ID}&client_id=${CODEARTS_CLIENT_ID}&port=${port}&code_challenge=${pkce.codeChallenge}&code_challenge_method=SHA-256&ticket_id=${ticketId}&plugin-name=snap_AIIDE&plugin-version=5.2.0`;
@@ -1475,25 +1488,30 @@ async function exchangeRefreshToken(refreshToken, codeVerifier, keyPair, fetcher
1475
1488
  grant_type: "refresh_token",
1476
1489
  refresh_token: refreshToken
1477
1490
  }, keyPair, fetcher), codeVerifier, keyPair);
1478
- const userName = credential.userName ?? readUserProfile(refreshToken)?.userName;
1491
+ const previous = readUserProfile(refreshToken);
1492
+ const userName = credential.userName ?? previous?.userName;
1493
+ const otSessionId = credential.otSessionId ?? otSessionIdOf(previous);
1479
1494
  return {
1480
1495
  ...credential,
1481
1496
  refreshToken: credential.refreshToken ?? refreshToken,
1482
- ...userName === void 0 ? {} : { userName }
1497
+ ...userName === void 0 ? {} : { userName },
1498
+ ...otSessionId === void 0 ? {} : { otSessionId }
1483
1499
  };
1484
1500
  }
1485
1501
  function credentialFrom(token, codeVerifier, keyPair) {
1486
1502
  const credentials = token.credentials ?? {};
1487
1503
  const expiration = credentials.expiration;
1488
1504
  const expiresAt = expiration === void 0 ? void 0 : Date.parse(expiration);
1489
- const userName = readUserProfile(token.refresh_token)?.userName;
1505
+ const profile = readUserProfile(token.refresh_token);
1506
+ const otSessionId = otSessionIdOf(profile);
1490
1507
  return {
1491
1508
  accessKeyId: credentials.access_key_id ?? "",
1492
1509
  secretAccessKey: credentials.secret_access_key ?? "",
1493
1510
  securityToken: credentials.security_token ?? "",
1494
1511
  ...expiresAt === void 0 || Number.isNaN(expiresAt) ? {} : { expiresAt },
1495
1512
  ...token.refresh_token === void 0 ? {} : { refreshToken: token.refresh_token },
1496
- ...userName === void 0 ? {} : { userName },
1513
+ ...profile?.userName === void 0 ? {} : { userName: profile.userName },
1514
+ ...otSessionId === void 0 ? {} : { otSessionId },
1497
1515
  codeVerifier,
1498
1516
  dpopPrivateJwk: keyPair.privateKeyJwk
1499
1517
  };
@@ -2224,7 +2242,8 @@ function decodeJwtFacts(token) {
2224
2242
  ...typeof record.name === "string" ? { name: record.name } : {},
2225
2243
  ...typeof record.phone === "string" ? { phone: record.phone } : {},
2226
2244
  ...typeof record.owner_type === "string" ? { ownerType: record.owner_type } : {},
2227
- ...typeof record.sid === "string" ? { sessionId: record.sid } : {}
2245
+ ...typeof record.sid === "string" ? { sessionId: record.sid } : {},
2246
+ ...typeof record.sub === "string" ? { subject: record.sub } : {}
2228
2247
  };
2229
2248
  } catch {
2230
2249
  return {};
@@ -2557,7 +2576,8 @@ var CodeArtsAuth = class {
2557
2576
  securityToken: next.securityToken,
2558
2577
  ...next.expiresAt === void 0 ? {} : { expiresAt: next.expiresAt },
2559
2578
  ...next.refreshToken === void 0 ? {} : { refreshToken: next.refreshToken },
2560
- ...next.userName === void 0 ? {} : { userName: next.userName }
2579
+ ...next.userName === void 0 ? {} : { userName: next.userName },
2580
+ ...next.otSessionId === void 0 ? {} : { otSessionId: next.otSessionId }
2561
2581
  });
2562
2582
  this.lastError = void 0;
2563
2583
  return next;
@@ -2577,6 +2597,7 @@ var CodeArtsAuth = class {
2577
2597
  }
2578
2598
  };
2579
2599
  function credentialOf(record) {
2600
+ const otSessionId = record.otSessionId ?? otSessionIdOf(readUserProfile(record.refreshToken));
2580
2601
  return {
2581
2602
  accessKeyId: record.accessKeyId,
2582
2603
  secretAccessKey: record.secretAccessKey,
@@ -2584,7 +2605,8 @@ function credentialOf(record) {
2584
2605
  ...record.expiresAt === void 0 ? {} : { expiresAt: record.expiresAt },
2585
2606
  ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken },
2586
2607
  ...record.codeVerifier === void 0 ? {} : { codeVerifier: record.codeVerifier },
2587
- ...record.dpopPrivateJwk === void 0 ? {} : { dpopPrivateJwk: record.dpopPrivateJwk }
2608
+ ...record.dpopPrivateJwk === void 0 ? {} : { dpopPrivateJwk: record.dpopPrivateJwk },
2609
+ ...otSessionId === void 0 ? {} : { otSessionId }
2588
2610
  };
2589
2611
  }
2590
2612
  // src/config.ts
@@ -2631,8 +2653,7 @@ const Config$1 = z.object({
2631
2653
  defaultMaxTokens: z.number().step(1).min(1).default(32e3).description("模型目录未声明输出上限时使用的输出上限。").volatile(),
2632
2654
  modelCacheTtlSeconds: z.number().step(1).min(10).max(86400).default(120).description("模型列表缓存时长(秒)。调小可更快看到账号侧变化。").volatile(),
2633
2655
  catalogFetchTimeoutSeconds: z.number().step(1).min(1).max(300).default(20).description("读取模型目录、额度等请求的时限(秒)。").volatile(),
2634
- openRouterSyncMinutes: z.number().step(1).min(0).max(43200).default(60).description("OpenRouter 模型元数据的自动同步间隔(分钟)。0 表示关闭自动同步;缓存保存在本机,连不上时继续使用上次成功同步的数据。").volatile(),
2635
- raccoonCheckinMinutes: z.number().step(1).min(0).max(1440).default(30).description("小浣熊每日积分自动监测间隔(分钟):到点核对积分流水,未到账时按网页方式尝试触发。0 表示关闭自动监测;「打开网页签到」入口始终保留。").volatile()
2656
+ openRouterSyncMinutes: z.number().step(1).min(0).max(43200).default(60).description("OpenRouter 模型元数据的自动同步间隔(分钟)。0 表示关闭自动同步;缓存保存在本机,连不上时继续使用上次成功同步的数据。").volatile()
2636
2657
  });
2637
2658
  function liveConfig(config) {
2638
2659
  const source = config ?? {};
@@ -2673,8 +2694,7 @@ function liveConfig(config) {
2673
2694
  defaultMaxTokens: () => positive(field("defaultMaxTokens"), 32e3),
2674
2695
  modelCacheTtlMs: () => positive(field("modelCacheTtlSeconds"), 120) * 1e3,
2675
2696
  catalogFetchTimeoutMs: () => positive(field("catalogFetchTimeoutSeconds"), 20) * 1e3,
2676
- openRouterSyncMinutes: () => nonNegative(field("openRouterSyncMinutes"), 60),
2677
- raccoonCheckinMinutes: () => nonNegative(field("raccoonCheckinMinutes"), 30)
2697
+ openRouterSyncMinutes: () => nonNegative(field("openRouterSyncMinutes"), 60)
2678
2698
  };
2679
2699
  }
2680
2700
  function resolveConfig(config) {
@@ -2712,8 +2732,7 @@ function resolveConfig(config) {
2712
2732
  defaultMaxTokens: live.defaultMaxTokens(),
2713
2733
  modelCacheTtlMs: live.modelCacheTtlMs(),
2714
2734
  catalogFetchTimeoutMs: live.catalogFetchTimeoutMs(),
2715
- openRouterSyncMinutes: live.openRouterSyncMinutes(),
2716
- raccoonCheckinMinutes: live.raccoonCheckinMinutes()
2735
+ openRouterSyncMinutes: live.openRouterSyncMinutes()
2717
2736
  };
2718
2737
  }
2719
2738
  function text$4(value) {
@@ -3056,117 +3075,6 @@ function headersForFetch(signed) {
3056
3075
  }
3057
3076
  return out;
3058
3077
  }
3059
- // src/codearts-window.ts
3060
- /**
3061
- * The trial plan's chat budget.
3062
- *
3063
- * The gateway opens one "session" per chat request and allows **three per
3064
- * rolling minute** for the whole account. The window is shared by every model,
3065
- * it is not keyed by `session-id` (re-sending the same id does not buy a slot),
3066
- * and a request rejected with `InferHub.4004.200` spends a slot just like a
3067
- * successful one. Measured behaviour, three slots then a lockout:
3068
- *
3069
- * ```
3070
- * t+0s ok ok ok cap cap cap burst of six
3071
- * t+10s cap …
3072
- * t+51s cap
3073
- * t+61s ok ok ok the burst ages out of the window
3074
- * ```
3075
- *
3076
- * So a turn cannot be made to "go faster" by retrying: the only way a request
3077
- * stops failing is to spend the budget deliberately — wait for the oldest start
3078
- * to age out instead of firing into a wall. This class owns that bookkeeping and
3079
- * is shared by every provider instance in the process, because a hot reload
3080
- * must not reset the count the gateway still remembers.
3081
- *
3082
- * @module dsh-llm-adapters/codearts-window
3083
- */
3084
- /** Sessions the trial plan allows inside one window. */
3085
- const CODEARTS_SESSION_LIMIT = 3;
3086
- /** How long a start keeps its slot. Measured: the burst frees at ~60 s. */
3087
- const CODEARTS_SESSION_WINDOW_MS = 6e4;
3088
- /** A little slack so a request never lands a few milliseconds early. */
3089
- const MARGIN_MS = 1500;
3090
- function sleep$2(ms, signal) {
3091
- return new Promise((resolve, reject) => {
3092
- if (signal?.aborted === true) {
3093
- reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
3094
- return;
3095
- }
3096
- const onAbort = () => {
3097
- clearTimeout(timer);
3098
- reject(signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
3099
- };
3100
- const timer = setTimeout(() => {
3101
- signal?.removeEventListener("abort", onAbort);
3102
- resolve();
3103
- }, ms);
3104
- signal?.addEventListener("abort", onAbort, { once: true });
3105
- });
3106
- }
3107
- var CodeArtsSessionWindow = class {
3108
- limit;
3109
- windowMs;
3110
- now;
3111
- starts = [];
3112
- /** Serialises waiters so two callers cannot both take the last slot. */
3113
- tail = Promise.resolve();
3114
- constructor(limit = 3, windowMs = CODEARTS_SESSION_WINDOW_MS, now = Date.now) {
3115
- this.limit = limit;
3116
- this.windowMs = windowMs;
3117
- this.now = now;
3118
- }
3119
- /** Starts recorded inside the current window. */
3120
- get size() {
3121
- this.trim();
3122
- return this.starts.length;
3123
- }
3124
- /** Milliseconds until another start fits; `0` when the window has room. */
3125
- waitMs() {
3126
- this.trim();
3127
- if (this.starts.length < this.limit) return 0;
3128
- const oldest = this.starts[0] ?? 0;
3129
- return Math.max(0, this.windowMs - (this.now() - oldest));
3130
- }
3131
- /**
3132
- * Claim a slot, waiting for the window to roll over when it is full.
3133
- *
3134
- * The slot is recorded on return, not on completion: the gateway counts the
3135
- * start, and holds it for a fixed time whether the answer was short or long.
3136
- */
3137
- async open(signal) {
3138
- const run = this.tail.then(async () => {
3139
- for (;;) {
3140
- const wait = this.waitMs();
3141
- if (wait === 0) {
3142
- const start = this.now();
3143
- this.starts.push(start);
3144
- return { release: () => {
3145
- const index = this.starts.indexOf(start);
3146
- if (index >= 0) this.starts.splice(index, 1);
3147
- } };
3148
- }
3149
- await sleep$2(wait + MARGIN_MS, signal);
3150
- }
3151
- });
3152
- this.tail = run.then(() => void 0, () => void 0);
3153
- return await run;
3154
- }
3155
- /** Forget every start (tests, and a fresh login on another account). */
3156
- reset() {
3157
- this.starts.length = 0;
3158
- }
3159
- trim() {
3160
- const now = this.now();
3161
- while (this.starts.length > 0 && now - (this.starts[0] ?? 0) >= this.windowMs) this.starts.shift();
3162
- }
3163
- };
3164
- let shared;
3165
- /** The process-wide window; a plugin reload reuses it on purpose. */
3166
- function sharedSessionWindow() {
3167
- shared ??= new CodeArtsSessionWindow();
3168
- return shared;
3169
- }
3170
3078
  // src/openrouter.ts
3171
3079
  /**
3172
3080
  * Live capability lookup from OpenRouter's public model catalog.
@@ -3692,18 +3600,20 @@ const sleep$1 = (ms, signal) => new Promise((resolve, reject) => {
3692
3600
  }, ms);
3693
3601
  signal?.addEventListener("abort", onAbort, { once: true });
3694
3602
  });
3695
- /** Retries for a gateway that is busy for reasons the window cannot predict. */
3603
+ /** Retries for a gateway that is busy for reasons the caller cannot predict. */
3696
3604
  const QUEUE_ATTEMPTS = 3;
3697
- /** Minimum pause before retrying a rejected call; the window adds its own. */
3605
+ /** Pause before retrying a rejected call. */
3698
3606
  const QUEUE_BACKOFF_MS = 2e4;
3699
3607
  /**
3700
3608
  * The message a user sees once waiting is hopeless.
3701
3609
  *
3702
- * The raw gateway text (`TM.00001041`) reads like a bug; the limit is the
3703
- * trial plan's, and the only fix on this side is to say so.
3610
+ * The raw gateway text (`TM.00001041`) reads like a bug. The three concurrent
3611
+ * sessions are the trial plan's, and one plugin chatting under a single session
3612
+ * id is not meant to reach them: the slots are being held by something else on
3613
+ * the same account.
3704
3614
  */
3705
3615
  function queueExhausted() {
3706
- return new ProviderHttpError("CodeArts 体验版每分钟只允许 3 个会话,当前已被占满。请稍等片刻再发,或关闭其它 CodeArts 会话。", 429);
3616
+ return new ProviderHttpError("CodeArts 这个账号的 3 个并发会话已被占满(通常是码道 IDE 或另一台设备),请稍后再发,或关闭其它 CodeArts 会话。", 429);
3707
3617
  }
3708
3618
  /** Image transcripts live beside accounts.json, so a restart does not re-bill. */
3709
3619
  const IMAGE_CACHE_FILE = "codearts-image-cache.json";
@@ -3736,7 +3646,6 @@ var CodeArtsProvider = class {
3736
3646
  /** Models that answered `InferHub.4004.200`: no allowance pack to bill. */
3737
3647
  noBenefit = /* @__PURE__ */ new Set();
3738
3648
  noBenefitLoaded = false;
3739
- window;
3740
3649
  openRouter;
3741
3650
  sessionId = randomUUID();
3742
3651
  /**
@@ -3751,7 +3660,6 @@ var CodeArtsProvider = class {
3751
3660
  imageCacheWrite = Promise.resolve();
3752
3661
  constructor(options) {
3753
3662
  this.options = options;
3754
- this.window = options.window ?? sharedSessionWindow();
3755
3663
  this.openRouter = options.openRouter ?? new OpenRouterCatalog(options.fetchImpl ?? fetch);
3756
3664
  this.catalog = new ModelCatalog(() => this.loadModels(), options.ttlMs);
3757
3665
  this.login = new CodeArtsLogin({
@@ -3760,7 +3668,6 @@ var CodeArtsProvider = class {
3760
3668
  ...options.logger === void 0 ? {} : { logger: options.logger },
3761
3669
  save: async (account) => {
3762
3670
  await options.store.saveCodearts(account);
3763
- this.window.reset();
3764
3671
  this.noBenefit.clear();
3765
3672
  this.invalidateModels();
3766
3673
  this.invalidateQuota();
@@ -4089,7 +3996,7 @@ var CodeArtsProvider = class {
4089
3996
  }
4090
3997
  }
4091
3998
  }
4092
- /** Ask the vision model to describe one image, within the same chat budget. */
3999
+ /** Ask the vision model to describe one image; another turn on the same session. */
4093
4000
  async describeImage(dataUrl, signal) {
4094
4001
  const payload = JSON.stringify({
4095
4002
  model: CODEARTS_VISION_MODEL,
@@ -4110,7 +4017,6 @@ var CodeArtsProvider = class {
4110
4017
  let authRefreshed = false;
4111
4018
  let queueAttempts = 0;
4112
4019
  for (;;) {
4113
- const slot = await this.window.open(signal);
4114
4020
  let response;
4115
4021
  try {
4116
4022
  response = await this.send(chatId, payload, CODEARTS_VISION_MODEL, false, signal);
@@ -4121,10 +4027,9 @@ var CodeArtsProvider = class {
4121
4027
  continue;
4122
4028
  }
4123
4029
  if (error instanceof CodeArtsRetry && error.kind === "queue") {
4124
- slot.release();
4125
4030
  if (queueAttempts < QUEUE_ATTEMPTS) {
4126
4031
  queueAttempts++;
4127
- await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), signal);
4032
+ await sleep$1(QUEUE_BACKOFF_MS, signal);
4128
4033
  continue;
4129
4034
  }
4130
4035
  throw queueExhausted();
@@ -4145,10 +4050,9 @@ var CodeArtsProvider = class {
4145
4050
  })) if (chunk.type === "text-delta") transcript += chunk.text;
4146
4051
  } catch (error) {
4147
4052
  if (error instanceof CodeArtsRetry && error.kind === "queue") {
4148
- slot.release();
4149
4053
  if (queueAttempts < QUEUE_ATTEMPTS) {
4150
4054
  queueAttempts++;
4151
- await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), signal);
4055
+ await sleep$1(QUEUE_BACKOFF_MS, signal);
4152
4056
  continue;
4153
4057
  }
4154
4058
  throw queueExhausted();
@@ -4187,7 +4091,6 @@ var CodeArtsProvider = class {
4187
4091
  let benefitDropped = false;
4188
4092
  let queueAttempts = 0;
4189
4093
  for (;;) {
4190
- const slot = await this.window.open(options.signal);
4191
4094
  let response;
4192
4095
  try {
4193
4096
  response = await this.send(chatId, payload, model, benefitDropped, options.signal);
@@ -4210,8 +4113,7 @@ var CodeArtsProvider = class {
4210
4113
  }
4211
4114
  if (error.kind === "queue" && queueAttempts < QUEUE_ATTEMPTS) {
4212
4115
  queueAttempts++;
4213
- slot.release();
4214
- await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal);
4116
+ await sleep$1(QUEUE_BACKOFF_MS, options.signal);
4215
4117
  continue;
4216
4118
  }
4217
4119
  if (error.kind === "queue") throw queueExhausted();
@@ -4238,8 +4140,7 @@ var CodeArtsProvider = class {
4238
4140
  } catch (error) {
4239
4141
  if (!produced && error instanceof CodeArtsRetry && error.kind === "queue" && queueAttempts < QUEUE_ATTEMPTS) {
4240
4142
  queueAttempts++;
4241
- slot.release();
4242
- await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal);
4143
+ await sleep$1(QUEUE_BACKOFF_MS, options.signal);
4243
4144
  continue;
4244
4145
  }
4245
4146
  if (error instanceof CodeArtsRetry && error.kind === "queue") throw queueExhausted();
@@ -4259,13 +4160,17 @@ var CodeArtsProvider = class {
4259
4160
  async send(chatId, payload, model, benefitDropped, signal) {
4260
4161
  const credential = await this.credential();
4261
4162
  const benefit = !benefitDropped && this.isBenefitModel(model);
4163
+ const otSessionId = credential.otSessionId ?? this.sessionId.replace(/-/g, "");
4262
4164
  const headers = {
4263
4165
  ...headersForFetch(await signRequestHuawei(credential.accessKeyId, credential.secretAccessKey, credential.securityToken, "POST", CODEARTS_CHAT_URL, payload, benefit ? { maas_type: "benefit" } : void 0)),
4264
4166
  "content-type": "application/json",
4265
4167
  accept: "text/event-stream",
4266
4168
  "chat-id": chatId,
4267
4169
  "session-id": this.sessionId,
4268
- lang: "en"
4170
+ lang: "en",
4171
+ "x-ot-session-id": otSessionId,
4172
+ "user-session-id": otSessionId,
4173
+ "x-ot-trace-id": randomUUID().replace(/-/g, "")
4269
4174
  };
4270
4175
  const timeout = AbortSignal.timeout(Math.max(this.options.timeoutMs(), 6e4));
4271
4176
  const response = await this.fetcher(CODEARTS_CHAT_URL, {
@@ -4295,7 +4200,6 @@ var CodeArtsProvider = class {
4295
4200
  }
4296
4201
  async logoutAccount() {
4297
4202
  await this.options.auth.logout();
4298
- this.window.reset();
4299
4203
  this.invalidateModels();
4300
4204
  this.invalidateQuota();
4301
4205
  }
@@ -6359,6 +6263,100 @@ function raccoonHeaders(token) {
6359
6263
  "x-client-version": RACCOON_CLIENT_VERSION
6360
6264
  };
6361
6265
  }
6266
+ /** Wording the gateway has used alongside that code. */
6267
+ const RATE_LIMIT_WORDS = [
6268
+ "user_model_rate_limited",
6269
+ "rate_limit",
6270
+ "频率超限"
6271
+ ];
6272
+ /**
6273
+ * Whether a rejection is the token wall rather than a generic failure.
6274
+ *
6275
+ * Matched on the status, the code and the wording, because the same condition
6276
+ * arrives as an HTTP 429 and as an error frame inside a 200 stream.
6277
+ */
6278
+ function isRaccoonRateLimited(status, text) {
6279
+ if (status === 429) return true;
6280
+ const lower = text.toLowerCase();
6281
+ return RATE_LIMIT_WORDS.some((word) => lower.includes(word.toLowerCase()));
6282
+ }
6283
+ /**
6284
+ * The wall, as an error the caller can tell apart from a generic 429.
6285
+ *
6286
+ * A subclass rather than a flag: the in-band path and the HTTP path would
6287
+ * otherwise both be caught by the same handler, and the second pass would wrap
6288
+ * the message the first pass had already rewritten.
6289
+ */
6290
+ var RaccoonRateLimitedError = class extends ProviderHttpError {
6291
+ constructor(message) {
6292
+ super(message, 429);
6293
+ this.name = "RaccoonRateLimitedError";
6294
+ }
6295
+ };
6296
+ /**
6297
+ * The message a user sees once the wall is hit.
6298
+ *
6299
+ * The raw text says "retry later" and gives no reason, which is why this reads
6300
+ * as the plugin being broken. The honest answer names the cause and the one
6301
+ * lever the user actually has on this side.
6302
+ */
6303
+ function raccoonRateLimited(detail) {
6304
+ return new RaccoonRateLimitedError(`小浣熊账号的模型吞吐已达上限(不是每分钟请求数,而是按账号的 token 速率)。一轮对话带的上下文越大越快撞到,撞到后额度按吞吐缓慢回血,上游也没有给恢复时间。稍等一会儿再发;把该模型的上下文窗口调小,同一会话能撑得更久。上游原文:${detail}`);
6305
+ }
6306
+ /** The `error` object a compatible gateway puts in an SSE frame. */
6307
+ function frameFailure(frame) {
6308
+ const error = frame.error;
6309
+ if (error === void 0) return void 0;
6310
+ if (typeof error === "string") return { message: error };
6311
+ if (error === null || typeof error !== "object") return void 0;
6312
+ const record = error;
6313
+ const code = typeof record.code === "string" ? record.code : void 0;
6314
+ const message = typeof record.message === "string" ? record.message : void 0;
6315
+ return {
6316
+ ...code === void 0 ? {} : { code },
6317
+ ...message === void 0 ? {} : { message }
6318
+ };
6319
+ }
6320
+ /**
6321
+ * One conversation's session id, stable across the turns of that conversation.
6322
+ *
6323
+ * The official client never opens a session per request — measured on its own
6324
+ * logs, 125 calls over 26 minutes all carried one `sess-0-53927d74` — while an
6325
+ * implementation that mints a fresh id per request tells the gateway that one
6326
+ * account opened hundreds of sessions within minutes.
6327
+ *
6328
+ * The id is a pure function of (account, model) rather than a random value, so a
6329
+ * restarted or hot-reloaded process continues the same conversation instead of
6330
+ * presenting a new session, and two models stay two conversations. The principal
6331
+ * itself never leaves the process — only a truncated digest of it does.
6332
+ */
6333
+ var RaccoonSessionIds = class {
6334
+ principal;
6335
+ /**
6336
+ * @param principal - a value that is stable for one account and identifies it
6337
+ * without being the token itself (the JWT's `sub`).
6338
+ */
6339
+ constructor(principal) {
6340
+ this.principal = principal;
6341
+ }
6342
+ async for(model) {
6343
+ const principal = await this.principal();
6344
+ if (principal === void 0) return void 0;
6345
+ return raccoonSessionId(principal, model);
6346
+ }
6347
+ };
6348
+ /**
6349
+ * `sess-<sequence>-<8 hex>`, the shape the official client sends.
6350
+ *
6351
+ * The sequence is the official client's process-local counter and is pinned to 0
6352
+ * here on purpose: an incrementing value would make one conversation look like a
6353
+ * different session after a restart, which is the one thing this id exists to
6354
+ * prevent. Identity lives in the digest, which is truncated on purpose — the id
6355
+ * must separate accounts without naming them on the wire.
6356
+ */
6357
+ function raccoonSessionId(principal, model) {
6358
+ return `sess-0-${createHash("sha256").update(`${principal}\0${model}`).digest("hex").slice(0, 8)}`;
6359
+ }
6362
6360
  /** Whether the gateway accepts a token harvested from the browser. */
6363
6361
  async function raccoonAccessTokenValid(token, fetchImpl = fetch) {
6364
6362
  try {
@@ -6416,12 +6414,16 @@ var RaccoonProvider = class {
6416
6414
  catalog;
6417
6415
  quotaCache;
6418
6416
  quotaInflight;
6419
- checkinInflight;
6420
6417
  loginFlow;
6421
6418
  openRouter;
6419
+ sessionIds;
6422
6420
  constructor(options) {
6423
6421
  this.options = options;
6424
6422
  this.openRouter = options.openRouter ?? new OpenRouterCatalog(options.fetchImpl ?? fetch);
6423
+ this.sessionIds = options.sessionIds ?? new RaccoonSessionIds(async () => {
6424
+ const stored = await options.store.raccoon();
6425
+ return stored === void 0 ? void 0 : decodeJwtFacts(stored.accessToken).subject;
6426
+ });
6425
6427
  this.catalog = new ModelCatalog(() => this.fetchModels(), options.ttlMs);
6426
6428
  }
6427
6429
  async status() {
@@ -6475,6 +6477,21 @@ var RaccoonProvider = class {
6475
6477
  headers(token) {
6476
6478
  return raccoonHeaders(token);
6477
6479
  }
6480
+ /**
6481
+ * The chat headers, including the session id the gateway counts sessions by.
6482
+ *
6483
+ * Only chat carries it: the catalogue, quota and check-in routes are plain
6484
+ * business calls the official client sends outside any session, so leaving
6485
+ * them alone keeps this change to the one route it was measured on.
6486
+ */
6487
+ async chatHeaders(token, model) {
6488
+ const base = raccoonHeaders(token);
6489
+ const sessionId = await this.sessionIds.for(model).catch(() => void 0);
6490
+ return sessionId === void 0 ? base : {
6491
+ ...base,
6492
+ "x-raccoon-session-id": sessionId
6493
+ };
6494
+ }
6478
6495
  /** Run one JSON call; a rejected token means this login needs a new scan. */
6479
6496
  async authorizedJson(path, init = {}, extraHeaders = {}) {
6480
6497
  const token = await this.options.auth.accessToken();
@@ -6590,6 +6607,15 @@ var RaccoonProvider = class {
6590
6607
  };
6591
6608
  }
6592
6609
  }
6610
+ /**
6611
+ * Read today's grant. **Read-only on purpose.**
6612
+ *
6613
+ * The gateway has no claim endpoint: points are issued on the day's first
6614
+ * website visit. Replaying that visit (`setting_info`) can trigger the grant
6615
+ * from here, but doing so takes a state-changing action on the account without
6616
+ * the user asking for it in that moment, so the plugin only reports. Claiming
6617
+ * is the website's job, and the card links there.
6618
+ */
6593
6619
  async checkinStatus() {
6594
6620
  const grant = await this.dailyGrant();
6595
6621
  if (grant !== void 0) return {
@@ -6609,46 +6635,6 @@ var RaccoonProvider = class {
6609
6635
  url: RACCOON_CHECKIN_URL
6610
6636
  };
6611
6637
  }
6612
- async claimCheckin() {
6613
- await this.authorizedJson("/office/v3/setting_info", {}, {
6614
- "x-client-platform": "web",
6615
- "x-client-version": "131.0.0.0"
6616
- });
6617
- const grant = await this.dailyGrant();
6618
- if (grant === void 0) return {
6619
- available: true,
6620
- claimed: false,
6621
- deviceClaimed: false,
6622
- message: "每日积分尚未到账;请打开网页签到",
6623
- url: RACCOON_CHECKIN_URL
6624
- };
6625
- this.invalidateQuota();
6626
- return {
6627
- available: true,
6628
- claimed: true,
6629
- deviceClaimed: false,
6630
- ...grant.points === void 0 ? {} : { reward: grant.points },
6631
- ...grant.grantedAt === void 0 ? {} : { grantedAt: grant.grantedAt },
6632
- message: "每日积分已到账",
6633
- url: RACCOON_CHECKIN_URL
6634
- };
6635
- }
6636
- /**
6637
- * One automatic check-in pass: report today's grant, or ask the website path
6638
- * to issue it. Shared so overlapping timers cannot spend two call slots.
6639
- */
6640
- async ensureDailyCheckin() {
6641
- if (this.checkinInflight !== void 0) return this.checkinInflight;
6642
- this.checkinInflight = (async () => {
6643
- const status = await this.checkinStatus();
6644
- return status.claimed ? status : this.claimCheckin();
6645
- })();
6646
- try {
6647
- return await this.checkinInflight;
6648
- } finally {
6649
- this.checkinInflight = void 0;
6650
- }
6651
- }
6652
6638
  async *stream(options, model, readImage, onUnauthorized) {
6653
6639
  const token = await this.options.auth.accessToken();
6654
6640
  const body = buildBody(options, model, await buildMessages(options, readImage), buildTools(options));
@@ -6665,7 +6651,7 @@ var RaccoonProvider = class {
6665
6651
  try {
6666
6652
  yield* openAiStream({
6667
6653
  url: `${RACCOON_BASE}/llm/v2/chat/completions`,
6668
- headers: this.headers(token),
6654
+ headers: await this.chatHeaders(token, model),
6669
6655
  body,
6670
6656
  signal: options.signal,
6671
6657
  fetchImpl: this.options.fetchImpl,
@@ -6673,10 +6659,17 @@ var RaccoonProvider = class {
6673
6659
  this.invalidateModels();
6674
6660
  this.invalidateQuota();
6675
6661
  onUnauthorized?.();
6662
+ },
6663
+ onFrame: (frame) => {
6664
+ const failure = frameFailure(frame);
6665
+ if (failure === void 0) return;
6666
+ const detail = failure.message ?? failure.code ?? "上游返回错误";
6667
+ throw isRaccoonRateLimited(void 0, `${failure.code ?? ""} ${detail}`) ? raccoonRateLimited(detail) : new ProviderHttpError(detail, 502);
6676
6668
  }
6677
6669
  });
6678
6670
  } catch (error) {
6679
6671
  if (error instanceof ProviderHttpError && (error.status === 401 || error.status === 403)) throw new Error("小浣熊登录已失效,请在设置页点「打开扫码登录」。");
6672
+ if (error instanceof ProviderHttpError && !(error instanceof RaccoonRateLimitedError) && error.status === 429) throw raccoonRateLimited(error.message);
6680
6673
  throw error;
6681
6674
  }
6682
6675
  }
@@ -8883,23 +8876,6 @@ function apply(ctx, config) {
8883
8876
  syncTimer.unref?.();
8884
8877
  syncOpenRouter();
8885
8878
  ctx.effect(() => () => clearInterval(syncTimer), "llm-adapters: openrouter sync");
8886
- let lastRaccoonCheckin = Date.now();
8887
- const checkinMonitor = () => {
8888
- if (!live.raccoonEnabled()) return;
8889
- const minutes = live.raccoonCheckinMinutes();
8890
- if (minutes <= 0) return;
8891
- if (Date.now() - lastRaccoonCheckin < minutes * 6e4) return;
8892
- lastRaccoonCheckin = Date.now();
8893
- (async () => {
8894
- if (!await raccoonProvider.signedIn()) return;
8895
- await raccoonProvider.ensureDailyCheckin();
8896
- })().catch((error) => {
8897
- ctx.logger?.warn(`llm-adapters: 小浣熊签到监测失败:${error instanceof Error ? error.message : String(error)}`);
8898
- });
8899
- };
8900
- const checkinTimer = setInterval(checkinMonitor, 6e4);
8901
- checkinTimer.unref?.();
8902
- ctx.effect(() => () => clearInterval(checkinTimer), "llm-adapters: raccoon check-in monitor");
8903
8879
  ctx.inject(["attachments"], (scope) => {
8904
8880
  attachments = scope.attachments;
8905
8881
  scope.effect(() => () => {
@@ -8965,4 +8941,4 @@ function apply(ctx, config) {
8965
8941
  ctx.logger?.info(`llm-adapters: 设置接口已挂载于 ${API_PREFIX}`);
8966
8942
  });
8967
8943
  }
8968
- export { API_PREFIX, AccountStore, BRANDS, CODEARTS_BENEFIT_FALLBACK, CODEARTS_BRAND, CODEARTS_CHAT_URL, CODEARTS_CONTEXT_WINDOWS, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, CODEARTS_EFFORT_LEVELS, CODEARTS_FALLBACK_CONTEXT_WINDOW, CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_PROVIDER, CODEARTS_SESSION_LIMIT, CODEARTS_SESSION_WINDOW_MS, CODEARTS_SNAP_BASE, CODEARTS_VISION_MODELS, CodeArtsAdapter, CodeArtsAuth, CodeArtsLogin, CodeArtsProvider, CodeArtsSessionWindow, Config, EMPTY_OPENROUTER_INDEX, LOOMY_ACCOUNT_BASE, LOOMY_BASE, LOOMY_BRAND, LOOMY_PROVIDER, LOOMY_VERSION, LOOMY_WECHAT_APP_ID, LOOMY_WECHAT_REDIRECT_URI, LocalAdapter, LoomyAdapter, LoomyAuth, LoomyProvider, LoomyQrLogin, ModelCatalog, OpenRouterCatalog, ProviderHttpError, RACCOON_BASE, RACCOON_BRAND, RACCOON_CLIENT_VERSION, RACCOON_IAM_BASE, RACCOON_PROVIDER, RACCOON_QR_APP_NAME, RACCOON_QR_ORIGIN, RaccoonAdapter, RaccoonAuth, RaccoonIamError, RaccoonProvider, RaccoonQrLogin, RefreshTokenExpiredError, TRAE_APP_ID, TRAE_BRAND, TRAE_CHAT_FUNCTIONS, TRAE_CHECKIN_REQ_SOURCE, TRAE_DIRECTORY_FUNCTIONS, TRAE_PROVIDER, TRAE_REGION_PROFILES, TRAE_SOLO_CHAT_PATH, TRAE_WEB_USER_AGENT, TraeAdapter, TraeAuth, TraeProvider, TraeWebLogin, apply, buildAuthorizeUrl, buildLoomyWechatAuthUrl, buildMessages, buildOpenRouterIndex, buildQrMatrix, buildRows, buildTools, clampMaxTokens, codeartsContextWindow, codeartsEfforts, consumeOpenAiStream, createRaccoonQr, decodeJwtFacts, defaultEffortOf, effortLabel, exchangeAuthorizationCode, exchangeRefreshToken, extractWechatUuid, fetchLoomyWechatUuid, generateDpopKeyPair, generatePkcePair, headersForFetch, inject, isBrandId, liveConfig, loomyGatewaySessionValid, machineIdOf, mergeTraeModels, modelKey, name, normalizeCodeArtsModelId, normalizeEffort, normalizeTraeRegion, openAiStream, openTraeStream, parseCodeArtsPackage, parseCodeArtsRoster, parseLoomyCatalog, parseModelName, parseOpenRouterModels, parseRaccoonCatalog, parseTraeCheckin, parseTraeClientStorage, parseTraeGlobalUsage, parseTraeRemoteModels, parseTraeUsage, parseTraeWireModels, pickActiveCredit, pollRaccoonQr, priceFactorOf, priceFactorOfLabel, raccoonAccessTokenValid, raccoonQrContent, raccoonRefreshCandidate, readJwtExpiry, readOpenRouter, readTraeClientLogin, renderQrSvg, resolveCapabilities, resolveConfig, resolveModelId, sharedSessionWindow, signDpopJws, signRequestHuawei, sseEvents, startLoginPage, traeClientDataDirs, traeMessages, traeTools };
8944
+ export { API_PREFIX, AccountStore, BRANDS, CODEARTS_BENEFIT_FALLBACK, CODEARTS_BRAND, CODEARTS_CHAT_URL, CODEARTS_CONTEXT_WINDOWS, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, CODEARTS_EFFORT_LEVELS, CODEARTS_FALLBACK_CONTEXT_WINDOW, CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_PROVIDER, CODEARTS_SNAP_BASE, CODEARTS_VISION_MODELS, CodeArtsAdapter, CodeArtsAuth, CodeArtsLogin, CodeArtsProvider, Config, EMPTY_OPENROUTER_INDEX, LOOMY_ACCOUNT_BASE, LOOMY_BASE, LOOMY_BRAND, LOOMY_PROVIDER, LOOMY_VERSION, LOOMY_WECHAT_APP_ID, LOOMY_WECHAT_REDIRECT_URI, LocalAdapter, LoomyAdapter, LoomyAuth, LoomyProvider, LoomyQrLogin, ModelCatalog, OpenRouterCatalog, ProviderHttpError, RACCOON_BASE, RACCOON_BRAND, RACCOON_CLIENT_VERSION, RACCOON_IAM_BASE, RACCOON_PROVIDER, RACCOON_QR_APP_NAME, RACCOON_QR_ORIGIN, RaccoonAdapter, RaccoonAuth, RaccoonIamError, RaccoonProvider, RaccoonQrLogin, RefreshTokenExpiredError, TRAE_APP_ID, TRAE_BRAND, TRAE_CHAT_FUNCTIONS, TRAE_CHECKIN_REQ_SOURCE, TRAE_DIRECTORY_FUNCTIONS, TRAE_PROVIDER, TRAE_REGION_PROFILES, TRAE_SOLO_CHAT_PATH, TRAE_WEB_USER_AGENT, TraeAdapter, TraeAuth, TraeProvider, TraeWebLogin, apply, buildAuthorizeUrl, buildLoomyWechatAuthUrl, buildMessages, buildOpenRouterIndex, buildQrMatrix, buildRows, buildTools, clampMaxTokens, codeartsContextWindow, codeartsEfforts, consumeOpenAiStream, createRaccoonQr, decodeJwtFacts, defaultEffortOf, effortLabel, exchangeAuthorizationCode, exchangeRefreshToken, extractWechatUuid, fetchLoomyWechatUuid, generateDpopKeyPair, generatePkcePair, headersForFetch, inject, isBrandId, liveConfig, loomyGatewaySessionValid, machineIdOf, mergeTraeModels, modelKey, name, normalizeCodeArtsModelId, normalizeEffort, normalizeTraeRegion, openAiStream, openTraeStream, otSessionIdOf, parseCodeArtsPackage, parseCodeArtsRoster, parseLoomyCatalog, parseModelName, parseOpenRouterModels, parseRaccoonCatalog, parseTraeCheckin, parseTraeClientStorage, parseTraeGlobalUsage, parseTraeRemoteModels, parseTraeUsage, parseTraeWireModels, pickActiveCredit, pollRaccoonQr, priceFactorOf, priceFactorOfLabel, raccoonAccessTokenValid, raccoonQrContent, raccoonRefreshCandidate, readJwtExpiry, readOpenRouter, readTraeClientLogin, renderQrSvg, resolveCapabilities, resolveConfig, resolveModelId, signDpopJws, signRequestHuawei, sseEvents, startLoginPage, traeClientDataDirs, traeMessages, traeTools };
@@ -83,12 +83,19 @@ export interface CodeArtsAccountRecord {
83
83
  d: string;
84
84
  };
85
85
  userName?: string;
86
+ /**
87
+ * The `x-ot-session-id` this account chats under: one stable value instead of
88
+ * one per request, because the gateway counts concurrent sessions by it.
89
+ *
90
+ * A record written before the field existed derives the same value from the
91
+ * refresh token's profile, so an existing sign-in needs no new login.
92
+ */
93
+ otSessionId?: string;
86
94
  /**
87
95
  * Models the account answered `InferHub.4004.200` for.
88
96
  *
89
- * The verdict is worth persisting: asking with `maas_type: benefit` spends
90
- * one of the three chat slots the plan allows per minute, even though the
91
- * call cannot succeed.
97
+ * Worth persisting: the account has no allowance pack for them, so without the
98
+ * verdict every call would pay for a request that cannot succeed.
92
99
  */
93
100
  noBenefit?: string[];
94
101
  loginAt: number;