@bitkyc08/opencodex 2.10.0 → 2.10.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/AGENTS_INSTALL.md +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
5
|
+
import { expandUserPath, readConfigDiagnostics, websocketsEnabled } from "../../config";
|
|
6
|
+
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, getCodexHome, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
7
|
import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
|
|
8
8
|
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
9
9
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
@@ -14,10 +14,9 @@ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../..
|
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
15
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
16
16
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
|
-
import {
|
|
17
|
+
import { identifyRoutedModel } from "../../adapters/identity";
|
|
18
18
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
19
19
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
20
|
-
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
21
20
|
import {
|
|
22
21
|
COMBO_NAMESPACE,
|
|
23
22
|
comboModelId,
|
|
@@ -31,15 +30,31 @@ import { redactSecretString } from "../../lib/redact";
|
|
|
31
30
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
import { activeCodexModelsCachePath, applyJawcodeCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride,
|
|
35
|
-
import type { CatalogModel, MultiAgentMode, RawEntry } from "./parsing";
|
|
36
|
-
import { applyNativeVisibility, disabledNativeSlugs, isUnsupportedOpenAiNativeSlug, nativeOpenAiSlugs, shouldUpgradeToUpstreamEntry, upstreamNativeEntry } from "./metadata";
|
|
37
|
-
import {
|
|
33
|
+
import { activeCodexModelsCachePath, applyJawcodeCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
|
|
34
|
+
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
35
|
+
import { applyNativeVisibility, disabledNativeSlugs, isUnsupportedOpenAiNativeSlug, nativeOpenAiSlugs, NATIVE_OPENAI_MODELS, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry } from "./metadata";
|
|
36
|
+
import {
|
|
37
|
+
bundledCatalogCacheState,
|
|
38
|
+
loadBundledCodexCatalog,
|
|
39
|
+
resetBundledCatalogCacheForTests,
|
|
40
|
+
} from "./bundled";
|
|
38
41
|
import { isMultiAgentV2Enabled } from "../features";
|
|
39
42
|
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
40
43
|
import { clearGatherRoutedModelsInflight, filterCatalogVisibleModels, gatherRoutedModels, lastDropWarnSignature } from "./provider-fetch";
|
|
41
|
-
import { clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnComboMasqueradeCollisionOnce } from "./aggregation";
|
|
44
|
+
import { accountSelectorShadowCollisionWarnings, clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnAccountSelectorShadowedProviderOnce, warnComboMasqueradeCollisionOnce } from "./aggregation";
|
|
42
45
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
46
|
+
import {
|
|
47
|
+
withCatalogWriteSerialization,
|
|
48
|
+
type CatalogWritePermit,
|
|
49
|
+
} from "../catalog-write-serialization";
|
|
50
|
+
import {
|
|
51
|
+
publishHashedCodexCatalogBackup,
|
|
52
|
+
publishLegacyCodexCatalogBackup,
|
|
53
|
+
replaceActiveCodexCatalog,
|
|
54
|
+
replaceCodexModelsCache,
|
|
55
|
+
} from "../internal/catalog-writer";
|
|
56
|
+
import { codexRuntimeStatePath } from "../runtime";
|
|
57
|
+
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
43
58
|
|
|
44
59
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
45
60
|
|
|
@@ -88,21 +103,32 @@ export interface EffectiveSubagentRoster {
|
|
|
88
103
|
excluded: SubagentRosterExclusion[];
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
export function configuredCatalogEntry(entries: RawEntry[], configured: string): RawEntry | undefined {
|
|
106
|
+
export function configuredCatalogEntry(entries: readonly RawEntry[], configured: string): RawEntry | undefined {
|
|
92
107
|
return entries.find(entry => entry.slug === configured)
|
|
93
108
|
?? entries.find(entry => typeof entry.slug === "string" && slugsEquivalent(configured, entry.slug));
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
function configuredSubagentModelMatchesEntry(configured: string, entry: RawEntry): boolean {
|
|
112
|
+
if (typeof entry.slug !== "string") return false;
|
|
113
|
+
if (slugsEquivalent(configured, entry.slug)) return true;
|
|
114
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
115
|
+
return !configured.includes("/")
|
|
116
|
+
&& nativeSlug !== undefined
|
|
117
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)
|
|
118
|
+
&& slugsEquivalent(configured, nativeSlug);
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
export function effectiveSubagentRoster(
|
|
97
122
|
configuredModels: readonly string[],
|
|
98
123
|
surface: SpawnAgentSurface,
|
|
124
|
+
catalogEntries?: readonly RawEntry[],
|
|
99
125
|
): EffectiveSubagentRoster {
|
|
100
126
|
const configured = configuredModels
|
|
101
127
|
.filter(model => model.trim().length > 0)
|
|
102
128
|
.filter((model, index, all) =>
|
|
103
129
|
!all.slice(0, index).some(previous => slugsEquivalent(previous, model))
|
|
104
130
|
);
|
|
105
|
-
const entries = readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
131
|
+
const entries = catalogEntries ?? readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
106
132
|
const ordered = entries
|
|
107
133
|
.map((entry, index) => ({ entry, index }))
|
|
108
134
|
.filter(({ entry }) => typeof entry.slug === "string")
|
|
@@ -116,28 +142,43 @@ export function effectiveSubagentRoster(
|
|
|
116
142
|
return leftPriority - rightPriority || left.index - right.index;
|
|
117
143
|
})
|
|
118
144
|
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
145
|
+
const orderedEntries = new Set(ordered.map(({ entry }) => entry));
|
|
119
146
|
|
|
120
147
|
const candidates = ordered.map(({ entry }) => ({
|
|
121
148
|
model: entry.slug as string,
|
|
122
149
|
efforts: catalogEntryEfforts(entry),
|
|
123
150
|
}));
|
|
124
|
-
const advertised =
|
|
125
|
-
configured.some(model =>
|
|
126
|
-
|
|
151
|
+
const advertised = ordered
|
|
152
|
+
.filter(({ entry }) => configured.some(model => configuredSubagentModelMatchesEntry(model, entry)))
|
|
153
|
+
.map(({ entry }) => ({
|
|
154
|
+
model: entry.slug as string,
|
|
155
|
+
efforts: catalogEntryEfforts(entry),
|
|
156
|
+
}));
|
|
127
157
|
const excluded = configured.flatMap((model): SubagentRosterExclusion[] => {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
const matchingEntries = entries.filter(entry => configuredSubagentModelMatchesEntry(model, entry));
|
|
159
|
+
if (matchingEntries.some(entry => orderedEntries.has(entry))) return [];
|
|
160
|
+
if (matchingEntries.length === 0) return [{ configured: model, reason: "missing_catalog_entry" }];
|
|
161
|
+
const visibleCompatible = matchingEntries.find(entry =>
|
|
162
|
+
entry.visibility === "list"
|
|
163
|
+
&& (surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
164
|
+
);
|
|
165
|
+
if (visibleCompatible) {
|
|
166
|
+
return [{
|
|
167
|
+
configured: model,
|
|
168
|
+
catalogModel: visibleCompatible.slug as string,
|
|
169
|
+
reason: "outside_display_limit",
|
|
170
|
+
}];
|
|
136
171
|
}
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
const visible = matchingEntries.find(entry => entry.visibility === "list");
|
|
173
|
+
if (visible) {
|
|
174
|
+
return [{
|
|
175
|
+
configured: model,
|
|
176
|
+
catalogModel: visible.slug as string,
|
|
177
|
+
reason: "surface_incompatible",
|
|
178
|
+
}];
|
|
139
179
|
}
|
|
140
|
-
|
|
180
|
+
const hidden = configuredCatalogEntry(entries, model) ?? matchingEntries[0]!;
|
|
181
|
+
return [{ configured: model, catalogModel: hidden.slug as string, reason: "picker_hidden" }];
|
|
141
182
|
});
|
|
142
183
|
return { candidates, advertised, excluded };
|
|
143
184
|
}
|
|
@@ -188,16 +229,20 @@ export function deriveEntry(
|
|
|
188
229
|
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
|
|
189
230
|
// and advertise the reasoning ladder Codex accepts.
|
|
190
231
|
if (isRouted) {
|
|
232
|
+
// A routed model is NOT the native template: never inherit its context
|
|
233
|
+
// window when /models omits context metadata (#992). Known metadata
|
|
234
|
+
// restores exact values below; otherwise the strict-fields fallback
|
|
235
|
+
// supplies the conservative 128k triple.
|
|
236
|
+
delete e.context_window;
|
|
237
|
+
delete e.max_context_window;
|
|
238
|
+
delete e.auto_compact_token_limit;
|
|
191
239
|
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
192
240
|
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
193
241
|
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
194
242
|
if (typeof e.base_instructions === "string") {
|
|
195
243
|
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
|
|
196
244
|
// (leaking that into base_instructions is a non-first-party signature → ToS risk).
|
|
197
|
-
e.base_instructions = e.base_instructions
|
|
198
|
-
CODEX_GPT5_IDENTITY_LINE,
|
|
199
|
-
`You are a coding agent powered by the ${modelName} model. Do not claim to be GPT-5 or made by OpenAI.`,
|
|
200
|
-
);
|
|
245
|
+
e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
|
|
201
246
|
}
|
|
202
247
|
applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
203
248
|
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
|
|
@@ -255,13 +300,16 @@ export function buildCatalogEntries(
|
|
|
255
300
|
wsEnabled = false,
|
|
256
301
|
multiAgentMode: MultiAgentMode = "default",
|
|
257
302
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
303
|
+
accountSelectors: readonly string[] = [],
|
|
258
304
|
): RawEntry[] {
|
|
259
305
|
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
260
306
|
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
261
307
|
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
262
308
|
// it sorts to the front. This works for native gpt slugs AND routed slugs alike.
|
|
263
309
|
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
310
|
+
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
264
311
|
const out: RawEntry[] = [];
|
|
312
|
+
const nativeEntries: RawEntry[] = [];
|
|
265
313
|
const collisionSkipped = resolveSlugAliasCollisions(goModels);
|
|
266
314
|
const comboPublicSlugs = new Set(goModels
|
|
267
315
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
@@ -270,6 +318,26 @@ export function buildCatalogEntries(
|
|
|
270
318
|
const e = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
|
|
271
319
|
if (rank.has(slug)) e.priority = rank.get(slug)!;
|
|
272
320
|
out.push(e);
|
|
321
|
+
nativeEntries.push(e);
|
|
322
|
+
}
|
|
323
|
+
for (const [selectorIndex, selector] of accountSelectors.entries()) {
|
|
324
|
+
for (const [nativeIndex, native] of nativeEntries.entries()) {
|
|
325
|
+
const e = JSON.parse(JSON.stringify(native)) as RawEntry;
|
|
326
|
+
const nativeSlug = String(native.slug);
|
|
327
|
+
const catalogSlug = `${selector}/${nativeSlug}`;
|
|
328
|
+
e.slug = catalogSlug;
|
|
329
|
+
e.display_name = accountBoundNativeDisplayName(selector, native);
|
|
330
|
+
// Codex ignores this OpenCodex extension; preserve the native comp_hash unchanged.
|
|
331
|
+
e.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
332
|
+
const exactRank = rank.get(catalogSlug);
|
|
333
|
+
const inheritedRank = rank.get(nativeSlug);
|
|
334
|
+
const featuredRank = exactRank ?? inheritedRank;
|
|
335
|
+
e.priority = featuredRank !== undefined
|
|
336
|
+
? featuredRank * priorityStride + selectorIndex
|
|
337
|
+
: ((featured?.length ?? 0) + nativeIndex) * accountSelectors.length + selectorIndex;
|
|
338
|
+
e.visibility = "list";
|
|
339
|
+
out.push(e);
|
|
340
|
+
}
|
|
273
341
|
}
|
|
274
342
|
for (const m of goModels) {
|
|
275
343
|
if (collisionSkipped.has(m)) continue;
|
|
@@ -290,7 +358,11 @@ export function buildCatalogEntries(
|
|
|
290
358
|
);
|
|
291
359
|
// Featured picks may be stored raw (legacy) or encoded — honor both.
|
|
292
360
|
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
|
|
293
|
-
if (rankHit !== undefined) e.priority = rankHit;
|
|
361
|
+
if (rankHit !== undefined) e.priority = rankHit * priorityStride;
|
|
362
|
+
else if (accountSelectors.length > 0) {
|
|
363
|
+
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
364
|
+
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
365
|
+
}
|
|
294
366
|
out.push(e);
|
|
295
367
|
}
|
|
296
368
|
// Central capability override (phase 120.4): the advertised flag must match the implemented WS
|
|
@@ -315,6 +387,7 @@ export function resetCatalogRuntimeStateForTests(): void {
|
|
|
315
387
|
comboCatalogWarningSignatures.clear();
|
|
316
388
|
slugAliasCollisionWarnings.clear();
|
|
317
389
|
comboMasqueradeCollisionWarnings.clear();
|
|
390
|
+
accountSelectorShadowCollisionWarnings.clear();
|
|
318
391
|
clearLastComboCatalogOmissions();
|
|
319
392
|
clearModelCache();
|
|
320
393
|
clearGatherRoutedModelsInflight();
|
|
@@ -357,7 +430,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
357
430
|
wsEnabled: boolean,
|
|
358
431
|
goIds: Set<string> = new Set(),
|
|
359
432
|
template: RawEntry | null = null,
|
|
360
|
-
|
|
433
|
+
disabledModels: ReadonlySet<string> = new Set(),
|
|
361
434
|
gatheredProviderNames: Set<string> = new Set(routedEntries.flatMap(entry => {
|
|
362
435
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
363
436
|
const slash = slug.indexOf("/");
|
|
@@ -367,6 +440,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
367
440
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
368
441
|
hasPhysicalComboProvider = false,
|
|
369
442
|
includeNativeOpenAi = true,
|
|
443
|
+
accountBoundEntries: readonly RawEntry[] = [],
|
|
370
444
|
): RawEntry[] {
|
|
371
445
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
372
446
|
const native = includeNativeOpenAi
|
|
@@ -426,24 +500,45 @@ export function mergeCatalogEntriesForSync(
|
|
|
426
500
|
}
|
|
427
501
|
}
|
|
428
502
|
|
|
503
|
+
const nativeBySlug = new Map(native.flatMap(entry =>
|
|
504
|
+
typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
|
|
505
|
+
));
|
|
506
|
+
const alignedAccountBoundEntries = accountBoundEntries.map(entry => {
|
|
507
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
508
|
+
const source = nativeSlug === undefined ? undefined : nativeBySlug.get(nativeSlug);
|
|
509
|
+
if (!source) return entry;
|
|
510
|
+
const aligned = JSON.parse(JSON.stringify(source)) as RawEntry;
|
|
511
|
+
aligned.slug = entry.slug;
|
|
512
|
+
aligned.display_name = entry.display_name;
|
|
513
|
+
aligned.priority = entry.priority;
|
|
514
|
+
aligned.visibility = "list";
|
|
515
|
+
aligned.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
516
|
+
return aligned;
|
|
517
|
+
});
|
|
518
|
+
|
|
429
519
|
const freshSlugs = new Set(
|
|
430
520
|
routedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
431
521
|
);
|
|
432
522
|
let finalRoutedEntries = routedEntries;
|
|
523
|
+
const existingRoutedEntries = catalogModels.filter(m =>
|
|
524
|
+
typeof m.slug === "string"
|
|
525
|
+
&& m.slug.includes("/")
|
|
526
|
+
&& trustedAccountBoundNativeCatalogSlug(m) === undefined
|
|
527
|
+
);
|
|
433
528
|
const preservingExistingRouted = routedEntries.length === 0
|
|
434
|
-
&&
|
|
529
|
+
&& existingRoutedEntries.length > 0;
|
|
435
530
|
if (preservingExistingRouted) {
|
|
436
531
|
// #855: transient-fetch protection keeps existing rows, but rows OpenCodex
|
|
437
532
|
// itself authored for a provider that is no longer configured are ghosts,
|
|
438
533
|
// not protected foreign entries.
|
|
439
|
-
finalRoutedEntries =
|
|
440
|
-
if (typeof m.slug !== "string" || !(m.slug as string).includes("/")) return false;
|
|
534
|
+
finalRoutedEntries = existingRoutedEntries.filter(m => {
|
|
441
535
|
const provider = (m.slug as string).slice(0, (m.slug as string).indexOf("/"));
|
|
442
536
|
return !(isOcxAuthoredRoutedEntry(m) && !gatheredProviderNames.has(provider));
|
|
443
537
|
});
|
|
444
538
|
} else {
|
|
445
539
|
const preservedForeignRouted = catalogModels.filter(m => {
|
|
446
540
|
if (typeof m.slug !== "string" || !m.slug.includes("/")) return false;
|
|
541
|
+
if (trustedAccountBoundNativeCatalogSlug(m) !== undefined) return false;
|
|
447
542
|
const provider = m.slug.slice(0, m.slug.indexOf("/"));
|
|
448
543
|
if (gatheredProviderNames.has(provider) || freshSlugs.has(m.slug)) return false;
|
|
449
544
|
// #855: an OpenCodex-authored row whose provider was deleted is a ghost;
|
|
@@ -469,11 +564,20 @@ export function mergeCatalogEntriesForSync(
|
|
|
469
564
|
finalRoutedEntries = finalRoutedEntries.filter(entry =>
|
|
470
565
|
typeof entry.slug !== "string" || !isRoutedModelCompatibilityExcluded(entry.slug)
|
|
471
566
|
);
|
|
567
|
+
const accountBoundSlugs = new Set(alignedAccountBoundEntries.flatMap(entry =>
|
|
568
|
+
typeof entry.slug === "string" ? [entry.slug] : []
|
|
569
|
+
));
|
|
570
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
571
|
+
if (typeof entry.slug !== "string" || !accountBoundSlugs.has(entry.slug)) return true;
|
|
572
|
+
if (freshSlugs.has(entry.slug)) warnAccountSelectorShadowedProviderOnce(entry.slug);
|
|
573
|
+
return false;
|
|
574
|
+
});
|
|
472
575
|
if (preservingExistingRouted) {
|
|
473
576
|
console.warn(`[opencodex] catalog sync: routed model fetch returned empty; preserving ${finalRoutedEntries.length} existing routed entr${finalRoutedEntries.length === 1 ? "y" : "ies"} on disk.`);
|
|
474
577
|
}
|
|
475
578
|
|
|
476
|
-
const
|
|
579
|
+
const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
|
|
580
|
+
const mergedEntries = [...native, ...managedEntries].map(m => {
|
|
477
581
|
const normalized = normalizeServiceTiers(m);
|
|
478
582
|
applyNativeOpenAiContextOverride(normalized);
|
|
479
583
|
const exactCombo = typeof m.slug === "string" && exactComboSlugs.has(m.slug);
|
|
@@ -502,35 +606,186 @@ export function mergeCatalogEntriesForSync(
|
|
|
502
606
|
}
|
|
503
607
|
return e;
|
|
504
608
|
});
|
|
505
|
-
// Native enable/disable
|
|
506
|
-
//
|
|
507
|
-
|
|
609
|
+
// Native enable/disable runs as the LAST pass so the upstream-upgrade branch above can never
|
|
610
|
+
// clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
|
|
611
|
+
// only their generated account row.
|
|
612
|
+
return applyMultiAgentMode(
|
|
613
|
+
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0),
|
|
614
|
+
multiAgentMode,
|
|
615
|
+
isMultiAgentV2Enabled(),
|
|
616
|
+
);
|
|
508
617
|
}
|
|
509
618
|
|
|
510
|
-
|
|
619
|
+
interface RetainedCatalogSyncRead {
|
|
620
|
+
readonly catalogPath: string;
|
|
621
|
+
readonly catalog: RawCatalog;
|
|
622
|
+
readonly onDiskCatalog: RawCatalog | null;
|
|
623
|
+
readonly evidence: string;
|
|
624
|
+
/**
|
|
625
|
+
* Process-local epochs, baselined AFTER our own gather rather than with the
|
|
626
|
+
* filesystem bytes above. See `retainedCatalogProcessEvidence`.
|
|
627
|
+
*/
|
|
628
|
+
readonly processEvidence: string;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
interface RetainedCatalogSyncResult {
|
|
511
632
|
added: number;
|
|
512
633
|
path: string;
|
|
513
634
|
catalogWritten: boolean;
|
|
514
635
|
comboOmissions: ComboCatalogOmission[];
|
|
515
|
-
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
interface RetainedCatalogSyncWrite {
|
|
639
|
+
readonly config: OcxConfig;
|
|
640
|
+
readonly goModels: CatalogModel[];
|
|
641
|
+
readonly comboOmissions: ComboCatalogOmission[];
|
|
642
|
+
readonly read: RetainedCatalogSyncRead;
|
|
643
|
+
readonly permit: CatalogWritePermit;
|
|
644
|
+
readonly owningCodexHome: string;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function optionalFileBytes(path: string): string | null {
|
|
648
|
+
try {
|
|
649
|
+
return readFileSync(path).toString("base64");
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") return null;
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function loadCatalogForRetainedSync(path: string): RawCatalog | null {
|
|
657
|
+
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
658
|
+
if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
|
|
659
|
+
const active = readCatalog(path);
|
|
660
|
+
if (active && findNativeTemplate(active)) return active;
|
|
661
|
+
return readCatalog(catalogBackupPathFor(path))
|
|
662
|
+
?? (isDefaultCatalogPath(path) ? readCatalog(legacyCatalogBackupPath()) : null)
|
|
663
|
+
?? readCatalog(activeCodexModelsCachePath())
|
|
664
|
+
?? active;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function retainedCatalogSyncEvidence(
|
|
668
|
+
config: OcxConfig,
|
|
669
|
+
catalogPath: string,
|
|
670
|
+
catalog: RawCatalog,
|
|
671
|
+
): string {
|
|
672
|
+
return JSON.stringify({
|
|
673
|
+
config,
|
|
674
|
+
catalogPath,
|
|
675
|
+
catalog,
|
|
676
|
+
catalogBytes: optionalFileBytes(catalogPath),
|
|
677
|
+
hashedBackupBytes: optionalFileBytes(catalogBackupPathFor(catalogPath)),
|
|
678
|
+
legacyBackupBytes: isDefaultCatalogPath(catalogPath)
|
|
679
|
+
? optionalFileBytes(legacyCatalogBackupPath()) : null,
|
|
680
|
+
modelsCacheBytes: optionalFileBytes(activeCodexModelsCachePath()),
|
|
681
|
+
// The persisted runtime selection is a pre-await filesystem input, not a
|
|
682
|
+
// process epoch: another PROCESS can move runtime authority by rewriting this
|
|
683
|
+
// file, and that move is invisible to our in-process memo. Recorded PRESENT or
|
|
684
|
+
// ABSENT, because its absence is what makes the resolver fall back.
|
|
685
|
+
runtimeStateBytes: optionalFileBytes(codexRuntimeStatePath()),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The bundled-template half of the same evidence, observed separately.
|
|
691
|
+
*
|
|
692
|
+
* The runtime process memo is deliberately NOT here, and that exclusion took three
|
|
693
|
+
* attempts to get honest. Gathering resolves the Codex runtime lazily and under its
|
|
694
|
+
* own cache key, so this path cannot pre-settle that memo: baselining it before the
|
|
695
|
+
* await always detected our own side effect and refused every write, and baselining
|
|
696
|
+
* it after the await captured a runtime that ANOTHER process had moved as though it
|
|
697
|
+
* were ours — a catalog prepared from R1 committing after authority reached R2.
|
|
698
|
+
*
|
|
699
|
+
* Runtime authority is covered where it is actually durable instead: the persisted
|
|
700
|
+
* `codex-runtime.json` bytes sit in the pre-await filesystem evidence, PRESENT or
|
|
701
|
+
* ABSENT, so a cross-process runtime move is caught. What is left uncovered, and is
|
|
702
|
+
* written down rather than papered over, is a same-process in-memory runtime swap
|
|
703
|
+
* that never touches that file — WP11 owns the lock that makes that case decidable.
|
|
704
|
+
*/
|
|
705
|
+
function retainedCatalogProcessEvidence(): string {
|
|
706
|
+
return JSON.stringify({
|
|
707
|
+
bundledCatalogCache: bundledCatalogCacheState(),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Capture every local catalog input the retained sync path consults before its
|
|
713
|
+
* provider await. The exact evidence is compared after K acquisition; a newer
|
|
714
|
+
* catalog/backup/cache or target selection makes this attempt a no-write.
|
|
715
|
+
*/
|
|
716
|
+
function readRetainedCatalogSync(config: OcxConfig): RetainedCatalogSyncRead | null {
|
|
516
717
|
const catalogPath = readCodexCatalogPath();
|
|
517
|
-
const catalog =
|
|
518
|
-
if (!catalog) return
|
|
718
|
+
const catalog = loadCatalogForRetainedSync(catalogPath);
|
|
719
|
+
if (!catalog) return null;
|
|
519
720
|
|
|
520
721
|
// The bundled catalog is a reliable native template on the default path, but it is not the
|
|
521
722
|
// merge source. Preservation must inspect the file that this sync is about to overwrite;
|
|
522
723
|
// otherwise an empty/partial provider gather cannot see routed or user-native rows on disk.
|
|
523
724
|
const onDiskCatalog = readCatalog(catalogPath);
|
|
524
|
-
const
|
|
725
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, catalog);
|
|
726
|
+
// `processEvidence` is filled in after the provider await, not here.
|
|
727
|
+
return { catalogPath, catalog, onDiskCatalog, evidence, processEvidence: "" };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function revalidateRetainedCatalogSync(
|
|
731
|
+
config: OcxConfig,
|
|
732
|
+
prepared: RetainedCatalogSyncRead,
|
|
733
|
+
): RetainedCatalogSyncRead | null {
|
|
734
|
+
const catalogPath = readCodexCatalogPath();
|
|
735
|
+
if (catalogPath !== prepared.catalogPath) return null;
|
|
736
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, prepared.catalog);
|
|
737
|
+
if (evidence !== prepared.evidence) return null;
|
|
738
|
+
if (retainedCatalogProcessEvidence() !== prepared.processEvidence) return null;
|
|
739
|
+
return {
|
|
740
|
+
catalogPath,
|
|
741
|
+
catalog: JSON.parse(JSON.stringify(prepared.catalog)) as RawCatalog,
|
|
742
|
+
onDiskCatalog: readCatalog(catalogPath),
|
|
743
|
+
evidence,
|
|
744
|
+
processEvidence: prepared.processEvidence,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
525
747
|
|
|
748
|
+
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
749
|
+
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
750
|
+
try {
|
|
751
|
+
return readFileSync(read.catalogPath, "utf8");
|
|
752
|
+
} catch {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return catalogHasRoutedEntries(read.catalog)
|
|
757
|
+
? null
|
|
758
|
+
: `${JSON.stringify(read.catalog, null, 2)}\n`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function writeRetainedCatalogSync({
|
|
762
|
+
config,
|
|
763
|
+
goModels,
|
|
764
|
+
comboOmissions,
|
|
765
|
+
read,
|
|
766
|
+
permit,
|
|
767
|
+
owningCodexHome,
|
|
768
|
+
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
769
|
+
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
770
|
+
const catalogModelsForMerge = onDiskCatalog?.models ?? catalog.models ?? [];
|
|
526
771
|
const template = findNativeTemplate(catalog);
|
|
527
772
|
|
|
528
|
-
const comboOmissions: ComboCatalogOmission[] = [];
|
|
529
|
-
const goModels = await gatherRoutedModels(config, { comboOmissions });
|
|
530
773
|
try {
|
|
531
774
|
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
|
532
775
|
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
533
|
-
|
|
776
|
+
const pristine = pristineCatalogBytes(read);
|
|
777
|
+
if (pristine !== null) {
|
|
778
|
+
publishHashedCodexCatalogBackup(permit, owningCodexHome, {
|
|
779
|
+
path: catalogBackupPathFor(catalogPath),
|
|
780
|
+
content: pristine,
|
|
781
|
+
});
|
|
782
|
+
if (isDefaultCatalogPath(catalogPath)) {
|
|
783
|
+
publishLegacyCodexCatalogBackup(permit, owningCodexHome, {
|
|
784
|
+
path: legacyCatalogBackupPath(),
|
|
785
|
+
content: pristine,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
534
789
|
} catch { /* backup best-effort */ }
|
|
535
790
|
|
|
536
791
|
// Hide disabled models from Codex, then feature the chosen subagent models (native OR routed)
|
|
@@ -541,7 +796,22 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
|
|
|
541
796
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
542
797
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
543
798
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
544
|
-
const
|
|
799
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
800
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
801
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
802
|
+
? visibleCodexAccountSelectors(config)
|
|
803
|
+
: [];
|
|
804
|
+
const wsEnabled = websocketsEnabled(config);
|
|
805
|
+
const goEntries = buildCatalogEntries(
|
|
806
|
+
template ? JSON.parse(JSON.stringify(template)) : null,
|
|
807
|
+
[],
|
|
808
|
+
orderedGoModels,
|
|
809
|
+
featured,
|
|
810
|
+
wsEnabled,
|
|
811
|
+
multiAgentMode,
|
|
812
|
+
exactComboSlugs,
|
|
813
|
+
accountSelectors,
|
|
814
|
+
);
|
|
545
815
|
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
|
|
546
816
|
// routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
|
|
547
817
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
@@ -555,53 +825,210 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
|
|
|
555
825
|
// Central WS capability override on the FINAL on-disk catalog (the file Codex reads). Applies to
|
|
556
826
|
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
557
827
|
// native template can never leak supports_websockets while the flag is off.
|
|
558
|
-
const wsEnabled = websocketsEnabled(config);
|
|
559
|
-
const enabledProviders = Object.entries(config.providers ?? {})
|
|
560
|
-
.filter(([, prov]) => prov.disabled !== true);
|
|
561
|
-
const hasCanonicalOpenai = enabledProviders.some(([name, prov]) =>
|
|
562
|
-
name === "openai" && isCanonicalOpenAiForwardProvider(prov),
|
|
563
|
-
);
|
|
564
828
|
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
|
|
565
829
|
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
|
|
566
830
|
// providers are configured yet (fresh install / catalog bootstrap tests).
|
|
567
|
-
const
|
|
568
|
-
|
|
831
|
+
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
832
|
+
? buildCatalogEntries(
|
|
833
|
+
template ? JSON.parse(JSON.stringify(template)) : null,
|
|
834
|
+
NATIVE_OPENAI_MODELS,
|
|
835
|
+
[],
|
|
836
|
+
featured,
|
|
837
|
+
wsEnabled,
|
|
838
|
+
multiAgentMode,
|
|
839
|
+
exactComboSlugs,
|
|
840
|
+
accountSelectors,
|
|
841
|
+
).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
842
|
+
: [];
|
|
843
|
+
catalog.models = mergeCatalogEntriesForSync(
|
|
844
|
+
catalogModelsForMerge,
|
|
845
|
+
goEntries,
|
|
846
|
+
baseline,
|
|
847
|
+
featured,
|
|
848
|
+
wsEnabled,
|
|
849
|
+
goIds,
|
|
850
|
+
template,
|
|
851
|
+
new Set(config.disabledModels ?? []),
|
|
852
|
+
gatheredProviderNames,
|
|
853
|
+
multiAgentMode,
|
|
854
|
+
exactComboSlugs,
|
|
855
|
+
hasPhysicalComboProvider,
|
|
856
|
+
includeNativeOpenAi,
|
|
857
|
+
accountBoundEntries,
|
|
858
|
+
);
|
|
569
859
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
570
860
|
|
|
571
|
-
|
|
572
|
-
|
|
861
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
862
|
+
path: catalogPath,
|
|
863
|
+
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
864
|
+
});
|
|
865
|
+
return {
|
|
866
|
+
added: goEntries.length + accountBoundEntries.length,
|
|
867
|
+
path: catalogPath,
|
|
868
|
+
catalogWritten: true,
|
|
869
|
+
comboOmissions,
|
|
870
|
+
};
|
|
573
871
|
}
|
|
574
872
|
|
|
575
|
-
|
|
873
|
+
function visibleAccountReplacementNatives(
|
|
874
|
+
models: readonly RawEntry[],
|
|
875
|
+
disabledModels: ReadonlySet<string> | null,
|
|
876
|
+
): Map<string, boolean> {
|
|
877
|
+
const replacements = new Map<string, boolean>();
|
|
878
|
+
for (const entry of models) {
|
|
879
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
880
|
+
if (nativeSlug === undefined || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)) continue;
|
|
881
|
+
const exactSlug = typeof entry.slug === "string" ? entry.slug : "";
|
|
882
|
+
const visible = entry.visibility === "list"
|
|
883
|
+
|| (disabledModels !== null
|
|
884
|
+
&& (disabledModels.has(nativeSlug) || disabledModels.has(exactSlug)));
|
|
885
|
+
replacements.set(nativeSlug, (replacements.get(nativeSlug) ?? true) && visible);
|
|
886
|
+
}
|
|
887
|
+
return replacements;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function restoreAccountHiddenBareNatives(
|
|
891
|
+
entries: readonly RawEntry[],
|
|
892
|
+
replacementVisibility: ReadonlyMap<string, boolean>,
|
|
893
|
+
disabledModels: ReadonlySet<string> | null,
|
|
894
|
+
): RawEntry[] {
|
|
895
|
+
return entries.map(entry => {
|
|
896
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
897
|
+
if (
|
|
898
|
+
entry.visibility !== "hide"
|
|
899
|
+
|| !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
900
|
+
|| replacementVisibility.get(slug) !== true
|
|
901
|
+
|| disabledModels === null
|
|
902
|
+
|| disabledModels.has(slug)
|
|
903
|
+
) {
|
|
904
|
+
return entry;
|
|
905
|
+
}
|
|
906
|
+
return { ...entry, visibility: "list" };
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function currentDisabledModelsForRestore(): Set<string> | null {
|
|
911
|
+
try {
|
|
912
|
+
const diagnostics = readConfigDiagnostics();
|
|
913
|
+
if (diagnostics.source === "fallback" || diagnostics.error !== null) return null;
|
|
914
|
+
return new Set(diagnostics.config.disabledModels ?? []);
|
|
915
|
+
} catch {
|
|
916
|
+
// An unreadable config cannot safely authorize a visibility change during restore.
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export async function syncCatalogModels(config: OcxConfig): Promise<RetainedCatalogSyncResult> {
|
|
922
|
+
const owningCodexHome = getCodexHome();
|
|
923
|
+
const preflightRead = readRetainedCatalogSync(config);
|
|
924
|
+
if (preflightRead === null) {
|
|
925
|
+
return {
|
|
926
|
+
added: 0,
|
|
927
|
+
path: readCodexCatalogPath(),
|
|
928
|
+
catalogWritten: false,
|
|
929
|
+
comboOmissions: [],
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const comboOmissions: ComboCatalogOmission[] = [];
|
|
934
|
+
// Settle the bundled template, then baseline, and only then await. Reading it
|
|
935
|
+
// here makes the memo ours before anyone else can move it, so a bundled swap
|
|
936
|
+
// during the await is an outside change rather than our own side effect.
|
|
937
|
+
//
|
|
938
|
+
// The persisted runtime selection is covered by the filesystem evidence above
|
|
939
|
+
// rather than by a process epoch; see `retainedCatalogProcessEvidence` for why
|
|
940
|
+
// the in-memory runtime memo cannot be baselined honestly from this path.
|
|
941
|
+
loadBundledCodexCatalog();
|
|
942
|
+
const prepared: RetainedCatalogSyncRead = {
|
|
943
|
+
...preflightRead,
|
|
944
|
+
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
945
|
+
processEvidence: retainedCatalogProcessEvidence(),
|
|
946
|
+
};
|
|
947
|
+
const goModels = await gatherRoutedModels(config, { comboOmissions });
|
|
948
|
+
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
949
|
+
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
950
|
+
if (current === null) return null;
|
|
951
|
+
return writeRetainedCatalogSync({
|
|
952
|
+
config,
|
|
953
|
+
goModels,
|
|
954
|
+
comboOmissions,
|
|
955
|
+
read: current,
|
|
956
|
+
permit,
|
|
957
|
+
owningCodexHome,
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
if (committed.kind === "completed" && committed.value !== null) return committed.value;
|
|
961
|
+
return {
|
|
962
|
+
added: 0,
|
|
963
|
+
path: prepared.catalogPath,
|
|
964
|
+
catalogWritten: false,
|
|
965
|
+
comboOmissions,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
export function restoreCodexCatalogWithPermit(
|
|
970
|
+
permit: CatalogWritePermit,
|
|
971
|
+
owningCodexHome: string,
|
|
972
|
+
): { removed: number; kept: number; path: string } {
|
|
576
973
|
const catalogPath = readCodexCatalogPath();
|
|
577
974
|
const catalog = readCatalog(catalogPath);
|
|
578
975
|
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
976
|
+
const disabledModels = currentDisabledModelsForRestore();
|
|
977
|
+
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
579
978
|
const backup = readCatalogBackup(catalogPath);
|
|
580
979
|
if (backup && Array.isArray(backup.models)) {
|
|
581
980
|
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" && m.slug.includes("/")).length;
|
|
582
981
|
const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
583
|
-
const userNativeAdditions = (
|
|
584
|
-
|
|
982
|
+
const userNativeAdditions = restoreAccountHiddenBareNatives(
|
|
983
|
+
(catalog.models ?? []).filter(m =>
|
|
984
|
+
typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
|
|
985
|
+
),
|
|
986
|
+
replacementVisibility,
|
|
987
|
+
disabledModels,
|
|
585
988
|
);
|
|
586
989
|
const restored = {
|
|
587
990
|
...backup,
|
|
588
991
|
models: [...backup.models, ...userNativeAdditions],
|
|
589
992
|
};
|
|
590
|
-
|
|
993
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
994
|
+
path: catalogPath,
|
|
995
|
+
content: `${JSON.stringify(restored, null, 2)}\n`,
|
|
996
|
+
});
|
|
591
997
|
return { removed, kept: restored.models.length, path: catalogPath };
|
|
592
998
|
}
|
|
593
999
|
const before = catalog.models.length;
|
|
594
|
-
const native =
|
|
1000
|
+
const native = restoreAccountHiddenBareNatives(
|
|
1001
|
+
catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/"))),
|
|
1002
|
+
replacementVisibility,
|
|
1003
|
+
disabledModels,
|
|
1004
|
+
);
|
|
595
1005
|
const removed = before - native.length;
|
|
596
1006
|
if (removed > 0) {
|
|
597
1007
|
catalog.models = native;
|
|
598
|
-
|
|
1008
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1009
|
+
path: catalogPath,
|
|
1010
|
+
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
1011
|
+
});
|
|
599
1012
|
}
|
|
600
1013
|
return { removed, kept: native.length, path: catalogPath };
|
|
601
1014
|
}
|
|
602
1015
|
|
|
1016
|
+
export function restoreCodexCatalog(): { removed: number; kept: number; path: string } {
|
|
1017
|
+
const owningCodexHome = getCodexHome();
|
|
1018
|
+
const outcome = withCatalogWriteSerialization(
|
|
1019
|
+
owningCodexHome,
|
|
1020
|
+
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
1021
|
+
);
|
|
1022
|
+
return outcome.kind === "completed"
|
|
1023
|
+
? outcome.value
|
|
1024
|
+
: { removed: 0, kept: 0, path: readCodexCatalogPath() };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
603
1027
|
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|
|
604
|
-
export function
|
|
1028
|
+
export function invalidateCodexModelsCacheWithPermit(
|
|
1029
|
+
permit: CatalogWritePermit,
|
|
1030
|
+
owningCodexHome: string,
|
|
1031
|
+
): boolean {
|
|
605
1032
|
try {
|
|
606
1033
|
const catalogPath = readCodexCatalogPath();
|
|
607
1034
|
if (!existsSync(catalogPath)) return false;
|
|
@@ -612,9 +1039,21 @@ export function invalidateCodexModelsCache(): boolean {
|
|
|
612
1039
|
client_version: "0.0.0",
|
|
613
1040
|
models,
|
|
614
1041
|
};
|
|
615
|
-
|
|
1042
|
+
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
1043
|
+
path: activeCodexModelsCachePath(),
|
|
1044
|
+
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
1045
|
+
});
|
|
616
1046
|
return true;
|
|
617
1047
|
} catch {
|
|
618
1048
|
return false;
|
|
619
1049
|
}
|
|
620
1050
|
}
|
|
1051
|
+
|
|
1052
|
+
export function invalidateCodexModelsCache(): boolean {
|
|
1053
|
+
const owningCodexHome = getCodexHome();
|
|
1054
|
+
const outcome = withCatalogWriteSerialization(
|
|
1055
|
+
owningCodexHome,
|
|
1056
|
+
permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome),
|
|
1057
|
+
);
|
|
1058
|
+
return outcome.kind === "completed" && outcome.value;
|
|
1059
|
+
}
|