@bitkyc08/opencodex 2.60.0 → 2.61.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 (249) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-CH6C5H7x.js +50 -0
  5. package/gui/dist/assets/Tray-CLZh48fM.js +1 -0
  6. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  7. package/gui/dist/assets/index-wpTOyepx.js +86 -0
  8. package/gui/dist/assets/usage-companion-chart-a0N58rRI.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +5 -1
  13. package/src/adapters/anthropic.ts +16 -0
  14. package/src/adapters/coding-agent/protocol.ts +36 -6
  15. package/src/adapters/coding-agent/turn.ts +10 -2
  16. package/src/adapters/command-code.ts +2 -1
  17. package/src/adapters/cursor/catalog.ts +51 -7
  18. package/src/adapters/cursor/protobuf-request.ts +6 -3
  19. package/src/adapters/cursor/request-builder.ts +13 -3
  20. package/src/adapters/cursor.ts +11 -2
  21. package/src/adapters/declaration-carrier.ts +45 -0
  22. package/src/adapters/devin.ts +75 -23
  23. package/src/adapters/google-antigravity-wire.ts +5 -2
  24. package/src/adapters/google-errors.ts +7 -1
  25. package/src/adapters/google.ts +29 -5
  26. package/src/adapters/image.ts +4 -1
  27. package/src/adapters/input-media-guard.ts +21 -9
  28. package/src/adapters/kiro/usage.ts +3 -2
  29. package/src/adapters/kiro-tool-fallback.ts +1 -1
  30. package/src/adapters/ollama-native.ts +6 -0
  31. package/src/adapters/openai-chat/developer-role.ts +61 -0
  32. package/src/adapters/openai-chat/messages.ts +46 -27
  33. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  34. package/src/adapters/openai-chat/passthrough.ts +33 -9
  35. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  36. package/src/adapters/openai-chat.ts +18 -57
  37. package/src/adapters/openai-responses/passthrough.ts +2 -0
  38. package/src/adapters/registry.ts +3 -2
  39. package/src/adapters/run-turn-queue.ts +178 -29
  40. package/src/adapters/xai-web-search.ts +16 -1
  41. package/src/bridge/errors.ts +8 -2
  42. package/src/bridge/response-json.ts +9 -1
  43. package/src/bridge/sse.ts +10 -0
  44. package/src/chat/inbound.ts +141 -5
  45. package/src/claude/desktop-3p.ts +7 -1
  46. package/src/claude/desktop-first-party.ts +183 -0
  47. package/src/claude/desktop-gateway-state.ts +41 -0
  48. package/src/claude/inbound-content-options.ts +6 -0
  49. package/src/claude/inbound.ts +32 -6
  50. package/src/claude/intercept/connect-proxy.ts +179 -0
  51. package/src/claude/intercept/listener.ts +122 -0
  52. package/src/claude/intercept/local-ca.ts +298 -0
  53. package/src/claude/intercept/runtime.ts +98 -0
  54. package/src/claude/intercept/settings.ts +189 -0
  55. package/src/cli/access.ts +87 -0
  56. package/src/cli/account-auth.ts +19 -0
  57. package/src/cli/capabilities.ts +31 -0
  58. package/src/cli/claude-desktop.ts +206 -16
  59. package/src/cli/codex-shim-autorestore.ts +3 -0
  60. package/src/cli/companion.ts +56 -0
  61. package/src/cli/dispatch.ts +43 -4
  62. package/src/cli/ensure-desired-integrations.ts +43 -5
  63. package/src/cli/help.ts +7 -9
  64. package/src/cli/index.ts +200 -61
  65. package/src/cli/init.ts +8 -0
  66. package/src/cli/integrations.ts +7 -1
  67. package/src/cli/registry.ts +41 -2
  68. package/src/cli/resolve.ts +230 -0
  69. package/src/cli/root.ts +24 -1
  70. package/src/cli/start-ownership-publication.ts +56 -0
  71. package/src/cli/status-probes.ts +2 -18
  72. package/src/cli/status.ts +62 -0
  73. package/src/cli/stop-report.ts +143 -0
  74. package/src/cli/uninstall-plan.ts +9 -0
  75. package/src/client/machine-listener.ts +2 -5
  76. package/src/clients/aside-profiles.ts +4 -0
  77. package/src/clients/config-export/zcode-store.ts +157 -0
  78. package/src/clients/config-export.ts +36 -0
  79. package/src/codex/app-server-processes.ts +72 -40
  80. package/src/codex/auth-api/login-flow.ts +6 -1
  81. package/src/codex/autostart-health.ts +28 -0
  82. package/src/codex/catalog/build-entries.ts +2 -2
  83. package/src/codex/catalog/effort.ts +3 -3
  84. package/src/codex/catalog/provider-models.ts +24 -15
  85. package/src/codex/catalog/retained-sync.ts +2 -2
  86. package/src/codex/convergence.ts +2 -2
  87. package/src/codex/history-provider.ts +12 -1
  88. package/src/codex/inject/config-toml.ts +41 -6
  89. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  90. package/src/codex/inject.ts +18 -15
  91. package/src/codex/injected-marker.ts +18 -0
  92. package/src/codex/main-account.ts +6 -0
  93. package/src/codex/model-cache.ts +52 -6
  94. package/src/codex/model-entitlement-admission.ts +59 -0
  95. package/src/codex/model-entitlements.ts +87 -44
  96. package/src/codex/native-main-admission.ts +83 -0
  97. package/src/codex/routing/health-store.ts +39 -0
  98. package/src/codex/routing/selection.ts +37 -1
  99. package/src/codex/routing.ts +5 -41
  100. package/src/codex/shim-templates.ts +29 -3
  101. package/src/companion/settings.ts +132 -0
  102. package/src/config/atomic-write.ts +117 -5
  103. package/src/config/load-degrade.ts +34 -7
  104. package/src/config/process-state.ts +1 -1
  105. package/src/config/schema/config-schema.ts +27 -1
  106. package/src/config/schema/leaf-validators.ts +47 -0
  107. package/src/config.ts +1 -1
  108. package/src/generated/compatibility-version.json +418 -174
  109. package/src/integrations/config-io.ts +44 -10
  110. package/src/integrations/merge.ts +120 -13
  111. package/src/integrations/mutation-plan.ts +124 -18
  112. package/src/integrations/registry.ts +38 -0
  113. package/src/integrations/state.ts +78 -45
  114. package/src/integrations/target.ts +208 -0
  115. package/src/integrations/writer.ts +49 -11
  116. package/src/lab/conformance/fixture-provider.ts +5 -0
  117. package/src/lib/browser-launch-notice.ts +59 -0
  118. package/src/lib/bun-runtime.ts +6 -2
  119. package/src/lib/debug.ts +40 -0
  120. package/src/lib/open-url.ts +51 -7
  121. package/src/lib/package-tree-integrity.ts +2 -1
  122. package/src/lib/package-version.ts +8 -0
  123. package/src/lib/provider-egress.ts +310 -0
  124. package/src/lib/provider-outbound.ts +59 -14
  125. package/src/lib/proxy-env.ts +82 -7
  126. package/src/lib/request-execution-budget.ts +72 -0
  127. package/src/lib/request-failure-attribution.ts +183 -0
  128. package/src/lib/request-failure-model.ts +236 -0
  129. package/src/lib/request-resend-gate.ts +138 -0
  130. package/src/lib/standalone.ts +16 -0
  131. package/src/lib/upstream-retry.ts +167 -16
  132. package/src/lib/winsw.ts +2 -2
  133. package/src/oauth/index.ts +24 -1
  134. package/src/oauth/login-cli.ts +80 -29
  135. package/src/providers/api-key-resolve.ts +133 -0
  136. package/src/providers/api-key-selection.ts +5 -1
  137. package/src/providers/key-failover.ts +31 -1
  138. package/src/providers/key-store.ts +34 -110
  139. package/src/providers/model-rename-fields.ts +147 -0
  140. package/src/providers/model-rename-migration.ts +124 -37
  141. package/src/providers/quota/vendor-probes-key.ts +37 -22
  142. package/src/providers/reasoning-metadata.ts +43 -18
  143. package/src/providers/registry/entries-core.ts +9 -4
  144. package/src/providers/registry/entries-extended.ts +29 -4
  145. package/src/providers/registry/model-seeds.ts +47 -10
  146. package/src/providers/xai-transport.ts +12 -1
  147. package/src/reasoning-effort.ts +8 -0
  148. package/src/responses/function-call-compat.ts +38 -1
  149. package/src/responses/inline-document.ts +65 -0
  150. package/src/responses/input-media.ts +42 -8
  151. package/src/responses/muse-tool-name-alias.ts +19 -0
  152. package/src/responses/parser-content.ts +8 -2
  153. package/src/responses/parser-tools.ts +3 -0
  154. package/src/responses/parser.ts +3 -1
  155. package/src/responses/schema.ts +3 -0
  156. package/src/router.ts +17 -2
  157. package/src/server/admission-model-scope.ts +219 -0
  158. package/src/server/audio-live.ts +9 -3
  159. package/src/server/audio-upstream.ts +18 -0
  160. package/src/server/auth-cors.ts +26 -0
  161. package/src/server/chat-completions.ts +55 -2
  162. package/src/server/chat-native.ts +19 -4
  163. package/src/server/claude-messages.ts +55 -17
  164. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  165. package/src/server/gui-freshness.ts +103 -0
  166. package/src/server/gui-static.ts +7 -9
  167. package/src/server/images.ts +59 -6
  168. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  169. package/src/server/index/serve-options.ts +56 -10
  170. package/src/server/index/spend-ledger-lifecycle.ts +34 -8
  171. package/src/server/index/startup-warnings.ts +24 -0
  172. package/src/server/index.ts +21 -28
  173. package/src/server/lifecycle.ts +4 -4
  174. package/src/server/live-call-bindings.ts +6 -0
  175. package/src/server/live.ts +88 -3
  176. package/src/server/management/agent-settings-routes.ts +121 -36
  177. package/src/server/management/companion-routes.ts +77 -0
  178. package/src/server/management/logs-usage-routes.ts +19 -0
  179. package/src/server/management/native-integration-routes.ts +103 -6
  180. package/src/server/management/oauth-account-routes.ts +45 -7
  181. package/src/server/management/route-registry.ts +6 -0
  182. package/src/server/management/shared.ts +18 -1
  183. package/src/server/management/usage-timeline-routes.ts +44 -0
  184. package/src/server/management-api.ts +8 -9
  185. package/src/server/proxy-liveness.ts +75 -0
  186. package/src/server/relay.ts +19 -2
  187. package/src/server/request-log-failure-attribution.ts +99 -0
  188. package/src/server/request-log.ts +114 -0
  189. package/src/server/request-metrics.ts +92 -30
  190. package/src/server/responses/codex-ws-wire.ts +34 -8
  191. package/src/server/responses/combo-stream-preflight.ts +168 -6
  192. package/src/server/responses/compact.ts +11 -0
  193. package/src/server/responses/core-opaque-recovery.ts +90 -0
  194. package/src/server/responses/fetch-helpers.ts +124 -8
  195. package/src/server/responses/input-admission.ts +10 -0
  196. package/src/server/responses/passthrough-delivery.ts +14 -1
  197. package/src/server/responses/passthrough-dispatch.ts +179 -35
  198. package/src/server/responses/passthrough-error.ts +27 -8
  199. package/src/server/responses/request-prepare.ts +42 -1
  200. package/src/server/responses/request-send-budget.ts +12 -0
  201. package/src/server/responses/request-transport.ts +24 -4
  202. package/src/server/responses/reset-replay.ts +108 -0
  203. package/src/server/responses-request-tool-scope.ts +214 -0
  204. package/src/server/responses-undeclared-tool-guard.ts +4 -1
  205. package/src/server/search.ts +25 -1
  206. package/src/server/usage-ledger-retention.ts +73 -0
  207. package/src/service/cli.ts +48 -2
  208. package/src/service/health.ts +3 -2
  209. package/src/service/install-state-contract.d.mts +27 -0
  210. package/src/service/install-state-contract.mjs +34 -0
  211. package/src/service/launchd.ts +1 -1
  212. package/src/service/orchestration.ts +2 -4
  213. package/src/service/ownership-compatibility.ts +164 -0
  214. package/src/service/ownership-mutation-lease.d.mts +32 -0
  215. package/src/service/ownership-mutation-lease.mjs +211 -0
  216. package/src/service/repair.ts +45 -1
  217. package/src/service/state-lock.ts +269 -0
  218. package/src/service/state-record.d.mts +36 -0
  219. package/src/service/state-record.mjs +138 -0
  220. package/src/service/state.ts +582 -68
  221. package/src/service/windows-taskxml.ts +11 -10
  222. package/src/service.ts +7 -3
  223. package/src/tray/windows-tray.ps1 +1 -1
  224. package/src/types/config.ts +37 -0
  225. package/src/types/provider.ts +73 -0
  226. package/src/types/request.ts +28 -2
  227. package/src/types/tools.ts +19 -0
  228. package/src/types.ts +3 -0
  229. package/src/update/index.ts +207 -63
  230. package/src/update/job.ts +9 -5
  231. package/src/update/ownership-transaction.ts +47 -0
  232. package/src/update/restart-ownership.ts +54 -0
  233. package/src/update/runtime-ownership.d.mts +40 -0
  234. package/src/update/runtime-ownership.mjs +122 -0
  235. package/src/usage/attempt-delivery.ts +198 -0
  236. package/src/usage/cache-diagnostic.ts +305 -0
  237. package/src/usage/failure-fingerprint.ts +118 -0
  238. package/src/usage/failure-projection-cache.ts +174 -0
  239. package/src/usage/failure-projection.ts +174 -0
  240. package/src/usage/ledger-retention.ts +165 -0
  241. package/src/usage/log.ts +126 -79
  242. package/src/usage/request-outcome.ts +150 -0
  243. package/src/usage/retention-contract.ts +28 -0
  244. package/src/usage/summary.ts +2 -2
  245. package/src/usage/telemetry-contract.ts +237 -0
  246. package/src/usage/timeline.ts +236 -0
  247. package/src/web-search/alpha-search.ts +21 -1
  248. package/gui/dist/assets/index-BTuCbqQd.css +0 -1
  249. package/gui/dist/assets/index-DoBVdPHP.js +0 -134
