@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927

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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-B5-cAEd2.js +51 -0
  3. package/gui/dist/assets/App-DCBismRi.css +1 -0
  4. package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-BcXblvem.js +86 -0
  7. package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -22,7 +22,8 @@ import { readBoundedResponseBody } from "../lib/bounded-body";
22
22
  * `ultra` returned 400. A correction therefore adds the rungs a profile newly lists and keeps the
23
23
  * rungs the upstream measurably accepts; dropping them would strip an effort that works today,
24
24
  * including Codex's default `high`. The refresh path decodes the same payload, so it narrows a row
25
- * only after the upstream actually rejects a rung.
25
+ * only after the upstream actually rejects a rung. Issue #5096 supplies additional live API
26
+ * measurements that widen the DeepSeek Flash, GLM-5.3, Gemini-3.7-Flash and HY4 rows.
26
27
  */
27
28
  const COMMAND_CODE_MODEL_EFFORTS = {
28
29
  // Captured profile payload 2026-09-23: claude-fable-5-1.html.
@@ -36,11 +37,11 @@ const COMMAND_CODE_MODEL_EFFORTS = {
36
37
  profileUrl: "https://commandcode.ai/models/claude-opus-5-5",
37
38
  },
38
39
  "deepseek/deepseek-v4-flash": {
39
- efforts: ["high", "max"],
40
+ efforts: ["low", "medium", "high", "xhigh", "max"],
40
41
  profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
41
42
  },
42
43
  "deepseek/deepseek-v4-flash-vision-exp": {
43
- efforts: ["high", "max"],
44
+ efforts: ["low", "medium", "high", "xhigh", "max"],
44
45
  profileUrl: "https://commandcode.ai/models/deepseek-v4-flash-vision-exp",
45
46
  },
46
47
  // Captured profile payload 2026-09-23: deepseek-v4-flash-fast.html.
@@ -50,7 +51,7 @@ const COMMAND_CODE_MODEL_EFFORTS = {
50
51
  },
51
52
  // Captured profile payload 2026-09-23: deepseek-v4-1-flash.html.
52
53
  "deepseek/deepseek-v4.1-flash": {
53
- efforts: ["low", "high", "max"],
54
+ efforts: ["low", "medium", "high", "xhigh", "max"],
54
55
  profileUrl: "https://commandcode.ai/models/deepseek-v4-1-flash",
55
56
  },
56
57
  "gpt-5.6-luna": {
@@ -58,7 +59,7 @@ const COMMAND_CODE_MODEL_EFFORTS = {
58
59
  profileUrl: "https://commandcode.ai/models/gpt-5-6-luna",
59
60
  },
60
61
  "google/gemini-3.7-flash": {
61
- efforts: ["low", "medium", "high"],
62
+ efforts: ["low", "medium", "high", "xhigh", "max"],
62
63
  profileUrl: "https://commandcode.ai/models/gemini-3-7-flash",
63
64
  },
64
65
  // Captured profile payload 2026-09-23: gemini-3-8-flash.html.
@@ -83,11 +84,11 @@ const COMMAND_CODE_MODEL_EFFORTS = {
83
84
  profileUrl: "https://commandcode.ai/models/glm-5-2-fast",
84
85
  },
85
86
  "zai-org/GLM-5.3": {
86
- efforts: ["low", "high", "max"],
87
+ efforts: ["low", "medium", "high", "xhigh", "max"],
87
88
  profileUrl: "https://commandcode.ai/models/glm-5-3",
88
89
  },
89
90
  "z-ai/glm-5.3-flash": {
90
- efforts: ["low", "high", "max"],
91
+ efforts: ["low", "medium", "high", "xhigh", "max"],
91
92
  profileUrl: "https://commandcode.ai/models/glm-5-3-flash",
92
93
  },
93
94
  // Captured profile payload 2026-09-23: glm-5-3-flashx.html.
@@ -143,7 +144,7 @@ const COMMAND_CODE_MODEL_EFFORTS = {
143
144
  },
144
145
  // Captured profile payload 2026-09-23: hy4-preview.html.
145
146
  "tencent/hy4-preview": {
146
- efforts: ["low", "medium", "high"],
147
+ efforts: ["low", "medium", "high", "xhigh", "max"],
147
148
  profileUrl: "https://commandcode.ai/models/hy4-preview",
148
149
  },
149
150
  // Captured profile payload 2026-09-23: grok-4-7.html.
@@ -151,10 +152,31 @@ const COMMAND_CODE_MODEL_EFFORTS = {
151
152
  efforts: ["low", "medium", "high", "xhigh"],
152
153
  profileUrl: "https://commandcode.ai/models/grok-4-7",
153
154
  },
155
+ // Live API measurements supplied in #5096; no verified public profile URL.
156
+ "moonshotai/Kimi-K3": { efforts: ["low", "medium", "high", "xhigh", "max"] },
157
+ "MiniMaxAI/MiniMax-M3": { efforts: ["low", "medium", "high", "xhigh", "max"] },
158
+ "xiaomi/mimo-v2.5": { efforts: ["low", "medium", "high", "xhigh", "max"] },
159
+ "xai/grok-4.5": { efforts: ["low", "medium", "high", "xhigh", "max"] },
160
+ "xai/grok-4.6": { efforts: ["low", "medium", "high", "xhigh", "max"] },
161
+ "tencent/hy3-paid": { efforts: ["low", "medium", "high", "xhigh", "max"] },
162
+ "stepfun/Step-3.7-Flash": { efforts: ["low", "medium", "high", "xhigh", "max"] },
163
+ "Qwen/Qwen3.8-Max": { efforts: ["low", "medium", "high", "xhigh", "max"] },
164
+ "Qwen/Qwen3.8-27B": { efforts: ["low", "medium", "high", "xhigh", "max"] },
165
+ "nvidia/nemotron-3-ultra-550b-a55b": { efforts: ["low", "medium", "high", "xhigh", "max"] },
166
+ "meituan/LongCat-2.0:free": { efforts: ["low", "medium", "high", "xhigh", "max"] },
167
+ "inclusionai/ling-3.0-flash-sante:free": { efforts: ["low", "medium", "high", "xhigh", "max"] },
168
+ "thinkingmachines/inkling-small": { efforts: ["low", "medium", "high", "xhigh", "max"] },
169
+ "moonshotai/Kimi-K2.7-Code": { efforts: ["low", "medium", "high", "xhigh"] },
170
+ "moonshotai/Kimi-K2.7-Code-Highspeed": { efforts: ["low", "high", "xhigh", "max"] },
171
+ "xiaomi/mimo-v2.5-pro": { efforts: ["low", "medium", "high"] },
172
+ "Qwen/Qwen3.7-32B": { efforts: ["low", "medium", "high", "xhigh"] },
173
+ "Qwen/Qwen3.7-72B": { efforts: ["low", "medium", "high", "xhigh"] },
174
+ "Qwen/Qwen3.6-35B-A22B": { efforts: ["low", "medium", "high", "xhigh"] },
175
+ "poolside/laguna-s-2.1-free": { efforts: ["medium"] },
154
176
  } as const;
155
177
 
156
178
  /**
157
- * Official Command Code model-profile facts, not a model catalog. Models remain
179
+ * Command Code profile facts and live API measurements (#5096), not a model catalog. Models remain
158
180
  * account-scoped and come exclusively from the authenticated /provider/v1/models endpoint.
159
181
  */
160
182
  export const COMMAND_CODE_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
@@ -273,7 +295,7 @@ export async function refreshCommandCodeReasoningEfforts(
273
295
  destination = DEFAULT_EFFORT_DESTINATION,
274
296
  ): Promise<readonly string[] | undefined> {
275
297
  const key = cacheKey(modelId, destination);
276
- let profile: { efforts: readonly string[]; profileUrl: string } | undefined;
298
+ let profile: { efforts: readonly string[]; profileUrl?: string } | undefined;
277
299
  for (const [id, row] of Object.entries(COMMAND_CODE_MODEL_EFFORTS)) {
278
300
  if (keyFor(id) === keyFor(modelId)) {
279
301
  profile = row;
@@ -286,6 +308,7 @@ export async function refreshCommandCodeReasoningEfforts(
286
308
  rejected.add(rejectedEffort);
287
309
  rejectedEfforts.set(key, rejected);
288
310
  }
311
+ if (!profile.profileUrl) return commandCodeReasoningEfforts(modelId, destination);
289
312
  try {
290
313
  const response = await fetchFn(profile.profileUrl, {
291
314
  headers: { Accept: "text/html" },
@@ -0,0 +1,44 @@
1
+ /** Kiro login identity and persisted evidence entrypoints. */
2
+ import { createHash } from "node:crypto";
3
+ import type { ProviderAccount } from "../oauth/types";
4
+ import type { ProviderQuota } from "./quota-types";
5
+ import { hydrateAccountQuotaCache, persistAccountQuotaCache } from "./quota/account-cache";
6
+
7
+ export function kiroEvidenceIdentity(account: ProviderAccount): string {
8
+ const cred = account.credential;
9
+ return createHash("sha256").update(JSON.stringify([
10
+ account.id, account.loginId ?? String(account.addedAt ?? ""),
11
+ cred.accountId ?? cred.email ?? "", cred.kiro?.profileArn ?? "",
12
+ cred.kiro?.clientId ?? "",
13
+ ])).digest("hex");
14
+ }
15
+
16
+ export interface KiroPersistedQuota extends ProviderQuota { identity: string; }
17
+
18
+ /** Keep only the closed set emitted by the Kiro parser when disk data is rewritten. */
19
+ export function sanitizeKiroQuota(quota: ProviderQuota): ProviderQuota {
20
+ const trial = quota.customWindows?.find(window => window.label === "Free trial"
21
+ && typeof window.percent === "number" && Number.isFinite(window.percent)
22
+ && window.percent >= 0 && window.percent <= 100);
23
+ return {
24
+ monthlyPercent: quota.monthlyPercent,
25
+ ...(typeof quota.kiroCreditsUsed === "number" && Number.isFinite(quota.kiroCreditsUsed)
26
+ && quota.kiroCreditsUsed >= 0 ? { kiroCreditsUsed: quota.kiroCreditsUsed } : {}),
27
+ ...(typeof quota.kiroCreditsLimit === "number" && Number.isFinite(quota.kiroCreditsLimit)
28
+ && quota.kiroCreditsLimit > 0 ? { kiroCreditsLimit: quota.kiroCreditsLimit } : {}),
29
+ ...(typeof quota.monthlyResetAt === "number" && Number.isFinite(quota.monthlyResetAt)
30
+ && Number.isFinite(new Date(quota.monthlyResetAt).getTime())
31
+ ? { monthlyResetAt: quota.monthlyResetAt } : {}),
32
+ ...(trial ? { customWindows: [{ label: "Free trial", percent: trial.percent }] } : {}),
33
+ updatedAt: quota.updatedAt,
34
+ };
35
+ }
36
+ export interface KiroPersistedVerdict {
37
+ exhausted: boolean;
38
+ resetAt?: number;
39
+ observedAt: number;
40
+ identity: string;
41
+ }
42
+
43
+ export function hydrateKiroAccountState(): void { hydrateAccountQuotaCache(); }
44
+ export function persistKiroAccountState(): void { persistAccountQuotaCache(); }
@@ -0,0 +1,162 @@
1
+ /** Account-scoped Kiro management model evidence; never required for a request. */
2
+ import type { ProviderAccount } from "../oauth/types";
3
+ import { getAccountSet } from "../oauth/store";
4
+ import type { OcxProviderConfig } from "../types";
5
+ import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
6
+ import { kiroEvidenceIdentity } from "./kiro-account-state-disk";
7
+ import { normalizeKiroModelId, KIRO_MODEL_CONTEXT_WINDOWS } from "./kiro-models";
8
+ import { kiroManagementHost, kiroUsageContextForAccount } from "./kiro-usage";
9
+ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
10
+ import { asRecord, QUOTA_JSON_READ_FAILURE, readQuotaJson, REQUEST_TIMEOUT_MS } from "./quota-wire";
11
+
12
+ const TARGET = "KiroControlPlaneBearerService.ListAvailableModels";
13
+ export const KIRO_MODEL_CATALOG_TTL_MS = 60 * 60_000;
14
+ const LAST_GOOD_MS = 24 * 60 * 60_000;
15
+ const FAILURE_RETRY_MS = 60_000;
16
+
17
+ export interface KiroAccountModel { modelId: string; contextWindow?: number }
18
+ interface Row { identity: string; models: KiroAccountModel[]; observedAt: number; nextRefreshAt: number }
19
+ interface Flight { identity: string; promise: Promise<void> }
20
+ const rows = new Map<string, Row>();
21
+ const flights = new Map<string, Flight>();
22
+ /** Retry time after a failed discovery for an account that has no last good row to carry it. */
23
+ const failedUntil = new Map<string, { identity: string; at: number }>();
24
+
25
+ export function kiroModelDiscoveryEnabled(): boolean {
26
+ return process.env.OPENCODEX_KIRO_MODEL_DISCOVERY !== "0";
27
+ }
28
+
29
+ function liveAccount(accountId: string): ProviderAccount | undefined {
30
+ return getAccountSet("kiro")?.accounts.find(account => account.id === accountId && account.needsReauth !== true);
31
+ }
32
+
33
+ function currentIdentity(accountId: string): string | undefined {
34
+ const account = liveAccount(accountId);
35
+ return account ? kiroEvidenceIdentity(account) : undefined;
36
+ }
37
+
38
+ function validRow(account: ProviderAccount): Row | undefined {
39
+ const live = liveAccount(account.id);
40
+ if (!live || kiroEvidenceIdentity(live) !== kiroEvidenceIdentity(account)) return undefined;
41
+ const row = rows.get(account.id);
42
+ if (!row || row.identity !== kiroEvidenceIdentity(live)) return undefined;
43
+ if (Date.now() - row.observedAt >= LAST_GOOD_MS) return undefined;
44
+ return row;
45
+ }
46
+
47
+ /** Synchronous cache read; no token refresh and no network. */
48
+ export function readKiroAccountModels(account: ProviderAccount): KiroAccountModel[] | undefined {
49
+ return validRow(account)?.models;
50
+ }
51
+
52
+ function parseModels(value: unknown): KiroAccountModel[] | undefined {
53
+ const payload = asRecord(value);
54
+ if (!payload || !Array.isArray(payload.models) || payload.models.length === 0
55
+ || payload.models.length > MODEL_DISCOVERY_MAX_MODELS) return undefined;
56
+ const seen = new Set<string>();
57
+ const models: KiroAccountModel[] = [];
58
+ for (const value of payload.models) {
59
+ const item = asRecord(value);
60
+ if (!item || !isValidModelDiscoveryModelId(item.modelId) || seen.has(item.modelId)) continue;
61
+ seen.add(item.modelId);
62
+ const limits = asRecord(item.tokenLimits);
63
+ const limit = limits?.maxInputTokens;
64
+ models.push({ modelId: item.modelId,
65
+ ...(typeof limit === "number" && Number.isSafeInteger(limit) && limit > 0
66
+ ? { contextWindow: limit } : {}) });
67
+ }
68
+ return models.length ? models : undefined;
69
+ }
70
+
71
+ /** Start an identity-fenced refresh without adding its latency to the serving request. */
72
+ export function refreshKiroAccountModelsDetached(
73
+ account: ProviderAccount,
74
+ providerConfig: OcxProviderConfig,
75
+ dependencies: ProviderOutboundDependencies = {},
76
+ ): void {
77
+ if (!kiroModelDiscoveryEnabled() || account.needsReauth === true) return;
78
+ const identity = kiroEvidenceIdentity(account);
79
+ if (currentIdentity(account.id) !== identity) return;
80
+ const old = validRow(account);
81
+ if (old && Date.now() < old.nextRefreshAt) return;
82
+ const failed = failedUntil.get(account.id);
83
+ if (!old && failed?.identity === identity && Date.now() < failed.at) return;
84
+ if (flights.get(account.id)?.identity === identity) return;
85
+
86
+ const flight = (async (): Promise<void> => {
87
+ let fresh: KiroAccountModel[] | undefined;
88
+ try {
89
+ const ctx = await kiroUsageContextForAccount(account.id);
90
+ if (currentIdentity(account.id) !== identity || !ctx.profileArn) return;
91
+ const url = new URL(kiroManagementHost(ctx));
92
+ url.searchParams.set("origin", "AI_EDITOR");
93
+ url.searchParams.set("profileArn", ctx.profileArn);
94
+ const response = await providerOutboundPost("kiro", providerConfig, url.toString(), {
95
+ headers: {
96
+ authorization: `Bearer ${ctx.access}`,
97
+ "content-type": "application/x-amz-json-1.0",
98
+ accept: "application/json",
99
+ "x-amz-target": TARGET,
100
+ "x-amzn-codewhisperer-optout": "true",
101
+ },
102
+ body: JSON.stringify({ origin: "AI_EDITOR", profileArn: ctx.profileArn }),
103
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
104
+ }, dependencies);
105
+ if (response.ok && !await providerRedirectError(response, url.toString())) {
106
+ const body = await readQuotaJson(response);
107
+ if (body !== QUOTA_JSON_READ_FAILURE) fresh = parseModels(body);
108
+ }
109
+ } catch {
110
+ // Management discovery is advisory. Keep the last good row on every failure.
111
+ }
112
+ if (currentIdentity(account.id) !== identity) return;
113
+ const now = Date.now();
114
+ if (fresh) {
115
+ rows.set(account.id, { identity, models: fresh, observedAt: now,
116
+ nextRefreshAt: now + KIRO_MODEL_CATALOG_TTL_MS });
117
+ failedUntil.delete(account.id);
118
+ } else if (old) rows.set(account.id, { ...old, nextRefreshAt: now + FAILURE_RETRY_MS });
119
+ // Without a last good row the failure still has to back off, or every serving request
120
+ // after a restart would start another discovery while the endpoint is failing.
121
+ else failedUntil.set(account.id, { identity, at: now + FAILURE_RETRY_MS });
122
+ })();
123
+ flights.set(account.id, { identity, promise: flight });
124
+ void flight.catch(() => {}).finally(() => {
125
+ if (flights.get(account.id)?.promise === flight) flights.delete(account.id);
126
+ });
127
+ }
128
+
129
+ export function kiroAccountSupportsModel(accountId: string, model: string): boolean | undefined {
130
+ const account = liveAccount(accountId);
131
+ const models = account ? readKiroAccountModels(account) : undefined;
132
+ if (!models) return undefined;
133
+ const normalized = normalizeKiroModelId(model);
134
+ return models.some(row => row.modelId === model || row.modelId === normalized);
135
+ }
136
+
137
+ /** Include the static floor while any live account has no observed limit. */
138
+ export function kiroObservedContextWindow(model: string): number | undefined {
139
+ const normalized = normalizeKiroModelId(model);
140
+ if (normalized === "auto") return undefined;
141
+ const accounts = getAccountSet("kiro")?.accounts.filter(account => account.needsReauth !== true) ?? [];
142
+ const limits: number[] = [];
143
+ let unknown = accounts.length === 0;
144
+ for (const account of accounts) {
145
+ const hit = readKiroAccountModels(account)?.find(row => row.modelId === model || row.modelId === normalized);
146
+ if (hit?.contextWindow === undefined) unknown = true;
147
+ else limits.push(hit.contextWindow);
148
+ }
149
+ const staticWindow = KIRO_MODEL_CONTEXT_WINDOWS[normalized];
150
+ if (unknown && staticWindow !== undefined) limits.push(staticWindow);
151
+ return limits.length ? Math.min(...limits) : undefined;
152
+ }
153
+
154
+ export function clearKiroAccountModels(accountId?: string): void {
155
+ if (accountId) { rows.delete(accountId); flights.delete(accountId); failedUntil.delete(accountId); }
156
+ else { rows.clear(); flights.clear(); failedUntil.clear(); }
157
+ }
158
+
159
+ /** Deterministic test seam; production requests never call this. */
160
+ export function awaitKiroModelRefreshForTests(accountId: string): Promise<void> {
161
+ return flights.get(accountId)?.promise ?? Promise.resolve();
162
+ }
@@ -28,12 +28,13 @@ export const KIRO_MODELS = [
28
28
  ];
29
29
 
30
30
  // Per-model context windows as documented on Kiro's official model catalog
31
- // (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-07-14).
31
+ // (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-09-25).
32
32
  // "Auto" is a router with no fixed window on Kiro's table, so it is intentionally omitted.
33
+ // GPT-5.6 accepts 1M tokens; requests above 272K bill at double the listed rate (two-tier pricing).
33
34
  export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
34
- "gpt-5.6-sol": 272_000,
35
- "gpt-5.6-terra": 272_000,
36
- "gpt-5.6-luna": 272_000,
35
+ "gpt-5.6-sol": 1_000_000,
36
+ "gpt-5.6-terra": 1_000_000,
37
+ "gpt-5.6-luna": 1_000_000,
37
38
  "gpt-6-sol": 272_000,
38
39
  "gpt-6-luna": 272_000,
39
40
  "claude-sonnet-5": 1_000_000,
@@ -0,0 +1,35 @@
1
+ /** Cache-only Kiro quota projection for the opt-in metrics snapshot. */
2
+ import { oauthAccountLogLabel } from "../codex/account-label";
3
+ import { getAccountSet } from "../oauth/store";
4
+ import { kiroAccountEvidence } from "./kiro-usage";
5
+
6
+ export interface KiroQuotaMetricRow {
7
+ account: string;
8
+ used: number;
9
+ limit: number;
10
+ percent: number;
11
+ secondsToReset?: number;
12
+ }
13
+
14
+ const MAX_KIRO_METRIC_ACCOUNTS = 32;
15
+
16
+ export function cachedKiroQuotaMetricRows(now = Date.now()): KiroQuotaMetricRow[] {
17
+ const accounts = getAccountSet("kiro")?.accounts ?? [];
18
+ const rows: KiroQuotaMetricRow[] = [];
19
+ const labels = new Set<string>();
20
+ for (const account of [...accounts].sort((a, b) => a.id.localeCompare(b.id))) {
21
+ const { quotaPercent: percent, creditsUsed: used, creditsLimit: limit, resetAt } =
22
+ kiroAccountEvidence(account, now, { hydrate: false });
23
+ if (typeof used !== "number" || !Number.isFinite(used) || used < 0
24
+ || typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0
25
+ || typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100
26
+ || (resetAt !== undefined && (!Number.isFinite(resetAt) || resetAt <= now))) continue;
27
+ const label = oauthAccountLogLabel(account.id, "kiro");
28
+ if (labels.has(label)) continue;
29
+ labels.add(label);
30
+ rows.push({ account: label, used, limit, percent,
31
+ ...(resetAt !== undefined ? { secondsToReset: (resetAt - now) / 1000 } : {}) });
32
+ if (rows.length === MAX_KIRO_METRIC_ACCOUNTS) break;
33
+ }
34
+ return rows;
35
+ }
@@ -12,6 +12,11 @@
12
12
  * the pool needs those two answers to decide how long to cool a 429'd account.
13
13
  */
14
14
  import { getValidAccessSnapshotForAccount } from "../oauth";
15
+ import { resolveKiroRequestProfile } from "../oauth/kiro";
16
+ import { credentialGeneration, getAccountSet } from "../oauth/store";
17
+ import type { ProviderAccount } from "../oauth/types";
18
+ import { hydrateKiroAccountState, kiroEvidenceIdentity, type KiroPersistedVerdict } from "./kiro-account-state-disk";
19
+ import { accountCacheKey, accountQuotaCache } from "./quota/account-cache";
15
20
  import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types";
16
21
  import {
17
22
  ACCOUNT_QUOTA_TTL_MS,
@@ -46,6 +51,8 @@ export interface KiroUsageContext {
46
51
  accountId: string;
47
52
  access: string;
48
53
  profileArn?: string;
54
+ /** The ARN is the Builder ID service profile, not the account's; it must not pick the region. */
55
+ builderIdFallback?: boolean;
49
56
  apiRegion?: string;
50
57
  ssoRegion?: string;
51
58
  }
@@ -61,7 +68,8 @@ export interface KiroUsageSnapshot {
61
68
  interface KiroUsageStateEntry {
62
69
  exhausted: boolean;
63
70
  nextResetAt?: number;
64
- ts: number;
71
+ observedAt: number;
72
+ identity: string;
65
73
  }
66
74
 
67
75
  /**
@@ -82,7 +90,10 @@ function safeRegion(value: string | undefined): string | undefined {
82
90
  * the SSO session that minted the token.
83
91
  */
84
92
  function usageRegion(ctx: KiroUsageContext): string {
85
- return safeRegion(ctx.profileArn?.split(":")[3])
93
+ // The Builder ID service profile is Amazon's fixed us-east-1 ARN, not the account's own, so it
94
+ // must not pin the region, as kiro-constants.ts requires for the runtime path.
95
+ const arnRegion = ctx.builderIdFallback ? undefined : ctx.profileArn?.split(":")[3];
96
+ return safeRegion(arnRegion)
86
97
  ?? safeRegion(ctx.apiRegion)
87
98
  ?? safeRegion(ctx.ssoRegion)
88
99
  ?? "us-east-1";
@@ -92,6 +103,10 @@ export function kiroUsageManagementUrl(region: string): string {
92
103
  return `https://management.${region}.kiro.dev/`;
93
104
  }
94
105
 
106
+ export function kiroManagementHost(ctx: KiroUsageContext): string {
107
+ return kiroUsageManagementUrl(usageRegion(ctx));
108
+ }
109
+
95
110
  /** Credit balances are fractional; the integer fields round 695.17 down to 695. */
96
111
  function preciseNumber(row: Record<string, unknown>, precise: string, whole: string): number | undefined {
97
112
  return toFiniteNumber(row[precise]) ?? toFiniteNumber(row[whole]);
@@ -115,7 +130,8 @@ function parseKiroUsage(body: unknown): KiroUsageSnapshot | null {
115
130
 
116
131
  const used = preciseNumber(breakdown, "currentUsageWithPrecision", "currentUsage");
117
132
  const limit = preciseNumber(breakdown, "usageLimitWithPrecision", "usageLimit");
118
- if (used === undefined || limit === undefined || limit <= 0) return null;
133
+ if (used === undefined || !Number.isFinite(used) || used < 0
134
+ || limit === undefined || !Number.isFinite(limit) || limit <= 0) return null;
119
135
 
120
136
  const percent = normalizePercent((used / limit) * 100);
121
137
  if (percent === undefined) return null;
@@ -137,6 +153,8 @@ function parseKiroUsage(body: unknown): KiroUsageSnapshot | null {
137
153
 
138
154
  const quota: ProviderQuota = {
139
155
  monthlyPercent: percent,
156
+ kiroCreditsUsed: used,
157
+ kiroCreditsLimit: limit,
140
158
  ...(nextResetAt !== undefined ? { monthlyResetAt: nextResetAt } : {}),
141
159
  ...(customWindows.length > 0 ? { customWindows } : {}),
142
160
  updatedAt: Date.now(),
@@ -144,13 +162,12 @@ function parseKiroUsage(body: unknown): KiroUsageSnapshot | null {
144
162
 
145
163
  // Enterprise accounts with overage enabled keep serving past the included limit, so
146
164
  // "used >= limit" is not by itself a reason to stop routing to the account.
147
- const overageEnabled = String(asRecord(payload.overageConfiguration)?.overageStatus ?? "")
148
- .trim()
149
- .toUpperCase() === "ENABLED";
165
+ const overageStatus = String(asRecord(payload.overageConfiguration)?.overageStatus ?? "")
166
+ .trim().toUpperCase();
150
167
 
151
168
  return {
152
169
  quota,
153
- exhausted: used >= limit && !overageEnabled,
170
+ exhausted: used >= limit && overageStatus === "DISABLED",
154
171
  ...(nextResetAt !== undefined ? { nextResetAt } : {}),
155
172
  };
156
173
  }
@@ -164,17 +181,18 @@ function parseKiroUsage(body: unknown): KiroUsageSnapshot | null {
164
181
  * a log line.
165
182
  */
166
183
  export async function fetchKiroUsageSnapshot(ctx: KiroUsageContext): Promise<KiroUsageSnapshot | null> {
184
+ if (!ctx.profileArn) return null;
167
185
  const region = usageRegion(ctx);
168
186
  const url = new URL(kiroUsageManagementUrl(region));
169
187
  url.searchParams.set("origin", "AI_EDITOR");
170
188
  url.searchParams.set("isEmailRequired", "true");
171
- if (ctx.profileArn) url.searchParams.set("profileArn", ctx.profileArn);
189
+ url.searchParams.set("profileArn", ctx.profileArn);
172
190
 
173
191
  // The modeled arguments appear in BOTH the query string and the body. That duplication is
174
192
  // the observed Kiro CLI contract, not an oversight; we have no way to test which side the
175
193
  // service actually reads, so we reproduce both.
176
194
  const body: Record<string, unknown> = { origin: "AI_EDITOR", isEmailRequired: true };
177
- if (ctx.profileArn) body.profileArn = ctx.profileArn;
195
+ body.profileArn = ctx.profileArn;
178
196
 
179
197
  try {
180
198
  const response = await fetch(url, {
@@ -207,28 +225,56 @@ export async function fetchKiroUsageSnapshot(ctx: KiroUsageContext): Promise<Kir
207
225
  */
208
226
  export async function kiroUsageContextForAccount(accountId: string): Promise<KiroUsageContext> {
209
227
  const snapshot = await getValidAccessSnapshotForAccount("kiro", accountId);
228
+ // Builder ID accounts never get an account-scoped ARN, and GetUsageLimits rejects a missing one
229
+ // with 400 "Invalid profileArn". Ask the same resolver the runtime path uses, so the usage probe
230
+ // sends exactly the ARN a generation request would. An account object is always passed, so the
231
+ // accountless env/local-import fallbacks never apply to a pooled account.
232
+ const profile = resolveKiroRequestProfile({
233
+ profileArn: snapshot.kiro?.profileArn,
234
+ authType: snapshot.kiro?.authType,
235
+ });
210
236
  return {
211
237
  accountId,
212
238
  access: snapshot.accessToken,
213
- ...(snapshot.kiro?.profileArn ? { profileArn: snapshot.kiro.profileArn } : {}),
239
+ ...(profile.profileArn ? { profileArn: profile.profileArn } : {}),
240
+ ...(profile.builderIdFallback ? { builderIdFallback: true } : {}),
214
241
  ...(snapshot.kiro?.apiRegion ? { apiRegion: snapshot.kiro.apiRegion } : {}),
215
242
  ...(snapshot.kiro?.ssoRegion ? { ssoRegion: snapshot.kiro.ssoRegion } : {}),
216
243
  };
217
244
  }
218
245
 
219
246
  /** Record exhaustion for a probed account. Called from the quota cache's commit guard. */
220
- export function commitKiroAccountUsageState(key: string, snapshot: KiroUsageSnapshot | null): void {
247
+ export function commitKiroAccountUsageState(key: string, snapshot: KiroUsageSnapshot | null, identity?: string): void {
221
248
  if (!snapshot) {
222
249
  usageState.delete(key);
223
250
  return;
224
251
  }
252
+ if (!identity) return;
225
253
  usageState.set(key, {
226
254
  exhausted: snapshot.exhausted,
227
255
  ...(snapshot.nextResetAt !== undefined ? { nextResetAt: snapshot.nextResetAt } : {}),
228
- ts: Date.now(),
256
+ observedAt: Date.now(), identity,
229
257
  });
230
258
  }
231
259
 
260
+ export function* kiroPersistableVerdicts(now = Date.now()): IterableIterator<[string, KiroPersistedVerdict]> {
261
+ const live = new Map(getAccountSet("kiro")?.accounts.map(account =>
262
+ [accountCacheKey("kiro", account.id), kiroEvidenceIdentity(account)]) ?? []);
263
+ for (const [key, entry] of usageState) {
264
+ if (live.get(key) !== entry.identity || entry.observedAt > now
265
+ || now - entry.observedAt >= ACCOUNT_QUOTA_TTL_MS
266
+ || (entry.nextResetAt !== undefined && entry.nextResetAt <= now)) continue;
267
+ yield [key, { exhausted: entry.exhausted, observedAt: entry.observedAt, identity: entry.identity,
268
+ ...(entry.nextResetAt !== undefined ? { resetAt: entry.nextResetAt } : {}) }];
269
+ }
270
+ }
271
+
272
+ export function hydrateKiroUsageVerdict(key: string, verdict: KiroPersistedVerdict, account: ProviderAccount): void {
273
+ if (usageState.has(key) || verdict.identity !== kiroEvidenceIdentity(account)) return;
274
+ usageState.set(key, { exhausted: verdict.exhausted, observedAt: verdict.observedAt,
275
+ identity: verdict.identity, ...(verdict.resetAt !== undefined ? { nextResetAt: verdict.resetAt } : {}) });
276
+ }
277
+
232
278
  /**
233
279
  * Is this account known to be out of allowance right now?
234
280
  *
@@ -237,11 +283,12 @@ export function commitKiroAccountUsageState(key: string, snapshot: KiroUsageSnap
237
283
  */
238
284
  export function getKiroAccountExhaustion(
239
285
  key: string,
286
+ account: ProviderAccount,
240
287
  now = Date.now(),
241
288
  ): { exhausted: boolean; nextResetAt?: number } | null {
242
289
  const entry = usageState.get(key);
243
- if (!entry) return null;
244
- if (now - entry.ts >= ACCOUNT_QUOTA_TTL_MS) return null;
290
+ if (!entry || entry.identity !== kiroEvidenceIdentity(account)) return null;
291
+ if (entry.observedAt > now || now - entry.observedAt >= ACCOUNT_QUOTA_TTL_MS) return null;
245
292
  if (entry.nextResetAt !== undefined && entry.nextResetAt <= now) return null;
246
293
  return {
247
294
  exhausted: entry.exhausted,
@@ -249,6 +296,63 @@ export function getKiroAccountExhaustion(
249
296
  };
250
297
  }
251
298
 
299
+ /** The only Kiro routing evidence read; each half expires on its own clock. */
300
+ export function kiroAccountEvidence(account: ProviderAccount, now = Date.now(), opts: { hydrate?: boolean } = {}):
301
+ { quotaPercent?: number; creditsUsed?: number; creditsLimit?: number; exhausted?: boolean; resetAt?: number } {
302
+ // Routing hydrates saved evidence on first use; the metrics scrape passes hydrate:false so a
303
+ // scrape never touches the disk snapshot and simply reports nothing until routing has loaded it.
304
+ if (opts.hydrate !== false) hydrateKiroAccountState();
305
+ const key = accountCacheKey("kiro", account.id);
306
+ const row = accountQuotaCache.get(key);
307
+ const quota = row?.identity === kiroEvidenceIdentity(account) && row.quota
308
+ && typeof row.quota.monthlyPercent === "number" && Number.isFinite(row.quota.monthlyPercent)
309
+ && row.quota.monthlyPercent >= 0 && row.quota.monthlyPercent <= 100
310
+ && row.quota.updatedAt <= now && now - row.quota.updatedAt < ACCOUNT_QUOTA_TTL_MS
311
+ && (row.quota.monthlyResetAt === undefined || (row.quota.monthlyResetAt > now
312
+ && Number.isFinite(new Date(row.quota.monthlyResetAt).getTime()))) ? row.quota : null;
313
+ const verdict = getKiroAccountExhaustion(key, account, now);
314
+ return {
315
+ ...(quota?.monthlyPercent !== undefined ? { quotaPercent: quota.monthlyPercent } : {}),
316
+ ...(typeof quota?.kiroCreditsUsed === "number" && Number.isFinite(quota.kiroCreditsUsed)
317
+ && quota.kiroCreditsUsed >= 0 ? { creditsUsed: quota.kiroCreditsUsed } : {}),
318
+ ...(typeof quota?.kiroCreditsLimit === "number" && Number.isFinite(quota.kiroCreditsLimit)
319
+ && quota.kiroCreditsLimit > 0 ? { creditsLimit: quota.kiroCreditsLimit } : {}),
320
+ ...(verdict ? { exhausted: verdict.exhausted } : {}),
321
+ ...(verdict?.nextResetAt !== undefined ? { resetAt: verdict.nextResetAt }
322
+ : quota?.monthlyResetAt !== undefined ? { resetAt: quota.monthlyResetAt } : {}),
323
+ };
324
+ }
325
+
326
+ /** A confirmed refusal supersedes only older evidence from the same login. */
327
+ export function noteKiroMonthlyRefusal(accountId: string, generation: string, observedAt = Date.now()): number {
328
+ hydrateKiroAccountState();
329
+ const live = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
330
+ if (!live || credentialGeneration(live.credential) !== generation) return 0;
331
+ const key = accountCacheKey("kiro", accountId);
332
+ const identity = kiroEvidenceIdentity(live);
333
+ const old = usageState.get(key);
334
+ if (old?.identity === identity && old.observedAt >= observedAt)
335
+ return Math.max(0, (old.nextResetAt ?? observedAt) - observedAt);
336
+ const resetAt = kiroAccountEvidence(live, observedAt).resetAt;
337
+ const until = Math.min(resetAt ?? observedAt + ACCOUNT_QUOTA_TTL_MS, observedAt + ACCOUNT_QUOTA_TTL_MS);
338
+ usageState.set(key, { identity, exhausted: true, observedAt,
339
+ ...(resetAt !== undefined ? { nextResetAt: resetAt } : {}) });
340
+ return Math.max(0, until - observedAt);
341
+ }
342
+
343
+ /** Completion clears an older verdict only for the credential that actually served. */
344
+ export function noteKiroServedSuccess(accountId: string, generation: string, observedAt = Date.now()): boolean {
345
+ hydrateKiroAccountState();
346
+ const live = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
347
+ if (!live || credentialGeneration(live.credential) !== generation) return false;
348
+ const key = accountCacheKey("kiro", accountId);
349
+ const old = usageState.get(key);
350
+ if (!old || old.identity !== kiroEvidenceIdentity(live) || old.observedAt >= observedAt || !old.exhausted)
351
+ return false;
352
+ usageState.set(key, { ...old, exhausted: false, observedAt });
353
+ return true;
354
+ }
355
+
252
356
  /** Drop rows for one provider prefix, or all of them. Mirrors clearAccountQuotaCache. */
253
357
  export function clearKiroAccountUsageState(prefix?: string): void {
254
358
  if (!prefix) {