@bitkyc08/opencodex 2.11.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CatalogDisposition, CatalogNotice } from "./convergence-types";
|
|
2
|
+
|
|
3
|
+
const INVALID_CATALOG_DISPOSITION_FIELD = Symbol("invalid-catalog-disposition-field");
|
|
4
|
+
|
|
5
|
+
/** Read an own data property without invoking prototype getters or accessors. */
|
|
6
|
+
function ownDataProperty(value: object, key: PropertyKey): unknown {
|
|
7
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
8
|
+
return descriptor && "value" in descriptor
|
|
9
|
+
? descriptor.value
|
|
10
|
+
: INVALID_CATALOG_DISPOSITION_FIELD;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Accept only the public catalog degradation codes. */
|
|
14
|
+
function isCatalogNotice(value: unknown): value is CatalogNotice {
|
|
15
|
+
return value === "provider-auth" || value === "provider-network" || value === "fallback";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Copy a bounded notice array without trusting accessors or a custom iterator. */
|
|
19
|
+
function normalizeCatalogNotices(value: unknown): CatalogNotice[] | null {
|
|
20
|
+
if (!Array.isArray(value)) return null;
|
|
21
|
+
const length = ownDataProperty(value, "length");
|
|
22
|
+
if (typeof length !== "number" || !Number.isInteger(length) || length < 0 || length > 3) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const notices: CatalogNotice[] = [];
|
|
26
|
+
for (let index = 0; index < length; index += 1) {
|
|
27
|
+
const notice = ownDataProperty(value, String(index));
|
|
28
|
+
if (!isCatalogNotice(notice)) return null;
|
|
29
|
+
notices.push(notice);
|
|
30
|
+
}
|
|
31
|
+
return notices;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Rebuild a catalog disposition from a strict set of own data properties.
|
|
36
|
+
*
|
|
37
|
+
* Management callbacks are internal today, but this response is a privacy
|
|
38
|
+
* boundary. Avoid coercion, accessors, custom iterators, and `toJSON` hooks so a
|
|
39
|
+
* malformed callback cannot smuggle private provider or account detail into a
|
|
40
|
+
* management response.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeCatalogDisposition(value: unknown): CatalogDisposition | null {
|
|
43
|
+
if (value === null || typeof value !== "object") return null;
|
|
44
|
+
try {
|
|
45
|
+
if (Array.isArray(value)) return null;
|
|
46
|
+
const status = ownDataProperty(value, "status");
|
|
47
|
+
if (status === "committed") {
|
|
48
|
+
const changed = ownDataProperty(value, "changed");
|
|
49
|
+
const degraded = ownDataProperty(value, "degraded");
|
|
50
|
+
const notices = normalizeCatalogNotices(ownDataProperty(value, "notices"));
|
|
51
|
+
if (typeof changed !== "boolean" || typeof degraded !== "boolean" || notices === null) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { status, changed, degraded, notices };
|
|
55
|
+
}
|
|
56
|
+
if (status === "skipped") {
|
|
57
|
+
const reason = ownDataProperty(value, "reason");
|
|
58
|
+
const retryable = ownDataProperty(value, "retryable");
|
|
59
|
+
if ((reason !== "not-requested"
|
|
60
|
+
&& reason !== "catalog-unavailable"
|
|
61
|
+
&& reason !== "busy"
|
|
62
|
+
&& reason !== "stale"
|
|
63
|
+
&& reason !== "refused")
|
|
64
|
+
|| typeof retryable !== "boolean") return null;
|
|
65
|
+
return { status, reason, retryable };
|
|
66
|
+
}
|
|
67
|
+
if (status === "failed") {
|
|
68
|
+
const reason = ownDataProperty(value, "reason");
|
|
69
|
+
const phase = ownDataProperty(value, "phase");
|
|
70
|
+
const retryable = ownDataProperty(value, "retryable");
|
|
71
|
+
const partialWrite = ownDataProperty(value, "partialWrite");
|
|
72
|
+
if ((reason !== "provider-auth" && reason !== "provider-network" && reason !== "disk")
|
|
73
|
+
|| (phase !== "gather" && phase !== "commit")
|
|
74
|
+
|| typeof retryable !== "boolean"
|
|
75
|
+
|| typeof partialWrite !== "boolean") return null;
|
|
76
|
+
return { status, reason, phase, retryable, partialWrite };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Whether a persisted mutation still needs a successful catalog commit. */
|
|
85
|
+
export function catalogRefreshIsPending(disposition: CatalogDisposition): boolean {
|
|
86
|
+
return disposition.status !== "committed";
|
|
87
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
CodexUserIdentityRefusal,
|
|
33
33
|
resolveCodexCatalogSerializationDatabasePath,
|
|
34
34
|
resolveEffectiveUserIdentity,
|
|
35
|
+
samePathIdentity,
|
|
35
36
|
} from "./user-identity";
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -180,7 +181,7 @@ export function withCatalogWriteSerialization<T>(
|
|
|
180
181
|
}
|
|
181
182
|
const opened = lstatSync(databasePath);
|
|
182
183
|
if (opened.isSymbolicLink() || !opened.isFile()
|
|
183
|
-
|| realpathSync.native(databasePath)
|
|
184
|
+
|| !samePathIdentity(realpathSync.native(databasePath), databasePath)) {
|
|
184
185
|
return { kind: "unavailable", reason: "unsafe-path" };
|
|
185
186
|
}
|
|
186
187
|
|
package/src/codex/catalog.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
3
|
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
|
-
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
|
|
5
|
+
export { CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
7
7
|
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
|
|
8
|
-
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows,
|
|
8
|
+
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember } from "./catalog/provider-fetch";
|
|
9
9
|
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
|
|
10
10
|
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
|
|
11
|
-
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, effectiveSubagentRoster, buildCatalogEntries, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
|
|
11
|
+
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
|
|
12
|
+
export type { ObservedCatalogMergeInput } from "./catalog/sync";
|
|
12
13
|
export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
|
|
13
14
|
export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";
|
|
@@ -422,7 +422,7 @@ export interface CatalogProviderDiscoveryPolicySnapshot {
|
|
|
422
422
|
readonly path: CatalogDiscoveryPolicyField<string | undefined>;
|
|
423
423
|
readonly query: CatalogDiscoveryPolicyField<Readonly<Record<string, string>> | undefined>;
|
|
424
424
|
}>;
|
|
425
|
-
readonly finalMethod: "GET";
|
|
425
|
+
readonly finalMethod: "GET" | "POST";
|
|
426
426
|
readonly finalUrl: string;
|
|
427
427
|
readonly filter: CatalogDiscoveryPolicyField<ProviderModelDiscoveryFilter | undefined>;
|
|
428
428
|
readonly maxResponseBytes: number;
|
package/src/codex/convergence.ts
CHANGED
|
@@ -2,16 +2,17 @@ import { join } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { getConfigDir, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
|
|
4
4
|
import { COMBO_NAMESPACE } from "../combos";
|
|
5
|
-
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
6
5
|
import { getAuthStorePath } from "../oauth/store";
|
|
7
6
|
import type { OcxConfig } from "../types";
|
|
8
7
|
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
|
|
8
|
+
import { legacyCustomModelCatalogSlugs } from "./custom-model-catalog-migration";
|
|
9
9
|
import {
|
|
10
|
+
type CatalogGatherPathKind,
|
|
10
11
|
type CatalogSourceForGather,
|
|
11
12
|
bundledCatalogCacheState,
|
|
12
13
|
resolveCatalogSourceForGather,
|
|
13
|
-
} from "./catalog/bundled";
|
|
14
|
-
import {
|
|
14
|
+
} from "./catalog/bundled";
|
|
15
|
+
import {
|
|
15
16
|
acceptCatalogGatherSourcePath,
|
|
16
17
|
captureAndSealCatalogHomeSelection,
|
|
17
18
|
captureCatalogGatherTargetIdentity,
|
|
@@ -19,28 +20,49 @@ import {
|
|
|
19
20
|
readCatalogGatherSource,
|
|
20
21
|
sealCatalogGatherEvidenceSession,
|
|
21
22
|
type CatalogFilesystemEvidenceSession,
|
|
22
|
-
} from "./catalog/filesystem-evidence";
|
|
23
|
-
import {
|
|
23
|
+
} from "./catalog/filesystem-evidence";
|
|
24
|
+
import {
|
|
24
25
|
CatalogGatherBusyError,
|
|
25
26
|
createCatalogGatherAuthorityIdentity,
|
|
26
27
|
filterCatalogVisibleModels,
|
|
27
28
|
gatherRoutedModelsForCatalogGather,
|
|
28
29
|
type CatalogGatherProviderAuthOutcome,
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
type CatalogGatherProviderModelOutcome,
|
|
31
|
+
} from "./catalog/provider-fetch";
|
|
32
|
+
import {
|
|
31
33
|
catalogBackupPathFor,
|
|
32
34
|
catalogHasRoutedEntries,
|
|
33
35
|
findNativeTemplate,
|
|
34
36
|
legacyCatalogBackupPath,
|
|
35
37
|
parseCatalogJson,
|
|
36
38
|
type RawCatalog,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
type RawEntry,
|
|
40
|
+
} from "./catalog/parsing";
|
|
41
|
+
import {
|
|
42
|
+
buildCatalogEntriesFromObservedState,
|
|
43
|
+
CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
44
|
+
mergeCatalogEntriesFromObservedState,
|
|
45
|
+
mergeCatalogModelsWithNativeRecovery,
|
|
40
46
|
orderForSubagents,
|
|
41
|
-
} from "./catalog/sync";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
47
|
+
} from "./catalog/sync";
|
|
48
|
+
import { multiAgentV2EnabledFromConfigText } from "./features";
|
|
49
|
+
import { exactComboCatalogSlugs } from "./catalog/aggregation";
|
|
50
|
+
import {
|
|
51
|
+
isNativeAliasCatalogEntry,
|
|
52
|
+
disabledNativeSlugs,
|
|
53
|
+
desktopAllowlistSuppressedNativeSlugs,
|
|
54
|
+
NATIVE_OPENAI_MODELS,
|
|
55
|
+
shouldIncludeAccountBoundNativeOpenAi,
|
|
56
|
+
shouldIncludeNativeOpenAi,
|
|
57
|
+
} from "./catalog/metadata";
|
|
58
|
+
import {
|
|
59
|
+
trustedAccountBoundNativeCatalogSlug,
|
|
60
|
+
visibleCodexAccountSelectors,
|
|
61
|
+
} from "./catalog/account-models";
|
|
62
|
+
import {
|
|
63
|
+
clampCatalogModelsToObservedCodexSupport,
|
|
64
|
+
supportedCodexReasoningEffortsFromObservedCatalog,
|
|
65
|
+
} from "./catalog/effort";
|
|
44
66
|
import { codexRuntimeStatePath, peekCodexRuntimeProcessCache } from "./runtime";
|
|
45
67
|
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
46
68
|
import {
|
|
@@ -119,12 +141,23 @@ function catalogBytes(catalog: RawCatalog): string {
|
|
|
119
141
|
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
120
142
|
}
|
|
121
143
|
|
|
144
|
+
function nativePriorityBaseline(catalog: ReadonlyRawCatalogLike | null): Map<string, number> {
|
|
145
|
+
return new Map((catalog?.models ?? []).flatMap(entry => (
|
|
146
|
+
typeof entry.slug === "string"
|
|
147
|
+
&& !entry.slug.includes("/")
|
|
148
|
+
&& typeof entry.priority === "number"
|
|
149
|
+
? [[entry.slug, entry.priority] as const]
|
|
150
|
+
: []
|
|
151
|
+
)));
|
|
152
|
+
}
|
|
153
|
+
|
|
122
154
|
interface ReadonlyRawCatalogLike {
|
|
123
155
|
readonly models?: readonly Readonly<Record<string, unknown>>[];
|
|
124
156
|
}
|
|
125
157
|
|
|
126
158
|
function hasRoutedEntries(catalog: ReadonlyRawCatalogLike): boolean {
|
|
127
|
-
return (catalog.models ?? []).some(entry => typeof entry.slug === "string"
|
|
159
|
+
return (catalog.models ?? []).some(entry => typeof entry.slug === "string"
|
|
160
|
+
&& (entry.slug.includes("/") || isNativeAliasCatalogEntry(entry as RawEntry)));
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
function processEvidence(source: CatalogSourceForGather): CatalogProcessLocalEvidence {
|
|
@@ -137,7 +170,14 @@ function processEvidence(source: CatalogSourceForGather): CatalogProcessLocalEvi
|
|
|
137
170
|
function bindGatherPaths(
|
|
138
171
|
session: CatalogFilesystemEvidenceSession,
|
|
139
172
|
snapshot: CatalogAdmissionSnapshot,
|
|
140
|
-
): Readonly<{
|
|
173
|
+
): Readonly<{
|
|
174
|
+
catalog: string;
|
|
175
|
+
cache: string;
|
|
176
|
+
keyedBackup: string;
|
|
177
|
+
legacyBackup?: string;
|
|
178
|
+
catalogKind: CatalogGatherPathKind;
|
|
179
|
+
multiAgentV2Enabled: boolean;
|
|
180
|
+
}> {
|
|
141
181
|
const catalog = targetPath(snapshot.targets.catalog);
|
|
142
182
|
const cache = targetPath(snapshot.targets.cache);
|
|
143
183
|
const keyedBackup = targetPath(snapshot.targets.catalogBackups[0]!);
|
|
@@ -146,7 +186,7 @@ function bindGatherPaths(
|
|
|
146
186
|
const configPath = snapshot.sourceEvidence.required["catalog-target-selection"].logicalPath;
|
|
147
187
|
|
|
148
188
|
acceptCatalogGatherSourcePath(session, "catalog-target-selection", configPath);
|
|
149
|
-
readCatalogGatherSource(session, "catalog-target-selection");
|
|
189
|
+
const configBytes = readCatalogGatherSource(session, "catalog-target-selection");
|
|
150
190
|
acceptCatalogGatherSourcePath(session, "active-catalog-merge", catalog);
|
|
151
191
|
acceptCatalogGatherSourcePath(session, "hashed-backup-fallback", keyedBackup);
|
|
152
192
|
acceptCatalogGatherSourcePath(session, "legacy-backup-fallback", legacyBackup ?? legacyCatalogBackupPath());
|
|
@@ -154,7 +194,16 @@ function bindGatherPaths(
|
|
|
154
194
|
acceptCatalogGatherSourcePath(session, "runtime-selection", codexRuntimeStatePath(getConfigDir()));
|
|
155
195
|
acceptCatalogGatherSourcePath(session, "provider-auth-selection", getAuthStorePath());
|
|
156
196
|
acceptCatalogGatherSourcePath(session, "native-catalog-selection", catalog);
|
|
157
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
catalog,
|
|
199
|
+
cache,
|
|
200
|
+
keyedBackup,
|
|
201
|
+
...(legacyBackup ? { legacyBackup } : {}),
|
|
202
|
+
catalogKind: legacyBackup ? "default" : "custom",
|
|
203
|
+
multiAgentV2Enabled: multiAgentV2EnabledFromConfigText(
|
|
204
|
+
configBytes === null ? null : Buffer.from(configBytes).toString("utf8"),
|
|
205
|
+
),
|
|
206
|
+
};
|
|
158
207
|
}
|
|
159
208
|
|
|
160
209
|
function prepareCatalog(
|
|
@@ -162,6 +211,11 @@ function prepareCatalog(
|
|
|
162
211
|
source: Extract<CatalogSourceForGather, { kind: "available" }>,
|
|
163
212
|
active: RawCatalog | null,
|
|
164
213
|
routedModels: Awaited<ReturnType<typeof gatherRoutedModelsForCatalogGather>>,
|
|
214
|
+
multiAgentV2Enabled: boolean,
|
|
215
|
+
baseline: ReadonlyMap<string, number>,
|
|
216
|
+
baselineCatalogModels: readonly Readonly<Record<string, unknown>>[],
|
|
217
|
+
degradedProviderNames: ReadonlySet<string>,
|
|
218
|
+
nativeRecoverySources: readonly (readonly RawEntry[])[] = [],
|
|
165
219
|
): RawCatalog {
|
|
166
220
|
const catalog = JSON.parse(JSON.stringify(source.catalog)) as RawCatalog;
|
|
167
221
|
const template = findNativeTemplate(catalog);
|
|
@@ -171,41 +225,87 @@ function prepareCatalog(
|
|
|
171
225
|
const multiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2"
|
|
172
226
|
? config.multiAgentMode : "default";
|
|
173
227
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
228
|
+
const suppressedBareNativeSlugs = desktopAllowlistSuppressedNativeSlugs(config);
|
|
174
229
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
175
230
|
const enabledProviders = Object.entries(config.providers).filter(([, provider]) => provider.disabled !== true);
|
|
176
|
-
const includeNativeOpenAi =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const disabledNative = disabledNativeSlugs(config);
|
|
180
|
-
const nativeSlugs = includeNativeOpenAi
|
|
181
|
-
? [...new Set((active?.models ?? catalog.models ?? []).flatMap(entry => (
|
|
182
|
-
typeof entry.slug === "string" && !entry.slug.includes("/") && !disabledNative.has(entry.slug)
|
|
183
|
-
? [entry.slug] : []
|
|
184
|
-
)))]
|
|
231
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
232
|
+
const accountSelectors = shouldIncludeAccountBoundNativeOpenAi(config)
|
|
233
|
+
? visibleCodexAccountSelectors(config)
|
|
185
234
|
: [];
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
235
|
+
const disabledNative = disabledNativeSlugs(config);
|
|
236
|
+
const nativeCatalogModels = mergeCatalogModelsWithNativeRecovery(
|
|
237
|
+
active?.models ?? catalog.models ?? [],
|
|
238
|
+
[catalog.models ?? [], ...nativeRecoverySources],
|
|
189
239
|
);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
const catalogModels = nativeCatalogModels;
|
|
241
|
+
const routedEntries = buildCatalogEntriesFromObservedState({
|
|
242
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
243
|
+
gptSlugs: [],
|
|
244
|
+
goModels: ordered,
|
|
245
|
+
featured,
|
|
246
|
+
wsEnabled: websocketsEnabled(config),
|
|
247
|
+
multiAgentMode,
|
|
248
|
+
exactComboSlugs,
|
|
249
|
+
accountSelectors,
|
|
250
|
+
suppressedBareNativeSlugs,
|
|
251
|
+
disabledNativeAccountSlugs: new Set(),
|
|
252
|
+
multiAgentV2Enabled,
|
|
253
|
+
});
|
|
254
|
+
const accountBoundEntries = accountSelectors.length === 0
|
|
255
|
+
? []
|
|
256
|
+
: buildCatalogEntriesFromObservedState({
|
|
257
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
258
|
+
gptSlugs: NATIVE_OPENAI_MODELS,
|
|
259
|
+
goModels: [],
|
|
260
|
+
featured,
|
|
261
|
+
wsEnabled: websocketsEnabled(config),
|
|
262
|
+
multiAgentMode,
|
|
263
|
+
exactComboSlugs,
|
|
264
|
+
accountSelectors,
|
|
265
|
+
suppressedBareNativeSlugs,
|
|
266
|
+
disabledNativeAccountSlugs: new Set([...disabledNative].filter(slug => suppressedBareNativeSlugs.has(slug))),
|
|
267
|
+
multiAgentV2Enabled,
|
|
268
|
+
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined);
|
|
269
|
+
const gatheredProviderNames = new Set(enabledProviders.map(([name]) => name));
|
|
270
|
+
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
271
|
+
enabledProviders.flatMap(([name, provider]) => (
|
|
272
|
+
Array.isArray(provider.selectedModels) && provider.selectedModels.length > 0
|
|
273
|
+
? [[name, new Set(provider.selectedModels)] as const]
|
|
274
|
+
: []
|
|
275
|
+
)),
|
|
276
|
+
);
|
|
277
|
+
const mergedModels = mergeCatalogEntriesFromObservedState({
|
|
278
|
+
catalogModels,
|
|
279
|
+
baselineCatalogModels,
|
|
280
|
+
routedEntries,
|
|
281
|
+
baseline,
|
|
282
|
+
featured,
|
|
283
|
+
wsEnabled: websocketsEnabled(config),
|
|
284
|
+
template,
|
|
285
|
+
disabledModels: new Set(config.disabledModels ?? []),
|
|
286
|
+
selectedModelsByProvider,
|
|
287
|
+
gatheredProviderNames,
|
|
288
|
+
degradedProviderNames,
|
|
289
|
+
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
290
|
+
multiAgentMode,
|
|
291
|
+
multiAgentV2Enabled,
|
|
292
|
+
exactComboSlugs,
|
|
293
|
+
hasPhysicalComboProvider,
|
|
294
|
+
includeNativeOpenAi,
|
|
295
|
+
accountBoundEntries,
|
|
296
|
+
suppressedBareNativeSlugs,
|
|
297
|
+
policy: {
|
|
298
|
+
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
299
|
+
warningPolicy: "suppress",
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
clampCatalogModelsToObservedCodexSupport(
|
|
303
|
+
mergedModels,
|
|
304
|
+
source.runtimeSupport.kind === "available"
|
|
305
|
+
? supportedCodexReasoningEffortsFromObservedCatalog(source.runtimeSupport.catalog)
|
|
306
|
+
: null,
|
|
307
|
+
);
|
|
308
|
+
catalog.models = mergedModels;
|
|
209
309
|
return catalog;
|
|
210
310
|
}
|
|
211
311
|
|
|
@@ -220,7 +320,7 @@ export async function gatherCodexCatalogCandidate(
|
|
|
220
320
|
return { kind: "disposition", disposition: { status: "skipped", reason: "stale", retryable: true } };
|
|
221
321
|
}
|
|
222
322
|
const paths = bindGatherPaths(session, snapshot);
|
|
223
|
-
const source = resolveCatalogSourceForGather(session);
|
|
323
|
+
const source = resolveCatalogSourceForGather(session, paths.catalogKind);
|
|
224
324
|
if (source.kind === "catalog-unavailable") {
|
|
225
325
|
return { kind: "disposition", disposition: { status: "skipped", reason: "catalog-unavailable", retryable: false } };
|
|
226
326
|
}
|
|
@@ -234,10 +334,12 @@ export async function gatherCodexCatalogCandidate(
|
|
|
234
334
|
readCatalogGatherSource(session, "native-catalog-selection");
|
|
235
335
|
}
|
|
236
336
|
const authOutcomes: CatalogGatherProviderAuthOutcome[] = [];
|
|
337
|
+
const providerModelOutcomes: CatalogGatherProviderModelOutcome[] = [];
|
|
237
338
|
const discoveryPolicies: CatalogProviderDiscoveryPolicySnapshot[] = [];
|
|
238
339
|
providerGatherStarted = true;
|
|
239
340
|
const routedModels = await gatherRoutedModelsForCatalogGather(snapshot.config, session, {
|
|
240
341
|
providerAuthOutcomes: authOutcomes,
|
|
342
|
+
providerModelOutcomes,
|
|
241
343
|
discoveryPolicySnapshots: discoveryPolicies,
|
|
242
344
|
});
|
|
243
345
|
const processLocal = processEvidence(source);
|
|
@@ -255,7 +357,34 @@ export async function gatherCodexCatalogCandidate(
|
|
|
255
357
|
}
|
|
256
358
|
|
|
257
359
|
const active = catalogFrom(activeBytes);
|
|
258
|
-
|
|
360
|
+
// Retained sync restores native priorities from the once-only pristine backup. Convergence
|
|
361
|
+
// must use the same admitted evidence instead of treating its already-featured active catalog
|
|
362
|
+
// as the baseline, or feature A -> feature B -> none would retain stale priorities.
|
|
363
|
+
const backupCanAcceptPristineCatalog = keyedBackupBytes === null
|
|
364
|
+
|| (paths.legacyBackup !== undefined && legacyBackupBytes === null);
|
|
365
|
+
const baselineCatalog = catalogFrom(keyedBackupBytes)
|
|
366
|
+
?? catalogFrom(legacyBackupBytes)
|
|
367
|
+
?? (backupCanAcceptPristineCatalog
|
|
368
|
+
? (active && !catalogHasRoutedEntries(active)
|
|
369
|
+
? active
|
|
370
|
+
: !hasRoutedEntries(source.catalog) ? source.catalog : null)
|
|
371
|
+
: null);
|
|
372
|
+
const preparedCatalog = prepareCatalog(
|
|
373
|
+
snapshot.config,
|
|
374
|
+
source,
|
|
375
|
+
active,
|
|
376
|
+
routedModels,
|
|
377
|
+
paths.multiAgentV2Enabled,
|
|
378
|
+
nativePriorityBaseline(baselineCatalog),
|
|
379
|
+
baselineCatalog?.models ?? [],
|
|
380
|
+
new Set(providerModelOutcomes
|
|
381
|
+
.filter(outcome => outcome.state === "degraded")
|
|
382
|
+
.map(outcome => outcome.provider)),
|
|
383
|
+
[
|
|
384
|
+
catalogFrom(keyedBackupBytes)?.models ?? [],
|
|
385
|
+
catalogFrom(legacyBackupBytes)?.models ?? [],
|
|
386
|
+
],
|
|
387
|
+
);
|
|
259
388
|
const preparedCatalogBytes = catalogBytes(preparedCatalog);
|
|
260
389
|
const preparedCacheBytes = `${JSON.stringify({
|
|
261
390
|
fetched_at: "2000-01-01T00:00:00Z",
|
|
@@ -266,8 +395,17 @@ export async function gatherCodexCatalogCandidate(
|
|
|
266
395
|
? Buffer.from(activeBytes!).toString("utf8")
|
|
267
396
|
: !hasRoutedEntries(source.catalog) ? `${JSON.stringify(source.catalog, null, 2)}\n` : null;
|
|
268
397
|
const notices = new Set<CatalogNotice>();
|
|
269
|
-
|
|
270
|
-
|
|
398
|
+
const sourceIsAuthoritative = paths.catalogKind === "default"
|
|
399
|
+
? source.source === "bundled-catalog-template"
|
|
400
|
+
: source.source === "active-catalog-merge";
|
|
401
|
+
if (!sourceIsAuthoritative) notices.add("fallback");
|
|
402
|
+
const authDegradedProviders = new Set(authOutcomes
|
|
403
|
+
.filter(outcome => outcome.state !== "available")
|
|
404
|
+
.map(outcome => outcome.provider));
|
|
405
|
+
if (authDegradedProviders.size > 0) notices.add("provider-auth");
|
|
406
|
+
if (providerModelOutcomes.some(outcome => (
|
|
407
|
+
outcome.state === "degraded" && !authDegradedProviders.has(outcome.provider)
|
|
408
|
+
))) notices.add("provider-network");
|
|
271
409
|
const candidate = {} as CodexCatalogCandidate;
|
|
272
410
|
candidateStates.set(candidate, {
|
|
273
411
|
consumed: false,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { routedSlug, slugEquivalenceKey } from "../providers/slug-codec";
|
|
2
|
+
import type { OcxConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
const MIGRATION_FIELD = "customModelCatalogMigration";
|
|
5
|
+
const MIGRATION_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
type ConfigRecord = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
interface SupportedMigrationState {
|
|
10
|
+
readonly record: ConfigRecord;
|
|
11
|
+
readonly legacyOwnedSlugs: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ParsedMigrationState =
|
|
15
|
+
| { readonly kind: "absent" }
|
|
16
|
+
| { readonly kind: "supported"; readonly state: SupportedMigrationState }
|
|
17
|
+
| { readonly kind: "unsupported"; readonly value: unknown };
|
|
18
|
+
|
|
19
|
+
type CustomModelSlugSet =
|
|
20
|
+
| { readonly kind: "valid"; readonly byKey: ReadonlyMap<string, string> }
|
|
21
|
+
| { readonly kind: "invalid" };
|
|
22
|
+
|
|
23
|
+
function configRecord(value: unknown): ConfigRecord | null {
|
|
24
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
25
|
+
? value as ConfigRecord
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canonicalRoutedSlug(value: unknown): string | null {
|
|
30
|
+
if (typeof value !== "string") return null;
|
|
31
|
+
const slug = value.trim();
|
|
32
|
+
const slash = slug.indexOf("/");
|
|
33
|
+
if (slash <= 0 || slash === slug.length - 1) return null;
|
|
34
|
+
const provider = slug.slice(0, slash).trim();
|
|
35
|
+
const modelId = slug.slice(slash + 1).trim();
|
|
36
|
+
if (!provider || !modelId || provider.includes("/")) return null;
|
|
37
|
+
return routedSlug(provider, modelId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function canonicalSlugMap(values: readonly unknown[]): ReadonlyMap<string, string> | null {
|
|
41
|
+
const byKey = new Map<string, string>();
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
const slug = canonicalRoutedSlug(value);
|
|
44
|
+
if (slug === null) return null;
|
|
45
|
+
byKey.set(slugEquivalenceKey(slug), slug);
|
|
46
|
+
}
|
|
47
|
+
return byKey;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseMigrationState(config: unknown): ParsedMigrationState {
|
|
51
|
+
const record = configRecord(config);
|
|
52
|
+
if (!record || !Object.hasOwn(record, MIGRATION_FIELD)) return { kind: "absent" };
|
|
53
|
+
const value = record[MIGRATION_FIELD];
|
|
54
|
+
const state = configRecord(value);
|
|
55
|
+
if (!state || state.version !== MIGRATION_VERSION || !Array.isArray(state.legacyOwnedSlugs)) {
|
|
56
|
+
return { kind: "unsupported", value };
|
|
57
|
+
}
|
|
58
|
+
const slugs = canonicalSlugMap(state.legacyOwnedSlugs);
|
|
59
|
+
if (slugs === null) return { kind: "unsupported", value };
|
|
60
|
+
return {
|
|
61
|
+
kind: "supported",
|
|
62
|
+
state: {
|
|
63
|
+
record: state,
|
|
64
|
+
legacyOwnedSlugs: [...slugs.values()].sort(),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function customModelSlugs(config: unknown): CustomModelSlugSet {
|
|
70
|
+
const record = configRecord(config);
|
|
71
|
+
if (!record || record.customModels === undefined) {
|
|
72
|
+
return { kind: "valid", byKey: new Map() };
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(record.customModels)) return { kind: "invalid" };
|
|
75
|
+
const byKey = new Map<string, string>();
|
|
76
|
+
for (const value of record.customModels) {
|
|
77
|
+
const custom = configRecord(value);
|
|
78
|
+
if (!custom || typeof custom.provider !== "string" || typeof custom.modelId !== "string") {
|
|
79
|
+
return { kind: "invalid" };
|
|
80
|
+
}
|
|
81
|
+
const provider = custom.provider.trim();
|
|
82
|
+
const modelId = custom.modelId.trim();
|
|
83
|
+
if (!provider || !modelId || provider.includes("/")) return { kind: "invalid" };
|
|
84
|
+
const slug = routedSlug(provider, modelId);
|
|
85
|
+
byKey.set(slugEquivalenceKey(slug), slug);
|
|
86
|
+
}
|
|
87
|
+
return { kind: "valid", byKey };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function copyMigrationField(target: OcxConfig, source: OcxConfig): void {
|
|
91
|
+
const targetRecord = target as unknown as ConfigRecord;
|
|
92
|
+
const sourceRecord = source as unknown as ConfigRecord;
|
|
93
|
+
if (Object.hasOwn(sourceRecord, MIGRATION_FIELD)) {
|
|
94
|
+
targetRecord[MIGRATION_FIELD] = structuredClone(sourceRecord[MIGRATION_FIELD]);
|
|
95
|
+
} else {
|
|
96
|
+
delete targetRecord[MIGRATION_FIELD];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Project one-time legacy custom-model ownership onto a whole-config write.
|
|
102
|
+
*
|
|
103
|
+
* The persisted pre-write config is the only authority for pre-marker ownership.
|
|
104
|
+
* Unknown future/malformed state is carried forward byte-for-value and grants no
|
|
105
|
+
* deletion authority; an older binary must never reinterpret or erase it.
|
|
106
|
+
*/
|
|
107
|
+
export function projectCustomModelCatalogMigration(
|
|
108
|
+
persistedConfig: unknown,
|
|
109
|
+
candidateConfig: OcxConfig,
|
|
110
|
+
): OcxConfig {
|
|
111
|
+
const persistedState = parseMigrationState(persistedConfig);
|
|
112
|
+
const candidateState = parseMigrationState(candidateConfig);
|
|
113
|
+
const projected = { ...candidateConfig } as OcxConfig;
|
|
114
|
+
const projectedRecord = projected as unknown as ConfigRecord;
|
|
115
|
+
|
|
116
|
+
if (persistedState.kind === "unsupported") {
|
|
117
|
+
projectedRecord[MIGRATION_FIELD] = structuredClone(persistedState.value);
|
|
118
|
+
return projected;
|
|
119
|
+
}
|
|
120
|
+
if (persistedState.kind === "absent" && candidateState.kind === "unsupported") {
|
|
121
|
+
projectedRecord[MIGRATION_FIELD] = structuredClone(candidateState.value);
|
|
122
|
+
return projected;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const legacyByKey = new Map<string, string>();
|
|
126
|
+
const addLegacy = (slugs: readonly string[]): void => {
|
|
127
|
+
for (const slug of slugs) legacyByKey.set(slugEquivalenceKey(slug), slug);
|
|
128
|
+
};
|
|
129
|
+
if (persistedState.kind === "supported") addLegacy(persistedState.state.legacyOwnedSlugs);
|
|
130
|
+
if (candidateState.kind === "supported") addLegacy(candidateState.state.legacyOwnedSlugs);
|
|
131
|
+
|
|
132
|
+
const persistedCustom = customModelSlugs(persistedConfig);
|
|
133
|
+
const candidateCustom = customModelSlugs(candidateConfig);
|
|
134
|
+
// Only an absent persisted version means these rows can predate the ownership marker.
|
|
135
|
+
// Never add candidate-only or post-version models: those are born with custom-model-v1.
|
|
136
|
+
if (persistedState.kind === "absent" && persistedCustom.kind === "valid") {
|
|
137
|
+
for (const [key, slug] of persistedCustom.byKey) legacyByKey.set(key, slug);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Establish an empty v1 on the first custom-model add too: the cutover, not list
|
|
141
|
+
// non-emptiness, proves that candidate-only models can never be inferred as legacy.
|
|
142
|
+
// Unrelated saves for users who never configured custom models remain untouched.
|
|
143
|
+
const shouldVersion = persistedState.kind === "supported"
|
|
144
|
+
|| candidateState.kind === "supported"
|
|
145
|
+
|| (persistedState.kind === "absent"
|
|
146
|
+
&& ((persistedCustom.kind === "valid" && persistedCustom.byKey.size > 0)
|
|
147
|
+
|| (candidateCustom.kind === "valid" && candidateCustom.byKey.size > 0)));
|
|
148
|
+
if (!shouldVersion) {
|
|
149
|
+
delete projectedRecord[MIGRATION_FIELD];
|
|
150
|
+
return projected;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const priorRecord = persistedState.kind === "supported" ? persistedState.state.record : {};
|
|
154
|
+
const nextRecord = candidateState.kind === "supported" ? candidateState.state.record : {};
|
|
155
|
+
projectedRecord[MIGRATION_FIELD] = {
|
|
156
|
+
...structuredClone(priorRecord),
|
|
157
|
+
...structuredClone(nextRecord),
|
|
158
|
+
version: MIGRATION_VERSION,
|
|
159
|
+
legacyOwnedSlugs: [...legacyByKey.values()].sort(),
|
|
160
|
+
};
|
|
161
|
+
return projected;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Copy only the internal migration state after the projected write succeeds. */
|
|
165
|
+
export function adoptCustomModelCatalogMigration(
|
|
166
|
+
target: OcxConfig,
|
|
167
|
+
projected: OcxConfig,
|
|
168
|
+
): void {
|
|
169
|
+
copyMigrationField(target, projected);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Canonical slugs that may classify old, unmarked OpenCodex custom rows. */
|
|
173
|
+
export function legacyCustomModelCatalogSlugs(config: OcxConfig): ReadonlySet<string> {
|
|
174
|
+
const state = parseMigrationState(config);
|
|
175
|
+
return new Set(state.kind === "supported" ? state.state.legacyOwnedSlugs : []);
|
|
176
|
+
}
|