@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -10,7 +10,7 @@
10
10
  * Exceptions:
11
11
  * - `chatgpt` stays single-slot (always replaced): codex-auth-api uses it as a scratch slot
12
12
  * for Codex pool logins, which have their own ledger (codex-accounts.json).
13
- * - Credentials without identity (no accountId/email — e.g. kiro) replace the active slot
13
+ * - Credentials without identity (no accountId/email) replace the active slot
14
14
  * instead of appending: their refresh tokens rotate, so a derived id would duplicate the
15
15
  * same human on every re-login. Kimi extracts JWT `user_id`/`sub` as accountId; Cursor
16
16
  * extracts JWT `sub` — both append distinct accounts under multiauth.
@@ -19,6 +19,8 @@ import { createHash, randomUUID } from "node:crypto";
19
19
  import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
22
+ import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
23
+ import { recordOwnedConfigPath } from "../lib/config-ownership";
22
24
  import { validateCopilotApiBaseUrl } from "./github-copilot";
23
25
  import type { OAuthCredentialSource, OAuthCredentials, ProviderAccount, ProviderAccountSet } from "./types";
24
26
 
@@ -130,6 +132,7 @@ export function peekAuthStore(): AuthStore {
130
132
 
131
133
  function persist(store: AuthStore): void {
132
134
  const dir = getConfigDir();
135
+ assertNotRealHomeUnderTest(dir);
133
136
  if (!existsSync(dir)) {
134
137
  mkdirSync(dir, { recursive: true, mode: 0o700 });
135
138
  } else {
@@ -150,7 +153,7 @@ function sameFd(a: LockSnapshot,b: ReturnType<typeof fstatSync>): boolean { retu
150
153
  export function createOAuthFileLock(options: OAuthFileLockOptions): { acquire(): Promise<OAuthFileLockGuard> } {
151
154
  const wait=options.waitTimeoutMs??5000, stale=options.staleAfterMs??120000, min=options.pollMinMs??25,max=options.pollMaxMs??100,sleep=options.sleep??(ms=>Bun.sleep(ms)),now=options.now??Date.now,random=options.random??Math.random,write=options.writeMetadata??((fd,b)=>writeFileSync(fd,b,"utf8"));
152
155
  if(wait<0||stale<=0||min<0||max<min) throw new OAuthFileLockError("Invalid OAuth file-lock timing options");
153
- return { async acquire() { hardenConfigDir(); if(!existsSync(getConfigDir())) mkdirSync(getConfigDir(),{recursive:true,mode:0o700}); const ownerId=randomUUID(),started=now(); for(;;){ let fd:number|undefined; try { fd=openSync(options.path,"wx",0o600); const bytes=`${JSON.stringify({version:1,ownerId,pid:process.pid,createdAt:now()})}\n`; write(fd,bytes); const fs=fstatSync(fd); closeSync(fd); fd=undefined; const owned=snapshot(options.path); if(owned.bytes!==bytes||!sameFd(owned,fs)) throw new OAuthFileLockError("OAuth lock changed during creation"); let released=false; return {ownerId,release(){if(released)return;released=true;try{const a=snapshot(options.path);if(!sameSnapshot(owned,a))return;options.beforeReleaseUnlink?.();const b=snapshot(options.path);if(sameSnapshot(owned,b))unlinkSync(options.path);}catch(e){if(errorCode(e)!=="ENOENT")console.warn(`[oauth] lock release failed: ${e instanceof Error?e.message:String(e)}`);}}}; } catch(e) { if(fd!==undefined){let fs;try{fs=fstatSync(fd);}catch{}try{closeSync(fd);}catch{}if(fs)try{const a=snapshot(options.path);if(sameFd(a,fs)){options.beforeFailedCreateUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b)&&sameFd(b,fs))unlinkSync(options.path);}}catch{}} if(errorCode(e)!=="EEXIST")throw e instanceof OAuthFileLockError?e:new OAuthFileLockError("Could not create OAuth file lock",{cause:e}); }
156
+ return { async acquire() { hardenConfigDir(); recordOwnedConfigPath(getConfigDir(),options.path); if(!existsSync(getConfigDir())) mkdirSync(getConfigDir(),{recursive:true,mode:0o700}); const ownerId=randomUUID(),started=now(); for(;;){ let fd:number|undefined; try { fd=openSync(options.path,"wx",0o600); const bytes=`${JSON.stringify({version:1,ownerId,pid:process.pid,createdAt:now()})}\n`; write(fd,bytes); const fs=fstatSync(fd); closeSync(fd); fd=undefined; const owned=snapshot(options.path); if(owned.bytes!==bytes||!sameFd(owned,fs)) throw new OAuthFileLockError("OAuth lock changed during creation"); let released=false; return {ownerId,release(){if(released)return;released=true;try{const a=snapshot(options.path);if(!sameSnapshot(owned,a))return;options.beforeReleaseUnlink?.();const b=snapshot(options.path);if(sameSnapshot(owned,b))unlinkSync(options.path);}catch(e){if(errorCode(e)!=="ENOENT")console.warn(`[oauth] lock release failed: ${e instanceof Error?e.message:String(e)}`);}}}; } catch(e) { if(fd!==undefined){let fs;try{fs=fstatSync(fd);}catch{}try{closeSync(fd);}catch{}if(fs)try{const a=snapshot(options.path);if(sameFd(a,fs)){options.beforeFailedCreateUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b)&&sameFd(b,fs))unlinkSync(options.path);}}catch{}} if(errorCode(e)!=="EEXIST")throw e instanceof OAuthFileLockError?e:new OAuthFileLockError("Could not create OAuth file lock",{cause:e}); }
154
157
  try{const a=snapshot(options.path);let created=a.mtimeMs;try{const p=JSON.parse(a.bytes);if(typeof p.createdAt==="number")created=Math.max(created,p.createdAt);}catch{}if(now()-created>stale){options.beforeStaleUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b))unlinkSync(options.path);continue;}}catch(e){if(errorCode(e)==="ENOENT")continue;throw new OAuthFileLockError("Could not inspect OAuth file lock",{cause:e});} const elapsed=now()-started;if(elapsed>=wait)throw new OAuthFileLockError(`Timed out after ${wait}ms waiting for OAuth file lock`);await sleep(Math.min(wait-elapsed,min+Math.floor(random()*(max-min+1)))); } } };
155
158
  }
156
159
  /** Wait long enough for slow IdP refreshes (e.g. Cursor 15s × 3 attempts) before timing out. */
@@ -206,6 +209,28 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
206
209
  const validated = validateCopilotApiBaseUrl(candidate.apiBaseUrl);
207
210
  if (validated) normalized.apiBaseUrl = validated;
208
211
  }