@@ -19,6 +19,7 @@
19
19
  // Everything else in the row is left alone.
20
20
 
21
21
  import { PROVIDER_REGISTRY } from "./registry";
22
+ import { MODEL_KEYED_RECORDS, MODEL_ID_LISTS, MODEL_ID_SCALARS, MODEL_NESTED_RECORDS } from "./model-rename-fields";
22
23
  import { providerConfigSeed } from "./derive";
23
24
  import type { OcxConfig, OcxProviderConfig } from "../types";
24
25
 
@@ -45,6 +46,52 @@ export interface ModelRename {
45
46
  * but lose the reasoning picker entirely.
46
47
  */
47
48
  dropReasoningEffortMap?: boolean;
49
+ /**
50
+ * Remove both the retired id and its replacement from `noReasoningModels`.
51
+ *
52
+ * Use this only when the rename also marks a capability change: carrying the old
53
+ * no-reasoning classification onto a newly adjustable alias would keep the picker
54
+ * disabled after the model id itself was repaired.
55
+ */
56
+ dropNoReasoningModels?: boolean;
57
+ /** Refresh exact registry defaults already saved under the replacement id. */
58
+ targetSeedRefresh?: {
59
+ contextWindow?: { from: number; to: number };
60
+ reasoning?: {
61
+ fromEfforts: readonly string[];
62
+ toEfforts: readonly string[];
63
+ defaultEffort: string;
64
+ effortMap: Readonly<Record<string, string>>;
65
+ };
66
+ };
67
+ }
68
+
69
+ const KIMI_K28_ALIAS = "kimi-for-coding";
70
+ const KIMI_RETIRED_CODING_IDS = [
71
+ "kimi-k2.7-code",
72
+ "kimi-k2.7-code-highspeed",
73
+ "kimi-k2.6",
74
+ "kimi-k2.5",
75
+ ] as const;
76
+ const KIMI_K28_TARGET_SEED_REFRESH: NonNullable<ModelRename["targetSeedRefresh"]> = {
77
+ contextWindow: { from: 262_144, to: 1_048_576 },
78
+ reasoning: {
79
+ fromEfforts: [],
80
+ toEfforts: ["low", "high", "max"],
81
+ defaultEffort: "max",
82
+ effortMap: { none: "none", low: "low", medium: "high", high: "high", xhigh: "max", max: "max" },
83
+ },
84
+ };
85
+
86
+ function kimiCodingRenames(provider: "kimi" | "kimi-code", endpoint: string): ModelRename[] {
87
+ return KIMI_RETIRED_CODING_IDS.map(from => ({
88
+ provider,
89
+ from,
90
+ to: KIMI_K28_ALIAS,
91
+ reason: `Moonshot retired the k2.x coding ids from the ${endpoint}; kimi-for-coding is the stable alias the endpoint still serves`,
92
+ dropNoReasoningModels: true,
93
+ targetSeedRefresh: KIMI_K28_TARGET_SEED_REFRESH,
94
+ }));
48
95
  }
