@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -4,7 +4,7 @@ import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
4
4
  import { loadConfig, resolveEnvValue, saveConfig } from "../config";
5
5
  import { maskEmail } from "../lib/privacy";
6
6
  import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
7
- import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, OAuthMutationBusyError } from "./store";
7
+ import { getAccountCredential, getAccountSet, removeAccount, saveAccountCredential, saveCredential, setActiveAccount, getCredential, credentialGeneration, createOAuthRefreshIntentLock, mergeAccountCredential, markAccountNeedsReauthIfGeneration, readOAuthRefreshIntent, writeOAuthRefreshIntent, markOAuthRefreshIntentStaleOwner, clearOAuthRefreshIntent, normalizeAuthStoreBuffer, OAuthMutationBusyError } from "./store";
8
8
  import { loginXai, refreshXaiToken, XAI_LOCAL_CLI_DETACH_WARNING, XaiTokenRequestError } from "./xai";
9
9
  import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthropicToken } from "./anthropic";
10
10
  import { loginKimi, refreshKimiToken } from "./kimi";
@@ -56,6 +56,20 @@ export interface OAuthAccessSnapshot {
56
56
  kiro?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
57
57
  }
58
58
 
59
+ export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
60
+ /** Allowlisted provider API origin consumed by GitHub Copilot model discovery. */
61
+ apiBaseUrl?: string;
62
+ }
63
+
64
+ export type OAuthActiveTokenObservation =
65
+ | { readonly kind: "available"; readonly snapshot: ObservedOAuthAccessSnapshot }
66
+ | { readonly kind: "missing" }
67
+ | { readonly kind: "malformed" }
68
+ | { readonly kind: "needs-reauth" }
69
+ | { readonly kind: "expired" }
70
+ | { readonly kind: "near-expiry" }
71
+ | { readonly kind: "unsupported" };
72
+
59
73
  const MAX_OAUTH_TOKEN_REFRESH_FLIGHTS = 32;
60
74
  const OAUTH_TOKEN_REFRESH_FLIGHT_STALE_MS = 120_000;
61
75
  interface OAuthRefreshFlightEvidence { flightId: string; dispatched: boolean }
@@ -278,6 +292,38 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
278
292
  };
279
293
  }
280
294
 