212
+ if (candidate.kiro && typeof candidate.kiro === "object") {
213
+ const kiro = candidate.kiro;
214
+ const clean = (value: unknown, max: number): string | undefined => {
215
+ if (typeof value !== "string") return undefined;
216
+ const trimmed = value.trim();
217
+ return trimmed && trimmed.length <= max && !/[\x00-\x1f\x7f]/.test(trimmed) ? trimmed : undefined;
218
+ };
219
+ const profileArn = clean(kiro.profileArn, 1024);
220
+ const ssoRegion = clean(kiro.ssoRegion, 64);
221
+ const apiRegion = clean(kiro.apiRegion, 64);
222
+ const clientId = clean(kiro.clientId, 4096);
223
+ const clientSecret = clean(kiro.clientSecret, 4096);
224
+ if (profileArn || ssoRegion || apiRegion || clientId || clientSecret) {
225
+ normalized.kiro = {
226
+ ...(profileArn ? { profileArn } : {}),
227
+ ...(ssoRegion ? { ssoRegion } : {}),
228
+ ...(apiRegion ? { apiRegion } : {}),
229
+ ...(clientId ? { clientId } : {}),
230
+ ...(clientSecret ? { clientSecret } : {}),
231
+ };
232
+ }
233
+ }
209
234
  return normalized;