49
96
 
50
97
  /**
@@ -65,6 +112,14 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
65
112
  to: "qwen3.8-max",
66
113
  reason: "Alibaba shipped Qwen3.8-Max as stable and documents the preview endpoint as liable to be taken offline once preview concludes",
67
114
  },
115
+ // Kimi coding renames. Moonshot retired the k2.x ids from the subscription/coding
116
+ // endpoint when K2.8 Preview shipped (live /coding/v1/models lists only
117
+ // kimi-for-coding[-highspeed], k3, k3-256k); kimi-for-coding is the stable alias the
118
+ // endpoint still serves and currently routes to K2.8 Preview. The registry picker no
119
+ // longer seeds the retired ids, so a saved defaultModel naming one is a dead selection
120
+ // rather than a merely outdated one.
121
+ ...kimiCodingRenames("kimi", "subscription endpoint after K2.8 Preview shipped"),
122
+ ...kimiCodingRenames("kimi-code", "coding endpoint after K2.8 Preview shipped"),
68
123
  // Antigravity Flash generations. Google takes the previous Flash model off Cloud Code
69
124
  // Assist almost immediately when the next ships, so a saved 3.6 (or older 3.5) id is a
70
125
  // dead selection rather than a merely outdated one. Routing already redirects these ids
@@ -83,40 +138,6 @@ export const MODEL_RENAMES: readonly ModelRename[] = [
83
138
  })),
84
139
  ];
85
140
 
86
- /** Provider fields that key metadata by model id. */
87
- const MODEL_KEYED_RECORDS = [
88
- "modelContextWindows",
89
- "modelMaxOutputTokens",
90
- "modelInputModalities",
91
- "modelReasoningEfforts",
92
- "modelSuppressSyntheticMax",
93
- "modelDefaultReasoningEfforts",
94
- "modelReasoningEffortMap",
95
- ] as const;
96
-
97
- /** Provider fields that are flat lists of model ids. */
98
- const MODEL_ID_LISTS = [
99
- "models",
100
- // A retired id left here is worse than a stale label: `filterCatalogModels` treats
101
- // `selectedModels` as an exact-match allowlist, so a user who allowlisted only the
102
- // retired model gets NO replacement row at all — the model silently vanishes from
103
- // their catalog instead of being renamed. OAuth reconciliation does not cover this
104
- // field, so the rename has to.
105
- "selectedModels",
106
- // Same reasoning as `selectedModels`: a retired id pinned here would be resurrected as a
107
- // ghost row on every discovery instead of following the rename (#1690).
108
- "retainModels",
109
- "noVisionModels",
110
- "noReasoningModels",
111
- "noTemperatureModels",
112
- "noTopPModels",
113
- "noPenaltyModels",
114
- "autoToolChoiceOnlyModels",
115
- "preserveReasoningContentModels",
116
- "thinkingBudgetModels",
117
- "directReasoningEffortModels",
118
- ] as const;
119
-
120
141
  function renameInList(value: unknown, from: string, to: string): string[] | null {
121
142
  if (!Array.isArray(value) || !value.includes(from)) return null;
122
143
  const seen = new Set<string>();
@@ -133,6 +154,18 @@ function renameInList(value: unknown, from: string, to: string): string[] | null
133
154
  return next;
134
155
  }
