@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.14

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 (60) hide show
  1. package/gui/dist/assets/index-MUpaVatk.js +67 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -3
  4. package/packages/ocx-notch/README.md +2 -1
  5. package/src/adapters/cursor/discovery.ts +6 -2
  6. package/src/adapters/cursor/effort-map.ts +3 -0
  7. package/src/adapters/google-antigravity-replay.ts +24 -0
  8. package/src/adapters/google.ts +16 -11
  9. package/src/chat/inbound.ts +5 -11
  10. package/src/cli/account-api.ts +9 -1
  11. package/src/cli/account-extended.ts +4 -1
  12. package/src/codex/account-label.ts +14 -1
  13. package/src/codex/account-lifecycle.ts +12 -1
  14. package/src/codex/account-namespaces.ts +21 -0
  15. package/src/codex/account-priority.ts +49 -0
  16. package/src/codex/account-store.ts +2 -1
  17. package/src/codex/auth-api.ts +108 -17
  18. package/src/codex/auth-context.ts +61 -16
  19. package/src/codex/catalog/metadata.ts +34 -12
  20. package/src/codex/catalog/parsing.ts +8 -1
  21. package/src/codex/catalog/provider-fetch.ts +24 -6
  22. package/src/codex/catalog.ts +1 -1
  23. package/src/codex/pool-rotation.ts +51 -4
  24. package/src/codex/quota.ts +154 -35
  25. package/src/codex/routing.ts +133 -33
  26. package/src/codex/warmup.ts +193 -85
  27. package/src/config.ts +84 -1
  28. package/src/lib/bounded-body.ts +13 -6
  29. package/src/lib/bun-stream-caps.ts +5 -6
  30. package/src/lib/redact.ts +13 -0
  31. package/src/oauth/index.ts +79 -12
  32. package/src/oauth/log.ts +3 -1
  33. package/src/oauth/store.ts +31 -8
  34. package/src/providers/antigravity-models.ts +53 -24
  35. package/src/providers/codex-capacity.ts +303 -0
  36. package/src/providers/model-rename-migration.ts +147 -0
  37. package/src/providers/model-rename-startup.ts +29 -0
  38. package/src/providers/quota.ts +126 -16
  39. package/src/providers/registry.ts +258 -38
  40. package/src/responses/parser.ts +19 -12
  41. package/src/responses/spill-store.ts +14 -1
  42. package/src/responses/state.ts +108 -14
  43. package/src/server/index.ts +9 -1
  44. package/src/server/management/logs-usage-routes.ts +1 -0
  45. package/src/server/management/oauth-account-routes.ts +8 -1
  46. package/src/server/relay.ts +10 -42
  47. package/src/server/request-log.ts +42 -1
  48. package/src/server/responses/compact.ts +16 -4
  49. package/src/server/responses/core.ts +217 -59
  50. package/src/server/responses/empty-completion-guard.ts +275 -0
  51. package/src/server/responses/encrypted-payload.ts +54 -39
  52. package/src/server/responses/fetch-helpers.ts +24 -3
  53. package/src/server/responses/ws-upstream.ts +318 -0
  54. package/src/server/sse-frame-buffer.ts +292 -0
  55. package/src/server/ws-bridge.ts +17 -11
  56. package/src/types.ts +8 -0
  57. package/src/usage/log.ts +24 -0
  58. package/src/usage/summary.ts +152 -2
  59. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  60. package/gui/dist/assets/index-BucjyD4I.js +0 -67
@@ -249,12 +249,61 @@ export class UnsupportedOAuthProviderError extends Error {
249
249
  }
250
250
 
251
251
  export class OAuthLoginRequiredError extends Error {
252
+ readonly provider: string;
253
+
252
254
  constructor(provider: string) {
253
255
  super(`Not logged in to ${provider}. Run: ocx login ${provider}`);
254
256
  this.name = "OAuthLoginRequiredError";
257
+ this.provider = provider;
258
+ }
259
+ }
260
+
261
+ export class OAuthProviderPublicationError extends Error {
262
+ constructor() {
263
+ super("OAuth credential was saved, but the provider entry was not written. Resolve the account namespace collision, then retry login.");
264
+ this.name = "OAuthProviderPublicationError";
265
+ }
266
+ }
267
+
268
+ export class OAuthReauthIdentityMismatchError extends Error {
269
+ constructor() {
270
+ super("Signed-in account does not match the selected account. Sign in with the same account.");
271
+ this.name = "OAuthReauthIdentityMismatchError";
272
+ }
273
+ }
274
+
275
+ export class OAuthReauthIdentityUnverifiedError extends Error {
276
+ constructor() {
277
+ super("Could not verify signed-in account identity for reauth.");
278
+ this.name = "OAuthReauthIdentityUnverifiedError";
255
279
  }
256
280
  }