210
235
  }
211
236
 
@@ -293,7 +318,11 @@ export function getCredential(provider: string): OAuthCredentials | null {
293
318
  * (rotating refresh tokens would fabricate duplicates) and single-slot providers replace the
294
319
  * active slot / whole set instead.
295
320
  */
296
- export async function saveCredential(provider: string, cred: OAuthCredentials): Promise<void> {
321
+ export async function saveCredential(
322
+ provider: string,
323
+ cred: OAuthCredentials,
324
+ opts: { preserveIdentityless?: boolean } = {},
325
+ ): Promise<void> {
297
326
  const safe = normalizeCredential(cred);
298
327
  if (!safe) return;
299
328
  await mutateStore(store => {
@@ -317,7 +346,7 @@ export async function saveCredential(provider: string, cred: OAuthCredentials):
317
346
  // active identity-less row in place prevents a stale duplicate that stays selectable
318
347
  // and would re-refresh into a second row with the same identity.
319
348
  const active = set.accounts.find(a => a.id === set.activeAccountId);
320
- if (active && active.credential.accountId === undefined && active.credential.email === undefined) {
349
+ if (!opts.preserveIdentityless && active && active.credential.accountId === undefined && active.credential.email === undefined) {
321
350
  active.credential = safe;
322
351
  delete active.needsReauth;
323
352
  return;
@@ -418,6 +447,29 @@ export async function removeAccount(provider: string, accountId: string): Promis
418
447
  });
419
448
  }
420
449
 
450
+ /** Replace or clear a provider account set (used for transactional Kiro add-account rollback). */
451
+ export async function replaceProviderAccountSet(
452
+ provider: string,
453
+ set: ProviderAccountSet | null,
454
+ ): Promise<void> {
455
+ await mutateStore(store => {
456
+ if (!set || set.accounts.length === 0) {
457
+ delete store[provider];
458
+ return;
459
+ }
460
+ store[provider] = {
461
+ activeAccountId: set.activeAccountId,
462
+ accounts: set.accounts.map(account => ({
463
+ id: account.id,
464
+ credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) },
465
+ ...(account.alias ? { alias: account.alias } : {}),
466
+ ...(account.needsReauth ? { needsReauth: true } : {}),
467
+ ...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
468
+ })),
469
+ };
470
+ });
471
+ }
472
+
421
473
  export async function markAccountNeedsReauth(provider: string, accountId: string, needsReauth: boolean): Promise<void> {
422
474
  await mutateStore(store => {
423
475
  const account = store[provider]?.accounts.find(a => a.id === accountId);
@@ -1,6 +1,15 @@
1
1
  /** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
2
2
  export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
3
3
 
4
+ /** Account-scoped Kiro data required for refresh and request routing. */
5
+ export interface KiroOAuthMetadata {
6
+ profileArn?: string;
7
+ ssoRegion?: string;
8
+ apiRegion?: string;
9
+ clientId?: string;
10
+ clientSecret?: string;
11
+ }
12
+
4
13
  export type OAuthCredentials = {
5
14
  refresh: string;
6
15
  access: string;
@@ -15,6 +24,8 @@ export type OAuthCredentials = {
15
24
  * Never reuse for Antigravity projectId; validated on write and again at request time.
16
25
  */
17
26
  apiBaseUrl?: string;
27
+ /** Never returned by management APIs; persisted only inside the protected auth-store boundary. */
28
+ kiro?: KiroOAuthMetadata;
18
29
  };
19
30
 
20
31
  /** One logged-in account inside a provider's account set (multiauth). */
@@ -2,6 +2,7 @@ import { ALIBABA_INTL_BASE_URL_CHOICES } from "./base-url-choices";
2
2
  import { providerConfigSeed } from "./derive";
3
3
  import { rewriteProviderReferences } from "./provider-id-rewrite";
4
4
  import { PROVIDER_REGISTRY } from "./registry";
5
+ import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
5
6
  import type { OcxConfig, OcxProviderConfig } from "../types";
6
7
 
7
8
  const BEIJING_ID = "alibaba-token-plan";
@@ -91,9 +92,10 @@ function buildIntlRow(source: OcxProviderConfig): OcxProviderConfig {
91
92
  * those contracts across. That override fix shipped once, was backed out twice,
92
93
  * and was refused again when PR #459 closed.
93
94
  *
94
- * Refuses to act when the destination already holds a value a provider row or a
95
- * key the reference rewrite would land on because merging two settings is a
96
- * user decision, not a migration's.
95
+ * Refuses to act when the destination is reserved by an account namespace or
96
+ * already holds a value a provider row or a key the reference rewrite would
97
+ * land on — because choosing which setting wins is a user decision, not a
98
+ * migration's.
97
99
  */
98
100
  export function projectAlibabaRegionMigration(config: OcxConfig): AlibabaRegionMigrationProjection {
99
101
  const beijing = config.providers[BEIJING_ID];
@@ -101,6 +103,17 @@ export function projectAlibabaRegionMigration(config: OcxConfig): AlibabaRegionM
101
103
  if (!beijing || !savedBaseUrl || !isInternationalEndpoint(savedBaseUrl)) {
102
104
  return { config, changed: false, warnings: [] };
103
105
  }
106
+ if (codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, INTL_ID)) {
107
+ return {
108
+ config,
109
+ changed: false,
110
+ warnings: [
111
+ `provider "${BEIJING_ID}" needs to move to "${INTL_ID}", but that destination is reserved `
112
+ + `by a configured Codex account namespace. Nothing was changed. Rename the account `
113
+ + `selector or move the provider manually, then restart.`,
114
+ ],
115
+ };
116
+ }
104
117
  if (config.providers[INTL_ID]) {
105
118
  return {
106
119
  config,
@@ -13,6 +13,7 @@ const ANTIGRAVITY_WIRE_MODELS = [
13
13
  "gemini-3.6-flash-high",
14
14
  "gemini-3.1-pro-low",
15
15
  "gemini-pro-agent",
16
+ "gemini-3.1-flash-image",
16
17
  "claude-sonnet-4-6",
17
18
  "claude-opus-4-6-thinking",
18
19
  "gpt-oss-120b-medium",
@@ -85,6 +86,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
85
86
  export const ANTIGRAVITY_MODELS = [
86
87
  "gemini-3.6-flash",
87
88
  "gemini-3.1-pro",
89
+ "gemini-3.1-flash-image",
88
90
  "claude-sonnet-4-6",
89
91
  "claude-opus-4-6-thinking",
90
92
  "gpt-oss-120b-medium",
@@ -97,6 +99,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
97
99
  "gemini-3.6-flash-high": 1_048_576,
98
100
  "gemini-3.1-pro-low": 1_048_576,
99
101
  "gemini-pro-agent": 1_048_576,
102
+ "gemini-3.1-flash-image": 1_048_576,
100
103
  "claude-sonnet-4-6": 200_000,
101
104
  "claude-opus-4-6-thinking": 1_000_000,
102
105
  "gpt-oss-120b-medium": 131_072,
@@ -30,7 +30,7 @@ export function maskApiKey(value: string): string {
30
30
  }
31
31
 
32
32
  /** Content-derived id: re-adding the same key upserts instead of duplicating. */
33
- function keyId(key: string): string {
33
+ export function apiKeyPoolEntryId(key: string): string {
34
34
  return createHash("sha256").update(key).digest("hex").slice(0, 8);
35
35
  }
36
36
 
@@ -39,11 +39,18 @@ export function isKeyAuthProvider(provider: OcxProviderConfig): boolean {
39
39
  return provider.authMode !== "oauth" && provider.authMode !== "forward";
40
40
  }
41
41
 
42
+ /** Trim and reject blank / CRLF-bearing secrets. Shared by pool writes and OAuth upsert. */
43
+ export function sanitizeApiKeyValue(value: unknown): string | undefined {
44
+ if (typeof value !== "string") return undefined;
45
+ const trimmed = value.trim();
46
+ return trimmed && !/[\r\n]/.test(trimmed) ? trimmed : undefined;
47
+ }
48
+
42
49
  /** Seed the pool from a legacy bare `apiKey`, and keep `apiKey` mirrored to the active entry. */
43
50
  function ensurePool(provider: OcxProviderConfig): NonNullable<OcxProviderConfig["apiKeyPool"]> {
44
51
  if (!provider.apiKeyPool) provider.apiKeyPool = [];
45
52
  if (provider.apiKeyPool.length === 0 && provider.apiKey) {
46
- provider.apiKeyPool.push({ id: keyId(provider.apiKey), key: provider.apiKey });
53
+ provider.apiKeyPool.push({ id: apiKeyPoolEntryId(provider.apiKey), key: provider.apiKey });
47
54
  }
48
55
  return provider.apiKeyPool;
49
56
  }
@@ -75,11 +82,11 @@ export function listProviderApiKeys(config: OcxConfig, name: string): { activeId
75
82
  export function addProviderApiKey(config: OcxConfig, name: string, key: string, label?: string): { id: string } | { error: string } {
76
83
  const provider = config.providers[name];
77
84
  if (!provider || !isKeyAuthProvider(provider)) return { error: "provider does not use API-key auth" };
78
- const trimmed = key.trim();
79
- if (!trimmed) return { error: "key is required" };
80
- if (/[\r\n]/.test(trimmed)) return { error: "key must not include line breaks" };
85
+ if (typeof key !== "string" || !key.trim()) return { error: "key is required" };
86
+ const trimmed = sanitizeApiKeyValue(key);
87
+ if (!trimmed) return { error: "key must not include line breaks" };
81
88
  const pool = ensurePool(provider);
82
- const id = keyId(trimmed);
89
+ const id = apiKeyPoolEntryId(trimmed);
83
90
  const existing = pool.find(e => e.id === id);
84
91
  if (existing) {
85
92
  if (label?.trim()) existing.label = label.trim();
@@ -1,10 +1,11 @@
1
1
  import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
- import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "./registry";
2
+ import { PROVIDER_REGISTRY, providerMatchesRegistryTransport, type ProviderRegistryEntry } from "./registry";
3
3
 
4
4
  export interface DerivedKeyLoginProvider {
5
5
  label: string;
6
6
  baseUrl: string;
7
7
  adapter: string;
8
+ apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
8
9
  dashboardUrl: string;
9
10
  models?: string[];
10
11
  liveModels?: boolean;
@@ -103,6 +104,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
103
104
  return {
104
105
  adapter: entry.adapter,
105
106
  baseUrl: entry.baseUrl,
107
+ ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
106
108
  authMode: entry.authKind === "local" ? undefined : entry.authKind,
107
109
  ...(entry.codexAccountMode ? { codexAccountMode: entry.codexAccountMode } : {}),
108
110
  ...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
@@ -129,6 +131,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
129
131
  ...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
130
132
  ...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
131
133
  ...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
134
+ ...(entry.promptCacheKey !== undefined ? { promptCacheKey: entry.promptCacheKey } : {}),
132
135
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
133
136
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
134
137
  ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
@@ -150,6 +153,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
150
153
  label: entry.label,
151
154
  baseUrl: entry.baseUrl,
152
155
  adapter: entry.adapter,
156
+ ...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
153
157
  dashboardUrl: entry.dashboardUrl,
154
158
  ...(entry.models ? { models: [...entry.models] } : {}),
155
159
  ...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
@@ -219,8 +223,9 @@ export function deriveProviderPresets(): DerivedProviderPreset[] {
219
223
 
220
224
  export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
221
225
  const entry = PROVIDER_REGISTRY.find(row => row.id === name);
222
- if (!entry) return;
226
+ if (!entry || !providerMatchesRegistryTransport(name, prov)) return;
223
227
  const seed = providerConfigSeed(entry);
228
+ if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
224
229
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
225
230
  // Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
226
231
  if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
@@ -242,6 +247,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
242
247
  if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
243
248
  if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
244
249
  if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
250
+ if (prov.promptCacheKey === undefined && seed.promptCacheKey !== undefined) prov.promptCacheKey = seed.promptCacheKey;
245
251
  if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
246
252
  if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
247
253
  if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { saveConfigPreservingClaudeCode } from "../config";
12
12
  import type { OcxConfig, OcxProviderConfig } from "../types";
13
- import { resolveProviderTransport } from "./xai-transport";
13
+ import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
14
14
 
15
15
  // ---- cooldown state (in-memory, same as codex/routing.ts) ----
16
16
 
@@ -69,6 +69,11 @@ export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
69
69
  *
70
70
  * @returns A new OcxProviderConfig with the swapped key (and mutated config on disk),
71
71
  * or `null` when no alternative key is available (all in cooldown or pool < 2).
72
+ *
73
+ * The returned object is a snapshot of the PERSISTED config — it carries none of the
74
+ * registry backfills `routedProviderConfig` merges in at request time. Request paths must
75
+ * not assign it to an active route wholesale; use `rotateProviderTransportOn429`, which
76
+ * takes only the swapped key and keeps the routed provider intact.
72
77
  */
73
78
  export function rotateKeyOn429(
74
79
  config: OcxConfig,
@@ -133,12 +138,23 @@ interface RotateProviderTransportOptions {
133
138
  promptCacheKey?: string;
134
139
  }
135
140
 
136
- /** Rotate a failed key and re-apply provider-specific transport metadata to the replacement. */
141
+ /**
142
+ * Rotate a failed key and re-apply provider-specific transport metadata to the replacement.
143
+ *
144
+ * `routedProvider` is the request's active provider (the `routedProviderConfig` output the
145
+ * route was built with). The result inherits it and swaps ONLY the API key: the persisted
146
+ * config that `rotateKeyOn429` snapshots predates registry backfill, so building the retry
147
+ * provider from that snapshot would silently drop every field the registry merged in at
148
+ * routing time (scalar flags like `promptCacheKey`/`parallelToolCalls`, merged model
149
+ * metadata such as `noTemperatureModels`, a pinned baseUrl). Mirrors the OAuth-401 replay
150
+ * path in src/server/responses/core.ts, which spreads `route.provider` for the same reason.
151
+ */
137
152
  export function rotateProviderTransportOn429(
138
153
  config: OcxConfig,
139
154
  providerName: string,
155
+ routedProvider: OcxProviderTransport,
140
156
  options: RotateProviderTransportOptions = {},
141
- ): OcxProviderConfig | null {
157
+ ): OcxProviderTransport | null {
142
158
  const rotated = rotateKeyOn429(
143
159
  config,
144
160
  providerName,
@@ -147,7 +163,11 @@ export function rotateProviderTransportOn429(
147
163
  options.attemptedKey,
148
164
  );
149
165
  return rotated
150
- ? resolveProviderTransport(providerName, rotated, options.promptCacheKey)
166
+ ? resolveProviderTransport(
167
+ providerName,
168
+ { ...routedProvider, apiKey: rotated.apiKey },
169
+ options.promptCacheKey,
170
+ )
151
171
  : null;
152
172
  }
153
173