135
156
 
157
+ function dropRenamedIdsFromList(
158
+ value: unknown,
159
+ from: string,
160
+ to: string,
161
+ dropStaleTarget: boolean,
162
+ ): string[] | null {
163
+ if (!Array.isArray(value)) return null;
164
+ const hasRetired = value.includes(from);
165
+ if (!hasRetired && !(dropStaleTarget && value.includes(to))) return null;
166
+ return value.filter(entry => typeof entry === "string" && entry !== from && entry !== to);
167
+ }
168
+
136
169
  function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
137
170
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
138
171
  const record = value as Record<string, unknown>;
@@ -160,6 +193,42 @@ function dropFromRecord(value: unknown, from: string): Record<string, unknown> |
160
193
  return next;
161
194
  }
162
195
 
196
+ function sameStringArray(value: unknown, expected: readonly string[]): boolean {
197
+ return Array.isArray(value)
198
+ && value.length === expected.length
199
+ && value.every((entry, index) => entry === expected[index]);
200
+ }
201
+
202
+ function targetReasoningMatchesStaleSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
203
+ const reasoning = rename.targetSeedRefresh?.reasoning;
204
+ const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
205
+ return !!reasoning && !!efforts && sameStringArray(efforts[rename.to], reasoning.fromEfforts);
206
+ }
207
+
208
+ /** Refresh only exact defaults emitted by the previous registry; preserve user overrides. */
209
+ function refreshTargetSeed(row: Record<string, unknown>, rename: ModelRename): boolean {
210
+ const refresh = rename.targetSeedRefresh;
211
+ if (!refresh) return false;
212
+ let changed = false;
213
+
214
+ const windows = row.modelContextWindows as Record<string, unknown> | undefined;
215
+ if (windows && refresh.contextWindow && windows[rename.to] === refresh.contextWindow.from) {
216
+ windows[rename.to] = refresh.contextWindow.to;
217
+ changed = true;
218
+ }
219
+
220
+ const reasoning = refresh.reasoning;
221
+ const efforts = row.modelReasoningEfforts as Record<string, unknown> | undefined;
222
+ if (!reasoning || !efforts || !sameStringArray(efforts[rename.to], reasoning.fromEfforts)) return changed;
223
+ efforts[rename.to] = [...reasoning.toEfforts];
224
+
225
+ const defaults = (row.modelDefaultReasoningEfforts ??= {}) as Record<string, unknown>;
226
+ if (!(rename.to in defaults)) defaults[rename.to] = reasoning.defaultEffort;
227
+ const maps = (row.modelReasoningEffortMap ??= {}) as Record<string, unknown>;
228
+ if (!(rename.to in maps)) maps[rename.to] = { ...reasoning.effortMap };
229
+ return true;
230
+ }
231
+
163
232
  /**
164
233
  * `provider/model` rows in the top-level `disabledModels` list.
165
234
  *
@@ -281,7 +350,14 @@ export function projectModelRenames(
281
350
  let touched = false;
282
351
  for (const field of MODEL_ID_LISTS) {
283
352
  if (isRegistryResidue(seed, field, row[field], rename)) continue;
284
- const next = renameInList(row[field], rename.from, rename.to);
353
+ const next = rename.dropNoReasoningModels && field === "noReasoningModels"
354
+ ? dropRenamedIdsFromList(
355
+ row[field],
356
+ rename.from,
357
+ rename.to,
358
+ targetReasoningMatchesStaleSeed(row, rename),
359
+ )
360
+ : renameInList(row[field], rename.from, rename.to);
285
361
  if (!next) continue;
286
362
  row[field] = next;
287
363
  touched = true;
@@ -295,11 +371,22 @@ export function projectModelRenames(
295
371
  row[field] = next;
296
372
  touched = true;
297
373
  }
298
- if (prov.defaultModel === rename.from) {
299
- prov.defaultModel = rename.to;
374
+ for (const field of MODEL_NESTED_RECORDS) {
375
+ const container = row[field];
376
+ if (!container || typeof container !== "object" || Array.isArray(container)) continue;
377
+ const nested = container as Record<string, unknown>;
378
+ const next = renameInRecord(nested.models, rename.from, rename.to);
379
+ if (!next) continue;
380
+ row[field] = { ...nested, models: next };
381
+ touched = true;
382
+ }
383
+ for (const field of MODEL_ID_SCALARS) {
384
+ if (row[field] !== rename.from) continue;
385
+ row[field] = rename.to;
300
386
  touched = true;
301
387
  }
302
388
  if (renameDisabledModels(config, rename)) touched = true;
389
+ if (touched && refreshTargetSeed(row, rename)) touched = true;
303
390
 
304
391
  if (touched) {
305
392
  changed = true;
@@ -1,6 +1,8 @@
1
1
  import { resolveProviderApiKey } from "../key-store";
2
2
  import { getProviderRegistryEntry, registryEntryForProviderDestination } from "../registry";
3
3
  import { isCanonicalOllamaCloudUrl } from "../../adapters/ollama-native-url";
4
+ import { providerEgressFetchInit, resolveProviderEgress } from "../../lib/provider-egress";
5
+ import { configuredOutboundFetch } from "../../lib/proxy-env";
4
6
  import { QUOTA_JSON_READ_FAILURE, asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
5
7
  import {
6
8
  AUTHORITATIVE_EMPTY_QUOTA,
@@ -15,6 +17,12 @@ import { getTokenForAccountQuotaProbe } from "./account-cache";
15
17
  import type { AccountQuotaMode, ProviderQuota, ProviderQuotaCreditsUsd } from "../quota-types";
16
18
  import type { OcxProviderConfig } from "../../types";
17
19
 
20
+ // A quota probe must use the provider's inference route so it neither reports a false healthy path nor leaks a key through another exit.
21
+ async function quotaFetch(providerName: string, config: OcxProviderConfig, url: string, init: RequestInit): Promise<Response> {
22
+ const egress = resolveProviderEgress({ providerName, provider: config, url });
23
+ return configuredOutboundFetch(url, { ...init, ...providerEgressFetchInit(egress) });
24
+ }
25
+
18
26
  const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
19
27
  const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
20
28
  const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
@@ -144,10 +152,10 @@ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Pro
144
152
  if (!apiKey) return null;
145
153
  const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
146
154
  const [subscriptionResponse, tokenResponse] = await Promise.all([
147
- fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
155
+ quotaFetch(provider, config, `${A6API_BASE_URL}/dashboard/billing/subscription`, {
148
156
  headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
149
157
  }),
150
- fetch(`${A6API_BASE_URL}/api/usage/token/`, {
158
+ quotaFetch(provider, config, `${A6API_BASE_URL}/api/usage/token/`, {
151
159
  headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
152
160
  }),
153
161
  ]);
@@ -241,7 +249,7 @@ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig)
241
249
  if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
242
250
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
243
251
  if (!apiKey) return null;
244
- const response = await fetch(OPENCODE_GO_USAGE_URL, {
252
+ const response = await quotaFetch(provider, config, OPENCODE_GO_USAGE_URL, {
245
253
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
246
254
  redirect: "error",
247
255
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -287,7 +295,7 @@ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig)
287
295
  if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
288
296
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
289
297
  if (!apiKey) return null;
290
- const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
298
+ const response = await quotaFetch(provider, config, `${OPENROUTER_BASE_URL}/key`, {
291
299
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
292
300
  redirect: "error",
293
301
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -337,7 +345,7 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
337
345
  if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
338
346
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
339
347
  if (!apiKey) return null;
340
- const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
348
+ const response = await quotaFetch(provider, config, `${DEEPSEEK_BASE_URL}/user/balance`, {
341
349
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
342
350
  redirect: "error",
343
351
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -382,7 +390,7 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
382
390
  if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
383
391
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
384
392
  if (!apiKey) return null;
385
- const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
393
+ const response = await quotaFetch(provider, config, `${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
386
394
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
387
395
  redirect: "error",
388
396
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -480,7 +488,7 @@ async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig
480
488
  if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
481
489
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
482
490
  if (!apiKey) return null;
483
- const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
491
+ const response = await quotaFetch(provider, config, OLLAMA_CLOUD_USAGE_URL, {
484
492
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
485
493
  redirect: "error",
486
494
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -604,7 +612,7 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
604
612
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
605
613
  if (!apiKey) return null;
606
614
  const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
607
- const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
615
+ const response = await quotaFetch(provider, config, `${monitorHost}/api/monitor/usage/quota/limit`, {
608
616
  headers: { Accept: "application/json", Authorization: authorization },
609
617
  redirect: "error",
610
618
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -652,7 +660,7 @@ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): P
652
660
  if (!apiKey) return null;
653
661
  const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
654
662
  const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
655
- const response = await fetch(remainsUrl, {
663
+ const response = await quotaFetch(provider, config, remainsUrl, {
656
664
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
657
665
  redirect: "error",
658
666
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -695,7 +703,7 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
695
703
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
696
704
  if (!apiKey) return null;
697
705
  const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
698
- const response = await fetch(`${host}/users/me/balance`, {
706
+ const response = await quotaFetch(provider, config, `${host}/users/me/balance`, {
699
707
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
700
708
  redirect: "error",
701
709
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -738,7 +746,7 @@ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Pr
738
746
  if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
739
747
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
740
748
  if (!apiKey) return null;
741
- const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
749
+ const response = await quotaFetch(provider, config, `${VENICE_BASE_URL}/billing/balance`, {
742
750
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
743
751
  redirect: "error",
744
752
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -781,7 +789,7 @@ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig):
781
789
  if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
782
790
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
783
791
  if (!apiKey) return null;
784
- const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
792
+ const response = await quotaFetch(provider, config, `${SYNTHETIC_BASE_URL}/quotas`, {
785
793
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
786
794
  redirect: "error",
787
795
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -831,7 +839,7 @@ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig):
831
839
  if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
832
840
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
833
841
  if (!apiKey) return null;
834
- const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
842
+ const response = await quotaFetch(provider, config, `${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
835
843
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
836
844
  redirect: "error",
837
845
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -873,7 +881,7 @@ async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig)
873
881
  if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
874
882
  const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
875
883
  if (!apiKey) return null;
876
- const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
884
+ const response = await quotaFetch(provider, config, `${NEURALWATT_BASE_URL}/quota`, {
877
885
  headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
878
886
  redirect: "error",
879
887
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1050,7 +1058,7 @@ export async function fetchKimiQuota(provider: string, config: OcxProviderConfig
1050
1058
  // Never release credentials to a user-edited or lookalike provider host.
1051
1059
  if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
1052
1060
  if (!accessToken) return null;
1053
- const response = await fetch(KIMI_CODE_USAGE_URL, {
1061
+ const response = await quotaFetch(provider, config, KIMI_CODE_USAGE_URL, {
1054
1062
  headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
1055
1063
  redirect: "error",
1056
1064
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1077,9 +1085,14 @@ function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: nu
1077
1085
  }
1078
1086
 
1079
1087
  /** Soft-fail GET returning a parsed record, or null when unavailable. */
