@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -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/commandcode-color.svg +1 -0
- 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 +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- 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 +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- 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 +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- 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 +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +353 -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 +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -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 +492 -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 +682 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- 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 +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- 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
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -1,23 +1,23 @@
|
|
|
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 {
|
|
5
|
+
import { expandUserPath, loadConfig, readConfigDiagnostics, websocketsEnabled } from "../../config";
|
|
6
|
+
import { shouldSyncCodexOnStart } from "../desired-state";
|
|
7
|
+
import { legacyCustomModelCatalogSlugs } from "../custom-model-catalog-migration";
|
|
8
|
+
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, getCodexHome, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
9
|
import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
|
|
8
10
|
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
9
11
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
12
|
import { modelInList } from "../../types";
|
|
11
13
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
12
|
-
import {
|
|
14
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
13
15
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
|
-
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
15
16
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
16
|
-
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
17
|
-
import {
|
|
17
|
+
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
|
+
import { identifyRoutedModel } from "../../adapters/identity";
|
|
18
19
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
19
20
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
20
|
-
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
21
21
|
import {
|
|
22
22
|
COMBO_NAMESPACE,
|
|
23
23
|
comboModelId,
|
|
@@ -29,20 +29,65 @@ import type { NormalizedComboConfig } from "../../combos/types";
|
|
|
29
29
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
30
30
|
import { redactSecretString } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
33
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
34
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../account-namespaces";
|
|
35
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
36
|
+
import {
|
|
37
|
+
availableAccountGatedNativeModels,
|
|
38
|
+
isCodexModelEntitlementSnapshotCurrent,
|
|
39
|
+
resolveCodexModelEntitlements,
|
|
40
|
+
type CodexModelEntitlementSnapshot,
|
|
41
|
+
} from "../model-entitlements";
|
|
32
42
|
|
|
33
43
|
|
|
34
|
-
import { activeCodexModelsCachePath,
|
|
35
|
-
import type { CatalogModel, MultiAgentMode, RawEntry } from "./parsing";
|
|
36
|
-
import { applyNativeVisibility, disabledNativeSlugs, isUnsupportedOpenAiNativeSlug,
|
|
37
|
-
import {
|
|
44
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
|
|
45
|
+
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
46
|
+
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
47
|
+
import {
|
|
48
|
+
bundledCatalogCacheState,
|
|
49
|
+
loadBundledCodexCatalog,
|
|
50
|
+
resetBundledCatalogCacheForTests,
|
|
51
|
+
} from "./bundled";
|
|
38
52
|
import { isMultiAgentV2Enabled } from "../features";
|
|
39
53
|
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
40
|
-
import {
|
|
41
|
-
|
|
54
|
+
import {
|
|
55
|
+
clearGatherRoutedModelsInflight,
|
|
56
|
+
filterCatalogVisibleModels,
|
|
57
|
+
gatherRoutedModels,
|
|
58
|
+
lastDropWarnSignature,
|
|
59
|
+
type CatalogGatherProviderModelOutcome,
|
|
60
|
+
} from "./provider-fetch";
|
|
61
|
+
import { accountSelectorShadowCollisionWarnings, clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, comboUnrestorableShadowWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnAccountSelectorShadowedProviderOnce, warnComboMasqueradeCollisionOnce, warnComboUnrestorableShadowOnce } from "./aggregation";
|
|
42
62
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
63
|
+
import {
|
|
64
|
+
withCatalogWriteSerialization,
|
|
65
|
+
type CatalogWritePermit,
|
|
66
|
+
} from "../catalog-write-serialization";
|
|
67
|
+
import {
|
|
68
|
+
publishHashedCodexCatalogBackup,
|
|
69
|
+
publishLegacyCodexCatalogBackup,
|
|
70
|
+
replaceActiveCodexCatalog,
|
|
71
|
+
replaceCodexModelsCache,
|
|
72
|
+
} from "../internal/catalog-writer";
|
|
73
|
+
import { codexRuntimeStatePath } from "../runtime";
|
|
74
|
+
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
75
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./native-models";
|
|
43
76
|
|
|
44
77
|
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
45
78
|
|
|
79
|
+
// Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
|
|
80
|
+
// reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but never the
|
|
81
|
+
// spawn_agent candidate window. The window is derived from SPAWN_PRIORITY_FIELD (the natural
|
|
82
|
+
// priority captured before the override), so display order and spawn candidates are decoupled.
|
|
83
|
+
export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
84
|
+
|
|
85
|
+
// OpenCodex-private catalog field: the spawn_agent candidate priority a row would have WITHOUT
|
|
86
|
+
// modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
|
|
87
|
+
// is invisible to Codex; effectiveSubagentRoster reads it so a display reorder cannot change which
|
|
88
|
+
// rows are spawn_agent candidates. Absent on rows modelPickerOrder did not move.
|
|
89
|
+
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
90
|
+
|
|
46
91
|
export type SpawnAgentSurface = "v1" | "v2";
|
|
47
92
|
|
|
48
93
|
export type SubagentRosterExclusionReason =
|
|
@@ -54,21 +99,36 @@ export type SubagentRosterExclusionReason =
|
|
|
54
99
|
/**
|
|
55
100
|
* Whether a catalog entry may be offered as a V2 subagent model.
|
|
56
101
|
*
|
|
57
|
-
* Upstream
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
102
|
+
* Upstream changed this rule in codex-rs `6d4d9442c` ("Support leaf models in
|
|
103
|
+
* multi-agent v2"). `model_supports_multi_agent_backend`
|
|
104
|
+
* (core/src/tools/handlers/multi_agents_common.rs:36-42) now admits EVERY model
|
|
105
|
+
* except one explicitly marked `disabled`; the older `== Some(V2)` equality that
|
|
106
|
+
* `92938d880` introduced is gone.
|
|
107
|
+
*
|
|
108
|
+
* The field no longer answers "may I be a delegation target". It answers "does the
|
|
109
|
+
* CHILD get collaboration tools": `collab_tools_enabled`
|
|
110
|
+
* (core/src/tools/spec_plan.rs:599-610) grants a child recursive tools only when its
|
|
111
|
+
* own catalog value is exactly `Some(V2)`. The three-way distinction survives, but it
|
|
112
|
+
* now means eligible-recursive / eligible-LEAF / excluded:
|
|
61
113
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* absent
|
|
67
|
-
*
|
|
114
|
+
* - `"v2"` -> eligible, and the child may itself delegate.
|
|
115
|
+
* - `"v1"` -> eligible LEAF worker. This is upstream's pin for `gpt-5.6-luna`
|
|
116
|
+
* (models-manager/models.json); excluding it here is exactly what
|
|
117
|
+
* kept Luna out of opencodex's roster.
|
|
118
|
+
* - absent/null -> eligible LEAF worker (routed or unpinned-native model).
|
|
119
|
+
* - `"disabled"` -> the sole capability-based exclusion.
|
|
120
|
+
*
|
|
121
|
+
* This is the roster filter only. Catalog STAMPING is a separate concern owned by
|
|
122
|
+
* `applyMultiAgentMode`, including the `keepNativeChatGptOnV1` policy (#1728) that
|
|
123
|
+
* keeps ChatGPT-native rows on `v1` so a native parent can still spawn a routed child
|
|
124
|
+
* despite backend-encrypted NEW_TASK bodies (#92). Recognizing those `v1` rows as
|
|
125
|
+
* eligible leaves here is what makes that policy usable, not a contradiction of it.
|
|
126
|
+
*
|
|
127
|
+
* Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 (C1), superseding the
|
|
128
|
+
* option-B decision in 260730_codex_rs_upstream_v2_live_handoff/060.
|
|
68
129
|
*/
|
|
69
130
|
export function isEligibleV2SubagentEntry(entry: RawEntry): boolean {
|
|
70
|
-
|
|
71
|
-
return pinned === "v2" || pinned === null || pinned === undefined;
|
|
131
|
+
return entry.multi_agent_version !== "disabled";
|
|
72
132
|
}
|
|
73
133
|
|
|
74
134
|
export interface EffectiveSubagentModel {
|
|
@@ -88,63 +148,96 @@ export interface EffectiveSubagentRoster {
|
|
|
88
148
|
excluded: SubagentRosterExclusion[];
|
|
89
149
|
}
|
|
90
150
|
|
|
91
|
-
export function configuredCatalogEntry(entries: RawEntry[], configured: string): RawEntry | undefined {
|
|
151
|
+
export function configuredCatalogEntry(entries: readonly RawEntry[], configured: string): RawEntry | undefined {
|
|
92
152
|
return entries.find(entry => entry.slug === configured)
|
|
93
153
|
?? entries.find(entry => typeof entry.slug === "string" && slugsEquivalent(configured, entry.slug));
|
|
94
154
|
}
|
|
95
155
|
|
|
156
|
+
function configuredSubagentModelMatchesEntry(configured: string, entry: RawEntry): boolean {
|
|
157
|
+
if (typeof entry.slug !== "string") return false;
|
|
158
|
+
if (slugsEquivalent(configured, entry.slug)) return true;
|
|
159
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
160
|
+
return !configured.includes("/")
|
|
161
|
+
&& nativeSlug !== undefined
|
|
162
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)
|
|
163
|
+
&& slugsEquivalent(configured, nativeSlug);
|
|
164
|
+
}
|
|
165
|
+
|
|
96
166
|
export function effectiveSubagentRoster(
|
|
97
167
|
configuredModels: readonly string[],
|
|
98
168
|
surface: SpawnAgentSurface,
|
|
169
|
+
catalogEntries?: readonly RawEntry[],
|
|
99
170
|
): EffectiveSubagentRoster {
|
|
100
171
|
const configured = configuredModels
|
|
101
172
|
.filter(model => model.trim().length > 0)
|
|
102
173
|
.filter((model, index, all) =>
|
|
103
174
|
!all.slice(0, index).some(previous => slugsEquivalent(previous, model))
|
|
104
175
|
);
|
|
105
|
-
const entries = readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
176
|
+
const entries = catalogEntries ?? readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
106
177
|
const ordered = entries
|
|
107
178
|
.map((entry, index) => ({ entry, index }))
|
|
108
179
|
.filter(({ entry }) => typeof entry.slug === "string")
|
|
109
180
|
.filter(({ entry }) => entry.visibility === "list")
|
|
110
181
|
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
111
182
|
.sort((left, right) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
183
|
+
// Spawn candidates rank by the natural priority (SPAWN_PRIORITY_FIELD when present), so a
|
|
184
|
+
// modelPickerOrder display reorder (#1649) can never change candidate membership. Rows the
|
|
185
|
+
// override did not move fall back to their Codex-visible `priority`.
|
|
186
|
+
const spawnPriorityOf = (entry: RawEntry): number => {
|
|
187
|
+
const spawn = entry[SPAWN_PRIORITY_FIELD];
|
|
188
|
+
if (typeof spawn === "number" && Number.isFinite(spawn)) return spawn;
|
|
189
|
+
return typeof entry.priority === "number" && Number.isFinite(entry.priority)
|
|
190
|
+
? entry.priority : Number.MAX_SAFE_INTEGER;
|
|
191
|
+
};
|
|
192
|
+
const leftPriority = spawnPriorityOf(left.entry);
|
|
193
|
+
const rightPriority = spawnPriorityOf(right.entry);
|
|
116
194
|
return leftPriority - rightPriority || left.index - right.index;
|
|
117
195
|
})
|
|
118
196
|
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
197
|
+
const orderedEntries = new Set(ordered.map(({ entry }) => entry));
|
|
119
198
|
|
|
120
199
|
const candidates = ordered.map(({ entry }) => ({
|
|
121
200
|
model: entry.slug as string,
|
|
122
201
|
efforts: catalogEntryEfforts(entry),
|
|
123
202
|
}));
|
|
124
|
-
const advertised =
|
|
125
|
-
configured.some(model =>
|
|
126
|
-
|
|
203
|
+
const advertised = ordered
|
|
204
|
+
.filter(({ entry }) => configured.some(model => configuredSubagentModelMatchesEntry(model, entry)))
|
|
205
|
+
.map(({ entry }) => ({
|
|
206
|
+
model: entry.slug as string,
|
|
207
|
+
efforts: catalogEntryEfforts(entry),
|
|
208
|
+
}));
|
|
127
209
|
const excluded = configured.flatMap((model): SubagentRosterExclusion[] => {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
const matchingEntries = entries.filter(entry => configuredSubagentModelMatchesEntry(model, entry));
|
|
211
|
+
if (matchingEntries.some(entry => orderedEntries.has(entry))) return [];
|
|
212
|
+
if (matchingEntries.length === 0) return [{ configured: model, reason: "missing_catalog_entry" }];
|
|
213
|
+
const visibleCompatible = matchingEntries.find(entry =>
|
|
214
|
+
entry.visibility === "list"
|
|
215
|
+
&& (surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
216
|
+
);
|
|
217
|
+
if (visibleCompatible) {
|
|
218
|
+
return [{
|
|
219
|
+
configured: model,
|
|
220
|
+
catalogModel: visibleCompatible.slug as string,
|
|
221
|
+
reason: "outside_display_limit",
|
|
222
|
+
}];
|
|
136
223
|
}
|
|
137
|
-
|
|
138
|
-
|
|
224
|
+
const visible = matchingEntries.find(entry => entry.visibility === "list");
|
|
225
|
+
if (visible) {
|
|
226
|
+
return [{
|
|
227
|
+
configured: model,
|
|
228
|
+
catalogModel: visible.slug as string,
|
|
229
|
+
reason: "surface_incompatible",
|
|
230
|
+
}];
|
|
139
231
|
}
|
|
140
|
-
|
|
232
|
+
const hidden = configuredCatalogEntry(entries, model) ?? matchingEntries[0]!;
|
|
233
|
+
return [{ configured: model, catalogModel: hidden.slug as string, reason: "picker_hidden" }];
|
|
141
234
|
});
|
|
142
235
|
return { candidates, advertised, excluded };
|
|
143
236
|
}
|
|
144
237
|
|
|
145
|
-
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry {
|
|
238
|
+
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
|
|
146
239
|
if (priority !== 9) clone.priority = priority;
|
|
147
|
-
applyNativeOpenAiContextOverride(clone);
|
|
240
|
+
applyNativeOpenAiContextOverride(clone, contextCap);
|
|
148
241
|
// GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
|
|
149
242
|
// Older natives (gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex-spark) get mock max + ultra
|
|
150
243
|
// (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
|
|
@@ -156,7 +249,36 @@ export function isExactComboCatalogModel(
|
|
|
156
249
|
model: CatalogModel | undefined,
|
|
157
250
|
exactComboSlugs: ReadonlySet<string>,
|
|
158
251
|
): boolean {
|
|
159
|
-
return model
|
|
252
|
+
return model?.provider === COMBO_NAMESPACE && exactComboSlugs.has(catalogModelSlug(model));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isExactComboCatalogEntry(
|
|
256
|
+
entry: RawEntry,
|
|
257
|
+
exactComboSlugs: ReadonlySet<string>,
|
|
258
|
+
): boolean {
|
|
259
|
+
return entry.owned_by === COMBO_NAMESPACE
|
|
260
|
+
&& typeof entry.slug === "string"
|
|
261
|
+
&& exactComboSlugs.has(entry.slug);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
|
|
266
|
+
* ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
|
|
267
|
+
* lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
|
|
268
|
+
* The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
|
|
269
|
+
* that is dropped for display. All other providers keep the raw slug exactly as before.
|
|
270
|
+
*/
|
|
271
|
+
function routedDisplayName(slug: string): string {
|
|
272
|
+
const slash = slug.indexOf("/");
|
|
273
|
+
if (slash <= 0) return slug;
|
|
274
|
+
const provider = slug.slice(0, slash);
|
|
275
|
+
let model = slug.slice(slash + 1);
|
|
276
|
+
if (provider === "command-code" || provider === "commandcode") {
|
|
277
|
+
const m = model.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
|
|
278
|
+
if (m && model.startsWith(`${m[1]}-${m[1]}-`)) model = model.slice(m[1]!.length + 1);
|
|
279
|
+
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${model}`;
|
|
280
|
+
}
|
|
281
|
+
return slug;
|
|
160
282
|
}
|
|
161
283
|
|
|
162
284
|
export function deriveEntry(
|
|
@@ -166,20 +288,24 @@ export function deriveEntry(
|
|
|
166
288
|
priority: number,
|
|
167
289
|
model?: CatalogModel,
|
|
168
290
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
291
|
+
contextCap?: NativeContextLimitsInput,
|
|
169
292
|
): RawEntry {
|
|
170
293
|
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
294
|
+
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
|
|
295
|
+
? upstreamNativeEntry(model.id)
|
|
296
|
+
: null;
|
|
171
297
|
const isRouted = model !== undefined;
|
|
172
298
|
if (!isRouted && !slug.includes("/")) {
|
|
173
299
|
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
|
|
174
300
|
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
|
|
175
301
|
// instead of cloning an older template.
|
|
176
302
|
const upstream = upstreamNativeEntry(slug);
|
|
177
|
-
if (upstream) return finishUpstreamNativeEntry(upstream, priority);
|
|
303
|
+
if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
|
|
178
304
|
}
|
|
179
|
-
if (template) {
|
|
180
|
-
const e = JSON.parse(JSON.stringify(template)) as RawEntry;
|
|
305
|
+
if (template || codexForwardNativeCapabilityAlias) {
|
|
306
|
+
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
|
|
181
307
|
e.slug = slug;
|
|
182
|
-
e.display_name = slug;
|
|
308
|
+
e.display_name = routedDisplayName(slug);
|
|
183
309
|
e.description = desc;
|
|
184
310
|
e.priority = priority;
|
|
185
311
|
e.visibility = "list";
|
|
@@ -188,23 +314,41 @@ export function deriveEntry(
|
|
|
188
314
|
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
|
|
189
315
|
// and advertise the reasoning ladder Codex accepts.
|
|
190
316
|
if (isRouted) {
|
|
317
|
+
// A routed model is NOT the native template: never inherit its context
|
|
318
|
+
// window when /models omits context metadata (#992). Known metadata
|
|
319
|
+
// restores exact values below; an enabled Context cap fills the gap;
|
|
320
|
+
// otherwise the strict-fields fallback supplies the 128k triple.
|
|
321
|
+
if (!codexForwardNativeCapabilityAlias) {
|
|
322
|
+
delete e.context_window;
|
|
323
|
+
delete e.max_context_window;
|
|
324
|
+
delete e.auto_compact_token_limit;
|
|
325
|
+
}
|
|
191
326
|
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
192
327
|
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
193
328
|
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
194
329
|
if (typeof e.base_instructions === "string") {
|
|
195
330
|
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
|
|
196
331
|
// (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
|
-
);
|
|
332
|
+
e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
|
|
201
333
|
}
|
|
202
|
-
applyReasoningLevels(
|
|
203
|
-
|
|
204
|
-
|
|
334
|
+
applyReasoningLevels(
|
|
335
|
+
e,
|
|
336
|
+
model?.reasoningEfforts,
|
|
337
|
+
model?.defaultReasoningEffort,
|
|
338
|
+
preserveExact || codexForwardNativeCapabilityAlias !== null,
|
|
339
|
+
);
|
|
340
|
+
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
|
|
341
|
+
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
|
|
342
|
+
if (!codexForwardNativeCapabilityAlias) {
|
|
343
|
+
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true, model?.codexToolMode);
|
|
344
|
+
} else if (model?.codexToolMode !== undefined) {
|
|
345
|
+
applyRoutedCodexToolMode(e, model.codexToolMode);
|
|
346
|
+
}
|
|
347
|
+
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
205
348
|
applyCatalogModelMetadata(e, model);
|
|
349
|
+
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
|
|
206
350
|
} else {
|
|
207
|
-
applyNativeOpenAiContextOverride(e);
|
|
351
|
+
applyNativeOpenAiContextOverride(e, contextCap);
|
|
208
352
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
209
353
|
else ensureUltraReasoningLevel(e);
|
|
210
354
|
// Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite;
|
|
@@ -225,28 +369,61 @@ export function deriveEntry(
|
|
|
225
369
|
});
|
|
226
370
|
}
|
|
227
371
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
372
|
+
// Routed fallbacks default to code-mode tool exposure (or shell mode when codexToolMode === "shell");
|
|
373
|
+
// otherwise the nested catalog expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
|
|
374
|
+
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
|
|
375
|
+
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
228
376
|
const entry: RawEntry = {
|
|
229
|
-
slug, display_name: slug, description: desc,
|
|
377
|
+
slug, display_name: routedDisplayName(slug), description: desc,
|
|
230
378
|
shell_type: "shell_command", visibility: "list", supported_in_api: true,
|
|
231
379
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
232
|
-
...(isRouted
|
|
380
|
+
...(isRouted
|
|
381
|
+
? isCursorFallback
|
|
382
|
+
? { supports_search_tool: true }
|
|
383
|
+
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
384
|
+
: {}),
|
|
233
385
|
};
|
|
234
386
|
if (isRouted) {
|
|
387
|
+
applyRoutedCodexToolMode(entry, model?.codexToolMode);
|
|
235
388
|
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
236
389
|
}
|
|
237
390
|
else {
|
|
238
391
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
239
392
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
|
|
240
393
|
}
|
|
241
|
-
if (model && isRouted)
|
|
394
|
+
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
242
395
|
applyCatalogModelMetadata(entry, model);
|
|
243
|
-
if (
|
|
396
|
+
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
|
|
397
|
+
if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
|
|
244
398
|
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
245
399
|
preserveExactInputModalities: preserveExact,
|
|
246
400
|
isRouted,
|
|
247
401
|
});
|
|
248
402
|
}
|
|
249
403
|
|
|
404
|
+
export interface ObservedCatalogEntryBuildInput {
|
|
405
|
+
readonly template: RawEntry | null;
|
|
406
|
+
readonly gptSlugs: readonly string[];
|
|
407
|
+
readonly goModels: readonly CatalogModel[];
|
|
408
|
+
readonly featured?: readonly string[];
|
|
409
|
+
/** Optional full picker ordering (config.modelPickerOrder); orders non-featured rows. */
|
|
410
|
+
readonly modelPickerOrder?: readonly string[];
|
|
411
|
+
readonly wsEnabled: boolean;
|
|
412
|
+
readonly multiAgentMode: MultiAgentMode;
|
|
413
|
+
readonly exactComboSlugs: ReadonlySet<string>;
|
|
414
|
+
readonly accountSelectors: readonly string[];
|
|
415
|
+
readonly suppressedBareNativeSlugs: ReadonlySet<string>;
|
|
416
|
+
readonly disabledNativeAccountSlugs: ReadonlySet<string>;
|
|
417
|
+
readonly multiAgentV2Enabled: boolean;
|
|
418
|
+
readonly keepNativeChatGptOnV1?: boolean;
|
|
419
|
+
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
420
|
+
/** Additional native ids to clone under account selectors, without creating bare rows. */
|
|
421
|
+
readonly accountNativeSlugs?: readonly string[];
|
|
422
|
+
/** Per-selector account ids; unknown observations must not be copied to unrelated accounts. */
|
|
423
|
+
readonly accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Build entries with the process-observed Codex feature state. */
|
|
250
427
|
export function buildCatalogEntries(
|
|
251
428
|
template: RawEntry | null,
|
|
252
429
|
gptSlugs: string[],
|
|
@@ -255,24 +432,175 @@ export function buildCatalogEntries(
|
|
|
255
432
|
wsEnabled = false,
|
|
256
433
|
multiAgentMode: MultiAgentMode = "default",
|
|
257
434
|
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
435
|
+
accountSelectors: readonly string[] = [],
|
|
436
|
+
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
|
|
437
|
+
disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
|
|
438
|
+
contextCap?: NativeContextLimitsInput,
|
|
439
|
+
accountNativeSlugs?: readonly string[],
|
|
440
|
+
accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
|
|
441
|
+
keepNativeChatGptOnV1 = false,
|
|
258
442
|
): RawEntry[] {
|
|
443
|
+
return buildCatalogEntriesFromObservedState({
|
|
444
|
+
template,
|
|
445
|
+
gptSlugs,
|
|
446
|
+
goModels,
|
|
447
|
+
featured,
|
|
448
|
+
wsEnabled,
|
|
449
|
+
multiAgentMode,
|
|
450
|
+
exactComboSlugs,
|
|
451
|
+
accountSelectors,
|
|
452
|
+
suppressedBareNativeSlugs,
|
|
453
|
+
disabledNativeAccountSlugs,
|
|
454
|
+
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
455
|
+
keepNativeChatGptOnV1,
|
|
456
|
+
openaiContextCap: contextCap,
|
|
457
|
+
accountNativeSlugs,
|
|
458
|
+
accountNativeSlugsBySelector,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Build entries solely from caller-observed inputs, with no feature-state filesystem read. */
|
|
463
|
+
export function buildCatalogEntriesFromObservedState({
|
|
464
|
+
template,
|
|
465
|
+
gptSlugs,
|
|
466
|
+
goModels,
|
|
467
|
+
featured,
|
|
468
|
+
modelPickerOrder,
|
|
469
|
+
wsEnabled,
|
|
470
|
+
multiAgentMode,
|
|
471
|
+
exactComboSlugs,
|
|
472
|
+
accountSelectors,
|
|
473
|
+
suppressedBareNativeSlugs,
|
|
474
|
+
disabledNativeAccountSlugs,
|
|
475
|
+
multiAgentV2Enabled,
|
|
476
|
+
keepNativeChatGptOnV1,
|
|
477
|
+
openaiContextCap,
|
|
478
|
+
accountNativeSlugs,
|
|
479
|
+
accountNativeSlugsBySelector,
|
|
480
|
+
}: ObservedCatalogEntryBuildInput): RawEntry[] {
|
|
259
481
|
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
260
482
|
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
261
483
|
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
262
484
|
// it sorts to the front. This works for native gpt slugs AND routed slugs alike.
|
|
263
485
|
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
486
|
+
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
487
|
+
// Optional full picker order (#1649). Independent of the 5-slot spawn_agent cap: it only
|
|
488
|
+
// rewrites the Codex-visible display `priority` of listed non-featured routed rows so a >5
|
|
489
|
+
// catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
|
|
490
|
+
// modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
|
|
491
|
+
// before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
|
|
492
|
+
// this display reorder cannot change which rows are spawn candidates.
|
|
493
|
+
const pickerOrder = Array.isArray(modelPickerOrder)
|
|
494
|
+
? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
|
|
495
|
+
: [];
|
|
496
|
+
const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
|
|
497
|
+
const pickerOrderActive = pickerOrder.length > 0;
|
|
498
|
+
// The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
|
|
499
|
+
// same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
|
|
500
|
+
// band. Candidate membership does not depend on this — see SPAWN_PRIORITY_FIELD.
|
|
501
|
+
/**
|
|
502
|
+
* Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
|
|
503
|
+
* slugs sort in declared order within the high picker-order display tier
|
|
504
|
+
* (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
|
|
505
|
+
* the row's natural priority in SPAWN_PRIORITY_FIELD so the spawn_agent candidate window is
|
|
506
|
+
* unchanged. Returns undefined when the feature is off or the row is not listed, so those rows
|
|
507
|
+
* keep their original assignment (default 5 / account 1_000+) untouched.
|
|
508
|
+
*
|
|
509
|
+
* Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
|
|
510
|
+
* below). Native passthrough rows and account-qualified native rows keep their own priority
|
|
511
|
+
* logic and are intentionally not reordered here — this matches the documented contract on
|
|
512
|
+
* OcxConfig.modelPickerOrder (route native ordering through subagentModels instead).
|
|
513
|
+
*/
|
|
514
|
+
const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
|
|
515
|
+
if (!pickerOrderActive) return undefined;
|
|
516
|
+
const hit = pickerOrderRank.get(slug) ?? (altSlug !== undefined ? pickerOrderRank.get(altSlug) : undefined);
|
|
517
|
+
if (hit === undefined) return undefined;
|
|
518
|
+
return PICKER_ORDER_PRIORITY_BASE + hit * priorityStride;
|
|
519
|
+
};
|
|
264
520
|
const out: RawEntry[] = [];
|
|
265
|
-
const
|
|
521
|
+
const nativeEntries: RawEntry[] = [];
|
|
522
|
+
const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
|
|
523
|
+
const emittedNativeAliases = new Set<CatalogModel>();
|
|
524
|
+
const emittedNativeAliasSlugs = new Set<string>();
|
|
525
|
+
const nativeAliasesBySlug = new Map<string, CatalogModel>();
|
|
526
|
+
for (const model of goModels) {
|
|
527
|
+
if (model.provider !== COMBO_NAMESPACE
|
|
528
|
+
|| model.nativeAlias !== true
|
|
529
|
+
|| typeof model.alias !== "string"
|
|
530
|
+
|| model.alias.includes("/")) continue;
|
|
531
|
+
if (nativeAliasesBySlug.has(model.alias)) {
|
|
532
|
+
collisionSkipped.add(model);
|
|
533
|
+
if (!slugAliasCollisionWarnings.has(model.alias)) {
|
|
534
|
+
slugAliasCollisionWarnings.add(model.alias);
|
|
535
|
+
console.warn(
|
|
536
|
+
`[opencodex] native combo alias collision on "${model.alias}": keeping the first configured combo and omitting later duplicates from the catalog.`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
nativeAliasesBySlug.set(model.alias, model);
|
|
542
|
+
}
|
|
266
543
|
const comboPublicSlugs = new Set(goModels
|
|
267
544
|
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
268
545
|
.map(catalogModelSlug));
|
|
269
546
|
for (const slug of gptSlugs) {
|
|
270
|
-
const
|
|
271
|
-
if (rank.has(slug))
|
|
272
|
-
|
|
547
|
+
const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
|
|
548
|
+
if (rank.has(slug)) native.priority = rank.get(slug)!;
|
|
549
|
+
nativeEntries.push(native);
|
|
550
|
+
const nativeAlias = nativeAliasesBySlug.get(slug);
|
|
551
|
+
if (!nativeAlias || collisionSkipped.has(nativeAlias)) {
|
|
552
|
+
if (!suppressedBareNativeSlugs.has(slug)) out.push(native);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const routed = deriveEntry(
|
|
556
|
+
template,
|
|
557
|
+
slug,
|
|
558
|
+
`Routed via opencodex → ${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
|
|
559
|
+
5,
|
|
560
|
+
nativeAlias,
|
|
561
|
+
exactComboSlugs,
|
|
562
|
+
);
|
|
563
|
+
routed.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
564
|
+
const rankHit = rank.get(slug) ?? rank.get(`${nativeAlias.provider}/${nativeAlias.id}`);
|
|
565
|
+
if (rankHit !== undefined) routed.priority = rankHit * priorityStride;
|
|
566
|
+
else if (accountSelectors.length > 0) routed.priority = 1_000 + (typeof routed.priority === "number" ? routed.priority : 5);
|
|
567
|
+
out.push(routed);
|
|
568
|
+
emittedNativeAliases.add(nativeAlias);
|
|
569
|
+
emittedNativeAliasSlugs.add(slug);
|
|
570
|
+
}
|
|
571
|
+
const nativeEntriesBySlug = new Map(nativeEntries.map(entry => [String(entry.slug), entry] as const));
|
|
572
|
+
for (const [selectorIndex, selector] of accountSelectors.entries()) {
|
|
573
|
+
const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
|
|
574
|
+
?? accountNativeSlugs
|
|
575
|
+
?? gptSlugs;
|
|
576
|
+
const accountNativeEntries = selectorNativeSlugs.map(slug => (
|
|
577
|
+
nativeEntriesBySlug.get(slug)
|
|
578
|
+
?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
|
|
579
|
+
));
|
|
580
|
+
for (const [nativeIndex, native] of accountNativeEntries.entries()) {
|
|
581
|
+
const nativeSlug = String(native.slug);
|
|
582
|
+
if (disabledNativeAccountSlugs.has(nativeSlug)) continue;
|
|
583
|
+
const e = JSON.parse(JSON.stringify(native)) as RawEntry;
|
|
584
|
+
const catalogSlug = `${selector}/${nativeSlug}`;
|
|
585
|
+
e.slug = catalogSlug;
|
|
586
|
+
e.display_name = accountBoundNativeDisplayName(selector, native);
|
|
587
|
+
// Codex ignores this OpenCodex extension; preserve the native comp_hash unchanged.
|
|
588
|
+
e.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
589
|
+
const exactRank = rank.get(catalogSlug);
|
|
590
|
+
// A bare featured id belongs to the compatibility combo once shadowed. Exact
|
|
591
|
+
// account-qualified picks still rank normally, but the account clone must not
|
|
592
|
+
// inherit the bare alias rank and consume another top spawn_agent slot.
|
|
593
|
+
const inheritedRank = emittedNativeAliasSlugs.has(nativeSlug) ? undefined : rank.get(nativeSlug);
|
|
594
|
+
const featuredRank = exactRank ?? inheritedRank;
|
|
595
|
+
e.priority = featuredRank !== undefined
|
|
596
|
+
? featuredRank * priorityStride + selectorIndex
|
|
597
|
+
: ((featured?.length ?? 0) + nativeIndex) * accountSelectors.length + selectorIndex;
|
|
598
|
+
e.visibility = "list";
|
|
599
|
+
out.push(e);
|
|
600
|
+
}
|
|
273
601
|
}
|
|
274
602
|
for (const m of goModels) {
|
|
275
|
-
if (collisionSkipped.has(m)) continue;
|
|
603
|
+
if (collisionSkipped.has(m) || emittedNativeAliases.has(m)) continue;
|
|
276
604
|
const slug = catalogModelSlug(m);
|
|
277
605
|
if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
|
|
278
606
|
warnComboMasqueradeCollisionOnce(slug);
|
|
@@ -288,9 +616,29 @@ export function buildCatalogEntries(
|
|
|
288
616
|
m,
|
|
289
617
|
exactComboSlugs,
|
|
290
618
|
);
|
|
619
|
+
if (m.provider === COMBO_NAMESPACE && m.nativeAlias === true && !slug.includes("/")) {
|
|
620
|
+
e.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
621
|
+
}
|
|
291
622
|
// Featured picks may be stored raw (legacy) or encoded — honor both.
|
|
292
623
|
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
|
|
293
|
-
|
|
624
|
+
// Natural priority: what the row would get WITHOUT modelPickerOrder. This is the value the
|
|
625
|
+
// spawn_agent candidate window is derived from (see effectiveSubagentRoster), so it must never
|
|
626
|
+
// move when modelPickerOrder reorders the picker.
|
|
627
|
+
if (rankHit !== undefined) e.priority = rankHit * priorityStride;
|
|
628
|
+
else if (accountSelectors.length > 0) {
|
|
629
|
+
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
630
|
+
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
631
|
+
}
|
|
632
|
+
// #1649: modelPickerOrder is a DISPLAY-ONLY override. Record the natural priority spawn_agent
|
|
633
|
+
// must keep using, then let modelPickerOrder move only the Codex-visible `priority`. Featured
|
|
634
|
+
// rows are never overridden (their rank is authoritative for both display and spawn).
|
|
635
|
+
if (rankHit === undefined) {
|
|
636
|
+
const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
|
|
637
|
+
if (pickerPriority !== undefined) {
|
|
638
|
+
e[SPAWN_PRIORITY_FIELD] = typeof e.priority === "number" ? e.priority : 5;
|
|
639
|
+
e.priority = pickerPriority;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
294
642
|
out.push(e);
|
|
295
643
|
}
|
|
296
644
|
// Central capability override (phase 120.4): the advertised flag must match the implemented WS
|
|
@@ -305,7 +653,9 @@ export function buildCatalogEntries(
|
|
|
305
653
|
delete entry.prefer_websockets;
|
|
306
654
|
}
|
|
307
655
|
}
|
|
308
|
-
return applyMultiAgentMode(out, multiAgentMode,
|
|
656
|
+
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
|
|
657
|
+
keepNativeChatGptOnV1,
|
|
658
|
+
});
|
|
309
659
|
}
|
|
310
660
|
|
|
311
661
|
export function resetCatalogRuntimeStateForTests(): void {
|
|
@@ -315,8 +665,10 @@ export function resetCatalogRuntimeStateForTests(): void {
|
|
|
315
665
|
comboCatalogWarningSignatures.clear();
|
|
316
666
|
slugAliasCollisionWarnings.clear();
|
|
317
667
|
comboMasqueradeCollisionWarnings.clear();
|
|
668
|
+
comboUnrestorableShadowWarnings.clear();
|
|
669
|
+
accountSelectorShadowCollisionWarnings.clear();
|
|
318
670
|
clearLastComboCatalogOmissions();
|
|
319
|
-
clearModelCache();
|
|
671
|
+
clearModelCache(undefined, "eviction");
|
|
320
672
|
clearGatherRoutedModelsInflight();
|
|
321
673
|
}
|
|
322
674
|
|
|
@@ -344,124 +696,361 @@ export function orderForSubagents(goModels: CatalogModel[], featured?: string[])
|
|
|
344
696
|
* row.
|
|
345
697
|
*/
|
|
346
698
|
function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
699
|
+
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
347
700
|
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
348
701
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
349
702
|
return slug.includes("/") && desc.startsWith("Routed via opencodex → ");
|
|
350
703
|
}
|
|
351
704
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
})),
|
|
366
|
-
multiAgentMode: MultiAgentMode = "default",
|
|
367
|
-
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
368
|
-
hasPhysicalComboProvider = false,
|
|
369
|
-
includeNativeOpenAi = true,
|
|
705
|
+
function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
706
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
707
|
+
return SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
708
|
+
&& !isNativeAliasCatalogEntry(entry)
|
|
709
|
+
&& entry.owned_by !== COMBO_NAMESPACE
|
|
710
|
+
? slug
|
|
711
|
+
: null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** Append missing supported native rows from trusted catalog sources only. */
|
|
715
|
+
export function mergeCatalogModelsWithNativeRecovery(
|
|
716
|
+
primaryCatalogModels: readonly RawEntry[],
|
|
717
|
+
nativeRecoverySources: readonly (readonly RawEntry[])[],
|
|
370
718
|
): RawEntry[] {
|
|
719
|
+
const merged = [...primaryCatalogModels];
|
|
720
|
+
const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
|
|
721
|
+
const slug = recoverableNativeSlug(entry);
|
|
722
|
+
return slug === null ? [] : [slug];
|
|
723
|
+
}));
|
|
724
|
+
for (const source of nativeRecoverySources) {
|
|
725
|
+
for (const entry of source) {
|
|
726
|
+
const slug = recoverableNativeSlug(entry);
|
|
727
|
+
if (slug === null || recoveredNativeSlugs.has(slug)) continue;
|
|
728
|
+
merged.push(structuredClone(entry) as RawEntry);
|
|
729
|
+
recoveredNativeSlugs.add(slug);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return merged;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface ObservedCatalogMergePolicy {
|
|
736
|
+
/** Required observed/fixed set; the core merge never consults ambient catalog state. */
|
|
737
|
+
readonly nativeBackfillSlugs: readonly string[];
|
|
738
|
+
/** Whether unsupported OpenAI-family bare rows survive the merge. */
|
|
739
|
+
readonly unsupportedNativeEntries: "preserve" | "drop";
|
|
740
|
+
/** Whether merge-policy collision/preservation warnings belong to this caller's flow. */
|
|
741
|
+
readonly warningPolicy: "emit" | "suppress";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Content policy shared by every writer of the canonical Codex model catalog. */
|
|
745
|
+
export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
|
|
746
|
+
Pick<ObservedCatalogMergePolicy, "nativeBackfillSlugs" | "unsupportedNativeEntries">
|
|
747
|
+
> = Object.freeze({
|
|
748
|
+
nativeBackfillSlugs: Object.freeze([...NATIVE_OPENAI_MODELS]),
|
|
749
|
+
unsupportedNativeEntries: "drop",
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
export interface ObservedCatalogMergeInput {
|
|
753
|
+
readonly catalogModels: readonly RawEntry[];
|
|
754
|
+
readonly baselineCatalogModels: readonly RawEntry[];
|
|
755
|
+
readonly routedEntries: readonly RawEntry[];
|
|
756
|
+
readonly baseline: ReadonlyMap<string, number>;
|
|
757
|
+
readonly featured: readonly string[];
|
|
758
|
+
readonly wsEnabled: boolean;
|
|
759
|
+
readonly template: RawEntry | null;
|
|
760
|
+
readonly disabledModels: ReadonlySet<string>;
|
|
761
|
+
readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
|
|
762
|
+
readonly gatheredProviderNames: ReadonlySet<string>;
|
|
763
|
+
readonly degradedProviderNames: ReadonlySet<string>;
|
|
764
|
+
readonly legacyCustomModelSlugs: ReadonlySet<string>;
|
|
765
|
+
readonly multiAgentMode: MultiAgentMode;
|
|
766
|
+
readonly multiAgentV2Enabled: boolean;
|
|
767
|
+
readonly keepNativeChatGptOnV1?: boolean;
|
|
768
|
+
readonly exactComboSlugs: ReadonlySet<string>;
|
|
769
|
+
readonly hasPhysicalComboProvider: boolean;
|
|
770
|
+
readonly includeNativeOpenAi: boolean;
|
|
771
|
+
readonly accountBoundEntries: readonly RawEntry[];
|
|
772
|
+
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
773
|
+
readonly policy: ObservedCatalogMergePolicy;
|
|
774
|
+
readonly openaiContextCap?: NativeContextLimitsInput;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Deterministically merge one fully observed catalog state.
|
|
779
|
+
*
|
|
780
|
+
* Every non-catalog input is explicit so evidence-bound convergence cannot
|
|
781
|
+
* accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
|
|
782
|
+
*/
|
|
783
|
+
export function mergeCatalogEntriesFromObservedState({
|
|
784
|
+
catalogModels,
|
|
785
|
+
baselineCatalogModels,
|
|
786
|
+
routedEntries,
|
|
787
|
+
baseline,
|
|
788
|
+
featured,
|
|
789
|
+
wsEnabled,
|
|
790
|
+
template,
|
|
791
|
+
disabledModels,
|
|
792
|
+
selectedModelsByProvider,
|
|
793
|
+
gatheredProviderNames,
|
|
794
|
+
degradedProviderNames,
|
|
795
|
+
legacyCustomModelSlugs,
|
|
796
|
+
multiAgentMode,
|
|
797
|
+
multiAgentV2Enabled,
|
|
798
|
+
keepNativeChatGptOnV1,
|
|
799
|
+
exactComboSlugs,
|
|
800
|
+
hasPhysicalComboProvider,
|
|
801
|
+
includeNativeOpenAi,
|
|
802
|
+
accountBoundEntries,
|
|
803
|
+
suppressedBareNativeSlugs = new Set(),
|
|
804
|
+
policy,
|
|
805
|
+
openaiContextCap,
|
|
806
|
+
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
807
|
+
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
808
|
+
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
809
|
+
const detachedCatalogModels = catalogModels.map(entry => structuredClone(entry) as RawEntry);
|
|
810
|
+
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
811
|
+
.map(entry => structuredClone(entry) as RawEntry);
|
|
812
|
+
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
813
|
+
const detachedAccountBoundEntries = accountBoundEntries
|
|
814
|
+
.map(entry => structuredClone(entry) as RawEntry);
|
|
815
|
+
const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
|
|
816
|
+
const legacyCustomModelKeys = new Set(
|
|
817
|
+
[...legacyCustomModelSlugs].map(slugEquivalenceKey),
|
|
818
|
+
);
|
|
819
|
+
const selectedModelKeysByProvider = new Map([...selectedModelsByProvider].map(([provider, models]) => (
|
|
820
|
+
[provider, new Set([...models].map(model => slugEquivalenceKey(routedSlug(provider, model))))] as const
|
|
821
|
+
)));
|
|
822
|
+
const freshAccountKeys = new Set(detachedAccountBoundEntries.flatMap(entry => (
|
|
823
|
+
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
824
|
+
)));
|
|
825
|
+
const wouldSurviveUnreplaced = (entry: RawEntry): boolean => {
|
|
826
|
+
if (entry.owned_by === COMBO_NAMESPACE
|
|
827
|
+
|| trustedAccountBoundNativeCatalogSlug(entry) !== undefined
|
|
828
|
+
|| entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
829
|
+
|| isOcxAuthoredRoutedEntry(entry)
|
|
830
|
+
|| typeof entry.slug !== "string") return false;
|
|
831
|
+
const slug = entry.slug;
|
|
832
|
+
if (!slug.includes("/")) {
|
|
833
|
+
if (!includeNativeOpenAi || policy.nativeBackfillSlugs.includes(slug)) return false;
|
|
834
|
+
return policy.unsupportedNativeEntries === "preserve" || !isUnsupportedOpenAiNativeSlug(slug);
|
|
835
|
+
}
|
|
836
|
+
if (isRoutedModelCompatibilityExcluded(slug)) return false;
|
|
837
|
+
if (!hasPhysicalComboProvider && slug.startsWith(`${COMBO_NAMESPACE}/`)) return false;
|
|
838
|
+
const key = slugEquivalenceKey(slug);
|
|
839
|
+
if (freshAccountKeys.has(key)) return false;
|
|
840
|
+
if (disabledModelKeys.has(key)) return false;
|
|
841
|
+
const slash = slug.indexOf("/");
|
|
842
|
+
const provider = slug.slice(0, slash);
|
|
843
|
+
const selected = selectedModelKeysByProvider.get(provider);
|
|
844
|
+
if (selected !== undefined && !selected.has(key)) return false;
|
|
845
|
+
return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
|
|
846
|
+
};
|
|
847
|
+
const validRoutedEntries = detachedRoutedEntries.filter(entry => {
|
|
848
|
+
return !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
849
|
+
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
850
|
+
});
|
|
851
|
+
const restorableCatalogKeys = new Set(detachedBaselineCatalogModels.flatMap(entry => (
|
|
852
|
+
wouldSurviveUnreplaced(entry) && typeof entry.slug === "string"
|
|
853
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
854
|
+
: []
|
|
855
|
+
)));
|
|
856
|
+
const unrestorableCatalogKeys = new Set(detachedCatalogModels.flatMap(entry => {
|
|
857
|
+
if (!wouldSurviveUnreplaced(entry) || typeof entry.slug !== "string") return [];
|
|
858
|
+
const key = slugEquivalenceKey(entry.slug);
|
|
859
|
+
return restorableCatalogKeys.has(key) ? [] : [key];
|
|
860
|
+
}));
|
|
861
|
+
const admittedRoutedEntries = validRoutedEntries.filter(entry => {
|
|
862
|
+
if (!isExactComboCatalogEntry(entry, exactComboSlugs)) return true;
|
|
863
|
+
const slug = entry.slug as string;
|
|
864
|
+
const key = slugEquivalenceKey(slug);
|
|
865
|
+
if (!unrestorableCatalogKeys.has(key)) return true;
|
|
866
|
+
if (policy.warningPolicy === "emit") warnComboUnrestorableShadowOnce(slug);
|
|
867
|
+
return false;
|
|
868
|
+
});
|
|
869
|
+
// A fresh non-custom row authoritatively resolves a historically ambiguous slug as a normal
|
|
870
|
+
// provider model. Persist that classification so the durable deletion evidence cannot remove
|
|
871
|
+
// the legitimate row during a later degraded refresh.
|
|
872
|
+
for (const entry of admittedRoutedEntries) {
|
|
873
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
874
|
+
if (!slug
|
|
875
|
+
|| entry.opencodex_catalog_kind !== undefined
|
|
876
|
+
|| entry.owned_by === COMBO_NAMESPACE
|
|
877
|
+
|| !isOcxAuthoredRoutedEntry(entry)
|
|
878
|
+
|| !legacyCustomModelKeys.has(slugEquivalenceKey(slug))) continue;
|
|
879
|
+
entry.opencodex_catalog_kind = CODEX_PROVIDER_MODEL_CATALOG_KIND;
|
|
880
|
+
}
|
|
881
|
+
const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
|
|
882
|
+
isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
883
|
+
&& typeof entry.description === "string"
|
|
884
|
+
&& entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
|
|
885
|
+
)));
|
|
371
886
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
372
|
-
const
|
|
373
|
-
?
|
|
887
|
+
const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
888
|
+
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
889
|
+
)));
|
|
890
|
+
const freshEquivalent = (slug: string): boolean => (
|
|
891
|
+
freshEquivalentKeys.has(slugEquivalenceKey(slug))
|
|
892
|
+
);
|
|
893
|
+
const freshBareComboAliases = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
894
|
+
typeof entry.slug === "string"
|
|
895
|
+
&& !entry.slug.includes("/")
|
|
896
|
+
&& entry.owned_by === COMBO_NAMESPACE
|
|
897
|
+
? [entry.slug]
|
|
898
|
+
: []
|
|
899
|
+
)));
|
|
900
|
+
const staleComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
901
|
+
typeof entry.slug === "string"
|
|
902
|
+
&& entry.owned_by === COMBO_NAMESPACE
|
|
903
|
+
&& !freshEquivalent(entry.slug)
|
|
904
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
905
|
+
: []
|
|
906
|
+
)));
|
|
907
|
+
const currentNonComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
908
|
+
entry.owned_by !== COMBO_NAMESPACE && typeof entry.slug === "string"
|
|
909
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
910
|
+
: []
|
|
911
|
+
)));
|
|
912
|
+
const restoredComboShadows = detachedBaselineCatalogModels.filter(entry => {
|
|
913
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
914
|
+
if (!slug || entry.owned_by === COMBO_NAMESPACE) return false;
|
|
915
|
+
const key = slugEquivalenceKey(slug);
|
|
916
|
+
return staleComboKeys.has(key) && !currentNonComboKeys.has(key);
|
|
917
|
+
});
|
|
918
|
+
const catalogModelsForMerge = [...detachedCatalogModels, ...restoredComboShadows];
|
|
919
|
+
const nativePriority = (slug: string, fallback: unknown): number => {
|
|
920
|
+
const base = baseline.get(slug)
|
|
921
|
+
?? (typeof fallback === "number" ? fallback : 9);
|
|
922
|
+
if (rank.has(slug)) return rank.get(slug)!;
|
|
923
|
+
return featured.length > 0 ? Math.max(base, featured.length + 100) : base;
|
|
924
|
+
};
|
|
925
|
+
const nativeSourceEntries = includeNativeOpenAi
|
|
926
|
+
? catalogModelsForMerge
|
|
374
927
|
.filter(m => typeof m.slug === "string"
|
|
375
928
|
&& !(m.slug as string).includes("/")
|
|
376
929
|
&& m.owned_by !== COMBO_NAMESPACE
|
|
377
|
-
&&
|
|
378
|
-
|
|
930
|
+
&& (policy.unsupportedNativeEntries === "preserve"
|
|
931
|
+
|| policy.nativeBackfillSlugs.includes(m.slug as string)
|
|
932
|
+
|| !isUnsupportedOpenAiNativeSlug(m.slug as string)))
|
|
379
933
|
.map(m => {
|
|
380
934
|
const slug = m.slug as string;
|
|
381
|
-
// Featured models rank first (rank order); non-featured natives are pushed below the featured
|
|
382
|
-
// block when any model is featured, else keep their pristine baseline priority.
|
|
383
|
-
const baselinePriority = baseline.get(slug) ?? (m.priority as number);
|
|
384
|
-
const priority = rank.has(slug)
|
|
385
|
-
? rank.get(slug)!
|
|
386
|
-
: featured.length > 0
|
|
387
|
-
? Math.max(typeof baselinePriority === "number" ? baselinePriority : 9, featured.length + 100)
|
|
388
|
-
: baselinePriority;
|
|
389
935
|
// Fallback-quality entries (ocx synthesis / codex-rs model_info fallback: display_name
|
|
390
936
|
// stamped with the bare slug) are upgraded to the pinned upstream snapshot entry so a
|
|
391
937
|
// previously synthesized ladder (e.g. luna advertising ultra) self-heals on sync. A
|
|
392
938
|
// genuine catalog entry (real display name) is preserved untouched.
|
|
393
939
|
if (shouldUpgradeToUpstreamEntry(m)) {
|
|
394
940
|
const upstream = upstreamNativeEntry(slug)!;
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
: featured.length > 0
|
|
398
|
-
? Math.max(typeof upstream.priority === "number" ? upstream.priority : 9, featured.length + 100)
|
|
399
|
-
: typeof upstream.priority === "number" ? upstream.priority : priority;
|
|
400
|
-
const finished = finishUpstreamNativeEntry(upstream, 9);
|
|
401
|
-
finished.priority = upgradePriority;
|
|
941
|
+
const finished = finishUpstreamNativeEntry(upstream, 9, openaiContextCap);
|
|
942
|
+
finished.priority = nativePriority(slug, upstream.priority);
|
|
402
943
|
return finished;
|
|
403
944
|
}
|
|
404
|
-
const preserved = normalizeServiceTiers({ ...m, priority });
|
|
945
|
+
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
|
|
405
946
|
// Older natives kept from disk still need the mock top tiers (max + ultra always
|
|
406
947
|
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
407
948
|
if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
|
|
408
949
|
return preserved;
|
|
409
950
|
})
|
|
410
951
|
: [];
|
|
952
|
+
const native = nativeSourceEntries.filter(entry =>
|
|
953
|
+
typeof entry.slug !== "string"
|
|
954
|
+
|| (!freshBareComboAliases.has(entry.slug) && !suppressedBareNativeSlugs.has(entry.slug))
|
|
955
|
+
);
|
|
411
956
|
|
|
412
957
|
// Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
|
|
413
958
|
// routed provider exposing the same id can never delete the native OpenAI/Codex base row.
|
|
414
959
|
// Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
|
|
415
960
|
const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
416
961
|
if (includeNativeOpenAi) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
962
|
+
for (const slug of policy.nativeBackfillSlugs) {
|
|
963
|
+
if (nativeSlugs.has(slug) || freshBareComboAliases.has(slug) || suppressedBareNativeSlugs.has(slug)) continue;
|
|
964
|
+
nativeSlugs.add(slug);
|
|
965
|
+
const entry = deriveEntry(
|
|
966
|
+
template ? JSON.parse(JSON.stringify(template)) : null,
|
|
967
|
+
slug,
|
|
968
|
+
"OpenAI native model (Codex OAuth passthrough).",
|
|
969
|
+
nativePriority(slug, upstreamNativeEntry(slug)?.priority),
|
|
970
|
+
undefined,
|
|
971
|
+
new Set(),
|
|
972
|
+
openaiContextCap,
|
|
973
|
+
);
|
|
974
|
+
entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority);
|
|
975
|
+
native.push(entry);
|
|
976
|
+
}
|
|
427
977
|
}
|
|
428
978
|
|
|
979
|
+
const nativeSourceBySlug = new Map([...nativeSourceEntries, ...native].flatMap(entry =>
|
|
980
|
+
typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
|
|
981
|
+
));
|
|
982
|
+
const alignedAccountBoundEntries = detachedAccountBoundEntries.map(entry => {
|
|
983
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
984
|
+
const source = nativeSlug === undefined ? undefined : nativeSourceBySlug.get(nativeSlug);
|
|
985
|
+
if (!source) return entry;
|
|
986
|
+
const aligned = JSON.parse(JSON.stringify(source)) as RawEntry;
|
|
987
|
+
aligned.slug = entry.slug;
|
|
988
|
+
aligned.display_name = entry.display_name;
|
|
989
|
+
aligned.priority = entry.priority;
|
|
990
|
+
aligned.visibility = "list";
|
|
991
|
+
aligned.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
992
|
+
return aligned;
|
|
993
|
+
});
|
|
994
|
+
|
|
429
995
|
const freshSlugs = new Set(
|
|
430
|
-
|
|
996
|
+
admittedRoutedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
431
997
|
);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
&&
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
998
|
+
const existingRoutedEntries = catalogModelsForMerge.filter(m =>
|
|
999
|
+
typeof m.slug === "string"
|
|
1000
|
+
&& (m.slug.includes("/") || isNativeAliasCatalogEntry(m))
|
|
1001
|
+
&& trustedAccountBoundNativeCatalogSlug(m) === undefined
|
|
1002
|
+
);
|
|
1003
|
+
const preservedRoutedEntries = existingRoutedEntries.filter(entry => {
|
|
1004
|
+
const slug = entry.slug as string;
|
|
1005
|
+
if (freshEquivalent(slug)) return false;
|
|
1006
|
+
if (isNativeAliasCatalogEntry(entry)) return exactComboSlugs.has(slug);
|
|
1007
|
+
// Current custom rows are always regenerated from config, even while provider discovery is
|
|
1008
|
+
// degraded. A marked row absent from the fresh projection is therefore an intentional delete.
|
|
1009
|
+
if (entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND) return false;
|
|
1010
|
+
// Before custom rows had a marker, a config deletion could otherwise be mistaken for a
|
|
1011
|
+
// provider outage. Only explicit save-boundary evidence may classify an unmarked OpenCodex
|
|
1012
|
+
// row; foreign and future-marked rows fail closed and remain preserved.
|
|
1013
|
+
if (entry.opencodex_catalog_kind === undefined
|
|
1014
|
+
&& entry.owned_by !== COMBO_NAMESPACE
|
|
1015
|
+
&& isOcxAuthoredRoutedEntry(entry)
|
|
1016
|
+
&& legacyCustomModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
1017
|
+
const provider = slug.slice(0, slug.indexOf("/"));
|
|
1018
|
+
if (gatheredProviderNames.has(provider)) {
|
|
1019
|
+
// A provider-local degraded observation preserves only that namespace. Authoritative empty
|
|
1020
|
+
// catalogs and successful removals still delete stale rows even when another provider fails.
|
|
1021
|
+
return degradedProviderNames.has(provider);
|
|
1022
|
+
}
|
|
1023
|
+
// Deleted/disabled providers cannot retain OpenCodex-authored ghosts. Foreign catalog rows
|
|
1024
|
+
// remain outside provider ownership and survive unless a fresh row replaces their exact slug.
|
|
1025
|
+
return !isOcxAuthoredRoutedEntry(entry);
|
|
1026
|
+
});
|
|
1027
|
+
let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
|
|
1028
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1029
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1030
|
+
if (!slug.includes("/")) return true;
|
|
1031
|
+
if (disabledModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
1032
|
+
// Provider allowlists own provider rows, not a current combo's public alias. Exempt only an
|
|
1033
|
+
// identity from this gather's generated combo projection: provider discovery may supply a
|
|
1034
|
+
// spoofed `owned_by`, and persisted combo-shaped rows are not fresh authority.
|
|
1035
|
+
if (freshExactComboEntries.has(entry)) return true;
|
|
1036
|
+
const slash = slug.indexOf("/");
|
|
1037
|
+
const provider = slug.slice(0, slash);
|
|
1038
|
+
const selected = selectedModelKeysByProvider.get(provider);
|
|
1039
|
+
return selected === undefined || selected.has(slugEquivalenceKey(slug));
|
|
1040
|
+
});
|
|
455
1041
|
if (!hasPhysicalComboProvider) {
|
|
456
1042
|
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
457
1043
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
458
1044
|
const comboOwned = slug.startsWith(`${COMBO_NAMESPACE}/`) || entry.owned_by === COMBO_NAMESPACE;
|
|
459
|
-
|
|
1045
|
+
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
1046
|
+
return !comboOwned || freshSlugs.has(slug) || retainedNativeAlias;
|
|
460
1047
|
});
|
|
461
1048
|
}
|
|
462
1049
|
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
463
1050
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
464
|
-
|
|
1051
|
+
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
1052
|
+
return retainedNativeAlias
|
|
1053
|
+
|| !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
465
1054
|
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
466
1055
|
});
|
|
467
1056
|
// Reapply final catalog policy to rows preserved from disk. Those rows bypass
|
|
@@ -469,17 +1058,40 @@ export function mergeCatalogEntriesForSync(
|
|
|
469
1058
|
finalRoutedEntries = finalRoutedEntries.filter(entry =>
|
|
470
1059
|
typeof entry.slug !== "string" || !isRoutedModelCompatibilityExcluded(entry.slug)
|
|
471
1060
|
);
|
|
472
|
-
|
|
473
|
-
|
|
1061
|
+
const accountBoundSlugs = new Set(alignedAccountBoundEntries.flatMap(entry =>
|
|
1062
|
+
typeof entry.slug === "string" ? [entry.slug] : []
|
|
1063
|
+
));
|
|
1064
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1065
|
+
if (typeof entry.slug !== "string" || !accountBoundSlugs.has(entry.slug)) return true;
|
|
1066
|
+
if (freshSlugs.has(entry.slug) && policy.warningPolicy === "emit") {
|
|
1067
|
+
warnAccountSelectorShadowedProviderOnce(entry.slug);
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
});
|
|
1071
|
+
const finalRoutedEntrySet = new Set(finalRoutedEntries);
|
|
1072
|
+
const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
|
|
1073
|
+
if (!finalRoutedEntrySet.has(entry)) return false;
|
|
1074
|
+
const slug = entry.slug as string;
|
|
1075
|
+
const provider = slug.slice(0, slug.indexOf("/"));
|
|
1076
|
+
return gatheredProviderNames.has(provider) && degradedProviderNames.has(provider);
|
|
1077
|
+
}).length;
|
|
1078
|
+
if (degradedPreservedCount > 0 && policy.warningPolicy === "emit") {
|
|
1079
|
+
console.warn(`[opencodex] catalog sync: provider discovery degraded; preserving ${degradedPreservedCount} existing routed entr${degradedPreservedCount === 1 ? "y" : "ies"} on disk.`);
|
|
474
1080
|
}
|
|
475
1081
|
|
|
476
|
-
const
|
|
1082
|
+
const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
|
|
1083
|
+
const observedNativeSlugs = new Set(alignedAccountBoundEntries.flatMap(entry => {
|
|
1084
|
+
const slug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
1085
|
+
return slug === undefined ? [] : [slug];
|
|
1086
|
+
}));
|
|
1087
|
+
for (const slug of policy.nativeBackfillSlugs) observedNativeSlugs.add(slug);
|
|
1088
|
+
const mergedEntries = [...native, ...managedEntries].map(m => {
|
|
477
1089
|
const normalized = normalizeServiceTiers(m);
|
|
478
|
-
applyNativeOpenAiContextOverride(normalized);
|
|
479
|
-
const exactCombo =
|
|
1090
|
+
if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
|
|
1091
|
+
const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
|
|
480
1092
|
const e = ensureStrictCatalogFields(normalized, {
|
|
481
1093
|
preserveExactInputModalities: exactCombo,
|
|
482
|
-
isRouted:
|
|
1094
|
+
isRouted: finalRoutedEntrySet.has(m),
|
|
483
1095
|
});
|
|
484
1096
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
485
1097
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
@@ -502,35 +1114,329 @@ export function mergeCatalogEntriesForSync(
|
|
|
502
1114
|
}
|
|
503
1115
|
return e;
|
|
504
1116
|
});
|
|
505
|
-
// Native enable/disable
|
|
506
|
-
//
|
|
507
|
-
|
|
1117
|
+
// Native enable/disable runs as the LAST pass so the upstream-upgrade branch above can never
|
|
1118
|
+
// clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
|
|
1119
|
+
// only their generated account row.
|
|
1120
|
+
const versionedEntries = applyMultiAgentMode(
|
|
1121
|
+
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
|
|
1122
|
+
multiAgentMode,
|
|
1123
|
+
multiAgentV2Enabled,
|
|
1124
|
+
{ keepNativeChatGptOnV1 },
|
|
1125
|
+
);
|
|
1126
|
+
for (const entry of versionedEntries) {
|
|
1127
|
+
const kind = entry.opencodex_catalog_kind;
|
|
1128
|
+
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
1129
|
+
&& kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
1130
|
+
&& kind !== CODEX_PROVIDER_MODEL_CATALOG_KIND) continue;
|
|
1131
|
+
// Canonicalize extension-field order after every normalizer. This keeps an unchanged catalog
|
|
1132
|
+
// byte-idempotent whether an owned row was freshly built or retained from the prior pass.
|
|
1133
|
+
delete entry.opencodex_catalog_kind;
|
|
1134
|
+
entry.opencodex_catalog_kind = kind;
|
|
1135
|
+
}
|
|
1136
|
+
return versionedEntries;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/** Merge retained-sync rows using the process-observed Codex feature state. */
|
|
1140
|
+
export function mergeCatalogEntriesForSync(
|
|
1141
|
+
catalogModels: RawEntry[],
|
|
1142
|
+
routedEntries: RawEntry[],
|
|
1143
|
+
baseline: Map<string, number>,
|
|
1144
|
+
featured: string[],
|
|
1145
|
+
wsEnabled: boolean,
|
|
1146
|
+
_goIds: Set<string> = new Set(),
|
|
1147
|
+
template: RawEntry | null = null,
|
|
1148
|
+
disabledModels: ReadonlySet<string> = new Set(),
|
|
1149
|
+
gatheredProviderNames?: Set<string>,
|
|
1150
|
+
multiAgentMode: MultiAgentMode = "default",
|
|
1151
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
1152
|
+
hasPhysicalComboProvider = false,
|
|
1153
|
+
includeNativeOpenAi = true,
|
|
1154
|
+
accountBoundEntries: readonly RawEntry[] = [],
|
|
1155
|
+
legacyCustomModelSlugs: ReadonlySet<string> = new Set(),
|
|
1156
|
+
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(
|
|
1157
|
+
routedEntries.flatMap(entry => (
|
|
1158
|
+
isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
|
|
1159
|
+
)),
|
|
1160
|
+
),
|
|
1161
|
+
openaiContextCap?: number,
|
|
1162
|
+
keepNativeChatGptOnV1 = false,
|
|
1163
|
+
): RawEntry[] {
|
|
1164
|
+
// Retained for source compatibility with the original helper contract. Raw provider ids must
|
|
1165
|
+
// not suppress same-named native rows; actual admitted combo entries own that decision now.
|
|
1166
|
+
void _goIds;
|
|
1167
|
+
const effectiveGatheredProviderNames = gatheredProviderNames ?? new Set(
|
|
1168
|
+
routedEntries.flatMap(entry => {
|
|
1169
|
+
// A slashed combo alias is not evidence that its public prefix is an authoritative provider
|
|
1170
|
+
// namespace. Treating it as one would let the combo replace an unrestorable foreign row.
|
|
1171
|
+
if (isExactComboCatalogEntry(entry, exactComboSlugs)) return [];
|
|
1172
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1173
|
+
const slash = slug.indexOf("/");
|
|
1174
|
+
return slash > 0 ? [slug.slice(0, slash)] : [];
|
|
1175
|
+
}),
|
|
1176
|
+
);
|
|
1177
|
+
return mergeCatalogEntriesFromObservedState({
|
|
1178
|
+
catalogModels,
|
|
1179
|
+
baselineCatalogModels: [],
|
|
1180
|
+
routedEntries,
|
|
1181
|
+
baseline,
|
|
1182
|
+
featured,
|
|
1183
|
+
wsEnabled,
|
|
1184
|
+
template,
|
|
1185
|
+
disabledModels,
|
|
1186
|
+
selectedModelsByProvider: new Map(),
|
|
1187
|
+
gatheredProviderNames: effectiveGatheredProviderNames,
|
|
1188
|
+
degradedProviderNames: new Set(),
|
|
1189
|
+
legacyCustomModelSlugs,
|
|
1190
|
+
multiAgentMode,
|
|
1191
|
+
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
1192
|
+
keepNativeChatGptOnV1,
|
|
1193
|
+
exactComboSlugs,
|
|
1194
|
+
hasPhysicalComboProvider,
|
|
1195
|
+
includeNativeOpenAi,
|
|
1196
|
+
accountBoundEntries,
|
|
1197
|
+
suppressedBareNativeSlugs,
|
|
1198
|
+
openaiContextCap,
|
|
1199
|
+
policy: {
|
|
1200
|
+
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1201
|
+
warningPolicy: "emit",
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
interface RetainedCatalogSyncRead {
|
|
1207
|
+
readonly catalogPath: string;
|
|
1208
|
+
readonly catalog: RawCatalog;
|
|
1209
|
+
readonly onDiskCatalog: RawCatalog | null;
|
|
1210
|
+
readonly modelsCache: RawCatalog | null;
|
|
1211
|
+
readonly evidence: string;
|
|
1212
|
+
/**
|
|
1213
|
+
* Process-local epochs, baselined AFTER our own gather rather than with the
|
|
1214
|
+
* filesystem bytes above. See `retainedCatalogProcessEvidence`.
|
|
1215
|
+
*/
|
|
1216
|
+
readonly processEvidence: string;
|
|
508
1217
|
}
|
|
509
1218
|
|
|
510
|
-
|
|
1219
|
+
interface RetainedCatalogSyncResult {
|
|
511
1220
|
added: number;
|
|
512
1221
|
path: string;
|
|
513
1222
|
catalogWritten: boolean;
|
|
514
1223
|
comboOmissions: ComboCatalogOmission[];
|
|
515
|
-
|
|
1224
|
+
/** `desired_disabled` observed under K after the provider await; nothing was written. */
|
|
1225
|
+
skippedReason?: "desired_disabled";
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Catalog/cache commit overrides.
|
|
1230
|
+
*
|
|
1231
|
+
* An explicit `ocx sync` is also the refresh path for side profiles that consume
|
|
1232
|
+
* the OpenCodex catalog without injection (for example a custom `model_provider`
|
|
1233
|
+
* that routes to the proxy). In that mode the Codex integration toggle only
|
|
1234
|
+
* governs config/history injection; the catalog and models cache may still be
|
|
1235
|
+
* refreshed, so `allowWhenDesiredDisabled` lets the commit path ignore the OFF
|
|
1236
|
+
* gate that otherwise protects a fully native home.
|
|
1237
|
+
*/
|
|
1238
|
+
export interface CodexCatalogSyncOptions {
|
|
1239
|
+
allowWhenDesiredDisabled?: boolean;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
interface RetainedCatalogSyncWrite {
|
|
1243
|
+
readonly config: OcxConfig;
|
|
1244
|
+
readonly goModels: CatalogModel[];
|
|
1245
|
+
readonly providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
1246
|
+
readonly comboOmissions: ComboCatalogOmission[];
|
|
1247
|
+
readonly read: RetainedCatalogSyncRead;
|
|
1248
|
+
readonly permit: CatalogWritePermit;
|
|
1249
|
+
readonly owningCodexHome: string;
|
|
1250
|
+
readonly modelEntitlements: CodexModelEntitlementSnapshot;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function optionalFileBytes(path: string): string | null {
|
|
1254
|
+
try {
|
|
1255
|
+
return readFileSync(path).toString("base64");
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") return null;
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function loadCatalogForRetainedSync(path: string): RawCatalog | null {
|
|
1263
|
+
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
1264
|
+
if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
|
|
1265
|
+
const active = readCatalog(path);
|
|
1266
|
+
// A valid configured custom file remains the content authority even when it has no bare native
|
|
1267
|
+
// template. The null-template builder is deliberate; a stale backup must not replace active
|
|
1268
|
+
// custom root metadata merely because the current file contains only routed rows.
|
|
1269
|
+
if (active && (!isDefaultCatalogPath(path) || findNativeTemplate(active))) return active;
|
|
1270
|
+
return readCatalog(catalogBackupPathFor(path))
|
|
1271
|
+
?? (isDefaultCatalogPath(path) ? readCatalog(legacyCatalogBackupPath()) : null)
|
|
1272
|
+
?? readCatalog(activeCodexModelsCachePath())
|
|
1273
|
+
?? active;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function retainedCatalogSyncEvidence(
|
|
1277
|
+
config: OcxConfig,
|
|
1278
|
+
catalogPath: string,
|
|
1279
|
+
catalog: RawCatalog,
|
|
1280
|
+
): string {
|
|
1281
|
+
return JSON.stringify({
|
|
1282
|
+
config,
|
|
1283
|
+
catalogPath,
|
|
1284
|
+
catalog,
|
|
1285
|
+
catalogBytes: optionalFileBytes(catalogPath),
|
|
1286
|
+
hashedBackupBytes: optionalFileBytes(catalogBackupPathFor(catalogPath)),
|
|
1287
|
+
legacyBackupBytes: isDefaultCatalogPath(catalogPath)
|
|
1288
|
+
? optionalFileBytes(legacyCatalogBackupPath()) : null,
|
|
1289
|
+
modelsCacheBytes: optionalFileBytes(activeCodexModelsCachePath()),
|
|
1290
|
+
// The persisted runtime selection is a pre-await filesystem input, not a
|
|
1291
|
+
// process epoch: another PROCESS can move runtime authority by rewriting this
|
|
1292
|
+
// file, and that move is invisible to our in-process memo. Recorded PRESENT or
|
|
1293
|
+
// ABSENT, because its absence is what makes the resolver fall back.
|
|
1294
|
+
runtimeStateBytes: optionalFileBytes(codexRuntimeStatePath()),
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* The bundled-template half of the same evidence, observed separately.
|
|
1300
|
+
*
|
|
1301
|
+
* The runtime process memo is deliberately NOT here, and that exclusion took three
|
|
1302
|
+
* attempts to get honest. Gathering resolves the Codex runtime lazily and under its
|
|
1303
|
+
* own cache key, so this path cannot pre-settle that memo: baselining it before the
|
|
1304
|
+
* await always detected our own side effect and refused every write, and baselining
|
|
1305
|
+
* it after the await captured a runtime that ANOTHER process had moved as though it
|
|
1306
|
+
* were ours — a catalog prepared from R1 committing after authority reached R2.
|
|
1307
|
+
*
|
|
1308
|
+
* Runtime authority is covered where it is actually durable instead: the persisted
|
|
1309
|
+
* `codex-runtime.json` bytes sit in the pre-await filesystem evidence, PRESENT or
|
|
1310
|
+
* ABSENT, so a cross-process runtime move is caught. What is left uncovered, and is
|
|
1311
|
+
* written down rather than papered over, is a same-process in-memory runtime swap
|
|
1312
|
+
* that never touches that file — WP11 owns the lock that makes that case decidable.
|
|
1313
|
+
*/
|
|
1314
|
+
function retainedCatalogProcessEvidence(): string {
|
|
1315
|
+
return JSON.stringify({
|
|
1316
|
+
bundledCatalogCache: bundledCatalogCacheState(),
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Capture every local catalog input the retained sync path consults before its
|
|
1322
|
+
* provider await. The exact evidence is compared after K acquisition; a newer
|
|
1323
|
+
* catalog/backup/cache or target selection makes this attempt a no-write.
|
|
1324
|
+
*/
|
|
1325
|
+
function readRetainedCatalogSync(config: OcxConfig): RetainedCatalogSyncRead | null {
|
|
516
1326
|
const catalogPath = readCodexCatalogPath();
|
|
517
|
-
const catalog =
|
|
518
|
-
if (!catalog) return
|
|
1327
|
+
const catalog = loadCatalogForRetainedSync(catalogPath);
|
|
1328
|
+
if (!catalog) return null;
|
|
519
1329
|
|
|
520
1330
|
// The bundled catalog is a reliable native template on the default path, but it is not the
|
|
521
1331
|
// merge source. Preservation must inspect the file that this sync is about to overwrite;
|
|
522
1332
|
// otherwise an empty/partial provider gather cannot see routed or user-native rows on disk.
|
|
523
1333
|
const onDiskCatalog = readCatalog(catalogPath);
|
|
524
|
-
const
|
|
1334
|
+
const modelsCache = readCatalog(activeCodexModelsCachePath());
|
|
1335
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, catalog);
|
|
1336
|
+
// `processEvidence` is filled in after the provider await, not here.
|
|
1337
|
+
return { catalogPath, catalog, onDiskCatalog, modelsCache, evidence, processEvidence: "" };
|
|
1338
|
+
}
|
|
525
1339
|
|
|
1340
|
+
function revalidateRetainedCatalogSync(
|
|
1341
|
+
config: OcxConfig,
|
|
1342
|
+
prepared: RetainedCatalogSyncRead,
|
|
1343
|
+
): RetainedCatalogSyncRead | null {
|
|
1344
|
+
const catalogPath = readCodexCatalogPath();
|
|
1345
|
+
if (catalogPath !== prepared.catalogPath) return null;
|
|
1346
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, prepared.catalog);
|
|
1347
|
+
if (evidence !== prepared.evidence) return null;
|
|
1348
|
+
if (retainedCatalogProcessEvidence() !== prepared.processEvidence) return null;
|
|
1349
|
+
return {
|
|
1350
|
+
catalogPath,
|
|
1351
|
+
catalog: JSON.parse(JSON.stringify(prepared.catalog)) as RawCatalog,
|
|
1352
|
+
onDiskCatalog: readCatalog(catalogPath),
|
|
1353
|
+
modelsCache: readCatalog(activeCodexModelsCachePath()),
|
|
1354
|
+
evidence,
|
|
1355
|
+
processEvidence: prepared.processEvidence,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Exact bytes currently on disk at `path`, or null when unreadable/absent.
|
|
1361
|
+
*
|
|
1362
|
+
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
1363
|
+
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
1364
|
+
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
|
|
1365
|
+
* decoded strings would then classify a malformed catalog as identical, skip the
|
|
1366
|
+
* atomic repair write, and leave the corruption on disk while reporting
|
|
1367
|
+
* `catalogWritten: false`.
|
|
1368
|
+
*/
|
|
1369
|
+
function currentCatalogFileContent(path: string): Buffer | null {
|
|
1370
|
+
try {
|
|
1371
|
+
return readFileSync(path);
|
|
1372
|
+
} catch {
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
1378
|
+
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
1379
|
+
try {
|
|
1380
|
+
return readFileSync(read.catalogPath, "utf8");
|
|
1381
|
+
} catch {
|
|
1382
|
+
return null;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return catalogHasRoutedEntries(read.catalog)
|
|
1386
|
+
? null
|
|
1387
|
+
: `${JSON.stringify(read.catalog, null, 2)}\n`;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function catalogModelsForMergeWithNativeRecovery(
|
|
1391
|
+
catalogPath: string,
|
|
1392
|
+
catalog: RawCatalog,
|
|
1393
|
+
onDiskCatalog: RawCatalog | null,
|
|
1394
|
+
): RawEntry[] {
|
|
1395
|
+
const primaryCatalogModels = onDiskCatalog?.models ?? catalog.models ?? [];
|
|
1396
|
+
// Native-alias compatibility can omit disabled native rows from the effective catalog because
|
|
1397
|
+
// Desktop's remote allowlist ignores `visibility: "hide"`. Keep current/pristine native recovery
|
|
1398
|
+
// sources beside the on-disk rows so re-enabling a model restores its real metadata. Routed and
|
|
1399
|
+
// user-authored rows still come only from the on-disk catalog.
|
|
1400
|
+
return mergeCatalogModelsWithNativeRecovery(primaryCatalogModels, [
|
|
1401
|
+
catalog.models ?? [],
|
|
1402
|
+
readCatalogBackup(catalogPath)?.models ?? [],
|
|
1403
|
+
]);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function writeRetainedCatalogSync({
|
|
1407
|
+
config,
|
|
1408
|
+
goModels,
|
|
1409
|
+
providerModelOutcomes,
|
|
1410
|
+
comboOmissions,
|
|
1411
|
+
read,
|
|
1412
|
+
permit,
|
|
1413
|
+
owningCodexHome,
|
|
1414
|
+
modelEntitlements,
|
|
1415
|
+
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
1416
|
+
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
1417
|
+
const catalogModelsForMerge = catalogModelsForMergeWithNativeRecovery(
|
|
1418
|
+
catalogPath,
|
|
1419
|
+
catalog,
|
|
1420
|
+
onDiskCatalog,
|
|
1421
|
+
);
|
|
526
1422
|
const template = findNativeTemplate(catalog);
|
|
527
1423
|
|
|
528
|
-
const comboOmissions: ComboCatalogOmission[] = [];
|
|
529
|
-
const goModels = await gatherRoutedModels(config, { comboOmissions });
|
|
530
1424
|
try {
|
|
531
1425
|
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
|
532
1426
|
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
533
|
-
|
|
1427
|
+
const pristine = pristineCatalogBytes(read);
|
|
1428
|
+
if (pristine !== null) {
|
|
1429
|
+
publishHashedCodexCatalogBackup(permit, owningCodexHome, {
|
|
1430
|
+
path: catalogBackupPathFor(catalogPath),
|
|
1431
|
+
content: pristine,
|
|
1432
|
+
});
|
|
1433
|
+
if (isDefaultCatalogPath(catalogPath)) {
|
|
1434
|
+
publishLegacyCodexCatalogBackup(permit, owningCodexHome, {
|
|
1435
|
+
path: legacyCatalogBackupPath(),
|
|
1436
|
+
content: pristine,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
534
1440
|
} catch { /* backup best-effort */ }
|
|
535
1441
|
|
|
536
1442
|
// Hide disabled models from Codex, then feature the chosen subagent models (native OR routed)
|
|
@@ -538,83 +1444,440 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
|
|
|
538
1444
|
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
539
1445
|
const featured = config.subagentModels ?? [];
|
|
540
1446
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
1447
|
+
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
541
1448
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
542
1449
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1450
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
1451
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
1452
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
1453
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
1454
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
1455
|
+
modelEntitlements,
|
|
1456
|
+
bareEligibleAccountIds,
|
|
1457
|
+
);
|
|
1458
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
1459
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1460
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
1461
|
+
));
|
|
1462
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
1463
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
1464
|
+
));
|
|
1465
|
+
const unavailableGatedNativeSlugs = new Set([...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => (
|
|
1466
|
+
!availableBareGatedNativeSlugs.has(slug)
|
|
1467
|
+
)));
|
|
1468
|
+
const suppressedBareNativeSlugs = new Set([
|
|
1469
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
1470
|
+
...unavailableGatedNativeSlugs,
|
|
1471
|
+
]);
|
|
543
1472
|
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
544
|
-
const
|
|
1473
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1474
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
1475
|
+
// Both user levers. Passing only the cap here is what let a per-model window the dashboard
|
|
1476
|
+
// had accepted get written back at full width in the on-disk catalog.
|
|
1477
|
+
const openaiContextCap = nativeContextLimits(config);
|
|
1478
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
1479
|
+
? visibleCodexAccountSelectors(config)
|
|
1480
|
+
: [];
|
|
1481
|
+
const observedAccountNativeEntries = [
|
|
1482
|
+
...(read.modelsCache?.models ?? []),
|
|
1483
|
+
...(onDiskCatalog?.models ?? []).filter(entry =>
|
|
1484
|
+
trustedAccountBoundNativeCatalogSlug(entry) !== undefined),
|
|
1485
|
+
];
|
|
1486
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
1487
|
+
const accountNativeSlugsBySelector = accountSelectors.length > 0
|
|
1488
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config, observedAccountNativeEntries)].map(([selector, slugs]) => {
|
|
1489
|
+
const target = accountTargets.get(selector);
|
|
1490
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
1491
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
1492
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
1493
|
+
return [selector, slugs.filter(slug => (
|
|
1494
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
1495
|
+
))] as const;
|
|
1496
|
+
}))
|
|
1497
|
+
: new Map<string, readonly string[]>();
|
|
1498
|
+
const accountNativeSlugs = accountSelectors.length > 0
|
|
1499
|
+
? [...new Set([...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]))]
|
|
1500
|
+
: [];
|
|
1501
|
+
// Unknown account-native ids have no safe bare/global identity. They are only projected through
|
|
1502
|
+
// the selector map above; the no-selector catalog remains the static native/API-key surface.
|
|
1503
|
+
const observedNativeSlugs: string[] = [];
|
|
1504
|
+
const wsEnabled = websocketsEnabled(config);
|
|
1505
|
+
const multiAgentV2Enabled = isMultiAgentV2Enabled();
|
|
1506
|
+
const goEntries = buildCatalogEntriesFromObservedState({
|
|
1507
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1508
|
+
gptSlugs: [],
|
|
1509
|
+
goModels: orderedGoModels,
|
|
1510
|
+
featured,
|
|
1511
|
+
modelPickerOrder,
|
|
1512
|
+
wsEnabled,
|
|
1513
|
+
multiAgentMode,
|
|
1514
|
+
exactComboSlugs,
|
|
1515
|
+
accountSelectors,
|
|
1516
|
+
suppressedBareNativeSlugs,
|
|
1517
|
+
disabledNativeAccountSlugs: new Set(),
|
|
1518
|
+
multiAgentV2Enabled,
|
|
1519
|
+
openaiContextCap,
|
|
1520
|
+
});
|
|
545
1521
|
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
|
|
546
1522
|
// routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
|
|
547
1523
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
1524
|
+
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
548
1525
|
const baseline = readNativeBaseline(catalogPath);
|
|
549
|
-
const goIds = new Set(enabledGo.map(m => m.id));
|
|
550
1526
|
const gatheredProviderNames = new Set(
|
|
551
1527
|
Object.entries(config.providers ?? {})
|
|
552
1528
|
.filter(([, prov]) => prov.disabled !== true)
|
|
553
1529
|
.map(([name]) => name),
|
|
554
1530
|
);
|
|
1531
|
+
const degradedProviderNames = new Set(
|
|
1532
|
+
providerModelOutcomes
|
|
1533
|
+
.filter(outcome => outcome.state === "degraded")
|
|
1534
|
+
.map(outcome => outcome.provider),
|
|
1535
|
+
);
|
|
1536
|
+
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
1537
|
+
Object.entries(config.providers ?? {}).flatMap(([name, provider]) => (
|
|
1538
|
+
provider.disabled !== true
|
|
1539
|
+
&& Array.isArray(provider.selectedModels)
|
|
1540
|
+
&& provider.selectedModels.length > 0
|
|
1541
|
+
? [[name, new Set(provider.selectedModels)] as const]
|
|
1542
|
+
: []
|
|
1543
|
+
)),
|
|
1544
|
+
);
|
|
555
1545
|
// Central WS capability override on the FINAL on-disk catalog (the file Codex reads). Applies to
|
|
556
1546
|
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
557
1547
|
// 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
1548
|
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
|
|
565
1549
|
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
|
|
566
1550
|
// providers are configured yet (fresh install / catalog bootstrap tests).
|
|
567
|
-
const
|
|
568
|
-
|
|
1551
|
+
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
1552
|
+
? buildCatalogEntriesFromObservedState({
|
|
1553
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1554
|
+
gptSlugs: availableAccountNativeSlugs,
|
|
1555
|
+
goModels: [],
|
|
1556
|
+
featured,
|
|
1557
|
+
wsEnabled,
|
|
1558
|
+
multiAgentMode,
|
|
1559
|
+
exactComboSlugs,
|
|
1560
|
+
accountSelectors,
|
|
1561
|
+
suppressedBareNativeSlugs,
|
|
1562
|
+
disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))),
|
|
1563
|
+
multiAgentV2Enabled,
|
|
1564
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
1565
|
+
openaiContextCap,
|
|
1566
|
+
accountNativeSlugs,
|
|
1567
|
+
accountNativeSlugsBySelector,
|
|
1568
|
+
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
1569
|
+
: [];
|
|
1570
|
+
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
1571
|
+
catalogModels: catalogModelsForMerge,
|
|
1572
|
+
baselineCatalogModels: baselineCatalog?.models ?? [],
|
|
1573
|
+
routedEntries: goEntries,
|
|
1574
|
+
baseline,
|
|
1575
|
+
featured,
|
|
1576
|
+
wsEnabled,
|
|
1577
|
+
template,
|
|
1578
|
+
disabledModels: new Set(config.disabledModels ?? []),
|
|
1579
|
+
selectedModelsByProvider,
|
|
1580
|
+
gatheredProviderNames,
|
|
1581
|
+
degradedProviderNames,
|
|
1582
|
+
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
1583
|
+
multiAgentMode,
|
|
1584
|
+
multiAgentV2Enabled,
|
|
1585
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
1586
|
+
exactComboSlugs,
|
|
1587
|
+
hasPhysicalComboProvider,
|
|
1588
|
+
includeNativeOpenAi,
|
|
1589
|
+
accountBoundEntries,
|
|
1590
|
+
suppressedBareNativeSlugs,
|
|
1591
|
+
openaiContextCap,
|
|
1592
|
+
policy: {
|
|
1593
|
+
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1594
|
+
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
1595
|
+
warningPolicy: "emit",
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
569
1598
|
clampCatalogModelsToCodexSupport(catalog.models);
|
|
570
1599
|
|
|
571
|
-
|
|
572
|
-
|
|
1600
|
+
const added = goEntries.length + accountBoundEntries.length;
|
|
1601
|
+
const content = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
1602
|
+
// A byte-identical rewrite is not a catalog change, but every mtime-keyed reader
|
|
1603
|
+
// has to treat it as one. The app-server staleness classifier (#857) is the one
|
|
1604
|
+
// that matters: it compares this file's mtime against each running Codex's start
|
|
1605
|
+
// time, so an ordinary `ocx start` — or any dashboard action that re-syncs an
|
|
1606
|
+
// unchanged model set — marked every already-running Codex as holding an outdated
|
|
1607
|
+
// in-memory catalog. Since #1407 that verdict silences opencodex's own model
|
|
1608
|
+
// guidance entirely (no preferred model, no roster) for the rest of that Codex's
|
|
1609
|
+
// lifetime, so a configured injectionModel stops reaching the session even though
|
|
1610
|
+
// nothing about the catalog changed. Skipping the no-op write keeps both the mtime
|
|
1611
|
+
// and `catalogWritten` honest; `added` still reports the routed rows the catalog
|
|
1612
|
+
// carries, because they are on disk either way.
|
|
1613
|
+
const onDiskBytes = currentCatalogFileContent(catalogPath);
|
|
1614
|
+
if (onDiskBytes !== null && onDiskBytes.equals(Buffer.from(content, "utf8"))) {
|
|
1615
|
+
return { added, path: catalogPath, catalogWritten: false, comboOmissions };
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1619
|
+
path: catalogPath,
|
|
1620
|
+
content,
|
|
1621
|
+
});
|
|
1622
|
+
return {
|
|
1623
|
+
added,
|
|
1624
|
+
path: catalogPath,
|
|
1625
|
+
catalogWritten: true,
|
|
1626
|
+
comboOmissions,
|
|
1627
|
+
};
|
|
573
1628
|
}
|
|
574
1629
|
|
|
575
|
-
|
|
576
|
-
|
|
1630
|
+
function visibleAccountReplacementNatives(
|
|
1631
|
+
models: readonly RawEntry[],
|
|
1632
|
+
disabledModels: ReadonlySet<string> | null,
|
|
1633
|
+
): Map<string, boolean> {
|
|
1634
|
+
const replacements = new Map<string, boolean>();
|
|
1635
|
+
for (const entry of models) {
|
|
1636
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
1637
|
+
if (nativeSlug === undefined || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)) continue;
|
|
1638
|
+
const exactSlug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1639
|
+
const visible = entry.visibility === "list"
|
|
1640
|
+
|| (disabledModels !== null
|
|
1641
|
+
&& (disabledModels.has(nativeSlug) || disabledModels.has(exactSlug)));
|
|
1642
|
+
replacements.set(nativeSlug, (replacements.get(nativeSlug) ?? true) && visible);
|
|
1643
|
+
}
|
|
1644
|
+
return replacements;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function restoreAccountHiddenBareNatives(
|
|
1648
|
+
entries: readonly RawEntry[],
|
|
1649
|
+
replacementVisibility: ReadonlyMap<string, boolean>,
|
|
1650
|
+
disabledModels: ReadonlySet<string> | null,
|
|
1651
|
+
): RawEntry[] {
|
|
1652
|
+
return entries.map(entry => {
|
|
1653
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1654
|
+
if (
|
|
1655
|
+
entry.visibility !== "hide"
|
|
1656
|
+
|| !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
1657
|
+
|| replacementVisibility.get(slug) !== true
|
|
1658
|
+
|| disabledModels === null
|
|
1659
|
+
|| disabledModels.has(slug)
|
|
1660
|
+
) {
|
|
1661
|
+
return entry;
|
|
1662
|
+
}
|
|
1663
|
+
return { ...entry, visibility: "list" };
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function currentDisabledModelsForRestore(): Set<string> | null {
|
|
1668
|
+
try {
|
|
1669
|
+
const diagnostics = readConfigDiagnostics();
|
|
1670
|
+
if (diagnostics.source === "fallback" || diagnostics.error !== null) return null;
|
|
1671
|
+
return new Set(diagnostics.config.disabledModels ?? []);
|
|
1672
|
+
} catch {
|
|
1673
|
+
// An unreadable config cannot safely authorize a visibility change during restore.
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
export async function syncCatalogModels(
|
|
1679
|
+
config: OcxConfig,
|
|
1680
|
+
options?: CodexCatalogSyncOptions,
|
|
1681
|
+
): Promise<RetainedCatalogSyncResult> {
|
|
1682
|
+
const owningCodexHome = getCodexHome();
|
|
1683
|
+
const preflightRead = readRetainedCatalogSync(config);
|
|
1684
|
+
if (preflightRead === null) {
|
|
1685
|
+
return {
|
|
1686
|
+
added: 0,
|
|
1687
|
+
path: readCodexCatalogPath(),
|
|
1688
|
+
catalogWritten: false,
|
|
1689
|
+
comboOmissions: [],
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
const comboOmissions: ComboCatalogOmission[] = [];
|
|
1694
|
+
const providerModelOutcomes: CatalogGatherProviderModelOutcome[] = [];
|
|
1695
|
+
// Settle the bundled template, then baseline, and only then await. Reading it
|
|
1696
|
+
// here makes the memo ours before anyone else can move it, so a bundled swap
|
|
1697
|
+
// during the await is an outside change rather than our own side effect.
|
|
1698
|
+
//
|
|
1699
|
+
// The persisted runtime selection is covered by the filesystem evidence above
|
|
1700
|
+
// rather than by a process epoch; see `retainedCatalogProcessEvidence` for why
|
|
1701
|
+
// the in-memory runtime memo cannot be baselined honestly from this path.
|
|
1702
|
+
loadBundledCodexCatalog();
|
|
1703
|
+
const prepared: RetainedCatalogSyncRead = {
|
|
1704
|
+
...preflightRead,
|
|
1705
|
+
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
1706
|
+
processEvidence: retainedCatalogProcessEvidence(),
|
|
1707
|
+
};
|
|
1708
|
+
const [goModels, modelEntitlements] = await Promise.all([
|
|
1709
|
+
gatherRoutedModels(config, {
|
|
1710
|
+
comboOmissions,
|
|
1711
|
+
providerModelOutcomes,
|
|
1712
|
+
}),
|
|
1713
|
+
resolveCodexModelEntitlements(config),
|
|
1714
|
+
]);
|
|
1715
|
+
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
1716
|
+
// Desired state can flip OFF during the provider await above. The catalog
|
|
1717
|
+
// evidence revalidation below cannot see that — intent lives in our config,
|
|
1718
|
+
// not in the catalog files — so the policy is re-read here, under K, right
|
|
1719
|
+
// before the only write. A lost race becomes the discriminated skip instead
|
|
1720
|
+
// of a routed catalog/cache surviving a completed disable. An explicit
|
|
1721
|
+
// catalog-only sync opts out of that gate: the user asked for a refresh even
|
|
1722
|
+
// when injection is OFF, and the toggle only protects config/history writes.
|
|
1723
|
+
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) {
|
|
1724
|
+
return {
|
|
1725
|
+
added: 0,
|
|
1726
|
+
path: prepared.catalogPath,
|
|
1727
|
+
catalogWritten: false,
|
|
1728
|
+
comboOmissions,
|
|
1729
|
+
skippedReason: "desired_disabled" as const,
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
1733
|
+
if (current === null) return null;
|
|
1734
|
+
if (!isCodexModelEntitlementSnapshotCurrent(modelEntitlements)) return null;
|
|
1735
|
+
return writeRetainedCatalogSync({
|
|
1736
|
+
config,
|
|
1737
|
+
goModels,
|
|
1738
|
+
providerModelOutcomes,
|
|
1739
|
+
comboOmissions,
|
|
1740
|
+
read: current,
|
|
1741
|
+
permit,
|
|
1742
|
+
owningCodexHome,
|
|
1743
|
+
modelEntitlements,
|
|
1744
|
+
});
|
|
1745
|
+
});
|
|
1746
|
+
if (committed.kind === "completed" && committed.value !== null) return committed.value;
|
|
1747
|
+
return {
|
|
1748
|
+
added: 0,
|
|
1749
|
+
path: prepared.catalogPath,
|
|
1750
|
+
catalogWritten: false,
|
|
1751
|
+
comboOmissions,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
export function restoreCodexCatalogWithPermit(
|
|
1756
|
+
permit: CatalogWritePermit,
|
|
1757
|
+
owningCodexHome: string,
|
|
1758
|
+
/**
|
|
1759
|
+
* The catalog this injection actually wrote, when it is known (#1798).
|
|
1760
|
+
*
|
|
1761
|
+
* Re-resolving from the CURRENT config is wrong after a Codex app rewrite that dropped
|
|
1762
|
+
* `model_catalog_json`: that sends restore to the default catalog while the routed file we
|
|
1763
|
+
* really wrote is left untouched. The recorded path is the file whose routing is ours.
|
|
1764
|
+
*/
|
|
1765
|
+
injectedCatalogPath?: string | null,
|
|
1766
|
+
): { removed: number; kept: number; path: string } {
|
|
1767
|
+
const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
|
|
577
1768
|
const catalog = readCatalog(catalogPath);
|
|
578
1769
|
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
1770
|
+
const disabledModels = currentDisabledModelsForRestore();
|
|
1771
|
+
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
579
1772
|
const backup = readCatalogBackup(catalogPath);
|
|
580
1773
|
if (backup && Array.isArray(backup.models)) {
|
|
581
1774
|
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" && m.slug.includes("/")).length;
|
|
582
1775
|
const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
583
|
-
const userNativeAdditions = (
|
|
584
|
-
|
|
1776
|
+
const userNativeAdditions = restoreAccountHiddenBareNatives(
|
|
1777
|
+
(catalog.models ?? []).filter(m =>
|
|
1778
|
+
typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
|
|
1779
|
+
),
|
|
1780
|
+
replacementVisibility,
|
|
1781
|
+
disabledModels,
|
|
585
1782
|
);
|
|
586
1783
|
const restored = {
|
|
587
1784
|
...backup,
|
|
588
1785
|
models: [...backup.models, ...userNativeAdditions],
|
|
589
1786
|
};
|
|
590
|
-
|
|
1787
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1788
|
+
path: catalogPath,
|
|
1789
|
+
content: `${JSON.stringify(restored, null, 2)}\n`,
|
|
1790
|
+
});
|
|
591
1791
|
return { removed, kept: restored.models.length, path: catalogPath };
|
|
592
1792
|
}
|
|
593
1793
|
const before = catalog.models.length;
|
|
594
|
-
const native =
|
|
1794
|
+
const native = restoreAccountHiddenBareNatives(
|
|
1795
|
+
catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/"))),
|
|
1796
|
+
replacementVisibility,
|
|
1797
|
+
disabledModels,
|
|
1798
|
+
);
|
|
595
1799
|
const removed = before - native.length;
|
|
596
1800
|
if (removed > 0) {
|
|
597
1801
|
catalog.models = native;
|
|
598
|
-
|
|
1802
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1803
|
+
path: catalogPath,
|
|
1804
|
+
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
1805
|
+
});
|
|
599
1806
|
}
|
|
600
1807
|
return { removed, kept: native.length, path: catalogPath };
|
|
601
1808
|
}
|
|
602
1809
|
|
|
1810
|
+
export function restoreCodexCatalog(): { removed: number; kept: number; path: string } {
|
|
1811
|
+
const owningCodexHome = getCodexHome();
|
|
1812
|
+
const outcome = withCatalogWriteSerialization(
|
|
1813
|
+
owningCodexHome,
|
|
1814
|
+
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
1815
|
+
);
|
|
1816
|
+
return outcome.kind === "completed"
|
|
1817
|
+
? outcome.value
|
|
1818
|
+
: { removed: 0, kept: 0, path: readCodexCatalogPath() };
|
|
1819
|
+
}
|
|
1820
|
+
|
|
603
1821
|
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|
|
604
|
-
export function
|
|
1822
|
+
export function invalidateCodexModelsCacheWithPermit(
|
|
1823
|
+
permit: CatalogWritePermit,
|
|
1824
|
+
owningCodexHome: string,
|
|
1825
|
+
options?: CodexCatalogSyncOptions,
|
|
1826
|
+
): boolean {
|
|
605
1827
|
try {
|
|
1828
|
+
// This permit is a REACQUISITION: refreshCodexModelCatalog's commit released
|
|
1829
|
+
// K before this rewrite runs, so the commit-path desired-state check cannot
|
|
1830
|
+
// cover it. A disable landing in that gap must not be overwritten by a
|
|
1831
|
+
// routed cache write — re-read intent under this permit, same as the commit.
|
|
1832
|
+
// The catalog-only sync override applies here too so an explicit refresh
|
|
1833
|
+
// keeps the cache consistent with the catalog it just wrote.
|
|
1834
|
+
if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
|
|
606
1835
|
const catalogPath = readCodexCatalogPath();
|
|
607
1836
|
if (!existsSync(catalogPath)) return false;
|
|
608
1837
|
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
609
1838
|
const models = catalog.models ?? catalog;
|
|
1839
|
+
const currentCache = readCatalog(activeCodexModelsCachePath());
|
|
1840
|
+
const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
|
|
1841
|
+
typeof entry.slug === "string" ? [entry.slug] : []));
|
|
1842
|
+
const currentConfig = loadConfig();
|
|
1843
|
+
const mainSelectors = visibleCodexAccountSelectors(currentConfig).filter(selector => {
|
|
1844
|
+
const target = new Map(codexAccountNamespaceEntries(currentConfig)).get(selector);
|
|
1845
|
+
return isMainCodexAccountTarget(target ?? "");
|
|
1846
|
+
});
|
|
1847
|
+
const observedAccountModels = observedAccountBoundNativeEntries(currentCache?.models ?? [])
|
|
1848
|
+
.filter(entry => {
|
|
1849
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1850
|
+
return !existingSlugs.has(slug);
|
|
1851
|
+
})
|
|
1852
|
+
.map(entry => ({
|
|
1853
|
+
...entry,
|
|
1854
|
+
// Keep the observation in Codex's cache without advertising a new bare picker row. The
|
|
1855
|
+
// next OpenCodex catalog sync consumes this marker and creates only selector-qualified
|
|
1856
|
+
// rows for the currently configured public account selectors.
|
|
1857
|
+
visibility: "hide",
|
|
1858
|
+
opencodex_account_observed_native: true,
|
|
1859
|
+
opencodex_account_observed_selectors: mainSelectors,
|
|
1860
|
+
}));
|
|
610
1861
|
const wrapper = {
|
|
611
1862
|
fetched_at: "2000-01-01T00:00:00Z",
|
|
612
1863
|
client_version: "0.0.0",
|
|
613
|
-
models,
|
|
1864
|
+
models: [...models, ...observedAccountModels],
|
|
614
1865
|
};
|
|
615
|
-
|
|
1866
|
+
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
1867
|
+
path: activeCodexModelsCachePath(),
|
|
1868
|
+
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
1869
|
+
});
|
|
616
1870
|
return true;
|
|
617
1871
|
} catch {
|
|
618
1872
|
return false;
|
|
619
1873
|
}
|
|
620
1874
|
}
|
|
1875
|
+
|
|
1876
|
+
export function invalidateCodexModelsCache(options?: CodexCatalogSyncOptions): boolean {
|
|
1877
|
+
const owningCodexHome = getCodexHome();
|
|
1878
|
+
const outcome = withCatalogWriteSerialization(
|
|
1879
|
+
owningCodexHome,
|
|
1880
|
+
permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, options),
|
|
1881
|
+
);
|
|
1882
|
+
return outcome.kind === "completed" && outcome.value;
|
|
1883
|
+
}
|