257
281
 
282
+ class OAuthLoginSupersededError extends Error {
283
+ constructor() {
284
+ super("OAuth login was superseded before credential persistence");
285
+ this.name = "OAuthLoginSupersededError";
286
+ }
287
+ }
288
+
289
+ /** Project arbitrary OAuth failures onto the small, stable public error vocabulary. */
290
+ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
291
+ if (error instanceof OAuthMutationBusyError) {
292
+ return error.message === "OAuth mutation queue wait timed out"
293
+ ? "OAuth mutation queue wait timed out"
294
+ : "OAuth mutation queue is busy";
295
+ }
296
+ if (
297
+ (error instanceof OAuthLoginRequiredError && isOAuthProvider(error.provider))
298
+ || error instanceof OAuthProviderPublicationError
299
+ || error instanceof OAuthReauthIdentityMismatchError
300
+ || error instanceof OAuthReauthIdentityUnverifiedError
301
+ || error instanceof OAuthTokenRefreshBusyError
302
+ || error instanceof OAuthTokenRefreshStaleError
303
+ ) return error.message;
304
+ return "OAuth authentication failed. Check the OpenCodex account status and retry.";
305
+ }
306
+
258
307
  function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
259
308
  const storedKiroRouting = {
260
309
  ...(cred.kiro?.profileArn ? { profileArn: cred.kiro.profileArn } : {}),
@@ -817,6 +866,7 @@ interface RunLoginDeps {
817
866
  settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
818
867
  removeAccount?: typeof removeAccount;
819
868
  setActiveAccount?: typeof setActiveAccount;
869
+ assertCurrentOwner?: () => void;
820
870
  }
821
871
 
822
872
  /** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
@@ -866,6 +916,7 @@ export async function runLogin(
866
916
  const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
867
917
  const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
868
918
  try {
919
+ deps.assertCurrentOwner?.();
869
920
  // Validate the provider row before credential persistence. A namespace claimed during the
870
921
  // credential write is handled again below before the latest row is re-upserted.
871
922
  if (provider !== "chatgpt") {
@@ -876,7 +927,7 @@ export async function runLogin(
876
927
  const existing = getAccountCredential(provider, opts.reauthAccountId);
877
928
  if (!existing) throw new Error(`Unknown account for reauth: ${opts.reauthAccountId}`);
878
929
  if (!existing.accountId && !existing.email) {
879
- throw new Error("Could not verify signed-in account identity for reauth.");
930
+ throw new OAuthReauthIdentityUnverifiedError();
880
931
  }
881
932
  // Kiro social accounts share one device-scoped profile ARN, so reauth identity
882
933
  // must compare the signed-in email first; otherwise reauthenticating a different
@@ -890,12 +941,15 @@ export async function runLogin(
890
941
  ? existing.email.toLowerCase() === cred.email.toLowerCase()
891
942
  : false;
892
943
  if (!identityMatches) {
893
- throw new Error("Signed-in account does not match the selected account. Sign in with the same account.");
944
+ throw new OAuthReauthIdentityMismatchError();
894
945
  }
895
- await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred);
946
+ await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred, {
947
+ assertBeforePersist: deps.assertCurrentOwner,
948
+ });
896
949
  } else {
897
950
  await (deps.saveCredential ?? saveCredential)(provider, cred, {
898
- preserveIdentityless: provider === "kiro" && opts?.forceLogin === true,
951
+ preserveIdentityless: opts?.forceLogin === true,
952
+ assertBeforePersist: deps.assertCurrentOwner,
899
953
  });
900
954
  }
901
955
  if (provider !== "chatgpt") {
@@ -907,10 +961,7 @@ export async function runLogin(
907
961
  provider,
908
962
  );
909
963
  if (lateCollision) {
910
- throw new Error(
911
- `${lateCollision}. The credential for "${provider}" was saved, but the provider entry was not written. `
912
- + "Rename the account selector, then re-run the login.",
913
- );
964
+ throw new OAuthProviderPublicationError();
914
965
  }
915
966
  upsertOAuthProvider(latestConfig, provider);
916
967
  saveLatestConfig(latestConfig);
@@ -961,6 +1012,7 @@ export async function runLogin(
961
1012
  */
962
1013
  const loginState = new Map<string, { error?: string; done: boolean }>();
963
1014
  const loginAbort = new Map<string, AbortController>();
1015
+ const kiroLoginSettling = new Set<string>();
964
1016
 
965
1017
  /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
966
1018
  interface ManualCodeSlot {
@@ -1117,13 +1169,14 @@ export async function startLoginFlow(
1117
1169
  const def = OAUTH_PROVIDERS[provider];
1118
1170
  if (!def) throw new UnsupportedOAuthProviderError(provider);
1119
1171
  const existing = loginState.get(provider);
1120
- if (existing && !existing.done) {
1172
+ if ((existing && !existing.done) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
1121
1173
  throw new Error(`A login for ${provider} is already in progress`);
1122
1174
  }
1123
1175
  clearManualCodeSlot(provider);
1124
1176
  loginState.set(provider, { done: false });
1125
1177
  const abort = new AbortController();
1126
1178
  loginAbort.set(provider, abort);
1179
+ if (provider === "kiro") kiroLoginSettling.add(provider);
1127
1180
  return new Promise((resolve, reject) => {
1128
1181
  let urlResolved = false;
1129
1182
  const ctrl: OAuthController = {
@@ -1136,7 +1189,14 @@ export async function startLoginFlow(
1136
1189
  onManualCodeInput: (expectedState?: string) => waitForManualLoginCode(provider, abort.signal, expectedState),
1137
1190
  signal: abort.signal,
1138
1191
  };
1192
+ const abandonIfNotOwner = (error?: unknown): boolean => {
1193
+ if (loginAbort.get(provider) === abort) return false;
1194
+ if (!urlResolved) reject(error ?? new Error("OAuth login was superseded"));
1195
+ return true;
1196
+ };
1139
1197
  const settle = async (error?: unknown): Promise<void> => {
1198
+ // A cancelled or superseded flow must not overwrite the newer owner's terminal state.
1199
+ if (abandonIfNotOwner(error)) return;
1140
1200
  let finalError = error;
1141
1201
  try {
1142
1202
  await lifecycle?.onSettled?.();
@@ -1145,6 +1205,7 @@ export async function startLoginFlow(
1145
1205
  // runtime config. For an already-failed login, keep the original recovery error.
1146
1206
  if (finalError === undefined) finalError = settleError;
1147
1207
  }
1208
+ if (abandonIfNotOwner(finalError)) return;
1148
1209
  if (finalError === undefined) {
1149
1210
  loginAbort.delete(provider);
1150
1211
  clearManualCodeSlot(provider);
@@ -1158,22 +1219,28 @@ export async function startLoginFlow(
1158
1219
  const e = finalError;
1159
1220
  loginAbort.delete(provider);
1160
1221
  clearManualCodeSlot(provider);
1161
- const msg = e instanceof Error ? e.message : String(e);
1222
+ const msg = publicOAuthAuthenticationErrorMessage(e);
1162
1223
  loginState.set(provider, { done: true, error: msg });
1163
1224
  if (!urlResolved) reject(e);
1164
1225
  };
1165
1226
  // Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
1166
1227
  // lets a long-lived server config adopt that settled state before clients observe done=true.
1167
- void runLogin(provider, ctrl, opts).then(
1228
+ const assertCurrentOwner = (): void => {
1229
+ if (loginAbort.get(provider) !== abort) throw new OAuthLoginSupersededError();
1230
+ };
1231
+ void runLogin(provider, ctrl, opts, { assertCurrentOwner }).then(
1168
1232
  () => settle(),
1169
1233
  (e: unknown) => settle(e),
1170
1234
  ).catch((e: unknown) => {
1171
1235
  // settle catches lifecycle failures, so this is only a defensive promise-boundary guard.
1236
+ if (abandonIfNotOwner(e)) return;
1172
1237
  loginAbort.delete(provider);
1173
1238
  clearManualCodeSlot(provider);
1174
- const msg = e instanceof Error ? e.message : String(e);
1239
+ const msg = publicOAuthAuthenticationErrorMessage(e);
1175
1240
  loginState.set(provider, { done: true, error: msg });
1176
1241
  if (!urlResolved) reject(e);
1242
+ }).finally(() => {
1243
+ if (provider === "kiro") kiroLoginSettling.delete(provider);
1177
1244
  });
1178
1245
  });
1179
1246
  }
package/src/oauth/log.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/oauth/log.ts
2
2
  import { maskAccountId } from "../lib/privacy";
3
+ import { redactSecretString } from "../lib/redact";
3
4
 
4
5
  /** Normalize camelCase / snake_case / kebab-case field names before secret checks. */
5
6
  function normalizeFieldKey(key: string): string {
@@ -20,6 +21,7 @@ const FORBIDDEN_NORMALIZED = new Set([
20
21
  "id_token",
21
22
  "client_secret",
22
23
  "oauth_code",
24
+ "code_verifier",
23
25
  "clientsecret",
24
26
  ]);
25
27
 
@@ -44,5 +46,5 @@ export function logOAuthEvent(
44
46
  if (value === undefined) continue;
45
47
  parts.push(`${key}=${String(value)}`);
46
48
  }
47
- console.info(parts.join(" "));
49
+ console.info(redactSecretString(parts.join(" ")));
48
50
  }
@@ -272,6 +272,17 @@ function newAccountId(cred: OAuthCredentials): string {
272
272
  return createHash("sha256").update(identity).digest("hex").slice(0, 8);
273
273
  }
274
274
 
275
+ /** Allocate a persisted slot id without reusing any existing account's ownership key. */
276
+ function distinctAccountId(cred: OAuthCredentials, accounts: readonly ProviderAccount[]): string {
277
+ const base = newAccountId(cred);
278
+ const occupied = new Set(accounts.map(account => account.id));
279
+ if (!occupied.has(base)) return base;
280
+ for (let suffix = 1; ; suffix += 1) {
281
+ const candidate = `${base}-${suffix}`;
282
+ if (!occupied.has(candidate)) return candidate;
283
+ }
284
+ }
285
+
275
286
  function normalizeAccount(value: unknown): ProviderAccount | null {
276
287
  if (!value || typeof value !== "object") return null;
277
288
  const candidate = value as Partial<ProviderAccount>;
@@ -425,10 +436,11 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
425
436
  drainOAuthMutations();
426
437
  return result;
427
438
  }
428
- export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
439
+ export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number; assertBeforePersist?: () => void }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
429
440
  const { store, hadLegacy } = loadAuthStoreInternal();
430
441
  if (hadLegacy) backupLegacyOnce();
431
442
  const result = await fn(store);
443
+ options?.assertBeforePersist?.();
432
444
  persist(store);
433
445
  return result;
434
446
  }finally{guard.release();}}, retainedValues, options?.waitMs);
@@ -450,7 +462,7 @@ export function getCredential(provider: string): OAuthCredentials | null {
450
462
  export async function saveCredential(
451
463
  provider: string,
452
464
  cred: OAuthCredentials,
453
- opts: { preserveIdentityless?: boolean } = {},
465
+ opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
454
466
  ): Promise<void> {
455
467
  const safe = normalizeCredential(cred);
456
468
  if (!safe) return;
@@ -503,22 +515,28 @@ export async function saveCredential(
503
515
  delete active.needsReauth;
504
516
  return;
505
517
  }
506
- const id = newAccountId(safe);
518
+ const id = distinctAccountId(safe, set.accounts);
507
519
  set.accounts.push({ id, credential: safe, addedAt: Date.now() });
508
520
  set.activeAccountId = id;
509
521
  return;
510
522
  }
511
- // No identity: replace the active slot in place (single-account semantics).
523
+ if (opts.preserveIdentityless) {
524
+ const id = distinctAccountId(safe, set.accounts);
525
+ set.accounts.push({ id, credential: safe, addedAt: Date.now() });
526
+ set.activeAccountId = id;
527
+ return;
528
+ }
529
+ // No identity during a normal login: replace the active slot in place.
512
530
  const active = set.accounts.find(a => a.id === set.activeAccountId);
513
531
  if (active) {
514
532
  active.credential = safe;
515
533
  delete active.needsReauth;
516
534
  } else {
517
- const id = newAccountId(safe);
535
+ const id = distinctAccountId(safe, set.accounts);
518
536
  set.accounts.push({ id, credential: safe, addedAt: Date.now() });
519
537
  set.activeAccountId = id;
520
538
  }
521
- }, [provider, safe]);
539
+ }, [provider, safe], { assertBeforePersist: opts.assertBeforePersist });
522
540
  }
