@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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.
- package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -1,2698 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} from "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
import {
|
|
54
|
-
bundledCatalogCacheState,
|
|
55
|
-
loadBundledCodexCatalog,
|
|
56
|
-
resetBundledCatalogCacheForTests,
|
|
57
|
-
} from "./bundled";
|
|
58
|
-
import { isMultiAgentV2Enabled } from "../features";
|
|
59
|
-
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
60
|
-
import {
|
|
61
|
-
clearGatherRoutedModelsInflight,
|
|
62
|
-
filterCatalogVisibleModels,
|
|
63
|
-
gatherRoutedModels,
|
|
64
|
-
lastDropWarnSignature,
|
|
65
|
-
type CatalogGatherProviderModelOutcome,
|
|
66
|
-
} from "./provider-fetch";
|
|
67
|
-
import { accountSelectorShadowCollisionWarnings, clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, comboUnrestorableShadowWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnAccountSelectorShadowedProviderOnce, warnComboMasqueradeCollisionOnce, warnComboUnrestorableShadowOnce } from "./aggregation";
|
|
68
|
-
import type { ComboCatalogOmission } from "./aggregation";
|
|
69
|
-
import {
|
|
70
|
-
withCatalogWriteSerialization,
|
|
71
|
-
type CatalogWritePermit,
|
|
72
|
-
} from "../catalog-write-serialization";
|
|
73
|
-
import {
|
|
74
|
-
publishHashedCodexCatalogBackup,
|
|
75
|
-
publishLegacyCodexCatalogBackup,
|
|
76
|
-
replaceActiveCodexCatalog,
|
|
77
|
-
replaceCodexModelsCache,
|
|
78
|
-
} from "../internal/catalog-writer";
|
|
79
|
-
import { codexRuntimeStatePath } from "../runtime";
|
|
80
|
-
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
81
|
-
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./native-models";
|
|
82
|
-
import { observedReserveCatalogSource } from "./metadata";
|
|
83
|
-
import {
|
|
84
|
-
createReserveCatalogProjection,
|
|
85
|
-
isReserveCatalogProjection,
|
|
86
|
-
RESERVE_LUNA_METADATA_SOURCE,
|
|
87
|
-
RESERVE_SOURCE_CATALOG_FIELD,
|
|
88
|
-
type ReserveCatalogProjection,
|
|
89
|
-
} from "./reserve";
|
|
90
|
-
|
|
91
|
-
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
92
|
-
|
|
93
|
-
// Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
|
|
94
|
-
// reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but not
|
|
95
|
-
// OpenCodex's natural-priority guidance window. Native Codex advertisements still follow the
|
|
96
|
-
// visible priority and can differ from that guidance window.
|
|
97
|
-
export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
98
|
-
|
|
99
|
-
// OpenCodex-private catalog field: the guidance candidate priority a row would have WITHOUT
|
|
100
|
-
// modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
|
|
101
|
-
// is invisible to Codex; effectiveSubagentRoster reads it to keep OpenCodex guidance candidates
|
|
102
|
-
// independent of display order. It does not freeze native advertisements. Absent on unmoved rows.
|
|
103
|
-
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
104
|
-
|
|
105
|
-
// OpenCodex-private catalog field: this row is listed but currently unable to serve (#1711).
|
|
106
|
-
// Codex ignores unknown catalog fields (same as opencodex_catalog_kind and the spawn priority
|
|
107
|
-
// above) and ensureStrictCatalogFields does not strip extras, so this is invisible to the native
|
|
108
|
-
// picker and cannot change what Codex offers. It never touches `visibility`.
|
|
109
|
-
export const CATALOG_INACTIVE_REASON_FIELD = "opencodex_inactive_reason";
|
|
110
|
-
|
|
111
|
-
export type SpawnAgentSurface = "v1" | "v2";
|
|
112
|
-
|
|
113
|
-
export type SubagentRosterExclusionReason =
|
|
114
|
-
| "missing_catalog_entry"
|
|
115
|
-
| "picker_hidden"
|
|
116
|
-
| "surface_incompatible"
|
|
117
|
-
| "outside_display_limit";
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Whether a catalog entry may be offered as a V2 subagent model.
|
|
121
|
-
*
|
|
122
|
-
* Upstream changed this rule in codex-rs `6d4d9442c` ("Support leaf models in
|
|
123
|
-
* multi-agent v2"). `model_supports_multi_agent_backend`
|
|
124
|
-
* (core/src/tools/handlers/multi_agents_common.rs:36-42) now admits EVERY model
|
|
125
|
-
* except one explicitly marked `disabled`; the older `== Some(V2)` equality that
|
|
126
|
-
* `92938d880` introduced is gone.
|
|
127
|
-
*
|
|
128
|
-
* The field no longer answers "may I be a delegation target". It answers "does the
|
|
129
|
-
* CHILD get collaboration tools": `collab_tools_enabled`
|
|
130
|
-
* (core/src/tools/spec_plan.rs:599-610) grants a child recursive tools only when its
|
|
131
|
-
* own catalog value is exactly `Some(V2)`. The three-way distinction survives, but it
|
|
132
|
-
* now means eligible-recursive / eligible-LEAF / excluded:
|
|
133
|
-
*
|
|
134
|
-
* - `"v2"` -> eligible, and the child may itself delegate.
|
|
135
|
-
* - `"v1"` -> eligible LEAF worker. This is upstream's pin for `gpt-5.6-luna`
|
|
136
|
-
* (models-manager/models.json); excluding it here is exactly what
|
|
137
|
-
* kept Luna out of opencodex's roster.
|
|
138
|
-
* - absent/null -> eligible LEAF worker (routed or unpinned-native model).
|
|
139
|
-
* - `"disabled"` -> the sole capability-based exclusion.
|
|
140
|
-
*
|
|
141
|
-
* This is the roster filter only. Catalog STAMPING is a separate concern owned by
|
|
142
|
-
* `applyMultiAgentMode`, including the `keepNativeChatGptOnV1` policy (#1728) that
|
|
143
|
-
* keeps ChatGPT-native rows on `v1` so a native parent can still spawn a routed child
|
|
144
|
-
* despite backend-encrypted NEW_TASK bodies (#92). Recognizing those `v1` rows as
|
|
145
|
-
* eligible leaves here is what makes that policy usable, not a contradiction of it.
|
|
146
|
-
*
|
|
147
|
-
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 (C1), superseding the
|
|
148
|
-
* option-B decision in 260730_codex_rs_upstream_v2_live_handoff/060.
|
|
149
|
-
*/
|
|
150
|
-
export function isEligibleV2SubagentEntry(entry: RawEntry): boolean {
|
|
151
|
-
return entry.multi_agent_version !== "disabled";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface EffectiveSubagentModel {
|
|
155
|
-
model: string;
|
|
156
|
-
efforts: string[];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface SubagentRosterExclusion {
|
|
160
|
-
configured: string;
|
|
161
|
-
reason: SubagentRosterExclusionReason;
|
|
162
|
-
catalogModel?: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface EffectiveSubagentRoster {
|
|
166
|
-
/** OpenCodex's natural-priority guidance projection, not captured native tool text. */
|
|
167
|
-
candidates: EffectiveSubagentModel[];
|
|
168
|
-
/** Configured models within that projection; exact-name eligibility is a separate check. */
|
|
169
|
-
advertised: EffectiveSubagentModel[];
|
|
170
|
-
excluded: SubagentRosterExclusion[];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function configuredCatalogEntry(entries: readonly RawEntry[], configured: string): RawEntry | undefined {
|
|
174
|
-
return entries.find(entry => entry.slug === configured)
|
|
175
|
-
?? entries.find(entry => typeof entry.slug === "string" && slugsEquivalent(configured, entry.slug));
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function configuredSubagentModelMatchesEntry(configured: string, entry: RawEntry): boolean {
|
|
179
|
-
if (typeof entry.slug !== "string") return false;
|
|
180
|
-
if (slugsEquivalent(configured, entry.slug)) return true;
|
|
181
|
-
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
182
|
-
return !configured.includes("/")
|
|
183
|
-
&& nativeSlug !== undefined
|
|
184
|
-
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)
|
|
185
|
-
&& slugsEquivalent(configured, nativeSlug);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function effectiveSubagentRoster(
|
|
189
|
-
configuredModels: readonly string[],
|
|
190
|
-
surface: SpawnAgentSurface,
|
|
191
|
-
catalogEntries?: readonly RawEntry[],
|
|
192
|
-
): EffectiveSubagentRoster {
|
|
193
|
-
const configured = configuredModels
|
|
194
|
-
.filter(model => model.trim().length > 0)
|
|
195
|
-
.filter((model, index, all) =>
|
|
196
|
-
!all.slice(0, index).some(previous => slugsEquivalent(previous, model))
|
|
197
|
-
);
|
|
198
|
-
const entries = catalogEntries ?? readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
199
|
-
const ordered = entries
|
|
200
|
-
.map((entry, index) => ({ entry, index }))
|
|
201
|
-
.filter(({ entry }) => typeof entry.slug === "string")
|
|
202
|
-
.filter(({ entry }) => entry.visibility === "list")
|
|
203
|
-
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
204
|
-
.sort((left, right) => {
|
|
205
|
-
// OpenCodex guidance candidates rank by natural priority (SPAWN_PRIORITY_FIELD when present),
|
|
206
|
-
// so modelPickerOrder does not change this projection. Native tool advertisements differ. Rows the
|
|
207
|
-
// override did not move fall back to their Codex-visible `priority`.
|
|
208
|
-
const spawnPriorityOf = (entry: RawEntry): number => {
|
|
209
|
-
const spawn = entry[SPAWN_PRIORITY_FIELD];
|
|
210
|
-
if (typeof spawn === "number" && Number.isFinite(spawn)) return spawn;
|
|
211
|
-
return typeof entry.priority === "number" && Number.isFinite(entry.priority)
|
|
212
|
-
? entry.priority : Number.MAX_SAFE_INTEGER;
|
|
213
|
-
};
|
|
214
|
-
const leftPriority = spawnPriorityOf(left.entry);
|
|
215
|
-
const rightPriority = spawnPriorityOf(right.entry);
|
|
216
|
-
return leftPriority - rightPriority || left.index - right.index;
|
|
217
|
-
})
|
|
218
|
-
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
219
|
-
const orderedEntries = new Set(ordered.map(({ entry }) => entry));
|
|
220
|
-
|
|
221
|
-
const candidates = ordered.map(({ entry }) => ({
|
|
222
|
-
model: entry.slug as string,
|
|
223
|
-
efforts: catalogEntryEfforts(entry),
|
|
224
|
-
}));
|
|
225
|
-
const advertised = ordered
|
|
226
|
-
.filter(({ entry }) => configured.some(model => configuredSubagentModelMatchesEntry(model, entry)))
|
|
227
|
-
.map(({ entry }) => ({
|
|
228
|
-
model: entry.slug as string,
|
|
229
|
-
efforts: catalogEntryEfforts(entry),
|
|
230
|
-
}));
|
|
231
|
-
const excluded = configured.flatMap((model): SubagentRosterExclusion[] => {
|
|
232
|
-
const matchingEntries = entries.filter(entry => configuredSubagentModelMatchesEntry(model, entry));
|
|
233
|
-
if (matchingEntries.some(entry => orderedEntries.has(entry))) return [];
|
|
234
|
-
if (matchingEntries.length === 0) return [{ configured: model, reason: "missing_catalog_entry" }];
|
|
235
|
-
const visibleCompatible = matchingEntries.find(entry =>
|
|
236
|
-
entry.visibility === "list"
|
|
237
|
-
&& (surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
238
|
-
);
|
|
239
|
-
if (visibleCompatible) {
|
|
240
|
-
return [{
|
|
241
|
-
configured: model,
|
|
242
|
-
catalogModel: visibleCompatible.slug as string,
|
|
243
|
-
reason: "outside_display_limit",
|
|
244
|
-
}];
|
|
245
|
-
}
|
|
246
|
-
const visible = matchingEntries.find(entry => entry.visibility === "list");
|
|
247
|
-
if (visible) {
|
|
248
|
-
return [{
|
|
249
|
-
configured: model,
|
|
250
|
-
catalogModel: visible.slug as string,
|
|
251
|
-
reason: "surface_incompatible",
|
|
252
|
-
}];
|
|
253
|
-
}
|
|
254
|
-
const hidden = configuredCatalogEntry(entries, model) ?? matchingEntries[0]!;
|
|
255
|
-
return [{ configured: model, catalogModel: hidden.slug as string, reason: "picker_hidden" }];
|
|
256
|
-
});
|
|
257
|
-
return { candidates, advertised, excluded };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
|
|
261
|
-
if (priority !== 9) clone.priority = priority;
|
|
262
|
-
applyNativeOpenAiContextOverride(clone, contextCap);
|
|
263
|
-
// GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
|
|
264
|
-
// Older natives (gpt-5.5) get mock max + ultra
|
|
265
|
-
// (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
|
|
266
|
-
if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
|
|
267
|
-
return ensureStrictCatalogFields(normalizeServiceTiers(clone));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export function isExactComboCatalogModel(
|
|
271
|
-
model: CatalogModel | undefined,
|
|
272
|
-
exactComboSlugs: ReadonlySet<string>,
|
|
273
|
-
): boolean {
|
|
274
|
-
return model?.provider === COMBO_NAMESPACE && exactComboSlugs.has(catalogModelSlug(model));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function isExactComboCatalogEntry(
|
|
278
|
-
entry: RawEntry,
|
|
279
|
-
exactComboSlugs: ReadonlySet<string>,
|
|
280
|
-
): boolean {
|
|
281
|
-
return entry.owned_by === COMBO_NAMESPACE
|
|
282
|
-
&& typeof entry.slug === "string"
|
|
283
|
-
&& exactComboSlugs.has(entry.slug);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
|
|
288
|
-
* ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
|
|
289
|
-
* lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
|
|
290
|
-
* The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
|
|
291
|
-
* that is dropped for display. Google Antigravity is relabeled to the compact `agy/` prefix for
|
|
292
|
-
* the same reason: `google-antigravity/` alone consumes most of the picker row. That prefix comes
|
|
293
|
-
* from the row's own `providerAlias`, decided once per gather flight; `null` means a cross-provider
|
|
294
|
-
* collision suppressed it and the canonical slug stands. This is the raw-slug path only -- a
|
|
295
|
-
* configured `modelAliases` entry is labeled by the effective-alias path in
|
|
296
|
-
* catalog/provider-fetch.ts (#2960) and keeps the canonical provider name. All other providers
|
|
297
|
-
* keep the raw slug exactly as before.
|
|
298
|
-
*/
|
|
299
|
-
function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<OcxConfig, "providers">): string {
|
|
300
|
-
const slash = slug.indexOf("/");
|
|
301
|
-
if (slash <= 0) return slug;
|
|
302
|
-
const provider = slug.slice(0, slash);
|
|
303
|
-
let modelId = slug.slice(slash + 1);
|
|
304
|
-
if (provider === "google-antigravity") {
|
|
305
|
-
if (model?.providerAlias === null) return slug;
|
|
306
|
-
const alias = (typeof model?.providerAlias === "string" && model.providerAlias.trim().length > 0)
|
|
307
|
-
? model.providerAlias.trim()
|
|
308
|
-
: effectiveProviderAlias(provider, undefined, config);
|
|
309
|
-
return alias ? `${alias}/${modelId}` : slug;
|
|
310
|
-
}
|
|
311
|
-
if (provider === "command-code" || provider === "commandcode") {
|
|
312
|
-
const m = modelId.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
|
|
313
|
-
if (m && modelId.startsWith(`${m[1]}-${m[1]}-`)) modelId = modelId.slice(m[1]!.length + 1);
|
|
314
|
-
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${modelId}`;
|
|
315
|
-
}
|
|
316
|
-
return slug;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function preservePinnedNativeCustomReasoning(model?: CatalogModel): boolean {
|
|
320
|
-
return model !== undefined
|
|
321
|
-
&& model.catalogKind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
322
|
-
&& hasNativeOpenAiCapabilityMetadata(model.id)
|
|
323
|
-
&& Array.isArray(model.reasoningEfforts);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Cria uma entrada nativa ou roteada a partir do snapshot upstream, de um clone
|
|
328
|
-
* do template ou de campos mínimos. Aplica os metadados e limites pertinentes
|
|
329
|
-
* sem alterar o template nem herdar sua marca de nome ou histórico de prioridade.
|
|
330
|
-
*/
|
|
331
|
-
export function deriveEntry(
|
|
332
|
-
template: RawEntry | null,
|
|
333
|
-
slug: string,
|
|
334
|
-
desc: string,
|
|
335
|
-
priority: number,
|
|
336
|
-
model?: CatalogModel,
|
|
337
|
-
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
338
|
-
contextCap?: NativeContextLimitsInput,
|
|
339
|
-
): RawEntry {
|
|
340
|
-
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
341
|
-
// Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
|
|
342
|
-
const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
|
|
343
|
-
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
|
|
344
|
-
? upstreamNativeEntry(model.id)
|
|
345
|
-
: null;
|
|
346
|
-
const isRouted = model !== undefined;
|
|
347
|
-
if (!isRouted && !slug.includes("/")) {
|
|
348
|
-
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
|
|
349
|
-
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
|
|
350
|
-
// instead of cloning an older template.
|
|
351
|
-
const upstream = upstreamNativeEntry(slug);
|
|
352
|
-
if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
|
|
353
|
-
}
|
|
354
|
-
if (template || codexForwardNativeCapabilityAlias) {
|
|
355
|
-
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
|
|
356
|
-
delete e.opencodex_native_display_name;
|
|
357
|
-
// A cached template may carry display-order history; each new row owns its natural rank.
|
|
358
|
-
delete e[SPAWN_PRIORITY_FIELD];
|
|
359
|
-
e.slug = slug;
|
|
360
|
-
e.display_name = routedDisplayName(slug, model);
|
|
361
|
-
e.description = desc;
|
|
362
|
-
e.priority = priority;
|
|
363
|
-
e.visibility = "list";
|
|
364
|
-
if ("upgrade" in e) e.upgrade = null;
|
|
365
|
-
delete e.availability_nux; // don't replay another model's "now available" NUX
|
|
366
|
-
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
|
|
367
|
-
// and advertise the reasoning ladder Codex accepts.
|
|
368
|
-
if (isRouted) {
|
|
369
|
-
// A routed model is NOT the native template: never inherit its context
|
|
370
|
-
// window when /models omits context metadata (#992). Known metadata
|
|
371
|
-
// restores exact values below; an enabled Context cap fills the gap;
|
|
372
|
-
// otherwise the strict-fields fallback supplies the 128k triple.
|
|
373
|
-
if (!codexForwardNativeCapabilityAlias) {
|
|
374
|
-
delete e.context_window;
|
|
375
|
-
delete e.max_context_window;
|
|
376
|
-
delete e.auto_compact_token_limit;
|
|
377
|
-
}
|
|
378
|
-
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
379
|
-
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
380
|
-
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
381
|
-
if (typeof e.base_instructions === "string") {
|
|
382
|
-
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
|
|
383
|
-
// (leaking that into base_instructions is a non-first-party signature → ToS risk).
|
|
384
|
-
e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
|
|
385
|
-
}
|
|
386
|
-
applyReasoningLevels(
|
|
387
|
-
e,
|
|
388
|
-
model?.reasoningEfforts,
|
|
389
|
-
model?.defaultReasoningEffort,
|
|
390
|
-
preserveExactReasoning
|
|
391
|
-
|| codexForwardNativeCapabilityAlias !== null
|
|
392
|
-
|| preservePinnedNativeCustomReasoning(model),
|
|
393
|
-
);
|
|
394
|
-
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
395
|
-
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
396
|
-
if (!codexForwardNativeCapabilityAlias) {
|
|
397
|
-
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true, model?.codexToolMode);
|
|
398
|
-
} else if (model?.codexToolMode !== undefined) {
|
|
399
|
-
applyRoutedCodexToolMode(e, model.codexToolMode);
|
|
400
|
-
}
|
|
401
|
-
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
402
|
-
applyCatalogModelMetadata(e, model);
|
|
403
|
-
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
|
|
404
|
-
// Additive only. `visibility` is untouched: an inactive row must still be OFFERED, which is
|
|
405
|
-
// the whole point of #1711 — operator disable is what removes rows, and it stays a separate
|
|
406
|
-
// path from this one.
|
|
407
|
-
if (model?.quotaInactiveReason) e[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
408
|
-
} else {
|
|
409
|
-
applyNativeOpenAiContextOverride(e, contextCap);
|
|
410
|
-
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
411
|
-
else ensureUltraReasoningLevel(e);
|
|
412
|
-
// Older natives do not support Responses Lite. A newer template must not enable
|
|
413
|
-
// reasoning.context or WebSockets on those models.
|
|
414
|
-
if (!isGpt56NativeSlug(slug)) {
|
|
415
|
-
delete e.use_responses_lite;
|
|
416
|
-
delete e.supports_websockets;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
|
|
420
|
-
preserveExactInputModalities: preserveExact,
|
|
421
|
-
isRouted,
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
425
|
-
// Routed fallbacks default to code-mode tool exposure (or shell mode when codexToolMode === "shell");
|
|
426
|
-
// otherwise the nested catalog expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
|
|
427
|
-
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
|
|
428
|
-
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
429
|
-
const entry: RawEntry = {
|
|
430
|
-
slug, display_name: routedDisplayName(slug, model), description: desc,
|
|
431
|
-
shell_type: "unified_exec", visibility: "list", supported_in_api: true,
|
|
432
|
-
priority, base_instructions: "You are a helpful coding assistant.",
|
|
433
|
-
...(isRouted
|
|
434
|
-
? isCursorFallback
|
|
435
|
-
? { supports_search_tool: true }
|
|
436
|
-
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
437
|
-
: {}),
|
|
438
|
-
};
|
|
439
|
-
if (isRouted) {
|
|
440
|
-
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
441
|
-
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning || preservePinnedNativeCustomReasoning(model));
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
445
|
-
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
|
|
446
|
-
}
|
|
447
|
-
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
448
|
-
applyCatalogModelMetadata(entry, model);
|
|
449
|
-
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
|
|
450
|
-
// Same additive stamp as the templated path above. A routed row that reaches the no-template
|
|
451
|
-
// fallback is still a served row, so omitting it here would make the field depend on whether a
|
|
452
|
-
// template happened to be cached — which is exactly what the regression test caught.
|
|
453
|
-
if (model?.quotaInactiveReason) entry[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
454
|
-
if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
|
|
455
|
-
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
456
|
-
preserveExactInputModalities: preserveExact,
|
|
457
|
-
isRouted,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export interface ObservedCatalogEntryBuildInput {
|
|
462
|
-
readonly template: RawEntry | null;
|
|
463
|
-
readonly gptSlugs: readonly string[];
|
|
464
|
-
readonly goModels: readonly CatalogModel[];
|
|
465
|
-
readonly featured?: readonly string[];
|
|
466
|
-
/** Optional full picker ordering (config.modelPickerOrder); orders non-featured rows. */
|
|
467
|
-
readonly modelPickerOrder?: readonly string[];
|
|
468
|
-
readonly wsEnabled: boolean;
|
|
469
|
-
readonly multiAgentMode: MultiAgentMode;
|
|
470
|
-
readonly exactComboSlugs: ReadonlySet<string>;
|
|
471
|
-
readonly accountSelectors: readonly string[];
|
|
472
|
-
readonly suppressedBareNativeSlugs: ReadonlySet<string>;
|
|
473
|
-
readonly disabledNativeAccountSlugs: ReadonlySet<string>;
|
|
474
|
-
readonly multiAgentV2Enabled: boolean;
|
|
475
|
-
readonly keepNativeChatGptOnV1?: boolean;
|
|
476
|
-
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
477
|
-
/** Additional native ids to clone under account selectors, without creating bare rows. */
|
|
478
|
-
readonly accountNativeSlugs?: readonly string[];
|
|
479
|
-
/** Per-selector account ids; unknown observations must not be copied to unrelated accounts. */
|
|
480
|
-
readonly accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>;
|
|
481
|
-
/** Codex-only manual selector metadata; deliberately independent of live permission. */
|
|
482
|
-
readonly reserve?: ReserveCatalogProjection;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/** Build entries with the process-observed Codex feature state. */
|
|
486
|
-
export function buildCatalogEntries(
|
|
487
|
-
template: RawEntry | null,
|
|
488
|
-
gptSlugs: string[],
|
|
489
|
-
goModels: CatalogModel[],
|
|
490
|
-
featured?: string[],
|
|
491
|
-
wsEnabled = false,
|
|
492
|
-
multiAgentMode: MultiAgentMode = "default",
|
|
493
|
-
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
494
|
-
accountSelectors: readonly string[] = [],
|
|
495
|
-
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
|
|
496
|
-
disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
|
|
497
|
-
contextCap?: NativeContextLimitsInput,
|
|
498
|
-
accountNativeSlugs?: readonly string[],
|
|
499
|
-
accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
|
|
500
|
-
keepNativeChatGptOnV1 = false,
|
|
501
|
-
modelPickerOrder: readonly string[] = [],
|
|
502
|
-
): RawEntry[] {
|
|
503
|
-
const entries = buildCatalogEntriesFromObservedState({
|
|
504
|
-
template,
|
|
505
|
-
gptSlugs,
|
|
506
|
-
goModels,
|
|
507
|
-
featured,
|
|
508
|
-
modelPickerOrder,
|
|
509
|
-
wsEnabled,
|
|
510
|
-
multiAgentMode,
|
|
511
|
-
exactComboSlugs,
|
|
512
|
-
accountSelectors,
|
|
513
|
-
suppressedBareNativeSlugs,
|
|
514
|
-
disabledNativeAccountSlugs,
|
|
515
|
-
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
516
|
-
keepNativeChatGptOnV1,
|
|
517
|
-
openaiContextCap: contextCap,
|
|
518
|
-
accountNativeSlugs,
|
|
519
|
-
accountNativeSlugsBySelector,
|
|
520
|
-
});
|
|
521
|
-
applyFullModelPickerOrder(entries, modelPickerOrder);
|
|
522
|
-
return entries;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** Build entries solely from caller-observed inputs, with no feature-state filesystem read. */
|
|
526
|
-
export function buildCatalogEntriesFromObservedState({
|
|
527
|
-
template,
|
|
528
|
-
gptSlugs,
|
|
529
|
-
goModels,
|
|
530
|
-
featured,
|
|
531
|
-
modelPickerOrder,
|
|
532
|
-
wsEnabled,
|
|
533
|
-
multiAgentMode,
|
|
534
|
-
exactComboSlugs,
|
|
535
|
-
accountSelectors,
|
|
536
|
-
suppressedBareNativeSlugs,
|
|
537
|
-
disabledNativeAccountSlugs,
|
|
538
|
-
multiAgentV2Enabled,
|
|
539
|
-
keepNativeChatGptOnV1,
|
|
540
|
-
openaiContextCap,
|
|
541
|
-
accountNativeSlugs,
|
|
542
|
-
accountNativeSlugsBySelector,
|
|
543
|
-
reserve,
|
|
544
|
-
}: ObservedCatalogEntryBuildInput): RawEntry[] {
|
|
545
|
-
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
546
|
-
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
547
|
-
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
548
|
-
// it sorts to the front. This works for native gpt slugs AND routed slugs alike.
|
|
549
|
-
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
550
|
-
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
551
|
-
// Optional full picker order (#1649). Independent of the 5-slot spawn_agent cap: it only
|
|
552
|
-
// rewrites the Codex-visible display `priority` of listed non-featured routed rows so a >5
|
|
553
|
-
// catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
|
|
554
|
-
// modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
|
|
555
|
-
// before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
|
|
556
|
-
// this display reorder does not change OpenCodex's guidance candidate calculation.
|
|
557
|
-
const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
|
|
558
|
-
const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
|
|
559
|
-
const pickerOrderActive = pickerOrder.length > 0;
|
|
560
|
-
// The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
|
|
561
|
-
// same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
|
|
562
|
-
// band. OpenCodex guidance membership does not depend on this — see SPAWN_PRIORITY_FIELD.
|
|
563
|
-
/**
|
|
564
|
-
* Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
|
|
565
|
-
* slugs sort in declared order within the high picker-order display tier
|
|
566
|
-
* (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
|
|
567
|
-
* the row's natural priority in SPAWN_PRIORITY_FIELD for OpenCodex's unchanged guidance window.
|
|
568
|
-
* Returns undefined when the feature is off or the row is not listed, so those rows
|
|
569
|
-
* keep their original assignment (default 5 / account 1_000+) untouched.
|
|
570
|
-
*
|
|
571
|
-
* Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
|
|
572
|
-
* below). Native passthrough rows and account-qualified native rows keep their own priority
|
|
573
|
-
* logic and are intentionally not reordered in this legacy builder pass. The final merge can
|
|
574
|
-
* apply complete ordering when the configured list includes a bare id.
|
|
575
|
-
*/
|
|
576
|
-
const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
|
|
577
|
-
if (!pickerOrderActive) return undefined;
|
|
578
|
-
const hit = pickerOrderRank.get(slug) ?? (altSlug !== undefined ? pickerOrderRank.get(altSlug) : undefined);
|
|
579
|
-
if (hit === undefined) return undefined;
|
|
580
|
-
return PICKER_ORDER_PRIORITY_BASE + hit * priorityStride;
|
|
581
|
-
};
|
|
582
|
-
const out: RawEntry[] = [];
|
|
583
|
-
const nativeEntries: RawEntry[] = [];
|
|
584
|
-
const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
|
|
585
|
-
const emittedNativeAliases = new Set<CatalogModel>();
|
|
586
|
-
const emittedNativeAliasSlugs = new Set<string>();
|
|
587
|
-
const nativeAliasesBySlug = new Map<string, CatalogModel>();
|
|
588
|
-
for (const model of goModels) {
|
|
589
|
-
if (model.provider !== COMBO_NAMESPACE
|
|
590
|
-
|| model.nativeAlias !== true
|
|
591
|
-
|| typeof model.alias !== "string"
|
|
592
|
-
|| model.alias.includes("/")) continue;
|
|
593
|
-
if (nativeAliasesBySlug.has(model.alias)) {
|
|
594
|
-
collisionSkipped.add(model);
|
|
595
|
-
if (!slugAliasCollisionWarnings.has(model.alias)) {
|
|
596
|
-
slugAliasCollisionWarnings.add(model.alias);
|
|
597
|
-
console.warn(
|
|
598
|
-
`[opencodex] native combo alias collision on "${model.alias}": keeping the first configured combo and omitting later duplicates from the catalog.`,
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
nativeAliasesBySlug.set(model.alias, model);
|
|
604
|
-
}
|
|
605
|
-
const comboPublicSlugs = new Set(goModels
|
|
606
|
-
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
607
|
-
.map(catalogModelSlug));
|
|
608
|
-
for (const slug of gptSlugs) {
|
|
609
|
-
const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
|
|
610
|
-
if (rank.has(slug)) native.priority = rank.get(slug)!;
|
|
611
|
-
nativeEntries.push(native);
|
|
612
|
-
const nativeAlias = nativeAliasesBySlug.get(slug);
|
|
613
|
-
if (!nativeAlias || collisionSkipped.has(nativeAlias)) {
|
|
614
|
-
if (!suppressedBareNativeSlugs.has(slug)) out.push(native);
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
const routed = deriveEntry(
|
|
618
|
-
template,
|
|
619
|
-
slug,
|
|
620
|
-
`Routed via opencodex → ${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
|
|
621
|
-
5,
|
|
622
|
-
nativeAlias,
|
|
623
|
-
exactComboSlugs,
|
|
624
|
-
);
|
|
625
|
-
routed.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
626
|
-
const rankHit = rank.get(slug) ?? rank.get(`${nativeAlias.provider}/${nativeAlias.id}`);
|
|
627
|
-
if (rankHit !== undefined) routed.priority = rankHit * priorityStride;
|
|
628
|
-
else if (accountSelectors.length > 0) routed.priority = 1_000 + (typeof routed.priority === "number" ? routed.priority : 5);
|
|
629
|
-
out.push(routed);
|
|
630
|
-
emittedNativeAliases.add(nativeAlias);
|
|
631
|
-
emittedNativeAliasSlugs.add(slug);
|
|
632
|
-
}
|
|
633
|
-
const nativeEntriesBySlug = new Map(nativeEntries.map(entry => [String(entry.slug), entry] as const));
|
|
634
|
-
for (const [selectorIndex, selector] of accountSelectors.entries()) {
|
|
635
|
-
const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
|
|
636
|
-
?? accountNativeSlugs
|
|
637
|
-
?? gptSlugs;
|
|
638
|
-
const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
|
|
639
|
-
nativeEntriesBySlug.get(slug)
|
|
640
|
-
?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
|
|
641
|
-
));
|
|
642
|
-
if (reserve?.mainSelectors.includes(selector)) accountNativeEntries.push(reserve.source);
|
|
643
|
-
for (const [nativeIndex, native] of accountNativeEntries.entries()) {
|
|
644
|
-
const nativeSlug = String(native.slug);
|
|
645
|
-
if (disabledNativeAccountSlugs.has(nativeSlug)) continue;
|
|
646
|
-
const e = JSON.parse(JSON.stringify(native)) as RawEntry;
|
|
647
|
-
const catalogSlug = `${selector}/${nativeSlug}`;
|
|
648
|
-
if (nativeSlug === NATIVE_RESERVE_MODEL && disabledNativeAccountSlugs.has(catalogSlug)) continue;
|
|
649
|
-
e.slug = catalogSlug;
|
|
650
|
-
e.display_name = accountBoundNativeDisplayName(selector, native);
|
|
651
|
-
// Codex ignores this OpenCodex extension; preserve the native comp_hash unchanged.
|
|
652
|
-
e.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
653
|
-
const exactRank = rank.get(catalogSlug);
|
|
654
|
-
// A bare featured id belongs to the compatibility combo once shadowed. Exact
|
|
655
|
-
// account-qualified picks still rank normally, but the account clone must not
|
|
656
|
-
// inherit the bare alias rank and consume another top spawn_agent slot.
|
|
657
|
-
const inheritedRank = emittedNativeAliasSlugs.has(nativeSlug) ? undefined : rank.get(nativeSlug);
|
|
658
|
-
const featuredRank = exactRank ?? inheritedRank;
|
|
659
|
-
e.priority = featuredRank !== undefined
|
|
660
|
-
? featuredRank * priorityStride + selectorIndex
|
|
661
|
-
: ((featured?.length ?? 0) + nativeIndex) * accountSelectors.length + selectorIndex;
|
|
662
|
-
e.visibility = "list";
|
|
663
|
-
out.push(e);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
for (const m of goModels) {
|
|
667
|
-
if (collisionSkipped.has(m) || emittedNativeAliases.has(m)) continue;
|
|
668
|
-
const slug = catalogModelSlug(m);
|
|
669
|
-
if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
|
|
670
|
-
warnComboMasqueradeCollisionOnce(slug);
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
// Provider rows use the one-slash slug codec; combo aliases intentionally override that
|
|
674
|
-
// public slug and may be bare.
|
|
675
|
-
const e = deriveEntry(
|
|
676
|
-
template,
|
|
677
|
-
slug,
|
|
678
|
-
`Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
|
|
679
|
-
5,
|
|
680
|
-
m,
|
|
681
|
-
exactComboSlugs,
|
|
682
|
-
);
|
|
683
|
-
if (m.provider === COMBO_NAMESPACE && m.nativeAlias === true && !slug.includes("/")) {
|
|
684
|
-
e.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
685
|
-
}
|
|
686
|
-
// Featured picks may be stored raw (legacy) or encoded — honor both.
|
|
687
|
-
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
|
|
688
|
-
// Natural priority: what the row would get WITHOUT modelPickerOrder. This is the value the
|
|
689
|
-
// spawn_agent candidate window is derived from (see effectiveSubagentRoster), so it must never
|
|
690
|
-
// move when modelPickerOrder reorders the picker.
|
|
691
|
-
if (rankHit !== undefined) e.priority = rankHit * priorityStride;
|
|
692
|
-
else if (accountSelectors.length > 0) {
|
|
693
|
-
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
694
|
-
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
695
|
-
}
|
|
696
|
-
// The legacy routed-only builder pass keeps featured ranks and records natural priority
|
|
697
|
-
// before changing non-featured display priority. The final complete-order pass may move
|
|
698
|
-
// featured display rows too; OpenCodex guidance continues to use their natural ranks.
|
|
699
|
-
if (rankHit === undefined) {
|
|
700
|
-
const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
|
|
701
|
-
if (pickerPriority !== undefined) {
|
|
702
|
-
e[SPAWN_PRIORITY_FIELD] = typeof e.priority === "number" ? e.priority : 5;
|
|
703
|
-
e.priority = pickerPriority;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
out.push(e);
|
|
707
|
-
}
|
|
708
|
-
// Central capability override (phase 120.4): the advertised flag must match the implemented WS
|
|
709
|
-
// endpoint. Overrides both the routed strip (normalizeRoutedCatalogEntry) and any native template
|
|
710
|
-
// leak (deriveEntry clones the template as-is for native slugs).
|
|
711
|
-
for (const entry of out) {
|
|
712
|
-
if (wsEnabled) entry.supports_websockets = true;
|
|
713
|
-
else {
|
|
714
|
-
delete entry.supports_websockets;
|
|
715
|
-
// Snapshot-backed native entries carry prefer_websockets: never advertise a preference
|
|
716
|
-
// for an endpoint ocx has disabled.
|
|
717
|
-
delete entry.prefer_websockets;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
|
|
721
|
-
keepNativeChatGptOnV1,
|
|
722
|
-
preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
export function resetCatalogRuntimeStateForTests(): void {
|
|
727
|
-
resetBundledCatalogCacheForTests();
|
|
728
|
-
lastDropWarnSignature.clear();
|
|
729
|
-
openAiApiCollisionWarnings.clear();
|
|
730
|
-
comboCatalogWarningSignatures.clear();
|
|
731
|
-
slugAliasCollisionWarnings.clear();
|
|
732
|
-
comboMasqueradeCollisionWarnings.clear();
|
|
733
|
-
comboUnrestorableShadowWarnings.clear();
|
|
734
|
-
accountSelectorShadowCollisionWarnings.clear();
|
|
735
|
-
clearLastComboCatalogOmissions();
|
|
736
|
-
clearModelCache(undefined, "eviction");
|
|
737
|
-
clearGatherRoutedModelsInflight();
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
export function orderForSubagents(goModels: CatalogModel[], featured?: string[]): CatalogModel[] {
|
|
741
|
-
if (!featured || featured.length === 0) return goModels;
|
|
742
|
-
const rank = new Map(featured.map((id, i) => [id, i]));
|
|
743
|
-
// Featured picks may be stored raw (legacy) or encoded — match both forms.
|
|
744
|
-
const rankOf = (m: CatalogModel) =>
|
|
745
|
-
(m.alias ? rank.get(m.alias) : undefined)
|
|
746
|
-
?? rank.get(`${m.provider}/${m.id}`)
|
|
747
|
-
?? rank.get(routedSlug(m.provider, m.id))
|
|
748
|
-
?? Number.MAX_SAFE_INTEGER;
|
|
749
|
-
return [...goModels].sort((a, b) => {
|
|
750
|
-
return rankOf(a) - rankOf(b);
|
|
751
|
-
});
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/** Routed discovery projection; native groups and alias ownership belong to the caller. */
|
|
755
|
-
export function orderForModelPicker(
|
|
756
|
-
models: readonly CatalogModel[],
|
|
757
|
-
order: readonly string[] = [],
|
|
758
|
-
featured: readonly string[] = [],
|
|
759
|
-
): CatalogModel[] {
|
|
760
|
-
const pickerOrder = normalizeModelPickerOrder(order);
|
|
761
|
-
if (pickerOrder.length === 0) return [...models];
|
|
762
|
-
const pickerRank = modelPickerRank(pickerOrder);
|
|
763
|
-
const featuredRank = modelPickerRank(featured);
|
|
764
|
-
const complete = pickerOrder.some(slug => !slug.includes("/"));
|
|
765
|
-
const rank = (model: CatalogModel): number => {
|
|
766
|
-
const slug = catalogModelSlug(model);
|
|
767
|
-
const featuredIndex = featuredRank(slug) ?? featuredRank(`${model.provider}/${model.id}`);
|
|
768
|
-
const natural = featuredIndex ?? 5;
|
|
769
|
-
const index = pickerRank(slug) ?? pickerRank(`${model.provider}/${model.id}`);
|
|
770
|
-
if (complete) return index ?? pickerOrder.length + natural;
|
|
771
|
-
// Preserve the legacy featured/alias bands, including unlisted rows before listed rows.
|
|
772
|
-
if (featuredIndex !== undefined || model.nativeAlias === true) return natural;
|
|
773
|
-
return index === undefined ? natural : PICKER_ORDER_PRIORITY_BASE + index;
|
|
774
|
-
};
|
|
775
|
-
return [...models].sort((a, b) => rank(a) - rank(b));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* True when an existing catalog row was authored by OpenCodex routing (#855).
|
|
780
|
-
* Every generated routed row — current full-slug form, the June–July 2026
|
|
781
|
-
* provider-name form, and legacy combo aliases — carries the stable
|
|
782
|
-
* description prefix `Routed via opencodex → `; foreign rows from Cursor or
|
|
783
|
-
* user tooling do not. `owned_by` cannot serve as the signal (upstream
|
|
784
|
-
* ownership), and `comp_hash` defaults to "opencodex" for every normalized
|
|
785
|
-
* row.
|
|
786
|
-
*/
|
|
787
|
-
function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
788
|
-
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
789
|
-
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
790
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
791
|
-
return slug.includes("/") && desc.startsWith("Routed via opencodex → ");
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
795
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
796
|
-
return SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
797
|
-
&& !isNativeAliasCatalogEntry(entry)
|
|
798
|
-
&& entry.owned_by !== COMBO_NAMESPACE
|
|
799
|
-
? slug
|
|
800
|
-
: null;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/** Undo our display overlay before native metadata normalization and template reuse. */
|
|
804
|
-
function restoreNativeDisplayName(entry: RawEntry): RawEntry {
|
|
805
|
-
const saved = entry.opencodex_native_display_name;
|
|
806
|
-
delete entry.opencodex_native_display_name;
|
|
807
|
-
if (saved && typeof saved === "object" && !Array.isArray(saved)) {
|
|
808
|
-
const label = saved as Record<string, unknown>;
|
|
809
|
-
if (recoverableNativeSlug(entry) === label.slug
|
|
810
|
-
&& typeof label.original === "string" && entry.display_name === label.applied) {
|
|
811
|
-
entry.display_name = label.original;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return entry;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/** Append missing supported native rows from trusted catalog sources only. */
|
|
818
|
-
export function mergeCatalogModelsWithNativeRecovery(
|
|
819
|
-
primaryCatalogModels: readonly RawEntry[],
|
|
820
|
-
nativeRecoverySources: readonly (readonly RawEntry[])[],
|
|
821
|
-
): RawEntry[] {
|
|
822
|
-
const merged = [...primaryCatalogModels];
|
|
823
|
-
const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
|
|
824
|
-
const slug = recoverableNativeSlug(entry);
|
|
825
|
-
return slug === null ? [] : [slug];
|
|
826
|
-
}));
|
|
827
|
-
for (const source of nativeRecoverySources) {
|
|
828
|
-
for (const entry of source) {
|
|
829
|
-
const slug = recoverableNativeSlug(entry);
|
|
830
|
-
if (slug === null || recoveredNativeSlugs.has(slug)) continue;
|
|
831
|
-
merged.push(structuredClone(entry) as RawEntry);
|
|
832
|
-
recoveredNativeSlugs.add(slug);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return merged;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
export interface ObservedCatalogMergePolicy {
|
|
839
|
-
/** Required observed/fixed set; the core merge never consults ambient catalog state. */
|
|
840
|
-
readonly nativeBackfillSlugs: readonly string[];
|
|
841
|
-
/** Whether unsupported OpenAI-family bare rows survive the merge. */
|
|
842
|
-
readonly unsupportedNativeEntries: "preserve" | "drop";
|
|
843
|
-
/** Whether merge-policy collision/preservation warnings belong to this caller's flow. */
|
|
844
|
-
readonly warningPolicy: "emit" | "suppress";
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/** Content policy shared by every writer of the canonical Codex model catalog. */
|
|
848
|
-
export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
|
|
849
|
-
Pick<ObservedCatalogMergePolicy, "nativeBackfillSlugs" | "unsupportedNativeEntries">
|
|
850
|
-
> = Object.freeze({
|
|
851
|
-
nativeBackfillSlugs: Object.freeze([...NATIVE_OPENAI_MODELS]),
|
|
852
|
-
unsupportedNativeEntries: "drop",
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
function normalizeModelPickerOrder(order: unknown): string[] {
|
|
856
|
-
return Array.isArray(order)
|
|
857
|
-
? order.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
|
|
858
|
-
: [];
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
/** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */
|
|
862
|
-
function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined {
|
|
863
|
-
const exact = new Map(order.map((slug, index) => [slug, index]));
|
|
864
|
-
const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index]));
|
|
865
|
-
return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug));
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/** Complete display ordering retains natural ranks for OpenCodex's separate guidance projection. */
|
|
869
|
-
export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void {
|
|
870
|
-
const pickerOrder = normalizeModelPickerOrder(order);
|
|
871
|
-
if (!pickerOrder.some(slug => !slug.includes("/"))) return;
|
|
872
|
-
const rankOf = modelPickerRank(pickerOrder);
|
|
873
|
-
for (const entry of entries) {
|
|
874
|
-
const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
|
|
875
|
-
entry[SPAWN_PRIORITY_FIELD] = natural;
|
|
876
|
-
entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
export interface ObservedCatalogMergeInput {
|
|
881
|
-
readonly catalogModels: readonly RawEntry[];
|
|
882
|
-
readonly baselineCatalogModels: readonly RawEntry[];
|
|
883
|
-
readonly routedEntries: readonly RawEntry[];
|
|
884
|
-
readonly baseline: ReadonlyMap<string, number>;
|
|
885
|
-
readonly featured: readonly string[];
|
|
886
|
-
readonly modelPickerOrder?: readonly string[];
|
|
887
|
-
readonly accountSelectors?: readonly string[];
|
|
888
|
-
readonly wsEnabled: boolean;
|
|
889
|
-
readonly template: RawEntry | null;
|
|
890
|
-
readonly disabledModels: ReadonlySet<string>;
|
|
891
|
-
readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
|
|
892
|
-
readonly gatheredProviderNames: ReadonlySet<string>;
|
|
893
|
-
readonly pendingProviderNames?: ReadonlySet<string>;
|
|
894
|
-
readonly degradedProviderNames: ReadonlySet<string>;
|
|
895
|
-
readonly legacyCustomModelSlugs: ReadonlySet<string>;
|
|
896
|
-
readonly multiAgentMode: MultiAgentMode;
|
|
897
|
-
readonly multiAgentV2Enabled: boolean;
|
|
898
|
-
readonly keepNativeChatGptOnV1?: boolean;
|
|
899
|
-
readonly exactComboSlugs: ReadonlySet<string>;
|
|
900
|
-
readonly hasPhysicalComboProvider: boolean;
|
|
901
|
-
readonly includeNativeOpenAi: boolean;
|
|
902
|
-
readonly accountBoundEntries: readonly RawEntry[];
|
|
903
|
-
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
904
|
-
readonly policy: ObservedCatalogMergePolicy;
|
|
905
|
-
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
906
|
-
/** Exact display-only labels for bare native OpenAI models. */
|
|
907
|
-
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Deterministically merge one fully observed catalog state.
|
|
912
|
-
*
|
|
913
|
-
* Every non-catalog input is explicit so evidence-bound convergence cannot
|
|
914
|
-
* accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
|
|
915
|
-
*/
|
|
916
|
-
export function mergeCatalogEntriesFromObservedState({
|
|
917
|
-
catalogModels,
|
|
918
|
-
baselineCatalogModels,
|
|
919
|
-
routedEntries,
|
|
920
|
-
baseline,
|
|
921
|
-
featured,
|
|
922
|
-
modelPickerOrder = [],
|
|
923
|
-
accountSelectors = [],
|
|
924
|
-
wsEnabled,
|
|
925
|
-
template,
|
|
926
|
-
disabledModels,
|
|
927
|
-
selectedModelsByProvider,
|
|
928
|
-
gatheredProviderNames,
|
|
929
|
-
pendingProviderNames = new Set(),
|
|
930
|
-
degradedProviderNames,
|
|
931
|
-
legacyCustomModelSlugs,
|
|
932
|
-
multiAgentMode,
|
|
933
|
-
multiAgentV2Enabled,
|
|
934
|
-
keepNativeChatGptOnV1,
|
|
935
|
-
exactComboSlugs,
|
|
936
|
-
hasPhysicalComboProvider,
|
|
937
|
-
includeNativeOpenAi,
|
|
938
|
-
accountBoundEntries,
|
|
939
|
-
suppressedBareNativeSlugs = new Set(),
|
|
940
|
-
policy,
|
|
941
|
-
openaiContextCap,
|
|
942
|
-
nativeDisplayNames,
|
|
943
|
-
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
944
|
-
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
945
|
-
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
946
|
-
const detachedCatalogModels = catalogModels
|
|
947
|
-
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
948
|
-
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
949
|
-
.map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
|
|
950
|
-
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
951
|
-
// Track this invocation's generated custom rows, not ownership markers read from disk.
|
|
952
|
-
// Their builder already finalized exact native ladders and ordinary routed mock tiers.
|
|
953
|
-
const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
|
|
954
|
-
entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND));
|
|
955
|
-
const detachedAccountBoundEntries = accountBoundEntries
|
|
956
|
-
.map(entry => structuredClone(entry) as RawEntry);
|
|
957
|
-
const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
|
|
958
|
-
const legacyCustomModelKeys = new Set(
|
|
959
|
-
[...legacyCustomModelSlugs].map(slugEquivalenceKey),
|
|
960
|
-
);
|
|
961
|
-
const selectedModelKeysByProvider = new Map([...selectedModelsByProvider].map(([provider, models]) => (
|
|
962
|
-
[provider, new Set([...models].map(model => slugEquivalenceKey(routedSlug(provider, model))))] as const
|
|
963
|
-
)));
|
|
964
|
-
const freshAccountKeys = new Set(detachedAccountBoundEntries.flatMap(entry => (
|
|
965
|
-
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
966
|
-
)));
|
|
967
|
-
const wouldSurviveUnreplaced = (entry: RawEntry): boolean => {
|
|
968
|
-
if (entry.owned_by === COMBO_NAMESPACE
|
|
969
|
-
|| trustedAccountBoundNativeCatalogSlug(entry) !== undefined
|
|
970
|
-
|| entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
971
|
-
|| isOcxAuthoredRoutedEntry(entry)
|
|
972
|
-
|| typeof entry.slug !== "string") return false;
|
|
973
|
-
const slug = entry.slug;
|
|
974
|
-
if (!slug.includes("/")) {
|
|
975
|
-
if (!includeNativeOpenAi || policy.nativeBackfillSlugs.includes(slug)) return false;
|
|
976
|
-
return policy.unsupportedNativeEntries === "preserve" || !isUnsupportedOpenAiNativeSlug(slug);
|
|
977
|
-
}
|
|
978
|
-
if (isRoutedModelCompatibilityExcluded(slug)) return false;
|
|
979
|
-
if (!hasPhysicalComboProvider && slug.startsWith(`${COMBO_NAMESPACE}/`)) return false;
|
|
980
|
-
const key = slugEquivalenceKey(slug);
|
|
981
|
-
if (freshAccountKeys.has(key)) return false;
|
|
982
|
-
if (disabledModelKeys.has(key)) return false;
|
|
983
|
-
const slash = slug.indexOf("/");
|
|
984
|
-
const provider = slug.slice(0, slash);
|
|
985
|
-
if (pendingProviderNames.has(provider)) return false;
|
|
986
|
-
const selected = selectedModelKeysByProvider.get(provider);
|
|
987
|
-
if (selected !== undefined && !selected.has(key)) return false;
|
|
988
|
-
return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
|
|
989
|
-
};
|
|
990
|
-
const validRoutedEntries = detachedRoutedEntries.filter(entry => {
|
|
991
|
-
return !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
992
|
-
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
993
|
-
});
|
|
994
|
-
const restorableCatalogKeys = new Set(detachedBaselineCatalogModels.flatMap(entry => (
|
|
995
|
-
wouldSurviveUnreplaced(entry) && typeof entry.slug === "string"
|
|
996
|
-
? [slugEquivalenceKey(entry.slug)]
|
|
997
|
-
: []
|
|
998
|
-
)));
|
|
999
|
-
const unrestorableCatalogKeys = new Set(detachedCatalogModels.flatMap(entry => {
|
|
1000
|
-
if (!wouldSurviveUnreplaced(entry) || typeof entry.slug !== "string") return [];
|
|
1001
|
-
const key = slugEquivalenceKey(entry.slug);
|
|
1002
|
-
return restorableCatalogKeys.has(key) ? [] : [key];
|
|
1003
|
-
}));
|
|
1004
|
-
const admittedRoutedEntries = validRoutedEntries.filter(entry => {
|
|
1005
|
-
if (!isExactComboCatalogEntry(entry, exactComboSlugs)) return true;
|
|
1006
|
-
const slug = entry.slug as string;
|
|
1007
|
-
const key = slugEquivalenceKey(slug);
|
|
1008
|
-
if (!unrestorableCatalogKeys.has(key)) return true;
|
|
1009
|
-
if (policy.warningPolicy === "emit") warnComboUnrestorableShadowOnce(slug);
|
|
1010
|
-
return false;
|
|
1011
|
-
});
|
|
1012
|
-
// A fresh non-custom row authoritatively resolves a historically ambiguous slug as a normal
|
|
1013
|
-
// provider model. Persist that classification so the durable deletion evidence cannot remove
|
|
1014
|
-
// the legitimate row during a later degraded refresh.
|
|
1015
|
-
for (const entry of admittedRoutedEntries) {
|
|
1016
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1017
|
-
if (!slug
|
|
1018
|
-
|| entry.opencodex_catalog_kind !== undefined
|
|
1019
|
-
|| entry.owned_by === COMBO_NAMESPACE
|
|
1020
|
-
|| !isOcxAuthoredRoutedEntry(entry)
|
|
1021
|
-
|| !legacyCustomModelKeys.has(slugEquivalenceKey(slug))) continue;
|
|
1022
|
-
entry.opencodex_catalog_kind = CODEX_PROVIDER_MODEL_CATALOG_KIND;
|
|
1023
|
-
}
|
|
1024
|
-
const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
|
|
1025
|
-
isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
1026
|
-
&& typeof entry.description === "string"
|
|
1027
|
-
&& entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
|
|
1028
|
-
)));
|
|
1029
|
-
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
1030
|
-
const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
1031
|
-
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
1032
|
-
)));
|
|
1033
|
-
const freshEquivalent = (slug: string): boolean => (
|
|
1034
|
-
freshEquivalentKeys.has(slugEquivalenceKey(slug))
|
|
1035
|
-
);
|
|
1036
|
-
const freshBareComboAliases = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
1037
|
-
typeof entry.slug === "string"
|
|
1038
|
-
&& !entry.slug.includes("/")
|
|
1039
|
-
&& entry.owned_by === COMBO_NAMESPACE
|
|
1040
|
-
? [entry.slug]
|
|
1041
|
-
: []
|
|
1042
|
-
)));
|
|
1043
|
-
const staleComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
1044
|
-
typeof entry.slug === "string"
|
|
1045
|
-
&& entry.owned_by === COMBO_NAMESPACE
|
|
1046
|
-
&& !freshEquivalent(entry.slug)
|
|
1047
|
-
? [slugEquivalenceKey(entry.slug)]
|
|
1048
|
-
: []
|
|
1049
|
-
)));
|
|
1050
|
-
const currentNonComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
1051
|
-
entry.owned_by !== COMBO_NAMESPACE && typeof entry.slug === "string"
|
|
1052
|
-
? [slugEquivalenceKey(entry.slug)]
|
|
1053
|
-
: []
|
|
1054
|
-
)));
|
|
1055
|
-
const restoredComboShadows = detachedBaselineCatalogModels.filter(entry => {
|
|
1056
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1057
|
-
if (!slug || entry.owned_by === COMBO_NAMESPACE) return false;
|
|
1058
|
-
const key = slugEquivalenceKey(slug);
|
|
1059
|
-
return staleComboKeys.has(key) && !currentNonComboKeys.has(key);
|
|
1060
|
-
});
|
|
1061
|
-
const catalogModelsForMerge = [...detachedCatalogModels, ...restoredComboShadows];
|
|
1062
|
-
const nativePriority = (slug: string, fallback: unknown): number => {
|
|
1063
|
-
const base = baseline.get(slug)
|
|
1064
|
-
?? (typeof fallback === "number" ? fallback : 9);
|
|
1065
|
-
if (rank.has(slug)) return rank.get(slug)!;
|
|
1066
|
-
return featured.length > 0 ? Math.max(base, featured.length + 100) : base;
|
|
1067
|
-
};
|
|
1068
|
-
const nativeSourceEntries = includeNativeOpenAi
|
|
1069
|
-
? catalogModelsForMerge
|
|
1070
|
-
.filter(m => typeof m.slug === "string"
|
|
1071
|
-
&& !(m.slug as string).includes("/")
|
|
1072
|
-
&& m.owned_by !== COMBO_NAMESPACE
|
|
1073
|
-
&& (policy.unsupportedNativeEntries === "preserve"
|
|
1074
|
-
|| policy.nativeBackfillSlugs.includes(m.slug as string)
|
|
1075
|
-
|| !isUnsupportedOpenAiNativeSlug(m.slug as string)))
|
|
1076
|
-
.map(m => {
|
|
1077
|
-
const slug = m.slug as string;
|
|
1078
|
-
// Fallback-quality entries (ocx synthesis / codex-rs model_info fallback: display_name
|
|
1079
|
-
// stamped with the bare slug) are upgraded to the pinned upstream snapshot entry so a
|
|
1080
|
-
// previously synthesized ladder (e.g. luna advertising ultra) self-heals on sync. A
|
|
1081
|
-
// genuine catalog entry (real display name) is preserved untouched.
|
|
1082
|
-
if (shouldUpgradeToUpstreamEntry(m)) {
|
|
1083
|
-
const upstream = upstreamNativeEntry(slug)!;
|
|
1084
|
-
const finished = finishUpstreamNativeEntry(upstream, 9, openaiContextCap);
|
|
1085
|
-
finished.priority = nativePriority(slug, upstream.priority);
|
|
1086
|
-
return finished;
|
|
1087
|
-
}
|
|
1088
|
-
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) });
|
|
1089
|
-
// Recompute spawn rank from current featured models, not a prior picker override.
|
|
1090
|
-
delete preserved[SPAWN_PRIORITY_FIELD];
|
|
1091
|
-
// Older natives kept from disk still need the mock top tiers (max + ultra always
|
|
1092
|
-
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
1093
|
-
if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
|
|
1094
|
-
return preserved;
|
|
1095
|
-
})
|
|
1096
|
-
: [];
|
|
1097
|
-
const native = nativeSourceEntries.filter(entry =>
|
|
1098
|
-
typeof entry.slug !== "string"
|
|
1099
|
-
|| (!freshBareComboAliases.has(entry.slug) && !suppressedBareNativeSlugs.has(entry.slug))
|
|
1100
|
-
);
|
|
1101
|
-
|
|
1102
|
-
// Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
|
|
1103
|
-
// routed provider exposing the same id can never delete the native OpenAI/Codex base row.
|
|
1104
|
-
// Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
|
|
1105
|
-
const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
1106
|
-
if (includeNativeOpenAi) {
|
|
1107
|
-
for (const slug of policy.nativeBackfillSlugs) {
|
|
1108
|
-
if (nativeSlugs.has(slug) || freshBareComboAliases.has(slug) || suppressedBareNativeSlugs.has(slug)) continue;
|
|
1109
|
-
nativeSlugs.add(slug);
|
|
1110
|
-
const entry = deriveEntry(
|
|
1111
|
-
template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1112
|
-
slug,
|
|
1113
|
-
"OpenAI native model (Codex OAuth passthrough).",
|
|
1114
|
-
nativePriority(slug, upstreamNativeEntry(slug)?.priority),
|
|
1115
|
-
undefined,
|
|
1116
|
-
new Set(),
|
|
1117
|
-
openaiContextCap,
|
|
1118
|
-
);
|
|
1119
|
-
entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority);
|
|
1120
|
-
native.push(entry);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
const nativeSourceBySlug = new Map([...nativeSourceEntries, ...native].flatMap(entry =>
|
|
1125
|
-
typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
|
|
1126
|
-
));
|
|
1127
|
-
const alignedAccountBoundEntries = detachedAccountBoundEntries.map(entry => {
|
|
1128
|
-
// The explicit Reserve source is already chosen (actual row or documented Luna adaptation).
|
|
1129
|
-
// A generic native merge must not replace its provenance or capability ladder.
|
|
1130
|
-
if (isReserveCatalogProjection(entry)) return entry;
|
|
1131
|
-
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
1132
|
-
const source = nativeSlug === undefined ? undefined : nativeSourceBySlug.get(nativeSlug);
|
|
1133
|
-
if (!source) return entry;
|
|
1134
|
-
const aligned = JSON.parse(JSON.stringify(source)) as RawEntry;
|
|
1135
|
-
aligned.slug = entry.slug;
|
|
1136
|
-
aligned.display_name = entry.display_name;
|
|
1137
|
-
aligned.priority = entry.priority;
|
|
1138
|
-
aligned.visibility = "list";
|
|
1139
|
-
aligned.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
1140
|
-
return aligned;
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
const freshSlugs = new Set(
|
|
1144
|
-
admittedRoutedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
1145
|
-
);
|
|
1146
|
-
const existingRoutedEntries = catalogModelsForMerge.filter(m =>
|
|
1147
|
-
typeof m.slug === "string"
|
|
1148
|
-
&& (m.slug.includes("/") || isNativeAliasCatalogEntry(m))
|
|
1149
|
-
&& trustedAccountBoundNativeCatalogSlug(m) === undefined
|
|
1150
|
-
);
|
|
1151
|
-
const preservedRoutedEntries = existingRoutedEntries.filter(entry => {
|
|
1152
|
-
const slug = entry.slug as string;
|
|
1153
|
-
if (freshEquivalent(slug)) return false;
|
|
1154
|
-
if (isNativeAliasCatalogEntry(entry)) return exactComboSlugs.has(slug);
|
|
1155
|
-
// Current custom rows are always regenerated from config, even while provider discovery is
|
|
1156
|
-
// degraded. A marked row absent from the fresh projection is therefore an intentional delete.
|
|
1157
|
-
if (entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND) return false;
|
|
1158
|
-
// Before custom rows had a marker, a config deletion could otherwise be mistaken for a
|
|
1159
|
-
// provider outage. Only explicit save-boundary evidence may classify an unmarked OpenCodex
|
|
1160
|
-
// row; foreign and future-marked rows fail closed and remain preserved.
|
|
1161
|
-
if (entry.opencodex_catalog_kind === undefined
|
|
1162
|
-
&& entry.owned_by !== COMBO_NAMESPACE
|
|
1163
|
-
&& isOcxAuthoredRoutedEntry(entry)
|
|
1164
|
-
&& legacyCustomModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
1165
|
-
const provider = slug.slice(0, slug.indexOf("/"));
|
|
1166
|
-
if (gatheredProviderNames.has(provider)) {
|
|
1167
|
-
// A provider-local degraded observation preserves only that namespace. Authoritative empty
|
|
1168
|
-
// catalogs and successful removals still delete stale rows even when another provider fails.
|
|
1169
|
-
return degradedProviderNames.has(provider);
|
|
1170
|
-
}
|
|
1171
|
-
// Deleted/disabled providers cannot retain OpenCodex-authored ghosts. Foreign catalog rows
|
|
1172
|
-
// remain outside provider ownership and survive unless a fresh row replaces their exact slug.
|
|
1173
|
-
return !isOcxAuthoredRoutedEntry(entry);
|
|
1174
|
-
});
|
|
1175
|
-
// Retained rows bypass the builder. Recompute managed spawn ranks from current config
|
|
1176
|
-
// before either display-order mode; a saved display override is not current roster authority.
|
|
1177
|
-
const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
|
|
1178
|
-
const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/"));
|
|
1179
|
-
const rankOf = modelPickerRank(pickerOrder);
|
|
1180
|
-
const featuredRankOf = modelPickerRank(featured);
|
|
1181
|
-
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
1182
|
-
for (const entry of preservedRoutedEntries) {
|
|
1183
|
-
const natural = entry[SPAWN_PRIORITY_FIELD];
|
|
1184
|
-
if (typeof natural === "number") {
|
|
1185
|
-
entry.priority = natural;
|
|
1186
|
-
delete entry[SPAWN_PRIORITY_FIELD];
|
|
1187
|
-
}
|
|
1188
|
-
const slug = String(entry.slug);
|
|
1189
|
-
if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
|
|
1190
|
-
const featuredRank = featuredRankOf(slug);
|
|
1191
|
-
entry.priority = featuredRank !== undefined
|
|
1192
|
-
? featuredRank * priorityStride
|
|
1193
|
-
: (accountSelectors.length > 0 ? 1_000 : 0) + 5;
|
|
1194
|
-
if (featuredRank !== undefined || fullPickerOrder) continue;
|
|
1195
|
-
const pickerIndex = rankOf(slug);
|
|
1196
|
-
if (pickerIndex !== undefined) {
|
|
1197
|
-
entry[SPAWN_PRIORITY_FIELD] = entry.priority;
|
|
1198
|
-
entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride;
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
|
|
1202
|
-
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1203
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1204
|
-
if (!slug.includes("/")) return true;
|
|
1205
|
-
if (disabledModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
1206
|
-
// Provider allowlists own provider rows, not a current combo's public alias. Exempt only an
|
|
1207
|
-
// identity from this gather's generated combo projection: provider discovery may supply a
|
|
1208
|
-
// spoofed `owned_by`, and persisted combo-shaped rows are not fresh authority.
|
|
1209
|
-
if (freshExactComboEntries.has(entry)) return true;
|
|
1210
|
-
const slash = slug.indexOf("/");
|
|
1211
|
-
const provider = slug.slice(0, slash);
|
|
1212
|
-
if (pendingProviderNames.has(provider)) return false;
|
|
1213
|
-
const selected = selectedModelKeysByProvider.get(provider);
|
|
1214
|
-
return selected === undefined || selected.has(slugEquivalenceKey(slug));
|
|
1215
|
-
});
|
|
1216
|
-
if (!hasPhysicalComboProvider) {
|
|
1217
|
-
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1218
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1219
|
-
const comboOwned = slug.startsWith(`${COMBO_NAMESPACE}/`) || entry.owned_by === COMBO_NAMESPACE;
|
|
1220
|
-
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
1221
|
-
return !comboOwned || freshSlugs.has(slug) || retainedNativeAlias;
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1225
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1226
|
-
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
1227
|
-
return retainedNativeAlias
|
|
1228
|
-
|| !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
1229
|
-
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
1230
|
-
});
|
|
1231
|
-
// Reapply final catalog policy to rows preserved from disk. Those rows bypass
|
|
1232
|
-
// gatherRoutedModels, so filtering only the freshly gathered list can resurrect an excluded id.
|
|
1233
|
-
finalRoutedEntries = finalRoutedEntries.filter(entry =>
|
|
1234
|
-
typeof entry.slug !== "string" || !isRoutedModelCompatibilityExcluded(entry.slug)
|
|
1235
|
-
);
|
|
1236
|
-
const accountBoundSlugs = new Set(alignedAccountBoundEntries.flatMap(entry =>
|
|
1237
|
-
typeof entry.slug === "string" ? [entry.slug] : []
|
|
1238
|
-
));
|
|
1239
|
-
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1240
|
-
if (typeof entry.slug !== "string" || !accountBoundSlugs.has(entry.slug)) return true;
|
|
1241
|
-
if (freshSlugs.has(entry.slug) && policy.warningPolicy === "emit") {
|
|
1242
|
-
warnAccountSelectorShadowedProviderOnce(entry.slug);
|
|
1243
|
-
}
|
|
1244
|
-
return false;
|
|
1245
|
-
});
|
|
1246
|
-
const finalRoutedEntrySet = new Set(finalRoutedEntries);
|
|
1247
|
-
const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
|
|
1248
|
-
if (!finalRoutedEntrySet.has(entry)) return false;
|
|
1249
|
-
const slug = entry.slug as string;
|
|
1250
|
-
const provider = slug.slice(0, slug.indexOf("/"));
|
|
1251
|
-
return gatheredProviderNames.has(provider) && degradedProviderNames.has(provider);
|
|
1252
|
-
}).length;
|
|
1253
|
-
if (degradedPreservedCount > 0 && policy.warningPolicy === "emit") {
|
|
1254
|
-
console.warn(`[opencodex] catalog sync: provider discovery degraded; preserving ${degradedPreservedCount} existing routed entr${degradedPreservedCount === 1 ? "y" : "ies"} on disk.`);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
|
|
1258
|
-
const observedNativeSlugs = new Set(alignedAccountBoundEntries.flatMap(entry => {
|
|
1259
|
-
const slug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
1260
|
-
return slug === undefined ? [] : [slug];
|
|
1261
|
-
}));
|
|
1262
|
-
for (const slug of policy.nativeBackfillSlugs) observedNativeSlugs.add(slug);
|
|
1263
|
-
const mergedEntries = [...native, ...managedEntries].map(m => {
|
|
1264
|
-
const reserveProjection = isReserveCatalogProjection(m);
|
|
1265
|
-
const normalized = reserveProjection ? m : normalizeServiceTiers(m);
|
|
1266
|
-
if (!reserveProjection && !isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
|
|
1267
|
-
const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
|
|
1268
|
-
const e = reserveProjection ? normalized : ensureStrictCatalogFields(normalized, {
|
|
1269
|
-
preserveExactInputModalities: exactCombo,
|
|
1270
|
-
isRouted: finalRoutedEntrySet.has(m),
|
|
1271
|
-
});
|
|
1272
|
-
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1273
|
-
// the max rung — ensure it here so subagent max spawns validate on every
|
|
1274
|
-
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
|
|
1275
|
-
if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
|
|
1276
|
-
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
1277
|
-
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
1278
|
-
: [];
|
|
1279
|
-
if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
|
|
1280
|
-
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
|
|
1281
|
-
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
|
|
1282
|
-
e.supported_reasoning_levels = levels;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
if (wsEnabled) e.supports_websockets = true;
|
|
1286
|
-
else {
|
|
1287
|
-
delete e.supports_websockets;
|
|
1288
|
-
// Match buildCatalogEntries: never advertise a websocket preference while WS is off.
|
|
1289
|
-
delete e.prefer_websockets;
|
|
1290
|
-
}
|
|
1291
|
-
return e;
|
|
1292
|
-
});
|
|
1293
|
-
// Native enable/disable runs as the LAST pass so the upstream-upgrade branch above can never
|
|
1294
|
-
// clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
|
|
1295
|
-
// only their generated account row.
|
|
1296
|
-
const versionedEntries = applyMultiAgentMode(
|
|
1297
|
-
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
1298
|
-
multiAgentMode,
|
|
1299
|
-
multiAgentV2Enabled,
|
|
1300
|
-
{ keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
|
|
1301
|
-
);
|
|
1302
|
-
applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
|
|
1303
|
-
for (const entry of versionedEntries) {
|
|
1304
|
-
// Templates and account clones must not inherit the native row's overlay marker.
|
|
1305
|
-
delete entry.opencodex_native_display_name;
|
|
1306
|
-
const slug = recoverableNativeSlug(entry);
|
|
1307
|
-
if (slug !== null) {
|
|
1308
|
-
const label = nativeDisplayNames && Object.hasOwn(nativeDisplayNames, slug)
|
|
1309
|
-
? nativeDisplayNames[slug]?.trim() : undefined;
|
|
1310
|
-
if (label && label !== entry.display_name) {
|
|
1311
|
-
entry.opencodex_native_display_name = { slug, original: entry.display_name, applied: label };
|
|
1312
|
-
entry.display_name = label;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
const kind = entry.opencodex_catalog_kind;
|
|
1316
|
-
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
1317
|
-
&& kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
1318
|
-
&& kind !== CODEX_PROVIDER_MODEL_CATALOG_KIND) continue;
|
|
1319
|
-
// Canonicalize extension-field order after every normalizer. This keeps an unchanged catalog
|
|
1320
|
-
// byte-idempotent whether an owned row was freshly built or retained from the prior pass.
|
|
1321
|
-
delete entry.opencodex_catalog_kind;
|
|
1322
|
-
entry.opencodex_catalog_kind = kind;
|
|
1323
|
-
}
|
|
1324
|
-
return versionedEntries;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/** Merge retained-sync rows using the process-observed Codex feature state. */
|
|
1328
|
-
export function mergeCatalogEntriesForSync(
|
|
1329
|
-
catalogModels: RawEntry[],
|
|
1330
|
-
routedEntries: RawEntry[],
|
|
1331
|
-
baseline: Map<string, number>,
|
|
1332
|
-
featured: string[],
|
|
1333
|
-
wsEnabled: boolean,
|
|
1334
|
-
_goIds: Set<string> = new Set(),
|
|
1335
|
-
template: RawEntry | null = null,
|
|
1336
|
-
disabledModels: ReadonlySet<string> = new Set(),
|
|
1337
|
-
gatheredProviderNames?: Set<string>,
|
|
1338
|
-
multiAgentMode: MultiAgentMode = "default",
|
|
1339
|
-
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
1340
|
-
hasPhysicalComboProvider = false,
|
|
1341
|
-
includeNativeOpenAi = true,
|
|
1342
|
-
accountBoundEntries: readonly RawEntry[] = [],
|
|
1343
|
-
legacyCustomModelSlugs: ReadonlySet<string> = new Set(),
|
|
1344
|
-
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(
|
|
1345
|
-
routedEntries.flatMap(entry => (
|
|
1346
|
-
isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
|
|
1347
|
-
)),
|
|
1348
|
-
),
|
|
1349
|
-
openaiContextCap?: NativeContextLimitsInput,
|
|
1350
|
-
keepNativeChatGptOnV1 = false,
|
|
1351
|
-
): RawEntry[] {
|
|
1352
|
-
// Retained for source compatibility with the original helper contract. Raw provider ids must
|
|
1353
|
-
// not suppress same-named native rows; actual admitted combo entries own that decision now.
|
|
1354
|
-
void _goIds;
|
|
1355
|
-
const effectiveGatheredProviderNames = gatheredProviderNames ?? new Set(
|
|
1356
|
-
routedEntries.flatMap(entry => {
|
|
1357
|
-
// A slashed combo alias is not evidence that its public prefix is an authoritative provider
|
|
1358
|
-
// namespace. Treating it as one would let the combo replace an unrestorable foreign row.
|
|
1359
|
-
if (isExactComboCatalogEntry(entry, exactComboSlugs)) return [];
|
|
1360
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1361
|
-
const slash = slug.indexOf("/");
|
|
1362
|
-
return slash > 0 ? [slug.slice(0, slash)] : [];
|
|
1363
|
-
}),
|
|
1364
|
-
);
|
|
1365
|
-
return mergeCatalogEntriesFromObservedState({
|
|
1366
|
-
catalogModels,
|
|
1367
|
-
baselineCatalogModels: [],
|
|
1368
|
-
routedEntries,
|
|
1369
|
-
baseline,
|
|
1370
|
-
featured,
|
|
1371
|
-
wsEnabled,
|
|
1372
|
-
template,
|
|
1373
|
-
disabledModels,
|
|
1374
|
-
selectedModelsByProvider: new Map(),
|
|
1375
|
-
gatheredProviderNames: effectiveGatheredProviderNames,
|
|
1376
|
-
degradedProviderNames: new Set(),
|
|
1377
|
-
legacyCustomModelSlugs,
|
|
1378
|
-
multiAgentMode,
|
|
1379
|
-
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
1380
|
-
keepNativeChatGptOnV1,
|
|
1381
|
-
exactComboSlugs,
|
|
1382
|
-
hasPhysicalComboProvider,
|
|
1383
|
-
includeNativeOpenAi,
|
|
1384
|
-
accountBoundEntries,
|
|
1385
|
-
suppressedBareNativeSlugs,
|
|
1386
|
-
openaiContextCap,
|
|
1387
|
-
policy: {
|
|
1388
|
-
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1389
|
-
warningPolicy: "emit",
|
|
1390
|
-
},
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
interface RetainedCatalogSyncRead {
|
|
1395
|
-
readonly catalogPath: string;
|
|
1396
|
-
readonly catalog: RawCatalog;
|
|
1397
|
-
readonly onDiskCatalog: RawCatalog | null;
|
|
1398
|
-
readonly modelsCache: RawCatalog | null;
|
|
1399
|
-
readonly evidence: string;
|
|
1400
|
-
/**
|
|
1401
|
-
* Process-local epochs, baselined AFTER our own gather rather than with the
|
|
1402
|
-
* filesystem bytes above. See `retainedCatalogProcessEvidence`.
|
|
1403
|
-
*/
|
|
1404
|
-
readonly processEvidence: string;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
interface RetainedCatalogSyncResult {
|
|
1408
|
-
added: number;
|
|
1409
|
-
path: string;
|
|
1410
|
-
catalogWritten: boolean;
|
|
1411
|
-
comboOmissions: ComboCatalogOmission[];
|
|
1412
|
-
/** Validated catalog commit (including identical bytes), or a refused refresh. */
|
|
1413
|
-
refreshOutcome?: "committed" | "refused";
|
|
1414
|
-
/** `desired_disabled` observed under K after the provider await; nothing was written. */
|
|
1415
|
-
skippedReason?: "desired_disabled";
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
/**
|
|
1419
|
-
* Catalog/cache commit overrides.
|
|
1420
|
-
*
|
|
1421
|
-
* An explicit `ocx sync` is also the refresh path for side profiles that consume
|
|
1422
|
-
* the OpenCodex catalog without injection (for example a custom `model_provider`
|
|
1423
|
-
* that routes to the proxy). In that mode the Codex integration toggle only
|
|
1424
|
-
* governs config/history injection; the catalog and models cache may still be
|
|
1425
|
-
* refreshed, so `allowWhenDesiredDisabled` lets the commit path ignore the OFF
|
|
1426
|
-
* gate that otherwise protects a fully native home.
|
|
1427
|
-
*/
|
|
1428
|
-
export interface CodexCatalogSyncOptions {
|
|
1429
|
-
allowWhenDesiredDisabled?: boolean;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
interface RetainedCatalogSyncWrite {
|
|
1433
|
-
readonly config: OcxConfig;
|
|
1434
|
-
readonly goModels: CatalogModel[];
|
|
1435
|
-
readonly providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
1436
|
-
readonly comboOmissions: ComboCatalogOmission[];
|
|
1437
|
-
readonly read: RetainedCatalogSyncRead;
|
|
1438
|
-
readonly permit: CatalogWritePermit;
|
|
1439
|
-
readonly owningCodexHome: string;
|
|
1440
|
-
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
function optionalFileBytes(path: string): string | null {
|
|
1444
|
-
try {
|
|
1445
|
-
return readFileSync(path).toString("base64");
|
|
1446
|
-
} catch (error) {
|
|
1447
|
-
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") return null;
|
|
1448
|
-
throw error;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
function loadCatalogForRetainedSync(path: string): RawCatalog | null {
|
|
1453
|
-
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
1454
|
-
if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
|
|
1455
|
-
const active = readCatalog(path);
|
|
1456
|
-
// A valid configured custom file remains the content authority even when it has no bare native
|
|
1457
|
-
// template. The null-template builder is deliberate; a stale backup must not replace active
|
|
1458
|
-
// custom root metadata merely because the current file contains only routed rows.
|
|
1459
|
-
if (active && (!isDefaultCatalogPath(path) || findNativeTemplate(active))) return active;
|
|
1460
|
-
return readCatalog(catalogBackupPathFor(path))
|
|
1461
|
-
?? (isDefaultCatalogPath(path) ? readCatalog(legacyCatalogBackupPath()) : null)
|
|
1462
|
-
?? readCatalog(activeCodexModelsCachePath())
|
|
1463
|
-
?? active;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function retainedCatalogSyncEvidence(
|
|
1467
|
-
config: OcxConfig,
|
|
1468
|
-
catalogPath: string,
|
|
1469
|
-
catalog: RawCatalog,
|
|
1470
|
-
): string {
|
|
1471
|
-
return JSON.stringify({
|
|
1472
|
-
config,
|
|
1473
|
-
catalogPath,
|
|
1474
|
-
catalog,
|
|
1475
|
-
catalogBytes: optionalFileBytes(catalogPath),
|
|
1476
|
-
hashedBackupBytes: optionalFileBytes(catalogBackupPathFor(catalogPath)),
|
|
1477
|
-
legacyBackupBytes: isDefaultCatalogPath(catalogPath)
|
|
1478
|
-
? optionalFileBytes(legacyCatalogBackupPath()) : null,
|
|
1479
|
-
modelsCacheBytes: optionalFileBytes(activeCodexModelsCachePath()),
|
|
1480
|
-
// The persisted runtime selection is a pre-await filesystem input, not a
|
|
1481
|
-
// process epoch: another PROCESS can move runtime authority by rewriting this
|
|
1482
|
-
// file, and that move is invisible to our in-process memo. Recorded PRESENT or
|
|
1483
|
-
// ABSENT, because its absence is what makes the resolver fall back.
|
|
1484
|
-
runtimeStateBytes: optionalFileBytes(codexRuntimeStatePath()),
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
/**
|
|
1489
|
-
* The bundled-template half of the same evidence, observed separately.
|
|
1490
|
-
*
|
|
1491
|
-
* The runtime process memo is deliberately NOT here, and that exclusion took three
|
|
1492
|
-
* attempts to get honest. Gathering resolves the Codex runtime lazily and under its
|
|
1493
|
-
* own cache key, so this path cannot pre-settle that memo: baselining it before the
|
|
1494
|
-
* await always detected our own side effect and refused every write, and baselining
|
|
1495
|
-
* it after the await captured a runtime that ANOTHER process had moved as though it
|
|
1496
|
-
* were ours — a catalog prepared from R1 committing after authority reached R2.
|
|
1497
|
-
*
|
|
1498
|
-
* Runtime authority is covered where it is actually durable instead: the persisted
|
|
1499
|
-
* `codex-runtime.json` bytes sit in the pre-await filesystem evidence, PRESENT or
|
|
1500
|
-
* ABSENT, so a cross-process runtime move is caught. What is left uncovered, and is
|
|
1501
|
-
* written down rather than papered over, is a same-process in-memory runtime swap
|
|
1502
|
-
* that never touches that file — WP11 owns the lock that makes that case decidable.
|
|
1503
|
-
*/
|
|
1504
|
-
function retainedCatalogProcessEvidence(): string {
|
|
1505
|
-
return JSON.stringify({
|
|
1506
|
-
bundledCatalogCache: bundledCatalogCacheState(),
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
|
|
1510
|
-
/**
|
|
1511
|
-
* Capture every local catalog input the retained sync path consults before its
|
|
1512
|
-
* provider await. The exact evidence is compared after K acquisition; a newer
|
|
1513
|
-
* catalog/backup/cache or target selection makes this attempt a no-write.
|
|
1514
|
-
*/
|
|
1515
|
-
function readRetainedCatalogSync(config: OcxConfig): RetainedCatalogSyncRead | null {
|
|
1516
|
-
const catalogPath = readCodexCatalogPath();
|
|
1517
|
-
const catalog = loadCatalogForRetainedSync(catalogPath);
|
|
1518
|
-
if (!catalog) return null;
|
|
1519
|
-
|
|
1520
|
-
// The bundled catalog is a reliable native template on the default path, but it is not the
|
|
1521
|
-
// merge source. Preservation must inspect the file that this sync is about to overwrite;
|
|
1522
|
-
// otherwise an empty/partial provider gather cannot see routed or user-native rows on disk.
|
|
1523
|
-
const onDiskCatalog = readCatalog(catalogPath);
|
|
1524
|
-
const modelsCache = readCatalog(activeCodexModelsCachePath());
|
|
1525
|
-
const evidence = retainedCatalogSyncEvidence(config, catalogPath, catalog);
|
|
1526
|
-
// `processEvidence` is filled in after the provider await, not here.
|
|
1527
|
-
return { catalogPath, catalog, onDiskCatalog, modelsCache, evidence, processEvidence: "" };
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function revalidateRetainedCatalogSync(
|
|
1531
|
-
config: OcxConfig,
|
|
1532
|
-
prepared: RetainedCatalogSyncRead,
|
|
1533
|
-
): RetainedCatalogSyncRead | null {
|
|
1534
|
-
const catalogPath = readCodexCatalogPath();
|
|
1535
|
-
if (catalogPath !== prepared.catalogPath) return null;
|
|
1536
|
-
const evidence = retainedCatalogSyncEvidence(config, catalogPath, prepared.catalog);
|
|
1537
|
-
if (evidence !== prepared.evidence) return null;
|
|
1538
|
-
if (retainedCatalogProcessEvidence() !== prepared.processEvidence) return null;
|
|
1539
|
-
return {
|
|
1540
|
-
catalogPath,
|
|
1541
|
-
catalog: JSON.parse(JSON.stringify(prepared.catalog)) as RawCatalog,
|
|
1542
|
-
onDiskCatalog: readCatalog(catalogPath),
|
|
1543
|
-
modelsCache: readCatalog(activeCodexModelsCachePath()),
|
|
1544
|
-
evidence,
|
|
1545
|
-
processEvidence: prepared.processEvidence,
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
/**
|
|
1550
|
-
* Exact bytes currently on disk at `path`, or null when unreadable/absent.
|
|
1551
|
-
*
|
|
1552
|
-
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
1553
|
-
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
1554
|
-
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
|
|
1555
|
-
* decoded strings would then classify a malformed catalog as identical, skip the
|
|
1556
|
-
* atomic repair write, and leave the corruption on disk while reporting
|
|
1557
|
-
* `catalogWritten: false`.
|
|
1558
|
-
*/
|
|
1559
|
-
function currentCatalogFileContent(path: string): Buffer | null {
|
|
1560
|
-
try {
|
|
1561
|
-
return readFileSync(path);
|
|
1562
|
-
} catch {
|
|
1563
|
-
return null;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
1568
|
-
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
1569
|
-
try {
|
|
1570
|
-
return readFileSync(read.catalogPath, "utf8");
|
|
1571
|
-
} catch {
|
|
1572
|
-
return null;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
return catalogHasRoutedEntries(read.catalog)
|
|
1576
|
-
? null
|
|
1577
|
-
: `${JSON.stringify(read.catalog, null, 2)}\n`;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function catalogModelsForMergeWithNativeRecovery(
|
|
1581
|
-
catalogPath: string,
|
|
1582
|
-
catalog: RawCatalog,
|
|
1583
|
-
onDiskCatalog: RawCatalog | null,
|
|
1584
|
-
): RawEntry[] {
|
|
1585
|
-
const primaryCatalogModels = onDiskCatalog?.models ?? catalog.models ?? [];
|
|
1586
|
-
// Native-alias compatibility can omit disabled native rows from the effective catalog because
|
|
1587
|
-
// Desktop's remote allowlist ignores `visibility: "hide"`. Keep current/pristine native recovery
|
|
1588
|
-
// sources beside the on-disk rows so re-enabling a model restores its real metadata. Routed and
|
|
1589
|
-
// user-authored rows still come only from the on-disk catalog.
|
|
1590
|
-
return mergeCatalogModelsWithNativeRecovery(primaryCatalogModels, [
|
|
1591
|
-
catalog.models ?? [],
|
|
1592
|
-
readCatalogBackup(catalogPath)?.models ?? [],
|
|
1593
|
-
]);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
|
|
1597
|
-
|
|
1598
|
-
interface RootAutoReviewStamp {
|
|
1599
|
-
slug: string;
|
|
1600
|
-
original: string | null;
|
|
1601
|
-
applied: string;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function rootAutoReviewStamp(entry: RawEntry): RootAutoReviewStamp | undefined {
|
|
1605
|
-
const value = entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1606
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1607
|
-
const stamp = value as Record<string, unknown>;
|
|
1608
|
-
if (stamp.slug !== entry.slug || typeof stamp.slug !== "string"
|
|
1609
|
-
|| typeof stamp.applied !== "string"
|
|
1610
|
-
|| (stamp.original !== null && typeof stamp.original !== "string")) return undefined;
|
|
1611
|
-
return stamp as unknown as RootAutoReviewStamp;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
/** True when the value is a valid Codex catalog auto-review selector. */
|
|
1616
|
-
export function isValidAutoReviewModel(value: unknown): value is string {
|
|
1617
|
-
return isValidAutoReviewTarget(value);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
|
|
1621
|
-
|
|
1622
|
-
/** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
|
|
1623
|
-
function isRoutedCatalogEntry(entry: RawEntry): boolean {
|
|
1624
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1625
|
-
return slug.includes("/")
|
|
1626
|
-
|| (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
/** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
|
|
1630
|
-
function clearAutoReviewOverrideValue(entry: RawEntry): void {
|
|
1631
|
-
const stamp = rootAutoReviewStamp(entry);
|
|
1632
|
-
if (stamp) {
|
|
1633
|
-
if (entry.auto_review_model_override === stamp.applied) entry.auto_review_model_override = stamp.original;
|
|
1634
|
-
} else {
|
|
1635
|
-
entry.auto_review_model_override = null;
|
|
1636
|
-
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
/**
|
|
1641
|
-
* Legacy whole-catalog root stamp: releases before AUTO_REVIEW_ROOT_MARKER wrote root stamps that
|
|
1642
|
-
* are textually identical to an upstream value, so the only way to recognize one is the uniform
|
|
1643
|
-
* signature the no-provider path relies on — a single value that a routed row also carries.
|
|
1644
|
-
* Returns the stamped values when the observed rows match that shape.
|
|
1645
|
-
*/
|
|
1646
|
-
function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
|
|
1647
|
-
if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
|
|
1648
|
-
const configuredValues = new Set(observedModels.flatMap(entry => {
|
|
1649
|
-
const value = entry?.auto_review_model_override;
|
|
1650
|
-
return typeof value === "string" && value.trim() ? [value] : [];
|
|
1651
|
-
}));
|
|
1652
|
-
const globalStamp = configuredValues.size === 1
|
|
1653
|
-
&& observedModels.some(entry => {
|
|
1654
|
-
const value = entry.auto_review_model_override;
|
|
1655
|
-
return isRoutedCatalogEntry(entry)
|
|
1656
|
-
&& typeof value === "string"
|
|
1657
|
-
&& value.trim().length > 0
|
|
1658
|
-
&& configuredValues.has(value);
|
|
1659
|
-
})
|
|
1660
|
-
&& observedModels.every(entry => {
|
|
1661
|
-
const value = entry?.auto_review_model_override;
|
|
1662
|
-
return value === null
|
|
1663
|
-
|| value === undefined
|
|
1664
|
-
|| (typeof value === "string" && configuredValues.has(value));
|
|
1665
|
-
});
|
|
1666
|
-
return globalStamp ? configuredValues : undefined;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
/**
|
|
1670
|
-
* Sweep legacy root stamps off the rows a root removal owns, before provider plans land.
|
|
1671
|
-
*
|
|
1672
|
-
* Root removal reaches marker-tagged native rows on its own, but a catalog written before the
|
|
1673
|
-
* marker only carries the legacy signature — and provider stamping rewrites that signature before
|
|
1674
|
-
* the root pass could read it, so the sweep has to run first.
|
|
1675
|
-
*/
|
|
1676
|
-
function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readonly RawEntry[] = []): void {
|
|
1677
|
-
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
1678
|
-
if (legacyStamp === undefined) return;
|
|
1679
|
-
for (const entry of models) {
|
|
1680
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1681
|
-
const current = entry.auto_review_model_override;
|
|
1682
|
-
if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
|
|
1683
|
-
&& typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
/**
|
|
1688
|
-
* Clear the root selector from every row this path owns: routed rows, rows stamped by a release
|
|
1689
|
-
* that writes the provenance marker, and the legacy whole-catalog stamp that predates it.
|
|
1690
|
-
*/
|
|
1691
|
-
function clearAutoReviewModelOverride(
|
|
1692
|
-
models: readonly RawEntry[],
|
|
1693
|
-
sourceModels: readonly RawEntry[] = [],
|
|
1694
|
-
): void {
|
|
1695
|
-
const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
|
|
1696
|
-
for (const entry of models) {
|
|
1697
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1698
|
-
const current = entry.auto_review_model_override;
|
|
1699
|
-
if (isRoutedCatalogEntry(entry)
|
|
1700
|
-
|| (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
|
|
1701
|
-
|| (legacyStamp !== undefined && typeof current === "string" && legacyStamp.has(current))) {
|
|
1702
|
-
clearAutoReviewOverrideValue(entry);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
/** Warn once about a malformed or unresolvable root auto-review selector. */
|
|
1708
|
-
function warnAutoReviewModelDiagnostic(
|
|
1709
|
-
reason: "invalid" | "unresolved",
|
|
1710
|
-
configured: string,
|
|
1711
|
-
): void {
|
|
1712
|
-
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1713
|
-
const detail = reason === "unresolved"
|
|
1714
|
-
? "the selector was not found in the final catalog"
|
|
1715
|
-
: "the selector format is invalid";
|
|
1716
|
-
console.warn(
|
|
1717
|
-
`[opencodex] auto_review_model ${detail} (${safeConfigured}); preserving normal upstream auto-review behavior.`,
|
|
1718
|
-
);
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
/** Warn once about a malformed or unresolvable provider-scoped auto-review selector. */
|
|
1722
|
-
function warnProviderAutoReviewModelDiagnostic(
|
|
1723
|
-
reason: "invalid" | "unresolved",
|
|
1724
|
-
provider: string,
|
|
1725
|
-
configured: string,
|
|
1726
|
-
): void {
|
|
1727
|
-
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
1728
|
-
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1729
|
-
const detail = reason === "unresolved"
|
|
1730
|
-
? "the selector was not found in the final catalog"
|
|
1731
|
-
: "the selector format is invalid";
|
|
1732
|
-
console.warn(
|
|
1733
|
-
`[opencodex] auto_review_model for provider ${safeProvider} ${detail} (${safeConfigured}); using the next valid provider/root selector or upstream behavior.`,
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
/**
|
|
1738
|
-
* Note once when a bare selector resolves to a row outside the provider it was configured on.
|
|
1739
|
-
*
|
|
1740
|
-
* That is how a native model is named as a reviewer, so it stays usable, but a mistyped target must
|
|
1741
|
-
* not be silent: the operator sees which catalog row actually supplies the reviewer.
|
|
1742
|
-
*/
|
|
1743
|
-
function warnProviderAutoReviewForeignTarget(provider: string, configured: string, target: string): void {
|
|
1744
|
-
const safeProvider = JSON.stringify(redactSecretString(provider));
|
|
1745
|
-
const safeConfigured = JSON.stringify(redactSecretString(configured));
|
|
1746
|
-
const safeTarget = JSON.stringify(redactSecretString(target));
|
|
1747
|
-
console.warn(
|
|
1748
|
-
`[opencodex] auto_review_model for provider ${safeProvider} (${safeConfigured}) resolved to ${safeTarget}, which is not a row of that provider; that catalog row supplies the reviewer.`,
|
|
1749
|
-
);
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
/** Preserve native upstream overrides and the root-derived provenance marker from source rows. */
|
|
1753
|
-
function preserveNativeAutoReviewModelOverrides(
|
|
1754
|
-
models: readonly RawEntry[],
|
|
1755
|
-
sourceModels: readonly RawEntry[],
|
|
1756
|
-
): void {
|
|
1757
|
-
const existing = new Map<string, { value: string | null; root: true | RootAutoReviewStamp | undefined }>();
|
|
1758
|
-
for (const entry of sourceModels) {
|
|
1759
|
-
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1760
|
-
const value = entry.auto_review_model_override;
|
|
1761
|
-
if (!slug || isRoutedCatalogEntry(entry)) continue;
|
|
1762
|
-
if (typeof value === "string" || value === null) {
|
|
1763
|
-
existing.set(slug, { value, root: rootAutoReviewStamp(entry) ?? (entry[AUTO_REVIEW_ROOT_MARKER] === true ? true : undefined) });
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
for (const entry of models) {
|
|
1767
|
-
const slug = typeof entry.slug === "string" ? entry.slug : undefined;
|
|
1768
|
-
if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
|
|
1769
|
-
const saved = existing.get(slug)!;
|
|
1770
|
-
entry.auto_review_model_override = saved.value;
|
|
1771
|
-
if (saved.root) entry[AUTO_REVIEW_ROOT_MARKER] = structuredClone(saved.root);
|
|
1772
|
-
else delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
/** Stamp a root-derived override and mark native rows so later root removal is durable. */
|
|
1777
|
-
function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
1778
|
-
if (!isRoutedCatalogEntry(entry)) {
|
|
1779
|
-
const previous = rootAutoReviewStamp(entry);
|
|
1780
|
-
const current = entry.auto_review_model_override;
|
|
1781
|
-
entry[AUTO_REVIEW_ROOT_MARKER] = {
|
|
1782
|
-
slug: typeof entry.slug === "string" ? entry.slug : "",
|
|
1783
|
-
original: previous && current === previous.applied
|
|
1784
|
-
? previous.original : typeof current === "string" ? current : null,
|
|
1785
|
-
applied: target,
|
|
1786
|
-
} satisfies RootAutoReviewStamp;
|
|
1787
|
-
} else {
|
|
1788
|
-
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1789
|
-
}
|
|
1790
|
-
entry.auto_review_model_override = target;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
/** Stamp a provider-derived override; provider stamps never fall under root removal. */
|
|
1794
|
-
function stampProviderAutoReviewOverride(entry: RawEntry, target: string): void {
|
|
1795
|
-
entry.auto_review_model_override = target;
|
|
1796
|
-
delete entry[AUTO_REVIEW_ROOT_MARKER];
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
/**
|
|
1800
|
-
* Apply the root Codex auto-review selector to every catalog row, or clear it when the value is
|
|
1801
|
-
* absent, blank, malformed, or does not resolve against the assembled catalog.
|
|
1802
|
-
*/
|
|
1803
|
-
export function applyAutoReviewModelOverride(
|
|
1804
|
-
models: RawEntry[] | undefined,
|
|
1805
|
-
autoReviewModel: string | null | undefined,
|
|
1806
|
-
sourceModels: readonly RawEntry[] = [],
|
|
1807
|
-
): AutoReviewModelOverrideResult {
|
|
1808
|
-
if (!models || !Array.isArray(models)) return "absent";
|
|
1809
|
-
if (autoReviewModel === null || autoReviewModel === undefined) {
|
|
1810
|
-
clearAutoReviewModelOverride(models, sourceModels);
|
|
1811
|
-
return "absent";
|
|
1812
|
-
}
|
|
1813
|
-
const trimmed = autoReviewModel.trim();
|
|
1814
|
-
if (!trimmed) {
|
|
1815
|
-
clearAutoReviewModelOverride(models, sourceModels);
|
|
1816
|
-
return "absent";
|
|
1817
|
-
}
|
|
1818
|
-
if (!isValidAutoReviewModel(trimmed)) {
|
|
1819
|
-
clearAutoReviewModelOverride(models, sourceModels);
|
|
1820
|
-
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
1821
|
-
return "invalid";
|
|
1822
|
-
}
|
|
1823
|
-
if (!configuredCatalogEntry(models, trimmed)) {
|
|
1824
|
-
clearAutoReviewModelOverride(models, sourceModels);
|
|
1825
|
-
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
1826
|
-
return "unresolved";
|
|
1827
|
-
}
|
|
1828
|
-
for (const entry of models) {
|
|
1829
|
-
if (entry && typeof entry === "object") {
|
|
1830
|
-
stampRootAutoReviewOverride(entry, trimmed);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
return "applied";
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
/** Validated provider-scoped target with both the configured spelling and catalog slug. */
|
|
1837
|
-
interface ValidProviderReviewTarget {
|
|
1838
|
-
configured: string;
|
|
1839
|
-
target: string;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
/** One provider's resolved provider-wide and per-model auto-review targets. */
|
|
1843
|
-
interface ProviderReviewPlan {
|
|
1844
|
-
wide?: ValidProviderReviewTarget;
|
|
1845
|
-
perModel: Map<string, ValidProviderReviewTarget>;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
/** Public provider namespace of a routed catalog row, when it has one. */
|
|
1849
|
-
function catalogEntryProviderName(entry: RawEntry): string | undefined {
|
|
1850
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1851
|
-
const slash = slug.indexOf("/");
|
|
1852
|
-
return slash > 0 && isRoutedCatalogEntry(entry) ? slug.slice(0, slash) : undefined;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
/** Encoded model-id segment of a routed catalog row, when it has one. */
|
|
1856
|
-
function catalogEntryModelSegment(entry: RawEntry): string | undefined {
|
|
1857
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1858
|
-
const slash = slug.indexOf("/");
|
|
1859
|
-
return slash > 0 ? slug.slice(slash + 1) : undefined;
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
/** Case-preserving encoded key used to match per-model override maps. */
|
|
1863
|
-
function providerModelKey(modelId: string): string {
|
|
1864
|
-
return canonicalAutoReviewModelKey(modelId);
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
/**
|
|
1868
|
-
* True when another routed row of this provider already carries `alias` as its own model id.
|
|
1869
|
-
*
|
|
1870
|
-
* The alias API validates against whatever ids discovery has reported so far, so on a cold start an
|
|
1871
|
-
* alias can be persisted that later turns out to name a different row. A key using it is then not
|
|
1872
|
-
* an alternate spelling of the aliased model — it is that row's id — and must not be propagated.
|
|
1873
|
-
*/
|
|
1874
|
-
function aliasNamesAnotherRoutedRow(models: readonly RawEntry[], provider: string, alias: string): boolean {
|
|
1875
|
-
const encoded = encodeRoutedModelId(alias);
|
|
1876
|
-
return models.some(entry => isRoutedCatalogEntry(entry)
|
|
1877
|
-
&& catalogEntryProviderName(entry) === provider
|
|
1878
|
-
&& catalogEntryModelSegment(entry) === encoded);
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
/** Resolve one configured target against the assembled catalog; bare values name a model of the same provider. */
|
|
1882
|
-
function resolveProviderReviewTarget(
|
|
1883
|
-
models: readonly RawEntry[],
|
|
1884
|
-
provider: string,
|
|
1885
|
-
configuredRaw: unknown,
|
|
1886
|
-
): { kind: "valid"; value: ValidProviderReviewTarget; foreign?: boolean } | { kind: "invalid"; configured: string } | { kind: "unresolved"; configured: string } | { kind: "absent" } {
|
|
1887
|
-
if (typeof configuredRaw !== "string") return { kind: "absent" };
|
|
1888
|
-
const configured = configuredRaw.trim();
|
|
1889
|
-
if (!configured) return { kind: "absent" };
|
|
1890
|
-
if (!isValidAutoReviewModel(configured)) return { kind: "invalid", configured };
|
|
1891
|
-
const prefix = `${provider}/`;
|
|
1892
|
-
let match: RawEntry | undefined;
|
|
1893
|
-
const sameProviderCandidate = (rawModelId: string): RawEntry | undefined => models.find(entry => {
|
|
1894
|
-
if (!isRoutedCatalogEntry(entry) || typeof entry.slug !== "string" || !entry.slug.startsWith(prefix)) return false;
|
|
1895
|
-
const segment = catalogEntryModelSegment(entry);
|
|
1896
|
-
return segment !== undefined && segment === encodeRoutedModelId(rawModelId);
|
|
1897
|
-
});
|
|
1898
|
-
// A bare selector names a model of this provider. A full selector that resolves in the
|
|
1899
|
-
// assembled catalog already names the exact row, including a same-provider encoded slug.
|
|
1900
|
-
if (!configured.includes("/")) {
|
|
1901
|
-
match = sameProviderCandidate(configured);
|
|
1902
|
-
}
|
|
1903
|
-
match ??= configuredCatalogEntry(models, configured);
|
|
1904
|
-
if (!match && configured.startsWith(prefix)) {
|
|
1905
|
-
match = sameProviderCandidate(configured.slice(prefix.length));
|
|
1906
|
-
}
|
|
1907
|
-
if (!match) {
|
|
1908
|
-
// A raw model id may itself contain "/" (for example zenmux moonshotai/kimi-k3).
|
|
1909
|
-
// After the full-selector lookup misses, try that spelling as a same-provider id.
|
|
1910
|
-
match = sameProviderCandidate(configured);
|
|
1911
|
-
}
|
|
1912
|
-
if (!match) return { kind: "unresolved", configured };
|
|
1913
|
-
const target = typeof match.slug === "string" ? match.slug : configured;
|
|
1914
|
-
// A qualified selector may name another provider's row on purpose; only a bare value that lands
|
|
1915
|
-
// outside this provider is worth reporting.
|
|
1916
|
-
const foreign = !configured.includes("/") && catalogEntryProviderName(match) !== provider;
|
|
1917
|
-
return { kind: "valid", value: { configured, target }, ...(foreign ? { foreign: true } : {}) };
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
/** Build resolved per-provider plans and emit one diagnostic per bad selector. */
|
|
1921
|
-
function buildProviderReviewPlans(
|
|
1922
|
-
models: readonly RawEntry[],
|
|
1923
|
-
config: Pick<OcxConfig, "providers">,
|
|
1924
|
-
): { plans: Map<string, ProviderReviewPlan>; failure?: "invalid" | "unresolved" } {
|
|
1925
|
-
const plans = new Map<string, ProviderReviewPlan>();
|
|
1926
|
-
let failure: "invalid" | "unresolved" | undefined;
|
|
1927
|
-
const warned = new Set<string>();
|
|
1928
|
-
const recordFailure = (kind: "invalid" | "unresolved", provider: string, configured: string): void => {
|
|
1929
|
-
const signature = `${provider}\u0000${configured}`;
|
|
1930
|
-
if (warned.has(signature)) return;
|
|
1931
|
-
warned.add(signature);
|
|
1932
|
-
warnProviderAutoReviewModelDiagnostic(kind, provider, configured);
|
|
1933
|
-
failure ??= kind;
|
|
1934
|
-
};
|
|
1935
|
-
const recordForeignTarget = (provider: string, configured: string, target: string): void => {
|
|
1936
|
-
const signature = `${provider}\u0000foreign\u0000${configured}`;
|
|
1937
|
-
if (warned.has(signature)) return;
|
|
1938
|
-
warned.add(signature);
|
|
1939
|
-
warnProviderAutoReviewForeignTarget(provider, configured, target);
|
|
1940
|
-
};
|
|
1941
|
-
for (const [name, provider] of Object.entries(config.providers ?? {})) {
|
|
1942
|
-
if (provider.autoReviewModel === undefined && provider.autoReviewModelOverrides === undefined) continue;
|
|
1943
|
-
const plan: ProviderReviewPlan = { perModel: new Map() };
|
|
1944
|
-
if (provider.autoReviewModel !== undefined) {
|
|
1945
|
-
const resolved = resolveProviderReviewTarget(models, name, provider.autoReviewModel);
|
|
1946
|
-
if (resolved.kind === "valid") {
|
|
1947
|
-
plan.wide = resolved.value;
|
|
1948
|
-
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
1949
|
-
}
|
|
1950
|
-
else if (resolved.kind !== "absent") recordFailure(resolved.kind, name, resolved.configured);
|
|
1951
|
-
}
|
|
1952
|
-
if (provider.autoReviewModelOverrides !== undefined) {
|
|
1953
|
-
for (const [modelId, rawTarget] of Object.entries(provider.autoReviewModelOverrides)) {
|
|
1954
|
-
const resolved = resolveProviderReviewTarget(models, name, rawTarget);
|
|
1955
|
-
if (resolved.kind === "valid") {
|
|
1956
|
-
plan.perModel.set(providerModelKey(modelId), resolved.value);
|
|
1957
|
-
if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
|
|
1958
|
-
} else if (resolved.kind !== "absent") {
|
|
1959
|
-
recordFailure(resolved.kind, name, resolved.configured);
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
// `modelAliases` publishes a second public name for a model id, and a routed row's slug always
|
|
1964
|
-
// carries the upstream id — so accept an override key written in either spelling.
|
|
1965
|
-
for (const [modelId, alias] of Object.entries(provider.modelAliases ?? {})) {
|
|
1966
|
-
if (typeof alias !== "string" || !alias.trim()) continue;
|
|
1967
|
-
if (aliasNamesAnotherRoutedRow(models, name, alias)) continue;
|
|
1968
|
-
const idKey = providerModelKey(modelId);
|
|
1969
|
-
const aliasKey = providerModelKey(alias);
|
|
1970
|
-
if (idKey === aliasKey) continue;
|
|
1971
|
-
const fromId = plan.perModel.get(idKey);
|
|
1972
|
-
const fromAlias = plan.perModel.get(aliasKey);
|
|
1973
|
-
if (fromId !== undefined && fromAlias === undefined) plan.perModel.set(aliasKey, fromId);
|
|
1974
|
-
else if (fromAlias !== undefined && fromId === undefined) plan.perModel.set(idKey, fromAlias);
|
|
1975
|
-
}
|
|
1976
|
-
if (plan.wide !== undefined || plan.perModel.size > 0) plans.set(name, plan);
|
|
1977
|
-
}
|
|
1978
|
-
return { plans, failure };
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
/** Apply or clear the root selector only on rows without a provider stamp. */
|
|
1982
|
-
function applyRootSelectorToRemaining(
|
|
1983
|
-
models: readonly RawEntry[],
|
|
1984
|
-
rootValue: string | null | undefined,
|
|
1985
|
-
providerStamped: ReadonlySet<RawEntry>,
|
|
1986
|
-
): AutoReviewModelOverrideResult {
|
|
1987
|
-
const clearRemaining = (): void => {
|
|
1988
|
-
for (const entry of models) {
|
|
1989
|
-
if (!entry || providerStamped.has(entry)) continue;
|
|
1990
|
-
// Native rows written by releases before the root marker cannot be told apart from upstream
|
|
1991
|
-
// values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
|
|
1992
|
-
// uniform signature still recognizes before provider plans land, because provider stamping
|
|
1993
|
-
// destroys that signature; a catalog that no longer matches it needs a one-off manual sync.
|
|
1994
|
-
if (isRoutedCatalogEntry(entry) || entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry)) clearAutoReviewOverrideValue(entry);
|
|
1995
|
-
}
|
|
1996
|
-
};
|
|
1997
|
-
if (rootValue === null || rootValue === undefined) {
|
|
1998
|
-
clearRemaining();
|
|
1999
|
-
return "absent";
|
|
2000
|
-
}
|
|
2001
|
-
const trimmed = rootValue.trim();
|
|
2002
|
-
if (!trimmed) {
|
|
2003
|
-
clearRemaining();
|
|
2004
|
-
return "absent";
|
|
2005
|
-
}
|
|
2006
|
-
if (!isValidAutoReviewModel(trimmed)) {
|
|
2007
|
-
clearRemaining();
|
|
2008
|
-
warnAutoReviewModelDiagnostic("invalid", trimmed);
|
|
2009
|
-
return "invalid";
|
|
2010
|
-
}
|
|
2011
|
-
if (!configuredCatalogEntry(models, trimmed)) {
|
|
2012
|
-
clearRemaining();
|
|
2013
|
-
warnAutoReviewModelDiagnostic("unresolved", trimmed);
|
|
2014
|
-
return "unresolved";
|
|
2015
|
-
}
|
|
2016
|
-
for (const entry of models) {
|
|
2017
|
-
if (!entry || providerStamped.has(entry)) continue;
|
|
2018
|
-
stampRootAutoReviewOverride(entry, trimmed);
|
|
2019
|
-
}
|
|
2020
|
-
return "applied";
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
/** Provider-aware variant: provider rows win and the root selector is the fallback. */
|
|
2024
|
-
export function applyConfiguredAutoReviewModelOverride(
|
|
2025
|
-
models: RawEntry[] | undefined,
|
|
2026
|
-
rootAutoReviewModel: string | null | undefined,
|
|
2027
|
-
config: Pick<OcxConfig, "providers">,
|
|
2028
|
-
sourceModels: readonly RawEntry[] = [],
|
|
2029
|
-
): AutoReviewModelOverrideResult {
|
|
2030
|
-
if (!models || !Array.isArray(models)) return "absent";
|
|
2031
|
-
// Runs unconditionally because the sweep only fires on the uniform legacy signature. A resolved
|
|
2032
|
-
// root selector restamps every row it touches below, so the call is behavior-preserving there;
|
|
2033
|
-
// with the root absent, invalid, or unresolved those clears are final — which is the point, and
|
|
2034
|
-
// also the limit: the legacy heuristic cannot tell a root stamp from an identical upstream value.
|
|
2035
|
-
clearLegacyRootStamps(models, sourceModels);
|
|
2036
|
-
const { plans, failure } = buildProviderReviewPlans(models, config);
|
|
2037
|
-
const providerStamped = new Set<RawEntry>();
|
|
2038
|
-
for (const entry of models) {
|
|
2039
|
-
if (!entry || typeof entry !== "object") continue;
|
|
2040
|
-
const provider = catalogEntryProviderName(entry);
|
|
2041
|
-
if (!provider) continue;
|
|
2042
|
-
const plan = plans.get(provider);
|
|
2043
|
-
if (!plan) continue;
|
|
2044
|
-
const modelSegment = catalogEntryModelSegment(entry);
|
|
2045
|
-
const perModel = modelSegment === undefined ? undefined : plan.perModel.get(providerModelKey(modelSegment));
|
|
2046
|
-
const selected = perModel ?? plan.wide;
|
|
2047
|
-
if (!selected) continue;
|
|
2048
|
-
stampProviderAutoReviewOverride(entry, selected.target);
|
|
2049
|
-
providerStamped.add(entry);
|
|
2050
|
-
}
|
|
2051
|
-
const rootResult = applyRootSelectorToRemaining(models, rootAutoReviewModel, providerStamped);
|
|
2052
|
-
const providerApplied = [...providerStamped].some(entry => typeof entry.auto_review_model_override === "string");
|
|
2053
|
-
if (providerApplied) {
|
|
2054
|
-
if (rootResult === "invalid" || rootResult === "unresolved") return rootResult;
|
|
2055
|
-
return failure ?? "applied";
|
|
2056
|
-
}
|
|
2057
|
-
return failure ?? rootResult;
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
/** True when any provider row configures a provider-scoped auto-review selector. */
|
|
2061
|
-
function configHasProviderAutoReview(config: Pick<OcxConfig, "providers">): boolean {
|
|
2062
|
-
return Object.values(config.providers ?? {}).some(provider =>
|
|
2063
|
-
provider.autoReviewModel !== undefined || provider.autoReviewModelOverrides !== undefined);
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
/** Apply the root Codex auto-review selector after the final catalog merge. */
|
|
2067
|
-
export function finalizeAutoReviewModelOverride(
|
|
2068
|
-
models: RawEntry[] | undefined,
|
|
2069
|
-
sourceModels: readonly RawEntry[] = [],
|
|
2070
|
-
config?: Pick<OcxConfig, "providers">,
|
|
2071
|
-
): AutoReviewModelOverrideResult {
|
|
2072
|
-
if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
|
|
2073
|
-
if (config && configHasProviderAutoReview(config)) {
|
|
2074
|
-
return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
|
|
2075
|
-
}
|
|
2076
|
-
return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
|
|
2077
|
-
}
|
|
2078
|
-
/**
|
|
2079
|
-
* Why an account-gated native model stopped being offered, but only when the answer is one the
|
|
2080
|
-
* operator can act on.
|
|
2081
|
-
*
|
|
2082
|
-
* Suppression is an omission: the row is never built, so there is no catalog entry for a reason
|
|
2083
|
-
* to ride on and no downstream consumer that could explain it later. #4212's reporter watched
|
|
2084
|
-
* their models disappear and reasonably concluded the proxy was broken, because every surface
|
|
2085
|
-
* that changed said nothing about the account that caused it.
|
|
2086
|
-
*
|
|
2087
|
-
* Returns `undefined` for the ordinary case — an account that is simply not entitled to a gated
|
|
2088
|
-
* model. That is the default state for most installations, it is not news, and warning about it
|
|
2089
|
-
* on every sync would bury the one case that matters. A credential the operator must repair is
|
|
2090
|
-
* the case that matters, so that is the only one this speaks up about.
|
|
2091
|
-
*
|
|
2092
|
-
* Accounts are named with the durable `p`-prefixed log label, the same identifier the dashboard
|
|
2093
|
-
* shows, never the raw pool id or the email.
|
|
2094
|
-
*/
|
|
2095
|
-
export function gatedNativeReauthSuppressionReason(args: {
|
|
2096
|
-
snapshot: CodexModelEntitlementSnapshot;
|
|
2097
|
-
slug: string;
|
|
2098
|
-
eligibleAccountIds?: ReadonlySet<string>;
|
|
2099
|
-
needsReauth: (accountId: string) => boolean;
|
|
2100
|
-
label: (accountId: string) => string;
|
|
2101
|
-
}): string | undefined {
|
|
2102
|
-
const observed = [...args.snapshot.modelsByAccount.keys()]
|
|
2103
|
-
.filter(accountId => !args.eligibleAccountIds || args.eligibleAccountIds.has(accountId))
|
|
2104
|
-
// Only accounts that could actually have served THIS model. An account upstream positively
|
|
2105
|
-
// denied is not why the model is missing, and blaming it would send the operator to repair a
|
|
2106
|
-
// credential that was never going to help. `unknown` has to stay in: an account whose roster
|
|
2107
|
-
// could not be confirmed reports `unknown` rather than `granted`, and a credential stuck on
|
|
2108
|
-
// a failed refresh is exactly that account.
|
|
2109
|
-
.filter(accountId => (
|
|
2110
|
-
codexModelEntitlementStateForAccount(args.snapshot, accountId, args.slug) !== "denied"
|
|
2111
|
-
));
|
|
2112
|
-
const stuck = observed.filter(accountId => args.needsReauth(accountId));
|
|
2113
|
-
if (stuck.length === 0) return undefined;
|
|
2114
|
-
const names = stuck.map(accountId => args.label(accountId)).sort().join(", ");
|
|
2115
|
-
return stuck.length === observed.length
|
|
2116
|
-
? `every Codex account that could serve it needs reauthentication (${names})`
|
|
2117
|
-
: `${stuck.length} of ${observed.length} Codex accounts that could serve it need reauthentication (${names})`;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
/** Durable, operator-facing label for a pool account id; never the raw id or the email. */
|
|
2121
|
-
function gatedNativeAccountLabel(config: OcxConfig, accountId: string): string {
|
|
2122
|
-
// Direct mode narrows eligibility to the native main credential, so this is the account most
|
|
2123
|
-
// likely to be named here. `codexAuthContextLogLabel` calls it "main" everywhere else; hashing
|
|
2124
|
-
// it into a `p`-prefixed digest would name the one account the operator cannot look up.
|
|
2125
|
-
if (accountId === MAIN_CODEX_ACCOUNT_ID) return "main";
|
|
2126
|
-
const account = (config.codexAccounts ?? []).find(candidate => candidate.id === accountId);
|
|
2127
|
-
return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(accountId);
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
const warnedGatedNativeSuppression = new Set<string>();
|
|
2131
|
-
|
|
2132
|
-
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
2133
|
-
export function resetGatedNativeSuppressionWarningsForTests(): void {
|
|
2134
|
-
warnedGatedNativeSuppression.clear();
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
function warnGatedNativeSuppressedOnce(slug: string, reason: string): void {
|
|
2138
|
-
const signature = `${slug}\u0000${reason}`;
|
|
2139
|
-
if (warnedGatedNativeSuppression.has(signature)) return;
|
|
2140
|
-
warnedGatedNativeSuppression.add(signature);
|
|
2141
|
-
console.warn(
|
|
2142
|
-
`[opencodex] catalog sync: ${slug} is not being offered because ${reason}. `
|
|
2143
|
-
+ "Sign in again to restore it.",
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
/**
|
|
2148
|
-
* Mescla o catálogo retido com os modelos visíveis e as configurações atuais,
|
|
2149
|
-
* incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão
|
|
2150
|
-
* de escrita para publicar o resultado apenas se os bytes mudarem, retornando
|
|
2151
|
-
* a contagem de entradas roteadas e por conta, o caminho e o estado da gravação.
|
|
2152
|
-
*/
|
|
2153
|
-
function writeRetainedCatalogSync({
|
|
2154
|
-
config,
|
|
2155
|
-
goModels,
|
|
2156
|
-
providerModelOutcomes,
|
|
2157
|
-
comboOmissions,
|
|
2158
|
-
read,
|
|
2159
|
-
permit,
|
|
2160
|
-
owningCodexHome,
|
|
2161
|
-
modelEntitlements,
|
|
2162
|
-
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
2163
|
-
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
2164
|
-
const catalogModelsForMerge = catalogModelsForMergeWithNativeRecovery(
|
|
2165
|
-
catalogPath,
|
|
2166
|
-
catalog,
|
|
2167
|
-
onDiskCatalog,
|
|
2168
|
-
);
|
|
2169
|
-
// Strict selector for template inheritance; the validity gate above keeps the broad one.
|
|
2170
|
-
const template = findSupportedNativeTemplate(catalog);
|
|
2171
|
-
|
|
2172
|
-
try {
|
|
2173
|
-
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
|
2174
|
-
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
2175
|
-
const pristine = pristineCatalogBytes(read);
|
|
2176
|
-
if (pristine !== null) {
|
|
2177
|
-
publishHashedCodexCatalogBackup(permit, owningCodexHome, {
|
|
2178
|
-
path: catalogBackupPathFor(catalogPath),
|
|
2179
|
-
content: pristine,
|
|
2180
|
-
});
|
|
2181
|
-
if (isDefaultCatalogPath(catalogPath)) {
|
|
2182
|
-
publishLegacyCodexCatalogBackup(permit, owningCodexHome, {
|
|
2183
|
-
path: legacyCatalogBackupPath(),
|
|
2184
|
-
content: pristine,
|
|
2185
|
-
});
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
} catch { /* backup best-effort */ }
|
|
2189
|
-
|
|
2190
|
-
// Hide disabled models from Codex, then feature the chosen subagent models (native OR routed)
|
|
2191
|
-
// by giving them the lowest priority — see buildCatalogEntries for why priority, not array order.
|
|
2192
|
-
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
2193
|
-
const featured = config.subagentModels ?? [];
|
|
2194
|
-
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
2195
|
-
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
2196
|
-
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
2197
|
-
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
2198
|
-
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
2199
|
-
OPENAI_CODEX_PROVIDER_ID,
|
|
2200
|
-
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
2201
|
-
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
2202
|
-
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
2203
|
-
modelEntitlements,
|
|
2204
|
-
bareEligibleAccountIds,
|
|
2205
|
-
);
|
|
2206
|
-
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
2207
|
-
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
2208
|
-
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
2209
|
-
));
|
|
2210
|
-
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
2211
|
-
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
2212
|
-
));
|
|
2213
|
-
const unavailableGatedNativeSlugs = new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => (
|
|
2214
|
-
!availableBareGatedNativeSlugs.has(slug)
|
|
2215
|
-
)));
|
|
2216
|
-
// #4212: this set is the whole record of a model vanishing, and it is a set of strings that
|
|
2217
|
-
// nothing downstream ever asks a question of. Explain it here, while the entitlement snapshot
|
|
2218
|
-
// that produced it is still in scope, because after this point the model is simply absent and
|
|
2219
|
-
// no later surface can tell "never entitled" apart from "the account broke this morning".
|
|
2220
|
-
for (const slug of unavailableGatedNativeSlugs) {
|
|
2221
|
-
const reason = gatedNativeReauthSuppressionReason({
|
|
2222
|
-
snapshot: modelEntitlements,
|
|
2223
|
-
slug,
|
|
2224
|
-
eligibleAccountIds: bareEligibleAccountIds,
|
|
2225
|
-
needsReauth: isAccountNeedsReauth,
|
|
2226
|
-
label: accountId => gatedNativeAccountLabel(config, accountId),
|
|
2227
|
-
});
|
|
2228
|
-
if (reason) warnGatedNativeSuppressedOnce(slug, reason);
|
|
2229
|
-
}
|
|
2230
|
-
const suppressedBareNativeSlugs = new Set([
|
|
2231
|
-
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
2232
|
-
...unavailableGatedNativeSlugs,
|
|
2233
|
-
]);
|
|
2234
|
-
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
2235
|
-
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
2236
|
-
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
2237
|
-
// Both user levers. Passing only the cap here is what let a per-model window the dashboard
|
|
2238
|
-
// had accepted get written back at full width in the on-disk catalog.
|
|
2239
|
-
const openaiContextCap = nativeContextLimits(config);
|
|
2240
|
-
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
2241
|
-
? visibleCodexAccountSelectors(config)
|
|
2242
|
-
: [];
|
|
2243
|
-
const observedAccountNativeEntries = [
|
|
2244
|
-
...(read.modelsCache?.models ?? []),
|
|
2245
|
-
...(onDiskCatalog?.models ?? []).filter(entry =>
|
|
2246
|
-
trustedAccountBoundNativeCatalogSlug(entry) !== undefined),
|
|
2247
|
-
];
|
|
2248
|
-
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
2249
|
-
const reserveMainSelectors = accountSelectors.filter(selector =>
|
|
2250
|
-
isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
|
|
2251
|
-
// The active file can own a bare source even when the bundled catalog is the build base.
|
|
2252
|
-
// A previously clamped qualified projection must not shorten a retained genuine ladder.
|
|
2253
|
-
const reserveObservations = [
|
|
2254
|
-
...(onDiskCatalog?.models ?? []),
|
|
2255
|
-
...(read.modelsCache?.models ?? []),
|
|
2256
|
-
...(catalog.models ?? []),
|
|
2257
|
-
];
|
|
2258
|
-
const retainedReserve = onDiskCatalog?.[RESERVE_SOURCE_CATALOG_FIELD];
|
|
2259
|
-
const retainedReserveSource = retainedReserve && typeof retainedReserve === "object" && !Array.isArray(retainedReserve)
|
|
2260
|
-
? observedReserveCatalogSource([retainedReserve as RawEntry], [])
|
|
2261
|
-
: null;
|
|
2262
|
-
const observedReserveSource = observedReserveCatalogSource(
|
|
2263
|
-
// Cache invalidation carries historical bare observations alongside emitted models.
|
|
2264
|
-
// Only unmarked observations are fresh enough to supersede the retained source.
|
|
2265
|
-
reserveObservations.filter(entry => entry.slug === NATIVE_RESERVE_MODEL
|
|
2266
|
-
&& entry.opencodex_account_observed_native === undefined), reserveMainSelectors,
|
|
2267
|
-
) ?? retainedReserveSource ?? observedReserveCatalogSource(reserveObservations, reserveMainSelectors);
|
|
2268
|
-
// This root is read only by OCX. Upstream ModelsResponse ignores unknown root fields.
|
|
2269
|
-
// Retain before final runtime clamping: an omitted row must not turn into Luna next sync.
|
|
2270
|
-
if (observedReserveSource) catalog[RESERVE_SOURCE_CATALOG_FIELD] = structuredClone(observedReserveSource);
|
|
2271
|
-
else delete catalog[RESERVE_SOURCE_CATALOG_FIELD];
|
|
2272
|
-
const lunaSource = upstreamNativeEntry(RESERVE_LUNA_METADATA_SOURCE);
|
|
2273
|
-
const reserve = createReserveCatalogProjection(
|
|
2274
|
-
config,
|
|
2275
|
-
reserveMainSelectors,
|
|
2276
|
-
observedReserveSource,
|
|
2277
|
-
lunaSource ? finishUpstreamNativeEntry(lunaSource, 9, openaiContextCap) : null,
|
|
2278
|
-
);
|
|
2279
|
-
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
2280
|
-
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
2281
|
-
const target = accountTargets.get(selector);
|
|
2282
|
-
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
2283
|
-
return [selector, slugs.filter(slug => (
|
|
2284
|
-
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug)
|
|
2285
|
-
|| (accountId !== undefined
|
|
2286
|
-
&& codexModelEntitlementStateForAccount(modelEntitlements, accountId, slug) === "granted")
|
|
2287
|
-
))] as const;
|
|
2288
|
-
}))
|
|
2289
|
-
: new Map<string, readonly string[]>();
|
|
2290
|
-
const accountNativeSlugs = accountSelectors.length > 0
|
|
2291
|
-
? [...new Set([...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]))]
|
|
2292
|
-
: [];
|
|
2293
|
-
// Unknown account-native ids have no safe bare/global identity. They are only projected through
|
|
2294
|
-
// the selector map above; the no-selector catalog remains the static native/API-key surface.
|
|
2295
|
-
const observedNativeSlugs: string[] = [];
|
|
2296
|
-
const wsEnabled = websocketsEnabled(config);
|
|
2297
|
-
const multiAgentV2Enabled = isMultiAgentV2Enabled();
|
|
2298
|
-
const goEntries = buildCatalogEntriesFromObservedState({
|
|
2299
|
-
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
2300
|
-
gptSlugs: [],
|
|
2301
|
-
goModels: orderedGoModels,
|
|
2302
|
-
featured,
|
|
2303
|
-
modelPickerOrder,
|
|
2304
|
-
wsEnabled,
|
|
2305
|
-
multiAgentMode,
|
|
2306
|
-
exactComboSlugs,
|
|
2307
|
-
accountSelectors,
|
|
2308
|
-
suppressedBareNativeSlugs,
|
|
2309
|
-
disabledNativeAccountSlugs: new Set(),
|
|
2310
|
-
multiAgentV2Enabled,
|
|
2311
|
-
openaiContextCap,
|
|
2312
|
-
});
|
|
2313
|
-
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
|
|
2314
|
-
// routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
|
|
2315
|
-
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
2316
|
-
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
2317
|
-
const baseline = readNativeBaseline(catalogPath);
|
|
2318
|
-
const gatheredProviderNames = new Set(
|
|
2319
|
-
Object.entries(config.providers ?? {})
|
|
2320
|
-
.filter(([, prov]) => prov.disabled !== true)
|
|
2321
|
-
.map(([name]) => name),
|
|
2322
|
-
);
|
|
2323
|
-
const degradedProviderNames = new Set(
|
|
2324
|
-
providerModelOutcomes
|
|
2325
|
-
.filter(outcome => outcome.state === "degraded")
|
|
2326
|
-
.map(outcome => outcome.provider),
|
|
2327
|
-
);
|
|
2328
|
-
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
2329
|
-
Object.entries(config.providers ?? {}).flatMap(([name, provider]) => (
|
|
2330
|
-
provider.disabled !== true
|
|
2331
|
-
&& Array.isArray(provider.selectedModels)
|
|
2332
|
-
&& provider.selectedModels.length > 0
|
|
2333
|
-
? [[name, new Set(provider.selectedModels)] as const]
|
|
2334
|
-
: []
|
|
2335
|
-
)),
|
|
2336
|
-
);
|
|
2337
|
-
// Central WS capability override on the FINAL on-disk catalog (the file Codex reads). Applies to
|
|
2338
|
-
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
2339
|
-
// native template can never leak supports_websockets while the flag is off.
|
|
2340
|
-
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
|
|
2341
|
-
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
|
|
2342
|
-
// providers are configured yet (fresh install / catalog bootstrap tests).
|
|
2343
|
-
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
2344
|
-
? buildCatalogEntriesFromObservedState({
|
|
2345
|
-
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
2346
|
-
gptSlugs: availableAccountNativeSlugs,
|
|
2347
|
-
goModels: [],
|
|
2348
|
-
featured,
|
|
2349
|
-
wsEnabled,
|
|
2350
|
-
multiAgentMode,
|
|
2351
|
-
exactComboSlugs,
|
|
2352
|
-
accountSelectors,
|
|
2353
|
-
suppressedBareNativeSlugs,
|
|
2354
|
-
disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))),
|
|
2355
|
-
multiAgentV2Enabled,
|
|
2356
|
-
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
2357
|
-
openaiContextCap,
|
|
2358
|
-
accountNativeSlugs,
|
|
2359
|
-
accountNativeSlugsBySelector,
|
|
2360
|
-
reserve,
|
|
2361
|
-
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
2362
|
-
: [];
|
|
2363
|
-
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
2364
|
-
modelPickerOrder,
|
|
2365
|
-
accountSelectors,
|
|
2366
|
-
catalogModels: catalogModelsForMerge,
|
|
2367
|
-
baselineCatalogModels: baselineCatalog?.models ?? [],
|
|
2368
|
-
routedEntries: goEntries,
|
|
2369
|
-
baseline,
|
|
2370
|
-
featured,
|
|
2371
|
-
wsEnabled,
|
|
2372
|
-
template,
|
|
2373
|
-
disabledModels: new Set(config.disabledModels ?? []),
|
|
2374
|
-
selectedModelsByProvider,
|
|
2375
|
-
gatheredProviderNames,
|
|
2376
|
-
pendingProviderNames: pendingModelSelectionProviders(config),
|
|
2377
|
-
degradedProviderNames,
|
|
2378
|
-
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
2379
|
-
multiAgentMode,
|
|
2380
|
-
multiAgentV2Enabled,
|
|
2381
|
-
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
2382
|
-
exactComboSlugs,
|
|
2383
|
-
hasPhysicalComboProvider,
|
|
2384
|
-
includeNativeOpenAi,
|
|
2385
|
-
accountBoundEntries,
|
|
2386
|
-
suppressedBareNativeSlugs,
|
|
2387
|
-
openaiContextCap,
|
|
2388
|
-
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
2389
|
-
policy: {
|
|
2390
|
-
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
2391
|
-
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
2392
|
-
warningPolicy: "emit",
|
|
2393
|
-
},
|
|
2394
|
-
});
|
|
2395
|
-
clampCatalogModelsToCodexSupport(catalog.models);
|
|
2396
|
-
finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge, config);
|
|
2397
|
-
|
|
2398
|
-
const added = goEntries.length + accountBoundEntries.length;
|
|
2399
|
-
const content = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
2400
|
-
// A byte-identical rewrite is not a catalog change, but every mtime-keyed reader
|
|
2401
|
-
// has to treat it as one. The app-server staleness classifier (#857) is the one
|
|
2402
|
-
// that matters: it compares this file's mtime against each running Codex's start
|
|
2403
|
-
// time, so an ordinary `ocx start` — or any dashboard action that re-syncs an
|
|
2404
|
-
// unchanged model set — marked every already-running Codex as holding an outdated
|
|
2405
|
-
// in-memory catalog. Since #1407 that verdict silences opencodex's own model
|
|
2406
|
-
// guidance entirely (no preferred model, no roster) for the rest of that Codex's
|
|
2407
|
-
// lifetime, so a configured injectionModel stops reaching the session even though
|
|
2408
|
-
// nothing about the catalog changed. Skipping the no-op write keeps both the mtime
|
|
2409
|
-
// and `catalogWritten` honest; `added` still reports the routed rows the catalog
|
|
2410
|
-
// carries, because they are on disk either way.
|
|
2411
|
-
const onDiskBytes = currentCatalogFileContent(catalogPath);
|
|
2412
|
-
if (onDiskBytes !== null && onDiskBytes.equals(Buffer.from(content, "utf8"))) {
|
|
2413
|
-
return { added, path: catalogPath, catalogWritten: false, comboOmissions };
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
2417
|
-
path: catalogPath,
|
|
2418
|
-
content,
|
|
2419
|
-
});
|
|
2420
|
-
return {
|
|
2421
|
-
added,
|
|
2422
|
-
path: catalogPath,
|
|
2423
|
-
catalogWritten: true,
|
|
2424
|
-
comboOmissions,
|
|
2425
|
-
};
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
function visibleAccountReplacementNatives(
|
|
2429
|
-
models: readonly RawEntry[],
|
|
2430
|
-
disabledModels: ReadonlySet<string> | null,
|
|
2431
|
-
): Map<string, boolean> {
|
|
2432
|
-
const replacements = new Map<string, boolean>();
|
|
2433
|
-
for (const entry of models) {
|
|
2434
|
-
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
2435
|
-
if (nativeSlug === undefined || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)) continue;
|
|
2436
|
-
const exactSlug = typeof entry.slug === "string" ? entry.slug : "";
|
|
2437
|
-
const visible = entry.visibility === "list"
|
|
2438
|
-
|| (disabledModels !== null
|
|
2439
|
-
&& (disabledModels.has(nativeSlug) || disabledModels.has(exactSlug)));
|
|
2440
|
-
replacements.set(nativeSlug, (replacements.get(nativeSlug) ?? true) && visible);
|
|
2441
|
-
}
|
|
2442
|
-
return replacements;
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
function restoreAccountHiddenBareNatives(
|
|
2446
|
-
entries: readonly RawEntry[],
|
|
2447
|
-
replacementVisibility: ReadonlyMap<string, boolean>,
|
|
2448
|
-
disabledModels: ReadonlySet<string> | null,
|
|
2449
|
-
): RawEntry[] {
|
|
2450
|
-
return entries.map(entry => {
|
|
2451
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
2452
|
-
if (
|
|
2453
|
-
entry.visibility !== "hide"
|
|
2454
|
-
|| !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
2455
|
-
|| replacementVisibility.get(slug) !== true
|
|
2456
|
-
|| disabledModels === null
|
|
2457
|
-
|| disabledModels.has(slug)
|
|
2458
|
-
) {
|
|
2459
|
-
return entry;
|
|
2460
|
-
}
|
|
2461
|
-
return { ...entry, visibility: "list" };
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
function currentDisabledModelsForRestore(): Set<string> | null {
|
|
2466
|
-
try {
|
|
2467
|
-
const diagnostics = readConfigDiagnostics();
|
|
2468
|
-
if (diagnostics.source === "fallback" || diagnostics.error !== null) return null;
|
|
2469
|
-
return new Set(diagnostics.config.disabledModels ?? []);
|
|
2470
|
-
} catch {
|
|
2471
|
-
// An unreadable config cannot safely authorize a visibility change during restore.
|
|
2472
|
-
return null;
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
export async function syncCatalogModels(
|
|
2477
|
-
config: OcxConfig,
|
|
2478
|
-
options?: CodexCatalogSyncOptions,
|
|
2479
|
-
): Promise<RetainedCatalogSyncResult> {
|
|
2480
|
-
if (pendingModelSelectionProviders(config).size) {
|
|
2481
|
-
const { resolvePendingInitialModelSelection } = await import("../../providers/initial-model-selection-runtime");
|
|
2482
|
-
await resolvePendingInitialModelSelection(config);
|
|
2483
|
-
}
|
|
2484
|
-
const owningCodexHome = getCodexHome();
|
|
2485
|
-
const preflightRead = readRetainedCatalogSync(config);
|
|
2486
|
-
if (preflightRead === null) {
|
|
2487
|
-
return {
|
|
2488
|
-
added: 0,
|
|
2489
|
-
path: readCodexCatalogPath(),
|
|
2490
|
-
catalogWritten: false,
|
|
2491
|
-
comboOmissions: [],
|
|
2492
|
-
refreshOutcome: "refused",
|
|
2493
|
-
};
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
const comboOmissions: ComboCatalogOmission[] = [];
|
|
2497
|
-
const providerModelOutcomes: CatalogGatherProviderModelOutcome[] = [];
|
|
2498
|
-
// Settle the bundled template, then baseline, and only then await. Reading it
|
|
2499
|
-
// here makes the memo ours before anyone else can move it, so a bundled swap
|
|
2500
|
-
// during the await is an outside change rather than our own side effect.
|
|
2501
|
-
//
|
|
2502
|
-
// The persisted runtime selection is covered by the filesystem evidence above
|
|
2503
|
-
// rather than by a process epoch; see `retainedCatalogProcessEvidence` for why
|
|
2504
|
-
// the in-memory runtime memo cannot be baselined honestly from this path.
|
|
2505
|
-
loadBundledCodexCatalog();
|
|
2506
|
-
const prepared: RetainedCatalogSyncRead = {
|
|
2507
|
-
...preflightRead,
|
|
2508
|
-
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
2509
|
-
processEvidence: retainedCatalogProcessEvidence(),
|
|
2510
|
-
};
|
|
2511
|
-
const [goModels, modelEntitlements] = await Promise.all([
|
|
2512
|
-
gatherRoutedModels(config, {
|
|
2513
|
-
comboOmissions,
|
|
2514
|
-
providerModelOutcomes,
|
|
2515
|
-
}),
|
|
2516
|
-
resolveCodexModelEntitlements(config),
|
|
2517
|
-
]);
|
|
2518
|
-
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
2519
|
-
// Desired state can flip OFF during the provider await above. The catalog
|
|
2520
|
-
// evidence revalidation below cannot see that — intent lives in our config,
|
|
2521
|
-
// not in the catalog files — so the policy is re-read here, under K, right
|
|
2522
|
-
// before the only write. A lost race becomes the discriminated skip instead
|
|
2523
|
-
// of a routed catalog/cache surviving a completed disable. An explicit
|
|
2524
|
-
// catalog-only sync opts out of that gate: the user asked for a refresh even
|
|
2525
|
-
// when injection is OFF, and the toggle only protects config/history writes.
|
|
2526
|
-
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) {
|
|
2527
|
-
return {
|
|
2528
|
-
added: 0,
|
|
2529
|
-
path: prepared.catalogPath,
|
|
2530
|
-
catalogWritten: false,
|
|
2531
|
-
comboOmissions,
|
|
2532
|
-
skippedReason: "desired_disabled" as const,
|
|
2533
|
-
};
|
|
2534
|
-
}
|
|
2535
|
-
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
2536
|
-
if (current === null) return null;
|
|
2537
|
-
if (!isCodexModelEntitlementSnapshotCurrent(modelEntitlements)) return null;
|
|
2538
|
-
return writeRetainedCatalogSync({
|
|
2539
|
-
config,
|
|
2540
|
-
goModels,
|
|
2541
|
-
providerModelOutcomes,
|
|
2542
|
-
comboOmissions,
|
|
2543
|
-
read: current,
|
|
2544
|
-
permit,
|
|
2545
|
-
owningCodexHome,
|
|
2546
|
-
modelEntitlements,
|
|
2547
|
-
});
|
|
2548
|
-
});
|
|
2549
|
-
if (committed.kind === "completed" && committed.value !== null) {
|
|
2550
|
-
return {
|
|
2551
|
-
...committed.value,
|
|
2552
|
-
refreshOutcome: committed.value.skippedReason ? "refused" : "committed",
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
return {
|
|
2556
|
-
added: 0,
|
|
2557
|
-
path: prepared.catalogPath,
|
|
2558
|
-
catalogWritten: false,
|
|
2559
|
-
comboOmissions,
|
|
2560
|
-
refreshOutcome: "refused",
|
|
2561
|
-
};
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
export function restoreCodexCatalogWithPermit(
|
|
2565
|
-
permit: CatalogWritePermit,
|
|
2566
|
-
owningCodexHome: string,
|
|
2567
|
-
/**
|
|
2568
|
-
* The catalog this injection actually wrote, when it is known (#1798).
|
|
2569
|
-
*
|
|
2570
|
-
* Re-resolving from the CURRENT config is wrong after a Codex app rewrite that dropped
|
|
2571
|
-
* `model_catalog_json`: that sends restore to the default catalog while the routed file we
|
|
2572
|
-
* really wrote is left untouched. The recorded path is the file whose routing is ours.
|
|
2573
|
-
*/
|
|
2574
|
-
injectedCatalogPath?: string | null,
|
|
2575
|
-
): { removed: number; kept: number; path: string } {
|
|
2576
|
-
const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
|
|
2577
|
-
const catalog = readCatalog(catalogPath);
|
|
2578
|
-
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
2579
|
-
const disabledModels = currentDisabledModelsForRestore();
|
|
2580
|
-
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
2581
|
-
const backup = readCatalogBackup(catalogPath);
|
|
2582
|
-
if (backup && Array.isArray(backup.models)) {
|
|
2583
|
-
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string"
|
|
2584
|
-
&& (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug))).length;
|
|
2585
|
-
const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
2586
|
-
const userNativeAdditions = restoreAccountHiddenBareNatives(
|
|
2587
|
-
(catalog.models ?? []).filter(m =>
|
|
2588
|
-
typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
|
|
2589
|
-
&& !RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)
|
|
2590
|
-
),
|
|
2591
|
-
replacementVisibility,
|
|
2592
|
-
disabledModels,
|
|
2593
|
-
);
|
|
2594
|
-
const restored = {
|
|
2595
|
-
...backup,
|
|
2596
|
-
// A pristine backup predates retirement; it must not revive withdrawn native rows.
|
|
2597
|
-
models: [...backup.models.filter(m => typeof m.slug !== "string"
|
|
2598
|
-
|| !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
|
|
2599
|
-
};
|
|
2600
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
2601
|
-
path: catalogPath,
|
|
2602
|
-
content: `${JSON.stringify(restored, null, 2)}\n`,
|
|
2603
|
-
});
|
|
2604
|
-
return { removed, kept: restored.models.length, path: catalogPath };
|
|
2605
|
-
}
|
|
2606
|
-
const before = catalog.models.length;
|
|
2607
|
-
const native = restoreAccountHiddenBareNatives(
|
|
2608
|
-
catalog.models.filter(m => !(typeof m.slug === "string"
|
|
2609
|
-
&& (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)))),
|
|
2610
|
-
replacementVisibility,
|
|
2611
|
-
disabledModels,
|
|
2612
|
-
);
|
|
2613
|
-
const removed = before - native.length;
|
|
2614
|
-
if (removed > 0) {
|
|
2615
|
-
catalog.models = native;
|
|
2616
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
2617
|
-
path: catalogPath,
|
|
2618
|
-
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
2619
|
-
});
|
|
2620
|
-
}
|
|
2621
|
-
return { removed, kept: native.length, path: catalogPath };
|
|
2622
|
-
}
|
|
2623
|
-
|
|
2624
|
-
export function restoreCodexCatalog(): { removed: number; kept: number; path: string } {
|
|
2625
|
-
const owningCodexHome = getCodexHome();
|
|
2626
|
-
const outcome = withCatalogWriteSerialization(
|
|
2627
|
-
owningCodexHome,
|
|
2628
|
-
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
2629
|
-
);
|
|
2630
|
-
return outcome.kind === "completed"
|
|
2631
|
-
? outcome.value
|
|
2632
|
-
: { removed: 0, kept: 0, path: readCodexCatalogPath() };
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|
|
2636
|
-
export function invalidateCodexModelsCacheWithPermit(
|
|
2637
|
-
permit: CatalogWritePermit,
|
|
2638
|
-
owningCodexHome: string,
|
|
2639
|
-
options?: CodexCatalogSyncOptions,
|
|
2640
|
-
): boolean {
|
|
2641
|
-
try {
|
|
2642
|
-
// This permit is a REACQUISITION: refreshCodexModelCatalog's commit released
|
|
2643
|
-
// K before this rewrite runs, so the commit-path desired-state check cannot
|
|
2644
|
-
// cover it. A disable landing in that gap must not be overwritten by a
|
|
2645
|
-
// routed cache write — re-read intent under this permit, same as the commit.
|
|
2646
|
-
// The catalog-only sync override applies here too so an explicit refresh
|
|
2647
|
-
// keeps the cache consistent with the catalog it just wrote.
|
|
2648
|
-
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
|
|
2649
|
-
const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
|
|
2650
|
-
if (!existsSync(catalogPath)) return false;
|
|
2651
|
-
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
2652
|
-
const models = catalog.models ?? catalog;
|
|
2653
|
-
const cachePath = join(owningCodexHome, "models_cache.json");
|
|
2654
|
-
const currentCache = readCatalog(cachePath);
|
|
2655
|
-
const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
|
|
2656
|
-
typeof entry.slug === "string" ? [entry.slug] : []));
|
|
2657
|
-
const currentConfig = loadConfig();
|
|
2658
|
-
const mainSelectors = visibleCodexAccountSelectors(currentConfig).filter(selector => {
|
|
2659
|
-
const target = new Map(codexAccountNamespaceEntries(currentConfig)).get(selector);
|
|
2660
|
-
return isMainCodexAccountTarget(target ?? "");
|
|
2661
|
-
});
|
|
2662
|
-
const observedAccountModels = observedAccountBoundNativeEntries(currentCache?.models ?? [])
|
|
2663
|
-
.filter(entry => {
|
|
2664
|
-
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
2665
|
-
return !existingSlugs.has(slug);
|
|
2666
|
-
})
|
|
2667
|
-
.map(entry => ({
|
|
2668
|
-
...entry,
|
|
2669
|
-
// Keep the observation in Codex's cache without advertising a new bare picker row. The
|
|
2670
|
-
// next OpenCodex catalog sync consumes this marker and creates only selector-qualified
|
|
2671
|
-
// rows for the currently configured public account selectors.
|
|
2672
|
-
visibility: "hide",
|
|
2673
|
-
opencodex_account_observed_native: true,
|
|
2674
|
-
opencodex_account_observed_selectors: mainSelectors,
|
|
2675
|
-
}));
|
|
2676
|
-
const wrapper = {
|
|
2677
|
-
fetched_at: "2000-01-01T00:00:00Z",
|
|
2678
|
-
client_version: "0.0.0",
|
|
2679
|
-
models: [...models, ...observedAccountModels],
|
|
2680
|
-
};
|
|
2681
|
-
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
2682
|
-
path: cachePath,
|
|
2683
|
-
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
2684
|
-
});
|
|
2685
|
-
return true;
|
|
2686
|
-
} catch {
|
|
2687
|
-
return false;
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
export function invalidateCodexModelsCache(options?: CodexCatalogSyncOptions): boolean {
|
|
2692
|
-
const owningCodexHome = getCodexHome();
|
|
2693
|
-
const outcome = withCatalogWriteSerialization(
|
|
2694
|
-
owningCodexHome,
|
|
2695
|
-
permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, options),
|
|
2696
|
-
);
|
|
2697
|
-
return outcome.kind === "completed" && outcome.value;
|
|
2698
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
MAX_SPAWN_AGENT_MODEL_OVERRIDES,
|
|
3
|
+
PICKER_ORDER_PRIORITY_BASE,
|
|
4
|
+
SPAWN_PRIORITY_FIELD,
|
|
5
|
+
CATALOG_INACTIVE_REASON_FIELD,
|
|
6
|
+
isEligibleV2SubagentEntry,
|
|
7
|
+
configuredCatalogEntry,
|
|
8
|
+
effectiveSubagentRoster,
|
|
9
|
+
} from "./subagent-roster";
|
|
10
|
+
export type {
|
|
11
|
+
SpawnAgentSurface,
|
|
12
|
+
SubagentRosterExclusionReason,
|
|
13
|
+
EffectiveSubagentModel,
|
|
14
|
+
SubagentRosterExclusion,
|
|
15
|
+
EffectiveSubagentRoster,
|
|
16
|
+
} from "./subagent-roster";
|
|
17
|
+
export { finishUpstreamNativeEntry, isExactComboCatalogModel, deriveEntry } from "./derive-entry";
|
|
18
|
+
export {
|
|
19
|
+
buildCatalogEntries,
|
|
20
|
+
buildCatalogEntriesFromObservedState,
|
|
21
|
+
resetCatalogRuntimeStateForTests,
|
|
22
|
+
orderForSubagents,
|
|
23
|
+
orderForModelPicker,
|
|
24
|
+
mergeCatalogModelsWithNativeRecovery,
|
|
25
|
+
applyFullModelPickerOrder,
|
|
26
|
+
mergeCatalogEntriesFromObservedState,
|
|
27
|
+
mergeCatalogEntriesForSync,
|
|
28
|
+
CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
29
|
+
} from "./build-entries";
|
|
30
|
+
export type {
|
|
31
|
+
ObservedCatalogEntryBuildInput,
|
|
32
|
+
ObservedCatalogMergeInput,
|
|
33
|
+
ObservedCatalogMergePolicy,
|
|
34
|
+
} from "./build-entries";
|
|
35
|
+
export {
|
|
36
|
+
isValidAutoReviewModel,
|
|
37
|
+
applyAutoReviewModelOverride,
|
|
38
|
+
applyConfiguredAutoReviewModelOverride,
|
|
39
|
+
finalizeAutoReviewModelOverride,
|
|
40
|
+
} from "./auto-review";
|
|
41
|
+
export type { AutoReviewModelOverrideResult } from "./auto-review";
|
|
42
|
+
export {
|
|
43
|
+
gatedNativeReauthSuppressionReason,
|
|
44
|
+
resetGatedNativeSuppressionWarningsForTests,
|
|
45
|
+
} from "./gated-native-warn";
|
|
46
|
+
export {
|
|
47
|
+
syncCatalogModels,
|
|
48
|
+
invalidateCodexModelsCache,
|
|
49
|
+
invalidateCodexModelsCacheWithPermit,
|
|
50
|
+
} from "./retained-sync";
|
|
51
|
+
export type { CodexCatalogSyncOptions } from "./retained-sync";
|
|
52
|
+
export { restoreCodexCatalog, restoreCodexCatalogWithPermit } from "./restore";
|