@eddyskywalker/dsh-chatgpt-subscription 0.65.0 → 0.66.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/lib/client.js +120 -9
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1836 -993
  5. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  6. package/lib/types/client/kimi-code/locales.d.ts +8 -2
  7. package/lib/types/client/kimi-code/locales.d.ts.map +1 -1
  8. package/lib/types/client/locales-ru/kimi-code.d.ts.map +1 -1
  9. package/lib/types/client/locales-ru/progress-updates.d.ts.map +1 -1
  10. package/lib/types/client/locales-ru/step-code.d.ts.map +1 -1
  11. package/lib/types/client/progress-updates/locales.d.ts +23 -11
  12. package/lib/types/client/progress-updates/locales.d.ts.map +1 -1
  13. package/lib/types/client/progress-updates/settings-binding.d.ts +11 -1
  14. package/lib/types/client/progress-updates/settings-binding.d.ts.map +1 -1
  15. package/lib/types/client/progress-updates/settings-section.d.ts +2 -2
  16. package/lib/types/client/progress-updates/settings-section.d.ts.map +1 -1
  17. package/lib/types/client/step-code/StepCodeSection.d.ts.map +1 -1
  18. package/lib/types/client/step-code/locales.d.ts +5 -2
  19. package/lib/types/client/step-code/locales.d.ts.map +1 -1
  20. package/lib/types/compat.d.ts +1 -1
  21. package/lib/types/host/antigravity/adapter.d.ts +2 -0
  22. package/lib/types/host/antigravity/adapter.d.ts.map +1 -1
  23. package/lib/types/host/cache-warmup/budget.d.ts +2 -2
  24. package/lib/types/host/cache-warmup/budget.d.ts.map +1 -1
  25. package/lib/types/host/cache-warmup/contracts.d.ts +2 -0
  26. package/lib/types/host/cache-warmup/contracts.d.ts.map +1 -1
  27. package/lib/types/host/cache-warmup/install.d.ts +4 -4
  28. package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
  29. package/lib/types/host/cache-warmup/result-tool.d.ts +3 -1
  30. package/lib/types/host/cache-warmup/result-tool.d.ts.map +1 -1
  31. package/lib/types/host/cache-warmup/settings.d.ts +5 -1
  32. package/lib/types/host/cache-warmup/settings.d.ts.map +1 -1
  33. package/lib/types/host/cache-warmup/usage.d.ts +1 -1
  34. package/lib/types/host/cache-warmup/usage.d.ts.map +1 -1
  35. package/lib/types/host/claude/adapter.d.ts +3 -0
  36. package/lib/types/host/claude/adapter.d.ts.map +1 -1
  37. package/lib/types/host/cline-pass/mapper.d.ts.map +1 -1
  38. package/lib/types/host/common/account-pool.d.ts +27 -1
  39. package/lib/types/host/common/account-pool.d.ts.map +1 -1
  40. package/lib/types/host/common/cache-activity.d.ts +14 -0
  41. package/lib/types/host/common/cache-activity.d.ts.map +1 -0
  42. package/lib/types/host/common/cache-affinity.d.ts +23 -0
  43. package/lib/types/host/common/cache-affinity.d.ts.map +1 -0
  44. package/lib/types/host/common/cache-hint.d.ts +18 -0
  45. package/lib/types/host/common/cache-hint.d.ts.map +1 -0
  46. package/lib/types/host/common/failure-classification.d.ts +21 -0
  47. package/lib/types/host/common/failure-classification.d.ts.map +1 -0
  48. package/lib/types/host/common/hosted-media.d.ts +36 -0
  49. package/lib/types/host/common/hosted-media.d.ts.map +1 -0
  50. package/lib/types/host/common/prefix-request-coordinator.d.ts +46 -0
  51. package/lib/types/host/common/prefix-request-coordinator.d.ts.map +1 -0
  52. package/lib/types/host/common/prefix-stability.d.ts +24 -0
  53. package/lib/types/host/common/prefix-stability.d.ts.map +1 -0
  54. package/lib/types/host/common/prompt-cache-policy.d.ts +43 -0
  55. package/lib/types/host/common/prompt-cache-policy.d.ts.map +1 -0
  56. package/lib/types/host/common/reasoning-dialect.d.ts +24 -0
  57. package/lib/types/host/common/reasoning-dialect.d.ts.map +1 -0
  58. package/lib/types/host/common/session-cache-state.d.ts +29 -0
  59. package/lib/types/host/common/session-cache-state.d.ts.map +1 -0
  60. package/lib/types/host/common/sticky-image-budget.d.ts +10 -0
  61. package/lib/types/host/common/sticky-image-budget.d.ts.map +1 -0
  62. package/lib/types/host/common/tool-runaway.d.ts +52 -0
  63. package/lib/types/host/common/tool-runaway.d.ts.map +1 -0
  64. package/lib/types/host/kimi-code/account-pool.d.ts +19 -2
  65. package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
  66. package/lib/types/host/kimi-code/adapter.d.ts +44 -30
  67. package/lib/types/host/kimi-code/adapter.d.ts.map +1 -1
  68. package/lib/types/host/kimi-code/cache-affinity.d.ts +112 -0
  69. package/lib/types/host/kimi-code/cache-affinity.d.ts.map +1 -0
  70. package/lib/types/host/kimi-code/cache-hint.d.ts +3 -28
  71. package/lib/types/host/kimi-code/cache-hint.d.ts.map +1 -1
  72. package/lib/types/host/kimi-code/cache-wait.d.ts +6 -4
  73. package/lib/types/host/kimi-code/cache-wait.d.ts.map +1 -1
  74. package/lib/types/host/kimi-code/mapper.d.ts +2 -6
  75. package/lib/types/host/kimi-code/mapper.d.ts.map +1 -1
  76. package/lib/types/host/kimi-code/media-files.d.ts +2 -28
  77. package/lib/types/host/kimi-code/media-files.d.ts.map +1 -1
  78. package/lib/types/host/kimi-code/repeat-breaker.d.ts +13 -16
  79. package/lib/types/host/kimi-code/repeat-breaker.d.ts.map +1 -1
  80. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  81. package/lib/types/host/kimi-code/session-cache-state.d.ts +16 -53
  82. package/lib/types/host/kimi-code/session-cache-state.d.ts.map +1 -1
  83. package/lib/types/host/minimax-code/adapter.d.ts +5 -1
  84. package/lib/types/host/minimax-code/adapter.d.ts.map +1 -1
  85. package/lib/types/host/minimax-code/files-api.d.ts.map +1 -1
  86. package/lib/types/host/minimax-code/runtime-enhancements.d.ts.map +1 -1
  87. package/lib/types/host/opendesign/mapper.d.ts.map +1 -1
  88. package/lib/types/host/qoder/mapper.d.ts.map +1 -1
  89. package/lib/types/host/responses-client.d.ts +4 -0
  90. package/lib/types/host/responses-client.d.ts.map +1 -1
  91. package/lib/types/host/step-code/account-pool.d.ts +2 -2
  92. package/lib/types/host/step-code/account-pool.d.ts.map +1 -1
  93. package/lib/types/host/step-code/adapter.d.ts +10 -0
  94. package/lib/types/host/step-code/adapter.d.ts.map +1 -1
  95. package/lib/types/host/step-code/routes.d.ts +3 -0
  96. package/lib/types/host/step-code/routes.d.ts.map +1 -1
  97. package/lib/types/index.d.ts +22 -3
  98. package/lib/types/index.d.ts.map +1 -1
  99. package/lib/types/shared/kimi-code-contracts.d.ts +16 -0
  100. package/lib/types/shared/kimi-code-contracts.d.ts.map +1 -1
  101. package/lib/types/shared/step-code-contracts.d.ts +5 -0
  102. package/lib/types/shared/step-code-contracts.d.ts.map +1 -1
  103. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -809,7 +809,7 @@ const ROUTE_PREFIX$5 = "/api/dsh-chatgpt-subscription";
809
809
  * `test/responses-client.test.ts` asserts the two agree, so a release that
810
810
  * forgets this constant fails the suite instead of shipping quietly.
811
811
  */
812
- const PLUGIN_VERSION = "0.65.0";
812
+ const PLUGIN_VERSION = "0.66.0";
813
813
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
814
814
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
815
815
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
@@ -3656,10 +3656,18 @@ var AccountPoolCore = class {
3656
3656
  return true;
3657
3657
  }
3658
3658
  /** The account a rotation strategy picks out of an already-eligible set. */
