@bitkyc08/opencodex 2.11.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
  2. package/gui/dist/assets/index-DDZpgzKk.js +70 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +6 -4
  5. package/src/adapters/anthropic.ts +10 -2
  6. package/src/adapters/cursor/native-exec-common.ts +6 -2
  7. package/src/adapters/google-antigravity-replay.ts +3 -2
  8. package/src/adapters/google-antigravity-wire.ts +38 -6
  9. package/src/adapters/google.ts +57 -10
  10. package/src/adapters/openai-chat.ts +19 -2
  11. package/src/claude/model-info.ts +1 -1
  12. package/src/cli/account-auth.ts +3 -1
  13. package/src/cli/account-catalog-refresh.ts +14 -0
  14. package/src/cli/account-extended.ts +15 -1
  15. package/src/cli/codex-shim-readiness.ts +69 -0
  16. package/src/cli/combo.ts +5 -0
  17. package/src/cli/config-command.ts +33 -4
  18. package/src/cli/doctor.ts +21 -0
  19. package/src/cli/export-command.ts +6 -6
  20. package/src/cli/help.ts +6 -6
  21. package/src/cli/index.ts +22 -5
  22. package/src/cli/models-runtime.ts +13 -1
  23. package/src/cli/provider.ts +7 -0
  24. package/src/clients/config-export.ts +67 -5
  25. package/src/codex/account-lifecycle.ts +99 -10
  26. package/src/codex/auth-api.ts +222 -34
  27. package/src/codex/catalog/account-models.ts +9 -4
  28. package/src/codex/catalog/aggregation.ts +41 -8
  29. package/src/codex/catalog/bundled.ts +54 -22
  30. package/src/codex/catalog/effort.ts +47 -20
  31. package/src/codex/catalog/kinds.ts +2 -0
  32. package/src/codex/catalog/metadata.ts +70 -18
  33. package/src/codex/catalog/native-models.ts +7 -0
  34. package/src/codex/catalog/parsing.ts +29 -11
  35. package/src/codex/catalog/provider-fetch.ts +335 -50
  36. package/src/codex/catalog/sync.ts +588 -126
  37. package/src/codex/catalog-refresh-status.ts +87 -0
  38. package/src/codex/catalog-write-serialization.ts +2 -1
  39. package/src/codex/catalog.ts +4 -3
  40. package/src/codex/convergence-types.ts +1 -1
  41. package/src/codex/convergence.ts +190 -52
  42. package/src/codex/custom-model-catalog-migration.ts +176 -0
  43. package/src/codex/features.ts +79 -1
  44. package/src/codex/history-job.ts +81 -1
  45. package/src/codex/history-lock.ts +2 -1
  46. package/src/codex/history-provider.ts +4 -3
  47. package/src/codex/inject.ts +56 -12
  48. package/src/codex/model-cache.ts +50 -10
  49. package/src/codex/transition-state.ts +10 -2
  50. package/src/codex/user-identity.ts +110 -2
  51. package/src/combos/index.ts +3 -0
  52. package/src/combos/types.ts +75 -9
  53. package/src/config.ts +26 -12
  54. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
  55. package/src/integrations/registry.ts +7 -0
  56. package/src/integrations/writer.ts +1 -1
  57. package/src/lib/pinned-http.ts +40 -9
  58. package/src/lib/process-control.ts +4 -1
  59. package/src/lib/provider-outbound.ts +42 -9
  60. package/src/oauth/cursor.ts +25 -5
  61. package/src/oauth/index.ts +59 -45
  62. package/src/providers/antigravity-models.ts +115 -3
  63. package/src/providers/context-cap.ts +13 -5
  64. package/src/providers/model-discovery-limits.ts +16 -0
  65. package/src/providers/model-discovery.ts +14 -15
  66. package/src/providers/provider-id-rewrite.ts +29 -0
  67. package/src/providers/quota.ts +93 -19
  68. package/src/providers/registry.ts +16 -5
  69. package/src/providers/slug-codec.ts +13 -6
  70. package/src/reasoning-effort.ts +22 -0
  71. package/src/router.ts +0 -20
  72. package/src/routing/history/indexer.ts +54 -39
  73. package/src/routing/quota.ts +77 -56
  74. package/src/server/index.ts +32 -7
  75. package/src/server/management/combo-routes.ts +43 -19
  76. package/src/server/management/config-routes.ts +115 -20
  77. package/src/server/management/model-routes.ts +10 -7
  78. package/src/server/management/model-rows.ts +6 -2
  79. package/src/server/management/oauth-account-routes.ts +12 -0
  80. package/src/server/management/provider-routes.ts +114 -40
  81. package/src/server/management/routing-profile-routes.ts +0 -22
  82. package/src/server/management-api.ts +7 -25
  83. package/src/server/responses/core.ts +3 -3
  84. package/src/server/responses/policy-fallback.ts +152 -0
  85. package/src/server/responses.ts +3 -2
  86. package/src/types.ts +15 -2
  87. package/src/usage/cost.ts +0 -0
  88. package/src/vision/describe.ts +3 -1
  89. package/src/vision/index.ts +17 -6
  90. package/src/vision/reasoning.ts +55 -0
  91. package/src/web-search/parse.ts +15 -3
  92. package/gui/dist/assets/index-BynIEIV-.js +0 -70
@@ -7,10 +7,12 @@ import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readR
7
7
  import {
8
8
  clearModelCache,
9
9
  clearProviderDiscoveryStatus,
10
+ captureModelCacheGeneration,
10
11
  DEFAULT_MODEL_CACHE_TTL_MS,
11
12
  getFreshCached,
12
13
  getStaleCached,
13
14
  isModelsFetchCoolingDown,
15
+ isModelCacheGenerationCurrent,
14
16
  markModelsFetchFailure,
15
17
  markProviderDiscoveryFailed,
16
18
  markProviderDiscoveryOk,
@@ -20,6 +22,7 @@ import {
20
22
  } from "../model-cache";
21
23
  import {
22
24
  buildModelsRequest,
25
+ getValidAccessTokenSnapshot,
23
26
  observeActiveOAuthAccessToken,
24
27
  resolveModelsAuthToken,
25
28
  type OAuthActiveTokenObservation,
@@ -27,9 +30,10 @@ import {
27
30
  import type { OcxConfig, OcxProviderConfig } from "../../types";
28
31
  import { modelInList } from "../../types";
29
32
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
30
- import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
33
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
31
34
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
32
- import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
35
+ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
36
+ import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
33
37
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
34
38
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
35
39
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
@@ -47,6 +51,7 @@ import type { NormalizedComboConfig } from "../../combos/types";
47
51
  import {
48
52
  ProviderOutboundPolicyError,
49
53
  providerOutboundGet,
54
+ providerOutboundPost,
50
55
  providerRedirectError,
51
56
  } from "../../lib/provider-outbound";
52
57
  import { redactSecretString } from "../../lib/redact";
@@ -62,9 +67,9 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
62
67
  import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
63
68
 
64
69
 
65
- import { JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
70
+ import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
66
71
  import type { CatalogModel } from "./parsing";
67
- import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
72
+ import { disabledNativeSlugs, hasComboTargets, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
68
73
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
69
74
  import type { ComboCatalogOmission } from "./aggregation";
70
75
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -87,9 +92,16 @@ export interface CatalogGatherProviderAuthOutcome {
87
92
  readonly state: OAuthActiveTokenObservation["kind"];
88
93
  }
89
94
 
95
+ export interface CatalogGatherProviderModelOutcome {
96
+ readonly provider: string;
97
+ readonly state: "authoritative" | "degraded";
98
+ }
99
+
90
100
  export interface GatherRoutedModelsOptions {
91
101
  comboOmissions?: ComboCatalogOmission[];
92
102
  providerAuthOutcomes?: CatalogGatherProviderAuthOutcome[];
103
+ /** Flight-local authority of each provider's returned model rows. */
104
+ providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
93
105
  /** Internal convergence sink for the immutable policy that produced the returned rows. */
94
106
  discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
95
107
  }
@@ -98,13 +110,20 @@ interface GatherFlightResult {
98
110
  models: CatalogModel[];
99
111
  comboOmissions: ComboCatalogOmission[];
100
112
  providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
113
+ providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
101
114
  discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
102
115
  }
103
116
 
117
+ interface ProviderModelsResult {
118
+ readonly models: CatalogModel[];
119
+ readonly outcome: CatalogGatherProviderModelOutcome;
120
+ }
121
+
104
122
  interface ModelsAuthResolution {
105
123
  readonly apiKey: string | undefined;
106
124
  readonly observed: boolean;
107
125
  readonly oauthApiBaseUrl?: string;
126
+ readonly oauthProjectId?: string;
108
127
  }
109
128
 
110
129
  type ModelsAuthResolver =
@@ -119,7 +138,7 @@ type ModelsAuthResolverFactory = (
119
138
  ) => ModelsAuthResolver;
120
139
 
121
140
  interface CapturedModelsRequest {
122
- readonly method: "GET";
141
+ readonly method: "GET" | "POST";
123
142
  readonly url: string;
124
143
  readonly headersWithoutCredential: Readonly<Record<string, string>>;
125
144
  readonly headersWithCredential: Readonly<Record<string, string>>;
@@ -346,11 +365,12 @@ function captureModelsRequest(
346
365
  : undefined;
347
366
  const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
348
367
  const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
349
- if (withoutCredential.url !== withCredential.url) {
368
+ const method = withoutCredential.method ?? "GET";
369
+ if (withoutCredential.url !== withCredential.url || method !== (withCredential.method ?? "GET")) {
350
370
  throw new TypeError(`Provider model discovery URL for ${name} depends on credential bytes.`);
351
371
  }
352
372
  return detachedFrozen({
353
- method: "GET" as const,
373
+ method,
354
374
  url: withoutCredential.url,
355
375
  headersWithoutCredential: withoutCredential.headers,
356
376
  headersWithCredential: withCredential.headers,
@@ -607,6 +627,141 @@ export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderCo
607
627
  return models.map(model => applyProviderConfigHints(name, prov, model, contextCap));
608
628
  }
609
629
 
630
+
631
+ /**
632
+ * Last-resort context window for combo member synthesis when discovery and
633
+ * provider config both omit one. Matches the catalog entry default in
634
+ * `normalizeRoutedCatalogEntry` so incomplete live rows still catalog.
635
+ */
636
+ const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000;
637
+
638
+ interface ComboCatalogMemberFallback {
639
+ readonly contextWindow?: number;
640
+ readonly inputModalities?: readonly string[];
641
+ readonly reasoningEfforts?: readonly string[];
642
+ }
643
+
644
+ /**
645
+ * Resolve a combo target to a catalog member for derivation.
646
+ * Prefer discovery metadata; when the target is missing from the gather map or
647
+ * lacks a positive contextWindow, synthesize from the (registry-enriched)
648
+ * provider config so combos remain catalogued when targets are configured but
649
+ * discovery metadata is incomplete. Disabled providers stay unresolved.
650
+ * When hints still omit contextWindow, prefer known maxInputTokens, else
651
+ * COMBO_MEMBER_CONTEXT_FALLBACK so a live row without ctx does not drop the
652
+ * whole combo from the public catalog.
653
+ */
654
+ export function resolveComboCatalogMember(
655
+ target: { provider: string; model: string },
656
+ memberByKey: ReadonlyMap<string, CatalogModel>,
657
+ providers: ReadonlyMap<string, OcxProviderConfig>,
658
+ contextCap?: number,
659
+ fallback?: ComboCatalogMemberFallback,
660
+ ): CatalogModel | undefined {
661
+ const existing = memberByKey.get(targetKey(target));
662
+ const prov = providers.get(target.provider);
663
+ // Disabled providers never contribute members — even a complete discovery row
664
+ // is unusable for catalog derivation while the provider is off.
665
+ if (prov?.disabled === true) return undefined;
666
+
667
+ const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
668
+ if (!fallback) return member;
669
+ const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
670
+ ? member.contextWindow
671
+ : undefined;
672
+ const addMaxInput = contextWindow !== undefined
673
+ && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
674
+ const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
675
+ && fallback.inputModalities !== undefined;
676
+ const addReasoning = member.reasoningEfforts === undefined
677
+ && fallback.reasoningEfforts !== undefined;
678
+ if (!addMaxInput && !addModalities && !addReasoning) return member;
679
+ return {
680
+ ...member,
681
+ ...(addMaxInput ? { maxInputTokens: contextWindow } : {}),
682
+ ...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
683
+ ...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
684
+ };
685
+ };
686
+
687
+ // Complete live/configured rows still honour providerContextCaps so a high
688
+ // discovery window cannot outrun an operator-configured cap. Native-alias
689
+ // fallback metadata may fill only capability gaps; it never raises an explicit
690
+ // discovered/configured context window.
691
+ if (
692
+ existing
693
+ && typeof existing.contextWindow === "number"
694
+ && existing.contextWindow > 0
695
+ ) {
696
+ const capped = applyProviderContextCap(existing.contextWindow, contextCap);
697
+ if (capped === undefined || capped === existing.contextWindow) {
698
+ return withFallbackMetadata(existing);
699
+ }
700
+ const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0
701
+ ? Math.min(existing.maxInputTokens, capped)
702
+ : capped;
703
+ return withFallbackMetadata({
704
+ ...existing,
705
+ contextWindow: capped,
706
+ maxInputTokens: maxInput,
707
+ contextCap,
708
+ contextCapped: true as const,
709
+ });
710
+ }
711
+
712
+ const base: CatalogModel = existing ?? {
713
+ id: target.model,
714
+ provider: target.provider,
715
+ };
716
+ const hinted = prov
717
+ ? applyProviderConfigHints(target.provider, prov, base, contextCap)
718
+ : base;
719
+ const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
720
+ ? hinted.contextWindow
721
+ : undefined;
722
+ const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
723
+ ? hinted.maxInputTokens
724
+ : (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0
725
+ ? base.maxInputTokens
726
+ : undefined);
727
+ // Real discovery/config values win. A native alias is the next fallback tier.
728
+ // The generic 128k/text synthesis from #1305 remains the final fallback.
729
+ const fallbackContext = existing || prov ? fallback?.contextWindow : undefined;
730
+ const uncappedContext = hintedContext
731
+ ?? knownMaxInput
732
+ ?? fallbackContext
733
+ ?? (existing || prov ? COMBO_MEMBER_CONTEXT_FALLBACK : undefined);
734
+ if (uncappedContext === undefined) return undefined;
735
+ const usedFallback = hintedContext === undefined;
736
+ const cappedContext = applyProviderContextCap(uncappedContext, contextCap);
737
+ const contextWindow = cappedContext ?? uncappedContext;
738
+ const fallbackCapped = usedFallback
739
+ && contextCap !== undefined
740
+ && cappedContext !== undefined
741
+ && cappedContext !== uncappedContext;
742
+
743
+ const inputModalities = hinted.inputModalities
744
+ ?? base.inputModalities
745
+ ?? (fallback?.inputModalities ? [...fallback.inputModalities] : undefined)
746
+ ?? ["text"];
747
+ const reasoningEfforts = hinted.reasoningEfforts
748
+ ?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined)
749
+ ?? base.reasoningEfforts
750
+ ?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined);
751
+ const maxInputTokens = knownMaxInput !== undefined
752
+ ? Math.min(knownMaxInput, contextWindow)
753
+ : contextWindow;
754
+
755
+ return {
756
+ ...hinted,
757
+ inputModalities,
758
+ ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
759
+ contextWindow,
760
+ maxInputTokens,
761
+ ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
762
+ };
763
+ }
764
+
610
765
  export function isDatedVariantId(liveId: string, configuredId: string): boolean {
611
766
  if (!liveId.startsWith(`${configuredId}-`)) return false;
612
767
  return /^\d{8}$/.test(liveId.slice(configuredId.length + 1));
@@ -817,6 +972,7 @@ function observedModelsAuthResolver(
817
972
  apiKey: observation.snapshot.accessToken,
818
973
  observed: true,
819
974
  ...(observation.snapshot.apiBaseUrl ? { oauthApiBaseUrl: observation.snapshot.apiBaseUrl } : {}),
975
+ ...(observation.snapshot.projectId ? { oauthProjectId: observation.snapshot.projectId } : {}),
820
976
  };
821
977
  },
822
978
  };
@@ -827,9 +983,17 @@ async function fetchProviderModelsWithAuth(
827
983
  ttlMs: number,
828
984
  contextCap: number | undefined,
829
985
  resolveAuth: ModelsAuthResolver,
830
- ): Promise<CatalogModel[]> {
986
+ ): Promise<ProviderModelsResult> {
831
987
  const { name, provider: prov, discovery, request } = captured;
832
- if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
988
+ const observed = (
989
+ models: CatalogModel[],
990
+ state: CatalogGatherProviderModelOutcome["state"],
991
+ ): ProviderModelsResult => ({ models, outcome: { provider: name, state } });
992
+ // Capture before any credential refresh or outbound await. OAuth account changes clear this
993
+ // generation, so a request started with the former account cannot later publish its result.
994
+ const cacheGeneration = captureModelCacheGeneration(name);
995
+ const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
996
+ if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
833
997
  const seedVertexDefault = prov.adapter === "google"
834
998
  && prov.googleMode === "vertex"
835
999
  && (prov.models?.length ?? 0) === 0
@@ -844,10 +1008,18 @@ async function fetchProviderModelsWithAuth(
844
1008
  // discovery failure left by an older live configuration even when the account is logged out.
845
1009
  if (prov.liveModels === false) {
846
1010
  clearProviderDiscoveryStatus(name);
847
- return configured;
1011
+ return observed(configured, "authoritative");
848
1012
  }
849
1013
  const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
850
- ? { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
1014
+ ? prov.authMode === "oauth" && effectiveGoogleMode(name, prov) === "cloud-code-assist"
1015
+ ? await getValidAccessTokenSnapshot(name)
1016
+ .then(snapshot => ({
1017
+ apiKey: snapshot.accessToken,
1018
+ observed: false,
1019
+ ...(snapshot.projectId ? { oauthProjectId: snapshot.projectId } : {}),
1020
+ }))
1021
+ .catch(() => ({ apiKey: undefined, observed: false }))
1022
+ : { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
851
1023
  : resolveAuth.resolve(name, prov));
852
1024
  const apiKey = auth.apiKey;
853
1025
  // A configured default is a real callable selector and must remain discoverable when a
@@ -868,47 +1040,70 @@ async function fetchProviderModelsWithAuth(
868
1040
  : models
869
1041
  );
870
1042
  if (prov.adapter === "cursor") {
871
- if (!apiKey) return configured;
1043
+ if (!apiKey) return observed(configured, "degraded");
872
1044
  // Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
873
1045
  // variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
874
1046
  // suffix) but filter the static seed to the bases the account actually has — so models not on the
875
1047
  // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
876
1048
  const cachedCursor = getFreshCached(name, ttlMs);
877
- if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
1049
+ if (cachedCursor) {
1050
+ return observed(applyConfigHintsToCachedModels(name, prov, cachedCursor), "authoritative");
1051
+ }
878
1052
  if (isModelsFetchCoolingDown(name)) {
879
1053
  const cooling = getStaleCached(name);
880
- return cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured;
1054
+ return observed(
1055
+ cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
1056
+ "degraded",
1057
+ );
881
1058
  }
882
1059
  const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
883
1060
  if (liveResult.ok) {
884
1061
  const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
885
1062
  const result = available.length > 0 ? available : configured;
886
1063
  // Count what discovery actually returned, not the configured rows we fall back to.
1064
+ if (!setCached(name, result, Date.now(), cacheGeneration)) return observed(configured, "degraded");
887
1065
  markProviderDiscoveryOk(name, liveResult.models.length);
888
- setCached(name, result);
889
- return result;
1066
+ return observed(result, "authoritative");
1067
+ }
1068
+ if (isCurrentCacheGeneration()) {
1069
+ markModelsFetchFailure(name);
1070
+ markProviderDiscoveryFailed(name, { reason: "provider" });
1071
+ console.warn(
1072
+ `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
1073
+ );
890
1074
  }
891
- markModelsFetchFailure(name);
892
- markProviderDiscoveryFailed(name, { reason: "provider" });
893
- console.warn(
894
- `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
895
- );
896
1075
  const staleCursor = getStaleCached(name);
897
- return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured;
1076
+ return observed(
1077
+ staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured,
1078
+ "degraded",
1079
+ );
898
1080
  }
899
1081
  if (prov.authMode === "oauth" && !apiKey) {
900
1082
  // No usable token (logged out, or account marked needsReauth). Still surface the
901
1083
  // configured static catalog so the GUI Models tab / rail counts are not empty —
902
1084
  // matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
903
- return configured;
1085
+ return observed(configured, "degraded");
904
1086
  }
1087
+ const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist";
1088
+ const project = prov.project ?? auth.oauthProjectId;
1089
+ if (cloudCodeAssist && !project) return observed(configured, "degraded");
905
1090
  const fresh = getFreshCached(name, ttlMs);
906
- if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
1091
+ if (fresh) {
1092
+ return observed(
1093
+ withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)),
1094
+ "authoritative",
1095
+ ); // dedups Codex's frequent /v1/models polling within the TTL
1096
+ }
907
1097
  if (isModelsFetchCoolingDown(name)) {
908
1098
  // A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
909
1099
  // fetch timeout on every catalog poll — the dashboard polls this path per page load.
910
1100
  const stale = getStaleCached(name);
911
- return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : failedDiscoveryConfigured;
1101
+ return observed(
1102
+ stale
1103
+ ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
1104
+ : failedDiscoveryConfigured,
1105
+ "degraded",
1106
+ );
912
1107
  }
913
1108
  const url = request.url;
914
1109
  const headers = materializeCapturedHeaders(request, apiKey);
@@ -918,6 +1113,9 @@ async function fetchProviderModelsWithAuth(
918
1113
  const failedDiscoveryFallback = (
919
1114
  failure: ProviderModelDiscoveryFailure,
920
1115
  ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
1116
+ if (!isCurrentCacheGeneration()) {
1117
+ return { models: failedDiscoveryConfigured, fallback: "configured", shouldLog: false };
1118
+ }
921
1119
  // Decide logging BEFORE recording the new status, so we can compare against the prior one and
922
1120
  // suppress an identical repeated failure (#395 log flood). The failure stays observable via the
923
1121
  // discovery-status API regardless.
@@ -934,10 +1132,16 @@ async function fetchProviderModelsWithAuth(
934
1132
  };
935
1133
  };
936
1134
  try {
937
- const res = await providerOutboundGet(name, prov, url, {
938
- headers,
939
- signal: AbortSignal.timeout(8000),
940
- });
1135
+ const res = request.method === "POST"
1136
+ ? await providerOutboundPost(name, prov, url, {
1137
+ headers,
1138
+ body: JSON.stringify({ project }),
1139
+ signal: AbortSignal.timeout(8000),
1140
+ })
1141
+ : await providerOutboundGet(name, prov, url, {
1142
+ headers,
1143
+ signal: AbortSignal.timeout(8000),
1144
+ });
941
1145
  const redirectError = await providerRedirectError(res, url);
942
1146
  if (redirectError) {
943
1147
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
@@ -946,7 +1150,7 @@ async function fetchProviderModelsWithAuth(
946
1150
  `[opencodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`,
947
1151
  );
948
1152
  }
949
- return models;
1153
+ return observed(models, "degraded");
950
1154
  }
951
1155
  if (!res.ok) {
952
1156
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
@@ -955,7 +1159,7 @@ async function fetchProviderModelsWithAuth(
955
1159
  `[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
956
1160
  );
957
1161
  }
958
- return models;
1162
+ return observed(models, "degraded");
959
1163
  }
960
1164
 
961
1165
  const contentType = (
@@ -974,7 +1178,33 @@ async function fetchProviderModelsWithAuth(
974
1178
  `[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
975
1179
  );
976
1180
  }
977
- return models;
1181
+ return observed(models, "degraded");
1182
+ }
1183
+ const antigravity = cloudCodeAssist
1184
+ ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels)
1185
+ : undefined;
1186
+ if (cloudCodeAssist && !antigravity) {
1187
+ const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
1188
+ if (shouldLog) {
1189
+ console.warn(
1190
+ `[opencodex] Provider model discovery for "${name}" returned malformed CCA model data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
1191
+ );
1192
+ }
1193
+ return observed(models, "degraded");
1194
+ }
1195
+ if (antigravity) {
1196
+ const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
1197
+ id: model.id,
1198
+ provider: name,
1199
+ // CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex
1200
+ // effort-to-wire mapping for a newly discovered model, do not advertise a false ladder.
1201
+ reasoningEfforts: [],
1202
+ ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
1203
+ ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
1204
+ }, contextCap));
1205
+ if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
1206
+ markProviderDiscoveryOk(name, live.length);
1207
+ return observed(live, "authoritative");
978
1208
  }
979
1209
  const extracted = extractProviderModelItems(bounded.value, discovery);
980
1210
  if (!extracted.ok) {
@@ -990,7 +1220,7 @@ async function fetchProviderModelsWithAuth(
990
1220
  `[opencodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
991
1221
  );
992
1222
  }
993
- return models;
1223
+ return observed(models, "degraded");
994
1224
  }
995
1225
  const items = extracted.items;
996
1226
  const live = items.map(m => {
@@ -1034,9 +1264,9 @@ async function fetchProviderModelsWithAuth(
1034
1264
  && !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
1035
1265
  warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
1036
1266
  }
1267
+ if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
1037
1268
  markProviderDiscoveryOk(name, liveModelCount);
1038
- setCached(name, live);
1039
- return live;
1269
+ return observed(live, "authoritative");
1040
1270
  } catch (error) {
1041
1271
  if (error instanceof ProviderOutboundPolicyError) {
1042
1272
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
@@ -1045,7 +1275,7 @@ async function fetchProviderModelsWithAuth(
1045
1275
  `[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`,
1046
1276
  );
1047
1277
  }
1048
- return models;
1278
+ return observed(models, "degraded");
1049
1279
  }
1050
1280
  const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" });
1051
1281
  if (shouldLog) {
@@ -1053,7 +1283,7 @@ async function fetchProviderModelsWithAuth(
1053
1283
  `[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
1054
1284
  );
1055
1285
  }
1056
- return models;
1286
+ return observed(models, "degraded");
1057
1287
  }
1058
1288
  }
1059
1289
 
@@ -1064,7 +1294,12 @@ export async function fetchProviderModels(
1064
1294
  contextCap?: number,
1065
1295
  ): Promise<CatalogModel[]> {
1066
1296
  const captured = captureProviderGather(name, prov, refreshingModelsAuthResolver);
1067
- return fetchProviderModelsWithAuth(captured, ttlMs, contextCap, refreshingModelsAuthResolver);
1297
+ return (await fetchProviderModelsWithAuth(
1298
+ captured,
1299
+ ttlMs,
1300
+ contextCap,
1301
+ refreshingModelsAuthResolver,
1302
+ )).models;
1068
1303
  }
1069
1304
 
1070
1305
  export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
@@ -1089,11 +1324,12 @@ export function filterCatalogVisibleModels(
1089
1324
  if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
1090
1325
  }
1091
1326
  return models.filter(m => {
1327
+ const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
1092
1328
  // disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
1093
1329
  for (const stored of disabled) {
1094
1330
  // Combo management stores the public alias, while canonical `combo/<id>` references
1095
1331
  // remain valid for backward compatibility through slugEquals below.
1096
- if (m.alias !== undefined && stored === catalogModelSlug(m)) return false;
1332
+ if (m.alias !== undefined && stored === catalogModelSlug(m) && !nativeAlias) return false;
1097
1333
  if (slugEquals(stored, m.provider, m.id)) return false;
1098
1334
  }
1099
1335
  const allow = allowByProvider.get(m.provider);
@@ -1176,6 +1412,7 @@ async function gatherRoutedModelsWithAuth(
1176
1412
  models,
1177
1413
  comboOmissions,
1178
1414
  providerAuthOutcomes,
1415
+ providerModelOutcomes,
1179
1416
  discoveryPolicySnapshots,
1180
1417
  } = await entry.promise;
1181
1418
  if (options?.comboOmissions) {
@@ -1186,6 +1423,10 @@ async function gatherRoutedModelsWithAuth(
1186
1423
  options.providerAuthOutcomes.length = 0;
1187
1424
  options.providerAuthOutcomes.push(...providerAuthOutcomes);
1188
1425
  }
1426
+ if (options?.providerModelOutcomes) {
1427
+ options.providerModelOutcomes.length = 0;
1428
+ options.providerModelOutcomes.push(...providerModelOutcomes);
1429
+ }
1189
1430
  if (options?.discoveryPolicySnapshots) {
1190
1431
  options.discoveryPolicySnapshots.length = 0;
1191
1432
  options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
@@ -1209,7 +1450,7 @@ async function gatherRoutedModelsUncached(
1209
1450
  // vision-sidecar model advertised text-only, blocking image attachments app-side).
1210
1451
  // Enrich a CLONE: hydrated defaults must never leak into the persisted config.
1211
1452
  const activeProviders = capture.providers;
1212
- const lists = await Promise.all(
1453
+ const providerResults = await Promise.all(
1213
1454
  activeProviders.map(provider => fetchProviderModelsWithAuth(
1214
1455
  provider,
1215
1456
  ttlMs,
@@ -1217,12 +1458,13 @@ async function gatherRoutedModelsUncached(
1217
1458
  resolveAuth,
1218
1459
  )),
1219
1460
  );
1461
+ const lists = providerResults.map(result => result.models);
1220
1462
  const apiAugmented = augmentRoutedModelsWithCapturedOpenAiApiRows(
1221
1463
  lists.flat(),
1222
1464
  config,
1223
1465
  capture.openAiApiPolicy,
1224
1466
  );
1225
- const all = augmentRoutedModelsWithJawcodeMetadata(apiAugmented, activeProviders.map(provider => provider.name), config.providers, config)
1467
+ const all = augmentRoutedModelsWithMetadata(apiAugmented, activeProviders.map(provider => provider.name), config.providers, config)
1226
1468
  // Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
1227
1469
  // intentionally mirrors Cursor's public model table, including Gemini image preview, so the
1228
1470
  // exposure decision goes through shouldExposeRoutedModel (single choke point).
@@ -1253,8 +1495,16 @@ async function gatherRoutedModelsUncached(
1253
1495
  // configs that will never need it.
1254
1496
  } else {
1255
1497
  const disabled = disabledNativeSlugs(config);
1498
+ const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => {
1499
+ const combo = getCombo(config, id);
1500
+ return combo?.targets.flatMap(target => (
1501
+ target.provider === "openai" ? [target.model] : []
1502
+ )) ?? [];
1503
+ }));
1256
1504
  for (const slug of nativeOpenAiSlugs()) {
1257
- if (disabled.has(slug)) continue;
1505
+ // A bare native disable key hides the native row, not a combo that targets it.
1506
+ // Keep synthetic native metadata available to those combos.
1507
+ if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue;
1258
1508
  const contextWindow = nativeOpenAiContextWindow(slug);
1259
1509
  if (contextWindow === undefined) continue;
1260
1510
  const synthetic: CatalogModel = {
@@ -1273,21 +1523,47 @@ async function gatherRoutedModelsUncached(
1273
1523
  if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
1274
1524
  }
1275
1525
  }
1526
+ // Enriched (registry-hydrated) provider clones — shared by combo member synthesis and
1527
+ // custom-model vision-sidecar inheritance so both see the same merged registry view.
1528
+ const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider]));
1276
1529
  for (const id of listComboIds(config)) {
1277
1530
  const combo = getCombo(config, id);
1278
1531
  if (!combo) continue;
1532
+ const nativeContextWindow = combo.nativeAlias && combo.alias
1533
+ ? nativeOpenAiContextWindow(combo.alias)
1534
+ : undefined;
1535
+ const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
1536
+ ? {
1537
+ contextWindow: nativeContextWindow,
1538
+ inputModalities: nativeInputModalities(combo.alias),
1539
+ reasoningEfforts: nativeReasoningEfforts(combo.alias),
1540
+ }
1541
+ : undefined;
1279
1542
  const members = combo.targets
1280
- .map(target => memberByKey.get(targetKey(target)))
1543
+ .map(target => resolveComboCatalogMember(
1544
+ target,
1545
+ memberByKey,
1546
+ enrichedByName,
1547
+ providerContextCap(config, target.provider),
1548
+ nativeAliasFallback,
1549
+ ))
1281
1550
  .filter((member): member is CatalogModel => member !== undefined);
1282
1551
  const derived = deriveComboCatalogModel(id, combo, members);
1283
- if (derived) all.push(derived);
1552
+ if (derived) {
1553
+ const nativeDefault = combo.nativeAlias && combo.alias
1554
+ ? nativeDefaultReasoningEffort(combo.alias)
1555
+ : undefined;
1556
+ if (combo.defaultEffort === null
1557
+ && nativeDefault
1558
+ && derived.reasoningEfforts?.includes(nativeDefault)) {
1559
+ derived.defaultReasoningEffort = nativeDefault;
1560
+ }
1561
+ all.push(derived);
1562
+ }
1284
1563
  else warnUncataloguedComboOnce(id, combo, members, localOmissions);
1285
1564
  }
1286
1565
  replaceLastComboCatalogOmissions(localOmissions);
1287
1566
  all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
1288
- // Enriched (registry-hydrated) provider clones, keyed by name — the same view used above so
1289
- // custom rows get the same noVisionModels / inputModalities treatment as discovered rows.
1290
- const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider]));
1291
1567
  // Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row
1292
1568
  // with the same slug below, so that row's provider capability metadata is the inheritance source.
1293
1569
  const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
@@ -1297,6 +1573,7 @@ async function gatherRoutedModelsUncached(
1297
1573
  const base: CatalogModel = {
1298
1574
  id: cm.modelId,
1299
1575
  provider: cm.provider,
1576
+ catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
1300
1577
  // Display-only label: never feeds routing (customModels are keyed by routedSlug below).
1301
1578
  ...(cm.displayName ? { displayName: cm.displayName } : {}),
1302
1579
  ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
@@ -1339,10 +1616,18 @@ async function gatherRoutedModelsUncached(
1339
1616
  // Custom rows override discovered rows that encode to the same Codex-facing slug.
1340
1617
  const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
1341
1618
  const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
1619
+ const providerModelOutcomes = providerResults.map(result => (
1620
+ result.outcome.provider === OPENAI_API_PROVIDER_ID
1621
+ && capture.openAiApiPolicy.state === "captured"
1622
+ && capture.openAiApiPolicy.models !== undefined
1623
+ ? { provider: result.outcome.provider, state: "authoritative" as const }
1624
+ : result.outcome
1625
+ ));
1342
1626
  return {
1343
1627
  models: [...deduped, ...customModels],
1344
1628
  comboOmissions: localOmissions,
1345
1629
  providerAuthOutcomes: localProviderAuthOutcomes,
1630
+ providerModelOutcomes,
1346
1631
  discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
1347
1632
  };
1348
1633
  }
@@ -1413,7 +1698,7 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
1413
1698
  ];
1414
1699
  }
1415
1700
 
1416
- export function augmentRoutedModelsWithJawcodeMetadata(
1701
+ export function augmentRoutedModelsWithMetadata(
1417
1702
  models: CatalogModel[],
1418
1703
  providerNames: string[],
1419
1704
  providers?: Record<string, OcxProviderConfig>,
@@ -1424,9 +1709,9 @@ export function augmentRoutedModelsWithJawcodeMetadata(
1424
1709
  for (const provider of providerNames) {
1425
1710
  if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
1426
1711
  if (providers?.[provider]?.liveModels === false) continue;
1427
- const jawcodeProvider = resolveJawcodeProvider(provider);
1712
+ const jawcodeProvider = resolveMetadataProvider(provider);
1428
1713
  if (!jawcodeProvider) continue;
1429
- for (const meta of listJawcodeModelMetadata(jawcodeProvider)) {
1714
+ for (const meta of listModelMetadata(jawcodeProvider)) {
1430
1715
  const key = `${provider}/${meta.id}`;
1431
1716
  if (seen.has(key)) continue;
1432
1717
  seen.add(key);