@bitkyc08/opencodex 2.26.0 → 2.28.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 (83) hide show
  1. package/gui/dist/assets/{index-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic.ts +60 -1
  5. package/src/adapters/base.ts +16 -2
  6. package/src/adapters/command-code.ts +4 -3
  7. package/src/adapters/cursor/cursor-errors.ts +15 -0
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-antigravity-replay.ts +16 -8
  10. package/src/adapters/google.ts +22 -5
  11. package/src/adapters/openai-chat.ts +189 -60
  12. package/src/adapters/openai-responses.ts +37 -0
  13. package/src/adapters/tool-catalog-nudge.ts +1 -1
  14. package/src/bridge.ts +11 -5
  15. package/src/cli/doctor.ts +76 -0
  16. package/src/cli/help.ts +2 -0
  17. package/src/cli/index.ts +19 -6
  18. package/src/cli/models.ts +13 -6
  19. package/src/codex/account-usability.ts +3 -0
  20. package/src/codex/app-server-processes.ts +269 -37
  21. package/src/codex/auth-api.ts +22 -5
  22. package/src/codex/auth-context.ts +108 -3
  23. package/src/codex/catalog/aggregation.ts +3 -0
  24. package/src/codex/catalog/metadata.ts +17 -3
  25. package/src/codex/catalog/native-models.ts +22 -14
  26. package/src/codex/catalog/parsing.ts +20 -3
  27. package/src/codex/catalog/provider-fetch.ts +8 -0
  28. package/src/codex/catalog/sync.ts +63 -15
  29. package/src/codex/convergence.ts +61 -13
  30. package/src/codex/log-guard/path-safety.ts +52 -3
  31. package/src/codex/model-entitlements.ts +353 -0
  32. package/src/codex/native-profile-startup.ts +100 -2
  33. package/src/codex/quota.ts +28 -3
  34. package/src/codex/routing.ts +14 -8
  35. package/src/codex/user-identity.ts +21 -1
  36. package/src/config/provider-name.ts +24 -0
  37. package/src/config.ts +11 -24
  38. package/src/generated/compatibility-version.json +110 -70
  39. package/src/images/loop.ts +11 -4
  40. package/src/lib/destination-policy.ts +47 -0
  41. package/src/lib/shadow-call.ts +15 -0
  42. package/src/lib/state-store-registrations.ts +8 -2
  43. package/src/oauth/index.ts +33 -5
  44. package/src/oauth/store.ts +11 -5
  45. package/src/providers/antigravity-models.ts +70 -5
  46. package/src/providers/derive.ts +12 -2
  47. package/src/providers/fastwire.ts +39 -8
  48. package/src/providers/quota.ts +9 -2
  49. package/src/providers/registry.ts +120 -6
  50. package/src/providers/service-tier.ts +50 -15
  51. package/src/responses/parser.ts +59 -11
  52. package/src/responses/state.ts +162 -5
  53. package/src/responses/tool-search-compat.ts +301 -0
  54. package/src/router.ts +17 -3
  55. package/src/routing/capability.ts +26 -9
  56. package/src/routing/compatibility/behavior.ts +44 -6
  57. package/src/routing/profile.ts +1 -1
  58. package/src/server/chat-native.ts +11 -2
  59. package/src/server/index.ts +59 -8
  60. package/src/server/management/agent-settings-routes.ts +16 -2
  61. package/src/server/management/shared.ts +3 -1
  62. package/src/server/request-log.ts +31 -0
  63. package/src/server/responses/collaboration.ts +34 -9
  64. package/src/server/responses/compact.ts +54 -7
  65. package/src/server/responses/core.ts +259 -43
  66. package/src/server/responses/input-admission.ts +7 -2
  67. package/src/server/responses/responses-field-backfill.ts +88 -6
  68. package/src/server/responses/terminal-guard.ts +10 -0
  69. package/src/server/responses-tool-search-repair.ts +217 -0
  70. package/src/server/system-env.ts +74 -5
  71. package/src/service-manager-probe.ts +99 -0
  72. package/src/service.ts +86 -6
  73. package/src/tray/windows.ts +25 -5
  74. package/src/types/accounts.ts +37 -0
  75. package/src/types/config.ts +818 -0
  76. package/src/types/provider.ts +521 -0
  77. package/src/types/request.ts +358 -0
  78. package/src/types/tools.ts +131 -0
  79. package/src/types/wire.ts +80 -0
  80. package/src/types.ts +103 -1883
  81. package/src/usage/cost.ts +37 -1
  82. package/src/usage/log.ts +4 -0
  83. package/src/web-search/loop.ts +11 -4
@@ -130,6 +130,50 @@ function registryAllowsPrivateNetwork(name: string): boolean {
130
130
  return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
131
131
  }
132
132
 
133
+ /**
134
+ * OAuth registry entries that opt into `allowBaseUrlOverride` send bearer credentials to a
135
+ * user-configured endpoint (review findings, PR #2109 / PR #2110): a cleartext `http:`
136
+ * override would expose the OAuth token on the wire. `https:` is therefore required for
137
+ * every non-local destination. Loopback/localhost/private relays keep working over
138
+ * `http:` because they already sit behind the explicit `allowPrivateNetwork` opt-in
139
+ * enforced by {@link providerDestinationConfigError}. Keyed/local providers (Ollama,
140
+ * vLLM, LM Studio, LiteLLM, Moonshot, Qwen, Alibaba) are untouched: they are not
141
+ * `authKind: "oauth"`, so this check never fires for them.
142
+ */
143
+ function registrySendsOAuthToOverriddenBaseUrl(name: string): boolean {
144
+ const entry = getProviderRegistryEntry(name);
145
+ return entry?.authKind === "oauth" && entry.allowBaseUrlOverride === true;
146
+ }
147
+
148
+ export function providerSecureTransportConfigError(
149
+ name: string,
150
+ provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">,
151
+ ): string | null {
152
+ if (!registrySendsOAuthToOverriddenBaseUrl(name)) return null;
153
+ let parsed: URL;
154
+ try {
155
+ parsed = new URL(provider.baseUrl.trim());
156
+ } catch {
157
+ return null; // invalid URLs are providerBaseUrlConfigError's concern
158
+ }
159
+ if (parsed.protocol !== "http:") return null;
160
+ const assessment = assessDestination(provider.baseUrl);
161
+ // Classify FIRST, then consult the opt-in. `allowPrivateNetwork` says "this destination is
162
+ // intentionally local", which is a statement about the address, not a waiver of transport
163
+ // security — reading it before classification let `http://attacker.example` with the opt-in
164
+ // set carry an OAuth bearer in cleartext to a public host.
165
+ if (!assessment) return null;
166
+ const local = assessment.kind === "localhost"
167
+ || assessment.kind === "loopback"
168
+ || assessment.kind === "private";
169
+ if (local && providerAllowsPrivateNetwork(name, provider)) {
170
+ // A genuinely local relay over http stays reachable through the explicit opt-in; the
171
+ // private-network gate still governs whether it may be reached at all.
172
+ return null;
173
+ }
174
+ return "baseUrl must use https: this provider sends OAuth credentials to its endpoint, and http is allowed only for loopback/private relays";
175
+ }
176
+
133
177
  /**
134
178
  * Whether a provider may reach loopback/private addresses.
135
179
  *
@@ -150,6 +194,8 @@ export function providerAllowsPrivateNetwork(
150
194
  }
151
195
 
152
196
  export function providerDestinationConfigError(name: string, provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">): string | null {
197
+ const secureTransportError = providerSecureTransportConfigError(name, provider);
198
+ if (secureTransportError) return secureTransportError;
153
199
  const assessment = assessDestination(provider.baseUrl);
154
200
  if (!assessment) return null;
155
201
  if (assessment.kind === "public" || assessment.kind === "hostname") return null;
@@ -331,3 +377,4 @@ export async function resolvePublicAddresses(
331
377
  export async function assertUrlResolvesPublic(url: string): Promise<void> {
332
378
  await resolvePublicAddresses(url);
333
379
  }
380
+
@@ -29,6 +29,21 @@ export function isShadowSourceModel(modelId: string, configured?: unknown): bool
29
29
  return shadowSourceModels(configured).some(prefix => modelId.startsWith(prefix));
30
30
  }
31
31
 
32
+ /**
33
+ * The configured source prefix this model matched, or undefined.
34
+ *
35
+ * Callers that RECORD the intercepted model must record this rather than the caller's raw
36
+ * `modelId`. Matching is by prefix, so `gpt-5.6-luna` plus arbitrary trailing text still
37
+ * intercepts — and the raw string is caller-controlled, reaches `usage.jsonl` and `/api/logs`,
38
+ * and only passes a pattern-based redactor on the way. A credential family that redactor does
39
+ * not recognize survives verbatim. Returning the operator-configured prefix keeps the log
40
+ * field inside a set the operator chose, so no caller string is ever persisted.
41
+ */
42
+ export function shadowSourceModelPrefix(modelId: string, configured?: unknown): string | undefined {
43
+ if (modelId.includes("/")) return undefined;
44
+ return shadowSourceModels(configured).find(prefix => modelId.startsWith(prefix));
45
+ }
46
+
32
47
  /**
33
48
  * Decide whether a matching source model should use the opt-in intercept.
34
49
  *
@@ -35,7 +35,7 @@ import { listLiveOAuthAccountKeys, reconcileOAuthReauthState } from "../oauth/st
35
35
  import { reconcileGuardianBackoff } from "../oauth/token-guardian";
36
36
  import { sweepExpiredApiKeyCooldowns } from "../providers/key-failover";
37
37
  import { reconcileProviderRequestPacing } from "../providers/request-pacing";
38
- import { sweepExpiredResponseStates } from "../responses/state";
38
+ import { sweepAbandonedResponseStateTemps, sweepExpiredResponseStates } from "../responses/state";
39
39
  import { sweepExpiredAntigravityReplay } from "../adapters/google-antigravity-replay";
40
40
  import { reconcileProviderAccountQuotaRows } from "../providers/quota";
41
41
  import { reconcileRouterWarningMemos } from "../router";
@@ -84,7 +84,13 @@ export const STATE_STORE_REGISTRATIONS = [
84
84
  },
85
85
  { name: "anthropic-routing-health", sweepExpired: sweepExpiredAnthropicRoutingHealth },
86
86
  { name: "xai-refresh-verdicts", sweepExpired: sweepExpiredXaiPermanentFailureVerdicts },
87
- { name: "responses-continuation", sweepExpired: sweepExpiredResponseStates },
87
+ {
88
+ name: "responses-continuation",
89
+ sweepExpired: sweepExpiredResponseStates,
90
+ // Disk reclaim rides the liveness tick, not the TTL tick: sweepExpiredOnWrite puts
91
+ // sweepExpired on hot write paths, where a directory scan does not belong.
92
+ sweepLiveness: sweepAbandonedResponseStateTemps,
93
+ },
88
94
  { name: "antigravity-replay", sweepExpired: sweepExpiredAntigravityReplay },
89
95
  { name: "config-warning-memos", reconcileGeneration: (context: GenerationContext) => reconcileConfigWarningMemos(context.generation) },
90
96
  { name: "catalog-warning-memos", reconcileGeneration: (context: GenerationContext) => reconcileCatalogWarningMemos(context.generation) },
@@ -17,7 +17,7 @@ import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
17
17
  import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
18
18
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
19
19
  import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
20
- import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry";
20
+ import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
21
21
  import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
22
22
  import { resolveProviderTransport } from "../providers/xai-transport";
23
23
  import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
@@ -322,6 +322,13 @@ export class OAuthReauthIdentityUnverifiedError extends Error {
322
322
  }
323
323
  }
324
324
 
325
+ class OAuthLoginSupersededError extends Error {
326
+ constructor() {
327
+ super("OAuth login was superseded before credential persistence");
328
+ this.name = "OAuthLoginSupersededError";
329
+ }
330
+ }
331
+
325
332
  /** Project arbitrary OAuth failures onto the small, stable public error vocabulary. */
326
333
  export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
327
334
  if (error instanceof OAuthMutationBusyError) {
@@ -828,7 +835,16 @@ export function buildModelsRequest(
828
835
  undefined,
829
836
  copilotApiBaseUrl,
830
837
  );
831
- const headers: Record<string, string> = { ...(effectiveProvider.headers ?? {}) };
838
+ // Model discovery is an upstream request like any other, so it carries the same registry
839
+ // static headers the inference path does. Without this a provider is identified correctly
840
+ // when it answers a completion but anonymously when it lists its own models, which is the
841
+ // kind of split fingerprint an upstream rate limiter reads as two different clients.
842
+ const registryStaticHeaders = providerMatchesRegistryTransport(providerName, effectiveProvider)
843
+ ? getProviderRegistryEntry(providerName)?.staticHeaders
844
+ : undefined;
845
+ const headers: Record<string, string> = {
846
+ ...(mergeRegistryStaticHeaders(registryStaticHeaders, effectiveProvider.headers) ?? {}),
847
+ };
832
848
  const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
833
849
  providerName,
834
850
  prov,
@@ -1096,6 +1112,7 @@ interface RunLoginDeps {
1096
1112
  settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
1097
1113
  removeAccount?: typeof removeAccount;
1098
1114
  setActiveAccount?: typeof setActiveAccount;
1115
+ assertCurrentOwner?: () => void;
1099
1116
  }
1100
1117
 
1101
1118
  /** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
@@ -1145,6 +1162,7 @@ export async function runLogin(
1145
1162
  const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
1146
1163
  const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
1147
1164
  try {
1165
+ deps.assertCurrentOwner?.();
1148
1166
  // Validate the provider row before credential persistence. A namespace claimed during the
1149
1167
  // credential write is handled again below before the latest row is re-upserted.
1150
1168
  if (provider !== "chatgpt") {
@@ -1165,10 +1183,13 @@ export async function runLogin(
1165
1183
  if (!identityMatches) {
1166
1184
  throw new OAuthReauthIdentityMismatchError();
1167
1185
  }
1168
- await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred);
1186
+ await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred, {
1187
+ assertBeforePersist: deps.assertCurrentOwner,
1188
+ });
1169
1189
  } else {
1170
1190
  await (deps.saveCredential ?? saveCredential)(provider, cred, {
1171
1191
  preserveIdentityless: opts?.forceLogin === true,
1192
+ assertBeforePersist: deps.assertCurrentOwner,
1172
1193
  });
1173
1194
  }
1174
1195
  if (provider !== "chatgpt") {
@@ -1235,6 +1256,7 @@ export async function runLogin(
1235
1256
  */
1236
1257
  const loginState = new Map<string, { error?: string; done: boolean }>();
1237
1258
  const loginAbort = new Map<string, AbortController>();
1259
+ const kiroLoginSettling = new Set<string>();
1238
1260
 
1239
1261
  /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
1240
1262
  interface ManualCodeSlot {
@@ -1403,13 +1425,14 @@ export async function startLoginFlow(
1403
1425
  const def = OAUTH_PROVIDERS[provider];
1404
1426
  if (!def) throw new UnsupportedOAuthProviderError(provider);
1405
1427
  const existing = loginState.get(provider);
1406
- if (existing && !existing.done) {
1428
+ if ((existing && !existing.done) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
1407
1429
  throw new Error(`A login for ${provider} is already in progress`);
1408
1430
  }
1409
1431
  clearManualCodeSlot(provider);
1410
1432
  loginState.set(provider, { done: false });
1411
1433
  const abort = new AbortController();
1412
1434
  loginAbort.set(provider, abort);
1435
+ if (provider === "kiro") kiroLoginSettling.add(provider);
1413
1436
  return new Promise((resolve, reject) => {
1414
1437
  let urlResolved = false;
1415
1438
  const ctrl: OAuthController = {
@@ -1460,7 +1483,10 @@ export async function startLoginFlow(
1460
1483
  };
1461
1484
  // Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
1462
1485
  // lets a long-lived server config adopt that settled state before clients observe done=true.
1463
- void runLogin(provider, ctrl, opts).then(
1486
+ const assertCurrentOwner = (): void => {
1487
+ if (loginAbort.get(provider) !== abort) throw new OAuthLoginSupersededError();
1488
+ };
1489
+ void runLogin(provider, ctrl, opts, { assertCurrentOwner }).then(
1464
1490
  () => settle(),
1465
1491
  (e: unknown) => settle(e),
1466
1492
  ).catch((e: unknown) => {
@@ -1471,6 +1497,8 @@ export async function startLoginFlow(
1471
1497
  const msg = publicOAuthAuthenticationErrorMessage(e);
1472
1498
  loginState.set(provider, { done: true, error: msg });
1473
1499
  if (!urlResolved) reject(e);
1500
+ }).finally(() => {
1501
+ if (provider === "kiro") kiroLoginSettling.delete(provider);
1474
1502
  });
1475
1503
  });
1476
1504
  }
@@ -465,10 +465,11 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
465
465
  drainOAuthMutations();
466
466
  return result;
467
467
  }
468
- 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{
468
+ 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{
469
469
  const { store, hadLegacy } = loadAuthStoreInternal();
470
470
  if (hadLegacy) backupLegacyOnce();
471
471
  const result = await fn(store);
472
+ options?.assertBeforePersist?.();
472
473
  persist(store);
473
474
  return result;
474
475
  }finally{guard.release();}}, retainedValues, options?.waitMs);
@@ -491,7 +492,7 @@ export function getCredential(provider: string): OAuthCredentials | null {
491
492
  export async function saveCredential(
492
493
  provider: string,
493
494
  cred: OAuthCredentials,
494
- opts: { preserveIdentityless?: boolean } = {},
495
+ opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
495
496
  ): Promise<void> {
496
497
  const safe = normalizeCredential(cred);
497
498
  if (!safe) return;
@@ -542,7 +543,7 @@ export async function saveCredential(
542
543
  set.accounts.push({ id, credential: safe, addedAt: Date.now() });
543
544
  set.activeAccountId = id;
544
545
  }
545
- }, [provider, safe]);
546
+ }, [provider, safe], { assertBeforePersist: opts.assertBeforePersist });
546
547
  }
547
548
 
548
549
  /**
@@ -632,7 +633,12 @@ export function getAccountCredential(provider: string, accountId: string): OAuth
632
633
  }
633
634
 
634
635
  /** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
635
- export async function saveAccountCredential(provider: string, accountId: string, cred: OAuthCredentials): Promise<void> {
636
+ export async function saveAccountCredential(
637
+ provider: string,
638
+ accountId: string,
639
+ cred: OAuthCredentials,
640
+ opts: { assertBeforePersist?: () => void } = {},
641
+ ): Promise<void> {
636
642
  const safe = normalizeCredential(cred);
637
643
  if (!safe) return;
638
644
  await mutateStore(store => {
@@ -640,7 +646,7 @@ export async function saveAccountCredential(provider: string, accountId: string,
640
646
  if (!account) return;
641
647
  account.credential = safe;
642
648
  delete account.needsReauth;
643
- }, [provider, accountId, safe]);
649
+ }, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
644
650
  }
645
651
 
646
652
  export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
@@ -72,6 +72,12 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
72
72
  }, {});
73
73
 
74
74
  const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
75
+ type AntigravityDiscoveryEffort = typeof ANTIGRAVITY_DISCOVERY_EFFORTS[number];
76
+ type AntigravityEffortWireModelIds = Partial<Record<AntigravityDiscoveryEffort, string>>;
77
+
78
+ function isAntigravityDiscoveryEffort(value: string): value is AntigravityDiscoveryEffort {
79
+ return (ANTIGRAVITY_DISCOVERY_EFFORTS as readonly string[]).includes(value);
80
+ }
75
81
 
76
82
  function pickerModelIdForDiscoveredWireId(
77
83
  wireId: string,
@@ -151,6 +157,25 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
151
157
  },
152
158
  };
153
159
 
160
+ function completeDiscoveredEffortWireModelIds(
161
+ pickerId: string,
162
+ available: ReadonlyMap<string, Record<string, unknown>>,
163
+ ): AntigravityEffortWireModelIds | undefined {
164
+ const explicitEffortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[pickerId];
165
+ if (explicitEffortMap && Object.values(explicitEffortMap).every(wireId => available.has(wireId))) {
166
+ return { ...explicitEffortMap };
167
+ }
168
+
169
+ if (!isKnownAntigravityPickerModelId(pickerId)) return undefined;
170
+ const suffixEffortMap: AntigravityEffortWireModelIds = {};
171
+ for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) {
172
+ const wireId = `${pickerId}-${effort}`;
173
+ if (!available.has(wireId)) return undefined;
174
+ suffixEffortMap[effort] = wireId;
175
+ }
176
+ return suffixEffortMap;
177
+ }
178
+
154
179
  // ── Default effort per Gemini base model ──
155
180
  const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
156
181
  "gemini-3.1-pro": "high",
@@ -270,6 +295,8 @@ export interface AntigravityAvailableModel {
270
295
  id: string;
271
296
  /** CCA model id used by the agent envelope when `id` comes from display metadata. */
272
297
  wireModelId: string;
298
+ /** Complete effort-to-wire mapping retained for collapsed discovered tier sets. */
299
+ effortWireModelIds?: AntigravityEffortWireModelIds;
273
300
  contextWindow?: number;
274
301
  inputModalities?: string[];
275
302
  }
@@ -286,6 +313,7 @@ function antigravityPositiveInteger(value: unknown): number | undefined {
286
313
 
287
314
  interface DiscoveredWireModelMapping {
288
315
  readonly models: ReadonlyMap<string, string>;
316
+ readonly effortModels: ReadonlyMap<string, AntigravityEffortWireModelIds>;
289
317
  readonly generation?: { provider: string; cacheGeneration: string };
290
318
  }
291
319
 
@@ -327,17 +355,21 @@ export function registerAntigravityDiscoveredWireModels(
327
355
  const key = antigravityBaseUrlKey(baseUrl);
328
356
  if (!key) return;
329
357
  const wireModels = new Map<string, string>();
330
- for (const model of models) wireModels.set(model.id, model.wireModelId);
358
+ const effortModels = new Map<string, AntigravityEffortWireModelIds>();
359
+ for (const model of models) {
360
+ wireModels.set(model.id, model.wireModelId);
361
+ if (model.effortWireModelIds) effortModels.set(model.id, { ...model.effortWireModelIds });
362
+ }
331
363
  discoveredWireModelsByBaseUrl.set(key, {
332
364
  models: wireModels,
365
+ effortModels,
333
366
  ...(generation ? { generation } : {}),
334
367
  });
335
368
  }
336
369
 
337
- function discoveredAntigravityWireModelId(
338
- modelId: string,
370
+ function discoveredAntigravityMapping(
339
371
  baseUrl: string | undefined,
340
- ): string | undefined {
372
+ ): DiscoveredWireModelMapping | undefined {
341
373
  const key = antigravityBaseUrlKey(baseUrl);
342
374
  if (!key) return undefined;
343
375
  const mapping = discoveredWireModelsByBaseUrl.get(key);
@@ -347,7 +379,35 @@ function discoveredAntigravityWireModelId(
347
379
  discoveredWireModelsByBaseUrl.delete(key);
348
380
  return undefined;
349
381
  }
350
- return mapping.models.get(modelId);
382
+ return mapping;
383
+ }
384
+
385
+ function discoveredAntigravityWireModelId(
386
+ modelId: string,
387
+ baseUrl: string | undefined,
388
+ ): string | undefined {
389
+ return discoveredAntigravityMapping(baseUrl)?.models.get(modelId);
390
+ }
391
+
392
+ function discoveredAntigravityEffortWireModelId(
393
+ modelId: string,
394
+ effort: string | undefined,
395
+ baseUrl: string | undefined,
396
+ ): string | undefined {
397
+ const effortMap = discoveredAntigravityMapping(baseUrl)?.effortModels.get(modelId);
398
+ if (!effortMap) return undefined;
399
+
400
+ const requestedEffort = effort ? resolveAntigravityThinkingLevel(effort) : undefined;
401
+ if (requestedEffort && isAntigravityDiscoveryEffort(requestedEffort) && effortMap[requestedEffort]) {
402
+ return effortMap[requestedEffort];
403
+ }
404
+
405
+ const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]
406
+ ?? ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
407
+ if (defaultEffort && isAntigravityDiscoveryEffort(defaultEffort) && effortMap[defaultEffort]) {
408
+ return effortMap[defaultEffort];
409
+ }
410
+ return Object.values(effortMap)[0];
351
411
  }
352
412
 
353
413
  /**
@@ -465,9 +525,11 @@ export function parseAntigravityAvailableModels(
465
525
  const id = pickerModelIdForDiscoveredWireId(wireId, info, available);
466
526
  if (seen.has(id)) continue;
467
527
  seen.add(id);
528
+ const effortWireModelIds = completeDiscoveredEffortWireModelIds(id, available);
468
529
  out.push({
469
530
  id,
470
531
  wireModelId: wireId,
532
+ ...(effortWireModelIds ? { effortWireModelIds } : {}),
471
533
  ...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
472
534
  // Tri-state, deliberately not a ternary: `true` asserts image support,
473
535
  // `false` asserts against it, and ABSENT is unknown. Collapsing absent into
@@ -522,6 +584,9 @@ export function resolveAntigravityEffortWireModel(
522
584
  effort?: string,
523
585
  baseUrl?: string,
524
586
  ): { wireModelId: string; thinkingLevel?: string } {
587
+ const discoveredEffortWireModelId = discoveredAntigravityEffortWireModelId(modelId, effort, baseUrl);
588
+ if (discoveredEffortWireModelId) return { wireModelId: discoveredEffortWireModelId };
589
+
525
590
  // A collapsed picker row reports ONE representative wire id (whichever tier CCA
526
591
  // listed first), so live discovery cannot describe a ladder — it can only name a
527
592
  // single rung. Letting it answer for a base model we already have a ladder for
@@ -3,6 +3,7 @@ import { cloneFastWire } from "./fastwire";
3
3
  import {
4
4
  PROVIDER_REGISTRY,
5
5
  registryEntryForProviderDestination,
6
+ registryModelServiceTierCapabilityApplies,
6
7
  type ProviderRegistryEntry,
7
8
  } from "./registry";
8
9
  import {
@@ -377,6 +378,15 @@ function applyServiceTierModelDefaults(
377
378
  };
378
379
  }
379
380
 
381
+ function serviceTierModelDefaultsFor(
382
+ entry: ProviderRegistryEntry | undefined,
383
+ prov: OcxProviderConfig,
384
+ ): Readonly<Record<string, boolean>> | undefined {
385
+ return entry && registryModelServiceTierCapabilityApplies(entry, prov)
386
+ ? entry.modelSupportsServiceTier
387
+ : undefined;
388
+ }
389
+
380
390
  /**
381
391
  * Last-resort enrichment for a provider whose NAME matches no registry id.
382
392
  *
@@ -412,7 +422,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
412
422
  // which vendor endpoint is this row talking to — and is already restricted to fixed key
413
423
  // destinations, so a templated or overridable base URL cannot be claimed by it.
414
424
  enrichReasoningSummariesByDestination(prov);
415
- applyServiceTierModelDefaults(prov, registryEntryForProviderDestination(prov)?.modelSupportsServiceTier);
425
+ applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(registryEntryForProviderDestination(prov), prov));
416
426
  return;
417
427
  }
418
428
  const explicitDirectReasoning: DirectReasoningEffortOverrides = {
@@ -466,7 +476,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
466
476
  if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
467
477
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
468
478
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
469
- applyServiceTierModelDefaults(prov, entry.modelSupportsServiceTier);
479
+ applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
470
480
  // Registry-only repair policy (#938): fill only when the runtime provider has
471
481
  // no explicit policy, and deep-clone so saved/user values never alias the
472
482
  // registry constant.
@@ -7,7 +7,7 @@ import type {
7
7
  } from "../types";
8
8
  import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
9
9
  import { sanitizeLogMetadataString } from "../lib/redact";
10
- import type { InboundWire, ModelWireDefault } from "./registry";
10
+ import type { InboundWire, ModelWireDefault, ProviderAuthKind } from "./registry";
11
11
 
12
12
  const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
13
13
  const FAST_WIRE_ADAPTERS: Readonly<Record<FastWire["kind"], ReadonlySet<string>>> = {
@@ -31,6 +31,7 @@ export type FastPolicyAuthTransport =
31
31
 
32
32
  export interface FastPolicyAuthority {
33
33
  readonly providerAdapter: string;
34
+ readonly providerAuthMode?: ProviderAuthKind;
34
35
  readonly fastWireDeclaration: FastWire | null | undefined;
35
36
  readonly modelWireOverrideAllowed: boolean;
36
37
  readonly authTransport: FastPolicyAuthTransport;
@@ -114,21 +115,33 @@ function registryDefaultForModel(
114
115
  defaults: Readonly<Record<string, ModelWireDefault>>,
115
116
  modelId: string,
116
117
  inbound: InboundWire,
117
- ): string | undefined {
118
+ authMode: ProviderAuthKind | undefined,
119
+ ): { adapter: string; forwardCallerServiceTier?: boolean } | undefined {
118
120
  const normalizedModelId = modelId.trim().toLowerCase();
119
121
  if (!Object.hasOwn(defaults, normalizedModelId)) return undefined;
120
122
  const declared = defaults[normalizedModelId];
121
123
  if (declared === undefined) return undefined;
122
- if (typeof declared !== "string" && !declared.inbound.includes(inbound)) return undefined;
124
+ if (typeof declared !== "string") {
125
+ if (!declared.inbound.includes(inbound)) return undefined;
126
+ if (declared.authModes && (authMode === undefined || !declared.authModes.includes(authMode))) {
127
+ return undefined;
128
+ }
129
+ }
123
130
  const wire = typeof declared === "string" ? declared : declared.wire;
124
- return MODEL_ADAPTER_OVERRIDE_ALLOWED.has(wire) ? wire : undefined;
131
+ if (!MODEL_ADAPTER_OVERRIDE_ALLOWED.has(wire)) return undefined;
132
+ return {
133
+ adapter: wire,
134
+ ...(typeof declared !== "string" && declared.forwardCallerServiceTier !== undefined
135
+ ? { forwardCallerServiceTier: declared.forwardCallerServiceTier }
136
+ : {}),
137
+ };
125
138
  }
126
139
 
127
140
  function resolvePolicyAdapter(
128
141
  authority: FastPolicyAuthority,
129
142
  modelId: string,
130
143
  inbound: InboundWire,
131
- ): { adapter: string; hardPinned: boolean } {
144
+ ): { adapter: string; hardPinned: boolean; forwardCallerServiceTier?: boolean } {
132
145
  // Hard pins and configured overrides deliberately use the same exact-key semantics as
133
146
  // resolveWireProtocolOverride(). Registry defaults alone normalize ids at their boundary.
134
147
  const hardPin = Object.hasOwn(authority.hardPins, modelId)
@@ -143,8 +156,21 @@ function resolvePolicyAdapter(
143
156
  return { adapter: configured, hardPinned: false };
144
157
  }
145
158
  if (MODEL_ADAPTER_OVERRIDE_ALLOWED.has(authority.providerAdapter)) {
146
- const registryDefault = registryDefaultForModel(authority.registryWireDefaults, modelId, inbound);
147
- if (registryDefault !== undefined) return { adapter: registryDefault, hardPinned: false };
159
+ const registryDefault = registryDefaultForModel(
160
+ authority.registryWireDefaults,
161
+ modelId,
162
+ inbound,
163
+ authority.providerAuthMode,
164
+ );
165
+ if (registryDefault !== undefined) {
166
+ return {
167
+ adapter: registryDefault.adapter,
168
+ hardPinned: false,
169
+ ...(registryDefault.forwardCallerServiceTier !== undefined
170
+ ? { forwardCallerServiceTier: registryDefault.forwardCallerServiceTier }
171
+ : {}),
172
+ };
173
+ }
148
174
  }
149
175
  }
150
176
  return { adapter: authority.providerAdapter, hardPinned: false };
@@ -155,7 +181,11 @@ export function resolveFastPolicy(
155
181
  modelId: string,
156
182
  inbound: InboundWire = "responses",
157
183
  ): ResolvedFastPolicy {
158
- const { adapter, hardPinned } = resolvePolicyAdapter(authority, modelId, inbound);
184
+ const { adapter, hardPinned, forwardCallerServiceTier } = resolvePolicyAdapter(
185
+ authority,
186
+ modelId,
187
+ inbound,
188
+ );
159
189
  const exactCapability = exactModelValue(authority.capability.models, modelId);
160
190
  const capability = authority.capability.provider === false
161
191
  ? false
@@ -173,6 +203,7 @@ export function resolveFastPolicy(
173
203
  // tier still needs the final wire's forwarding permission.
174
204
  const forwardCallerTier = capability !== false
175
205
  && callerWireAvailable
206
+ && forwardCallerServiceTier !== false
176
207
  && (adapter !== "openai-chat" || authority.capability.chatServiceTier === true);
177
208
 
178
209
  let eligibility: ResolvedFastPolicy["eligibility"];
@@ -13,7 +13,7 @@ import { getAccountCredential, getAccountSet, getCredential } from "../oauth/sto
13
13
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
14
14
  import { apiKeyPoolEntryId } from "./api-keys";
15
15
  import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
16
- import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
16
+ import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
17
17
  import type { OcxConfig, OcxProviderConfig } from "../types";
18
18
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
19
19
  import {
@@ -2084,7 +2084,14 @@ async function maybeFetchProviderQuota(
2084
2084
  && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
2085
2085
  return fetchCommandCodeQuota(name, provider);
2086
2086
  }
2087
- if ((provider.authMode ?? "key") === "key" && name === "opencode-go") {
2087
+ // Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
2088
+ // setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
2089
+ // silently had no quota panel and no `ocx provider quota --json` report while the literal
2090
+ // name was the gate. `registryEntryForProviderDestination` is the existing predicate for
2091
+ // exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
2092
+ // a different adapter is still not OpenCode Go. The defensive URL check inside
2093
+ // `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
2094
+ if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
2088
2095
  return fetchOpenCodeGoQuota(name, provider);
2089
2096
  }
2090
2097
  if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {