@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -51,6 +51,28 @@ interface GoogleTokenPayload {
51
51
  id_token?: unknown;
52
52
  }
53
53
 
54
+ const TERMINAL_OAUTH_ERRORS = new Set([
55
+ "access_denied",
56
+ "expired_token",
57
+ "invalid_grant",
58
+ "refresh_token_reused",
59
+ "refresh_token_revoked",
60
+ "revoked",
61
+ "revoked_token",
62
+ ]);
63
+
64
+ export class AntigravityTokenRequestError extends Error {
65
+ readonly httpStatus: number;
66
+ readonly oauthError?: string;
67
+
68
+ constructor(httpStatus: number, oauthError?: string) {
69
+ super(`Antigravity token request failed: ${httpStatus}`);
70
+ this.name = "AntigravityTokenRequestError";
71
+ this.httpStatus = httpStatus;
72
+ if (oauthError) this.oauthError = oauthError;
73
+ }
74
+ }
75
+
54
76
  function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
55
77
  const part = token.split(".")[1];
56
78
  if (!part) return undefined;
@@ -75,8 +97,17 @@ async function postToken(body: Record<string, string>, signal?: AbortSignal): Pr
75
97
  signal: requestSignal(signal),
76
98
  });
77
99
  if (!response.ok) {
78
- // Status only the body can carry grant/account details.
79
- throw new Error(`Antigravity token request failed: ${response.status}`);
100
+ let oauthError: string | undefined;
101
+ if (response.status === 400 || response.status === 401) {
102
+ try {
103
+ const body = await response.json() as { error?: unknown };
104
+ const candidate = typeof body.error === "string" ? body.error.trim().toLowerCase() : "";
105
+ if (TERMINAL_OAUTH_ERRORS.has(candidate)) oauthError = candidate;
106
+ } catch {
107
+ /* Keep status-only errors for malformed or non-JSON responses. */
108
+ }
109
+ }
110
+ throw new AntigravityTokenRequestError(response.status, oauthError);
80
111
  }
81
112
  return (await response.json()) as GoogleTokenPayload;
82
113
  }
@@ -221,7 +252,11 @@ export async function loginAntigravity(ctrl: OAuthController, opts?: { forceAcco
221
252
  return new AntigravityOAuthFlow(ctrl, opts).login();
222
253
  }
223
254
 
224
- export async function refreshAntigravityToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials> {
255
+ export async function refreshAntigravityToken(
256
+ refreshToken: string,
257
+ signal?: AbortSignal,
258
+ previousCredential?: OAuthCredentials,
259
+ ): Promise<OAuthCredentials> {
225
260
  if (!refreshToken) throw new Error("Antigravity credentials are expired and do not include a refresh token");
226
261
  const payload = await postToken({
227
262
  grant_type: "refresh_token",
@@ -230,8 +265,10 @@ export async function refreshAntigravityToken(refreshToken: string, signal?: Abo
230
265
  refresh_token: refreshToken,
231
266
  }, signal);
232
267
  const creds = credentialsFromPayload(payload, refreshToken);
233
- // Re-discover the project on refresh so a newly-onboarded account fills in projectId.
234
- const projectId = await discoverAntigravityProject(creds.access, signal).catch(() => undefined);
268
+ // Project discovery is onboarding work, not ordinary token renewal. Preserve the existing
269
+ // account-scoped project id and avoid an extra authenticated CCA call when it is present.
270
+ const projectId = previousCredential?.projectId
271
+ ?? await discoverAntigravityProject(creds.access, signal).catch(() => undefined);
235
272
  return projectId ? { ...creds, projectId } : creds;
236
273
  }
237
274
 
@@ -3,7 +3,7 @@ import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
3
3
  import { isAccountNeedsReauth } from "../codex/account-runtime-state";
4
4
  import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
5
5
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
6
- import { readRuntimePort } from "../config";
6
+ import { readRuntimePort } from "../config/process-state";
7
7
  import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
8
8
  import { maskAccountId } from "../lib/privacy";
9
9
  import { findLiveProxy } from "../server/proxy-liveness";
@@ -10,7 +10,7 @@ import { ANTHROPIC_OAUTH_BETA, AnthropicTokenError, loginAnthropic, refreshAnthr
10
10
  import { loginKimi, refreshKimiToken } from "./kimi";
11
11
  import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, RefreshIntentIOError } from "./nous";
12
12
  import { loginChatGPT, refreshChatGPTToken } from "./chatgpt";
13
- import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
13
+ import { AntigravityTokenRequestError, loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
14
14
  import { loginCursor, refreshCursorToken } from "./cursor";
15
15
  import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
16
16
  import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
@@ -222,6 +222,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
222
222
  refresh: refreshAntigravityToken,
223
223
  providerConfig: oauthConfig("google-antigravity"),
224
224
  defaultModel: oauthDefaultModel("google-antigravity"),
225
+ defaultRefreshPolicy: "lazy-only",
225
226
  },
226
227
  cursor: {
227
228
  login: (ctrl, opts) => loginCursor(ctrl, undefined, { forceLogin: opts?.forceLogin }),
@@ -466,8 +467,16 @@ export async function getValidAccessTokenSnapshot(provider: string): Promise<OAu
466
467
  return resolveAccessSnapshotForAccount(provider, set.activeAccountId);
467
468
  }
468
469
 
470
+ /** Resolve a specific stored account without consulting or changing the active account. */
471
+ export async function getValidAccessTokenSnapshotForAccount(
472
+ provider: string,
473
+ accountId: string,
474
+ ): Promise<OAuthAccessSnapshot> {
475
+ return resolveAccessSnapshotForAccount(provider, accountId);
476
+ }
477
+
469
478
  /** Providers whose upstream-401 replay path may force a snapshot refresh. */
470
- const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro"]);
479
+ const FORCE_REFRESH_PROVIDERS = new Set(["xai", "github-copilot", "kiro", "google-antigravity"]);
471
480
 
472
481
  export async function forceRefreshOAuthAccessSnapshot(
473
482
  rejected: OAuthAccessSnapshot,
@@ -501,6 +510,9 @@ function isTerminalRefreshError(err: unknown): boolean {
501
510
  || msg.includes("expired_token");
502
511
  }
503
512
  function terminal(error:unknown):boolean{
513
+ if (error instanceof AntigravityTokenRequestError) {
514
+ return (error.httpStatus === 400 || error.httpStatus === 401) && error.oauthError !== undefined;
515
+ }
504
516
  if(error instanceof XaiTokenRequestError)return ["invalid_grant","refresh_token_reused","revoked_token"].includes(error.oauthError??"");
505
517
  if(error instanceof AnthropicTokenError)return (error.httpStatus===400||error.httpStatus===401)&&["invalid_grant","refresh_token_reused","revoked","revoked_token","refresh_token_revoked"].includes(error.oauthError??"");
506
518
  if(error instanceof KiroTokenRefreshError)return (error.httpStatus===400||error.httpStatus===401)&&error.oauthError!==undefined;
@@ -630,7 +642,7 @@ export async function refreshAnthropicAccountWithLock(
630
642
  writeOAuthRefreshIntent(provider, accountId, generation, now(), deps.flight?.flightId);
631
643
  if (deps.signal?.aborted) throw deps.signal.reason;
632
644
  if (deps.flight) deps.flight.dispatched = true;
633
- const fresh = merged(await def.refresh(stored.refresh, deps.signal), stored);
645
+ const fresh = merged(await def.refresh(stored.refresh, deps.signal, stored), stored);
634
646
  const outcome = await mergeAccountCredential(provider, accountId, fresh, {
635
647
  expectedGeneration: generation,
636
648
  afterPrePersistRead: deps.afterPrePersistRead,
package/src/oauth/kimi.ts CHANGED
@@ -209,7 +209,15 @@ async function pollForToken(deviceCode: string, intervalMs: number, expiresInMs:
209
209
 
210
210
  export async function loginKimi(ctrl: OAuthController): Promise<OAuthCredentials> {
211
211
  const device = await requestDeviceAuthorization();
212
- ctrl.onAuth?.({ url: device.verificationUriComplete, instructions: `Enter code: ${device.userCode}` });
212
+ // `deviceCode` carries the human-typed user code, matching the other device
213
+ // providers. It is also what tells the management login route this is a device
214
+ // flow, so a provider-supplied verification URI is no longer handed to a local
215
+ // browser spawn — the URL stays visible and copyable in every login surface.
216
+ ctrl.onAuth?.({
217
+ url: device.verificationUriComplete,
218
+ instructions: `Enter code: ${device.userCode}`,
219
+ deviceCode: device.userCode,
220
+ });
213
221
  return pollForToken(device.deviceCode, device.intervalMs, device.expiresInMs, ctrl.signal);
214
222
  }
215
223
 
@@ -0,0 +1,26 @@
1
+ import type { OcxConfig } from "../types";
2
+
3
+ /**
4
+ * Whether a login should open a browser on the machine running the proxy.
5
+ *
6
+ * Both login routes share this so the two surfaces cannot drift: a per-request
7
+ * `openBrowser` beats the persisted setting, and the persisted setting beats
8
+ * the historical default. It lives in `src/oauth/` rather than beside either
9
+ * route because `src/codex/auth-api.ts` and
10
+ * `src/server/management/oauth-account-routes.ts` both already import from
11
+ * here, so no new edge is added to the import graph.
12
+ *
13
+ * The default is the important part. An operator who upgrades and configures
14
+ * nothing must see exactly what they saw before, so only an explicit `false`
15
+ * declines — `undefined`, `true`, and a malformed value all open.
16
+ *
17
+ * A malformed request field is ignored rather than rejected on purpose: this is
18
+ * a display preference, and no login should fail because of one.
19
+ */
20
+ export function shouldOpenBrowserForLogin(
21
+ requested: unknown,
22
+ config: Pick<OcxConfig, "oauthOpenBrowser">,
23
+ ): boolean {
24
+ if (typeof requested === "boolean") return requested;
25
+ return config.oauthOpenBrowser !== false;
26
+ }
@@ -593,9 +593,11 @@ export async function upsertCredentialByIdentity(
593
593
 
594
594
  /** Remove the ACTIVE account; remaining accounts promote the first one. */
595
595
  export async function removeCredential(provider: string): Promise<void> {
596
+ let removedAccountId: string | undefined;
596
597
  await mutateStore(store => {
597
598
  const set = store[provider];
598
599
  if (!set) return;
600
+ removedAccountId = set.activeAccountId;
599
601
  set.accounts = set.accounts.filter(a => a.id !== set.activeAccountId);
600
602
  if (set.accounts.length === 0) {
601
603
  delete store[provider];
@@ -603,6 +605,10 @@ export async function removeCredential(provider: string): Promise<void> {
603
605
  }
604
606
  set.activeAccountId = set.accounts[0]!.id;
605
607
  }, [provider]);
608
+ if (provider === "google-antigravity" && removedAccountId) {
609
+ const { clearAntigravityRoutingStateForAccount } = await import("./antigravity-routing");
610
+ clearAntigravityRoutingStateForAccount(removedAccountId);
611
+ }
606
612
  }
607
613
 
608
614
  // ---------------------------------------------------------------------------
@@ -90,7 +90,9 @@ function candidateModelName(record: Record<string, unknown>): string {
90
90
  ?? record.modelName
91
91
  ?? record.model
92
92
  ?? record.name
93
- ?? record.displayName;
93
+ ?? record.displayName
94
+ ?? record.modelFamily
95
+ ?? record.model_family;
94
96
  return typeof explicit === "string" ? explicit.toLowerCase() : "";
95
97
  }
96
98
 
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { createHash } from "node:crypto";
10
10
  import { saveConfigPreservingClaudeCode } from "../config";
11
+ import { isAzureIdentityProvider } from "../config/provider-validation";
11
12
  import type { OcxConfig, OcxProviderConfig } from "../types";
12
13
 
13
14
  export interface ProviderApiKeyInfo {
@@ -36,7 +37,7 @@ export function apiKeyPoolEntryId(key: string): string {
36
37
 
37
38
  /** True for providers whose upstream auth is a configured API key (not oauth/forward). */
38
39
  export function isKeyAuthProvider(provider: OcxProviderConfig): boolean {
39
- return provider.authMode !== "oauth" && provider.authMode !== "forward";
40
+ return !isAzureIdentityProvider(provider) && provider.authMode !== "oauth" && provider.authMode !== "forward";
40
41
  }
41
42
 
42
43
  /** Trim and reject blank / CRLF-bearing secrets. Shared by pool writes and OAuth upsert. */
@@ -0,0 +1,65 @@
1
+ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
2
+ import { redactSecretString } from "../lib/redact";
3
+
4
+ const RESERVED_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
5
+
6
+ function positiveSafeInteger(value: unknown): value is number {
7
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
8
+ }
9
+
10
+ /**
11
+ * Resolve a client-facing soft compaction budget without changing any hard
12
+ * model limit. Configuration and measured input ceilings may only lower the
13
+ * default 90% envelope.
14
+ */
15
+ export function clampAutoCompactTokenLimit(
16
+ contextWindow: number,
17
+ maxInputTokens?: number,
18
+ configuredLimit?: number,
19
+ ): number {
20
+ const candidates = [Math.floor(contextWindow * 0.9), contextWindow];
21
+ if (positiveSafeInteger(maxInputTokens)) candidates.push(maxInputTokens);
22
+ if (positiveSafeInteger(configuredLimit)) candidates.push(configuredLimit);
23
+ return Math.min(...candidates);
24
+ }
25
+
26
+ export type AutoCompactBudgetValidationOptions = Readonly<{
27
+ /** PATCH accepts null for whole-map and per-key deletion. */
28
+ allowTombstones?: boolean;
29
+ /** The canonical ChatGPT provider accepts only exact supported native ids. */
30
+ requireNativeIds?: boolean;
31
+ }>;
32
+
33
+ /** Shared config/load/management boundary for per-model soft budgets. */
34
+ export function modelAutoCompactTokenLimitsConfigError(
35
+ value: unknown,
36
+ options: AutoCompactBudgetValidationOptions = {},
37
+ ): string | null {
38
+ const field = "modelAutoCompactTokenLimits";
39
+ if (value === undefined || (options.allowTombstones && value === null)) return null;
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
41
+ return `${field} must be a plain object${options.allowTombstones ? " or null" : ""}`;
42
+ }
43
+ const prototype = Object.getPrototypeOf(value);
44
+ if (prototype !== Object.prototype && prototype !== null) {
45
+ return `${field} must be a plain object with own properties`;
46
+ }
47
+ for (const [modelId, entry] of Object.entries(value as Record<string, unknown>)) {
48
+ const safeModelId = JSON.stringify(redactSecretString(modelId));
49
+ if (!modelId.trim()) return `${field} keys must be nonblank model ids`;
50
+ if (RESERVED_OBJECT_KEYS.has(modelId)) {
51
+ return `${field} key ${safeModelId} is reserved`;
52
+ }
53
+ if (options.requireNativeIds
54
+ && (modelId.includes("/") || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(modelId))) {
55
+ return `${field} key ${safeModelId} must be an exact supported native model id`;
56
+ }
57
+ if (options.allowTombstones && entry === null) continue;
58
+ if (!positiveSafeInteger(entry)) {
59
+ return `${field}[${safeModelId}] must be a positive safe integer${
60
+ options.allowTombstones ? " or null" : ""
61
+ }`;
62
+ }
63
+ }
64
+ return null;
65
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
+ import { isAzureIdentityProvider } from "../config/provider-validation";
2
3
  import { cloneFastWire } from "./fastwire";
3
4
  import {
4
5
  PROVIDER_REGISTRY,
@@ -226,6 +227,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
226
227
  ...(entry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: entry.modelSuffixBracketStrip } : {}),
227
228
  ...(entry.staticHeaders ? { headers: { ...entry.staticHeaders } } : {}),
228
229
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
230
+ ...(entry.requestPacing ? { requestPacing: structuredClone(entry.requestPacing) } : {}),
229
231
  ...(entry.models ? { models: [...entry.models] } : {}),
230
232
  ...(liveModels !== undefined ? { liveModels } : {}),
231
233
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
@@ -416,6 +418,7 @@ export function hasLegacyClinePassReasoningEfforts(name: string, prov: OcxProvid
416
418
  }
417
419
 
418
420
  export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
421
+ if (isAzureIdentityProvider(prov)) prov.liveModels = false;
419
422
  const entry = PROVIDER_REGISTRY.find(row => row.id === name);
420
423
  if (!entry || !providerMatchesRegistryTransportWithStaticGuards(name, prov)) {
421
424
  // Name lookup failed, but the row may still point at a vendor route we know. #1100 was
@@ -438,6 +441,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
438
441
  repairStaticModelCatalogProvider(name, prov);
439
442
  if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
440
443
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
444
+ if (prov.requestPacing === undefined && seed.requestPacing) prov.requestPacing = structuredClone(seed.requestPacing);
441
445
  if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
442
446
  // Fill mode only when absent: an explicit persisted `direct` must never be overwritten.
443
447
  if (prov.codexAccountMode === undefined && seed.codexAccountMode !== undefined) prov.codexAccountMode = seed.codexAccountMode;
@@ -9,6 +9,7 @@
9
9
  * Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
10
10
  */
11
11
  import { saveConfigPreservingClaudeCode } from "../config";
12
+ import { isAzureIdentityProvider } from "../config/provider-validation";
12
13
  import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy } from "../types";
13
14
  import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
14
15
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
@@ -84,6 +85,7 @@ function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()):
84
85
  * Returns true only for key-auth providers with 2+ pool entries.
85
86
  */
86
87
  export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
88
+ if (isAzureIdentityProvider(provider)) return false;
87
89
  if (provider.authMode === "oauth" || provider.authMode === "forward") return false;
88
90
  return (provider.apiKeyPool?.length ?? 0) >= 2;
89
91
  }
@@ -96,7 +98,7 @@ export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
96
98
  * callers never re-check fields.
97
99
  */
98
100
  export function rateLimitRetryPolicyFor(
99
- provider: Pick<OcxProviderConfig, "retryOn429" | "authMode">,
101
+ provider: Pick<OcxProviderConfig, "retryOn429" | "authMode" | "adapter" | "azureCredential">,
100
102
  ): Required<RateLimitRetryPolicy> | null {
101
103
  const policy = provider.retryOn429;
102
104
  if (!policy || policy.enabled === false) return null;
@@ -105,6 +107,7 @@ export function rateLimitRetryPolicyFor(
105
107
  // same token, local runtimes have no remote key to preserve, and unknown/custom values are
106
108
  // rejected rather than guessed at.
107
109
  if (provider.authMode !== undefined && provider.authMode !== "key") return null;
110
+ if (isAzureIdentityProvider(provider)) return null;
108
111
  return {
109
112
  enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
110
113
  attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
@@ -153,6 +156,7 @@ export function rotateKeyOn429(
153
156
  ): OcxProviderConfig | null {
154
157
  const provider = config.providers[providerName];
155
158
  if (!provider) return null;
159
+ if (isAzureIdentityProvider(provider)) return null;
156
160
  if (provider.authMode === "oauth" || provider.authMode === "forward") return null;
157
161
 
158
162
  const pool = provider.apiKeyPool;
@@ -237,6 +237,7 @@ function hasKnownLegacyOpenAiReference(config: OcxConfig): boolean {
237
237
  || matchesList(config.subagentModels)
238
238
  || (config.subagentRoles ?? []).some(role => matches(role.model))
239
239
  || matches(config.injectionModel)
240
+ || matches(config.v2NativeParentOverride?.model)
240
241
  || matches(config.shadowCallIntercept?.model)
241
242
  || matches(config.webSearchSidecar?.model)
242
243
  || matches(config.visionSidecar?.model)
@@ -260,6 +261,9 @@ function rewriteLegacyOpenAiReferences(config: OcxConfig, warnings: string[]): v
260
261
  }
261
262
  }
262
263
  if (config.injectionModel) config.injectionModel = rewriteLegacyOpenAiSelectedId(config.injectionModel);
264
+ if (config.v2NativeParentOverride?.model) {
265
+ config.v2NativeParentOverride.model = rewriteLegacyOpenAiSelectedId(config.v2NativeParentOverride.model);
266
+ }
263
267
  if (config.shadowCallIntercept?.model) {
264
268
  config.shadowCallIntercept.model = rewriteLegacyOpenAiSelectedId(config.shadowCallIntercept.model);
265
269
  }
@@ -305,6 +309,7 @@ function isKnownLegacyValuePath(path: readonly string[]): boolean {
305
309
  if (/^providers\.(openai|openai-multi)\.selectedModels\.\d+$/.test(joined)) return true;
306
310
  return new Set([
307
311
  "injectionModel",
312
+ "v2NativeParentOverride.model",
308
313
  "shadowCallIntercept.model",
309
314
  "webSearchSidecar.model",
310
315
  "visionSidecar.model",
@@ -82,6 +82,7 @@ export function rewriteProviderReferences(config: OcxConfig, from: string, to: s
82
82
 
83
83
  const scalarOwners: Array<[Record<string, unknown> | undefined, string]> = [
84
84
  [config as unknown as Record<string, unknown>, "injectionModel"],
85
+ [config.v2NativeParentOverride as Record<string, unknown> | undefined, "model"],
85
86
  [config.shadowCallIntercept as Record<string, unknown> | undefined, "model"],
86
87
  [config.webSearchSidecar as Record<string, unknown> | undefined, "model"],
87
88
  [config.visionSidecar as Record<string, unknown> | undefined, "model"],
@@ -8,7 +8,7 @@ import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache
8
8
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
9
9
  import { codexPlanKey } from "../codex/plan";
10
10
  import { resolveEnvValue } from "../config";
11
- import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
11
+ import { getValidAccessToken, getValidAccessTokenForAccount, getValidAccessTokenSnapshot, getValidAccessTokenSnapshotForAccount, type OAuthAccessSnapshot } from "../oauth";
12
12
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
13
13
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
14
14
  import { apiKeyPoolEntryId } from "./api-keys";
@@ -34,6 +34,7 @@ import {
34
34
  isTerminalAntigravityQuotaStatus,
35
35
  } from "./antigravity-quota";
36
36
  import { antigravityHostCandidates, isAntigravityHttpsHost } from "../adapters/google-antigravity-hosts";
37
+ import { providerTlsFetch } from "../lib/provider-tls-profile";
37
38
 
38
39
  /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
39
40
  const ACCOUNT_TOKEN_SKEW_MS = 60_000;
@@ -1400,7 +1401,7 @@ export interface ProviderAccountQuota {
1400
1401
 
1401
1402
  /** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
1402
1403
  export function supportsPerAccountQuota(provider: string): boolean {
1403
- return provider === "anthropic";
1404
+ return provider === "anthropic" || provider === "google-antigravity";
1404
1405
  }
1405
1406
 
1406
1407
  function accountCacheKey(provider: string, accountId: string): string {
@@ -1519,8 +1520,13 @@ async function fetchAccountQuota(
1519
1520
 
1520
1521
  const probe = (async (): Promise<AccountQuotaCacheEntry> => {
1521
1522
  try {
1522
- const token = await getTokenForAccountQuotaProbe(provider, accountId);
1523
- const quota = await fetchAnthropicUsageQuota(token);
1523
+ let quota: ProviderQuota | null = null;
1524
+ if (provider === "anthropic") {
1525
+ const token = await getTokenForAccountQuotaProbe(provider, accountId);
1526
+ quota = await fetchAnthropicUsageQuota(token);
1527
+ } else if (provider === "google-antigravity") {
1528
+ quota = await fetchAntigravityAccountQuota(accountId);
1529
+ }
1524
1530
  if (!quota) {
1525
1531
  // Preserve last-good bars and mark unavailable; advance TTL so failures
1526
1532
  // negative-cache instead of re-probing on every GUI poll.
@@ -2077,23 +2083,63 @@ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | un
2077
2083
  return normalizePercent(100 - remaining);
2078
2084
  }
2079
2085
 
2086
+ async function fetchAntigravityAccountQuota(accountId: string): Promise<ProviderQuota | null> {
2087
+ let snapshot: OAuthAccessSnapshot;
2088
+ try {
2089
+ snapshot = await getValidAccessTokenSnapshotForAccount("google-antigravity", accountId);
2090
+ } catch {
2091
+ return null;
2092
+ }
2093
+ if (!snapshot.projectId) return null;
2094
+ const entry = getProviderRegistryEntry("google-antigravity");
2095
+ const baseUrl = (entry?.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
2096
+ const providerConfig: Pick<OcxProviderConfig, "adapter" | "authMode" | "baseUrl" | "googleMode" | "tlsProfile"> = {
2097
+ adapter: "google",
2098
+ authMode: "oauth",
2099
+ googleMode: "cloud-code-assist",
2100
+ baseUrl,
2101
+ };
2102
+ const fetchImpl = providerTlsFetch("google-antigravity", providerConfig, globalThis.fetch);
2103
+ let liveQuota: ProviderQuota | null;
2104
+ try {
2105
+ liveQuota = await fetchAntigravityLiveQuota({
2106
+ accessToken: snapshot.accessToken,
2107
+ projectId: snapshot.projectId,
2108
+ baseUrl,
2109
+ timeoutMs: REQUEST_TIMEOUT_MS,
2110
+ fetchImpl,
2111
+ });
2112
+ } catch (error) {
2113
+ if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
2114
+ return null;
2115
+ }
2116
+ liveQuota = null;
2117
+ }
2118
+ if (!liveQuota) return null;
2119
+ return {
2120
+ ...liveQuota,
2121
+ updatedAt: Date.now(),
2122
+ };
2123
+ }
2124
+
2080
2125
  async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
2081
- const credential = getCredential("google-antigravity");
2082
- if (!credential?.projectId) return null;
2083
- let accessToken: string;
2126
+ let snapshot: OAuthAccessSnapshot;
2084
2127
  try {
2085
- accessToken = await getValidAccessToken("google-antigravity");
2128
+ snapshot = await getValidAccessTokenSnapshot("google-antigravity");
2086
2129
  } catch {
2087
2130
  return null;
2088
2131
  }
2132
+ if (!snapshot.projectId) return null;
2089
2133
  const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
2134
+ const fetchImpl = providerTlsFetch(provider, config, globalThis.fetch);
2090
2135
  let liveQuota: ProviderQuota | null;
2091
2136
  try {
2092
2137
  liveQuota = await fetchAntigravityLiveQuota({
2093
- accessToken,
2094
- projectId: credential.projectId,
2138
+ accessToken: snapshot.accessToken,
2139
+ projectId: snapshot.projectId,
2095
2140
  baseUrl,
2096
2141
  timeoutMs: REQUEST_TIMEOUT_MS,
2142
+ fetchImpl,
2097
2143
  });
2098
2144
  } catch (error) {
2099
2145
  if (error instanceof AntigravityQuotaRpcError && isTerminalAntigravityQuotaStatus(error.status)) {
@@ -2106,15 +2152,15 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
2106
2152
  for (const [index, host] of antigravityHostCandidates(baseUrl).entries()) {
2107
2153
  if (!isAntigravityHttpsHost(host)) continue;
2108
2154
  try {
2109
- const response = await fetch(`${host}/v1internal:fetchAvailableModels`, {
2155
+ const response = await fetchImpl(`${host}/v1internal:fetchAvailableModels`, {
2110
2156
  method: "POST",
2111
2157
  headers: {
2112
2158
  Accept: "application/json",
2113
2159
  "Content-Type": "application/json",
2114
2160
  "User-Agent": antigravityUserAgent(),
2115
- Authorization: `Bearer ${accessToken}`,
2161
+ Authorization: `Bearer ${snapshot.accessToken}`,
2116
2162
  },
2117
- body: JSON.stringify({ project: credential.projectId }),
2163
+ body: JSON.stringify({ project: snapshot.projectId }),
2118
2164
  redirect: "error",
2119
2165
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2120
2166
  });
@@ -161,6 +161,7 @@ export interface ProviderRegistryEntry {
161
161
  note?: string;
162
162
  dashboardUrl?: string;
163
163
  defaultModel?: string;
164
+ requestPacing?: OcxProviderConfig["requestPacing"];
164
165
  models?: string[];
165
166
  liveModels?: boolean;
166
167
  /**
@@ -442,7 +443,7 @@ const THINKING_TOGGLE_MAP: Record<string, string> = {
442
443
  max: "enabled",
443
444
  };
444
445
  const OPENCODE_GO_THINKING_TOGGLE_MODELS = [
445
- "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2-omni", "mimo-v2-pro", "glm-5", "glm-5.1",
446
+ "mimo-v2.5", "mimo-v2.5-pro", "glm-5", "glm-5.1",
446
447
  ];
447
448
  /**
448
449
  * Zhipu's domestic BigModel platform. Text families first, then the vision member: modalities are
@@ -1597,7 +1598,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1597
1598
  // 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
1598
1599
  // evidence from ai.google.dev does not establish Vertex publisher availability.
1599
1600
  { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
1600
- { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1601
+ { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", requestPacing: { enabled: true, requestsPerMinute: 30, minIntervalMs: 2_000, jitterMs: 500 }, modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
1601
1602
  { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
1602
1603
  { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
1603
1604
  { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
@@ -1867,6 +1868,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1867
1868
  liveModels: true,
1868
1869
  preserveCustomDestination: true,
1869
1870
  defaultModel: "deepseek/deepseek-v4-flash",
1871
+ promptCacheKey: true,
1870
1872
  // The default is also the cold-start seed: live discovery failure must not empty the catalog
1871
1873
  // for a freshly configured provider with no stale cache (issue #308 pattern).
1872
1874
  models: ["deepseek/deepseek-v4-flash"],