523
541
 
524
542
  /** Remove the ACTIVE account; remaining accounts promote the first one. */
@@ -563,7 +581,12 @@ export function getAccountCredential(provider: string, accountId: string): OAuth
563
581
  }
564
582
 
565
583
  /** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
566
- export async function saveAccountCredential(provider: string, accountId: string, cred: OAuthCredentials): Promise<void> {
584
+ export async function saveAccountCredential(
585
+ provider: string,
586
+ accountId: string,
587
+ cred: OAuthCredentials,
588
+ opts: { assertBeforePersist?: () => void } = {},
589
+ ): Promise<void> {
567
590
  const safe = normalizeCredential(cred);
568
591
  if (!safe) return;
569
592
  await mutateStore(store => {
@@ -571,7 +594,7 @@ export async function saveAccountCredential(provider: string, accountId: string,
571
594
  if (!account) return;
572
595
  account.credential = safe;
573
596
  delete account.needsReauth;
574
- }, [provider, accountId, safe]);
597
+ }, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
575
598
  }
576
599
 
577
600
  export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
@@ -6,11 +6,22 @@
6
6
  // receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
7
7
  // picker exposes collapsed base models with reasoning-effort routing.
8
8
 
9
+ const GEMINI_FLASH_CURRENT = "gemini-3.7-flash";
10
+ const RETIRED_FLASH_TIERS: Record<string, string> = {
11
+ "gemini-3.6-flash": "medium",
12
+ "gemini-3.6-flash-low": "low",
13
+ "gemini-3.6-flash-medium": "medium",
14
+ "gemini-3.6-flash-high": "high",
15
+ "gemini-3.5-flash-extra-low": "low",
16
+ "gemini-3.5-flash-low": "medium",
17
+ "gemini-3.5-flash-mid": "medium",
18
+ "gemini-3.5-flash-high": "high",
19
+ "gemini-3-flash-agent": "high",
20
+ };
21
+
9
22
  // ── Wire IDs (what CCA :fetchAvailableModels returns) ──
10
23
  const ANTIGRAVITY_WIRE_MODELS = [
11
- "gemini-3.6-flash-low",
12
- "gemini-3.6-flash-medium",
13
- "gemini-3.6-flash-high",
24
+ GEMINI_FLASH_CURRENT,
14
25
  "gemini-3.1-pro-low",
15
26
  "gemini-pro-agent",
16
27
  "gemini-3.1-flash-image",
@@ -23,7 +34,7 @@ const ANTIGRAVITY_WIRE_MODELS = [
23
34
  // Gemini models: effort → wire model suffix (official agy UI pattern).
24
35
  // Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
25
36
  export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
26
- "gemini-3.6-flash": ["low", "medium", "high"],
37
+ [GEMINI_FLASH_CURRENT]: ["low", "medium", "high"],
27
38
  "gemini-3.1-pro": ["low", "high"],
28
39
  "claude-sonnet-4-6": ["low", "medium", "high", "max"],
29
40
  "claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
@@ -31,11 +42,6 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
31
42
 
32
43
  // ── Effort → wire model map for Gemini base models ──
33
44
  const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
34
- "gemini-3.6-flash": {
35
- low: "gemini-3.6-flash-low",
36
- medium: "gemini-3.6-flash-medium",
37
- high: "gemini-3.6-flash-high",
38
- },
39
45
  "gemini-3.1-pro": {
40
46
  low: "gemini-3.1-pro-low",
41
47
  high: "gemini-pro-agent",
@@ -44,11 +50,13 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
44
50
 
45
51
  // ── Default effort per Gemini base model ──
46
52
  const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
47
- "gemini-3.6-flash": "medium",
48
53
  "gemini-3.1-pro": "high",
49
54
  };
50
55
 
51
- const ANTIGRAVITY_THINKING_LEVELS = new Set(["minimal", "low", "medium", "high"]);
56
+ const ANTIGRAVITY_THINKING_LEVEL_MODELS: Record<string, string> = {
57
+ [GEMINI_FLASH_CURRENT]: "medium",
58
+ };
59
+ const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
52
60
 
53
61
  function resolveAntigravityThinkingLevel(effort: string): string | undefined {
54
62
  if (effort === "xhigh" || effort === "max" || effort === "ultra") return "high";
@@ -65,16 +73,9 @@ const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
65
73
  // Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
66
74
  // with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
67
75
  const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
68
- "gemini-3.6-flash-low": "gemini-3.6-flash-low",
69
- "gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
70
- "gemini-3.6-flash-high": "gemini-3.6-flash-high",
71
76
  "gemini-3.1-pro-low": "gemini-3.1-pro-low",
72
77
  "gemini-pro-agent": "gemini-pro-agent",
73
- "gemini-3.5-flash-extra-low": "gemini-3.6-flash-low",
74
- "gemini-3.5-flash-low": "gemini-3.6-flash-medium",
75
- "gemini-3.5-flash-mid": "gemini-3.6-flash-medium",
76
- "gemini-3.5-flash-high": "gemini-3.6-flash-high",
77
- "gemini-3-flash-agent": "gemini-3.6-flash-high",
78
+ ...Object.fromEntries(Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_CURRENT])),
78
79
  };
79
80
 
80
81
  export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
@@ -84,7 +85,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
84
85
 
85
86
  // Picker-visible: collapsed base models only.
86
87
  export const ANTIGRAVITY_MODELS = [
87
- "gemini-3.6-flash",
88
+ GEMINI_FLASH_CURRENT,
88
89
  "gemini-3.1-pro",
89
90
  "gemini-3.1-flash-image",
90
91
  "claude-sonnet-4-6",
@@ -94,9 +95,7 @@ export const ANTIGRAVITY_MODELS = [
94
95
 
95
96
  // Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
96
97
  const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
97
- "gemini-3.6-flash-low": 1_048_576,
98
- "gemini-3.6-flash-medium": 1_048_576,
99
- "gemini-3.6-flash-high": 1_048_576,
98
+ [GEMINI_FLASH_CURRENT]: 1_048_576,
100
99
  "gemini-3.1-pro-low": 1_048_576,
101
100
  "gemini-pro-agent": 1_048_576,
102
101
  "gemini-3.1-flash-image": 1_048_576,
@@ -107,7 +106,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
107
106
 
108
107
  export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
109
108
  // Collapsed base IDs — explicit entries for the picker.
110
- "gemini-3.6-flash": 1_048_576,
109
+ [GEMINI_FLASH_CURRENT]: 1_048_576,
111
110
  "gemini-3.1-pro": 1_048_576,
112
111
  // Wire IDs and aliases via derivation.
113
112
  ...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
@@ -119,6 +118,15 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
119
118
  ),
120
119
  };
121
120
 
121
+ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
122
+ [GEMINI_FLASH_CURRENT]: ["text", "image"],
123
+ "gemini-3.1-pro": ["text", "image"],
124
+ "gemini-3.1-flash-image": ["text", "image"],
125
+ "claude-sonnet-4-6": ["text", "image"],
126
+ "claude-opus-4-6-thinking": ["text", "image"],
127
+ "gpt-oss-120b-medium": ["text"],
128
+ };
129
+
122
130
  export function resolveAntigravityWireModelId(modelId: string): string {
123
131
  return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
124
132
  }
@@ -132,6 +140,10 @@ export function isAntigravitySuffixModelId(modelId: string): boolean {
132
140
  return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
133
141
  }
134
142
 
143
+ export function retiredAntigravityFlashTier(modelId: string): string | undefined {
144
+ return RETIRED_FLASH_TIERS[modelId];
145
+ }
146
+
135
147
  /**
136
148
  * Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
137
149
  *
@@ -146,11 +158,27 @@ export function resolveAntigravityEffortWireModel(
146
158
  modelId: string,
147
159
  effort?: string,
148
160
  ): { wireModelId: string; thinkingLevel?: string } {
161
+ const retiredTier = RETIRED_FLASH_TIERS[modelId];
162
+ if (retiredTier) {
163
+ return {
164
+ wireModelId: GEMINI_FLASH_CURRENT,
165
+ thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
166
+ };
167
+ }
168
+
149
169
  // Rule 1: suffix/compat alias — suffix IS the effort.
150
170
  if (isAntigravitySuffixModelId(modelId)) {
151
171
  return { wireModelId: resolveAntigravityWireModelId(modelId) };
152
172
  }
153
173
 
174
+ const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
175
+ if (defaultLevel) {
176
+ return {
177
+ wireModelId: modelId,
178
+ thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel,
179
+ };
180
+ }
181
+
154
182
  // Rule 2/3: mapped Gemini base model.
155
183
  const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
156
184
  if (effortMap) {
@@ -189,6 +217,7 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record<string, string> = (() => {
189
217
  // If alias is itself a base/wire already mapped, keep that mapping.
190
218
  else if (rev[wire]) rev[alias] = rev[wire]!;
191
219
  }
220
+ for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired;
192
221
  // Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already
193
222
  // included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain.
194
223
  return rev;