1080
- async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
1088
+ async function fetchCommandCodeJson(
1089
+ provider: string,
1090
+ config: OcxProviderConfig,
1091
+ url: string,
1092
+ bearer: string,
1093
+ ): Promise<Record<string, unknown> | null> {
1081
1094
  try {
1082
- const response = await fetch(url, {
1095
+ const response = await quotaFetch(provider, config, url, {
1083
1096
  headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1084
1097
  redirect: "error",
1085
1098
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1097,12 +1110,14 @@ async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record
1097
1110
  * pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
1098
1111
  */
1099
1112
  async function fetchCommandCodeSpend(
1113
+ provider: string,
1114
+ config: OcxProviderConfig,
1100
1115
  bearer: string,
1101
1116
  credits: Record<string, unknown> | null,
1102
1117
  orgQuery: string,
1103
1118
  ): Promise<ProviderQuotaCreditsUsd | undefined> {
1104
1119
  if (!credits) return undefined;
1105
- const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1120
+ const subscriptionBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
1106
1121
  const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
1107
1122
  const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
1108
1123
  // Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
@@ -1110,7 +1125,7 @@ async function fetchCommandCodeSpend(
1110
1125
  if (!periodStart) return undefined;
1111
1126
  const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
1112
1127
  const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
1113
- const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1128
+ const summaryBody = await fetchCommandCodeJson(provider, config, `${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
1114
1129
  const summary = asRecord(summaryBody?.data) ?? summaryBody;
1115
1130
  const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
1116
1131
  if (used === undefined || used < 0) return undefined;
@@ -1161,12 +1176,12 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
1161
1176
  // Never release credentials to a user-edited or lookalike provider host.
1162
1177
  if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
1163
1178
  if (!bearer) return null;
1164
- const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
1179
+ const whoamiBody = await fetchCommandCodeJson(provider, config, COMMAND_CODE_WHOAMI_URL, bearer);
1165
1180
  const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
1166
1181
  const org = asRecord(whoami?.org);
1167
1182
  const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
1168
1183
  const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
1169
- const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1184
+ const response = await quotaFetch(provider, config, `${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
1170
1185
  headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
1171
1186
  redirect: "error",
1172
1187
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1183,7 +1198,7 @@ export async function fetchCommandCodeQuota(provider: string, config: OcxProvide
1183
1198
  if (!credits && !limits) return null;
1184
1199
  const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
1185
1200
  const weekly = parseCommandCodeWindow(limits?.weekly);
1186
- const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
1201
+ const creditsUsd = await fetchCommandCodeSpend(provider, config, bearer, credits, orgQuery);
1187
1202
  const quota: ProviderQuota = {
1188
1203
  ...(fiveHour ? {
1189
1204
  fiveHourPercent: fiveHour.percent,
@@ -18,6 +18,7 @@
18
18
  * entitlement gap (muse-spark max needs an active Muse Code subscription) costs one rejected
19
19
  * request instead of failing every turn that selects that rung.
20
20
  */
21
+ import { createHash } from "node:crypto";
21
22
  import { existsSync, readFileSync } from "node:fs";
22
23
  import { join } from "node:path";
23
24
  // Leaf modules on purpose: this file is imported from reasoning-effort.ts, which combos/types.ts
@@ -26,6 +27,7 @@ import { join } from "node:path";
26
27
  import { atomicWriteFile } from "../config/atomic-write";
27
28
  import { getConfigDir } from "../config/paths";
28
29
  import type { OcxProviderConfig } from "../types";
30
+ import { resolveProviderApiKey } from "./api-key-resolve";
29
31
 
30
32
  const FILENAME = "reasoning-metadata-cache.json";
31
33
  const SUPPORT_FILENAME = "reasoning-support-cache.json";
@@ -87,7 +89,7 @@ interface MetadataSnapshot {
87
89
  }
88
90
 
89
91
  interface SupportSnapshot {
90
- version: 1;
92
+ version: 2;
91
93
  rows: Record<string, { effort: string; at: number; evidence?: string }>;
92
94
  }
93
95
 
@@ -159,11 +161,31 @@ function modelLadderValue(
159
161
  return undefined;
160
162
  }
161
163
 
162
- /** Opaque row key: providerKey|modelId|effort. None of the three may contain a pipe. */
163
- const KEY_SEP = "|";
164
+ /**
165
+ * Bind learned capability to the credential that supplied the evidence. A digest keeps the
166
+ * credential itself out of the persisted cache while remaining stable across restarts and key
167
+ * selection. Providers without key-auth identity may use metadata, but cannot teach the cache.
168
+ *
169
+ * The hash input is the resolved wire credential, not the configured expression: the catalog
170
+ * path carries the raw config string in apiKey (a keychain:/env reference stays unresolved
171
+ * there), while the request path carries the resolved secret in apiKey and the configured
172
+ * expression in _apiKeyAttempt.reference. The request path hashes apiKey exactly as routed --
173
+ * the reference is only provenance, and re-resolving it at record time could read a credential
174
+ * rotated since the request was served. The catalog path resolves the configured expression,
175
+ * so both sides still bind learned refusals to the same wire credential, and a rotation behind
176
+ * a stable reference starts clean instead of inheriting the previous credential's refusals.
177
+ */
178
+ function credentialIdentity(provider: OcxProviderConfig): string | undefined {
179
+ const resolved = provider._apiKeyAttempt?.reference !== undefined
180
+ ? provider.apiKey
181
+ : resolveProviderApiKey(provider.apiKey);
182
+ if (typeof resolved !== "string" || resolved.length === 0) return undefined;
183
+ return createHash("sha256").update(resolved).digest("hex");
184
+ }
164
185
 
165
- function supportKey(providerKey: string, modelId: string, effort: string): string {
166
- return providerKey + KEY_SEP + modelId + KEY_SEP + effort;
186
+ /** JSON encoding avoids delimiter ambiguity in provider, model, and effort identifiers. */
187
+ function supportKey(providerKey: string, credential: string, modelId: string, effort: string): string {
188
+ return JSON.stringify([providerKey, credential, modelId, effort]);
167
189
  }
168
190
 
169
191
  function loadSnapshot(): MetadataSnapshot | null {
@@ -220,7 +242,9 @@ function loadSupport(): Map<string, number> {
220
242
  }
221
243
  const rows = new Map<string, number>();
222
244
  const parsed = readJsonFile<SupportSnapshot>(SUPPORT_FILENAME);
223
- if (parsed && parsed.version === 1 && parsed.rows && typeof parsed.rows === "object") {
245
+ // Version 1 rows had no credential identity and are deliberately invalidated: accepting them
246
+ // would preserve destination-wide refusals written by a lower-entitlement account.
247
+ if (parsed && parsed.version === 2 && parsed.rows && typeof parsed.rows === "object") {
224
248
  for (const [key, row] of Object.entries(parsed.rows)) {
225
249
  if (!row || typeof row.at !== "number") continue;
226
250
  if (nowMs - row.at > SUPPORT_TTL_MS) continue;
@@ -273,8 +297,9 @@ export function metadataDeclaresType(provider: OcxProviderConfig, modelId: strin
273
297
 
274
298
  export function isReasoningEffortLearnedUnsupported(provider: OcxProviderConfig, modelId: string, effort: string): boolean {
275
299
  const key = metadataProviderKey(provider);
276
- if (!key) return false;
277
- return loadSupport().has(supportKey(key, modelId, effort));
300
+ const credential = credentialIdentity(provider);
301
+ if (!key || !credential) return false;
302
+ return loadSupport().has(supportKey(key, credential, modelId, effort));
278
303
  }
279
304
 
280
305
  /**
@@ -291,10 +316,11 @@ export function dropLearnedUnsupportedReasoningEfforts(
291
316
  ): string[] {
292
317
  if (efforts.length === 0) return [...efforts];
293
318
  const key = metadataProviderKey(provider);
294
- if (!key) return [...efforts];
319
+ const credential = credentialIdentity(provider);
320
+ if (!key || !credential) return [...efforts];
295
321
  const support = loadSupport();
296
322
  if (support.size === 0) return [...efforts];
297
- const kept = efforts.filter(effort => !support.has(supportKey(key, modelId, effort)));
323
+ const kept = efforts.filter(effort => !support.has(supportKey(key, credential, modelId, effort)));
298
324
  return kept.length === 0 ? [...efforts] : kept;
299
325
  }
300
326
 
@@ -334,8 +360,9 @@ export function recordUnsupportedReasoningEffort(
334
360
  evidence?: string,
335
361
  ): boolean {
336
362
  const key = metadataProviderKey(provider);
337
- if (!key || !effort) return false;
338
- const rowKey = supportKey(key, modelId, effort);
363
+ const credential = credentialIdentity(provider);
364
+ if (!key || !credential || !effort) return false;
365
+ const rowKey = supportKey(key, credential, modelId, effort);
339
366
  const rows = loadSupport();
340
367
  if (rows.has(rowKey)) return false;
341
368
  rows.set(rowKey, Date.now());
@@ -346,15 +373,14 @@ export function recordUnsupportedReasoningEffort(
346
373
  try {
347
374
  const out: SupportSnapshot["rows"] = {};
348
375
  for (const [rowKey, at] of rows) {
349
- const parts = rowKey.split(KEY_SEP);
350
376
  const evidenceText = supportEvidence.get(rowKey);
351
377
  out[rowKey] = {
352
- effort: parts[2] ?? "",
378
+ effort: JSON.parse(rowKey)[3] ?? "",
353
379
  at,
354
380
  ...(evidenceText ? { evidence: evidenceText } : {}),
355
381
  };
356
382
  }
357
- atomicWriteFile(join(getConfigDir(), SUPPORT_FILENAME), JSON.stringify({ version: 1, rows: out }) + "\n");
383
+ atomicWriteFile(join(getConfigDir(), SUPPORT_FILENAME), JSON.stringify({ version: 2, rows: out }) + "\n");
358
384
  } catch {
359
385
  // Best-effort persistence only.
360
386
  }
@@ -371,11 +397,10 @@ export function flushReasoningSupportCache(): void {
371
397
  const rows = loadSupport();
372
398
  const out: SupportSnapshot["rows"] = {};
373
399
  for (const [rowKey, at] of rows) {
374
- const parts = rowKey.split(KEY_SEP);
375
400
  const evidenceText = supportEvidence.get(rowKey);
376
- out[rowKey] = { effort: parts[2] ?? "", at, ...(evidenceText ? { evidence: evidenceText } : {}) };
401
+ out[rowKey] = { effort: JSON.parse(rowKey)[3] ?? "", at, ...(evidenceText ? { evidence: evidenceText } : {}) };
377
402
  }
378
- atomicWriteFile(join(getConfigDir(), SUPPORT_FILENAME), JSON.stringify({ version: 1, rows: out }) + "\n");
403
+ atomicWriteFile(join(getConfigDir(), SUPPORT_FILENAME), JSON.stringify({ version: 2, rows: out }) + "\n");
379
404
  } catch {
380
405
  // Best-effort persistence only.
381
406
  }
@@ -55,7 +55,7 @@ import {
55
55
  deepseekThinkingEffortsFor,
56
56
  deepseekReasoningMapFor,
57
57
  KIMI_K3_STANDARD_CONTEXT_WINDOW,
58
- KIMI_CODING_MODELS,
58
+ KIMI_CODING_LIVE_MODELS,
59
59
  KIMI_THINKING_MODELS,
60
60
  KIMI_CODING_NO_REASONING_MODELS,
61
61
  KIMI_CODING_K3_REASONING_EFFORTS,
@@ -444,8 +444,14 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
444
444
  oauthId: "kimi",
445
445
  jawcodeBundle: "moonshot",
446
446
  note: "Log in with your Kimi account",
447
- models: KIMI_CODING_MODELS,
448
- defaultModel: "kimi-k2.7-code",
447
+ // 260921: the retired k2.x ids stay out of the picker — live /coding/v1/models lists
448
+ // only kimi-for-coding[-highspeed], k3, k3-256k. Saved rows still naming kimi-k2.7-code
449
+ // are repaired by MODEL_RENAMES in model-rename-migration.ts.
450
+ models: KIMI_CODING_LIVE_MODELS,
451
+ // 260921: kimi-k2.7-code was retired from the subscription endpoint (live /models lists
452
+ // only kimi-for-coding[-highspeed], k3, k3-256k). The kimi-for-coding alias is the
453
+ // stable ID and currently routes to K2.8 Preview.
454
+ defaultModel: "kimi-for-coding",
449
455
  modelContextWindows: KIMI_CODING_MODEL_CONTEXT_WINDOWS,
450
456
  modelInputModalities: KIMI_CODING_MODEL_INPUT_MODALITIES,
451
457
  // K3 accepts low/high/max; Codex aliases are normalized by the model-scoped wire map.
@@ -1258,4 +1264,3 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
1258
1264
  note: "Serverless Inference subscription API. Live discovery exposes only kimi-k2-instruct because Vultr documents it as the sole tool-calling model.",
1259
1265
  },
1260
1266
  ];
1261
-