3659
- selectAccount(eligible, data) {
3659
+ selectAccount(eligible, data, hint) {
3660
3660
  const pinned = this.hooks.preferAccountId?.() ?? null;
3661
3661
  const pinnedAccount = pinned === null ? void 0 : eligible.find((account) => account.id === pinned);
3662
3662
  if (pinnedAccount !== void 0) return pinnedAccount;
3663
+ const preferred = hint?.preferAccountIds;
3664
+ if (preferred !== void 0 && preferred.length > 0) {
3665
+ const eligibleById = new Map(eligible.map((account) => [account.id, account]));
3666
+ for (const id of preferred) {
3667
+ const account = eligibleById.get(id);
3668
+ if (account !== void 0) return account;
3669
+ }
3670
+ }
3663
3671
  if (data.rotationStrategy === "round-robin") return [...eligible].sort((a, b) => (a.lastUsedAt || 0) - (b.lastUsedAt || 0))[0];
3664
3672
  if (data.rotationStrategy === "sticky") return eligible.find((account) => account.id === data.activeAccountId) || eligible.find((account) => account.isPrimary) || eligible[0];
3665
3673
  return eligible.find((account) => account.isPrimary) || eligible[0];
@@ -3712,15 +3720,18 @@ var AccountPoolCore = class {
3712
3720
  * @param forceRefresh - renew even when the stored token still looks usable.
3713
3721
  * @param options - what the refresh may spend, passed down to the provider's
3714
3722
  * refresh hook. Absent means the provider's own default budget.
3723
+ * @param selection - this request's own account preference, when the caller
3724
+ * has one (a warm prefix cache, for instance). Absent means the rotation
3725
+ * strategy decides alone, exactly as before.
3715
3726
  * @throws LlmError with `RATE_LIMIT` when every account is cooling down.
3716
3727
  */
3717
- async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false, options) {
3728
+ async getEffectiveAccount(excludeIds, fetchFn = fetch, forceRefresh = false, options, selection) {
3718
3729
  const data = await this.read();
3719
3730
  if (data.accounts.length === 0) throw new Error(this.hooks.emptyMessage ?? t("pool.notSignedIn", { provider: this.hooks.displayName }));
3720
3731
  const now = Date.now();
3721
3732
  const eligible = data.accounts.filter((account) => this.isEligible(account, now, excludeIds));
3722
3733
  if (eligible.length === 0) throw this.allUnavailableError(data, now);
3723
- const selected = this.selectAccount(eligible, data);
3734
+ const selected = this.selectAccount(eligible, data, selection);
3724
3735
  let refreshed;
3725
3736
  try {
3726
3737
  refreshed = await this.freshAccount(selected, fetchFn, forceRefresh, options);
@@ -3728,7 +3739,7 @@ var AccountPoolCore = class {
3728
3739
  if (this.hooks.refreshFailureStatus?.(error) === void 0 && !(error instanceof PoolAccountUnavailableError)) throw error;
3729
3740
  const nextExclude = new Set(excludeIds ?? []);
3730
3741
  nextExclude.add(selected.id);
3731
- if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh, options);
3742
+ if (await this.hasAnotherAvailableAccount(nextExclude)) return this.getEffectiveAccount(nextExclude, fetchFn, forceRefresh, options, selection);
3732
3743
  throw error;
3733
3744
  }
3734
3745
  const account = await this.updatePool((current) => {
@@ -4477,7 +4488,7 @@ var OAuthService = class {
4477
4488
  throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device code could not be issued (${response.status}${detail === null ? "" : `, ${detail}`}).`, response.status);
4478
4489
  }
4479
4490
  const payload = await response.json().catch(() => null);
4480
- const record = isRecord$67(payload) ? payload : {};
4491
+ const record = isRecord$70(payload) ? payload : {};
4481
4492
  const authId = nonEmptyString$11(record.device_auth_id);
4482
4493
  const userCode = nonEmptyString$11(record.user_code) ?? nonEmptyString$11(record.usercode);
4483
4494
  if (authId === void 0 || userCode === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT returned no device code.");
@@ -4544,7 +4555,7 @@ var OAuthService = class {
4544
4555
  const detail = deviceErrorIdentifier(payload);
4545
4556
  throw new OAuthServiceError("oauth-token-exchange-failed", `The ChatGPT device confirmation was refused (${response.status}${detail === void 0 ? "" : `, ${detail}`}).`, response.status);
4546
4557
  }
4547
- const record = isRecord$67(payload) ? payload : {};
4558
+ const record = isRecord$70(payload) ? payload : {};
4548
4559
  const authorizationCode = nonEmptyString$11(record.authorization_code);
4549
4560
  const codeVerifier = nonEmptyString$11(record.code_verifier);
4550
4561
  if (authorizationCode === void 0 || codeVerifier === void 0) throw new OAuthServiceError("oauth-token-exchange-failed", "ChatGPT confirmed the device code but returned no authorization code.");
@@ -4877,11 +4888,11 @@ const DEVICE_PENDING_STATUSES = /* @__PURE__ */ new Set([
4877
4888
  function isPendingDeviceAnswer(response, payload) {
4878
4889
  if (response.status === 403 || response.status === 404) return true;
4879
4890
  if (!response.ok && response.status !== 400) return false;
4880
- if (!isRecord$67(payload)) return false;
4891
+ if (!isRecord$70(payload)) return false;
4881
4892
  const status = payload.status;
4882
4893
  if (typeof status === "string" && DEVICE_PENDING_STATUSES.has(status.toLowerCase())) return true;
4883
4894
  const error = payload.error;
4884
- if (isRecord$67(error)) {
4895
+ if (isRecord$70(error)) {
4885
4896
  const code = error.code;
4886
4897
  if (typeof code === "string" && DEVICE_PENDING_STATUSES.has(code.toLowerCase())) return true;
4887
4898
  }
@@ -4895,18 +4906,18 @@ function isPendingDeviceAnswer(response, payload) {
4895
4906
  * wrong.
4896
4907
  */
4897
4908
  function deviceErrorIdentifier(payload) {
4898
- if (!isRecord$67(payload)) return void 0;
4909
+ if (!isRecord$70(payload)) return void 0;
4899
4910
  const candidates = [payload.code];
4900
4911
  const error = payload.error;
4901
4912
  if (typeof error === "string") candidates.push(error);
4902
- else if (isRecord$67(error)) candidates.push(error.code, error.type);
4913
+ else if (isRecord$70(error)) candidates.push(error.code, error.type);
4903
4914
  candidates.push(payload.status);
4904
4915
  for (const candidate of candidates) if (typeof candidate === "string" && /^[a-z0-9_.-]{1,64}$/i.test(candidate)) return candidate;
4905
4916
  }
4906
4917
  function nonEmptyString$11(value) {
4907
4918
  return typeof value === "string" && value !== "" ? value : void 0;
4908
4919
  }
4909
- function isRecord$67(value) {
4920
+ function isRecord$70(value) {
4910
4921
  return typeof value === "object" && value !== null && !Array.isArray(value);
4911
4922
  }
4912
4923
  async function oauthErrorIdentifier(response) {
@@ -5087,7 +5098,7 @@ const CODEX_CLI_CREDENTIAL_SOURCE = "codex";
5087
5098
  * here, and the import has to be repeated afterwards.
5088
5099
  */
5089
5100
  const ADOPTED_CODEX_CREDENTIAL_EXPIRED_HINT = "The sign-in imported from the Codex CLI has expired. Sign in again in the Codex CLI, then import it again from this settings card.";
5090
- function isRecord$66(value) {
5101
+ function isRecord$69(value) {
5091
5102
  return typeof value === "object" && value !== null && !Array.isArray(value);
5092
5103
  }
5093
5104
  /** A trimmed, non-empty string, or nothing. */
@@ -5189,7 +5200,7 @@ function decodeJwtClaims$1(token) {
5189
5200
  if (parts.length !== 3) return void 0;
5190
5201
  try {
5191
5202
  const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
5192
- return isRecord$66(value) ? value : void 0;
5203
+ return isRecord$69(value) ? value : void 0;
5193
5204
  } catch {
5194
5205
  return;
5195
5206
  }
@@ -5234,10 +5245,10 @@ function expiryFromAccessToken(accessToken) {
5234
5245
  * the file carries cannot reach the result.
5235
5246
  */
5236
5247
  function parseAdoptedCodexCredential(value, sourcePath) {
5237
- if (!isRecord$66(value)) return void 0;
5248
+ if (!isRecord$69(value)) return void 0;
5238
5249
  if (value.auth_mode !== "chatgpt") return void 0;
5239
5250
  const tokens = value[CODEX_CLI_TOKENS_KEY];
5240
- if (!isRecord$66(tokens)) return void 0;
5251
+ if (!isRecord$69(tokens)) return void 0;
5241
5252
  const accessToken = nonEmptyString$10(tokens.access_token);
5242
5253
  if (accessToken === void 0) return void 0;
5243
5254
  const refreshToken = nonEmptyString$10(tokens.refresh_token);
@@ -5256,7 +5267,7 @@ function parseAdoptedCodexCredential(value, sourcePath) {
5256
5267
  const email = claims === void 0 ? void 0 : nonEmptyString$10(claims.email);
5257
5268
  if (email !== void 0) credentials.email = email;
5258
5269
  const authClaims = claims === void 0 ? void 0 : claims[CODEX_AUTH_CLAIMS_KEY];
5259
- const planType = isRecord$66(authClaims) ? nonEmptyString$10(authClaims.chatgpt_plan_type) : void 0;
5270
+ const planType = isRecord$69(authClaims) ? nonEmptyString$10(authClaims.chatgpt_plan_type) : void 0;
5260
5271
  if (planType !== void 0) credentials.planType = planType;
5261
5272
  }
5262
5273
  const accountId = nonEmptyString$10(tokens.account_id);
@@ -8149,7 +8160,7 @@ const schemaFields = /* @__PURE__ */ new Set([
8149
8160
  "required",
8150
8161
  "propertyOrdering"
8151
8162
  ]);
8152
- function isRecord$65(value) {
8163
+ function isRecord$68(value) {
8153
8164
  return typeof value === "object" && value !== null && !Array.isArray(value);
8154
8165
  }
8155
8166
  function mergeSchemas(left, right) {
@@ -8157,7 +8168,7 @@ function mergeSchemas(left, right) {
8157
8168
  ...left,
8158
8169
  ...right
8159
8170
  };
8160
- if (isRecord$65(left.properties) && isRecord$65(right.properties)) merged.properties = {
8171
+ if (isRecord$68(left.properties) && isRecord$68(right.properties)) merged.properties = {
8161
8172
  ...left.properties,
8162
8173
  ...right.properties
8163
8174
  };
@@ -8193,9 +8204,9 @@ const mergedTypeKeys = /* @__PURE__ */ new Set([
8193
8204
  * model with strictly less than the schema it started from.
8194
8205
  */
8195
8206
  function describeBranch(branch) {
8196
- const type = typeof branch.type === "string" ? branch.type : isRecord$65(branch.properties) ? "object" : isRecord$65(branch.items) ? "array" : "value";
8207
+ const type = typeof branch.type === "string" ? branch.type : isRecord$68(branch.properties) ? "object" : isRecord$68(branch.items) ? "array" : "value";
8197
8208
  const details = [];
8198
- if (isRecord$65(branch.properties)) {
8209
+ if (isRecord$68(branch.properties)) {
8199
8210
  const names = Object.keys(branch.properties);
8200
8211
  if (names.length > 0) details.push(`{${names.join(",")}}`);
8201
8212
  }
@@ -8209,7 +8220,7 @@ function describeBranch(branch) {
8209
8220
  }
8210
8221
  /** How well one branch can stand in for a whole union; a named object shape wins. */
8211
8222
  function branchRank(branch) {
8212
- if (isRecord$65(branch.properties)) return 3;
8223
+ if (isRecord$68(branch.properties)) return 3;
8213
8224
  if (branch.type === "object") return 2;
8214
8225
  if (branch.type === "array") return 1;
8215
8226
  return 0;
@@ -8238,7 +8249,7 @@ function unionShape(branches) {
8238
8249
  shape: branches[0],
8239
8250
  folded: false
8240
8251
  };
8241
- if (new Set(branches.map((branch) => branch.type)).size === 1 && branches.every((branch) => !isRecord$65(branch.properties) && !isRecord$65(branch.items))) {
8252
+ if (new Set(branches.map((branch) => branch.type)).size === 1 && branches.every((branch) => !isRecord$68(branch.properties) && !isRecord$68(branch.items))) {
8242
8253
  const shape = { type: branches[0].type };
8243
8254
  if (branches.every((branch) => Array.isArray(branch.enum))) {
8244
8255
  const values = [...new Set(branches.flatMap((branch) => branch.enum))];
@@ -8275,7 +8286,7 @@ function foldOwnFields(node) {
8275
8286
  const out = {};
8276
8287
  for (const [key, value] of Object.entries(node)) {
8277
8288
  if (key === "anyOf" || key === "oneOf" || key === "allOf") continue;
8278
- if (key === "properties" && isRecord$65(value)) {
8289
+ if (key === "properties" && isRecord$68(value)) {
8279
8290
  out.properties = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, foldSchemaUnions(child)]));
8280
8291
  continue;
8281
8292
  }
@@ -8304,15 +8315,15 @@ function foldOwnFields(node) {
8304
8315
  * argument the model sends anyway fails there, naming the actual problem.
8305
8316
  */
8306
8317
  function foldSchemaUnions(node) {
8307
- if (!isRecord$65(node)) return node;
8318
+ if (!isRecord$68(node)) return node;
8308
8319
  let own = foldOwnFields(node);
8309
8320
  if (Array.isArray(node.allOf)) for (const branch of node.allOf) {
8310
8321
  const folded = foldSchemaUnions(branch);
8311
- if (isRecord$65(folded)) own = mergeSchemas(own, folded);
8322
+ if (isRecord$68(folded)) own = mergeSchemas(own, folded);
8312
8323
  }
8313
8324
  const alternatives = Array.isArray(node.anyOf) ? node.anyOf : Array.isArray(node.oneOf) ? node.oneOf : [];
8314
8325
  if (alternatives.length === 0) return own;
8315
- const branches = alternatives.map((branch) => foldSchemaUnions(branch)).filter(isRecord$65);
8326
+ const branches = alternatives.map((branch) => foldSchemaUnions(branch)).filter(isRecord$68);
8316
8327
  if (branches.length === 0) return own;
8317
8328
  const shapes = branches.filter((branch) => branch.type !== "null");
8318
8329
  const acceptsNull = branches.some((branch) => branch.type === "null");
@@ -8328,7 +8339,7 @@ function foldSchemaUnions(node) {
8328
8339
  return merged;
8329
8340
  }
8330
8341
  function toAntigravityToolSchema(schema, options = {}) {
8331
- if (!isRecord$65(schema)) return schema;
8342
+ if (!isRecord$68(schema)) return schema;
8332
8343
  const root = schema;
8333
8344
  function resolveReference(reference) {
8334
8345
  let target = root;
@@ -8336,13 +8347,13 @@ function toAntigravityToolSchema(schema, options = {}) {
8336
8347
  const path = reference === "#" ? [] : reference.slice(2).split("/");
8337
8348
  for (const segment of path) {
8338
8349
  const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
8339
- target = isRecord$65(target) && Object.hasOwn(target, key) ? target[key] : void 0;
8350
+ target = isRecord$68(target) && Object.hasOwn(target, key) ? target[key] : void 0;
8340
8351
  }
8341
- if (!isRecord$65(target)) throw new LlmError(`Antigravity tool schema reference was not found: ${reference}`, "PROVIDER_ERROR");
8352
+ if (!isRecord$68(target)) throw new LlmError(`Antigravity tool schema reference was not found: ${reference}`, "PROVIDER_ERROR");
8342
8353
  return target;
8343
8354
  }
8344
8355
  function convert(node, references) {
8345
- if (!isRecord$65(node)) return {};
8356
+ if (!isRecord$68(node)) return {};
8346
8357
  let inherited = {};
8347
8358
  if (typeof node.$ref === "string") {
8348
8359
  if (references.has(node.$ref)) throw new LlmError(`Antigravity tool schema contains a recursive reference: ${node.$ref}`, "PROVIDER_ERROR");
@@ -8351,8 +8362,8 @@ function toAntigravityToolSchema(schema, options = {}) {
8351
8362
  if (Array.isArray(node.allOf)) for (const branch of node.allOf) inherited = mergeSchemas(inherited, convert(branch, references));
8352
8363
  const out = {};
8353
8364
  for (const [key, value] of Object.entries(node)) if (schemaFields.has(key)) out[key] = value;
8354
- if (isRecord$65(node.properties)) out.properties = Object.fromEntries(Object.entries(node.properties).map(([name, child]) => [name, convert(child, references)]));
8355
- if (isRecord$65(node.items)) out.items = convert(node.items, references);
8365
+ if (isRecord$68(node.properties)) out.properties = Object.fromEntries(Object.entries(node.properties).map(([name, child]) => [name, convert(child, references)]));
8366
+ if (isRecord$68(node.items)) out.items = convert(node.items, references);
8356
8367
  const alternatives = Array.isArray(node.anyOf) ? node.anyOf : node.oneOf;
8357
8368
  if (Array.isArray(alternatives)) out.anyOf = alternatives.map((child) => convert(child, references));
8358
8369
  if (Array.isArray(node.type)) {
@@ -8388,7 +8399,7 @@ let toolCallCounter = 0;
8388
8399
  function sanitizeText$9(text) {
8389
8400
  return text.replace(/\0/g, "");
8390
8401
  }
8391
- function isRecord$64(value) {
8402
+ function isRecord$67(value) {
8392
8403
  return typeof value === "object" && value !== null && !Array.isArray(value);
8393
8404
  }
8394
8405
  function asString$30(value) {
@@ -8408,25 +8419,25 @@ function toolCallIdNeeded(modelId, runtimeModel) {
8408
8419
  return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-") || runtimeModel.startsWith("claude-") || runtimeModel.startsWith("gpt-oss-");
8409
8420
  }
8410
8421
  function parseArguments$1(raw) {
8411
- if (isRecord$64(raw)) return raw;
8422
+ if (isRecord$67(raw)) return raw;
8412
8423
  if (raw === void 0 || raw === null || raw === "") return {};
8413
8424
  const parsed = typeof raw === "string" ? safeJsonParse$9(raw) : raw;
8414
- return isRecord$64(parsed) ? parsed : {};
8425
+ return isRecord$67(parsed) ? parsed : {};
8415
8426
  }
8416
8427
  const NO_RESOLVED_IMAGES$5 = /* @__PURE__ */ new Map();
8417
8428
  function attachmentOf$5(block) {
8418
8429
  const attachment = block.attachment;
8419
- if (!isRecord$64(attachment)) return void 0;
8430
+ if (!isRecord$67(attachment)) return void 0;
8420
8431
  return typeof attachment.attachmentId === "string" ? attachment : void 0;
8421
8432
  }
8422
8433
  function attachmentLabel$6(block) {
8423
- const attachment = isRecord$64(block.attachment) ? block.attachment : void 0;
8434
+ const attachment = isRecord$67(block.attachment) ? block.attachment : void 0;
8424
8435
  return asString$30(attachment?.name) || asString$30(attachment?.attachmentId);
8425
8436
  }
8426
8437
  function collectImageRefs$3(content, refs) {
8427
8438
  if (!Array.isArray(content)) return;
8428
8439
  for (const block of content) {
8429
- if (!isRecord$64(block)) continue;
8440
+ if (!isRecord$67(block)) continue;
8430
8441
  if (block.type === "image") {
8431
8442
  const attachment = attachmentOf$5(block);
8432
8443
  if (attachment) refs.set(attachment.attachmentId, attachment);
@@ -8559,7 +8570,7 @@ function unavailableImageText$6(block) {
8559
8570
  }
8560
8571
  function imageBlockToPart(block, images) {
8561
8572
  let data = asString$30(block.data) || asString$30(block.base64);
8562
- const source = isRecord$64(block.source) ? block.source : void 0;
8573
+ const source = isRecord$67(block.source) ? block.source : void 0;
8563
8574
  if (!data && source) data = asString$30(source.data) || asString$30(source.base64);
8564
8575
  let mimeType = asString$30(block.mimeType) || asString$30(block.mediaType) || (source ? asString$30(source.mimeType) || asString$30(source.mediaType) : void 0) || "image/png";
8565
8576
  if (data?.startsWith("data:")) {
@@ -8584,8 +8595,8 @@ function contentToUserParts(content, images) {
8584
8595
  if (typeof content === "string") return [{ text: sanitizeText$9(content) }];
8585
8596
  if (!Array.isArray(content)) return [];
8586
8597
  const parts = [];
8587
- for (const block of content) if (isRecord$64(block) && block.type === "text" && typeof block.text === "string") parts.push({ text: sanitizeText$9(block.text) });
8588
- else if (isRecord$64(block) && block.type === "image") {
8598
+ for (const block of content) if (isRecord$67(block) && block.type === "text" && typeof block.text === "string") parts.push({ text: sanitizeText$9(block.text) });
8599
+ else if (isRecord$67(block) && block.type === "image") {
8589
8600
  const img = imageBlockToPart(block, images);
8590
8601
  parts.push(img ?? { text: unavailableImageText$6(block) });
8591
8602
  }
@@ -8600,7 +8611,7 @@ function toolResultImageParts(blocks, images) {
8600
8611
  if (!Array.isArray(blocks)) return [];
8601
8612
  const parts = [];
8602
8613
  for (const block of blocks) {
8603
- if (!isRecord$64(block)) continue;
8614
+ if (!isRecord$67(block)) continue;
8604
8615
  if (block.type === "image") {
8605
8616
  parts.push(imageBlockToPart(block, images) ?? { text: unavailableImageText$6(block) });
8606
8617
  continue;
@@ -8612,7 +8623,7 @@ function toolResultImageParts(blocks, images) {
8612
8623
  function toolResultText$9(blocks) {
8613
8624
  if (!Array.isArray(blocks)) return "";
8614
8625
  return blocks.map((block) => {
8615
- if (!isRecord$64(block)) return "";
8626
+ if (!isRecord$67(block)) return "";
8616
8627
  if (block.type === "text" && typeof block.text === "string") return sanitizeText$9(block.text);
8617
8628
  if (block.type === "tool-result") return toolResultText$9(block.content);
8618
8629
  if (block.type === "image") {
@@ -8626,9 +8637,9 @@ function replayBlockFor$1(message, index) {
8626
8637
  const source = message.source;
8627
8638
  if (!source || source.kind !== "model" || source.provider !== "antigravity") return void 0;
8628
8639
  const state = source.replayState;
8629
- if (!isRecord$64(state)) return void 0;
8640
+ if (!isRecord$67(state)) return void 0;
8630
8641
  if (Array.isArray(state.blocks)) return state.blocks[index];
8631
- const resp = isRecord$64(state.response) ? state.response : void 0;
8642
+ const resp = isRecord$67(state.response) ? state.response : void 0;
8632
8643
  if (resp) {
8633
8644
  if (Array.isArray(resp.outputItems)) return resp.outputItems[index];
8634
8645
  if (Array.isArray(resp.blocks)) return resp.blocks[index];
@@ -8730,10 +8741,10 @@ function assistantParts(message, model, runtimeModel, toolCalls) {
8730
8741
  if (!Array.isArray(message.content)) return parts;
8731
8742
  for (let index = 0; index < message.content.length; index++) {
8732
8743
  const block = message.content[index];
8733
- if (!isRecord$64(block)) continue;
8744
+ if (!isRecord$67(block)) continue;
8734
8745
  if (block.type === "reasoning" && runtimeModel.startsWith("claude-") && (message.source?.kind !== "model" || message.source.provider !== "antigravity" || !("model" in message.source) || message.source.model !== model.id)) continue;
8735
8746
  const replay = replayBlockFor$1(message, index);
8736
- const originalParts = Array.isArray(replay?.parts) ? replay.parts.filter(isRecord$64) : [];
8747
+ const originalParts = Array.isArray(replay?.parts) ? replay.parts.filter(isRecord$67) : [];
8737
8748
  if ((block.type === "text" || block.type === "reasoning") && originalParts.length > 0 && originalParts.every((part) => !part.functionCall) && originalParts.map((part) => asString$30(part.text) || "").join("") === sanitizeText$9(String(block.text || ""))) {
8738
8749
  parts.push(...originalParts.map(replayPart));
8739
8750
  continue;
@@ -8754,8 +8765,8 @@ function assistantParts(message, model, runtimeModel, toolCalls) {
8754
8765
  } else if (block.type === "tool-call") {
8755
8766
  const toolId = String(block.id || "");
8756
8767
  const toolName = String(block.name || "");
8757
- const originalCall = originalParts.find((part) => isRecord$64(part.functionCall));
8758
- const wireId = asString$30((isRecord$64(originalCall?.functionCall) ? originalCall.functionCall : void 0)?.id) || (toolCallIdNeeded(model.id, runtimeModel) ? sanitizeToolCallId(toolId, toolName) : originalCall ? void 0 : toolId || void 0);
8768
+ const originalCall = originalParts.find((part) => isRecord$67(part.functionCall));
8769
+ const wireId = asString$30((isRecord$67(originalCall?.functionCall) ? originalCall.functionCall : void 0)?.id) || (toolCallIdNeeded(model.id, runtimeModel) ? sanitizeToolCallId(toolId, toolName) : originalCall ? void 0 : toolId || void 0);
8759
8770
  toolCalls.set(toolId, {
8760
8771
  name: toolName,
8761
8772
  id: wireId
@@ -8807,7 +8818,7 @@ function convertMessages(options, model, runtimeModel, images = NO_RESOLVED_IMAG
8807
8818
  continue;
8808
8819
  }
8809
8820
  const content = Array.isArray(message.content) ? message.content : [];
8810
- const userParts = contentToUserParts(content.filter((b) => !isRecord$64(b) || b.type !== "tool-result"), images);
8821
+ const userParts = contentToUserParts(content.filter((b) => !isRecord$67(b) || b.type !== "tool-result"), images);
8811
8822
  if (role === "system") {
8812
8823
  if (userParts.length) contents.push({
8813
8824
  role: GEMINI_ROLE.user,
@@ -8819,7 +8830,7 @@ function convertMessages(options, model, runtimeModel, images = NO_RESOLVED_IMAG
8819
8830
  role: GEMINI_ROLE.user,
8820
8831
  parts: userParts
8821
8832
  });
8822
- for (const b of content) if (isRecord$64(b) && b.type === "tool-result") pushToolResult(contents, b, toolCalls, model, runtimeModel, images);
8833
+ for (const b of content) if (isRecord$67(b) && b.type === "tool-result") pushToolResult(contents, b, toolCalls, model, runtimeModel, images);
8823
8834
  }
8824
8835
  return contents;
8825
8836
  }
@@ -8962,7 +8973,7 @@ const USAGE_FIELDS = [
8962
8973
  "totalTokenCount"
8963
8974
  ];
8964
8975
  function collectUsage(value, state) {
8965
- if (!isRecord$64(value)) return;
8976
+ if (!isRecord$67(value)) return;
8966
8977
  for (const key of USAGE_FIELDS) {
8967
8978
  const count = value[key];
8968
8979
  if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) continue;
@@ -8992,17 +9003,17 @@ function processStreamLine$4(line, state) {
8992
9003
  }
8993
9004
  if (!json) return [];
8994
9005
  const chunk = safeJsonParse$9(json);
8995
- if (!isRecord$64(chunk)) return [];
8996
- const responseData = isRecord$64(chunk.response) ? chunk.response : chunk;
8997
- const error = isRecord$64(chunk.error) ? chunk.error : isRecord$64(responseData.error) ? responseData.error : void 0;
9006
+ if (!isRecord$67(chunk)) return [];
9007
+ const responseData = isRecord$67(chunk.response) ? chunk.response : chunk;
9008
+ const error = isRecord$67(chunk.error) ? chunk.error : isRecord$67(responseData.error) ? responseData.error : void 0;
8998
9009
  if (error !== void 0) throw new LlmError$1(`Antigravity stream error: ${asString$30(error.message) ?? "unknown error"}`, isContextOverflow(error) ? CONTEXT_OVERFLOW_CODE : "PROVIDER_ERROR");
8999
9010
  const candidates = Array.isArray(responseData.candidates) ? responseData.candidates : [];
9000
- const candidate = isRecord$64(candidates[0]) ? candidates[0] : void 0;
9001
- const content = isRecord$64(candidate?.content) ? candidate.content : void 0;
9011
+ const candidate = isRecord$67(candidates[0]) ? candidates[0] : void 0;
9012
+ const content = isRecord$67(candidate?.content) ? candidate.content : void 0;
9002
9013
  const parts = Array.isArray(content?.parts) ? content.parts : [];
9003
9014
  const out = [];
9004
9015
  for (const part of parts) {
9005
- if (!isRecord$64(part)) continue;
9016
+ if (!isRecord$67(part)) continue;
9006
9017
  if (typeof part.text === "string" && part.text !== "") {
9007
9018
  const isThinking = Boolean(part.thought);
9008
9019
  const blockType = isThinking ? "reasoning" : "text";
@@ -9034,7 +9045,7 @@ function processStreamLine$4(line, state) {
9034
9045
  index: state.currentBlock.index,
9035
9046
  text: delta
9036
9047
  });
9037
- } else if (!isRecord$64(part.functionCall) && thoughtSignature(part)) {
9048
+ } else if (!isRecord$67(part.functionCall) && thoughtSignature(part)) {
9038
9049
  if (state.replayBlocks.length === 0) {
9039
9050
  const type = part.thought ? "reasoning" : "text";
9040
9051
  state.blocks.push({
@@ -9058,12 +9069,12 @@ function processStreamLine$4(line, state) {
9058
9069
  }
9059
9070
  state.replayBlocks[state.replayBlocks.length - 1].parts.push(replayPart(part));
9060
9071
  }
9061
- if (isRecord$64(part.functionCall)) {
9072
+ if (isRecord$67(part.functionCall)) {
9062
9073
  out.push(...closeCurrentBlock(state));
9063
9074
  const fc = part.functionCall;
9064
9075
  const toolName = asString$30(fc.name) || "";
9065
9076
  const toolId = asString$30(fc.id) || sanitizeToolCallId("", toolName);
9066
- const argsText = JSON.stringify(isRecord$64(fc.args) ? fc.args : {});
9077
+ const argsText = JSON.stringify(isRecord$67(fc.args) ? fc.args : {});
9067
9078
  const index = state.blocks.length;
9068
9079
  const block = {
9069
9080
  type: "tool-call",
@@ -9102,7 +9113,7 @@ function processStreamLine$4(line, state) {
9102
9113
  if (responseData !== chunk) collectUsage(responseData.usageMetadata, state);
9103
9114
  const finishMessage = asString$30(candidate?.finishMessage);
9104
9115
  if (finishMessage) state.finishMessage = finishMessage;
9105
- const blockReason = asString$30((isRecord$64(responseData.promptFeedback) ? responseData.promptFeedback : void 0)?.blockReason);
9116
+ const blockReason = asString$30((isRecord$67(responseData.promptFeedback) ? responseData.promptFeedback : void 0)?.blockReason);
9106
9117
  const finishReason = asString$30(candidate?.finishReason) || asString$30(responseData.finishReason) || (blockReason ? `PROMPT_BLOCKED:${blockReason}` : void 0);
9107
9118
  if (finishReason) {
9108
9119
  state.finishReason = finishReason;
@@ -9175,7 +9186,7 @@ function closeStream$7(state) {
9175
9186
  *
9176
9187
  * @module dsh-chatgpt-subscription/request-images
9177
9188
  */
9178
- function isRecord$63(value) {
9189
+ function isRecord$66(value) {
9179
9190
  return typeof value === "object" && value !== null && !Array.isArray(value);
9180
9191
  }
9181
9192
  function asString$29(value) {
@@ -9234,17 +9245,17 @@ const SUPPORTED_IMAGE_MEDIA_TYPES$4 = /* @__PURE__ */ new Set([
9234
9245
  ]);
9235
9246
  function attachmentOf$4(block) {
9236
9247
  const attachment = block.attachment;
9237
- if (!isRecord$63(attachment)) return void 0;
9248
+ if (!isRecord$66(attachment)) return void 0;
9238
9249
  return typeof attachment.attachmentId === "string" ? attachment : void 0;
9239
9250
  }
9240
9251
  function attachmentLabel$5(block) {
9241
- const attachment = isRecord$63(block.attachment) ? block.attachment : void 0;
9252
+ const attachment = isRecord$66(block.attachment) ? block.attachment : void 0;
9242
9253
  return asString$29(attachment?.name) || asString$29(attachment?.attachmentId);
9243
9254
  }
9244
9255
  function collectImageRefs$2(content, refs) {
9245
9256
  if (!Array.isArray(content)) return;
9246
9257
  for (const block of content) {
9247
- if (!isRecord$63(block)) continue;
9258
+ if (!isRecord$66(block)) continue;
9248
9259
  if (block.type === "image") {
9249
9260
  const attachment = attachmentOf$4(block);
9250
9261
  if (attachment) refs.set(attachment.attachmentId, attachment);
@@ -9274,7 +9285,7 @@ function base64Length$2(bytes) {
9274
9285
  */
9275
9286
  function inlinePayloadOf$1(block) {
9276
9287
  let data = asString$29(block.data) || asString$29(block.base64);
9277
- const source = isRecord$63(block.source) ? block.source : void 0;
9288
+ const source = isRecord$66(block.source) ? block.source : void 0;
9278
9289
  if (!data && source) data = asString$29(source.data) || asString$29(source.base64);
9279
9290
  if (data?.startsWith("data:")) {
9280
9291
  const matched = data.match(/^data:([^;,]+);base64,(.*)$/s);
@@ -9304,7 +9315,7 @@ function requestImagePayloadLength(block, images) {
9304
9315
  function collectRequestImagePayloads(content, images, lengths) {
9305
9316
  if (!Array.isArray(content)) return;
9306
9317
  for (const block of content) {
9307
- if (!isRecord$63(block)) continue;
9318
+ if (!isRecord$66(block)) continue;
9308
9319
  if (block.type === "image") {
9309
9320
  const bytes = requestImagePayloadLength(block, images);
9310
9321
  if (bytes !== void 0) lengths.push(bytes);
@@ -9358,7 +9369,7 @@ function replaceOldestRequestImages(options, count, images) {
9358
9369
  let replaced = false;
9359
9370
  return {
9360
9371
  content: content.map((block) => {
9361
- if (remaining.count === 0 || !isRecord$63(block)) return block;
9372
+ if (remaining.count === 0 || !isRecord$66(block)) return block;
9362
9373
  if (block.type === "image") {
9363
9374
  if (requestImagePayloadLength(block, images) === void 0) return block;
9364
9375
  remaining.count -= 1;
@@ -9500,7 +9511,7 @@ function collectImageOccurrences(options, images) {
9500
9511
  const walk = (content) => {
9501
9512
  if (!Array.isArray(content)) return;
9502
9513
  for (const block of content) {
9503
- if (!isRecord$63(block)) continue;
9514
+ if (!isRecord$66(block)) continue;
9504
9515
  if (block.type === "image") {
9505
9516
  const bytes = requestImagePayloadLength(block, images);
9506
9517
  const key = requestImageKey(block, images);
@@ -9554,7 +9565,7 @@ function replaceDroppedRequestImages(options, droppedKeys, images) {
9554
9565
  let replaced = false;
9555
9566
  return {
9556
9567
  content: content.map((block) => {
9557
- if (!isRecord$63(block)) return block;
9568
+ if (!isRecord$66(block)) return block;
9558
9569
  if (block.type === "image") {
9559
9570
  const key = requestImageKey(block, images);
9560
9571
  if (key === void 0 || !droppedKeys.has(key)) return block;
@@ -9739,7 +9750,7 @@ function unavailableImageText$5(block) {
9739
9750
  }
9740
9751
  function imageBlockToInline$3(block, images) {
9741
9752
  let data = asString$29(block.data) || asString$29(block.base64);
9742
- const source = isRecord$63(block.source) ? block.source : void 0;
9753
+ const source = isRecord$66(block.source) ? block.source : void 0;
9743
9754
  if (!data && source) data = asString$29(source.data) || asString$29(source.base64);
9744
9755
  let mediaType = asString$29(block.mimeType) || asString$29(block.mediaType) || (source ? asString$29(source.mimeType) || asString$29(source.mediaType) : void 0) || "image/png";
9745
9756
  if (data?.startsWith("data:")) {
@@ -9761,6 +9772,549 @@ function imageBlockToInline$3(block, images) {
9761
9772
  } : void 0;
9762
9773
  }
9763
9774
  //#endregion
9775
+ //#region src/host/common/prompt-cache-policy.ts
9776
+ const MINUTE_MS = 6e4;
9777
+ const MAX_CACHE_TTL_MS = 720 * 60 * MINUTE_MS;
9778
+ const DISABLED = Object.freeze({
9779
+ mode: "none",
9780
+ scope: "unknown",
9781
+ staticPrefix: false,
9782
+ coldStart: "off",
9783
+ stickyImages: false,
9784
+ affinity: false,
9785
+ waitWarmup: false,
9786
+ hint: false
9787
+ });
9788
+ function policy(input) {
9789
+ return Object.freeze(input);
9790
+ }
9791
+ /** Built-in facts already represented by a line's mapper, adapter, or official client. */
9792
+ const PROMPT_CACHE_POLICIES = Object.freeze({
9793
+ "kimi-code": policy({
9794
+ mode: "automatic",
9795
+ scope: "account",
9796
+ ttlMs: 25 * MINUTE_MS,
9797
+ staticPrefix: true,
9798
+ coldStart: "warm",
9799
+ stickyImages: true,
9800
+ affinity: true,
9801
+ waitWarmup: true,
9802
+ hint: true
9803
+ }),
9804
+ "codex-chatgpt": policy({
9805
+ mode: "automatic",
9806
+ scope: "unknown",
9807
+ staticPrefix: true,
9808
+ coldStart: "in-flight",
9809
+ stickyImages: false,
9810
+ affinity: false,
9811
+ waitWarmup: false,
9812
+ hint: false
9813
+ }),
9814
+ antigravity: policy({
9815
+ mode: "automatic",
9816
+ scope: "unknown",
9817
+ staticPrefix: true,
9818
+ coldStart: "in-flight",
9819
+ stickyImages: false,
9820
+ affinity: false,
9821
+ waitWarmup: false,
9822
+ hint: false
9823
+ }),
9824
+ "step-code": policy({
9825
+ mode: "automatic",
9826
+ scope: "unknown",
9827
+ ttlMs: 5 * MINUTE_MS,
9828
+ staticPrefix: true,
9829
+ coldStart: "in-flight",
9830
+ stickyImages: false,
9831
+ affinity: false,
9832
+ waitWarmup: false,
9833
+ hint: true
9834
+ }),
9835
+ claude: policy({
9836
+ mode: "explicit",
9837
+ scope: "unknown",
9838
+ staticPrefix: true,
9839
+ coldStart: "in-flight",
9840
+ stickyImages: true,
9841
+ affinity: false,
9842
+ waitWarmup: false,
9843
+ hint: false
9844
+ }),
9845
+ "minimax-code": policy({
9846
+ mode: "explicit",
9847
+ scope: "unknown",
9848
+ staticPrefix: true,
9849
+ coldStart: "in-flight",
9850
+ stickyImages: true,
9851
+ affinity: false,
9852
+ waitWarmup: false,
9853
+ hint: false
9854
+ }),
9855
+ "glm-coding-plan": policy({
9856
+ ...DISABLED,
9857
+ mode: "automatic"
9858
+ }),
9859
+ "command-code": policy({
9860
+ ...DISABLED,
9861
+ mode: "automatic"
9862
+ }),
9863
+ "cline-pass": policy({
9864
+ ...DISABLED,
9865
+ mode: "automatic"
9866
+ }),
9867
+ qoder: policy({
9868
+ ...DISABLED,
9869
+ mode: "automatic"
9870
+ }),
9871
+ opendesign: policy({
9872
+ ...DISABLED,
9873
+ mode: "automatic"
9874
+ }),
9875
+ "workbuddy-subscription": policy({
9876
+ ...DISABLED,
9877
+ mode: "automatic"
9878
+ }),
9879
+ ollama: DISABLED
9880
+ });
9881
+ const OVERRIDE_KEYS = /* @__PURE__ */ new Set([
9882
+ "mode",
9883
+ "scope",
9884
+ "ttlMs",
9885
+ "staticPrefix",
9886
+ "coldStart",
9887
+ "stickyImages",
9888
+ "affinity",
9889
+ "waitWarmup",
9890
+ "hint"
9891
+ ]);
9892
+ const BOOLEAN_KEYS = [
9893
+ "staticPrefix",
9894
+ "stickyImages",
9895
+ "affinity",
9896
+ "waitWarmup",
9897
+ "hint"
9898
+ ];
9899
+ function isRecord$65(value) {
9900
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9901
+ }
9902
+ /** Validate one provider override without silently dropping unknown keys. */
9903
+ function parsePromptCacheOverride(provider, value) {
9904
+ if (!isRecord$65(value)) throw new Error(`prompt cache policy for "${provider}" must be an object`);
9905
+ const out = {};
9906
+ for (const [key, entry] of Object.entries(value)) {
9907
+ if (!OVERRIDE_KEYS.has(key)) throw new Error(`prompt cache policy for "${provider}" has unknown field "${key}"`);
9908
+ if (entry === void 0) continue;
9909
+ if (key === "mode") {
9910
+ if (entry !== "automatic" && entry !== "explicit" && entry !== "none") throw new Error(`invalid prompt cache mode for "${provider}"`);
9911
+ } else if (key === "scope") {
9912
+ if (entry !== "account" && entry !== "shared" && entry !== "unknown") throw new Error(`invalid prompt cache scope for "${provider}"`);
9913
+ } else if (key === "coldStart") {
9914
+ if (entry !== "off" && entry !== "in-flight" && entry !== "warm") throw new Error(`invalid cold-start mode for "${provider}"`);
9915
+ } else if (key === "ttlMs") {
9916
+ if (!Number.isSafeInteger(entry) || entry <= 0 || entry > MAX_CACHE_TTL_MS) throw new Error(`prompt cache ttlMs for "${provider}" must be a positive safe integer no greater than ${MAX_CACHE_TTL_MS}`);
9917
+ } else if (BOOLEAN_KEYS.includes(key) && typeof entry !== "boolean") throw new Error(`prompt cache ${key} for "${provider}" must be boolean`);
9918
+ out[key] = entry;
9919
+ }
9920
+ return out;
9921
+ }
9922
+ /** Validate a whole config-owned map. Undefined means no overrides. */
9923
+ function parsePromptCacheOverrides(value) {
9924
+ if (value === void 0) return {};
9925
+ if (!isRecord$65(value)) throw new Error("promptCacheOverrides must be an object keyed by provider id");
9926
+ const out = {};
9927
+ for (const [provider, override] of Object.entries(value)) {
9928
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(provider)) throw new Error(`invalid prompt cache provider id "${provider}"`);
9929
+ out[provider] = parsePromptCacheOverride(provider, override);
9930
+ }
9931
+ return Object.freeze(out);
9932
+ }
9933
+ function validateResolved(provider, value) {
9934
+ if (value.mode === "none") return policy({ ...DISABLED });
9935
+ if ((value.coldStart === "warm" || value.waitWarmup || value.hint) && value.ttlMs === void 0) throw new Error(`prompt cache policy for "${provider}" needs a measured ttlMs for warm/waitWarmup/hint`);
9936
+ if (value.affinity && value.scope !== "account") throw new Error(`prompt cache affinity for "${provider}" needs scope:"account"`);
9937
+ return policy(value);
9938
+ }
9939
+ /** Resolve one line, applying an already-validated or config-shaped override. */
9940
+ function promptCachePolicyOf(provider, override) {
9941
+ const base = PROMPT_CACHE_POLICIES[provider] ?? DISABLED;
9942
+ const parsed = override === void 0 ? void 0 : parsePromptCacheOverride(provider, override);
9943
+ return validateResolved(provider, {
9944
+ ...base,
9945
+ ...parsed
9946
+ });
9947
+ }
9948
+ //#endregion
9949
+ //#region src/host/common/prefix-request-coordinator.ts
9950
+ const NOOP_LEASE = Object.freeze({
9951
+ leader: false,
9952
+ release: () => void 0,
9953
+ markWarm: () => void 0
9954
+ });
9955
+ var PrefixRequestCoordinator = class {
9956
+ leaders = /* @__PURE__ */ new Map();
9957
+ bursts = /* @__PURE__ */ new Map();
9958
+ mainHeaders = /* @__PURE__ */ new Map();
9959
+ warm = /* @__PURE__ */ new Map();
9960
+ waitMs;
9961
+ maxGroups;
9962
+ now;
9963
+ constructor(options = {}) {
9964
+ this.waitMs = options.waitMs ?? 2500;
9965
+ this.maxGroups = options.maxGroups ?? 256;
9966
+ this.now = options.now ?? Date.now;
9967
+ }
9968
+ /** Join or lead one cold prefix group. */
9969
+ async acquire(groupKey, signal, warmTtlMs) {
9970
+ if (groupKey === void 0 || groupKey === "") return NOOP_LEASE;
9971
+ if (warmTtlMs !== void 0 && this.isWarm(groupKey, warmTtlMs)) return NOOP_LEASE;
9972
+ const existing = this.leaders.get(groupKey);
9973
+ if (existing !== void 0 && this.now() - existing.startedAt < this.waitMs) {
9974
+ await this.race(existing.promise, signal);
9975
+ return NOOP_LEASE;
9976
+ }
9977
+ if (existing !== void 0) existing.release();
9978
+ return this.open(this.leaders, groupKey, warmTtlMs);
9979
+ }
9980
+ /** Serialize first-turn bursts on one account/model without a warm cache claim. */
9981
+ async acquireBurst(key, signal) {
9982
+ if (key === "") return NOOP_LEASE;
9983
+ while (true) {
9984
+ const existing = this.bursts.get(key);
9985
+ if (existing === void 0) break;
9986
+ await this.race(existing.promise, signal);
9987
+ if (signal.aborted) return NOOP_LEASE;
9988
+ if (this.bursts.get(key) === existing) existing.release();
9989
+ }
9990
+ return this.open(this.bursts, key);
9991
+ }
9992
+ /** A main request owns the prefill slot until its response headers arrive. */
9993
+ registerMainSession(sessionId) {
9994
+ if (sessionId.trim() === "") return NOOP_LEASE;
9995
+ const key = sessionId.trim();
9996
+ this.mainHeaders.get(key)?.release();
9997
+ return this.open(this.mainHeaders, key);
9998
+ }
9999
+ /** An auxiliary title request waits briefly for the main request's headers. */
10000
+ async waitForMainSession(sessionId, signal) {
10001
+ const existing = this.mainHeaders.get(sessionId.trim());
10002
+ if (existing !== void 0) await this.race(existing.promise, signal);
10003
+ }
10004
+ isWarm(groupKey, ttlMs, at = this.now()) {
10005
+ const warmedAt = this.warm.get(groupKey);
10006
+ return warmedAt !== void 0 && Number.isFinite(ttlMs) && ttlMs > 0 && at - warmedAt < ttlMs;
10007
+ }
10008
+ /** Release every waiter and clear all process-local evidence. */
10009
+ reset() {
10010
+ for (const store of [
10011
+ this.leaders,
10012
+ this.bursts,
10013
+ this.mainHeaders
10014
+ ]) {
10015
+ for (const entry of [...store.values()]) entry.release();
10016
+ store.clear();
10017
+ }
10018
+ this.warm.clear();
10019
+ }
10020
+ open(store, key, warmTtlMs) {
10021
+ let resolve;
10022
+ const promise = new Promise((done) => {
10023
+ resolve = done;
10024
+ });
10025
+ let settled = false;
10026
+ const release = () => {
10027
+ if (settled) return;
10028
+ settled = true;
10029
+ resolve();
10030
+ if (store.get(key) === entry) store.delete(key);
10031
+ };
10032
+ const entry = {
10033
+ promise,
10034
+ startedAt: this.now(),
10035
+ release
10036
+ };
10037
+ store.set(key, entry);
10038
+ return {
10039
+ leader: true,
10040
+ release,
10041
+ markWarm: () => {
10042
+ if (warmTtlMs === void 0 || !Number.isFinite(warmTtlMs) || warmTtlMs <= 0) return;
10043
+ this.warm.delete(key);
10044
+ this.warm.set(key, this.now());
10045
+ while (this.warm.size > this.maxGroups) {
10046
+ const oldest = this.warm.keys().next();
10047
+ if (oldest.done !== true) this.warm.delete(oldest.value);
10048
+ }
10049
+ }
10050
+ };
10051
+ }
10052
+ /** Wait for a leader, the bounded budget, or caller cancellation; clean every listener. */
10053
+ async race(leader, signal) {
10054
+ const cleanups = [];
10055
+ const budget = new Promise((resolve) => {
10056
+ const timer = setTimeout(resolve, this.waitMs);
10057
+ timer.unref?.();
10058
+ cleanups.push(() => clearTimeout(timer));
10059
+ });
10060
+ const aborted = new Promise((resolve) => {
10061
+ if (signal.aborted) {
10062
+ resolve();
10063
+ return;
10064
+ }
10065
+ const onAbort = () => resolve();
10066
+ signal.addEventListener("abort", onAbort, { once: true });
10067
+ cleanups.push(() => signal.removeEventListener("abort", onAbort));
10068
+ });
10069
+ await Promise.race([
10070
+ leader,
10071
+ budget,
10072
+ aborted
10073
+ ]).finally(() => {
10074
+ for (const cleanup of cleanups) cleanup();
10075
+ });
10076
+ }
10077
+ };
10078
+ /** Cheap structural gate for auxiliary calls; provider-specific legacy probes may extend it. */
10079
+ function isAuxiliaryRequest(options) {
10080
+ return options.purpose !== void 0;
10081
+ }
10082
+ /** Coordinate one fetch through response headers, releasing on every outcome. */
10083
+ async function fetchWithPrefixCoordination(fetchFn, coordinator, groupKey, warmTtlMs, input, init) {
10084
+ const signal = init?.signal;
10085
+ if (signal == null) {
10086
+ const controller = new AbortController();
10087
+ const lease = await coordinator.acquire(groupKey, controller.signal, warmTtlMs);
10088
+ try {
10089
+ const response = await fetchFn(input, init);
10090
+ if (response.ok) lease.markWarm();
10091
+ return response;
10092
+ } finally {
10093
+ lease.release();
10094
+ }
10095
+ }
10096
+ const lease = await coordinator.acquire(groupKey, signal, warmTtlMs);
10097
+ try {
10098
+ const response = await fetchFn(input, init);
10099
+ if (response.ok) lease.markWarm();
10100
+ return response;
10101
+ } finally {
10102
+ lease.release();
10103
+ }
10104
+ }
10105
+ //#endregion
10106
+ //#region src/host/common/prefix-stability.ts
10107
+ function isRecord$64(value) {
10108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10109
+ }
10110
+ function firstTextOf$1(content) {
10111
+ if (typeof content === "string") return content.trim() === "" ? void 0 : content;
10112
+ if (!Array.isArray(content)) return void 0;
10113
+ for (const block of content) {
10114
+ if (!isRecord$64(block)) continue;
10115
+ if (block.type === "text" && typeof block.text === "string" && block.text !== "") return block.text;
10116
+ if (block.type === "tool-result") {
10117
+ const nested = firstTextOf$1(block.content);
10118
+ if (nested !== void 0) return nested;
10119
+ }
10120
+ }
10121
+ }
10122
+ function isToolResultMessage$9(message) {
10123
+ return message?.source?.kind === "tool";
10124
+ }
10125
+ /** Stability tier for an initial bootstrap reminder; null means dynamic/history-bound. */
10126
+ function classifyBootstrapUserMessage$1(message) {
10127
+ if (message.role !== "user" || isToolResultMessage$9(message)) return null;
10128
+ const source = isRecord$64(message.source) ? message.source : void 0;
10129
+ const kind = source !== void 0 && typeof source.kind === "string" ? source.kind : void 0;
10130
+ const text = firstTextOf$1(message.content)?.trimStart() ?? "";
10131
+ if (kind === "progress-updates-language" || text.startsWith("Progress update output language override:")) return 1;
10132
+ if (text.startsWith("<system-reminder>\nThe following workspace instructions") || kind === "agent-instructions" && !text.startsWith("<system-reminder>\nUpdated instructions") && !text.startsWith("<system-reminder>\nAdditional instructions") && !text.startsWith("<system-reminder>\nInstructions removed")) return 2;
10133
+ if (text.startsWith("<system-reminder>\nA skill is a reusable set of task-specific instructions.") || kind === "skill-catalog" && !text.startsWith("<system-reminder>\nThe available skill catalog changed.")) return 3;
10134
+ if (kind === "runtime-context" || text.startsWith("Current runtime context. This snapshot supersedes")) return 4;
10135
+ return null;
10136
+ }
10137
+ function isCompactCheckpointMessage(message) {
10138
+ if (message === void 0 || message.role !== "user" || isToolResultMessage$9(message)) return false;
10139
+ const source = isRecord$64(message.source) ? message.source : void 0;
10140
+ if (source !== void 0 && source.kind === "compact-checkpoint") return true;
10141
+ return firstTextOf$1(message.content)?.trimStart().startsWith("This is an automatically generated checkpoint condensing") === true;
10142
+ }
10143
+ /** Global instructions precede workspace instructions without reordering equals. */
10144
+ function normalizeAgentInstructionsMessage(message) {
10145
+ if (!Array.isArray(message.content) || message.content.length !== 1) return message;
10146
+ const first = message.content[0];
10147
+ if (!isRecord$64(first) || first.type !== "text" || typeof first.text !== "string") return message;
10148
+ const raw = first.text;
10149
+ const firstIdx = raw.indexOf("\n\nInstructions from: ");
10150
+ if (firstIdx < 0) return message;
10151
+ const closeIdx = raw.lastIndexOf("\n</system-reminder>");
10152
+ if (closeIdx <= firstIdx) return message;
10153
+ const header = raw.slice(0, firstIdx);
10154
+ const body = raw.slice(firstIdx + 2, closeIdx);
10155
+ const tail = raw.slice(closeIdx);
10156
+ const sections = body.split("\n\nInstructions from: ");
10157
+ if (sections.length < 2) return message;
10158
+ const tagged = sections.map((sec, idx) => {
10159
+ const firstLineEnd = sec.indexOf("\n");
10160
+ const sourcePath = (firstLineEnd < 0 ? sec : sec.slice(0, firstLineEnd)).trim();
10161
+ return {
10162
+ sec,
10163
+ idx,
10164
+ rank: sourcePath.startsWith("~/") || sourcePath.startsWith("~\\") || sourcePath.includes("/.dsh/") || sourcePath.includes("\\.dsh\\") ? 0 : 1
10165
+ };
10166
+ });
10167
+ if (!tagged.some((item, i) => i > 0 && item.rank < tagged[i - 1].rank)) return message;
10168
+ tagged.sort((a, b) => a.rank - b.rank || a.idx - b.idx);
10169
+ const rebuilt = `${header}\n\nInstructions from: ${tagged.map((item) => item.sec).join("\n\nInstructions from: ")}${tail}`;
10170
+ return {
10171
+ ...message,
10172
+ content: [{
10173
+ ...first,
10174
+ text: rebuilt
10175
+ }]
10176
+ };
10177
+ }
10178
+ /** Normalise the initial bootstrap run, including the post-compaction shape. */
10179
+ function orderStaticPrefixMessages$1(messages) {
10180
+ try {
10181
+ if (messages.length <= 1) return [...messages];
10182
+ let leadEnd = 0;
10183
+ while (leadEnd < messages.length && messages[leadEnd].role === "user" && !isToolResultMessage$9(messages[leadEnd])) leadEnd += 1;
10184
+ if (leadEnd > 0 && isCompactCheckpointMessage(messages[0])) {
10185
+ const seenInLead = /* @__PURE__ */ new Set();
10186
+ for (let i = 0; i < leadEnd; i++) {
10187
+ const tier = classifyBootstrapUserMessage$1(messages[i]);
10188
+ if (tier !== null) seenInLead.add(tier);
10189
+ }
10190
+ if (!seenInLead.has(1) && !seenInLead.has(2)) {
10191
+ const hoistedByTier = /* @__PURE__ */ new Map();
10192
+ for (let i = leadEnd; i < messages.length; i++) {
10193
+ const msg = messages[i];
10194
+ const tier = classifyBootstrapUserMessage$1(msg);
10195
+ if ((tier === 1 || tier === 2 || tier === 3) && !hoistedByTier.has(tier)) hoistedByTier.set(tier, {
10196
+ index: i,
10197
+ message: tier === 2 ? normalizeAgentInstructionsMessage(msg) : msg
10198
+ });
10199
+ }
10200
+ if (hoistedByTier.size > 0) {
10201
+ const indices = /* @__PURE__ */ new Set();
10202
+ const prefix = [];
10203
+ for (const tier of [
10204
+ 1,
10205
+ 2,
10206
+ 3
10207
+ ]) {
10208
+ const entry = hoistedByTier.get(tier);
10209
+ if (entry !== void 0) {
10210
+ indices.add(entry.index);
10211
+ prefix.push(entry.message);
10212
+ }
10213
+ }
10214
+ return [...prefix, ...messages.filter((_, index) => !indices.has(index))];
10215
+ }
10216
+ }
10217
+ }
10218
+ if (leadEnd <= 1) return [...messages];
10219
+ const seen = /* @__PURE__ */ new Set();
10220
+ const tagged = [];
10221
+ let hasBootstrap = false;
10222
+ for (let i = 0; i < leadEnd; i++) {
10223
+ const raw = messages[i];
10224
+ const tier = classifyBootstrapUserMessage$1(raw);
10225
+ if (tier !== null && !seen.has(tier)) {
10226
+ seen.add(tier);
10227
+ hasBootstrap = true;
10228
+ tagged.push({
10229
+ message: tier === 2 ? normalizeAgentInstructionsMessage(raw) : raw,
10230
+ rank: tier,
10231
+ index: i
10232
+ });
10233
+ } else tagged.push({
10234
+ message: raw,
10235
+ rank: 5,
10236
+ index: i
10237
+ });
10238
+ }
10239
+ if (!hasBootstrap) return [...messages];
10240
+ if (!tagged.some((item, i) => i > 0 && item.rank < tagged[i - 1].rank || item.message !== messages[i])) return [...messages];
10241
+ tagged.sort((a, b) => a.rank - b.rank || a.index - b.index);
10242
+ return [...tagged.map((item) => item.message), ...messages.slice(leadEnd)];
10243
+ } catch {
10244
+ return [...messages];
10245
+ }
10246
+ }
10247
+ /** Apply the policy at an adapter boundary; disabled providers keep object identity. */
10248
+ function withStaticPrefixOrdering(provider, options, override) {
10249
+ if (!promptCachePolicyOf(provider, override).staticPrefix) return options;
10250
+ const messages = orderStaticPrefixMessages$1(options.messages);
10251
+ return messages.some((message, index) => message !== options.messages[index]) ? {
10252
+ ...options,
10253
+ messages
10254
+ } : options;
10255
+ }
10256
+ function allTextOf(content) {
10257
+ if (typeof content === "string") return content;
10258
+ if (!Array.isArray(content)) return "";
10259
+ return content.map((block) => {
10260
+ if (!isRecord$64(block)) return "";
10261
+ if (block.type === "text" && typeof block.text === "string") return block.text;
10262
+ if (block.type === "tool-result") return allTextOf(block.content);
10263
+ return "";
10264
+ }).join("");
10265
+ }
10266
+ function prefixFingerprint(text) {
10267
+ let hash = 2166136261;
10268
+ for (let index = 0; index < text.length; index++) {
10269
+ hash ^= text.charCodeAt(index);
10270
+ hash = Math.imul(hash, 16777619);
10271
+ }
10272
+ return (hash >>> 0).toString(16);
10273
+ }
10274
+ /** Provider-neutral identity for requests that may share one cold prefix. */
10275
+ function sharedPrefixGroupKey(provider, options, namespace, accountScope) {
10276
+ if (isAuxiliaryRequest(options)) return void 0;
10277
+ const systemParts = [];
10278
+ if (typeof options.system === "string" && options.system.trim() !== "") systemParts.push(options.system);
10279
+ for (const message of options.messages) if (message.role === "system") {
10280
+ const text = allTextOf(message.content);
10281
+ if (text !== "") systemParts.push(text);
10282
+ }
10283
+ if (systemParts.length === 0) return void 0;
10284
+ const systemHash = prefixFingerprint(systemParts.join("\n\n"));
10285
+ const tools = options.tools ?? [];
10286
+ const toolsHash = tools.length === 0 ? "none" : prefixFingerprint(JSON.stringify(tools.map((tool) => [
10287
+ tool.name,
10288
+ tool.description ?? "",
10289
+ tool.parameters ?? {}
10290
+ ])));
10291
+ const staticParts = [];
10292
+ for (const message of orderStaticPrefixMessages$1(options.messages)) {
10293
+ const tier = classifyBootstrapUserMessage$1(message);
10294
+ if (tier !== 1 && tier !== 2 && tier !== 3) break;
10295
+ staticParts.push(allTextOf(message.content));
10296
+ }
10297
+ const staticHash = staticParts.length === 0 ? "none" : prefixFingerprint(staticParts.join("\n\n"));
10298
+ return [
10299
+ provider,
10300
+ namespace,
10301
+ options.model,
10302
+ systemHash,
10303
+ toolsHash,
10304
+ staticHash,
10305
+ accountScope
10306
+ ].join(":");
10307
+ }
10308
+ /** Pure prefix-diff policy shared by provider-specific telemetry stores. */
10309
+ function classifyPrefixDrift(previous, current) {
10310
+ if (previous === void 0 || previous.systemPromptHash === null && previous.toolsHash === null && previous.cacheKey === null) return "first-request";
10311
+ if (current.cacheKey === null) return "cold-key";
10312
+ if (previous.cacheKey !== current.cacheKey) return "cache-key";
10313
+ if (previous.systemPromptHash !== current.systemPromptHash) return "system-prompt";
10314
+ if (previous.toolsHash !== current.toolsHash) return "tools";
10315
+ return "stable";
10316
+ }
10317
+ //#endregion
9764
10318
  //#region src/host/antigravity/cache-stats.ts
9765
10319
  const EMPTY_TOTALS = {
9766
10320
  requests: 0,
@@ -10492,6 +11046,7 @@ function streamFailure$2(error, signal) {
10492
11046
  const chain = errorChain(error).trim();
10493
11047
  return new LlmError(`Antigravity stream failed: ${chain === "" ? "the response stream ended unexpectedly" : chain.slice(0, 500)}`, "TRANSPORT", { cause: error });
10494
11048
  }
11049
+ const antigravityPrefixRequests = new PrefixRequestCoordinator();
10495
11050
  var AntigravityAdapter = class extends LlmAdapter {
10496
11051
  store;
10497
11052
  modelSettings;
@@ -10614,7 +11169,7 @@ var AntigravityAdapter = class extends LlmAdapter {
10614
11169
  const runtimeModel = routing?.routing[effort] || routing?.defaultRequestId || model.id;
10615
11170
  const family = quotaFamilyForModel(model.id);
10616
11171
  const runtimeMaxOutput = catalogMaxOutputFor(runtimeModel, catalog);
10617
- const normalizedOptions = normalizeGenerateOptions(options);
11172
+ const normalizedOptions = withStaticPrefixOrdering("antigravity", normalizeGenerateOptions(options), this.options.promptCache);
10618
11173
  const images = await resolveRequestImages$3(normalizedOptions, this.options.attachments, signal);
10619
11174
  const requestOptions = offloadOldestRequestImages$2(normalizedOptions, MAX_REQUEST_IMAGE_BYTES$3, images);
10620
11175
  let recoveredMissingThinkingSignature = false;
@@ -10656,7 +11211,9 @@ var AntigravityAdapter = class extends LlmAdapter {
10656
11211
  let firstResponse;
10657
11212
  for (const endpoint of endpointCandidates()) try {
10658
11213
  let sentBody = body;
10659
- response = await fetchFn(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
11214
+ const cachePolicy = promptCachePolicyOf("antigravity", this.options.promptCache);
11215
+ const groupKey = cachePolicy.coldStart === "off" ? void 0 : sharedPrefixGroupKey("antigravity", requestOptions, endpoint, account.id);
11216
+ response = await fetchWithPrefixCoordination(fetchFn, antigravityPrefixRequests, groupKey, cachePolicy.coldStart === "warm" ? cachePolicy.ttlMs : void 0, `${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
10660
11217
  method: "POST",
10661
11218
  headers,
10662
11219
  body,
@@ -12173,6 +12730,135 @@ function createControlledModelFetch(baseFetch, options) {
12173
12730
  };
12174
12731
  }
12175
12732
  //#endregion
12733
+ //#region src/host/common/tool-runaway.ts
12734
+ /**
12735
+ * Provider-neutral tool-call repetition primitives.
12736
+ *
12737
+ * The detector knows nothing about model wire formats. Providers supply reminder
12738
+ * text and may opt into a hard handoff threshold; coordination tools are exempt
12739
+ * because they are expected to recur while real work continues.
12740
+ */
12741
+ function isRecord$63(value) {
12742
+ return typeof value === "object" && value !== null;
12743
+ }
12744
+ function isPlainRecord$2(value) {
12745
+ if (!isRecord$63(value) || Array.isArray(value)) return false;
12746
+ const prototype = Object.getPrototypeOf(value);
12747
+ return prototype === Object.prototype || prototype === null;
12748
+ }
12749
+ function sortJsonValue(value) {
12750
+ if (Array.isArray(value)) return value.map(sortJsonValue);
12751
+ if (!isPlainRecord$2(value)) return value;
12752
+ const out = {};
12753
+ for (const key of Object.keys(value).sort()) out[key] = sortJsonValue(value[key]);
12754
+ return out;
12755
+ }
12756
+ /** Stable identity for one tool call, independent of object key order. */
12757
+ function canonicalToolCallKey(toolName, args) {
12758
+ let json;
12759
+ try {
12760
+ json = JSON.stringify(sortJsonValue(args));
12761
+ } catch {
12762
+ json = void 0;
12763
+ }
12764
+ return toolName + " " + (json ?? String(args));
12765
+ }
12766
+ /** One immediately-repeated action streak. A different action starts at one. */
12767
+ function createExactRepeatTracker() {
12768
+ let current;
12769
+ return {
12770
+ observe(toolName, args) {
12771
+ const key = canonicalToolCallKey(toolName, args);
12772
+ current = {
12773
+ key,
12774
+ count: current?.key === key ? current.count + 1 : 1
12775
+ };
12776
+ return current;
12777
+ },
12778
+ current() {
12779
+ return current;
12780
+ },
12781
+ reset() {
12782
+ current = void 0;
12783
+ }
12784
+ };
12785
+ }
12786
+ /** Coordination calls that must not break or inflate an action streak. */
12787
+ const TOOL_RUNAWAY_EXEMPT_TOOLS = Object.freeze([
12788
+ "progress_update",
12789
+ "todo_write",
12790
+ "ask_user_question",
12791
+ "enter_plan_mode",
12792
+ "exit_plan_mode",
12793
+ "leave_plan_mode",
12794
+ "create_goal",
12795
+ "update_goal",
12796
+ "get_goal",
12797
+ "dispatch_ledger",
12798
+ "present"
12799
+ ]);
12800
+ /** Read the live global switch; legacy Kimi-only installs without it stay on. */
12801
+ function resolveToolRunawayGuardEnabled(config) {
12802
+ const field = config?.toolRunawayGuard;
12803
+ if (field !== null && typeof field === "object") try {
12804
+ return field.get() !== false;
12805
+ } catch {
12806
+ return true;
12807
+ }
12808
+ return field !== false;
12809
+ }
12810
+ /**
12811
+ * Exact-repeat state machine shared by provider profiles. A missing forceStopAt
12812
+ * gives reminders only; no provider inherits Kimi's hard stop accidentally.
12813
+ */
12814
+ function createExactRepeatBreaker(options) {
12815
+ const sessions = /* @__PURE__ */ new Map();
12816
+ const maxSessions = options.maxSessions ?? 256;
12817
+ const exempt = new Set(options.exemptTools ?? TOOL_RUNAWAY_EXEMPT_TOOLS);
12818
+ const stateOf = (sessionId) => {
12819
+ let state = sessions.get(sessionId);
12820
+ if (state === void 0) state = {
12821
+ tracker: createExactRepeatTracker(),
12822
+ count: 0,
12823
+ handoffActive: false
12824
+ };
12825
+ sessions.delete(sessionId);
12826
+ sessions.set(sessionId, state);
12827
+ if (sessions.size > maxSessions) {
12828
+ const oldest = sessions.keys().next();
12829
+ if (oldest.done !== true) sessions.delete(oldest.value);
12830
+ }
12831
+ return state;
12832
+ };
12833
+ return {
12834
+ checkBeforeExecute(sessionId, toolName, args) {
12835
+ if (options.forceStopAt === void 0) return void 0;
12836
+ const state = stateOf(sessionId);
12837
+ if (state.handoffActive) return options.handoffVeto ?? options.handoffReminder;
12838
+ const key = canonicalToolCallKey(toolName, args);
12839
+ if (state.tracker.current()?.key === key && state.count + 1 >= options.forceStopAt) {
12840
+ state.count += 1;
12841
+ state.handoffActive = true;
12842
+ return options.handoffVeto ?? options.handoffReminder;
12843
+ }
12844
+ },
12845
+ recordAfterExecute(sessionId, toolName, args) {
12846
+ if (exempt.has(toolName)) return void 0;
12847
+ const state = stateOf(sessionId);
12848
+ if (state.handoffActive) return options.handoffReminder ?? options.reminder(state.count);
12849
+ state.count = state.tracker.observe(toolName, args).count;
12850
+ if (options.forceStopAt !== void 0 && state.count >= options.forceStopAt) {
12851
+ state.handoffActive = true;
12852
+ return options.handoffReminder ?? options.reminder(state.count);
12853
+ }
12854
+ return options.reminder(state.count);
12855
+ },
12856
+ resetSession(sessionId) {
12857
+ sessions.delete(sessionId);
12858
+ }
12859
+ };
12860
+ }
12861
+ //#endregion
12176
12862
  //#region src/host/common/provider-route.ts
12177
12863
  /**
12178
12864
  * Claim one provider route for this plugin and keep claiming it.
@@ -12728,7 +13414,7 @@ function preferencesPath() {
12728
13414
  return path.join(dshHomeDir(), "storages", "dsh-chatgpt-subscription-preferences.json");
12729
13415
  }
12730
13416
  /** Distinguishes concurrent temporary files written by one process. */
12731
- let writeSequence$1 = 0;
13417
+ let writeSequence = 0;
12732
13418
  /** One JSON document, validated and shaped by a schemastery schema. */
12733
13419
  var FilePreferencesStore = class {
12734
13420
  schema;
@@ -12830,8 +13516,8 @@ var FilePreferencesStore = class {
12830
13516
  }
12831
13517
  async write(value) {
12832
13518
  await fs.mkdir(path.dirname(this.filePath), { recursive: true });
12833
- writeSequence$1 += 1;
12834
- const tmp = `${this.filePath}.tmp.${process.pid}.${writeSequence$1}`;
13519
+ writeSequence += 1;
13520
+ const tmp = `${this.filePath}.tmp.${process.pid}.${writeSequence}`;
12835
13521
  try {
12836
13522
  await fs.writeFile(tmp, JSON.stringify(value, null, 2), {
12837
13523
  encoding: "utf8",
@@ -14049,9 +14735,17 @@ const REASONING_DELTA_FLUSH_CHARS = 768;
14049
14735
  const REASONING_TRUNCATED_NOTICE = "\n\n[Reasoning summary truncated to keep the DSH web UI responsive.]";
14050
14736
  /** Cap on live turns whose routing state is remembered at once. */
14051
14737
  const MAX_TRACKED_TURN_STATES = 200;
14738
+ const codexPrefixRequests = new PrefixRequestCoordinator();
14739
+ function codexColdGroupKey(payload, credentials) {
14740
+ const instructions = typeof payload.instructions === "string" ? payload.instructions : "";
14741
+ if (instructions === "") return void 0;
14742
+ const digest = createHash("sha256").update(instructions).update("\0").update(JSON.stringify(Array.isArray(payload.tools) ? payload.tools : [])).digest("hex");
14743
+ return `${String(payload.model ?? "")}:${digest}:${authOwnerKey(credentials)}`;
14744
+ }
14052
14745
  var ResponsesClient = class {
14053
14746
  oauth;
14054
14747
  attachments;
14748
+ options;
14055
14749
  fetchFn;
14056
14750
  onGenerationFinished;
14057
14751
  outputVerbosity;
@@ -14080,6 +14774,7 @@ var ResponsesClient = class {
14080
14774
  constructor(oauth, attachments, options = {}) {
14081
14775
  this.oauth = oauth;
14082
14776
  this.attachments = attachments;
14777
+ this.options = options;
14083
14778
  this.fetchFn = options.fetchFn ?? fetch;
14084
14779
  this.localRawImages = options.localRawImages ?? {};
14085
14780
  this.onGenerationFinished = options.onGenerationFinished ?? (() => void 0);
@@ -14092,7 +14787,7 @@ var ResponsesClient = class {
14092
14787
  }
14093
14788
  localRawImages;
14094
14789
  async *stream(rawOptions, capabilities) {
14095
- const options = normalizeGenerateOptions(rawOptions);
14790
+ const options = withStaticPrefixOrdering("codex-chatgpt", normalizeGenerateOptions(rawOptions), this.options.promptCache);
14096
14791
  const sessionId = stableSessionId(options.sessionId);
14097
14792
  const resolveCapabilities = capabilities ?? this.capabilities ?? defaultWireCapabilities;
14098
14793
  const turnKey = turnKeyFor(sessionId);
@@ -14275,7 +14970,9 @@ var ResponsesClient = class {
14275
14970
  }
14276
14971
  async request(payload, credentials, sessionId, turnKey, signal, useResponsesLite = false) {
14277
14972
  for (let attempt = 0;; attempt += 1) try {
14278
- return await this.fetchFn(CODEX_RESPONSES_URL, {
14973
+ const cachePolicy = promptCachePolicyOf("codex-chatgpt", this.options.promptCache);
14974
+ const groupKey = cachePolicy.coldStart === "off" ? void 0 : codexColdGroupKey(payload, credentials);
14975
+ return await fetchWithPrefixCoordination(this.fetchFn, codexPrefixRequests, groupKey, cachePolicy.coldStart === "warm" ? cachePolicy.ttlMs : void 0, CODEX_RESPONSES_URL, {
14279
14976
  method: "POST",
14280
14977
  headers: {
14281
14978
  ...codexHeaders(credentials, sessionId, { turnState: this.turnStateFor(turnKey, credentials) }),
@@ -28497,6 +29194,10 @@ var FileResultReferenceStore = class {
28497
29194
  */
28498
29195
  /** 有界登记表:Map 的插入顺序同时是 LRU 顺序(命中即重新插入到队尾)。 */
28499
29196
  const registry = /* @__PURE__ */ new Map();
29197
+ const DEFAULT_USAGE_PROVIDER = "kimi-code";
29198
+ function usageScopeKey(sessionId, provider) {
29199
+ return (provider?.trim() || DEFAULT_USAGE_PROVIDER) + ":" + sessionId;
29200
+ }
28500
29201
  /** 一个中性返回值,异常路径复用(不含任何容器操作)。 */
28501
29202
  function neutralState(sessionId) {
28502
29203
  return {
@@ -28601,6 +29302,7 @@ function normalizeUsage(input) {
28601
29302
  const at = typeof input.at === "number" && Number.isFinite(input.at) ? input.at : Date.now();
28602
29303
  const normalized = {
28603
29304
  sessionId,
29305
+ provider: typeof input.provider === "string" && input.provider.trim() !== "" ? input.provider.trim() : DEFAULT_USAGE_PROVIDER,
28604
29306
  inputTokens: finiteCount(input.inputTokens),
28605
29307
  cacheReadTokens: finiteCount(input.cacheReadTokens),
28606
29308
  outputTokens: finiteCount(input.outputTokens),
@@ -28631,16 +29333,19 @@ function stateOf(sessionId, reports) {
28631
29333
  */
28632
29334
  function recordCacheWarmupUsage(input) {
28633
29335
  let sessionId = "";
29336
+ let provider;
28634
29337
  try {
28635
29338
  const usage = normalizeUsage(input);
28636
29339
  if (usage === void 0) return neutralState("");
28637
29340
  sessionId = usage.sessionId;
28638
- const previous = registry.get(usage.sessionId);
29341
+ provider = usage.provider;
29342
+ const scope = usageScopeKey(usage.sessionId, usage.provider);
29343
+ const previous = registry.get(scope);
28639
29344
  const kept = previous === void 0 ? [] : [...previous];
28640
29345
  kept.push(Object.freeze({ ...usage }));
28641
29346
  while (kept.length > CACHE_WARMUP_DEFAULTS.maxUsageReportsPerSession) kept.shift();
28642
- if (previous !== void 0) registry.delete(usage.sessionId);
28643
- registry.set(usage.sessionId, Object.freeze(kept));
29347
+ if (previous !== void 0) registry.delete(scope);
29348
+ registry.set(scope, Object.freeze(kept));
28644
29349
  while (registry.size > CACHE_WARMUP_DEFAULTS.maxUsageSessions) {
28645
29350
  const oldest = registry.keys().next();
28646
29351
  if (oldest.done === true) break;
@@ -28649,7 +29354,7 @@ function recordCacheWarmupUsage(input) {
28649
29354
  return stateOf(usage.sessionId, kept);
28650
29355
  } catch {
28651
29356
  try {
28652
- if (sessionId !== "") registry.delete(sessionId);
29357
+ if (sessionId !== "") registry.delete(usageScopeKey(sessionId, provider));
28653
29358
  } catch {}
28654
29359
  return neutralState(sessionId);
28655
29360
  }
@@ -28659,10 +29364,10 @@ function recordCacheWarmupUsage(input) {
28659
29364
  * @param sessionId - 会话 id。
28660
29365
  * @returns 报告副本;没有或读不到时为空数组。
28661
29366
  */
28662
- function cacheWarmupUsageReports(sessionId) {
29367
+ function cacheWarmupUsageReports(sessionId, provider) {
28663
29368
  try {
28664
29369
  if (typeof sessionId !== "string" || sessionId === "") return [];
28665
- const reports = registry.get(sessionId);
29370
+ const reports = registry.get(usageScopeKey(sessionId, provider));
28666
29371
  return reports === void 0 ? [] : [...reports];
28667
29372
  } catch {
28668
29373
  return [];
@@ -28675,11 +29380,12 @@ function cacheWarmupUsageReports(sessionId) {
28675
29380
  function cacheWarmupUsageSessions() {
28676
29381
  try {
28677
29382
  const summaries = [];
28678
- for (const [sessionId, reports] of registry.entries()) {
28679
- if (reports.length === 0) continue;
29383
+ for (const reports of registry.values()) {
29384
+ const sample = reports[0];
29385
+ if (sample === void 0) continue;
28680
29386
  const last = reports[reports.length - 1];
28681
29387
  summaries.push({
28682
- sessionId,
29388
+ sessionId: sample.sessionId,
28683
29389
  reports: reports.length,
28684
29390
  lastAt: last.at,
28685
29391
  hitRatio: cacheWarmupHitRatio(last)
@@ -28726,10 +29432,10 @@ const WARMUP_BUDGET = Object.freeze({
28726
29432
  * @param durable - 进程重启后尚无内存报告时的持久化回退摘要(可选)。
28727
29433
  * @returns 本次投影可用的预算。
28728
29434
  */
28729
- function cacheWarmupBudget(sessionId, now = Date.now(), durable) {
29435
+ function cacheWarmupBudget(sessionId, now = Date.now(), durable, provider) {
28730
29436
  try {
28731
29437
  if (typeof sessionId !== "string" || sessionId.trim() === "") return { ...WARMUP_BUDGET };
28732
- const reports = cacheWarmupUsageReports(sessionId);
29438
+ const reports = cacheWarmupUsageReports(sessionId, provider);
28733
29439
  if (reports.length === 0) {
28734
29440
  if (durable !== void 0 && durable !== null && typeof durable === "object") {
28735
29441
  const lastAt = durable.lastActiveAt;
@@ -28765,7 +29471,7 @@ function cacheWarmupBudget(sessionId, now = Date.now(), durable) {
28765
29471
  * @param now - 当前时间戳(毫秒),默认 `Date.now()`。
28766
29472
  * @returns 快照。
28767
29473
  */
28768
- function cacheWarmupSnapshot(sessionId, now = Date.now()) {
29474
+ function cacheWarmupSnapshot(sessionId, now = Date.now(), provider) {
28769
29475
  const fallback = {
28770
29476
  budget: { ...WARMUP_BUDGET },
28771
29477
  defaults: CACHE_WARMUP_DEFAULTS,
@@ -28783,7 +29489,7 @@ function cacheWarmupSnapshot(sessionId, now = Date.now()) {
28783
29489
  sessions: 0
28784
29490
  };
28785
29491
  try {
28786
- const budget = cacheWarmupBudget(sessionId, now);
29492
+ const budget = cacheWarmupBudget(sessionId, now, void 0, provider);
28787
29493
  const reports = typeof sessionId === "string" && sessionId.trim() !== "" ? cacheWarmupUsageReports(sessionId) : [];
28788
29494
  const totals = {
28789
29495
  inputTokens: 0,
@@ -29378,7 +30084,7 @@ function planRetrievalOutline(text, toolName) {
29378
30084
  * @returns 读到的元数据;两者都没有时 undefined。
29379
30085
  */
29380
30086
  function shellMetadataOf(holder) {
29381
- const job = isPlainRecord$2(holder.job) ? holder.job : void 0;
30087
+ const job = isPlainRecord$1(holder.job) ? holder.job : void 0;
29382
30088
  let exitCode;
29383
30089
  if (typeof holder.exitCode === "number" && Number.isFinite(holder.exitCode)) exitCode = holder.exitCode;
29384
30090
  if (exitCode === void 0 && typeof job?.exitCode === "number" && Number.isFinite(job.exitCode)) exitCode = job.exitCode;
@@ -29509,7 +30215,7 @@ const JSON_ENVELOPE_LOG_KEYS = [
29509
30215
  /** JSON envelope 其余字段允许占用的最大序列化字符数(防止把含批量源码的混合 envelope 压掉)。 */
29510
30216
  const JSON_ENVELOPE_MAX_OTHER_CHARS = 800;
29511
30217
  /** 判断一个值是不是普通 JSON 对象(不是数组,也不是 null)。 */
29512
- function isPlainRecord$2(value) {
30218
+ function isPlainRecord$1(value) {
29513
30219
  return typeof value === "object" && value !== null && !Array.isArray(value);
29514
30220
  }
29515
30221
  /** 重建信封:只把预览写回被替换的键,其余字段原样保留。 */
@@ -29611,7 +30317,7 @@ function extractJsonEnvelopeLogField(text, toolName) {
29611
30317
  });
29612
30318
  }
29613
30319
  for (const [outerKey, outerValue] of Object.entries(record)) {
29614
- if (!isPlainRecord$2(outerValue)) continue;
30320
+ if (!isPlainRecord$1(outerValue)) continue;
29615
30321
  for (const key of JSON_ENVELOPE_LOG_KEYS) {
29616
30322
  const value = outerValue[key];
29617
30323
  if (typeof value === "string" && value.length > 0) candidates.push({
@@ -29701,7 +30407,7 @@ function canonicalGrepMatches(value) {
29701
30407
  if (!Array.isArray(value) || value.length === 0) return void 0;
29702
30408
  const matches = [];
29703
30409
  for (const entry of value) {
29704
- if (!isPlainRecord$2(entry)) return void 0;
30410
+ if (!isPlainRecord$1(entry)) return void 0;
29705
30411
  if (typeof entry.path !== "string") return void 0;
29706
30412
  if (!Number.isSafeInteger(entry.lineNumber)) return void 0;
29707
30413
  if (typeof entry.line !== "string") return void 0;
@@ -29715,12 +30421,12 @@ function canonicalGrepMatches(value) {
29715
30421
  }
29716
30422
  /** 规范读取结果。 */
29717
30423
  function canonicalReadResult(value) {
29718
- if (!isPlainRecord$2(value)) return void 0;
30424
+ if (!isPlainRecord$1(value)) return void 0;
29719
30425
  if (typeof value.path !== "string" || value.path.trim() === "") return void 0;
29720
30426
  if (!Array.isArray(value.lines) || value.lines.length === 0) return void 0;
29721
30427
  const lines = [];
29722
30428
  for (const entry of value.lines) {
29723
- if (!isPlainRecord$2(entry)) return void 0;
30429
+ if (!isPlainRecord$1(entry)) return void 0;
29724
30430
  if (!Number.isSafeInteger(entry.number)) return void 0;
29725
30431
  if (typeof entry.text !== "string") return void 0;
29726
30432
  lines.push({
@@ -29745,7 +30451,7 @@ function findGrepBinding(record) {
29745
30451
  matches: top
29746
30452
  };
29747
30453
  for (const [outerKey, outerValue] of Object.entries(record)) {
29748
- if (!isPlainRecord$2(outerValue)) continue;
30454
+ if (!isPlainRecord$1(outerValue)) continue;
29749
30455
  const nested = canonicalGrepMatches(outerValue.matches);
29750
30456
  if (nested !== void 0) return {
29751
30457
  binding: {
@@ -29772,7 +30478,7 @@ function findGlobBinding(record) {
29772
30478
  ...typeof record.root === "string" ? { root: record.root } : {}
29773
30479
  };
29774
30480
  for (const [outerKey, outerValue] of Object.entries(record)) {
29775
- if (!isPlainRecord$2(outerValue)) continue;
30481
+ if (!isPlainRecord$1(outerValue)) continue;
29776
30482
  const nested = canonicalGlobPaths(outerValue.paths);
29777
30483
  if (nested !== void 0) return {
29778
30484
  binding: {
@@ -29950,13 +30656,13 @@ function collectCompositeEntries(record) {
29950
30656
  for (const [key, value] of Object.entries(record)) {
29951
30657
  if (Array.isArray(value)) {
29952
30658
  value.forEach((item, index) => {
29953
- if (isPlainRecord$2(item)) push(`${key}[${index}]`, item);
30659
+ if (isPlainRecord$1(item)) push(`${key}[${index}]`, item);
29954
30660
  });
29955
30661
  continue;
29956
30662
  }
29957
- if (!isPlainRecord$2(value)) continue;
30663
+ if (!isPlainRecord$1(value)) continue;
29958
30664
  if (push(key, value)) continue;
29959
- for (const [subKey, subValue] of Object.entries(value)) if (isPlainRecord$2(subValue)) push(`${key}.${subKey}`, subValue);
30665
+ for (const [subKey, subValue] of Object.entries(value)) if (isPlainRecord$1(subValue)) push(`${key}.${subKey}`, subValue);
29960
30666
  }
29961
30667
  return entries;
29962
30668
  }
@@ -30380,6 +31086,17 @@ async function projectToolResult(input) {
30380
31086
  };
30381
31087
  }
30382
31088
  //#endregion
31089
+ //#region src/host/common/failure-classification.ts
31090
+ /** Clamp a measured reset delay without parking an account forever or reopening at once. */
31091
+ function cooldownFromReset(input) {
31092
+ const now = input.now ?? Date.now();
31093
+ const minimum = input.minimumMs ?? 0;
31094
+ const maximum = input.maximumMs ?? Number.MAX_SAFE_INTEGER;
31095
+ if (input.resetAtMs === void 0 || input.resetAtMs === null || !Number.isFinite(input.resetAtMs)) return Math.min(maximum, Math.max(minimum, input.fallbackMs));
31096
+ if (input.resetAtMs <= now) return Math.min(maximum, Math.max(minimum, input.fallbackMs));
31097
+ return Math.min(maximum, Math.max(minimum, input.resetAtMs - now));
31098
+ }
31099
+ //#endregion
30383
31100
  //#region src/host/kimi-code/model-catalog.ts
30384
31101
  /**
30385
31102
  * Request-body ceiling the 1M-context tier defaults to.
@@ -31363,6 +32080,67 @@ async function persistRegion(region) {
31363
32080
  await fs.writeFile(file, region, "utf8");
31364
32081
  } catch {}
31365
32082
  }
32083
+ //#endregion
32084
+ //#region src/host/common/cache-hint.ts
32085
+ /** Missing data always skips; a provider must supply its own measured TTL. */
32086
+ function evaluateCacheHint$1(input) {
32087
+ const { now, lastActiveAt, totalTokens } = input;
32088
+ if (lastActiveAt === void 0 || totalTokens === void 0) return { kind: "skip" };
32089
+ if (!Number.isFinite(lastActiveAt) || !Number.isFinite(totalTokens)) return { kind: "skip" };
32090
+ const ttlMs = input.ttlMs;
32091
+ const minTokens = input.minTokensToHint ?? 1e5;
32092
+ if (ttlMs === void 0 || !Number.isFinite(ttlMs) || ttlMs <= 0) return { kind: "skip" };
32093
+ if (!Number.isFinite(minTokens) || minTokens < 0 || totalTokens < minTokens) return { kind: "skip" };
32094
+ const idleMs = now - lastActiveAt;
32095
+ if (idleMs <= ttlMs) return { kind: "skip" };
32096
+ return {
32097
+ kind: "hint",
32098
+ idleMs,
32099
+ totalTokens
32100
+ };
32101
+ }
32102
+ //#endregion
32103
+ //#region src/host/kimi-code/cache-hint.ts
32104
+ /**
32105
+ * When it is worth telling the user their prompt cache has gone cold.
32106
+ *
32107
+ * Kimi's cache is automatic and content-hash based, so a client gets no marker
32108
+ * that it is no longer being reused — the only evidence is the read ratio in
32109
+ * the usage stream. A conversation that sat idle past the cache lifetime pays
32110
+ * the full prompt again on its next turn, and it is large enough on a real
32111
+ * codebase to be worth a sentence.
32112
+ *
32113
+ * The trigger mirrors the official client's: idle beyond the cache lifetime AND
32114
+ * a context large enough that reprocessing it actually costs something. The
32115
+ * upstream rules arrive from a remote config that a private deployment cannot
32116
+ * reach, so the lifetime here is the measured one; the decision function stays
32117
+ * a pure function of its inputs so swapping the source later changes only where
32118
+ * the number comes from.
32119
+ */
32120
+ /**
32121
+ * How long an entry survives, in milliseconds.
32122
+ *
32123
+ * MEASURED, not documented. The coding endpoint publishes no cache lifetime and
32124
+ * its `5m`/`1h` write tiers cannot be selected from any official client, so the
32125
+ * only figure available is the survival curve: 30 minutes, derived from the
32126
+ * endpoint's own measured behaviour.
32127
+ *
32128
+ * The measurement (2026-10-02/03, three replicas per age, one fresh prefix per
32129
+ * age so the probe does not renew what it is timing) put Kimi's coding plan at
32130
+ * 3/3 warm through 25 minutes, 1/3 at 30 minutes, and 0/3 at 35 minutes — a
32131
+ * 25–35 minute bracket around a fuzzy edge.
32132
+ *
32133
+ * 25 minutes is the last age at which every replica was still warm, and that is
32134
+ * what this notice needs: it must arrive BEFORE the entry is surely gone. The
32135
+ * 30-minute point where most replicas had gone cold is too late — by then the
32136
+ * user has already paid for the reprocessed prefix, so a notice there describes
32137
+ * the bill instead of preventing it.
32138
+ *
32139
+ * The old value was 5 minutes, taken from the `5m` write tier's NAME. On this
32140
+ * endpoint the name is not the lifetime: that estimate warned from minute 6
32141
+ * onward while the entry was still warm for another 19.
32142
+ */
32143
+ const CACHE_TTL_MS = 1500 * 1e3;
31366
32144
  /**
31367
32145
  * Last time each session did something, epoch ms.
31368
32146
  *
@@ -31394,26 +32172,13 @@ function sessionLastActiveAt(sessionId) {
31394
32172
  if (sessionId === void 0 || sessionId.trim() === "") return void 0;
31395
32173
  return lastActiveBySession.get(sessionId.trim());
31396
32174
  }
31397
- /**
31398
- * Whether one conversation should be warned about a cold cache.
31399
- *
31400
- * Every missing-data branch skips. That asymmetry is deliberate and matches
31401
- * the official client: a false negative costs one turn nobody noticed, while a
31402
- * false positive teaches the user the notice is noise and stops them reading
31403
- * the one that mattered.
31404
- */
32175
+ /** Kimi supplies its measured defaults; other providers call the common pure function directly. */
31405
32176
  function evaluateCacheHint(input) {
31406
- const { now, lastActiveAt, totalTokens } = input;
31407
- if (lastActiveAt === void 0 || totalTokens === void 0) return { kind: "skip" };
31408
- if (!Number.isFinite(lastActiveAt) || !Number.isFinite(totalTokens)) return { kind: "skip" };
31409
- if (totalTokens < 1e5) return { kind: "skip" };
31410
- const idleMs = now - lastActiveAt;
31411
- if (idleMs <= 15e5) return { kind: "skip" };
31412
- return {
31413
- kind: "hint",
31414
- idleMs,
31415
- totalTokens
31416
- };
32177
+ return evaluateCacheHint$1({
32178
+ ...input,
32179
+ ttlMs: input.ttlMs ?? 15e5,
32180
+ minTokensToHint: input.minTokensToHint ?? 1e5
32181
+ });
31417
32182
  }
31418
32183
  //#endregion
31419
32184
  //#region src/host/kimi-code/oauth.ts
@@ -33414,6 +34179,48 @@ function estimatedInputTokens(options) {
33414
34179
  return Math.ceil(characters / 4);
33415
34180
  }
33416
34181
  //#endregion
34182
+ //#region src/host/common/reasoning-dialect.ts
34183
+ /** Learn the reasoning field a concrete OpenAI-compatible endpoint speaks. */
34184
+ const KNOWN_REASONING_KEYS$1 = [
34185
+ "reasoning_content",
34186
+ "reasoning_details",
34187
+ "reasoning"
34188
+ ];
34189
+ var ReasoningKeyDialect$1 = class {
34190
+ defaultKey;
34191
+ keys;
34192
+ detected;
34193
+ constructor(defaultKey = KNOWN_REASONING_KEYS$1[0], keys = KNOWN_REASONING_KEYS$1) {
34194
+ this.defaultKey = defaultKey;
34195
+ this.keys = keys;
34196
+ }
34197
+ /** Record an observed field and return its text. Empty strings still teach the key. */
34198
+ observe(source) {
34199
+ if (typeof source !== "object" || source === null) return void 0;
34200
+ const record = source;
34201
+ for (const key of this.keys) {
34202
+ const value = record[key];
34203
+ if (typeof value === "string") {
34204
+ this.learn(key);
34205
+ return value;
34206
+ }
34207
+ }
34208
+ }
34209
+ /** Record a previously parsed fragment without re-reading the wire object. */
34210
+ observeKey(key) {
34211
+ if (this.keys.includes(key)) this.learn(key);
34212
+ }
34213
+ learn(key) {
34214
+ this.detected = key;
34215
+ }
34216
+ outboundKey() {
34217
+ return this.detected ?? this.defaultKey;
34218
+ }
34219
+ reset() {
34220
+ this.detected = void 0;
34221
+ }
34222
+ };
34223
+ //#endregion
33417
34224
  //#region src/host/common/video.ts
33418
34225
  /**
33419
34226
  * Video input, shared by every line in this plugin that accepts it.
@@ -33717,13 +34524,8 @@ function sanitizeText$7(text) {
33717
34524
  * `reasoning_details`. The first entry doubles as the default for a request
33718
34525
  * made before any response has been seen.
33719
34526
  */
33720
- const KNOWN_REASONING_KEYS = [
33721
- "reasoning_content",
33722
- "reasoning_details",
33723
- "reasoning"
33724
- ];
33725
- /** Default outbound reasoning key, used until a response proves otherwise. */
33726
- const DEFAULT_REASONING_KEY = KNOWN_REASONING_KEYS[0];
34527
+ const KNOWN_REASONING_KEYS = KNOWN_REASONING_KEYS$1;
34528
+ KNOWN_REASONING_KEYS[0];
33727
34529
  /**
33728
34530
  * One `reasoning_details` element, in an official or a compatibility shape.
33729
34531
  *
@@ -33846,8 +34648,7 @@ function reasoningFragmentsOf(source) {
33846
34648
  * turn. Detection never clears: a response with no reasoning keeps the last
33847
34649
  * known dialect, and an endpoint that switches is adapted on its next reply.
33848
34650
  */
33849
- var ReasoningKeyDialect = class {
33850
- detected;
34651
+ var ReasoningKeyDialect = class extends ReasoningKeyDialect$1 {
33851
34652
  /**
33852
34653
  * Every reasoning fragment one inbound delta carries, remembering the dialect.
33853
34654
  *
@@ -33867,13 +34668,13 @@ var ReasoningKeyDialect = class {
33867
34668
  const fragments = reasoningFragmentsOf(source);
33868
34669
  const teaching = fragments.find((fragment) => fragment.form === void 0 || fragment.form === "string");
33869
34670
  if (teaching !== void 0) {
33870
- this.detected = teaching.key;
34671
+ this.learn(teaching.key);
33871
34672
  return fragments;
33872
34673
  }
33873
34674
  if (typeof source === "object" && source !== null) {
33874
34675
  const record = source;
33875
34676
  for (const key of KNOWN_REASONING_KEYS) if (typeof record[key] === "string") {
33876
- this.detected = key;
34677
+ this.learn(key);
33877
34678
  break;
33878
34679
  }
33879
34680
  }
@@ -33883,14 +34684,6 @@ var ReasoningKeyDialect = class {
33883
34684
  observe(source) {
33884
34685
  return this.observeAll(source)[0]?.text;
33885
34686
  }
33886
- /** The key to serialize thinking into on an outbound assistant message. */
33887
- outboundKey() {
33888
- return this.detected ?? DEFAULT_REASONING_KEY;
33889
- }
33890
- /** Forget the detected key, returning to the default. Test seam only. */
33891
- reset() {
33892
- this.detected = void 0;
33893
- }
33894
34687
  };
33895
34688
  /**
33896
34689
  * Process-wide reasoning dialect for this route.
@@ -34236,59 +35029,7 @@ function nonSystemMessages$6(options) {
34236
35029
  * arrived (append-only in-history).
34237
35030
  */
34238
35031
  function classifyBootstrapUserMessage(message) {
34239
- if (message.role !== "user" || isToolResultMessage$7(message)) return null;
34240
- const source = isRecord$53(message.source) ? message.source : void 0;
34241
- const kind = source !== void 0 ? asString$21(source.kind) : void 0;
34242
- const text = firstTextOf(message.content)?.trimStart() ?? "";
34243
- if (kind === "progress-updates-language" || text.startsWith("Progress update output language override:")) return 1;
34244
- if (text.startsWith("<system-reminder>\nThe following workspace instructions") || kind === "agent-instructions" && !text.startsWith("<system-reminder>\nUpdated instructions") && !text.startsWith("<system-reminder>\nAdditional instructions") && !text.startsWith("<system-reminder>\nInstructions removed")) return 2;
34245
- if (text.startsWith("<system-reminder>\nA skill is a reusable set of task-specific instructions.") || kind === "skill-catalog" && !text.startsWith("<system-reminder>\nThe available skill catalog changed.")) return 3;
34246
- if (kind === "runtime-context" || text.startsWith("Current runtime context. This snapshot supersedes")) return 4;
34247
- return null;
34248
- }
34249
- function isCompactCheckpointMessage(message) {
34250
- if (message === void 0 || message.role !== "user" || isToolResultMessage$7(message)) return false;
34251
- const source = isRecord$53(message.source) ? message.source : void 0;
34252
- if (source !== void 0 && asString$21(source.kind) === "compact-checkpoint") return true;
34253
- return (firstTextOf(message.content)?.trimStart() ?? "").startsWith("This is an automatically generated checkpoint condensing");
34254
- }
34255
- /**
34256
- * Normalize `agent-instructions` text so global `~/.dsh/` instructions precede
34257
- * workspace-local `AGENTS.md` instructions when both appear in the same block.
34258
- */
34259
- function normalizeAgentInstructionsMessage(message) {
34260
- if (!Array.isArray(message.content) || message.content.length !== 1) return message;
34261
- const first = message.content[0];
34262
- if (!isRecord$53(first) || first.type !== "text" || typeof first.text !== "string") return message;
34263
- const raw = first.text;
34264
- const firstIdx = raw.indexOf("\n\nInstructions from: ");
34265
- if (firstIdx < 0) return message;
34266
- const closeIdx = raw.lastIndexOf("\n</system-reminder>");
34267
- if (closeIdx <= firstIdx) return message;
34268
- const header = raw.slice(0, firstIdx);
34269
- const body = raw.slice(firstIdx + 2, closeIdx);
34270
- const tail = raw.slice(closeIdx);
34271
- const sections = body.split("\n\nInstructions from: ");
34272
- if (sections.length < 2) return message;
34273
- const tagged = sections.map((sec, idx) => {
34274
- const firstLineEnd = sec.indexOf("\n");
34275
- const sourcePath = (firstLineEnd < 0 ? sec : sec.slice(0, firstLineEnd)).trim();
34276
- return {
34277
- sec,
34278
- idx,
34279
- rank: sourcePath.startsWith("~/") || sourcePath.startsWith("~\\") || sourcePath.includes("/.dsh/") || sourcePath.includes("\\.dsh\\") ? 0 : 1
34280
- };
34281
- });
34282
- if (!tagged.some((item, i) => i > 0 && item.rank < tagged[i - 1].rank)) return message;
34283
- tagged.sort((a, b) => a.rank - b.rank || a.idx - b.idx);
34284
- const rebuilt = `${header}\n\nInstructions from: ${tagged.map((t) => t.sec).join("\n\nInstructions from: ")}${tail}`;
34285
- return {
34286
- ...message,
34287
- content: [{
34288
- ...first,
34289
- text: rebuilt
34290
- }]
34291
- };
35032
+ return classifyBootstrapUserMessage$1(message);
34292
35033
  }
34293
35034
  /**
34294
35035
  * Deterministic entropy-sorted ordering for leading bootstrap reminders.
@@ -34304,73 +35045,7 @@ function normalizeAgentInstructionsMessage(message) {
34304
35045
  * compaction boundaries and every step after compaction.
34305
35046
  */
34306
35047
  function orderStaticPrefixMessages(messages) {
34307
- try {
34308
- if (messages.length <= 1) return [...messages];
34309
- let leadEnd = 0;
34310
- while (leadEnd < messages.length && messages[leadEnd].role === "user" && !isToolResultMessage$7(messages[leadEnd])) leadEnd += 1;
34311
- if (leadEnd > 0 && isCompactCheckpointMessage(messages[0])) {
34312
- const seenInLead = /* @__PURE__ */ new Set();
34313
- for (let i = 0; i < leadEnd; i++) {
34314
- const tier = classifyBootstrapUserMessage(messages[i]);
34315
- if (tier !== null) seenInLead.add(tier);
34316
- }
34317
- if (!seenInLead.has(1) && !seenInLead.has(2)) {
34318
- const hoistedByTier = /* @__PURE__ */ new Map();
34319
- for (let i = leadEnd; i < messages.length; i++) {
34320
- const msg = messages[i];
34321
- const tier = classifyBootstrapUserMessage(msg);
34322
- if ((tier === 1 || tier === 2 || tier === 3) && !hoistedByTier.has(tier)) hoistedByTier.set(tier, {
34323
- index: i,
34324
- message: tier === 2 ? normalizeAgentInstructionsMessage(msg) : msg
34325
- });
34326
- }
34327
- if (hoistedByTier.size > 0) {
34328
- const hoistedIndices = /* @__PURE__ */ new Set();
34329
- const prefix = [];
34330
- for (const tier of [
34331
- 1,
34332
- 2,
34333
- 3
34334
- ]) {
34335
- const entry = hoistedByTier.get(tier);
34336
- if (entry !== void 0) {
34337
- hoistedIndices.add(entry.index);
34338
- prefix.push(entry.message);
34339
- }
34340
- }
34341
- const rest = messages.filter((_, idx) => !hoistedIndices.has(idx));
34342
- return [...prefix, ...rest];
34343
- }
34344
- }
34345
- }
34346
- if (leadEnd <= 1) return [...messages];
34347
- const seenTiers = /* @__PURE__ */ new Set();
34348
- const leadTagged = [];
34349
- let hasBootstrap = false;
34350
- for (let i = 0; i < leadEnd; i++) {
34351
- const rawMsg = messages[i];
34352
- const tier = classifyBootstrapUserMessage(rawMsg);
34353
- if (tier !== null && !seenTiers.has(tier)) {
34354
- seenTiers.add(tier);
34355
- hasBootstrap = true;
34356
- leadTagged.push({
34357
- message: tier === 2 ? normalizeAgentInstructionsMessage(rawMsg) : rawMsg,
34358
- rank: tier,
34359
- index: i
34360
- });
34361
- } else leadTagged.push({
34362
- message: rawMsg,
34363
- rank: 5,
34364
- index: i
34365
- });
34366
- }
34367
- if (!hasBootstrap) return [...messages];
34368
- if (!leadTagged.some((item, i) => i > 0 && item.rank < leadTagged[i - 1].rank || item.message !== messages[i])) return [...messages];
34369
- leadTagged.sort((a, b) => a.rank - b.rank || a.index - b.index);
34370
- return [...leadTagged.map((item) => item.message), ...messages.slice(leadEnd)];
34371
- } catch {
34372
- return [...messages];
34373
- }
35048
+ return orderStaticPrefixMessages$1(messages);
34374
35049
  }
34375
35050
  const NO_REASONING = {
34376
35051
  fields: [],
@@ -35992,12 +36667,11 @@ function trackPrefixStability(options) {
35992
36667
  toolsHash,
35993
36668
  cacheKey
35994
36669
  });
35995
- if (previous === void 0 || previous.systemPromptHash === null && previous.toolsHash === null && previous.cacheKey === null) return "first-request";
35996
- if (cacheKey === null) return "cold-key";
35997
- if (previous.cacheKey !== cacheKey) return "cache-key";
35998
- if (previous.systemPromptHash !== systemPromptHash) return "system-prompt";
35999
- if (previous.toolsHash !== toolsHash) return "tools";
36000
- return "stable";
36670
+ return classifyPrefixDrift(previous, {
36671
+ systemPromptHash,
36672
+ toolsHash,
36673
+ cacheKey
36674
+ });
36001
36675
  }
36002
36676
  /** Drift cause recorded for each session. */
36003
36677
  const driftCauses = /* @__PURE__ */ new Map();
@@ -36315,61 +36989,26 @@ function closeStream$4(state, accountId) {
36315
36989
  function assertStreamComplete$5(state) {
36316
36990
  if (!state.done && state.finishReason === null) throw new LlmError$1("Kimi Code stream ended before its terminal event", "TRANSPORT");
36317
36991
  }
36318
- /** Where the document lives; beside the other plugin-owned JSON. */
36319
- function sessionCacheStatePath() {
36320
- return path.join(dshHomeDir(), "storages", "dsh-chatgpt-subscription-kimi-cache.json");
36321
- }
36322
- /** Distinguishes concurrent temporary files written by one process. */
36323
- let writeSequence = 0;
36324
- /**
36325
- * Whether a scheduled write should reach the disk.
36326
- *
36327
- * A unit test that exercises the request path must not overwrite the state file
36328
- * of the machine running it; tests build their own store against a temporary
36329
- * path (which always writes) and call {@link SessionCacheStateStore.flush}.
36330
- */
36331
- function persistenceEnabled() {
36332
- return process.env.VITEST !== "true" && process.env.NODE_ENV !== "test";
36333
- }
36992
+ //#endregion
36993
+ //#region src/host/common/session-cache-state.ts
36334
36994
  /**
36335
- * Per-session cache state held in one JSON document.
36336
- *
36337
- * Reads are synchronous because every caller is answering a status request or
36338
- * closing a turn, both of which are allowed to block on a file this size. Writes
36339
- * are debounced: a turn reports once, and several turns in flight must not
36340
- * rewrite the document several times.
36995
+ * Generic bounded, durable session state for cache diagnostics and sticky media.
36996
+ * One provider owns one document; records remain keyed by session inside it.
36341
36997
  */
36342
- var SessionCacheStateStore = class {
36998
+ var CommonSessionCacheStateStore = class CommonSessionCacheStateStore {
36343
36999
  document;
36344
37000
  pending;
36345
- /**
36346
- * @param filePath - Document location; defaults to {@link sessionCacheStatePath}.
36347
- * @param maxEntries - Conversations kept; the oldest are dropped first.
36348
- */
36349
37001
  filePath;
36350
37002
  maxEntries;
36351
- /**
36352
- * Whether this store may write at all.
36353
- *
36354
- * A store built on an explicit path is someone's own file - a test's temporary
36355
- * document - and always writes. The default-path store belongs to the user's
36356
- * harness home, and a test that never named a path must not overwrite it.
36357
- */
36358
37003
  allowWrite;
36359
- constructor(filePath = sessionCacheStatePath(), maxEntries = 64) {
36360
- this.filePath = filePath;
36361
- this.maxEntries = maxEntries;
36362
- this.allowWrite = filePath !== sessionCacheStatePath() || persistenceEnabled();
37004
+ normalizeRecord;
37005
+ static writeSequence = 0;
37006
+ constructor(options) {
37007
+ this.filePath = options.filePath;
37008
+ this.maxEntries = options.maxEntries ?? 64;
37009
+ this.allowWrite = options.allowWrite ?? true;
37010
+ this.normalizeRecord = options.normalizeRecord;
36363
37011
  }
36364
- /**
36365
- * The record for one conversation, or the most recently active one.
36366
- *
36367
- * The fallback is what makes the status card useful without a session named:
36368
- * the card describes the line, and the only conversation it can honestly
36369
- * describe in that case is the one that ran last.
36370
- * @param sessionId - Conversation to read; omit for the most recent one.
36371
- * @returns the record, or undefined when nothing has ever been recorded.
36372
- */
36373
37012
  read(sessionId) {
36374
37013
  const sessions = this.load().sessions;
36375
37014
  const named = sessionId?.trim();
@@ -36378,80 +37017,59 @@ var SessionCacheStateStore = class {
36378
37017
  for (const record of Object.values(sessions)) if (newest === void 0 || record.updatedAt > newest.updatedAt) newest = record;
36379
37018
  return newest;
36380
37019
  }
36381
- /**
36382
- * File one conversation's state and schedule the document write.
36383
- * @param sessionId - Conversation the record belongs to; ignored when blank.
36384
- * @param record - Complete record for that conversation.
36385
- */
36386
37020
  write(sessionId, record) {
36387
37021
  const key = sessionId.trim();
36388
37022
  if (key === "") return;
36389
37023
  const document = this.load();
36390
37024
  document.sessions[key] = record;
36391
- this.prune(document);
37025
+ const entries = Object.entries(document.sessions);
37026
+ if (entries.length > this.maxEntries) {
37027
+ entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
37028
+ document.sessions = Object.fromEntries(entries.slice(0, this.maxEntries));
37029
+ }
36392
37030
  this.schedule();
36393
37031
  }
36394
- /**
36395
- * Update the warmup consecutive-hit streak on an already-written session record
36396
- * in place, without issuing a second `write()` call.
36397
- */
36398
- noteWarmupStreak(sessionId, consecutiveHits) {
37032
+ /** Update an existing record; absence is not created implicitly. */
37033
+ update(sessionId, update) {
36399
37034
  const key = sessionId.trim();
36400
- if (key === "") return;
37035
+ if (key === "") return false;
36401
37036
  const document = this.load();
36402
37037
  const existing = document.sessions[key];
36403
- if (existing === void 0) return;
36404
- const normalized = count$1(consecutiveHits);
36405
- if (normalized > 0) document.sessions[key] = {
36406
- ...existing,
36407
- warmupConsecutiveHits: normalized
36408
- };
36409
- else if (existing.warmupConsecutiveHits !== void 0) {
36410
- const { warmupConsecutiveHits: _dropped, ...rest } = existing;
36411
- document.sessions[key] = rest;
36412
- } else return;
37038
+ if (existing === void 0) return false;
37039
+ const next = update(existing);
37040
+ if (next === existing) return true;
37041
+ document.sessions[key] = next;
36413
37042
  this.schedule();
37043
+ return true;
36414
37044
  }
36415
- /**
36416
- * Write the document now, synchronously.
36417
- *
36418
- * Exposed for tests and for a caller that is about to lose the process; the
36419
- * debounced path is what normal turns use.
36420
- */
36421
37045
  flush() {
36422
37046
  this.cancelPending();
36423
- const document = this.document;
36424
- if (document === void 0) return;
36425
- this.persist(document);
37047
+ if (this.document !== void 0) this.persist(this.document);
36426
37048
  }
36427
- /** Drop everything older than the newest maxEntries conversations. */
36428
- prune(document) {
36429
- const entries = Object.entries(document.sessions);
36430
- if (entries.length <= this.maxEntries) return;
36431
- entries.sort((a, b) => b[1].updatedAt - a[1].updatedAt);
36432
- document.sessions = Object.fromEntries(entries.slice(0, this.maxEntries));
36433
- }
36434
- /** Read the document once; a missing or corrupt file reads as empty. */
36435
37049
  load() {
36436
37050
  if (this.document !== void 0) return this.document;
37051
+ const sessions = {};
36437
37052
  try {
36438
37053
  const parsed = JSON.parse(fs$1.readFileSync(this.filePath, "utf8"));
36439
- this.document = normalizeDocument(parsed);
36440
- } catch {
36441
- this.document = {
36442
- version: 1,
36443
- sessions: {}
36444
- };
36445
- }
37054
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
37055
+ const rawSessions = parsed.sessions;
37056
+ if (typeof rawSessions === "object" && rawSessions !== null && !Array.isArray(rawSessions)) for (const [key, entry] of Object.entries(rawSessions)) {
37057
+ const record = this.normalizeRecord(entry);
37058
+ if (record !== void 0) sessions[key] = record;
37059
+ }
37060
+ }
37061
+ } catch {}
37062
+ this.document = {
37063
+ version: 1,
37064
+ sessions
37065
+ };
36446
37066
  return this.document;
36447
37067
  }
36448
- /** Coalesce the writes one turn's several fields would otherwise cause. */
36449
37068
  schedule() {
36450
37069
  if (!this.allowWrite || this.pending !== void 0) return;
36451
37070
  this.pending = setTimeout(() => {
36452
37071
  this.pending = void 0;
36453
- const document = this.document;
36454
- if (document !== void 0) this.persist(document);
37072
+ if (this.document !== void 0) this.persist(this.document);
36455
37073
  }, 250);
36456
37074
  this.pending.unref?.();
36457
37075
  }
@@ -36460,35 +37078,69 @@ var SessionCacheStateStore = class {
36460
37078
  clearTimeout(this.pending);
36461
37079
  this.pending = void 0;
36462
37080
  }
36463
- /** Write through a temporary file so a crash cannot leave half a document. */
36464
37081
  persist(document) {
36465
37082
  if (!this.allowWrite) return;
36466
37083
  try {
36467
37084
  fs$1.mkdirSync(path.dirname(this.filePath), { recursive: true });
36468
- const temporary = this.filePath + "." + process.pid + "." + ++writeSequence + ".tmp";
37085
+ const temporary = this.filePath + "." + process.pid + "." + ++CommonSessionCacheStateStore.writeSequence + ".tmp";
36469
37086
  fs$1.writeFileSync(temporary, JSON.stringify(document), "utf8");
36470
37087
  fs$1.renameSync(temporary, this.filePath);
36471
37088
  } catch {}
36472
37089
  }
36473
37090
  };
36474
- /** Validate one parsed document, dropping entries that do not describe a record. */
36475
- function normalizeDocument(value) {
36476
- const sessions = {};
36477
- if (isRecord$52(value) && isRecord$52(value.sessions)) for (const [key, entry] of Object.entries(value.sessions)) {
36478
- const record = normalizeRecord(entry);
36479
- if (record !== void 0) sessions[key] = record;
36480
- }
36481
- return {
36482
- version: 1,
36483
- sessions
36484
- };
37091
+ /** Where the document lives; beside the other plugin-owned JSON. */
37092
+ function sessionCacheStatePath() {
37093
+ return path.join(dshHomeDir(), "storages", "dsh-chatgpt-subscription-kimi-cache.json");
36485
37094
  }
36486
- function normalizeRecord(value) {
37095
+ /**
37096
+ * Whether a scheduled write should reach the disk.
37097
+ *
37098
+ * A unit test that exercises the request path must not overwrite the state file
37099
+ * of the machine running it; tests build their own store against a temporary
37100
+ * path (which always writes) and call {@link SessionCacheStateStore.flush}.
37101
+ */
37102
+ function persistenceEnabled$2() {
37103
+ return process.env.VITEST !== "true" && process.env.NODE_ENV !== "test";
37104
+ }
37105
+ /**
37106
+ * Per-session cache state held in one JSON document.
37107
+ *
37108
+ * Reads are synchronous because every caller is answering a status request or
37109
+ * closing a turn, both of which are allowed to block on a file this size. Writes
37110
+ * are debounced: a turn reports once, and several turns in flight must not
37111
+ * rewrite the document several times.
37112
+ */
37113
+ var SessionCacheStateStore = class extends CommonSessionCacheStateStore {
37114
+ constructor(filePath = sessionCacheStatePath(), maxEntries = 64) {
37115
+ super({
37116
+ filePath,
37117
+ maxEntries,
37118
+ allowWrite: filePath !== sessionCacheStatePath() || persistenceEnabled$2(),
37119
+ normalizeRecord: normalizeRecord$2
37120
+ });
37121
+ }
37122
+ /** Update the warmup streak without creating a record for an unknown session. */
37123
+ noteWarmupStreak(sessionId, consecutiveHits) {
37124
+ this.update(sessionId, (existing) => {
37125
+ const normalized = count$1(consecutiveHits);
37126
+ if (normalized > 0) return {
37127
+ ...existing,
37128
+ warmupConsecutiveHits: normalized
37129
+ };
37130
+ if (existing.warmupConsecutiveHits === void 0) return existing;
37131
+ const { warmupConsecutiveHits: _dropped, ...rest } = existing;
37132
+ return rest;
37133
+ });
37134
+ }
37135
+ };
37136
+ function normalizeRecord$2(value) {
36487
37137
  if (!isRecord$52(value)) return void 0;
36488
37138
  const lastActiveAt = finite(value.lastActiveAt);
36489
37139
  const updatedAt = finite(value.updatedAt);
36490
37140
  if (lastActiveAt === void 0 || updatedAt === void 0) return void 0;
36491
37141
  const drift = typeof value.lastDrift === "string" && value.lastDrift !== "" ? value.lastDrift : void 0;
37142
+ const firstRequestCachedTokens = nonNegative(value.firstRequestCachedTokens);
37143
+ const firstRequestPromptTokens = nonNegative(value.firstRequestPromptTokens);
36492
37144
  const warmupStreak = count$1(value.warmupConsecutiveHits);
36493
37145
  const budgetDropped = droppedImageKeys(value.budgetDropped);
36494
37146
  const lastRequestImageBytes = nonNegative(value.lastRequestImageBytes);
@@ -36505,6 +37157,8 @@ function normalizeRecord(value) {
36505
37157
  cacheWrite5mTokens: count$1(value.cacheWrite5mTokens),
36506
37158
  cacheWrite1hTokens: count$1(value.cacheWrite1hTokens),
36507
37159
  ...drift === void 0 ? {} : { lastDrift: drift },
37160
+ ...firstRequestCachedTokens === void 0 ? {} : { firstRequestCachedTokens },
37161
+ ...firstRequestPromptTokens === void 0 ? {} : { firstRequestPromptTokens },
36508
37162
  ...warmupStreak > 0 ? { warmupConsecutiveHits: warmupStreak } : {},
36509
37163
  ...budgetDropped.length > 0 ? { budgetDropped } : {},
36510
37164
  ...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
@@ -36583,6 +37237,119 @@ function count$1(value) {
36583
37237
  return number === void 0 || number < 0 ? 0 : Math.floor(number);
36584
37238
  }
36585
37239
  //#endregion
37240
+ //#region src/host/common/hosted-media.ts
37241
+ /** Default bounded LRU; providers with durable caches can supply their own adapter. */
37242
+ var MemoryHostedMediaCache = class {
37243
+ maxEntries;
37244
+ entries = /* @__PURE__ */ new Map();
37245
+ constructor(maxEntries = 256) {
37246
+ this.maxEntries = maxEntries;
37247
+ }
37248
+ get size() {
37249
+ return this.entries.size;
37250
+ }
37251
+ get(key) {
37252
+ const value = this.entries.get(key);
37253
+ if (value === void 0) return void 0;
37254
+ this.entries.delete(key);
37255
+ this.entries.set(key, value);
37256
+ return value;
37257
+ }
37258
+ set(key, value) {
37259
+ this.entries.delete(key);
37260
+ this.entries.set(key, value);
37261
+ while (this.entries.size > this.maxEntries) {
37262
+ const oldest = this.entries.keys().next();
37263
+ if (oldest.done !== true) this.entries.delete(oldest.value);
37264
+ }
37265
+ }
37266
+ delete(key) {
37267
+ return this.entries.delete(key);
37268
+ }
37269
+ clear() {
37270
+ this.entries.clear();
37271
+ }
37272
+ };
37273
+ var HostedMediaUploadCoordinator = class {
37274
+ cache;
37275
+ flights = /* @__PURE__ */ new Map();
37276
+ constructor(options = {}) {
37277
+ this.cache = options.cache ?? new MemoryHostedMediaCache(options.maxCacheEntries);
37278
+ }
37279
+ get size() {
37280
+ return this.cache.size ?? 0;
37281
+ }
37282
+ /** Upload once per key; one caller aborts only its own wait. */
37283
+ async upload(key, callerSignal, start) {
37284
+ if (callerSignal?.aborted === true) throw abortWithReason(callerSignal);
37285
+ const cached = await this.cache.get(key);
37286
+ if (cached !== void 0) return cached;
37287
+ let flight = this.flights.get(key);
37288
+ if (flight === void 0) {
37289
+ const controller = new AbortController();
37290
+ const promise = start(controller).then(async (value) => {
37291
+ await this.cache.set(key, value);
37292
+ return value;
37293
+ });
37294
+ flight = {
37295
+ promise,
37296
+ controller,
37297
+ waiters: 0
37298
+ };
37299
+ this.flights.set(key, flight);
37300
+ promise.catch(() => void 0);
37301
+ promise.finally(() => {
37302
+ if (this.flights.get(key) === flight) this.flights.delete(key);
37303
+ }).catch(() => void 0);
37304
+ }
37305
+ return this.join(flight, callerSignal);
37306
+ }
37307
+ forget(key) {
37308
+ return this.cache.delete(key);
37309
+ }
37310
+ clear() {
37311
+ return this.cache.clear();
37312
+ }
37313
+ async join(flight, signal) {
37314
+ if (signal?.aborted === true) throw abortWithReason(signal);
37315
+ flight.waiters += 1;
37316
+ let released = false;
37317
+ const release = () => {
37318
+ if (released) return;
37319
+ released = true;
37320
+ flight.waiters -= 1;
37321
+ if (flight.waiters <= 0) {
37322
+ for (const [key, candidate] of this.flights) if (candidate === flight) this.flights.delete(key);
37323
+ flight.controller.abort();
37324
+ }
37325
+ };
37326
+ if (signal === void 0) try {
37327
+ return await flight.promise;
37328
+ } finally {
37329
+ release();
37330
+ }
37331
+ let detach;
37332
+ const cancelled = new Promise((_resolve, reject) => {
37333
+ const onAbort = () => reject(abortWithReason(signal));
37334
+ detach = () => signal.removeEventListener("abort", onAbort);
37335
+ signal.addEventListener("abort", onAbort, { once: true });
37336
+ });
37337
+ try {
37338
+ return await Promise.race([flight.promise, cancelled]);
37339
+ } finally {
37340
+ detach?.();
37341
+ release();
37342
+ }
37343
+ }
37344
+ };
37345
+ function abortWithReason(signal) {
37346
+ const reason = signal.reason;
37347
+ if (reason instanceof Error) return reason;
37348
+ const error = /* @__PURE__ */ new Error("the media upload was aborted");
37349
+ error.name = "AbortError";
37350
+ return error;
37351
+ }
37352
+ //#endregion
36586
37353
  //#region src/host/kimi-code/media-files.ts
36587
37354
  /**
36588
37355
  * Kimi Code hosted-file (media upload) client.
@@ -36863,60 +37630,38 @@ function abortError(signal) {
36863
37630
  */
36864
37631
  var KimiMediaFilesUploader = class {
36865
37632
  fetchFn;
36866
- maxCacheEntries;
36867
37633
  timeoutMs;
36868
- /** key -> `ms://<id>`, in least-recently-used order. */
36869
- cache = /* @__PURE__ */ new Map();
36870
- flights = /* @__PURE__ */ new Map();
37634
+ coordinator;
36871
37635
  constructor(options = {}) {
36872
37636
  this.fetchFn = options.fetchFn ?? fetch;
36873
- const cap = options.maxCacheEntries;
36874
- this.maxCacheEntries = cap !== void 0 && Number.isFinite(cap) && cap > 0 ? Math.floor(cap) : 256;
37637
+ this.coordinator = new HostedMediaUploadCoordinator({ maxCacheEntries: options.maxCacheEntries });
36875
37638
  const timeout = options.timeoutMs;
36876
37639
  this.timeoutMs = timeout !== void 0 && Number.isFinite(timeout) ? timeout : MEDIA_UPLOAD_TIMEOUT_MS;
36877
37640
  }
36878
- /** Cached references currently held, for a settings surface or a test. */
36879
37641
  get size() {
36880
- return this.cache.size;
37642
+ return this.coordinator.size;
36881
37643
  }
36882
- /** Uploads one file and returns the `ms://<id>` reference a request cites. */
36883
37644
  async upload(request) {
36884
37645
  const resolved = this.resolve(request);
36885
- if (resolved.signal?.aborted === true) throw abortError(resolved.signal);
36886
- const cached = this.readCache(resolved.key);
36887
- if (cached !== void 0) return cached;
36888
- let flight = this.flights.get(resolved.key);
36889
- if (flight === void 0) {
36890
- flight = this.startFlight(resolved);
36891
- this.flights.set(resolved.key, flight);
36892
- }
36893
- return this.join(flight, resolved);
37646
+ return this.coordinator.upload(resolved.key, resolved.signal, (controller) => this.performUpload(controller, { timedOut: false }, resolved));
36894
37647
  }
36895
- /**
36896
- * Drop the reference one upload would use.
36897
- *
36898
- * The caller that sees a stored id REJECTED on the wire calls this so the next
36899
- * attempt uploads again instead of replaying the same dead reference.
36900
- */
37648
+ /** Drop a dead reference so the next attempt uploads again. */
36901
37649
  forget(request) {
36902
- let key;
36903
37650
  try {
36904
- key = mediaFileCacheKey({
37651
+ const key = mediaFileCacheKey({
36905
37652
  accountScope: request.accountScope,
36906
37653
  baseUrl: request.baseUrl,
36907
37654
  mediaType: request.mediaType,
36908
37655
  data: request.data
36909
37656
  });
37657
+ return this.coordinator.forget(key);
36910
37658
  } catch {
36911
37659
  return false;
36912
37660
  }
36913
- return this.cache.delete(key);
36914
37661
  }
36915
- /** Forget everything; a sign-out or a test starting over. */
36916
37662
  clear() {
36917
- this.cache.clear();
37663
+ this.coordinator.clear();
36918
37664
  }
36919
- /** Validate and normalize one request, or throw before anything is sent. */
36920
37665
  resolve(request) {
36921
37666
  const accessToken = request.accessToken.trim();
36922
37667
  if (accessToken === "") throw new KimiMediaUploadError("invalid-request", "the media upload needs an access token");
@@ -36947,77 +37692,6 @@ var KimiMediaFilesUploader = class {
36947
37692
  })
36948
37693
  };
36949
37694
  }
36950
- readCache(key) {
36951
- const ref = this.cache.get(key);
36952
- if (ref === void 0) return void 0;
36953
- this.cache.delete(key);
36954
- this.cache.set(key, ref);
36955
- return ref;
36956
- }
36957
- writeCache(key, ref) {
36958
- this.cache.delete(key);
36959
- this.cache.set(key, ref);
36960
- while (this.cache.size > this.maxCacheEntries) {
36961
- const oldest = this.cache.keys().next();
36962
- if (oldest.done === true) break;
36963
- this.cache.delete(oldest.value);
36964
- }
36965
- }
36966
- /** Open one flight and run it; the map entry is dropped as it settles. */
36967
- startFlight(request) {
36968
- const controller = new AbortController();
36969
- const state = { timedOut: false };
36970
- const promise = this.performUpload(controller, state, request).finally(() => {
36971
- if (this.flights.get(request.key)?.promise === promise) this.flights.delete(request.key);
36972
- });
36973
- promise.catch(() => void 0);
36974
- return {
36975
- promise,
36976
- controller,
36977
- waiters: 0,
36978
- state
36979
- };
36980
- }
36981
- /**
36982
- * Wait for one flight, respecting THIS caller's abort.
36983
- *
36984
- * The flight is not bound to the caller's signal: a caller that aborts
36985
- * detaches, and the transport is cancelled only when the last waiter has gone.
36986
- */
36987
- async join(flight, request) {
36988
- const signal = request.signal;
36989
- if (signal?.aborted === true) throw abortError(signal);
36990
- flight.waiters += 1;
36991
- let released = false;
36992
- const release = () => {
36993
- if (released) return;
36994
- released = true;
36995
- flight.waiters -= 1;
36996
- if (flight.waiters <= 0) {
36997
- if (this.flights.get(request.key) === flight) this.flights.delete(request.key);
36998
- flight.controller.abort();
36999
- }
37000
- };
37001
- if (signal === void 0) try {
37002
- return await flight.promise;
37003
- } finally {
37004
- release();
37005
- }
37006
- const callerSignal = signal;
37007
- let detach;
37008
- const cancelled = new Promise((_resolve, reject) => {
37009
- const onAbort = () => reject(abortError(callerSignal));
37010
- detach = () => callerSignal.removeEventListener("abort", onAbort);
37011
- callerSignal.addEventListener("abort", onAbort, { once: true });
37012
- });
37013
- try {
37014
- return await Promise.race([flight.promise, cancelled]);
37015
- } finally {
37016
- detach?.();
37017
- release();
37018
- }
37019
- }
37020
- /** The one request a flight performs. */
37021
37695
  async performUpload(controller, state, request) {
37022
37696
  const form = new FormData();
37023
37697
  form.append("file", new Blob([request.data], { type: request.mediaType }), request.filename);
@@ -37054,7 +37728,6 @@ var KimiMediaFilesUploader = class {
37054
37728
  if (timer !== void 0) clearTimeout(timer);
37055
37729
  }
37056
37730
  }
37057
- /** Turn a transport rejection into the error the caller must see. */
37058
37731
  transportFailure(error, state, request) {
37059
37732
  if (looksAborted(error)) {
37060
37733
  if (state.timedOut) return new KimiMediaUploadError("timeout", "the media upload did not finish within " + request.timeoutMs + " ms");
@@ -37062,7 +37735,6 @@ var KimiMediaFilesUploader = class {
37062
37735
  }
37063
37736
  return new KimiMediaUploadError("network", "the media upload did not complete: " + redact(error instanceof Error ? error.message : String(error), request.accessToken));
37064
37737
  }
37065
- /** Read one response into a reference, or into a typed failure. */
37066
37738
  settle(response, raw, request) {
37067
37739
  const status = response.status;
37068
37740
  if (response.ok !== true) {
@@ -37075,7 +37747,7 @@ var KimiMediaFilesUploader = class {
37075
37747
  status,
37076
37748
  detail
37077
37749
  });
37078
- if (status === 413) throw new KimiMediaUploadError("too-large", "the Kimi Code hosted-file endpoint refused the upload as too large (HTTP 413)", {
37750
+ if (status === 413) throw new KimiMediaUploadError("too-large", "the Kimi Code hosted-file endpoint refused the upload as too large (HTTP " + status + ")", {
37079
37751
  status,
37080
37752
  detail
37081
37753
  });
@@ -37083,7 +37755,7 @@ var KimiMediaFilesUploader = class {
37083
37755
  status,
37084
37756
  detail
37085
37757
  });
37086
- throw new KimiMediaUploadError("rejected", "the Kimi Code hosted-file endpoint refused the upload (HTTP " + status + ")", {
37758
+ throw new KimiMediaUploadError("rejected", "the Kimi Code hosted-file endpoint rejected the upload (HTTP " + status + ")", {
37087
37759
  status,
37088
37760
  detail
37089
37761
  });
@@ -37094,13 +37766,11 @@ var KimiMediaFilesUploader = class {
37094
37766
  detail: redact(raw, request.accessToken)
37095
37767
  });
37096
37768
  const id = readFileId$1(parsed.value);
37097
- if (id === void 0) throw new KimiMediaUploadError("no-id", "the media upload answered HTTP " + status + " without a usable file id", {
37769
+ if (id === void 0) throw new KimiMediaUploadError("no-id", "the media upload answered HTTP " + status + " without a usable id", {
37098
37770
  status,
37099
37771
  detail: redact(raw, request.accessToken)
37100
37772
  });
37101
- const ref = fileReference(id);
37102
- this.writeCache(request.key, ref);
37103
- return ref;
37773
+ return fileReference(id);
37104
37774
  }
37105
37775
  };
37106
37776
  /** One uploader with the default transport and cache size. */
@@ -37108,6 +37778,118 @@ function createMediaFilesUploader(options = {}) {
37108
37778
  return new KimiMediaFilesUploader(options);
37109
37779
  }
37110
37780
  createMediaFilesUploader();
37781
+ function clampShare(value) {
37782
+ if (!Number.isFinite(value)) return 0;
37783
+ return Math.min(1, Math.max(0, value));
37784
+ }
37785
+ /** Tightest measured window; unknown remains null rather than pretending full/empty. */
37786
+ function remainingQuotaShare$1(quota) {
37787
+ if (quota === null || quota === void 0 || quota.windows.length === 0) return null;
37788
+ let tightest = null;
37789
+ for (const window of quota.windows) {
37790
+ const used = typeof window.usedFraction === "number" ? window.usedFraction : typeof window.usedPercent === "number" ? window.usedPercent / 100 : void 0;
37791
+ if (used === void 0 || !Number.isFinite(used)) continue;
37792
+ const remaining = clampShare(1 - used);
37793
+ tightest = tightest === null ? remaining : Math.min(tightest, remaining);
37794
+ }
37795
+ return tightest;
37796
+ }
37797
+ /** Warm and sufficiently-funded accounts first; no preference is an empty list. */
37798
+ function orderAffinityCandidates$1(input) {
37799
+ const floor = input.floorShare ?? .2;
37800
+ const measured = [];
37801
+ const unmeasured = [];
37802
+ for (const candidate of input.candidates) {
37803
+ if (!candidate.warm) continue;
37804
+ const share = input.remainingShareById?.[candidate.id];
37805
+ if (share === null || share === void 0 || !Number.isFinite(share)) {
37806
+ unmeasured.push(candidate.id);
37807
+ continue;
37808
+ }
37809
+ if (share < floor) continue;
37810
+ measured.push({
37811
+ id: candidate.id,
37812
+ share
37813
+ });
37814
+ }
37815
+ measured.sort((left, right) => right.share - left.share);
37816
+ return [...measured.map((entry) => entry.id), ...unmeasured];
37817
+ }
37818
+ //#endregion
37819
+ //#region src/host/kimi-code/cache-affinity.ts
37820
+ /**
37821
+ * The share of its tightest window an account has left, or null when unknown.
37822
+ *
37823
+ * The TIGHTEST window is the only honest answer: a pool row can carry several
37824
+ * at once, and an account with 90% of a weekly window but 2% of its five-hour
37825
+ * one is about to be refused. A window whose consumption was never measured
37826
+ * is skipped rather than read as empty or full, and a snapshot with no measured
37827
+ * window at all answers null — "unknown", which the caller treats as "not
37828
+ * provably short" instead of blocking the account on a number nobody measured.
37829
+ *
37830
+ * @param quota - The account's newest quota snapshot, when one was ever read.
37831
+ * @returns the smallest remaining fraction in `[0, 1]`, or null when unknown.
37832
+ */
37833
+ function remainingQuotaShare(quota) {
37834
+ return remainingQuotaShare$1(quota);
37835
+ }
37836
+ /** Settings-card description for the cache-affinity switch. */
37837
+ const CACHE_AFFINITY_SETTING_DESCRIPTION = "多账号轮换时优先选「这条前缀的缓存还热着」且额度充足的账号;默认用于 Kimi。其他线路需在 promptCacheOverrides 中证明 scope:\"account\" 并显式启用;关闭后回到原轮换策略。";
37838
+ /**
37839
+ * Environment override for the affinity switch.
37840
+ *
37841
+ * The same contract as the wait-time warmup switch beside it in settings: an
37842
+ * explicit value wins over the stored setting, so a deployment can pin the
37843
+ * behavioural change without editing preferences on disk.
37844
+ */
37845
+ const CACHE_AFFINITY_ENV = "DSH_KIMI_CODE_CACHE_AFFINITY";
37846
+ /**
37847
+ * Resolve whether cache-affinity routing is enabled.
37848
+ *
37849
+ * Enabled by default. `0`/`false`/`no`/`off` and `1`/`true`/`yes`/`on` override
37850
+ * the stored setting; otherwise the live `cacheAffinity` setting (default
37851
+ * `true`) governs. Anything unreadable falls back to enabled rather than
37852
+ * throwing inside a request path.
37853
+ *
37854
+ * @param config - Plugin config carrying the live setting.
37855
+ * @param env - Environment to consult; `process.env` in production.
37856
+ * @returns whether a request may state an account preference.
37857
+ */
37858
+ function resolveCacheAffinityEnabled(config, env = process.env) {
37859
+ const raw = env[CACHE_AFFINITY_ENV]?.trim().toLowerCase();
37860
+ if (raw === "0" || raw === "false" || raw === "no" || raw === "off") return false;
37861
+ if (raw === "1" || raw === "true" || raw === "yes" || raw === "on") return true;
37862
+ const field = config?.cacheAffinity;
37863
+ if (field === void 0 || field === null) return true;
37864
+ if (typeof field === "object" && typeof field.get === "function") try {
37865
+ return field.get() !== false;
37866
+ } catch {
37867
+ return true;
37868
+ }
37869
+ return field !== false;
37870
+ }
37871
+ /**
37872
+ * Order the accounts this request would rather use, warmest and safest first.
37873
+ *
37874
+ * Only accounts whose prefix is warm AND whose remaining share is either
37875
+ * unknown or at least the floor are returned: a warm account that is nearly
37876
+ * spent is deliberately absent, because the pool then falls back to its own
37877
+ * rotation instead of spending the last of a window on a discount. An empty
37878
+ * result means "no preference" — the caller passes no hint at all and the
37879
+ * strategy behaves exactly as it would have without this module.
37880
+ *
37881
+ * Order: a warm account with a known-and-sufficient share first (largest
37882
+ * remaining first, so two warm accounts spread the load instead of always
37883
+ * spending one), then a warm account whose share nobody measured. Unknown last
37884
+ * because a measured account is a better bet than an unmeasured one, but still
37885
+ * listed: an unmeasured warm account is not evidence of anything bad.
37886
+ *
37887
+ * @param input - candidates plus the quota evidence behind them.
37888
+ * @returns account ids in preference order; empty when nothing is preferred.
37889
+ */
37890
+ function orderAffinityCandidates(input) {
37891
+ return orderAffinityCandidates$1(input);
37892
+ }
37111
37893
  //#endregion
37112
37894
  //#region src/host/kimi-code/adapter.ts
37113
37895
  /**
@@ -37592,9 +38374,13 @@ const BILLING_CYCLE_PATTERN = /usage limit for this billing cycle/i;
37592
38374
  function accountLimitCooldownMs(bodyText, now = Date.now()) {
37593
38375
  const field = bodyText.match(RESET_FIELD_PATTERN);
37594
38376
  const instant = bodyText.match(RESET_INSTANT_PATTERN);
37595
- const resetsAt = field !== null ? parseTimestamp(Number(field[1])) : instant !== null ? Date.parse(instant[0].replace(" ", "T")) : null;
37596
- if (resetsAt !== null && Number.isFinite(resetsAt) && resetsAt > now) return Math.min(Math.max(resetsAt - now, POOL_COOLDOWN_MS$5), MAX_ACCOUNT_LIMIT_COOLDOWN_MS);
37597
- return BILLING_CYCLE_PATTERN.test(bodyText) ? BILLING_CYCLE_COOLDOWN_MS : ACCOUNT_LIMIT_COOLDOWN_MS;
38377
+ return cooldownFromReset({
38378
+ resetAtMs: field !== null ? parseTimestamp(Number(field[1])) : instant !== null ? Date.parse(instant[0].replace(" ", "T")) : null,
38379
+ now,
38380
+ fallbackMs: BILLING_CYCLE_PATTERN.test(bodyText) ? BILLING_CYCLE_COOLDOWN_MS : ACCOUNT_LIMIT_COOLDOWN_MS,
38381
+ minimumMs: POOL_COOLDOWN_MS$5,
38382
+ maximumMs: MAX_ACCOUNT_LIMIT_COOLDOWN_MS
38383
+ });
37598
38384
  }
37599
38385
  /**
37600
38386
  * Cooldown one rate-limited account takes when the provider states no delay.
@@ -37673,122 +38459,30 @@ function toolUpdateMode$2(modelId, catalog) {
37673
38459
  ].includes(raw)) return void 0;
37674
38460
  return "in-history";
37675
38461
  }
37676
- /**
37677
- * How long a cold-start follower waits for the leader warming the same prefix.
37678
- *
37679
- * The leader only has to reach the response headers: past that the service is
37680
- * already processing the prefix, and a follower that waited out the whole
37681
- * budget would have lost more than the shared warm-up was worth.
37682
- */
37683
- const COLD_START_WARMUP_WAIT_MS = 2500;
37684
- /** How many prefix groups are remembered as warm. */
37685
- const MAX_WARM_PREFIX_GROUPS = 256;
37686
- /**
37687
- * When each prefix group was last warmed.
37688
- *
37689
- * An entry older than the cache lifetime is treated as cold, because the
37690
- * service's own entry is gone by then too. Bounded, so a long-lived process
37691
- * cannot accumulate one entry per distinct prefix forever.
37692
- */
37693
- const warmPrefixTimestamps = /* @__PURE__ */ new Map();
37694
- /**
37695
- * The request currently warming each prefix group.
37696
- *
37697
- * The promise is resolved when that leader reaches the response headers, which
37698
- * is what releases the followers waiting behind it.
37699
- */
37700
- const inFlightColdLeaders = /* @__PURE__ */ new Map();
37701
- /** When each leader started, so a hung leader does not own its group forever. */
37702
- const inFlightColdLeaderStartedAt = /* @__PURE__ */ new Map();
37703
- /**
37704
- * The resolver belonging to each in-flight leader.
37705
- *
37706
- * Kept beside the promise it settles, because a promise cannot be resolved
37707
- * from the outside.
37708
- */
37709
- const coldLeaderReleases = /* @__PURE__ */ new Map();
37710
- /**
37711
- * In-flight main-session requests waiting for their HTTP response headers,
37712
- * keyed by sessionId. Used so a concurrent `session-title-first-prompt-llm`
37713
- * side-channel request yields to the main turn's prefill instead of competing
37714
- * on the same account during cold start.
37715
- */
37716
- const inFlightMainSessionHeaders = /* @__PURE__ */ new Map();
37717
- const mainSessionHeaderReleases = /* @__PURE__ */ new Map();
37718
- /**
37719
- * Per-account/model first-turn prefill pacing chain for concurrent subagent bursts.
37720
- */
37721
- const inFlightSubagentBurstLeaders = /* @__PURE__ */ new Map();
37722
- const subagentBurstReleases = /* @__PURE__ */ new Map();
37723
- /** The gate of a request that neither waits for nor leads a cold start. */
37724
- const NO_COLD_START_GATE = {
37725
- isLeader: false,
37726
- release: () => void 0,
37727
- markWarm: () => void 0
37728
- };
37729
- /**
37730
- * The group of requests that address the same cached prefix.
37731
- *
37732
- * The service derives its cache from the request content, so two requests with
37733
- * the same model, the same system prompt, the same tool list and the same
37734
- * leading static bootstrap reminders hit the same entry. The region is part of
37735
- * the key because each region is a separate service with a cache of its own.
37736
- *
37737
- * A request with no system prompt has no prefix worth warming, and gets no
37738
- * group at all.
37739
- *
37740
- * @param region - The region the request will be answered from.
37741
- * @param options - The request about to be sent.
37742
- * @returns The group key, or undefined when the request has no prefix.
37743
- */
37744
- function coldStartGroupKey(region, options) {
38462
+ const prefixRequests = new PrefixRequestCoordinator({ waitMs: 2500 });
38463
+ /** The prefix identity shared by requests on one region/model. */
38464
+ function prefixGroupKey(region, options) {
37745
38465
  const { systemPromptHash, toolsHash, staticPrefixHash } = prefixFingerprints(options);
37746
38466
  if (systemPromptHash === null) return void 0;
37747
38467
  return `${region}:${options.model}:${systemPromptHash}:${toolsHash ?? "none"}${staticPrefixHash ? ":" + staticPrefixHash : ""}`;
37748
38468
  }
37749
- /**
37750
- * Remember that a prefix group is warm.
37751
- *
37752
- * @param groupKey - The group that was just warmed.
37753
- */
37754
- function rememberWarmPrefix(groupKey, now = Date.now()) {
37755
- warmPrefixTimestamps.delete(groupKey);
37756
- warmPrefixTimestamps.set(groupKey, now);
37757
- while (warmPrefixTimestamps.size > MAX_WARM_PREFIX_GROUPS) {
37758
- const oldest = warmPrefixTimestamps.keys().next();
37759
- if (oldest.done === true) break;
37760
- warmPrefixTimestamps.delete(oldest.value);
37761
- }
38469
+ const GROUP_ACCOUNT_SEPARATOR = "|";
38470
+ /** The cache group on one account; cross-account warmth is never inferred. */
38471
+ function coldStartGroupKey(region, options, accountScope) {
38472
+ const prefix = prefixGroupKey(region, options);
38473
+ return prefix === void 0 ? void 0 : `${prefix}${GROUP_ACCOUNT_SEPARATOR}${accountScope}`;
37762
38474
  }
37763
- /**
37764
- * Register an in-flight main-session request until its HTTP response headers arrive.
37765
- */
38475
+ /** Whether this account's prefix is still warm under the measured Kimi TTL. */
38476
+ function isWarmPrefixGroup(region, options, accountScope, now = Date.now()) {
38477
+ const groupKey = coldStartGroupKey(region, options, accountScope);
38478
+ return groupKey !== void 0 && prefixRequests.isWarm(groupKey, 15e5, now);
38479
+ }
38480
+ /** Register a main request until response headers arrive. */
37766
38481
  function registerInFlightMainSessionHeader(sessionId) {
37767
- const key = sessionId.trim();
37768
- if (key === "") return () => void 0;
37769
- mainSessionHeaderReleases.get(key)?.();
37770
- let resolveHeader;
37771
- const promise = new Promise((resolve) => {
37772
- resolveHeader = resolve;
37773
- });
37774
- inFlightMainSessionHeaders.set(key, promise);
37775
- let released = false;
37776
- const release = () => {
37777
- if (released) return;
37778
- released = true;
37779
- resolveHeader();
37780
- if (mainSessionHeaderReleases.get(key) === release) {
37781
- mainSessionHeaderReleases.delete(key);
37782
- inFlightMainSessionHeaders.delete(key);
37783
- }
37784
- };
37785
- mainSessionHeaderReleases.set(key, release);
37786
- return release;
38482
+ const lease = prefixRequests.registerMainSession(sessionId);
38483
+ return () => lease.release();
37787
38484
  }
37788
- /**
37789
- * Whether a first-turn request carries a subagent mandate or large uncached
37790
- * dynamic payload that should stagger behind an already in-flight sibling burst.
37791
- */
38485
+ /** Whether a first-turn request carries a mandate or a large dynamic payload. */
37792
38486
  function isSubagentBurstCandidate(options) {
37793
38487
  let dynamicChars = 0;
37794
38488
  for (const message of options.messages) {
@@ -37804,113 +38498,14 @@ function isSubagentBurstCandidate(options) {
37804
38498
  }
37805
38499
  return dynamicChars >= 1200;
37806
38500
  }
37807
- /**
37808
- * Stagger concurrent subagent first-turn requests on the same account/model
37809
- * so they do not stampede prefill in the same millisecond.
37810
- */
38501
+ /** Serialize concurrent subagent first turns on one account/model. */
37811
38502
  async function acquireSubagentBurstPacing(pacingKey, signal) {
37812
- while (true) {
37813
- const existing = inFlightSubagentBurstLeaders.get(pacingKey);
37814
- if (existing === void 0) break;
37815
- await raceColdStartLeader(existing, signal);
37816
- if (signal.aborted) return () => void 0;
37817
- if (inFlightSubagentBurstLeaders.get(pacingKey) === existing) {
37818
- subagentBurstReleases.get(pacingKey)?.();
37819
- break;
37820
- }
37821
- }
37822
- let resolvePacing;
37823
- const promise = new Promise((resolve) => {
37824
- resolvePacing = resolve;
37825
- });
37826
- inFlightSubagentBurstLeaders.set(pacingKey, promise);
37827
- let settled = false;
37828
- const release = () => {
37829
- if (settled) return;
37830
- settled = true;
37831
- resolvePacing();
37832
- if (subagentBurstReleases.get(pacingKey) === release) {
37833
- subagentBurstReleases.delete(pacingKey);
37834
- inFlightSubagentBurstLeaders.delete(pacingKey);
37835
- }
37836
- };
37837
- subagentBurstReleases.set(pacingKey, release);
37838
- return release;
37839
- }
37840
- /**
37841
- * Wait for the leader of a prefix group, for the wait budget, or for an abort.
37842
- *
37843
- * The timer and the abort listener are removed whichever way the race ends, so
37844
- * a follower that proceeds leaves neither behind.
37845
- *
37846
- * @param leader - The promise the in-flight leader settles.
37847
- * @param signal - The caller's abort signal; an abort ends the wait.
37848
- */
37849
- function raceColdStartLeader(leader, signal) {
37850
- const cleanups = [];
37851
- const budget = new Promise((resolve) => {
37852
- const timer = setTimeout(() => resolve(), COLD_START_WARMUP_WAIT_MS);
37853
- cleanups.push(() => clearTimeout(timer));
37854
- });
37855
- const aborted = new Promise((resolve) => {
37856
- if (signal.aborted) {
37857
- resolve();
37858
- return;
37859
- }
37860
- const onAbort = () => resolve();
37861
- signal.addEventListener("abort", onAbort, { once: true });
37862
- cleanups.push(() => signal.removeEventListener("abort", onAbort));
37863
- });
37864
- return Promise.race([
37865
- leader,
37866
- budget,
37867
- aborted
37868
- ]).finally(() => {
37869
- for (const cleanup of cleanups) cleanup();
37870
- });
38503
+ const lease = await prefixRequests.acquireBurst(pacingKey, signal);
38504
+ return () => lease.release();
37871
38505
  }
37872
- /**
37873
- * Join the cold-start singleflight for one prefix group.
37874
- *
37875
- * A warm group returns at once. A cold group that already has a leader waits
37876
- * for that leader — but never longer than {@link COLD_START_WARMUP_WAIT_MS}.
37877
- * A cold group with no leader becomes the leader itself.
37878
- *
37879
- * @param groupKey - The prefix group this request belongs to.
37880
- * @param signal - The caller's abort signal; an abort ends the wait.
37881
- * @returns The gate this request holds.
37882
- */
38506
+ /** Join the shared cold-start coordinator with Kimi's measured warm TTL. */
37883
38507
  async function acquireColdStartWarmup(groupKey, signal) {
37884
- const warmedAt = warmPrefixTimestamps.get(groupKey);
37885
- if (warmedAt !== void 0 && Date.now() - warmedAt < 15e5) return NO_COLD_START_GATE;
37886
- if (inFlightColdLeaders.get(groupKey) !== void 0) {
37887
- const startedAt = inFlightColdLeaderStartedAt.get(groupKey);
37888
- if (startedAt !== void 0 && Date.now() - startedAt >= 2500) coldLeaderReleases.get(groupKey)?.();
37889
- }
37890
- const leader = inFlightColdLeaders.get(groupKey);
37891
- if (leader !== void 0) {
37892
- await raceColdStartLeader(leader, signal);
37893
- return NO_COLD_START_GATE;
37894
- }
37895
- let resolveLeader;
37896
- const promise = new Promise((resolve) => {
37897
- resolveLeader = resolve;
37898
- });
37899
- inFlightColdLeaders.set(groupKey, promise);
37900
- inFlightColdLeaderStartedAt.set(groupKey, Date.now());
37901
- const release = () => {
37902
- resolveLeader();
37903
- if (coldLeaderReleases.get(groupKey) !== release) return;
37904
- coldLeaderReleases.delete(groupKey);
37905
- inFlightColdLeaders.delete(groupKey);
37906
- inFlightColdLeaderStartedAt.delete(groupKey);
37907
- };
37908
- coldLeaderReleases.set(groupKey, release);
37909
- return {
37910
- isLeader: true,
37911
- release,
37912
- markWarm: () => rememberWarmPrefix(groupKey)
37913
- };
38508
+ return prefixRequests.acquire(groupKey, signal, CACHE_TTL_MS);
37914
38509
  }
37915
38510
  /** Sessions already told that their prefix broke, so the log stays readable. */
37916
38511
  const driftWarned = /* @__PURE__ */ new Set();
@@ -37944,6 +38539,13 @@ function noteSessionCacheState(sessionId, state) {
37944
38539
  const liveDropped = imageBudget?.droppedImages ?? 0;
37945
38540
  const lastRequestImageBytes = liveBytes > 0 ? liveBytes : priorRecord?.lastRequestImageBytes;
37946
38541
  const lastDroppedImages = liveDropped > 0 ? liveDropped : priorRecord?.lastDroppedImages;
38542
+ const firstRequest = stats.requests === 1 ? {
38543
+ firstRequestCachedTokens: state.cacheReadTokens,
38544
+ firstRequestPromptTokens: state.cacheReadTokens + state.inputTokens
38545
+ } : {
38546
+ ...priorRecord?.firstRequestCachedTokens === void 0 ? {} : { firstRequestCachedTokens: priorRecord.firstRequestCachedTokens },
38547
+ ...priorRecord?.firstRequestPromptTokens === void 0 ? {} : { firstRequestPromptTokens: priorRecord.firstRequestPromptTokens }
38548
+ };
37947
38549
  sessionCacheState.write(sessionId, {
37948
38550
  lastActiveAt: now,
37949
38551
  totalTokens: state.cacheReadTokens + state.inputTokens,
@@ -37958,10 +38560,12 @@ function noteSessionCacheState(sessionId, state) {
37958
38560
  ...budgetDropped === void 0 ? {} : { budgetDropped },
37959
38561
  ...lastRequestImageBytes === void 0 ? {} : { lastRequestImageBytes },
37960
38562
  ...lastDroppedImages === void 0 ? {} : { lastDroppedImages },
38563
+ ...firstRequest,
37961
38564
  updatedAt: now
37962
38565
  });
37963
38566
  const warmupState = recordCacheWarmupUsage({
37964
38567
  sessionId,
38568
+ provider: PROVIDER_ID$9,
37965
38569
  inputTokens: state.inputTokens,
37966
38570
  cacheReadTokens: state.cacheReadTokens,
37967
38571
  outputTokens: state.outputTokens,
@@ -38078,6 +38682,70 @@ var KimiCodeAdapter = class extends LlmAdapter {
38078
38682
  settings() {
38079
38683
  return this.preferences ? Promise.resolve(this.preferences.status()) : this.modelSettings.read();
38080
38684
  }
38685
+ /** Whether this deployment routes by warm prefix cache; on unless switched off. */
38686
+ cacheAffinityEnabled() {
38687
+ try {
38688
+ return this.options.cacheAffinity?.() !== false;
38689
+ } catch {
38690
+ return true;
38691
+ }
38692
+ }
38693
+ /**
38694
+ * Which accounts this request would rather be served by, when the prefix cache
38695
+ * can say something useful about it.
38696
+ *
38697
+ * The service keeps one prefix cache per account, so rotation — which knows
38698
+ * cooldowns, tokens and last use, never prompt content — can spend a request on
38699
+ * an account that has never seen this prompt while another one has it hot. Both
38700
+ * facts needed to do better are already in this process: the cold-start gate
38701
+ * records which (prefix, account) pairs are warm, and the quota cache says how
38702
+ * much each account has left.
38703
+ *
38704
+ * Returns undefined — no preference at all — wherever a preference would be a
38705
+ * guess: the switch is off, there is no pool or only one account, the pool
38706
+ * document cannot be read, a region cannot be resolved, or no account has this
38707
+ * prefix warm. The pool then applies its rotation strategy exactly as it would
38708
+ * have without this method.
38709
+ *
38710
+ * @param pool - The account pool serving this route, or null when none is installed.
38711
+ * @param options - The request about to be sent; its system prompt, tools and
38712
+ * static bootstrap are what decide the prefix identity.
38713
+ * @returns a preference for the pool, or undefined for "strategy decides".
38714
+ */
38715
+ async cacheAffinityHint(pool, options) {
38716
+ if (pool === null || !this.cacheAffinityEnabled()) return void 0;
38717
+ let scopes;
38718
+ try {
38719
+ scopes = await pool.accountScopes();
38720
+ } catch {
38721
+ return;
38722
+ }
38723
+ if (scopes.length < 2) return void 0;
38724
+ let fallbackRegion = null;
38725
+ if (scopes.some((scope) => scope.region === null)) try {
38726
+ fallbackRegion = await resolveRegion();
38727
+ } catch {
38728
+ fallbackRegion = null;
38729
+ }
38730
+ const candidates = [];
38731
+ for (const scope of scopes) {
38732
+ const region = scope.region ?? fallbackRegion;
38733
+ if (region === null) continue;
38734
+ if (!isWarmPrefixGroup(region, options, scope.id)) continue;
38735
+ candidates.push({
38736
+ id: scope.id,
38737
+ warm: true
38738
+ });
38739
+ }
38740
+ if (candidates.length === 0) return void 0;
38741
+ const remainingShareById = {};
38742
+ for (const candidate of candidates) remainingShareById[candidate.id] = remainingQuotaShare(cachedQuotaFor$1(candidate.id));
38743
+ const preferAccountIds = orderAffinityCandidates({
38744
+ candidates,
38745
+ remainingShareById
38746
+ });
38747
+ return preferAccountIds.length === 0 ? void 0 : { preferAccountIds };
38748
+ }
38081
38749
  /**
38082
38750
  * Catalog for the picker: the live managed listing when reachable, the
38083
38751
  * shipped fallback otherwise, narrowed by the user's enabled selection.
@@ -38182,7 +38850,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
38182
38850
  async *requestStream(options, signal, activity) {
38183
38851
  const fetchFn = this.options.fetchFn ?? fetch;
38184
38852
  const normalized = normalizeGenerateOptions(options);
38185
- const sideChannel = isSideChannelTitleRequest(normalized);
38853
+ const sideChannel = isAuxiliaryRequest(normalized) || isSideChannelTitleRequest(normalized);
38186
38854
  const sessionKey = typeof options.sessionId === "string" ? options.sessionId.trim() : "";
38187
38855
  const mainHeaderRelease = !sideChannel && sessionKey !== "" ? registerInFlightMainSessionHeader(sessionKey) : () => void 0;
38188
38856
  try {
@@ -38299,6 +38967,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
38299
38967
  let sentBuild;
38300
38968
  const pool = this.accountPool;
38301
38969
  const tried = /* @__PURE__ */ new Set();
38970
+ const affinity = sideChannel ? void 0 : await this.cacheAffinityHint(pool, requestOptions);
38302
38971
  let servedByAccountId;
38303
38972
  let response;
38304
38973
  while (true) {
@@ -38316,7 +38985,7 @@ var KimiCodeAdapter = class extends LlmAdapter {
38316
38985
  else {
38317
38986
  let effective;
38318
38987
  try {
38319
- effective = await pool.getEffectiveCredential(tried, fetchFn, { maxAttempts: MODEL_REFRESH_MAX_ATTEMPTS });
38988
+ effective = await pool.getEffectiveCredential(tried, fetchFn, { maxAttempts: MODEL_REFRESH_MAX_ATTEMPTS }, affinity);
38320
38989
  } catch (error) {
38321
38990
  throw credentialFailure(error, signal);
38322
38991
  }
@@ -38374,13 +39043,10 @@ var KimiCodeAdapter = class extends LlmAdapter {
38374
39043
  }
38375
39044
  };
38376
39045
  if (sideChannel && sessionKey !== "") {
38377
- const pendingMain = inFlightMainSessionHeaders.get(sessionKey);
38378
- if (pendingMain !== void 0) {
38379
- await raceColdStartLeader(pendingMain, signal);
38380
- if (signal.aborted) throw new LlmError("Kimi Code request aborted", "ABORTED");
38381
- }
39046
+ await prefixRequests.waitForMainSession(sessionKey, signal);
39047
+ if (signal.aborted) throw new LlmError("Kimi Code request aborted", "ABORTED");
38382
39048
  }
38383
- const coldGroupKey = sideChannel ? void 0 : coldStartGroupKey(region, requestOptions);
39049
+ const coldGroupKey = sideChannel ? void 0 : coldStartGroupKey(region, requestOptions, accountScope);
38384
39050
  const coldGate = coldGroupKey === void 0 ? void 0 : await acquireColdStartWarmup(coldGroupKey, signal);
38385
39051
  const burstPacingRelease = !sideChannel && sessionKey !== "" && getCacheStats(sessionKey).requests === 0 && isSubagentBurstCandidate(requestOptions) ? await acquireSubagentBurstPacing(`${accountScope}:${options.model}`, signal) : () => void 0;
38386
39052
  let coldGateSettled = false;
@@ -38857,9 +39523,28 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
38857
39523
  *
38858
39524
  * The returned credential carries its own region, OAuth host and base URL, so
38859
39525
  * the caller must use those rather than the process-wide defaults.
39526
+ *
39527
+ * @param selection - this request's own preference, used by the cache-affinity
39528
+ * routing to put an account whose prefix cache is still warm first. Absent
39529
+ * means the rotation strategy decides alone.
39530
+ */
39531
+ async getEffectiveCredential(excludeIds, fetchFn = fetch, options, selection) {
39532
+ return this.getEffectiveAccount(excludeIds, fetchFn, false, options, selection);
39533
+ }
39534
+ /**
39535
+ * Every pooled account's id with the region that decides its prefix group.
39536
+ *
39537
+ * The cache-affinity routing needs exactly two facts per account — which one
39538
+ * it is, and which regional service answers it, because each region keeps its
39539
+ * own prefix cache — and nothing else. Deliberately a narrow projection over
39540
+ * the pool document rather than a credential read: tokens never leave the
39541
+ * pool, and nothing here can be mistaken for a credential accessor.
38860
39542
  */
38861
- async getEffectiveCredential(excludeIds, fetchFn = fetch, options) {
38862
- return this.getEffectiveAccount(excludeIds, fetchFn, false, options);
39543
+ async accountScopes() {
39544
+ return (await this.read()).accounts.map((account) => ({
39545
+ id: account.id,
39546
+ region: account.region ?? account.credentials.region ?? null
39547
+ }));
38863
39548
  }
38864
39549
  /** The pre-pool credential file this pool mirrors its primary account into. */
38865
39550
  mirrorStore() {
@@ -39051,15 +39736,37 @@ function warmupStatusOf(sessionId) {
39051
39736
  requiredHits: CACHE_WARMUP_DEFAULTS.consecutiveHitsRequired
39052
39737
  };
39053
39738
  }
39739
+ /**
39740
+ * How much of this conversation's first prompt was served from cache.
39741
+ *
39742
+ * Read from the durable record on BOTH paths, because the in-memory totals carry
39743
+ * rolling sums only: the first request's own numbers exist exactly once, at the
39744
+ * turn that closed it, and the record is where the adapter filed them. A record
39745
+ * written before this field existed simply has nothing to say, which is the
39746
+ * honest answer for a conversation that started under an older build.
39747
+ */
39748
+ function openingInheritanceOf(sessionId) {
39749
+ const durable = sessionCacheState.read(sessionId);
39750
+ const cachedTokens = durable?.firstRequestCachedTokens;
39751
+ const promptTokens = durable?.firstRequestPromptTokens;
39752
+ if (cachedTokens === void 0 || promptTokens === void 0 || promptTokens <= 0) return void 0;
39753
+ return {
39754
+ cachedTokens,
39755
+ promptTokens,
39756
+ hitRatio: Math.min(1, Math.max(0, cachedTokens / promptTokens))
39757
+ };
39758
+ }
39054
39759
  function cacheStatsOrNull(sessionId) {
39055
39760
  const stats = getCacheStats(sessionId);
39056
39761
  if (stats.requests === 0) return durableCacheStats(sessionId);
39057
39762
  const lastDrift = getLastDriftCause(sessionId);
39058
39763
  const warmup = warmupStatusOf(sessionId);
39764
+ const opening = openingInheritanceOf(sessionId);
39059
39765
  return {
39060
39766
  ...stats,
39061
39767
  ...lastDrift === void 0 ? {} : { lastDrift },
39062
39768
  ...warmup === void 0 ? {} : { warmup },
39769
+ ...opening === void 0 ? {} : { opening },
39063
39770
  imageBudget: imageBudgetOf(sessionId),
39064
39771
  cacheWriteTokens: stats.cacheWriteTokens,
39065
39772
  cacheWrite5mTokens: stats.cacheWrite5mTokens,
@@ -39103,6 +39810,7 @@ function durableCacheStats(sessionId) {
39103
39810
  if (durable === void 0 || durable.requests === 0) return null;
39104
39811
  const lastDrift = driftCauseOf(durable.lastDrift);
39105
39812
  const warmup = warmupStatusOf(sessionId);
39813
+ const opening = openingInheritanceOf(sessionId);
39106
39814
  return {
39107
39815
  requests: durable.requests,
39108
39816
  cachedTokens: durable.cachedTokens,
@@ -39114,6 +39822,7 @@ function durableCacheStats(sessionId) {
39114
39822
  cacheWrite1hTokens: durable.cacheWrite1hTokens,
39115
39823
  ...lastDrift === void 0 ? {} : { lastDrift },
39116
39824
  ...warmup === void 0 ? {} : { warmup },
39825
+ ...opening === void 0 ? {} : { opening },
39117
39826
  imageBudget: imageBudgetOf(sessionId)
39118
39827
  };
39119
39828
  }
@@ -40391,7 +41100,7 @@ const MINIMAX_CODE_PROVIDER$1 = "minimax-code";
40391
41100
  * @param value - 任意值。
40392
41101
  * @returns 是普通对象时为 true。
40393
41102
  */
40394
- function isPlainRecord$1(value) {
41103
+ function isPlainRecord(value) {
40395
41104
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
40396
41105
  const prototype = Object.getPrototypeOf(value);
40397
41106
  return prototype === Object.prototype || prototype === null;
@@ -40511,7 +41220,7 @@ function coerceBySchema(value, schema, fixes) {
40511
41220
  const type = schema["type"];
40512
41221
  if (type === "array") {
40513
41222
  let current = value;
40514
- if (isPlainRecord$1(current) && Object.keys(current).length === 1 && "item" in current) {
41223
+ if (isPlainRecord(current) && Object.keys(current).length === 1 && "item" in current) {
40515
41224
  const inner = current["item"];
40516
41225
  current = Array.isArray(inner) ? [...inner] : [inner];
40517
41226
  fixes.push("item_wrapper");
@@ -40543,7 +41252,7 @@ function coerceBySchema(value, schema, fixes) {
40543
41252
  }
40544
41253
  if (type === "object") {
40545
41254
  const properties = schema["properties"];
40546
- if (!isPlainRecord$1(value) || !isRecord$50(properties)) return value;
41255
+ if (!isPlainRecord(value) || !isRecord$50(properties)) return value;
40547
41256
  const next = { ...value };
40548
41257
  for (const [key, subSchema] of Object.entries(properties)) {
40549
41258
  if (!(key in next)) continue;
@@ -40552,7 +41261,7 @@ function coerceBySchema(value, schema, fixes) {
40552
41261
  return next;
40553
41262
  }
40554
41263
  const properties = schema["properties"];
40555
- if (isPlainRecord$1(value) && isRecord$50(properties)) {
41264
+ if (isPlainRecord(value) && isRecord$50(properties)) {
40556
41265
  const next = { ...value };
40557
41266
  for (const [key, subSchema] of Object.entries(properties)) {
40558
41267
  if (!(key in next)) continue;
@@ -40612,7 +41321,7 @@ function coerceByKnownFields(args, fixes) {
40612
41321
  * @returns 修复后的参数与修复项列表。
40613
41322
  */
40614
41323
  function repairMinimaxToolArguments(toolName, args, schema) {
40615
- if (!isPlainRecord$1(args)) return {
41324
+ if (!isPlainRecord(args)) return {
40616
41325
  value: args,
40617
41326
  fixes: []
40618
41327
  };
@@ -40627,7 +41336,7 @@ function repairMinimaxToolArguments(toolName, args, schema) {
40627
41336
  }
40628
41337
  if (schema !== void 0) {
40629
41338
  const coerced = coerceBySchema(working, schema, fixes);
40630
- if (isPlainRecord$1(coerced)) Object.assign(working, coerced);
41339
+ if (isPlainRecord(coerced)) Object.assign(working, coerced);
40631
41340
  } else coerceByKnownFields(working, fixes);
40632
41341
  return {
40633
41342
  value: working,
@@ -40773,28 +41482,6 @@ function errorFamilyOf(text) {
40773
41482
  return (text.split(/\r?\n/, 1)[0] ?? "").replace(/\d+/g, "#").slice(0, 200);
40774
41483
  }
40775
41484
  /**
40776
- * 把参数规范化成可比较的字符串(键排序)。
40777
- * @param value - 参数值。
40778
- * @returns 规范化后的 JSON 文本。
40779
- */
40780
- function canonicalize(value) {
40781
- return JSON.stringify(sortJsonValue$1(value));
40782
- }
40783
- /**
40784
- * 深拷贝一份键排序后的 JSON 值。
40785
- * @param value - 任意值。
40786
- * @returns 排序后的副本。
40787
- */
40788
- function sortJsonValue$1(value) {
40789
- if (Array.isArray(value)) return value.map(sortJsonValue$1);
40790
- if (isPlainRecord$1(value)) {
40791
- const sorted = {};
40792
- for (const key of Object.keys(value).sort()) sorted[key] = sortJsonValue$1(value[key]);
40793
- return sorted;
40794
- }
40795
- return value;
40796
- }
40797
- /**
40798
41485
  * 造一台工具级失控守卫。
40799
41486
  *
40800
41487
  * 三种可提醒信号(顺序即官方提醒优先级):
@@ -40813,8 +41500,8 @@ function sortJsonValue$1(value) {
40813
41500
  */
40814
41501
  function createMinimaxRunawayGuard(options = {}) {
40815
41502
  const threshold = options.remindAfterOccurrences ?? 3;
40816
- /** 同一次动作的连击。 */
40817
- let actionStreak;
41503
+ /** 同一次动作的连击,由所有线路共享的规范键与跟踪器计算。 */
41504
+ const exactActionRepeats = createExactRepeatTracker();
40818
41505
  /** 同一族错误的连击(键是工具名 + 错误族)。 */
40819
41506
  const errorStreaks = /* @__PURE__ */ new Map();
40820
41507
  /** 轮询连击(键是工具名)。 */
@@ -40847,23 +41534,16 @@ function createMinimaxRunawayGuard(options = {}) {
40847
41534
  */
40848
41535
  const isPolling = (observation) => {
40849
41536
  if (!POLLING_TOOL_NAMES.includes(observation.toolName)) return false;
40850
- return (isPlainRecord$1(observation.arguments) ? observation.arguments : void 0)?.["wait"] !== true;
41537
+ return (isPlainRecord(observation.arguments) ? observation.arguments : void 0)?.["wait"] !== true;
40851
41538
  };
40852
41539
  return {
40853
41540
  observe(observation) {
40854
41541
  const toolName = observation.toolName;
40855
- const actionKey = `${toolName}\u0000${canonicalize(observation.arguments)}`;
41542
+ const actionKey = canonicalToolCallKey(toolName, observation.arguments);
40856
41543
  const countsAsAction = !POLLING_TOOL_NAMES.includes(toolName);
40857
41544
  if (countsAsAction) {
40858
- if (actionStreak?.key === actionKey) actionStreak = {
40859
- key: actionKey,
40860
- count: actionStreak.count + 1
40861
- };
40862
- else actionStreak = {
40863
- key: actionKey,
40864
- count: 1
40865
- };
40866
- record("exact_action_repeat", actionStreak.count, toolName);
41545
+ const action = exactActionRepeats.observe(toolName, observation.arguments);
41546
+ record("exact_action_repeat", action.count, toolName);
40867
41547
  }
40868
41548
  if (countsAsAction) {
40869
41549
  recentActions.push(actionKey);
@@ -40890,7 +41570,7 @@ function createMinimaxRunawayGuard(options = {}) {
40890
41570
  record("same_error_family", count, toolName);
40891
41571
  } else for (const [key, entry] of [...errorStreaks]) if (entry.toolName === toolName) errorStreaks.delete(key);
40892
41572
  if (isPolling(observation)) {
40893
- const key = `${toolName}\u0000${canonicalize(observation.arguments)}\u0000${observation.resultText ?? ""}`;
41573
+ const key = `${canonicalToolCallKey(toolName, observation.arguments)}\u0000${observation.resultText ?? ""}`;
40894
41574
  const previous = pollingStreaks.get(toolName);
40895
41575
  const count = previous !== void 0 && previous.key === key ? previous.count + 1 : 1;
40896
41576
  pollingStreaks.set(toolName, {
@@ -40909,6 +41589,7 @@ function createMinimaxRunawayGuard(options = {}) {
40909
41589
  text: renderSameErrorFamilyReminder(errorCandidate.count)
40910
41590
  };
40911
41591
  }
41592
+ const actionStreak = exactActionRepeats.current();
40912
41593
  if (countsAsAction && actionStreak !== void 0 && actionStreak.count >= threshold) {
40913
41594
  reminderAttempted = true;
40914
41595
  return {
@@ -40928,7 +41609,7 @@ function createMinimaxRunawayGuard(options = {}) {
40928
41609
  }
40929
41610
  },
40930
41611
  reset() {
40931
- actionStreak = void 0;
41612
+ exactActionRepeats.reset();
40932
41613
  errorStreaks.clear();
40933
41614
  pollingStreaks.clear();
40934
41615
  recentActions.length = 0;
@@ -41198,14 +41879,14 @@ function schemaOf(tools, name, agent) {
41198
41879
  function repairExecutionArguments(execution, tools) {
41199
41880
  const record = isRecord$50(execution) ? execution : void 0;
41200
41881
  const args = record?.["arguments"];
41201
- if (!isPlainRecord$1(args)) return;
41882
+ if (!isPlainRecord(args)) return;
41202
41883
  const name = stringOf$4(record?.["name"]);
41203
41884
  if (name === void 0) return;
41204
41885
  const agent = record?.["agent"];
41205
41886
  const repaired = repairMinimaxToolArguments(name, args, schemaOf(tools, name, agent));
41206
41887
  if (repaired.fixes.length === 0) return;
41207
41888
  const target = repaired.value;
41208
- if (!isPlainRecord$1(target)) return;
41889
+ if (!isPlainRecord(target)) return;
41209
41890
  try {
41210
41891
  for (const key of Object.keys(args)) if (!(key in target)) delete args[key];
41211
41892
  Object.assign(args, target);
@@ -44699,6 +45380,62 @@ var FileIdStore = class FileIdStore {
44699
45380
  /** Shared by every in-memory instance, so they see one cache. */
44700
45381
  static memory;
44701
45382
  };
45383
+ /** Durable MiniMax cache adapted to the shared upload singleflight core. */
45384
+ var MiniMaxHostedFileCache = class {
45385
+ store;
45386
+ now;
45387
+ constructor(options) {
45388
+ this.store = new FileIdStore(options.storePath);
45389
+ this.now = options.nowMs ?? Date.now;
45390
+ }
45391
+ async get(key) {
45392
+ const cached = (await this.store.read().catch(() => ({})))[key];
45393
+ return cached !== void 0 && cached.expiresAtMs > this.now() ? cached.fileId : void 0;
45394
+ }
45395
+ async set(key, fileId) {
45396
+ const cache = await this.store.read().catch(() => ({}));
45397
+ await this.store.write({
45398
+ ...cache,
45399
+ [key]: {
45400
+ fileId,
45401
+ expiresAtMs: this.now() + DEFAULT_FILE_ID_TTL_SEC * 1e3
45402
+ }
45403
+ }).catch(() => void 0);
45404
+ }
45405
+ async delete(key) {
45406
+ const cache = await this.store.read().catch(() => ({}));
45407
+ const existed = cache[key] !== void 0;
45408
+ if (existed) {
45409
+ delete cache[key];
45410
+ await this.store.write(cache).catch(() => void 0);
45411
+ }
45412
+ return existed;
45413
+ }
45414
+ async clear() {
45415
+ await this.store.write({}).catch(() => void 0);
45416
+ }
45417
+ };
45418
+ const UPLOAD_COORDINATORS = /* @__PURE__ */ new Map();
45419
+ const CLOCK_IDS = /* @__PURE__ */ new WeakMap();
45420
+ let clockSequence = 0;
45421
+ function uploadCoordinator(options) {
45422
+ let clockKey = "";
45423
+ if (options.nowMs !== void 0) {
45424
+ let id = CLOCK_IDS.get(options.nowMs);
45425
+ if (id === void 0) {
45426
+ id = ++clockSequence;
45427
+ CLOCK_IDS.set(options.nowMs, id);
45428
+ }
45429
+ clockKey = ":clock:" + String(id);
45430
+ }
45431
+ const key = (options.storePath ?? "<memory>") + clockKey;
45432
+ let coordinator = UPLOAD_COORDINATORS.get(key);
45433
+ if (coordinator === void 0) {
45434
+ coordinator = new HostedMediaUploadCoordinator({ cache: new MiniMaxHostedFileCache(options) });
45435
+ UPLOAD_COORDINATORS.set(key, coordinator);
45436
+ }
45437
+ return coordinator;
45438
+ }
44702
45439
  function isRecord$47(value) {
44703
45440
  return typeof value === "object" && value !== null && !Array.isArray(value);
44704
45441
  }
@@ -44759,25 +45496,19 @@ function filesPurposeFor(mediaType) {
44759
45496
  * inline instead (too large, unconfigured, or the upload failed).
44760
45497
  */
44761
45498
  async function uploadMediaFile(request, accountKey, options = {}) {
44762
- const purpose = filesPurposeFor(request.mediaType);
44763
- if (purpose === void 0) return void 0;
45499
+ if (filesPurposeFor(request.mediaType) === void 0) return void 0;
44764
45500
  if (request.bytes > 52428800) throw new FilesApiError("too-large", "the file is " + formatBytes(request.bytes) + ", above the " + formatBytes(MAX_UPLOAD_BYTES) + " upload ceiling");
44765
45501
  const key = cacheKey(request.base64, accountKey);
44766
- const nowMs = options.nowMs ?? Date.now;
44767
- const store = new FileIdStore(options.storePath);
44768
- const cache = await store.read().catch(() => ({}));
44769
- const cached = cache[key];
44770
- if (cached !== void 0 && cached.expiresAtMs > nowMs()) return cached.fileId;
45502
+ return uploadCoordinator(options).upload(key, request.signal, (controller) => performMediaUpload(request, options, controller));
45503
+ }
45504
+ /** One shared transport flight; cache reads/writes live in MiniMaxHostedFileCache. */
45505
+ async function performMediaUpload(request, options, controller) {
45506
+ const purpose = filesPurposeFor(request.mediaType);
44771
45507
  const fetchFn = options.fetchFn ?? fetch;
44772
45508
  const form = new FormData();
44773
45509
  form.append("purpose", purpose);
44774
45510
  form.append("file", new Blob([Buffer.from(request.base64, "base64")], { type: request.mediaType }), filenameForMediaType(request.mediaType));
44775
- const controller = new AbortController();
44776
45511
  const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
44777
- const abortForTurn = () => controller.abort(request.signal?.reason);
44778
- if (request.signal?.aborted === true) abortForTurn();
44779
- else request.signal?.addEventListener("abort", abortForTurn, { once: true });
44780
- let fileId;
44781
45512
  try {
44782
45513
  const response = await fetchFn(filesUploadUrl(request.baseUrl), {
44783
45514
  method: "POST",
@@ -44790,24 +45521,16 @@ async function uploadMediaFile(request, accountKey, options = {}) {
44790
45521
  if (!response.ok) throw new FilesApiError("rejected", "the Files API upload failed with HTTP " + response.status);
44791
45522
  const raw = await response.text().catch(() => "");
44792
45523
  const payload = safeJson(raw);
44793
- fileId = readFileId(payload);
45524
+ const fileId = readFileId(payload);
44794
45525
  if (fileId === void 0) throw new FilesApiError("rejected", describeRejection(payload, raw));
45526
+ return fileId;
44795
45527
  } catch (error) {
44796
45528
  if (error instanceof FilesApiError) throw error;
44797
- if (isAbort$1(error, request.signal)) throw error;
45529
+ if (isAbort$1(error, controller.signal)) throw error;
44798
45530
  throw new FilesApiError("network", "the Files API upload did not complete: " + (error instanceof Error ? error.message : String(error)));
44799
45531
  } finally {
44800
45532
  clearTimeout(timer);
44801
- request.signal?.removeEventListener("abort", abortForTurn);
44802
45533
  }
44803
- await store.write({
44804
- ...cache,
44805
- [key]: {
44806
- fileId,
44807
- expiresAtMs: nowMs() + DEFAULT_FILE_ID_TTL_SEC * 1e3
44808
- }
44809
- }).catch(() => void 0);
44810
- return fileId;
44811
45534
  }
44812
45535
  /** Whether one failure is a cancellation rather than a real upload failure. */
44813
45536
  function isAbort$1(error, signal) {
@@ -44848,6 +45571,50 @@ function formatBytes(bytes) {
44848
45571
  return (bytes / (1024 * 1024)).toFixed(1) + " MB";
44849
45572
  }
44850
45573
  //#endregion
45574
+ //#region src/host/common/sticky-image-budget.ts
45575
+ /** Durable dropped-image identities for a provider's sticky budget. */
45576
+ function normalizeRecord$1(value) {
45577
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
45578
+ const record = value;
45579
+ const updatedAt = record.updatedAt;
45580
+ if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return void 0;
45581
+ const keys = Array.isArray(record.budgetDropped) ? record.budgetDropped.filter((key) => typeof key === "string" && key !== "").slice(0, 512) : [];
45582
+ return {
45583
+ updatedAt,
45584
+ budgetDropped: [...new Set(keys)]
45585
+ };
45586
+ }
45587
+ function persistenceEnabled$1() {
45588
+ return process.env.VITEST !== "true" && process.env.NODE_ENV !== "test";
45589
+ }
45590
+ var StickyImageBudgetStore = class {
45591
+ store;
45592
+ constructor(provider, filePath) {
45593
+ const target = filePath ?? path.join(dshHomeDir(), "storages", `dsh-chatgpt-subscription-${provider}-cache.json`);
45594
+ this.store = new CommonSessionCacheStateStore({
45595
+ filePath: target,
45596
+ allowWrite: filePath !== void 0 || persistenceEnabled$1(),
45597
+ normalizeRecord: normalizeRecord$1
45598
+ });
45599
+ }
45600
+ /** Identities already omitted in this session, including before a restart. */
45601
+ priorDroppedKeys(sessionId) {
45602
+ if (sessionId === void 0 || sessionId.trim() === "") return /* @__PURE__ */ new Set();
45603
+ return new Set(this.store.read(sessionId)?.budgetDropped ?? []);
45604
+ }
45605
+ /** Persist the complete set, not just the newest omissions. */
45606
+ record(sessionId, keys) {
45607
+ if (sessionId === void 0 || sessionId.trim() === "") return;
45608
+ this.store.write(sessionId, {
45609
+ updatedAt: Date.now(),
45610
+ budgetDropped: [...keys].slice(0, 512)
45611
+ });
45612
+ }
45613
+ flush() {
45614
+ this.store.flush();
45615
+ }
45616
+ };
45617
+ //#endregion
44851
45618
  //#region src/host/minimax-code/adapter.ts
44852
45619
  /**
44853
45620
  * The DSH adapter for the MiniMax Code subscription.
@@ -45150,6 +45917,8 @@ function classifyMinimaxFailure(status, bodyText) {
45150
45917
  message: "MiniMax Code(编程订阅) API error (" + status + "): " + (detail === "" ? "No response" : detail)
45151
45918
  };
45152
45919
  }
45920
+ const minimaxPrefixRequests = new PrefixRequestCoordinator();
45921
+ const minimaxStickyImages = new StickyImageBudgetStore("minimax-code");
45153
45922
  /** The reasoning levels one model advertises, from the hardcoded directory. */
45154
45923
  function effortsForModel(modelId) {
45155
45924
  return [...minimaxCodeModelDef(modelId)?.reasoningEfforts ?? []];
@@ -45517,7 +46286,11 @@ var MinimaxCodeAdapter = class extends LlmAdapter {
45517
46286
  const settings = await this.settings();
45518
46287
  minimaxCodeModelDef(options.model);
45519
46288
  const contextWindow = contextWindowForModel(options.model, settings.contextWindowOverrides);
45520
- const requestOptions = offloadOldestRequestVideos(offloadOldestRequestImages$2(normalizeGenerateOptions(options), maxRequestImageBytes()), MAX_REQUEST_VIDEO_BYTES);
46289
+ const normalized = withStaticPrefixOrdering("minimax-code", normalizeGenerateOptions(options), this.options.promptCache);
46290
+ const imageBudget = maxRequestImageBytes();
46291
+ const sticky = promptCachePolicyOf("minimax-code", this.options.promptCache).stickyImages ? offloadRequestImagesWithStickyBudget(normalized, imageBudget, Math.max(1, Math.floor(imageBudget / 2)), void 0, minimaxStickyImages.priorDroppedKeys(normalized.sessionId)) : void 0;
46292
+ const requestOptions = offloadOldestRequestVideos(sticky?.options ?? offloadOldestRequestImages$2(normalized, imageBudget), MAX_REQUEST_VIDEO_BYTES);
46293
+ if (sticky !== void 0 && sticky.newlyDroppedKeys.length > 0) minimaxStickyImages.record(normalized.sessionId, sticky.allDroppedKeys);
45521
46294
  const [images, videos] = await Promise.all([resolveRequestImages$2(requestOptions, this.options.attachments, signal), resolveRequestVideos(requestOptions, this.options.videos, signal)]);
45522
46295
  const effectiveMax = clampOutputToContext(floorForcedThinkingTokens(options.model, requestOptions.maxTokens) ?? maxOutputTokensFor$1(options.model, contextWindow), contextWindow, estimatedInputTokens(requestOptions));
45523
46296
  const tried = /* @__PURE__ */ new Set();
@@ -45590,7 +46363,9 @@ var MinimaxCodeAdapter = class extends LlmAdapter {
45590
46363
  let refusedBody;
45591
46364
  for (let attempt = 0;; attempt += 1) {
45592
46365
  try {
45593
- response = await fetchWithContextBudgetRecovery(fetchFn)(messagesUrl(credentials.region), {
46366
+ const cachePolicy = promptCachePolicyOf("minimax-code", this.options.promptCache);
46367
+ const groupKey = cachePolicy.coldStart === "off" ? void 0 : sharedPrefixGroupKey("minimax-code", requestOptions, credentials.region, accountId ?? "legacy");
46368
+ response = await fetchWithPrefixCoordination(fetchWithContextBudgetRecovery(fetchFn), minimaxPrefixRequests, groupKey, cachePolicy.coldStart === "warm" ? cachePolicy.ttlMs : void 0, messagesUrl(credentials.region), {
45594
46369
  method: "POST",
45595
46370
  headers: modelRequestHeaders(credentials.accessToken, requestContext),
45596
46371
  body,
@@ -48681,34 +49456,6 @@ function makeReminderText2(repeatCount) {
48681
49456
  const REMINDER_TEXT_3 = "<system-reminder>\nWrite your final response now, without any further tool calls. Cover: the current blocker, each approach you have tried and what it established, and the specific information or decision you need from the user to unblock progress. Text only.\n</system-reminder>";
48682
49457
  /** 强停那一轮之后,守卫用来拒绝后续工具调用的文案。 */
48683
49458
  const HANDOFF_VETO_TEXT = "This turn was ended by the repeat breaker after the same tool call was issued " + String(12) + " times in a row. This step accepts a text response only, so the tool call was not executed. Reply in text: the current blocker, what you tried, and what you need next.";
48684
- /**
48685
- * 不计入连击的协调类工具。
48686
- *
48687
- * 这些工具**本来就会**被反复调用:报一次进展、更新一次待办、问一次用户。把它们
48688
- * 算进连击等于让刹车对着正常的心跳乱响。
48689
- */
48690
- const REPEAT_BREAKER_EXEMPT_TOOLS = Object.freeze([
48691
- "progress_update",
48692
- "todo_write",
48693
- "ask_user_question",
48694
- "enter_plan_mode",
48695
- "exit_plan_mode",
48696
- "leave_plan_mode",
48697
- "create_goal",
48698
- "update_goal",
48699
- "get_goal",
48700
- "dispatch_ledger",
48701
- "present"
48702
- ]);
48703
- /**
48704
- * 判断一个工具是不是不计入连击的协调类工具。
48705
- *
48706
- * @param toolName - 工具名。
48707
- * @returns 豁免时为 true。
48708
- */
48709
- function isRepeatBreakerExemptTool(toolName) {
48710
- return REPEAT_BREAKER_EXEMPT_TOOLS.includes(toolName);
48711
- }
48712
49459
  /** 明确表示「关掉」的环境变量取值。 */
48713
49460
  const DISABLED_ENV_VALUES = /* @__PURE__ */ new Set([
48714
49461
  "0",
@@ -48731,17 +49478,6 @@ function isKimiRepeatBreakerEnabled(env = process.env) {
48731
49478
  return !DISABLED_ENV_VALUES.has(raw.trim().toLowerCase());
48732
49479
  }
48733
49480
  /**
48734
- * 判断一个值是不是普通对象(原型是 Object.prototype 或 null)。
48735
- *
48736
- * @param value - 任意值。
48737
- * @returns 是普通对象时为 true。
48738
- */
48739
- function isPlainRecord(value) {
48740
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
48741
- const prototype = Object.getPrototypeOf(value);
48742
- return prototype === Object.prototype || prototype === null;
48743
- }
48744
- /**
48745
49481
  * 判断一个值是不是对象(数组也算)。
48746
49482
  *
48747
49483
  * @param value - 任意值。
@@ -48796,42 +49532,6 @@ function kimiSessionRouteOf(agent) {
48796
49532
  };
48797
49533
  }
48798
49534
  /**
48799
- * 递归把一个值重排成「键名有序」的形状。
48800
- *
48801
- * 数组**保持原序**:数组元素的顺序本身就是语义([1, 2] 与 [2, 1] 不是同一件事),
48802
- * 只有对象的键名顺序没有语义。
48803
- *
48804
- * @param value - 当前值。
48805
- * @returns 重排后的值。
48806
- */
48807
- function sortJsonValue(value) {
48808
- if (Array.isArray(value)) return value.map(sortJsonValue);
48809
- if (!isPlainRecord(value)) return value;
48810
- const out = {};
48811
- for (const key of Object.keys(value).sort()) out[key] = sortJsonValue(value[key]);
48812
- return out;
48813
- }
48814
- /**
48815
- * 造一次工具调用的**规范键**。
48816
- *
48817
- * 键 = 工具名 + 参数的规范 JSON。参数先递归重排键名再序列化,于是
48818
- * { a: 1, b: 2 } 与 { b: 2, a: 1 } 得到**完全相同**的键 —— 否则模型只是把参数
48819
- * 的书写顺序换了一下,刹车就看不见这是一个连击。
48820
- *
48821
- * @param toolName - 工具名。
48822
- * @param args - 工具参数。
48823
- * @returns 规范键。
48824
- */
48825
- function canonicalKimiToolCallKey(toolName, args) {
48826
- let json;
48827
- try {
48828
- json = JSON.stringify(sortJsonValue(args));
48829
- } catch {
48830
- json = void 0;
48831
- }
48832
- return toolName + " " + (json ?? String(args));
48833
- }
48834
- /**
48835
49535
  * 造一台重复刹车。
48836
49536
  *
48837
49537
  * 状态**按会话**分开:连击是「这场对话里的同一件事重复了多少次」,两个会话
@@ -48839,53 +49539,22 @@ function canonicalKimiToolCallKey(toolName, args) {
48839
49539
  *
48840
49540
  * @returns 一台重复刹车。
48841
49541
  */
49542
+ function repeatReminder(count) {
49543
+ if (count >= 8) return REMINDER_TEXT_3;
49544
+ if (count >= 5) return makeReminderText2(count);
49545
+ if (count >= 3) return REMINDER_TEXT_1;
49546
+ }
48842
49547
  function createKimiRepeatBreaker() {
48843
- const sessions = /* @__PURE__ */ new Map();
48844
- /** 取一个会话的状态;没有就新建一个。 */
48845
- const stateOf = (sessionId) => {
48846
- const existing = sessions.get(sessionId);
48847
- if (existing !== void 0) return existing;
48848
- const created = {
48849
- consecutiveKey: null,
48850
- consecutiveCount: 0,
48851
- handoffActive: false
48852
- };
48853
- sessions.set(sessionId, created);
48854
- return created;
48855
- };
48856
- return {
48857
- checkBeforeExecute(sessionId, toolName, args) {
48858
- const state = stateOf(sessionId);
48859
- if (state.handoffActive) return HANDOFF_VETO_TEXT;
48860
- const key = canonicalKimiToolCallKey(toolName, args);
48861
- if (state.consecutiveKey === key && state.consecutiveCount + 1 >= 12) {
48862
- state.consecutiveCount += 1;
48863
- state.handoffActive = true;
48864
- return HANDOFF_VETO_TEXT;
48865
- }
48866
- },
48867
- recordAfterExecute(sessionId, toolName, args) {
48868
- if (isRepeatBreakerExemptTool(toolName)) return void 0;
48869
- const state = stateOf(sessionId);
48870
- if (state.handoffActive) return REMINDER_TEXT_3;
48871
- const key = canonicalKimiToolCallKey(toolName, args);
48872
- if (state.consecutiveKey === key) state.consecutiveCount += 1;
48873
- else {
48874
- state.consecutiveKey = key;
48875
- state.consecutiveCount = 1;
48876
- }
48877
- if (state.consecutiveCount >= 12) {
48878
- state.handoffActive = true;
48879
- return REMINDER_TEXT_3;
48880
- }
48881
- if (state.consecutiveCount >= 8) return REMINDER_TEXT_3;
48882
- if (state.consecutiveCount >= 5) return makeReminderText2(state.consecutiveCount);
48883
- if (state.consecutiveCount >= 3) return REMINDER_TEXT_1;
48884
- },
48885
- resetSession(sessionId) {
48886
- sessions.delete(sessionId);
48887
- }
48888
- };
49548
+ return createExactRepeatBreaker({
49549
+ reminder: repeatReminder,
49550
+ forceStopAt: 12,
49551
+ handoffReminder: REMINDER_TEXT_3,
49552
+ handoffVeto: HANDOFF_VETO_TEXT
49553
+ });
49554
+ }
49555
+ /** Other providers receive the graduated reminders, never Kimi's hard stop. */
49556
+ function createReminderOnlyRepeatBreaker() {
49557
+ return createExactRepeatBreaker({ reminder: repeatReminder });
48889
49558
  }
48890
49559
  /**
48891
49560
  * 读宿主上下文上的工具注册面。
@@ -49007,37 +49676,43 @@ function isRealUserTurn(message) {
49007
49676
  function installKimiRepeatBreaker(ctx, options = {}) {
49008
49677
  const releaseMount = claimMount$1(ctx);
49009
49678
  if (releaseMount === void 0) return () => void 0;
49010
- const breaker = createKimiRepeatBreaker();
49679
+ const kimiBreaker = createKimiRepeatBreaker();
49680
+ const reminderBreaker = createReminderOnlyRepeatBreaker();
49011
49681
  const enabled = options.enabled ?? (() => isKimiRepeatBreakerEnabled());
49012
- /** 读一次开关。读失败按**开启**处理:保护不该因为一次读失败而失效。 */
49013
- const isEnabled = () => {
49682
+ const globalEnabled = options.globalEnabled ?? (() => true);
49683
+ const providers = new Set(options.providers ?? ["kimi-code"]);
49684
+ const readSwitch = (read) => {
49014
49685
  try {
49015
- return enabled() !== false;
49686
+ return read() !== false;
49016
49687
  } catch {
49017
49688
  return true;
49018
49689
  }
49019
49690
  };
49020
- /**
49021
- * 取一次执行所属的会话;不是 Kimi Code 这条线路时是 undefined。
49022
- *
49023
- * @param agent - 调用方 agent。
49024
- * @returns 会话标识;不该管时是 undefined。
49025
- */
49026
- const kimiSessionIdOf = (agent) => {
49691
+ const targetOf = (agent) => {
49027
49692
  const route = kimiSessionRouteOf(agent);
49028
- if (route.provider !== "kimi-code") return void 0;
49029
- return route.sessionId;
49693
+ const provider = route.provider;
49694
+ if (provider === void 0 || route.sessionId === void 0 || !providers.has(provider)) return void 0;
49695
+ return {
49696
+ provider,
49697
+ key: provider + ":" + route.sessionId,
49698
+ breaker: provider === "kimi-code" ? kimiBreaker : reminderBreaker
49699
+ };
49700
+ };
49701
+ const activeTargetOf = (agent) => {
49702
+ if (!readSwitch(globalEnabled)) return void 0;
49703
+ const target = targetOf(agent);
49704
+ if (target === void 0) return void 0;
49705
+ if (target.provider === "kimi-code" && !readSwitch(enabled)) return void 0;
49706
+ return target;
49030
49707
  };
49031
49708
  const disposeGuard = toolsServiceOf(ctx)?.guard?.((execution) => {
49032
49709
  try {
49033
- if (!isEnabled()) return void 0;
49034
49710
  const record = isRecord$45(execution) ? execution : void 0;
49035
- const agent = record?.["agent"];
49036
- const sessionId = kimiSessionIdOf(agent);
49037
- if (sessionId === void 0) return void 0;
49711
+ const target = activeTargetOf(record?.["agent"]);
49712
+ if (target === void 0) return void 0;
49038
49713
  const toolName = stringOf$3(record?.["name"]);
49039
49714
  if (toolName === void 0) return void 0;
49040
- const veto = breaker.checkBeforeExecute(sessionId, toolName, record?.["arguments"]);
49715
+ const veto = target.breaker.checkBeforeExecute(target.key, toolName, record?.["arguments"]);
49041
49716
  if (veto === HANDOFF_VETO_TEXT) return HANDOFF_VETO_TEXT;
49042
49717
  return veto;
49043
49718
  } catch {
@@ -49047,13 +49722,11 @@ function installKimiRepeatBreaker(ctx, options = {}) {
49047
49722
  const disposePostExecute = ctx.on?.("tools/post-execute", (async (exec, result, next) => {
49048
49723
  const decision = await next();
49049
49724
  try {
49050
- if (!isEnabled()) return decision;
49051
- const agent = exec.agent;
49052
- const sessionId = kimiSessionIdOf(agent);
49053
- if (sessionId === void 0) return decision;
49725
+ const target = activeTargetOf(exec.agent);
49726
+ if (target === void 0) return decision;
49054
49727
  const toolName = stringOf$3(exec.name);
49055
49728
  if (toolName === void 0) return decision;
49056
- const reminder = breaker.recordAfterExecute(sessionId, toolName, exec.arguments);
49729
+ const reminder = target.breaker.recordAfterExecute(target.key, toolName, exec.arguments);
49057
49730
  if (reminder === void 0) return decision;
49058
49731
  return withAdditionalContexts(decision, [createRepeatBreakerReminder(reminder)]);
49059
49732
  } catch {
@@ -49064,9 +49737,9 @@ function installKimiRepeatBreaker(ctx, options = {}) {
49064
49737
  const decision = await next();
49065
49738
  try {
49066
49739
  if (!isRealUserTurn((Array.isArray(payload?.messages) ? payload.messages : []).at(-1))) return decision;
49067
- const sessionId = kimiSessionIdOf(payload?.agent);
49068
- if (sessionId === void 0) return decision;
49069
- breaker.resetSession(sessionId);
49740
+ const target = activeTargetOf(payload?.agent);
49741
+ if (target === void 0) return decision;
49742
+ target.breaker.resetSession(target.key);
49070
49743
  } catch {
49071
49744
  return decision;
49072
49745
  }
@@ -49074,9 +49747,9 @@ function installKimiRepeatBreaker(ctx, options = {}) {
49074
49747
  }));
49075
49748
  const disposeDisposed = ctx.on?.("agent/disposed", ((payload) => {
49076
49749
  try {
49077
- const sessionId = kimiSessionIdOf(payload?.agent);
49078
- if (sessionId === void 0) return;
49079
- breaker.resetSession(sessionId);
49750
+ const target = targetOf(payload?.agent);
49751
+ if (target === void 0) return;
49752
+ target.breaker.resetSession(target.key);
49080
49753
  } catch {}
49081
49754
  }));
49082
49755
  return () => {
@@ -56502,6 +57175,8 @@ function assertClaudeCliVersionMeetsFloor(modelId, catalog = FALLBACK_MODELS) {
56502
57175
  if (meetsDottedVersionFloor(effective, floor)) return;
56503
57176
  throw new LlmError("Claude(订阅) will not serve " + modelId + " to a client reporting version " + effective + ": upstream requires " + floor + " or newer for this model, and a lower claim is refused with claude_code_version_too_old. This was caught locally, before the request was sent. Raise the reported version with the DSH_CLAUDE_CLI_VERSION environment variable, or with a setClaudeCliVersion pin, then retry. The stored sign-in is still valid — this is not a credential problem and signing in again will not change it.", "PROVIDER_ERROR");
56504
57177
  }
57178
+ const claudePrefixRequests = new PrefixRequestCoordinator();
57179
+ const claudeStickyImages = new StickyImageBudgetStore("claude");
56505
57180
  /**
56506
57181
  * The immutable id of the account a token came from, presented as the two-method
56507
57182
  * store oauth.ts wants. See module note 3.
@@ -56755,7 +57430,9 @@ var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
56755
57430
  cacheTtl
56756
57431
  });
56757
57432
  try {
56758
- return await fetchWithContextBudgetRecovery(fetchFn)("https://api.anthropic.com/v1/messages?beta=true", {
57433
+ const cachePolicy = promptCachePolicyOf("claude", this.options.promptCache);
57434
+ const groupKey = cachePolicy.coldStart === "off" ? void 0 : sharedPrefixGroupKey("claude", requestOptions, "subscription", credentials.account?.uuid ?? "legacy");
57435
+ return await fetchWithPrefixCoordination(fetchWithContextBudgetRecovery(fetchFn), claudePrefixRequests, groupKey, cachePolicy.coldStart === "warm" ? cachePolicy.ttlMs : void 0, "https://api.anthropic.com/v1/messages?beta=true", {
56759
57436
  method: "POST",
56760
57437
  headers: buildClaudeHeaders(credentials.accessToken, {
56761
57438
  model: requestOptions.model,
@@ -56775,9 +57452,12 @@ var ClaudeAdapter = class ClaudeAdapter extends LlmAdapter {
56775
57452
  }
56776
57453
  async *requestStream(options, signal, activity) {
56777
57454
  const fetchFn = this.options.fetchFn ?? fetch;
56778
- const normalized = normalizeGenerateOptions(options);
57455
+ const normalized = withStaticPrefixOrdering("claude", normalizeGenerateOptions(options), this.options.promptCache);
56779
57456
  const images = await resolveRequestImages(normalized, this.options.attachments, signal);
56780
- const requestOptions = offloadOldestRequestImages(normalized, images);
57457
+ const stickyImages = new Map([...images].map(([id, image]) => [id, image.kind === "oversized" ? { kind: "unavailable" } : image]));
57458
+ const sticky = promptCachePolicyOf("claude", this.options.promptCache).stickyImages ? offloadRequestImagesWithStickyBudget(normalized, MAX_REQUEST_IMAGE_BYTES$2, Math.max(1, Math.floor(MAX_REQUEST_IMAGE_BYTES$2 / 2)), stickyImages, claudeStickyImages.priorDroppedKeys(normalized.sessionId)) : void 0;
57459
+ const requestOptions = sticky?.options ?? offloadOldestRequestImages(normalized, images);
57460
+ if (sticky !== void 0 && sticky.newlyDroppedKeys.length > 0) claudeStickyImages.record(normalized.sessionId, sticky.allDroppedKeys);
56781
57461
  const toolNames = claudeToolNames(requestOptions.tools);
56782
57462
  assertClaudeCliVersionMeetsFloor(requestOptions.model, await this.catalogForRequest());
56783
57463
  const effort = requestOptions.reasoningEffort === void 0 || requestOptions.reasoningEffort === null ? void 0 : String(requestOptions.reasoningEffort);
@@ -66765,11 +67445,77 @@ function classifyStepCodeFailure(status, detail) {
66765
67445
  }
66766
67446
  //#endregion
66767
67447
  //#region src/host/step-code/cache-stats.ts
67448
+ /**
67449
+ * Misses at or below this are cache-breakpoint granularity, not waste.
67450
+ *
67451
+ * A prefix cache breaks at block boundaries, so a few hundred tokens of drift
67452
+ * between two turns is normal re-shuffling. Reporting it teaches the user to
67453
+ * ignore the notice, which costs the one notice that mattered.
67454
+ */
67455
+ const NOISE_FLOOR_TOKENS = 1024;
66768
67456
  /** Aggregate prompt count of one turn: uncached input plus both cache buckets. */
66769
67457
  function promptTokensOf(usage) {
66770
67458
  return usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
66771
67459
  }
66772
67460
  //#endregion
67461
+ //#region src/host/common/cache-activity.ts
67462
+ /** Last request activity used by cache-expiry hints and wait-time keepalive. */
67463
+ function normalizeRecord(value) {
67464
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
67465
+ const record = value;
67466
+ const lastActiveAt = record.lastActiveAt;
67467
+ const updatedAt = record.updatedAt;
67468
+ if (typeof lastActiveAt !== "number" || !Number.isFinite(lastActiveAt)) return void 0;
67469
+ if (typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) return void 0;
67470
+ return {
67471
+ lastActiveAt,
67472
+ totalTokens: typeof record.totalTokens === "number" && Number.isFinite(record.totalTokens) ? Math.max(0, Math.floor(record.totalTokens)) : 0,
67473
+ updatedAt
67474
+ };
67475
+ }
67476
+ function persistenceEnabled() {
67477
+ return process.env.VITEST !== "true" && process.env.NODE_ENV !== "test";
67478
+ }
67479
+ var PromptCacheActivityStore = class {
67480
+ store;
67481
+ constructor(provider, filePath) {
67482
+ const target = filePath ?? path.join(dshHomeDir(), "storages", `dsh-chatgpt-subscription-${provider}-activity.json`);
67483
+ this.store = new CommonSessionCacheStateStore({
67484
+ filePath: target,
67485
+ allowWrite: filePath !== void 0 || persistenceEnabled(),
67486
+ normalizeRecord
67487
+ });
67488
+ }
67489
+ read(sessionId) {
67490
+ const record = this.store.read(sessionId);
67491
+ return record === void 0 ? void 0 : {
67492
+ lastActiveAt: record.lastActiveAt,
67493
+ totalTokens: record.totalTokens
67494
+ };
67495
+ }
67496
+ record(sessionId, totalTokens, at = Date.now()) {
67497
+ if (sessionId === void 0 || sessionId.trim() === "") return;
67498
+ this.store.write(sessionId, {
67499
+ lastActiveAt: at,
67500
+ totalTokens: Number.isFinite(totalTokens) ? Math.max(0, Math.floor(totalTokens)) : 0,
67501
+ updatedAt: at
67502
+ });
67503
+ }
67504
+ flush() {
67505
+ this.store.flush();
67506
+ }
67507
+ };
67508
+ const SHARED_ACTIVITY_STORES = /* @__PURE__ */ new Map();
67509
+ /** One live store per provider; tests can still construct isolated instances by path. */
67510
+ function promptCacheActivityStore(provider) {
67511
+ let store = SHARED_ACTIVITY_STORES.get(provider);
67512
+ if (store === void 0) {
67513
+ store = new PromptCacheActivityStore(provider);
67514
+ SHARED_ACTIVITY_STORES.set(provider, store);
67515
+ }
67516
+ return store;
67517
+ }
67518
+ //#endregion
66773
67519
  //#region src/host/step-code/client.ts
66774
67520
  /**
66775
67521
  * Step Code 线路的上游 I/O:模型目录 + 开发者中心额度。
@@ -68446,6 +69192,8 @@ function streamHeadersFor(credentials) {
68446
69192
  accept: "text/event-stream"
68447
69193
  };
68448
69194
  }
69195
+ const stepPrefixRequests = new PrefixRequestCoordinator();
69196
+ const stepCacheActivity = promptCacheActivityStore("step-code");
68449
69197
  /**
68450
69198
  * Step Code(Step Plan 订阅)线路的 LlmAdapter 实现。
68451
69199
  *
@@ -68494,10 +69242,10 @@ var StepCodeAdapter = class extends LlmAdapter {
68494
69242
  * 两条路径都**不刷新**:Step 的 api_key 永不过期,号池的 refresh 钩子恒抛
68495
69243
  * `StepCodeCredentialRefreshError`,所以这里不存在「刷新失败后再试一次」的分支。
68496
69244
  */
68497
- async credentialFor(excludeIds) {
69245
+ async credentialFor(excludeIds, selection) {
68498
69246
  const fetchFn = this.options.fetchFn ?? fetch;
68499
69247
  if (this.accountPool !== null) {
68500
- const effective = await this.accountPool.getEffectiveCredential(excludeIds, fetchFn);
69248
+ const effective = await this.accountPool.getEffectiveCredential(excludeIds, fetchFn, selection);
68501
69249
  const credentials = effective.credentials;
68502
69250
  return {
68503
69251
  accountId: effective.account.id,
@@ -68519,6 +69267,31 @@ var StepCodeAdapter = class extends LlmAdapter {
68519
69267
  }
68520
69268
  };
68521
69269
  }
69270
+ /** Warm-prefix account preference, inert unless a deployment proves account scope. */
69271
+ async cacheAffinityHint(options) {
69272
+ const pool = this.accountPool;
69273
+ if (pool === null) return void 0;
69274
+ const policy = promptCachePolicyOf("step-code", this.options.promptCache);
69275
+ if (!policy.affinity || policy.ttlMs === void 0) return void 0;
69276
+ const data = await pool.read().catch(() => void 0);
69277
+ if (data === void 0 || data.accounts.length < 2) return void 0;
69278
+ const candidates = [];
69279
+ const remainingShareById = {};
69280
+ for (const account of data.accounts) {
69281
+ const key = sharedPrefixGroupKey("step-code", options, account.credentials.profile, account.id);
69282
+ if (key === void 0 || !stepPrefixRequests.isWarm(key, policy.ttlMs)) continue;
69283
+ candidates.push({
69284
+ id: account.id,
69285
+ warm: true
69286
+ });
69287
+ remainingShareById[account.id] = remainingQuotaShare$1(getCachedStepQuotaFor(account.id));
69288
+ }
69289
+ const preferAccountIds = orderAffinityCandidates$1({
69290
+ candidates,
69291
+ remainingShareById
69292
+ });
69293
+ return preferAccountIds.length === 0 ? void 0 : { preferAccountIds };
69294
+ }
68522
69295
  async listModels(provider) {
68523
69296
  const prov = provider || PROVIDER_ID$6;
68524
69297
  const settings = await this.settings();
@@ -68581,13 +69354,14 @@ var StepCodeAdapter = class extends LlmAdapter {
68581
69354
  async *requestStream(options, signal, activity) {
68582
69355
  const fetchFn = this.options.fetchFn ?? fetch;
68583
69356
  const settings = await this.settings();
68584
- const normalized = normalizeGenerateOptions(options);
69357
+ const normalized = withStaticPrefixOrdering("step-code", normalizeGenerateOptions(options), this.options.promptCache);
68585
69358
  const images = await resolveRequestImages$2(normalized, this.options.attachments, signal);
68586
69359
  const requestOptions = offloadOldestStepCodeRequestImages(normalized, images);
68587
69360
  const tried = /* @__PURE__ */ new Set();
68588
69361
  let response;
68589
69362
  let url = "";
68590
- let picked = await this.credentialFor(tried);
69363
+ const affinity = await this.cacheAffinityHint(requestOptions);
69364
+ let picked = await this.credentialFor(tried, affinity);
68591
69365
  while (true) {
68592
69366
  if (picked.accountId !== null) tried.add(picked.accountId);
68593
69367
  const catalog = this.catalog(picked.credentials.profile);
@@ -68595,7 +69369,9 @@ var StepCodeAdapter = class extends LlmAdapter {
68595
69369
  const body = JSON.stringify(buildChatRequest$1(requestOptions, images, stepCodeRequestOptionsFor(entry, { cacheRequestParams: settings.cacheRequestParams })));
68596
69370
  url = stepChatCompletionsUrl(picked.credentials.profile);
68597
69371
  try {
68598
- response = await fetchWithContextBudgetRecovery(fetchFn)(url, {
69372
+ const cachePolicy = promptCachePolicyOf("step-code", this.options.promptCache);
69373
+ const groupKey = cachePolicy.coldStart === "off" ? void 0 : sharedPrefixGroupKey("step-code", requestOptions, picked.credentials.profile, picked.accountId ?? "legacy");
69374
+ response = await fetchWithPrefixCoordination(fetchWithContextBudgetRecovery(fetchFn), stepPrefixRequests, groupKey, cachePolicy.coldStart === "warm" ? cachePolicy.ttlMs : void 0, url, {
68599
69375
  method: "POST",
68600
69376
  headers: streamHeadersFor(picked.credentials),
68601
69377
  body,
@@ -68617,13 +69393,13 @@ var StepCodeAdapter = class extends LlmAdapter {
68617
69393
  const after = retryAfterMs$1(response.headers);
68618
69394
  await this.accountPool.markCooldown(picked.accountId, after ?? 3e5, "Step Code(Step Plan 订阅) 429:上游限流或额度用尽").catch(() => void 0);
68619
69395
  if (await this.accountPool.hasAnotherAvailableAccount(tried)) {
68620
- picked = await this.credentialFor(tried);
69396
+ picked = await this.credentialFor(tried, affinity);
68621
69397
  continue;
68622
69398
  }
68623
69399
  } else if (failure.code === "AUTH") {
68624
69400
  await this.accountPool.markAuthFailed(picked.accountId, failure.message).catch(() => void 0);
68625
69401
  if (await this.accountPool.hasAnotherAvailableAccount(tried)) {
68626
- picked = await this.credentialFor(tried);
69402
+ picked = await this.credentialFor(tried, affinity);
68627
69403
  continue;
68628
69404
  }
68629
69405
  }
@@ -68656,16 +69432,42 @@ var StepCodeAdapter = class extends LlmAdapter {
68656
69432
  assertStreamComplete$2(state);
68657
69433
  for (const chunk of closeStream$1(state)) yield chunk;
68658
69434
  } finally {
68659
- if (state.sawUsage) rememberStepCodeCacheUsage({
68660
- inputTokens: state.inputTokens,
68661
- outputTokens: state.outputTokens,
68662
- cacheReadTokens: state.cacheReadTokens,
68663
- cacheWriteTokens: state.cacheWriteTokens
68664
- });
69435
+ if (state.sawUsage) {
69436
+ rememberStepCodeCacheUsage({
69437
+ inputTokens: state.inputTokens,
69438
+ outputTokens: state.outputTokens,
69439
+ cacheReadTokens: state.cacheReadTokens,
69440
+ cacheWriteTokens: state.cacheWriteTokens
69441
+ });
69442
+ stepCacheActivity.record(options.sessionId, promptTokensOf({
69443
+ inputTokens: state.inputTokens,
69444
+ outputTokens: state.outputTokens,
69445
+ cacheReadTokens: state.cacheReadTokens,
69446
+ cacheWriteTokens: state.cacheWriteTokens
69447
+ }));
69448
+ }
68665
69449
  reader.cancel().catch(() => void 0);
68666
69450
  }
68667
69451
  }
68668
69452
  };
69453
+ /** Cache-expiry hint for one session, or the latest recorded session when omitted. */
69454
+ function readStepCodeCacheHint(sessionId, override) {
69455
+ const policy = promptCachePolicyOf("step-code", override);
69456
+ if (!policy.hint) return null;
69457
+ const activity = stepCacheActivity.read(sessionId);
69458
+ if (activity === void 0) return null;
69459
+ const decision = evaluateCacheHint$1({
69460
+ now: Date.now(),
69461
+ lastActiveAt: activity.lastActiveAt,
69462
+ totalTokens: activity.totalTokens,
69463
+ ttlMs: policy.ttlMs,
69464
+ minTokensToHint: NOISE_FLOOR_TOKENS
69465
+ });
69466
+ return decision.kind === "hint" ? {
69467
+ idleMs: decision.idleMs,
69468
+ totalTokens: decision.totalTokens
69469
+ } : null;
69470
+ }
68669
69471
  //#endregion
68670
69472
  //#region src/host/step-code/oauth.ts
68671
69473
  /**
@@ -69457,8 +70259,8 @@ var StepCodeAccountPool = class extends AccountPoolCore {
69457
70259
  * 核心在这里做轮转、冷却、可用性判断与(被拒绝的)刷新;本方法只多给适配器一件事:
69458
70260
  * 一次请求内换过账号时,把试过的账号排除掉。
69459
70261
  */
69460
- async getEffectiveCredential(excludeIds, fetchFn = fetch) {
69461
- return this.getEffectiveAccount(excludeIds, fetchFn);
70262
+ async getEffectiveCredential(excludeIds, fetchFn = fetch, selection) {
70263
+ return this.getEffectiveAccount(excludeIds, fetchFn, false, void 0, selection);
69462
70264
  }
69463
70265
  /**
69464
70266
  * 一次额度读属于哪个账号,连同读它用的凭据。
@@ -69949,6 +70751,7 @@ async function getStepCodeStatus(sources) {
69949
70751
  const settingsValue = await settings.status();
69950
70752
  const quota = sources.quota;
69951
70753
  const cacheStats = sources.cacheStats;
70754
+ const cacheHint = readStepCodeCacheHint(void 0, sources.options.promptCache);
69952
70755
  const accountCount = sources.pool.accounts.length;
69953
70756
  return {
69954
70757
  ...sources.pool,
@@ -69973,6 +70776,7 @@ async function getStepCodeStatus(sources) {
69973
70776
  quotaCredential: sources.quotaCredential,
69974
70777
  cacheRequestParams: settingsValue.cacheRequestParams === true,
69975
70778
  ...cacheStats === void 0 ? {} : { cacheStats },
70779
+ ...cacheHint === null ? {} : { cacheHint },
69976
70780
  serving: readOption$2(options.serving, true),
69977
70781
  conflict: readOption$2(options.conflict, null)
69978
70782
  };
@@ -72296,6 +73100,7 @@ function isRecord$26(value) {
72296
73100
  *
72297
73101
  * @module dsh-chatgpt-subscription/cline-pass-mapper
72298
73102
  */
73103
+ const clineReasoningDialect = new ReasoningKeyDialect$1();
72299
73104
  /**
72300
73105
  * The one wording the shared detector does not know.
72301
73106
  *
@@ -72329,7 +73134,7 @@ function serializeAssistant$1(message) {
72329
73134
  return {
72330
73135
  role: "assistant",
72331
73136
  content: text,
72332
- ...reasoning.length > 0 ? { reasoning_content: reasoning } : {},
73137
+ ...reasoning.length > 0 ? { [clineReasoningDialect.outboundKey()]: reasoning } : {},
72333
73138
  ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
72334
73139
  };
72335
73140
  }
@@ -72624,6 +73429,7 @@ function acceptIdentity$1(current, incoming) {
72624
73429
  * parts, which is where a gateway that omits the flat field puts the same text.
72625
73430
  */
72626
73431
  function reasoningOf$1(delta) {
73432
+ clineReasoningDialect.observe(delta);
72627
73433
  for (const field of ["reasoning", "reasoning_content"]) {
72628
73434
  const value = delta[field];
72629
73435
  if (typeof value === "string" && value.length > 0) return value;
@@ -79028,6 +79834,7 @@ async function loadQoderQuota(credentials, options = {}) {
79028
79834
  *
79029
79835
  * @module dsh-chatgpt-subscription/qoder-mapper
79030
79836
  */
79837
+ const qoderReasoningDialect = new ReasoningKeyDialect$1();
79031
79838
  function isRecord$21(value) {
79032
79839
  return typeof value === "object" && value !== null && !Array.isArray(value);
79033
79840
  }
@@ -79374,7 +80181,7 @@ function buildQoderChatRequest(options, images, request, context) {
79374
80181
  role: "assistant",
79375
80182
  content: content !== "" ? content : toolCalls.length > 0 || reasoning !== "" ? " " : null
79376
80183
  };
79377
- if (reasoning !== "") entry.reasoning_content = reasoning;
80184
+ if (reasoning !== "") Object.assign(entry, { [qoderReasoningDialect.outboundKey()]: reasoning });
79378
80185
  if (toolCalls.length > 0) entry.tool_calls = toolCalls;
79379
80186
  if (entry.content !== null) messages.push(entry);
79380
80187
  continue;
@@ -79723,6 +80530,7 @@ function processQoderStreamLine(line, state) {
79723
80530
  const choice = isRecord$21(choices[0]) ? choices[0] : void 0;
79724
80531
  const delta = choice !== void 0 && isRecord$21(choice.delta) ? choice.delta : void 0;
79725
80532
  if (delta !== void 0) {
80533
+ qoderReasoningDialect.observe(delta);
79726
80534
  const reasoning = asString$7(delta.reasoning_content) ?? asString$7(delta.reasoning);
79727
80535
  if (reasoning !== void 0 && reasoning !== "") out.push(...appendTextDelta(state, "reasoning", sanitizeText$1(reasoning)));
79728
80536
  const content = asString$7(delta.content);
@@ -84252,6 +85060,7 @@ async function fetchCodingPlanUsage({ fetchFn, controlKey, runtimeKey, workspace
84252
85060
  *
84253
85061
  * @module dsh-chatgpt-subscription/opendesign-mapper
84254
85062
  */
85063
+ const openDesignReasoningDialect = new ReasoningKeyDialect$1();
84255
85064
  /** The sentence a no-content finish carries, shared with the sibling lines. */
84256
85065
  const NO_ANSWER_PHRASE = "ended the turn without an answer or a tool call";
84257
85066
  /**
@@ -84287,7 +85096,7 @@ function serializeAssistant(message) {
84287
85096
  return {
84288
85097
  role: "assistant",
84289
85098
  content: text,
84290
- ...reasoning.length > 0 ? { reasoning_content: reasoning } : {},
85099
+ ...reasoning.length > 0 ? { [openDesignReasoningDialect.outboundKey()]: reasoning } : {},
84291
85100
  ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
84292
85101
  };
84293
85102
  }
@@ -84630,6 +85439,7 @@ function acceptIdentity(current, incoming) {
84630
85439
  * parts, which is where a gateway that omits the flat field puts the same text.
84631
85440
  */
84632
85441
  function reasoningOf(delta) {
85442
+ openDesignReasoningDialect.observe(delta);
84633
85443
  for (const field of ["reasoning", "reasoning_content"]) {
84634
85444
  const value = delta[field];
84635
85445
  if (typeof value === "string" && value.length > 0) return value;
@@ -96110,7 +96920,7 @@ function createCacheWarmupResultTool(deps) {
96110
96920
  const route = deps.routeOf(exec);
96111
96921
  const refId = args.refId;
96112
96922
  const action = args.action;
96113
- if (route.provider !== deps.provider) return failure(action, refId, route.sessionId ?? "", "WRONG_PROVIDER", "ref handles belong to the " + deps.provider + " line; this session is routed to \"" + (route.provider ?? "unknown") + "\"");
96923
+ if (!(route.provider !== void 0 && (deps.allowProvider?.(route.provider) ?? route.provider === deps.provider))) return failure(action, refId, route.sessionId ?? "", "WRONG_PROVIDER", "ref handles are not available on this session route: \"" + (route.provider ?? "unknown") + "\"");
96114
96924
  const sessionId = route.sessionId;
96115
96925
  if (sessionId === void 0) return failure(action, refId, "", "NO_SESSION", "the current session could not be resolved, so the handle cannot be checked");
96116
96926
  const store = deps.store();
@@ -96252,8 +97062,8 @@ function cacheWarmupNavigationIntentOf(agent) {
96252
97062
  /**
96253
97063
  * 本模块服务的线路。
96254
97064
  *
96255
- * 结果投影的正确性依赖这条线路的缓存语义(内容前缀命中),别的线路上把结果换成
96256
- * 预览只有坏处没有好处,所以这里写死一个 provider 名而不是做成开关。
97065
+ * Kimi 是缺少显式 provider 列表时的兼容回退;宿主正常接线会注入受控线路表。
97066
+ * 是否投影由当前设置决定,是否补读已有 ref 则由会话所有权与受支持路由决定。
96257
97067
  */
96258
97068
  const CACHE_WARMUP_PROVIDER = "kimi-code";
96259
97069
  /**
@@ -96385,9 +97195,9 @@ function claimMount(ctx) {
96385
97195
  * @param sessionId - 会话标识。
96386
97196
  * @returns 快照,或读不到时的 undefined。
96387
97197
  */
96388
- function readCacheWarmupSnapshot(sessionId) {
97198
+ function readCacheWarmupSnapshot(sessionId, provider) {
96389
97199
  try {
96390
- const raw = cacheWarmupSnapshot(sessionId);
97200
+ const raw = cacheWarmupSnapshot(sessionId, void 0, provider);
96391
97201
  return typeof raw === "object" && raw !== null ? raw : void 0;
96392
97202
  } catch {
96393
97203
  return;
@@ -96453,6 +97263,7 @@ function installCacheWarmup(ctx, options) {
96453
97263
  if (tools === void 0) return () => void 0;
96454
97264
  return tools.register(createCacheWarmupResultTool({
96455
97265
  provider: CACHE_WARMUP_PROVIDER,
97266
+ allowProvider: (provider) => CONTROLLED_PROVIDERS.includes(provider),
96456
97267
  routeOf: (exec) => cacheWarmupRouteOf(exec?.agent),
96457
97268
  store: storeOf,
96458
97269
  log
@@ -96474,12 +97285,13 @@ function installCacheWarmup(ctx, options) {
96474
97285
  * @param sessionId - 会话标识。
96475
97286
  * @param budget - 本轮读到的预算对象。
96476
97287
  */
96477
- const trackWarmup = (sessionId, budget) => {
96478
- const snapshot = readCacheWarmupSnapshot(sessionId);
97288
+ const trackWarmup = (sessionId, budget, provider) => {
97289
+ const snapshot = readCacheWarmupSnapshot(sessionId, provider);
96479
97290
  const warmup = booleanOf(snapshot?.warmup) ?? booleanOf(budget.warmup);
96480
97291
  const consecutiveHits = numberOf$1(snapshot?.consecutiveHits) ?? numberOf$1(budget.consecutiveHits);
96481
97292
  const hitRatio = numberOf$1(snapshot?.hitRatio) ?? numberOf$1(budget.hitRatio);
96482
- if (warmupState.get(sessionId) === true && warmup === false) {
97293
+ const scope = (provider ?? "kimi-code") + ":" + sessionId;
97294
+ if (warmupState.get(scope) === true && warmup === false) {
96483
97295
  const reason = exitReasonOf(consecutiveHits);
96484
97296
  log({
96485
97297
  event: "warmup-exit",
@@ -96489,7 +97301,7 @@ function installCacheWarmup(ctx, options) {
96489
97301
  ...hitRatio === void 0 ? {} : { hitRatio }
96490
97302
  });
96491
97303
  }
96492
- if (warmup !== void 0) warmupState.set(sessionId, warmup);
97304
+ if (warmup !== void 0) warmupState.set(scope, warmup);
96493
97305
  return {
96494
97306
  ...warmup === void 0 ? {} : { warmup },
96495
97307
  ...consecutiveHits === void 0 ? {} : { consecutiveHits },
@@ -96533,7 +97345,8 @@ function installCacheWarmup(ctx, options) {
96533
97345
  if (retrievalTool && navigation !== true) return decision;
96534
97346
  if (sourceTool && !settings.enabled && navigation !== true) return decision;
96535
97347
  const route = cacheWarmupRouteOf(exec.agent);
96536
- if (route.provider !== "kimi-code") return decision;
97348
+ const admittedProviders = settings.providers ?? ["kimi-code"];
97349
+ if (route.provider === void 0 || !admittedProviders.includes(route.provider)) return decision;
96537
97350
  const sessionId = route.sessionId;
96538
97351
  if (sessionId === void 0) return decision;
96539
97352
  const store = storeOf();
@@ -96541,11 +97354,11 @@ function installCacheWarmup(ctx, options) {
96541
97354
  const content = blocks;
96542
97355
  let durable;
96543
97356
  try {
96544
- durable = options.durableHint?.(sessionId);
97357
+ durable = options.durableHint?.(sessionId, route.provider);
96545
97358
  } catch {
96546
97359
  durable = void 0;
96547
97360
  }
96548
- const budget = durable === void 0 ? cacheWarmupBudget(sessionId) : cacheWarmupBudget(sessionId, Date.now(), durable);
97361
+ const budget = durable === void 0 ? cacheWarmupBudget(sessionId, Date.now(), void 0, route.provider) : cacheWarmupBudget(sessionId, Date.now(), durable, route.provider);
96549
97362
  const projected = await projectToolResult({
96550
97363
  toolName: exec.name,
96551
97364
  provider: route.provider,
@@ -96560,7 +97373,7 @@ function installCacheWarmup(ctx, options) {
96560
97373
  loggerOf()?.warn?.("[dsh-chatgpt-subscription] cache warmup projection skipped: " + cacheWarmupErrorLabel(error));
96561
97374
  });
96562
97375
  if (projected === void 0) return decision;
96563
- const metrics = trackWarmup(sessionId, budget);
97376
+ const metrics = trackWarmup(sessionId, budget, route.provider);
96564
97377
  log({
96565
97378
  event: "projection",
96566
97379
  session: sessionId,
@@ -96607,7 +97420,7 @@ function blocksOf(content) {
96607
97420
  /** Environment override for wait-time cache keepalive warmup. */
96608
97421
  const CACHE_WAIT_WARMUP_ENV = "DSH_KIMI_CODE_CACHE_WAIT_WARMUP";
96609
97422
  /** Settings description for the progress-updates / Kimi cache-wait switch. */
96610
- const CACHE_WAIT_WARMUP_SETTING_DESCRIPTION = "Kimi Code 专用:等待子代理完成期间,主会话每空闲满 20 分钟自动触发一次轻量缓存续热,只要子代理仍在运行就持续续热以防 25 分钟缓存过期重建;非 Kimi 线路开关无影响。";
97423
+ const CACHE_WAIT_WARMUP_SETTING_DESCRIPTION = "等待子代理完成期间按线路缓存 TTL 轻量续热;默认用于 Kimi(20/25 分钟),其他线路需明确 TTL 并在 promptCacheOverrides 中显式启用。";
96611
97424
  const MAX_TRACKED_SESSIONS = 256;
96612
97425
  /** Pure cache-wait policy; no child deadline or forced progress is involved. */
96613
97426
  function evaluateCacheWait(input) {
@@ -96678,7 +97491,8 @@ function defaultTimer() {
96678
97491
  }
96679
97492
  };
96680
97493
  }
96681
- function defaultActivity(sessionId) {
97494
+ function defaultActivity(sessionId, provider = "kimi-code") {
97495
+ if (provider !== "kimi-code") return promptCacheActivityStore(provider).read(sessionId);
96682
97496
  const durable = sessionCacheState.read(sessionId);
96683
97497
  const lastActiveAt = sessionLastActiveAt(sessionId) ?? durable?.lastActiveAt;
96684
97498
  if (lastActiveAt === void 0) return void 0;
@@ -96707,7 +97521,6 @@ var KimiCacheWaitController = class {
96707
97521
  warmup;
96708
97522
  logger;
96709
97523
  decisionMs;
96710
- ttlMs;
96711
97524
  minWarmupTokens;
96712
97525
  disposed = false;
96713
97526
  constructor(options = {}) {
@@ -96721,7 +97534,6 @@ var KimiCacheWaitController = class {
96721
97534
  this.warmup = typeof warmupOption === "function" ? warmupOption : () => warmupOption ?? true;
96722
97535
  this.logger = options.logger;
96723
97536
  this.decisionMs = options.decisionMs ?? 12e5;
96724
- this.ttlMs = options.ttlMs ?? 15e5;
96725
97537
  this.minWarmupTokens = options.minWarmupTokens ?? 0;
96726
97538
  }
96727
97539
  /** Observe one durable Session event. */
@@ -96854,15 +97666,15 @@ var KimiCacheWaitController = class {
96854
97666
  this.timer.clear(state.timer);
96855
97667
  state.timer = void 0;
96856
97668
  }
96857
- isKimiSession(session) {
96858
- if (this.kimiSession !== void 0) return this.kimiSession(session);
97669
+ providerForSession(session) {
97670
+ if (this.kimiSession !== void 0) return this.kimiSession(session) ? "kimi-code" : void 0;
96859
97671
  try {
96860
97672
  const current = text(session.requestHeader?.()?.config?.provider);
96861
- if (current !== void 0) return current === "kimi-code";
97673
+ if (current !== void 0) return current;
96862
97674
  } catch {}
96863
97675
  const latest = this.providerBySession.get(session.id);
96864
- if (latest !== void 0) return latest === "kimi-code";
96865
- return this.activity(session.id)?.lastActiveAt !== void 0;
97676
+ if (latest !== void 0) return latest;
97677
+ return this.activity(session.id, "kimi-code")?.lastActiveAt !== void 0 ? "kimi-code" : void 0;
96866
97678
  }
96867
97679
  isWarmupEnabled() {
96868
97680
  try {
@@ -96874,8 +97686,11 @@ var KimiCacheWaitController = class {
96874
97686
  reconcile(session, state) {
96875
97687
  this.clearTimer(state);
96876
97688
  if (this.disposed || !state.waiting || state.handled || state.children.size === 0) return;
96877
- if (!this.isKimiSession(session)) return;
96878
- const activity = this.activity(session.id);
97689
+ const provider = this.providerForSession(session);
97690
+ if (provider === void 0) return;
97691
+ const cachePolicy = promptCachePolicyOf(provider, this.options.overrides?.[provider]);
97692
+ if (!cachePolicy.waitWarmup || cachePolicy.ttlMs === void 0) return;
97693
+ const activity = this.activity(session.id, provider);
96879
97694
  if (activity === void 0 && state.lastWarmupAt === void 0) return;
96880
97695
  const effectiveLastActiveAt = Math.max(activity?.lastActiveAt ?? -Infinity, state.lastWarmupAt ?? -Infinity);
96881
97696
  if (!Number.isFinite(effectiveLastActiveAt)) return;
@@ -96888,8 +97703,8 @@ var KimiCacheWaitController = class {
96888
97703
  totalTokens: activity?.totalTokens,
96889
97704
  warmupEnabled: this.isWarmupEnabled(),
96890
97705
  warmupFailed: state.warmupFailed,
96891
- decisionMs: this.decisionMs,
96892
- ttlMs: this.ttlMs,
97706
+ decisionMs: provider === "kimi-code" ? this.decisionMs : Math.max(1, Math.floor(cachePolicy.ttlMs * .8)),
97707
+ ttlMs: cachePolicy.ttlMs,
96893
97708
  minWarmupTokens: this.minWarmupTokens
96894
97709
  });
96895
97710
  if (action.kind === "none") return;
@@ -96944,8 +97759,14 @@ function installKimiCacheWait(ctx, options = {}) {
96944
97759
  * schema(`src/index.ts` 的 `Config`)与设置卡共用同一份文案:两处各写一份,
96945
97760
  * 迟早变成「卡片说只影响新结果、schema 说立刻生效」这种自相矛盾。
96946
97761
  */
96947
- const CACHE_WARMUP_SETTING_DESCRIPTION = "Kimi Code 专用:启用缓存预热的结果投影——Kimi 线路上可以可靠结构化提取的超大工具结果会被替换成带 ref:// 引用的预览,需要原文时由 cache_warmup_result 精确补读。关闭只影响之后产生的新结果,已进入历史的内容不变。本开关只对 Kimi Code 线路生效,其他线路开着关着都没有区别。";
96948
- const CACHE_WARMUP_RETRIEVAL_SETTING_DESCRIPTION = "Kimi Code 专用:启用检索型结果投影——明确导航任务中的大文件、网页和搜索结果可显示为大纲或分布预览,原文由 cache_warmup_result 补读。编辑、审查和意图不明的读取保持原文。默认开启,只影响之后的新结果。本开关只对 Kimi Code 线路生效,其他线路开着关着都没有区别。";
97762
+ const CACHE_WARMUP_SETTING_DESCRIPTION = "按线路配置结果投影:可以可靠结构化提取的超大工具结果会被替换成带 ref:// 引用的预览,需要原文时由 cache_warmup_result 精确补读。关闭只影响之后产生的新结果,已进入历史的内容不变。";
97763
+ const CACHE_WARMUP_RETRIEVAL_SETTING_DESCRIPTION = "按线路配置检索型结果投影:明确导航任务中的大文件、网页和搜索结果可显示为大纲或分布预览,原文由 cache_warmup_result 补读。编辑、审查和意图不明的读取保持原文。默认开启,只影响之后的新结果。";
97764
+ /** 缺省设置由宿主注入 controlled provider 列表;独立使用时保持 Kimi 兼容。 */
97765
+ const DEFAULT_CACHE_WARMUP_SETTINGS = {
97766
+ enabled: true,
97767
+ retrievalEnabled: true,
97768
+ providers: ["kimi-code"]
97769
+ };
96949
97770
  /**
96950
97771
  * 读取一个配置字段,沿着 volatile 活引用取当前值。
96951
97772
  * @param field - 字段本身。
@@ -96967,9 +97788,12 @@ function readCacheWarmupConfigField(field) {
96967
97788
  function resolveCacheWarmupSettings(config) {
96968
97789
  const enabled = readCacheWarmupConfigField(config?.cacheWarmup);
96969
97790
  const retrievalEnabled = readCacheWarmupConfigField(config?.cacheWarmupRetrieval);
97791
+ const configuredProviders = readCacheWarmupConfigField(config?.cacheWarmupProviders);
97792
+ const providers = configuredProviders === void 0 ? DEFAULT_CACHE_WARMUP_SETTINGS.providers : [...new Set(configuredProviders.map((provider) => provider.trim()).filter((provider) => provider !== ""))];
96970
97793
  return {
96971
97794
  enabled: enabled !== false,
96972
- retrievalEnabled: retrievalEnabled !== false
97795
+ retrievalEnabled: retrievalEnabled !== false,
97796
+ providers
96973
97797
  };
96974
97798
  }
96975
97799
  //#endregion
@@ -105052,10 +105876,14 @@ const Config = z$1.object({
105052
105876
  ]).default("auto").volatile().description("进展更新的输出语言:auto 跟随对话语言,zh 中文,en 英文。"),
105053
105877
  planModePipeline: z$1.boolean().default(true).volatile().description("启用计划模式管线:允许调度模式在澄清访谈收敛后用 enter_plan_mode 进入计划模式,放弃规划时用 leave_plan_mode 退出。"),
105054
105878
  dispatchExecutionGuard: z$1.boolean().default(true).volatile().description("启用调度执行守卫:在执行期拦截子代理越权调用主控专用工具,并拦截等待子代理期间的纯 sleep 空等命令;不修改工具声明面,零损耗保护 Kimi 缓存。"),
105879
+ toolRunawayGuard: z$1.boolean().default(true).volatile().description("启用工具失控治理:重复调用同一工具与参数时分级提醒;Kimi 保留第 12 次强停,其他线路只提醒。"),
105055
105880
  teamPolicyCacheGuard: z$1.boolean().default(true).volatile().description("把内置 Agent Teams 的 team:policy 段从 system prompt 摘掉,改为开关切换时以尾部用户消息投递一次,避免中途开团队协作时刷掉整段前缀缓存;默认开启。"),
105056
105881
  cacheWarmup: z$1.boolean().default(true).volatile().description(CACHE_WARMUP_SETTING_DESCRIPTION),
105882
+ cacheWarmupProviders: z$1.array(z$1.string()).default(CONTROLLED_PROVIDERS).volatile().description("允许产生新结果预览的线路;默认覆盖全部受控线路,空数组表示关闭所有新投影。"),
105057
105883
  cacheWarmupRetrieval: z$1.boolean().default(true).volatile().description(CACHE_WARMUP_RETRIEVAL_SETTING_DESCRIPTION),
105058
105884
  cacheWaitWarmup: z$1.boolean().default(true).volatile().description(CACHE_WAIT_WARMUP_SETTING_DESCRIPTION),
105885
+ cacheAffinity: z$1.boolean().default(true).volatile().description(CACHE_AFFINITY_SETTING_DESCRIPTION),
105886
+ promptCacheOverrides: z$1.object({}).default({}).description("按线路覆盖缓存语义(ttlMs、scope、coldStart、affinity、waitWarmup 等);仅用于已有证据的部署,修改后重载插件。"),
105059
105887
  codexStreamIdleTimeoutMs: z$1.number().min(1).max(MAX_TIMER_DELAY_MS).step(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS$2),
105060
105888
  poolStorage: z$1.union([z$1.const("keyring"), z$1.const("credentials")]).default("keyring").description("号池存储后端:keyring(默认,桌面平台的系统密钥环)或 credentials(DSH 凭据库,服务器推荐)。")
105061
105889
  });
@@ -105068,6 +105896,7 @@ const inject = [
105068
105896
  "loader"
105069
105897
  ];
105070
105898
  function apply(ctx, pluginConfig = {}) {
105899
+ const promptCacheOverrides = parsePromptCacheOverrides(pluginConfig.promptCacheOverrides);
105071
105900
  installHostLocale(new HostLocaleRuntime());
105072
105901
  ctx.effect(() => () => installHostLocale(void 0), "dsh-chatgpt-subscription: host locale");
105073
105902
  const fetchConfiguration = resolveFetchConfiguration(pluginConfig);
@@ -105134,7 +105963,10 @@ function apply(ctx, pluginConfig = {}) {
105134
105963
  return () => guard.dispose();
105135
105964
  }, "dsh-chatgpt-subscription: compaction policy guard");
105136
105965
  ctx.effect(() => installMinimaxRuntimeEnhancements(ctx), "dsh-chatgpt-subscription: MiniMax runtime enhancements");
105137
- ctx.effect(() => installKimiRepeatBreaker(ctx), "dsh-chatgpt-subscription: Kimi repeat breaker");
105966
+ ctx.effect(() => installKimiRepeatBreaker(ctx, {
105967
+ providers: CONTROLLED_PROVIDERS.filter((provider) => provider !== "minimax-code"),
105968
+ globalEnabled: () => resolveToolRunawayGuardEnabled(pluginConfig)
105969
+ }), "dsh-chatgpt-subscription: shared tool runaway guard");
105138
105970
  ctx.effect(() => installDispatchExecutionGuard(ctx, { enabled: () => resolveDispatchExecutionGuardEnabled(pluginConfig) }), "dsh-chatgpt-subscription: dispatch execution guard");
105139
105971
  ctx.effect(() => installDispatchLedger(ctx, {
105140
105972
  enabled: () => resolveDispatchExecutionGuardEnabled(pluginConfig),
@@ -105247,7 +106079,8 @@ function apply(ctx, pluginConfig = {}) {
105247
106079
  }
105248
106080
  ctx.effect(() => installCacheWarmup(ctx, {
105249
106081
  settings: () => resolveCacheWarmupSettings(pluginConfig),
105250
- durableHint: (sessionId) => {
106082
+ durableHint: (sessionId, provider) => {
106083
+ if (provider !== void 0 && provider !== "kimi-code") return void 0;
105251
106084
  const record = sessionCacheState.read(sessionId);
105252
106085
  if (record === void 0) return void 0;
105253
106086
  const total = record.cachedTokens + record.freshTokens;
@@ -105259,7 +106092,10 @@ function apply(ctx, pluginConfig = {}) {
105259
106092
  }
105260
106093
  }), "dsh-chatgpt-subscription: cache warmup");
105261
106094
  ctx.effect(() => {
105262
- const dispose = installKimiCacheWait(ctx, { warmup: () => resolveCacheWaitWarmupEnabled(pluginConfig, process.env) });
106095
+ const dispose = installKimiCacheWait(ctx, {
106096
+ warmup: () => resolveCacheWaitWarmupEnabled(pluginConfig, process.env),
106097
+ overrides: promptCacheOverrides
106098
+ });
105263
106099
  if (dispose === void 0) {
105264
106100
  ctx.logger.warn("[dsh-chatgpt-subscription] Kimi cache wait skipped: this harness exposes no event bus");
105265
106101
  return () => void 0;
@@ -105304,7 +106140,8 @@ function apply(ctx, pluginConfig = {}) {
105304
106140
  label: PROVIDER_NAME$11,
105305
106141
  adapter: new AntigravityAdapter(antigravityStore, antigravityModelSettings, antigravityPreferences, {
105306
106142
  fetchFn: modelFetch.antigravity,
105307
- attachments: ctx.attachments
106143
+ attachments: ctx.attachments,
106144
+ promptCache: promptCacheOverrides.antigravity
105308
106145
  }, antigravityAccountPool)
105309
106146
  });
105310
106147
  const disposeAntigravityRoutes = registerAntigravityRoutes(ctx, antigravityStore, antigravityModelSettings, antigravityPreferences, antigravityProxyFetch, antigravityAccountPool);
@@ -105333,7 +106170,8 @@ function apply(ctx, pluginConfig = {}) {
105333
106170
  videos: { readVideo: async (ref) => ({
105334
106171
  data: await readVideoBytes(ref),
105335
106172
  mediaType: ref.mediaType
105336
- }) }
106173
+ }) },
106174
+ cacheAffinity: () => resolveCacheAffinityEnabled(pluginConfig, process.env)
105337
106175
  }, kimiCodeAccountPool)
105338
106176
  });
105339
106177
  const ollamaClaim = claimProviderRoute(ctx, {
@@ -105378,7 +106216,8 @@ function apply(ctx, pluginConfig = {}) {
105378
106216
  adapter: new MinimaxCodeAdapter(minimaxCodeStore, {
105379
106217
  fetchFn: modelFetch["minimax-code"],
105380
106218
  attachments: ctx.attachments,
105381
- accountPool: minimaxCodeAccountPool
106219
+ accountPool: minimaxCodeAccountPool,
106220
+ promptCache: promptCacheOverrides["minimax-code"]
105382
106221
  }, minimaxCodeModelSettings, minimaxCodePreferences)
105383
106222
  });
105384
106223
  let minimaxCodeAppVersion;
@@ -105415,7 +106254,8 @@ function apply(ctx, pluginConfig = {}) {
105415
106254
  adapter: new ClaudeAdapter(claudeStore, claudeModelSettings, claudePreferences, {
105416
106255
  fetchFn: modelFetch.claude,
105417
106256
  attachments: ctx.attachments,
105418
- accountPool: claudeAccountPool
106257
+ accountPool: claudeAccountPool,
106258
+ promptCache: promptCacheOverrides.claude
105419
106259
  })
105420
106260
  });
105421
106261
  const disposeClaudeRoutes = registerClaudeRoutes(ctx, claudeStore, claudeModelSettings, claudePreferences, {
@@ -105430,14 +106270,16 @@ function apply(ctx, pluginConfig = {}) {
105430
106270
  adapter: new StepCodeAdapter(stepCodeStore, stepCodeModelSettings, stepCodePreferences, {
105431
106271
  fetchFn: modelFetch[PROVIDER_ID$6],
105432
106272
  attachments: ctx.attachments,
105433
- accountPool: stepCodeAccountPool
106273
+ accountPool: stepCodeAccountPool,
106274
+ promptCache: promptCacheOverrides["step-code"]
105434
106275
  })
105435
106276
  });
105436
106277
  const disposeStepCodeRoutes = registerStepCodeRoutes(ctx, stepCodeSettingsSource(stepCodePreferences, stepCodeModelSettings), {
105437
106278
  fetchFn: proxyFetch,
105438
106279
  serving: () => stepCodeClaim.serving(),
105439
106280
  conflict: () => stepCodeClaim.conflict(),
105440
- accountPool: stepCodeAccountPool
106281
+ accountPool: stepCodeAccountPool,
106282
+ promptCache: promptCacheOverrides["step-code"]
105441
106283
  });
105442
106284
  const clinePassLine = createClinePassLine(ctx, {
105443
106285
  fetchFn: proxyFetch,
@@ -105551,7 +106393,8 @@ function apply(ctx, pluginConfig = {}) {
105551
106393
  outputVerbosity: () => preferences.status().outputVerbosity,
105552
106394
  fastMode: () => preferences.status().fastMode,
105553
106395
  reasoningSummary: () => preferences.status().reasoningSummary,
105554
- codexStreamIdleTimeoutMs: pluginConfig.codexStreamIdleTimeoutMs
106396
+ codexStreamIdleTimeoutMs: pluginConfig.codexStreamIdleTimeoutMs,
106397
+ promptCache: promptCacheOverrides["codex-chatgpt"]
105555
106398
  }), preferences, (options) => oauth.credentials(void 0, { purpose: "tool" }).then((credentials) => loadCodexCatalog(credentials, {
105556
106399
  fetchFn: proxyFetch,
105557
106400
  signal: options?.signal