@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.
- package/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- 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
|
-
*
|
|
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
|
|
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-
|
|
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":
|
|
35
|
-
"gpt-5.6-terra":
|
|
36
|
-
"gpt-5.6-luna":
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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.
|
|
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) {
|