295
+ /**
296
+ * Observe the active OAuth token from an auth-store buffer supplied by its filesystem owner.
297
+ * Missing, malformed, reauth-required, and expiring credentials are typed no-token outcomes;
298
+ * this path never refreshes, locks, hardens, backs up, or persists credentials.
299
+ */
300
+ export function observeActiveOAuthAccessToken(
301
+ provider: string,
302
+ authStoreBuffer: Uint8Array | null,
303
+ now = Date.now(),
304
+ ): OAuthActiveTokenObservation {
305
+ const authStore = normalizeAuthStoreBuffer(authStoreBuffer);
306
+ if (authStore.kind === "absent") return { kind: "missing" };
307
+ if (authStore.kind === "malformed") return { kind: "malformed" };
308
+ if (!isOAuthProvider(provider)) return { kind: "unsupported" };
309
+
310
+ const accountSet = authStore.store[provider];
311
+ const account = accountSet?.accounts.find(candidate => candidate.id === accountSet.activeAccountId);
312
+ if (!account) return { kind: "missing" };
313
+ if (account.needsReauth) return { kind: "needs-reauth" };
314
+ if (account.credential.expires <= now) return { kind: "expired" };
315
+ if (account.credential.expires <= now + REFRESH_SKEW_MS) return { kind: "near-expiry" };
316
+
317
+ const apiBaseUrl = validateCopilotApiBaseUrl(account.credential.apiBaseUrl);
318
+ return {
319
+ kind: "available",
320
+ snapshot: {
321
+ ...accessSnapshot(provider, account.id, account.credential),
322
+ ...(apiBaseUrl ? { apiBaseUrl } : {}),
323
+ },
324
+ };
325
+ }
326
+
281
327
  async function resolveAccessSnapshotForAccount(
282
328
  provider: string,
283
329
  accountId: string,
@@ -583,13 +629,25 @@ function modelDiscoveryTransportSeed(providerName: string, prov: OcxProviderConf
583
629
  * Everyone else uses the OpenAI-style `/models` + Bearer with a `{ data: [{ id, owned_by? }] }`
584
630
  * response.
585
631
  */
586
- export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | undefined, providerName = ""): { url: string; headers: Record<string, string> } {
632
+ export interface ModelsRequestObservedAuth {
633
+ readonly oauthApiBaseUrl?: string;
634
+ }
635
+
636
+ export function buildModelsRequest(
637
+ prov: OcxProviderConfig,
638
+ apiKey: string | undefined,
639
+ providerName = "",
640
+ observedAuth?: ModelsRequestObservedAuth,
641
+ ): { url: string; headers: Record<string, string> } {
587
642
  const transportSeed = modelDiscoveryTransportSeed(providerName, prov);
643
+ const copilotApiBaseUrl = observedAuth === undefined
644
+ ? (providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined)
645
+ : observedAuth.oauthApiBaseUrl;
588
646
  const effectiveProvider = resolveProviderTransport(
589
647
  providerName,
590
648
  transportSeed,
591
649
  undefined,
592
- providerName === "github-copilot" ? getOAuthCredentialApiBaseUrl(providerName) : undefined,
650
+ copilotApiBaseUrl,
593
651
  );
594
652
  const headers: Record<string, string> = { ...(effectiveProvider.headers ?? {}) };
595
653
  const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
@@ -52,6 +52,10 @@ export async function validateApiKey(
52
52
  key: string,
53
53
  ): Promise<boolean | "unknown"> {
54
54
  try {
55
+ // A public model catalog cannot prove that the supplied key is valid. Returning unknown keeps
56
+ // the best-effort login flow available without persisting a false-positive validation result.
57
+ if (provider.apiKeyValidation === "unknown") return "unknown";
58
+
55
59
  if (provider.adapter === "anthropic") {
56
60
  const base = provider.baseUrl.replace(/\/v1\/?$/, "");
57
61
  const res = await fetch(`${base}/v1/messages`, {
package/src/oauth/kiro.ts CHANGED
@@ -172,13 +172,37 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi
172
172
  }
173
173
  }
174
174
 
175
- async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string }> {
175
+ /** Kiro profile ARN structure: arn:<partition>:codewhisperer:<region>:<account>:profile/<id> */
176
+ const KIRO_PROFILE_ARN_PATTERN = /^arn:[a-z0-9-]+:codewhisperer:[a-z0-9-]+:\d{12}:profile\/[A-Za-z0-9-]+$/;
177
+ const KIRO_PROFILE_ARN_MAX_LENGTH = 256;
178
+
179
+ function parseKiroProfileArn(value: unknown): string | undefined {
180
+ if (typeof value !== "string") return undefined;
181
+ const trimmed = value.trim();
182
+ if (trimmed.length === 0 || trimmed.length > KIRO_PROFILE_ARN_MAX_LENGTH) return undefined;
183
+ return KIRO_PROFILE_ARN_PATTERN.test(trimmed) ? trimmed : undefined;
184
+ }
185
+
186
+ function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefined {
187
+ // Only narrowly-named documented-ish shapes; never invent an ARN (#993).
188
+ return parseKiroProfileArn(parsed.profileArn)
189
+ ?? parseKiroProfileArn(parsed.profile_arn)
190
+ ?? (parsed.profile && typeof parsed.profile === "object" && !Array.isArray(parsed.profile)
191
+ ? parseKiroProfileArn((parsed.profile as Record<string, unknown>).arn)
192
+ : undefined);
193
+ }
194
+
195
+ async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> {
176
196
  try {
177
197
  const result = await runner(["whoami", "--format", "json"], signal);
178
198
  if (result.exitCode !== 0) return {};
179
- const parsed = JSON.parse(result.stdout) as { email?: unknown };
199
+ const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
180
200
  const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : "";
181
- return email && email.length <= 320 ? { email } : {};
201
+ const profileArn = profileArnFromWhoami(parsed);
202
+ return {
203
+ ...(email && email.length <= 320 ? { email } : {}),
204
+ ...(profileArn ? { profileArn } : {}),
205
+ };
182
206
  } catch {
183
207
  return {};
184
208
  }
@@ -251,14 +275,34 @@ async function oauthCredentialFromImported(
251
275
  runner: KiroCliRunner,
252
276
  signal?: AbortSignal,
253
277
  ): Promise<OAuthCredentials> {
254
- const identity = imported.source === "sqlite" ? await readKiroCliIdentity(runner, signal) : {};
255
- const metadata = metadataFromImported(imported);
278
+ let identity: { email?: string; profileArn?: string } = {};
279
+ if (imported.source === "sqlite") {
280
+ identity = await readKiroCliIdentity(runner, signal);
281
+ // Session-switch race (#993 review): another process may have switched the
282
+ // active Kiro CLI session between the SQLite read and whoami. Accept
283
+ // whoami's identity only when the session token STILL matches the import —
284
+ // refresh token, or access token when refresh is absent.
285
+ if (identity.profileArn !== undefined) {
286
+ const current = readKiroCliSqliteCredential();
287
+ const importedKey = imported.refresh || imported.access;
288
+ const currentKey = current ? current.refresh || current.access : "";
289
+ if (!current || currentKey !== importedKey) identity = {};
290
+ }
291
+ }
292
+ // Builder ID imports often lack a profileArn in SQLite; whoami against the
293
+ // SAME active CLI session can supply it (#993). Imported stays authoritative.
294
+ const resolvedProfileArn = imported.profileArn ?? identity.profileArn;
295
+ const metadata: KiroOAuthMetadata | undefined = (() => {
296
+ const base = metadataFromImported(imported) ?? {};
297
+ if (resolvedProfileArn && !base.profileArn) base.profileArn = resolvedProfileArn;
298
+ return Object.keys(base).length > 0 ? base : undefined;
299
+ })();
256
300
  return {
257
301
  access: imported.access,
258
302
  refresh: imported.refresh,
259
303
  expires: imported.expires,
260
304
  source: imported.source === "json" ? "credential-file" : "local-cli",
261
- ...(imported.profileArn ? { accountId: imported.profileArn } : {}),
305
+ ...(resolvedProfileArn ? { accountId: resolvedProfileArn } : {}),
262
306
  ...(identity.email ? { email: identity.email } : {}),
263
307
  ...(metadata ? { kiro: metadata } : {}),
264
308
  };
@@ -29,7 +29,14 @@ import {
29
29
  import { validateCopilotApiBaseUrl } from "./github-copilot";
30
30
  import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
31
31
 
32
- type AuthStore = Record<string, ProviderAccountSet>;
32
+ export type AuthStore = Record<string, ProviderAccountSet>;
33
+
34
+ export type AuthStoreBufferSnapshot =
35
+ | { readonly kind: "ready"; readonly store: AuthStore }
36
+ | { readonly kind: "absent" }
37
+ | { readonly kind: "malformed" };
38
+
39
+ const authStoreDecoder = new TextDecoder("utf-8", { fatal: true });
33
40
  let lastReconciledGeneration = 0;
34
41
  let liveOAuthAccountKeys = new Set<string>();
35
42
 
@@ -142,6 +149,27 @@ export function loadAuthStore(): AuthStore {
142
149
  return loadAuthStoreInternal().store;
143
150
  }
144
151
 
152
+ /**
153
+ * Pure normalization for auth-store bytes already read by another owner.
154
+ * This function performs no filesystem consultation, hardening, backup, or persistence.
155
+ */
156
+ export function normalizeAuthStoreBuffer(buffer: Uint8Array | null): AuthStoreBufferSnapshot {
157
+ if (buffer === null) return { kind: "absent" };
158
+ try {
159
+ const parsed: unknown = JSON.parse(authStoreDecoder.decode(buffer));
160
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
161
+ return { kind: "malformed" };
162
+ }
163
+ const { store } = normalizeAuthStore(parsed);
164
+ if (Object.keys(parsed).length > 0 && Object.keys(store).length === 0) {
165
+ return { kind: "malformed" };
166
+ }
167
+ return { kind: "ready", store };
168
+ } catch {
169
+ return { kind: "malformed" };
170
+ }
171
+ }
172
+
145
173
  /**
146
174
  * Observe-only auth store read for diagnostics (`ocx doctor` / status).
147
175
  * Does not chmod paths or backup invalid JSON — corrupt files are treated as empty.
@@ -149,11 +177,8 @@ export function loadAuthStore(): AuthStore {
149
177
  export function peekAuthStore(): AuthStore {
150
178
  const path = getAuthStorePath();
151
179
  if (!existsSync(path)) return {};
152
- try {
153
- return normalizeAuthStore(JSON.parse(readFileSync(path, "utf-8"))).store;
154
- } catch {
155
- return {};
156
- }
180
+ const snapshot = normalizeAuthStoreBuffer(readFileSync(path));
181
+ return snapshot.kind === "ready" ? snapshot.store : {};
157
182
  }
158
183
 
159
184
  function persist(store: AuthStore): void {
@@ -30,6 +30,7 @@ import { getMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "../codex/main-accoun
30
30
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
31
31
  import { providerCodexAccountMode } from "../providers/registry";
32
32
  import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
33
+ import { tryAcquireNativeMainProfileClaim } from "../codex/native-main-admission";
33
34
 
34
35
  export interface TokenGuardianHandle {
35
36
  stop(): void;
@@ -175,11 +176,14 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
175
176
  ) {
176
177
  const mode = providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) ?? "pool";
177
178
  if (opts.codexWarmupEnabled) {
178
- const mainToken = getMainAccountToken();
179
179
  const key = `codex:${MAIN_CODEX_ACCOUNT_ID}`;
180
- if (mainToken && !inBackoff(key, nowMs)) {
180
+ if (!inBackoff(key, nowMs)) {
181
181
  tasks.push(async () => {
182
+ const nativeMainLease = tryAcquireNativeMainProfileClaim();
183
+ if (!nativeMainLease) return;
182
184
  try {
185
+ const mainToken = getMainAccountToken();
186
+ if (!mainToken) return;
183
187
  await warmCodexAccount({
184
188
  accessToken: mainToken.accessToken,
185
189
  chatgptAccountId: mainToken.chatgptAccountId,
@@ -190,9 +194,11 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
190
194
  } catch (err) {
191
195
  recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, false, writerGeneration);
192
196
  result.failed.push(key);
197
+ } finally {
198
+ nativeMainLease.release();
193
199
  }
194
200
  });
195
- } else if (mainToken) {
201
+ } else {
196
202
  result.skippedBackoff.push(key);
197
203
  }
198
204
  }
@@ -0,0 +1,288 @@
1
+ export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
2
+ plus: 1,
3
+ business: 1,
4
+ prolite: 5,
5
+ pro: 20,
6
+ } as const;
7
+
8
+ /** Match the provider-report last-good freshness bound. */
9
+ export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
10
+
11
+ export type CodexCapacityQuota = {
12
+ fiveHourPercent?: number;
13
+ fiveHourResetAt?: number;
14
+ weeklyPercent?: number;
15
+ weeklyResetAt?: number;
16
+ monthlyPercent?: number;
17
+ monthlyResetAt?: number;
18
+ customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
19
+ updatedAt: number;
20
+ };
21
+
22
+ export interface CodexCapacityAccount {
23
+ isMain: boolean;
24
+ active?: boolean;
25
+ plan?: string | null;
26
+ paused: boolean;
27
+ needsReauth?: boolean;
28
+ quota: CodexCapacityQuota | null;
29
+ }
30
+
31
+ export interface CodexCapacityWindowAggregation {
32
+ usedPercent: number;
33
+ includedAccounts: number;
34
+ excludedAccounts: number;
35
+ incomplete: boolean;
36
+ /** Internal calculation evidence; stripped from the management API response. */
37
+ totalWeight?: number;
38
+ /** Internal calculation evidence; stripped from the management API response. */
39
+ consumedWeight?: number;
40
+ /** Internal calculation evidence; stripped from the management API response. */
41
+ remainingWeight?: number;
42
+ updatedAt: number;
43
+ nextRecoveryAt?: number;
44
+ nextRecoveryPercent?: number;
45
+ }
46
+
47
+ export interface CodexCapacityAggregation {
48
+ kind: "capacity-weighted-v1";
49
+ scope: "routable-known";
50
+ includedAccounts: number;
51
+ excludedAccounts: number;
52
+ unknownPlanAccounts: number;
53
+ missingQuotaAccounts: number;
54
+ pausedAccounts: number;
55
+ reauthAccounts: number;
56
+ staleQuotaAccounts: number;
57
+ partialWindowAccounts: number;
58
+ incomplete: boolean;
59
+ presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
60
+ fiveHour?: CodexCapacityWindowAggregation;
61
+ weekly?: CodexCapacityWindowAggregation;
62
+ monthly?: CodexCapacityWindowAggregation;
63
+ customWindows?: Array<CodexCapacityWindowAggregation & { label: string }>;
64
+ currentAccount?: {
65
+ isMain: boolean;
66
+ plan?: string | null;
67
+ quota: CodexCapacityQuota | null;
68
+ };
69
+ }
70
+
71
+ export interface CodexCapacityResult {
72
+ quota: CodexCapacityQuota | null;
73
+ aggregation: CodexCapacityAggregation | null;
74
+ currentAccount?: CodexCapacityAggregation["currentAccount"];
75
+ }
76
+
77
+ type MutableWindow = {
78
+ totalWeight: number;
79
+ consumedWeight: number;
80
+ includedAccounts: number;
81
+ recoveries: Map<number, number>;
82
+ oldestUpdatedAt: number;
83
+ };
84
+
85
+ function configuredWeight(plan: string | null | undefined): number | undefined {
86
+ const normalized = plan?.trim().toLowerCase();
87
+ return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
88
+ ? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
89
+ : undefined;
90
+ }
91
+
92
+ function normalizedPercent(value: unknown): number | undefined {
93
+ return typeof value === "number" && Number.isFinite(value)
94
+ ? Math.max(0, Math.min(100, value))
95
+ : undefined;
96
+ }
97
+
98
+ function futureResetMs(value: unknown, now: number): number | undefined {
99
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
100
+ const milliseconds = value > 10_000_000_000 ? value : value * 1000;
101
+ return milliseconds > now ? milliseconds : undefined;
102
+ }
103
+
104
+ function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
105
+ if (!quota) return false;
106
+ return normalizedPercent(quota.fiveHourPercent) !== undefined
107
+ || normalizedPercent(quota.weeklyPercent) !== undefined
108
+ || normalizedPercent(quota.monthlyPercent) !== undefined
109
+ || !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
110
+ }
111
+
112
+ function currentQuotaForDisplay(account: CodexCapacityAccount, now: number): CodexCapacityQuota | null {
113
+ const quota = account.quota;
114
+ const fresh = !!quota
115
+ && Number.isFinite(quota.updatedAt)
116
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
117
+ return !account.paused && !account.needsReauth && fresh && hasKnownQuotaWindow(quota) ? quota : null;
118
+ }
119
+
120
+ function addWindow(
121
+ windows: Map<string, MutableWindow>,
122
+ key: string,
123
+ weight: number,
124
+ percent: number,
125
+ resetAt: number | undefined,
126
+ updatedAt: number,
127
+ ): void {
128
+ const window = windows.get(key) ?? {
129
+ totalWeight: 0,
130
+ consumedWeight: 0,
131
+ includedAccounts: 0,
132
+ recoveries: new Map<number, number>(),
133
+ oldestUpdatedAt: updatedAt,
134
+ };
135
+ const consumed = weight * percent / 100;
136
+ window.totalWeight += weight;
137
+ window.consumedWeight += consumed;
138
+ window.includedAccounts += 1;
139
+ window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
140
+ if (resetAt !== undefined && consumed > 0) {
141
+ window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
142
+ }
143
+ windows.set(key, window);
144
+ }
145
+
146
+ function finalizeWindow(window: MutableWindow, totalAccounts: number): CodexCapacityWindowAggregation {
147
+ const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
148
+ const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
149
+ return {
150
+ usedPercent: window.consumedWeight / window.totalWeight * 100,
151
+ includedAccounts: window.includedAccounts,
152
+ excludedAccounts: totalAccounts - window.includedAccounts,
153
+ incomplete: window.includedAccounts < totalAccounts,
154
+ totalWeight: window.totalWeight,
155
+ consumedWeight: window.consumedWeight,
156
+ remainingWeight: window.totalWeight - window.consumedWeight,
157
+ updatedAt: window.oldestUpdatedAt,
158
+ ...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
159
+ ...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
160
+ };
161
+ }
162
+
163
+ /** Display-only configured-weight estimate. It never participates in account selection or routing. */
164
+ export function aggregateCodexPoolCapacity(
165
+ accounts: readonly CodexCapacityAccount[],
166
+ now = Date.now(),
167
+ ): CodexCapacityResult {
168
+ const current = accounts.find(account => account.active)
169
+ ?? accounts.find(account => account.isMain)
170
+ ?? accounts[0];
171
+ const currentAccount = current ? {
172
+ isMain: current.isMain,
173
+ ...(current.plan !== undefined ? { plan: current.plan } : {}),
174
+ quota: currentQuotaForDisplay(current, now),
175
+ } : undefined;
176
+ const windows = new Map<string, MutableWindow>();
177
+ const included = new Set<CodexCapacityAccount>();
178
+ const contributions = new Map<CodexCapacityAccount, Set<string>>();
179
+ let unknownPlanAccounts = 0;
180
+ let missingQuotaAccounts = 0;
181
+ let pausedAccounts = 0;
182
+ let reauthAccounts = 0;
183
+ let staleQuotaAccounts = 0;
184
+
185
+ for (const account of accounts) {
186
+ const weight = configuredWeight(account.plan);
187
+ if (weight === undefined) unknownPlanAccounts += 1;
188
+ if (account.paused) pausedAccounts += 1;
189
+ if (account.needsReauth) reauthAccounts += 1;
190
+ const quota = account.quota;
191
+ const quotaFresh = !!quota
192
+ && Number.isFinite(quota.updatedAt)
193
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
194
+ if (quota && !quotaFresh) staleQuotaAccounts += 1;
195
+ const standard = quota ? [
196
+ ["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
197
+ ["weekly", quota.weeklyPercent, quota.weeklyResetAt],
198
+ ["monthly", quota.monthlyPercent, quota.monthlyResetAt],
199
+ ] as const : [];
200
+ const custom = quota?.customWindows ?? [];
201
+ const hasQuota = hasKnownQuotaWindow(quota);
202
+ if (!hasQuota) missingQuotaAccounts += 1;
203
+ if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
204
+
205
+ let contributed = false;
206
+ const contributionKeys = new Set<string>();
207
+ for (const [key, rawPercent, rawReset] of standard) {
208
+ const percent = normalizedPercent(rawPercent);
209
+ if (percent === undefined) continue;
210
+ addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
211
+ contributionKeys.add(key);
212
+ contributed = true;
213
+ }
214
+ for (const customWindow of custom) {
215
+ const percent = normalizedPercent(customWindow.percent);
216
+ if (percent === undefined) continue;
217
+ addWindow(windows, `custom:${customWindow.label}`, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
218
+ contributionKeys.add(`custom:${customWindow.label}`);
219
+ contributed = true;
220
+ }
221
+ if (contributed) {
222
+ included.add(account);
223
+ contributions.set(account, contributionKeys);
224
+ }
225
+ }
226
+
227
+ if (windows.size === 0) {
228
+ if (accounts.length === 0) return { quota: null, aggregation: null, ...(currentAccount ? { currentAccount } : {}) };
229
+ const aggregation: CodexCapacityAggregation = {
230
+ kind: "capacity-weighted-v1",
231
+ scope: "routable-known",
232
+ includedAccounts: 0,
233
+ excludedAccounts: accounts.length,
234
+ unknownPlanAccounts,
235
+ missingQuotaAccounts,
236
+ pausedAccounts,
237
+ reauthAccounts,
238
+ staleQuotaAccounts,
239
+ partialWindowAccounts: 0,
240
+ incomplete: true,
241
+ ...(currentAccount ? { currentAccount } : {}),
242
+ };
243
+ return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
244
+ }
245
+ const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!, accounts.length) : undefined;
246
+ const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!, accounts.length) : undefined;
247
+ const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!, accounts.length) : undefined;
248
+ const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
249
+ ? [{ label: key.slice("custom:".length), ...finalizeWindow(window, accounts.length) }]
250
+ : []);
251
+ const quota: CodexCapacityQuota = {
252
+ ...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
253
+ ...(weekly ? { weeklyPercent: weekly.usedPercent } : {}),
254
+ ...(monthly ? { monthlyPercent: monthly.usedPercent } : {}),
255
+ ...(customWindows.length > 0 ? {
256
+ customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
257
+ } : {}),
258
+ updatedAt: Math.min(
259
+ ...[fiveHour, weekly, monthly, ...customWindows]
260
+ .flatMap(window => window ? [window.updatedAt] : []),
261
+ ),
262
+ };
263
+ const visibleWindowKeys = [...windows.keys()];
264
+ const partialWindowAccounts = accounts.filter(account => {
265
+ const keys = contributions.get(account);
266
+ return !!keys && visibleWindowKeys.some(key => !keys.has(key));
267
+ }).length;
268
+ const excludedAccounts = accounts.length - included.size;
269
+ const aggregation: CodexCapacityAggregation = {
270
+ kind: "capacity-weighted-v1",
271
+ scope: "routable-known",
272
+ includedAccounts: included.size,
273
+ excludedAccounts,
274
+ unknownPlanAccounts,
275
+ missingQuotaAccounts,
276
+ pausedAccounts,
277
+ reauthAccounts,
278
+ staleQuotaAccounts,
279
+ partialWindowAccounts,
280
+ incomplete: excludedAccounts > 0 || partialWindowAccounts > 0,
281
+ ...(fiveHour ? { fiveHour } : {}),
282
+ ...(weekly ? { weekly } : {}),
283
+ ...(monthly ? { monthly } : {}),
284
+ ...(customWindows.length > 0 ? { customWindows } : {}),
285
+ ...(currentAccount ? { currentAccount } : {}),
286
+ };
287
+ return { quota, aggregation, ...(currentAccount ? { currentAccount } : {}) };
288
+ }
@@ -6,6 +6,7 @@ export interface DerivedKeyLoginProvider {
6
6
  baseUrl: string;
7
7
  responsesPath?: string;
8
8
  adapter: string;
9
+ apiKeyValidation?: "unknown";
9
10
  apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
10
11
  dashboardUrl: string;
11
12
  models?: string[];
@@ -22,6 +23,7 @@ export interface DerivedKeyLoginProvider {
22
23
  modelDefaultReasoningEfforts?: Record<string, string>;
23
24
  reasoningEffortMap?: Record<string, string>;
24
25
  modelReasoningEffortMap?: Record<string, Record<string, string>>;
26
+ reasoningWireFormat?: OcxProviderConfig["reasoningWireFormat"];
25
27
  noVisionModels?: string[];
26
28
  noReasoningModels?: string[];
27
29
  noTemperatureModels?: string[];
@@ -102,13 +104,20 @@ function cloneNestedRecord(input: Record<string, Record<string, string>>): Recor
102
104
  return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
103
105
  }
104
106
 
107
+ /**
108
+ * Build the provider config a registry entry contributes when a preset is materialized.
109
+ * The registry auth kind is preserved verbatim (including `"local"`) so fail-closed gates
110
+ * keep distinguishing local runtimes from API-key providers after the seed round-trip.
111
+ */
105
112
  export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
106
113
  return {
107
114
  adapter: entry.adapter,
108
115
  baseUrl: entry.baseUrl,
109
116
  ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
110
117
  ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
111
- authMode: entry.authKind === "local" ? undefined : entry.authKind,
118
+ // Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
119
+ // distinguish local runtimes from API-key providers keep working after the seed round-trip.
120
+ authMode: entry.authKind,
112
121
  ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
113
122
  ...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
114
123
  ...(entry.freeTier !== undefined ? { freeTier: entry.freeTier } : {}),
@@ -128,6 +137,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
128
137
  ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
129
138
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
130
139
  ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
140
+ ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
131
141
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
132
142
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
133
143
  ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
@@ -159,6 +169,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
159
169
  baseUrl: entry.baseUrl,
160
170
  ...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
161
171
  adapter: entry.adapter,
172
+ ...(entry.apiKeyValidation !== undefined ? { apiKeyValidation: entry.apiKeyValidation } : {}),
162
173
  ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
163
174
  dashboardUrl: entry.dashboardUrl,
164
175
  ...(entry.models ? { models: [...entry.models] } : {}),
@@ -175,6 +186,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
175
186
  ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
176
187
  ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
177
188
  ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
189
+ ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
178
190
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
179
191
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
180
192
  ...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
@@ -248,6 +260,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
248
260
  if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
249
261
  if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
250
262
  if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
263
+ if (prov.reasoningWireFormat === undefined && seed.reasoningWireFormat !== undefined) prov.reasoningWireFormat = seed.reasoningWireFormat;
251
264
  if (!prov.noVisionModels && seed.noVisionModels) prov.noVisionModels = [...seed.noVisionModels];
252
265
  if (!prov.noReasoningModels && seed.noReasoningModels) prov.noReasoningModels = [...seed.noReasoningModels];
253
266
  if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
@@ -259,6 +272,25 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
259
272
  // learned this route still gets backfilled.
260
273
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
261
274
  if (prov.statelessResponses === undefined && seed.statelessResponses !== undefined) prov.statelessResponses = seed.statelessResponses;
275
+ // Registry-only metadata (never seeded into saved config): backfill straight from
276
+ // the entry so an explicit user value stays distinguishable from the default.
277
+ if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
278
+ if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
279
+ // Registry-only repair policy (#938): fill only when the runtime provider has
280
+ // no explicit policy, and deep-clone so saved/user values never alias the
281
+ // registry constant.
282
+ if (prov.responsesItemIdRepair === undefined && entry.responsesItemIdRepair) {
283
+ prov.responsesItemIdRepair = {
284
+ ...(entry.responsesItemIdRepair.message ? { message: [...entry.responsesItemIdRepair.message] } : {}),
285
+ ...(entry.responsesItemIdRepair.reasoning ? { reasoning: [...entry.responsesItemIdRepair.reasoning] } : {}),
286
+ ...(entry.responsesItemIdRepair.repairMissingTerminalIds !== undefined
287
+ ? { repairMissingTerminalIds: entry.responsesItemIdRepair.repairMissingTerminalIds }
288
+ : {}),
289
+ ...(entry.responsesItemIdRepair.repairInvalidIds !== undefined
290
+ ? { repairInvalidIds: entry.responsesItemIdRepair.repairInvalidIds }
291
+ : {}),
292
+ };
293
+ }
262
294
  if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
263
295
  if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
264
296
  if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];