@remodex/rmx 1.0.2
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 +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +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 +348 -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 +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- 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 +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,1606 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
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, externalCodexModelsPath, getCodexHome, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
9
|
+
import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
|
|
10
|
+
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
11
|
+
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
12
|
+
import { modelInList } from "../../types";
|
|
13
|
+
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
14
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
15
|
+
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
16
|
+
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
17
|
+
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
|
|
18
|
+
import { identifyRoutedModel } from "../../adapters/identity";
|
|
19
|
+
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
20
|
+
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
21
|
+
import {
|
|
22
|
+
COMBO_NAMESPACE,
|
|
23
|
+
comboModelId,
|
|
24
|
+
getCombo,
|
|
25
|
+
listComboIds,
|
|
26
|
+
targetKey,
|
|
27
|
+
} from "../../combos";
|
|
28
|
+
import type { NormalizedComboConfig } from "../../combos/types";
|
|
29
|
+
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
30
|
+
import { redactSecretString } from "../../lib/redact";
|
|
31
|
+
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline, samePath } from "./parsing";
|
|
35
|
+
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
36
|
+
import { applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry } from "./metadata";
|
|
37
|
+
import {
|
|
38
|
+
bundledCatalogCacheState,
|
|
39
|
+
loadBundledCodexCatalog,
|
|
40
|
+
resetBundledCatalogCacheForTests,
|
|
41
|
+
} from "./bundled";
|
|
42
|
+
import { isMultiAgentV2Enabled } from "../features";
|
|
43
|
+
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
44
|
+
import {
|
|
45
|
+
clearGatherRoutedModelsInflight,
|
|
46
|
+
filterCatalogVisibleModels,
|
|
47
|
+
gatherRoutedModels,
|
|
48
|
+
lastDropWarnSignature,
|
|
49
|
+
type CatalogGatherProviderModelOutcome,
|
|
50
|
+
} from "./provider-fetch";
|
|
51
|
+
import { accountSelectorShadowCollisionWarnings, clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, comboUnrestorableShadowWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnAccountSelectorShadowedProviderOnce, warnComboMasqueradeCollisionOnce, warnComboUnrestorableShadowOnce } from "./aggregation";
|
|
52
|
+
import type { ComboCatalogOmission } from "./aggregation";
|
|
53
|
+
import {
|
|
54
|
+
withCatalogWriteSerialization,
|
|
55
|
+
type CatalogWritePermit,
|
|
56
|
+
} from "../catalog-write-serialization";
|
|
57
|
+
import {
|
|
58
|
+
publishHashedCodexCatalogBackup,
|
|
59
|
+
publishLegacyCodexCatalogBackup,
|
|
60
|
+
replaceActiveCodexCatalog,
|
|
61
|
+
replaceCodexModelsCache,
|
|
62
|
+
} from "../internal/catalog-writer";
|
|
63
|
+
import { codexRuntimeStatePath } from "../runtime";
|
|
64
|
+
import { accountBoundNativeDisplayName, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
|
|
65
|
+
|
|
66
|
+
export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
|
|
67
|
+
|
|
68
|
+
export type SpawnAgentSurface = "v1" | "v2";
|
|
69
|
+
|
|
70
|
+
export type SubagentRosterExclusionReason =
|
|
71
|
+
| "missing_catalog_entry"
|
|
72
|
+
| "picker_hidden"
|
|
73
|
+
| "surface_incompatible"
|
|
74
|
+
| "outside_display_limit";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a catalog entry may be offered as a V2 subagent model.
|
|
78
|
+
*
|
|
79
|
+
* Upstream (codex-rs 92938d880) requires `multi_agent_version === "v2"` exactly,
|
|
80
|
+
* because upstream assumes a single backend serves every model. Remodex routes
|
|
81
|
+
* many providers, so that equality would reject the cross-provider spawns this
|
|
82
|
+
* proxy exists to enable.
|
|
83
|
+
*
|
|
84
|
+
* Decision (option B, devlog 260730_codex_rs_upstream_v2_live_handoff/060): any
|
|
85
|
+
* model Remodex actually routes is eligible. An entry pinned to a DIFFERENT
|
|
86
|
+
* multi-agent backend (`v1`) stays excluded, because that pin is a real capability
|
|
87
|
+
* statement rather than an absence of information. An unpinned entry (null or
|
|
88
|
+
* absent) is a routed or unpinned-native model and is allowed. The three-way
|
|
89
|
+
* distinction is the substance; do not flatten it into a truthiness check.
|
|
90
|
+
*/
|
|
91
|
+
export function isEligibleV2SubagentEntry(entry: RawEntry): boolean {
|
|
92
|
+
const pinned = entry.multi_agent_version;
|
|
93
|
+
return pinned === "v2" || pinned === null || pinned === undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface EffectiveSubagentModel {
|
|
97
|
+
model: string;
|
|
98
|
+
efforts: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SubagentRosterExclusion {
|
|
102
|
+
configured: string;
|
|
103
|
+
reason: SubagentRosterExclusionReason;
|
|
104
|
+
catalogModel?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface EffectiveSubagentRoster {
|
|
108
|
+
candidates: EffectiveSubagentModel[];
|
|
109
|
+
advertised: EffectiveSubagentModel[];
|
|
110
|
+
excluded: SubagentRosterExclusion[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function configuredCatalogEntry(entries: readonly RawEntry[], configured: string): RawEntry | undefined {
|
|
114
|
+
return entries.find(entry => entry.slug === configured)
|
|
115
|
+
?? entries.find(entry => typeof entry.slug === "string" && slugsEquivalent(configured, entry.slug));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function configuredSubagentModelMatchesEntry(configured: string, entry: RawEntry): boolean {
|
|
119
|
+
if (typeof entry.slug !== "string") return false;
|
|
120
|
+
if (slugsEquivalent(configured, entry.slug)) return true;
|
|
121
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
122
|
+
return !configured.includes("/")
|
|
123
|
+
&& nativeSlug !== undefined
|
|
124
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)
|
|
125
|
+
&& slugsEquivalent(configured, nativeSlug);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function effectiveSubagentRoster(
|
|
129
|
+
configuredModels: readonly string[],
|
|
130
|
+
surface: SpawnAgentSurface,
|
|
131
|
+
catalogEntries?: readonly RawEntry[],
|
|
132
|
+
): EffectiveSubagentRoster {
|
|
133
|
+
const configured = configuredModels
|
|
134
|
+
.filter(model => model.trim().length > 0)
|
|
135
|
+
.filter((model, index, all) =>
|
|
136
|
+
!all.slice(0, index).some(previous => slugsEquivalent(previous, model))
|
|
137
|
+
);
|
|
138
|
+
const entries = catalogEntries ?? readCatalog(readCodexCatalogPath())?.models ?? [];
|
|
139
|
+
const ordered = entries
|
|
140
|
+
.map((entry, index) => ({ entry, index }))
|
|
141
|
+
.filter(({ entry }) => typeof entry.slug === "string")
|
|
142
|
+
.filter(({ entry }) => entry.visibility === "list")
|
|
143
|
+
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
144
|
+
.sort((left, right) => {
|
|
145
|
+
const leftPriority = typeof left.entry.priority === "number" && Number.isFinite(left.entry.priority)
|
|
146
|
+
? left.entry.priority : Number.MAX_SAFE_INTEGER;
|
|
147
|
+
const rightPriority = typeof right.entry.priority === "number" && Number.isFinite(right.entry.priority)
|
|
148
|
+
? right.entry.priority : Number.MAX_SAFE_INTEGER;
|
|
149
|
+
return leftPriority - rightPriority || left.index - right.index;
|
|
150
|
+
})
|
|
151
|
+
.slice(0, MAX_SPAWN_AGENT_MODEL_OVERRIDES);
|
|
152
|
+
const orderedEntries = new Set(ordered.map(({ entry }) => entry));
|
|
153
|
+
|
|
154
|
+
const candidates = ordered.map(({ entry }) => ({
|
|
155
|
+
model: entry.slug as string,
|
|
156
|
+
efforts: catalogEntryEfforts(entry),
|
|
157
|
+
}));
|
|
158
|
+
const advertised = ordered
|
|
159
|
+
.filter(({ entry }) => configured.some(model => configuredSubagentModelMatchesEntry(model, entry)))
|
|
160
|
+
.map(({ entry }) => ({
|
|
161
|
+
model: entry.slug as string,
|
|
162
|
+
efforts: catalogEntryEfforts(entry),
|
|
163
|
+
}));
|
|
164
|
+
const excluded = configured.flatMap((model): SubagentRosterExclusion[] => {
|
|
165
|
+
const matchingEntries = entries.filter(entry => configuredSubagentModelMatchesEntry(model, entry));
|
|
166
|
+
if (matchingEntries.some(entry => orderedEntries.has(entry))) return [];
|
|
167
|
+
if (matchingEntries.length === 0) return [{ configured: model, reason: "missing_catalog_entry" }];
|
|
168
|
+
const visibleCompatible = matchingEntries.find(entry =>
|
|
169
|
+
entry.visibility === "list"
|
|
170
|
+
&& (surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
171
|
+
);
|
|
172
|
+
if (visibleCompatible) {
|
|
173
|
+
return [{
|
|
174
|
+
configured: model,
|
|
175
|
+
catalogModel: visibleCompatible.slug as string,
|
|
176
|
+
reason: "outside_display_limit",
|
|
177
|
+
}];
|
|
178
|
+
}
|
|
179
|
+
const visible = matchingEntries.find(entry => entry.visibility === "list");
|
|
180
|
+
if (visible) {
|
|
181
|
+
return [{
|
|
182
|
+
configured: model,
|
|
183
|
+
catalogModel: visible.slug as string,
|
|
184
|
+
reason: "surface_incompatible",
|
|
185
|
+
}];
|
|
186
|
+
}
|
|
187
|
+
const hidden = configuredCatalogEntry(entries, model) ?? matchingEntries[0]!;
|
|
188
|
+
return [{ configured: model, catalogModel: hidden.slug as string, reason: "picker_hidden" }];
|
|
189
|
+
});
|
|
190
|
+
return { candidates, advertised, excluded };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry {
|
|
194
|
+
if (priority !== 9) clone.priority = priority;
|
|
195
|
+
applyNativeOpenAiContextOverride(clone);
|
|
196
|
+
// GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
|
|
197
|
+
// Older natives (gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex-spark) get mock max + ultra
|
|
198
|
+
// (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
|
|
199
|
+
if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
|
|
200
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(clone));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function isExactComboCatalogModel(
|
|
204
|
+
model: CatalogModel | undefined,
|
|
205
|
+
exactComboSlugs: ReadonlySet<string>,
|
|
206
|
+
): boolean {
|
|
207
|
+
return model?.provider === COMBO_NAMESPACE && exactComboSlugs.has(catalogModelSlug(model));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isExactComboCatalogEntry(
|
|
211
|
+
entry: RawEntry,
|
|
212
|
+
exactComboSlugs: ReadonlySet<string>,
|
|
213
|
+
): boolean {
|
|
214
|
+
return entry.owned_by === COMBO_NAMESPACE
|
|
215
|
+
&& typeof entry.slug === "string"
|
|
216
|
+
&& exactComboSlugs.has(entry.slug);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
|
|
221
|
+
* ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
|
|
222
|
+
* lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
|
|
223
|
+
* The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
|
|
224
|
+
* that is dropped for display. All other providers keep the raw slug exactly as before.
|
|
225
|
+
*/
|
|
226
|
+
function routedDisplayName(slug: string): string {
|
|
227
|
+
const slash = slug.indexOf("/");
|
|
228
|
+
if (slash <= 0) return slug;
|
|
229
|
+
const provider = slug.slice(0, slash);
|
|
230
|
+
let model = slug.slice(slash + 1);
|
|
231
|
+
if (provider === "command-code" || provider === "commandcode") {
|
|
232
|
+
const m = model.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
|
|
233
|
+
if (m && model.startsWith(`${m[1]}-${m[1]}-`)) model = model.slice(m[1]!.length + 1);
|
|
234
|
+
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${model}`;
|
|
235
|
+
}
|
|
236
|
+
return slug;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function deriveEntry(
|
|
240
|
+
template: RawEntry | null,
|
|
241
|
+
slug: string,
|
|
242
|
+
desc: string,
|
|
243
|
+
priority: number,
|
|
244
|
+
model?: CatalogModel,
|
|
245
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
246
|
+
): RawEntry {
|
|
247
|
+
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
248
|
+
const isRouted = model !== undefined;
|
|
249
|
+
if (!isRouted && !slug.includes("/")) {
|
|
250
|
+
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
|
|
251
|
+
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
|
|
252
|
+
// instead of cloning an older template.
|
|
253
|
+
const upstream = upstreamNativeEntry(slug);
|
|
254
|
+
if (upstream) return finishUpstreamNativeEntry(upstream, priority);
|
|
255
|
+
}
|
|
256
|
+
if (template) {
|
|
257
|
+
const e = JSON.parse(JSON.stringify(template)) as RawEntry;
|
|
258
|
+
e.slug = slug;
|
|
259
|
+
e.display_name = routedDisplayName(slug);
|
|
260
|
+
e.description = desc;
|
|
261
|
+
e.priority = priority;
|
|
262
|
+
e.visibility = "list";
|
|
263
|
+
if ("upgrade" in e) e.upgrade = null;
|
|
264
|
+
delete e.availability_nux; // don't replay another model's "now available" NUX
|
|
265
|
+
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
|
|
266
|
+
// and advertise the reasoning ladder Codex accepts.
|
|
267
|
+
if (isRouted) {
|
|
268
|
+
// A routed model is NOT the native template: never inherit its context
|
|
269
|
+
// window when /models omits context metadata (#992). Known metadata
|
|
270
|
+
// restores exact values below; otherwise the strict-fields fallback
|
|
271
|
+
// supplies the conservative 128k triple.
|
|
272
|
+
delete e.context_window;
|
|
273
|
+
delete e.max_context_window;
|
|
274
|
+
delete e.auto_compact_token_limit;
|
|
275
|
+
// Native id for identity text + metadata lookups — the slug may be an encoded
|
|
276
|
+
// alias (`provider/vendor-model`); the model object carries the native id.
|
|
277
|
+
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
|
|
278
|
+
if (typeof e.base_instructions === "string") {
|
|
279
|
+
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the Remodex proxy
|
|
280
|
+
// (leaking that into base_instructions is a non-first-party signature → ToS risk).
|
|
281
|
+
e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
|
|
282
|
+
}
|
|
283
|
+
// Routed provider ladders are capability contracts, not hints. Keep them exact and expose
|
|
284
|
+
// no picker when capability evidence is absent instead of cloning the native GPT ladder.
|
|
285
|
+
applyReasoningLevels(e, model?.reasoningEfforts ?? [], model?.defaultReasoningEffort, true);
|
|
286
|
+
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
|
|
287
|
+
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
288
|
+
applyCatalogModelMetadata(e, model);
|
|
289
|
+
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
|
|
290
|
+
} else {
|
|
291
|
+
applyNativeOpenAiContextOverride(e);
|
|
292
|
+
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
293
|
+
else ensureUltraReasoningLevel(e);
|
|
294
|
+
// Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite;
|
|
295
|
+
// the template may carry the flag from a 5.6 entry — strip it so codex-rs does
|
|
296
|
+
// not inject reasoning.context: "all_turns" for models that reject it.
|
|
297
|
+
if (!isGpt56NativeSlug(slug)) {
|
|
298
|
+
// Spark NEEDS use_responses_lite: true — it controls the tool delivery format
|
|
299
|
+
// (AdditionalTools in input vs top-level tools). The reasoning params that
|
|
300
|
+
// use_responses_lite triggers (context: "all_turns", summary) are stripped
|
|
301
|
+
// separately in the passthrough adapter (stripUnsupportedReasoningParams).
|
|
302
|
+
if (!slug.includes("codex-spark")) delete e.use_responses_lite;
|
|
303
|
+
delete e.supports_websockets;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
|
|
307
|
+
preserveExactInputModalities: preserveExact,
|
|
308
|
+
isRouted,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
312
|
+
const entry: RawEntry = {
|
|
313
|
+
slug, display_name: routedDisplayName(slug), description: desc,
|
|
314
|
+
shell_type: "shell_command", visibility: "list", supported_in_api: true,
|
|
315
|
+
priority, base_instructions: "You are a helpful coding assistant.",
|
|
316
|
+
...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
|
|
317
|
+
};
|
|
318
|
+
if (isRouted) {
|
|
319
|
+
applyReasoningLevels(entry, model?.reasoningEfforts ?? [], model?.defaultReasoningEffort, true);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
323
|
+
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
|
|
324
|
+
}
|
|
325
|
+
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
326
|
+
applyCatalogModelMetadata(entry, model);
|
|
327
|
+
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
|
|
328
|
+
if (!isRouted) applyNativeOpenAiContextOverride(entry);
|
|
329
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
330
|
+
preserveExactInputModalities: preserveExact,
|
|
331
|
+
isRouted,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface ObservedCatalogEntryBuildInput {
|
|
336
|
+
readonly template: RawEntry | null;
|
|
337
|
+
readonly gptSlugs: readonly string[];
|
|
338
|
+
readonly goModels: readonly CatalogModel[];
|
|
339
|
+
readonly featured?: readonly string[];
|
|
340
|
+
readonly wsEnabled: boolean;
|
|
341
|
+
readonly multiAgentMode: MultiAgentMode;
|
|
342
|
+
readonly exactComboSlugs: ReadonlySet<string>;
|
|
343
|
+
readonly accountSelectors: readonly string[];
|
|
344
|
+
readonly suppressedBareNativeSlugs: ReadonlySet<string>;
|
|
345
|
+
readonly disabledNativeAccountSlugs: ReadonlySet<string>;
|
|
346
|
+
readonly multiAgentV2Enabled: boolean;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Build entries with the process-observed Codex feature state. */
|
|
350
|
+
export function buildCatalogEntries(
|
|
351
|
+
template: RawEntry | null,
|
|
352
|
+
gptSlugs: string[],
|
|
353
|
+
goModels: CatalogModel[],
|
|
354
|
+
featured?: string[],
|
|
355
|
+
wsEnabled = false,
|
|
356
|
+
multiAgentMode: MultiAgentMode = "default",
|
|
357
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
358
|
+
accountSelectors: readonly string[] = [],
|
|
359
|
+
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
|
|
360
|
+
disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
|
|
361
|
+
): RawEntry[] {
|
|
362
|
+
return buildCatalogEntriesFromObservedState({
|
|
363
|
+
template,
|
|
364
|
+
gptSlugs,
|
|
365
|
+
goModels,
|
|
366
|
+
featured,
|
|
367
|
+
wsEnabled,
|
|
368
|
+
multiAgentMode,
|
|
369
|
+
exactComboSlugs,
|
|
370
|
+
accountSelectors,
|
|
371
|
+
suppressedBareNativeSlugs,
|
|
372
|
+
disabledNativeAccountSlugs,
|
|
373
|
+
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Build entries solely from caller-observed inputs, with no feature-state filesystem read. */
|
|
378
|
+
export function buildCatalogEntriesFromObservedState({
|
|
379
|
+
template,
|
|
380
|
+
gptSlugs,
|
|
381
|
+
goModels,
|
|
382
|
+
featured,
|
|
383
|
+
wsEnabled,
|
|
384
|
+
multiAgentMode,
|
|
385
|
+
exactComboSlugs,
|
|
386
|
+
accountSelectors,
|
|
387
|
+
suppressedBareNativeSlugs,
|
|
388
|
+
disabledNativeAccountSlugs,
|
|
389
|
+
multiAgentV2Enabled,
|
|
390
|
+
}: ObservedCatalogEntryBuildInput): RawEntry[] {
|
|
391
|
+
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
392
|
+
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
393
|
+
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
394
|
+
// it sorts to the front. This works for native gpt slugs AND routed slugs alike.
|
|
395
|
+
const rank = new Map((featured ?? []).map((slug, i) => [slug, i] as const));
|
|
396
|
+
const priorityStride = Math.max(accountSelectors.length, 1);
|
|
397
|
+
const out: RawEntry[] = [];
|
|
398
|
+
const nativeEntries: RawEntry[] = [];
|
|
399
|
+
const collisionSkipped = resolveSlugAliasCollisions([...goModels]);
|
|
400
|
+
const emittedNativeAliases = new Set<CatalogModel>();
|
|
401
|
+
const emittedNativeAliasSlugs = new Set<string>();
|
|
402
|
+
const nativeAliasesBySlug = new Map<string, CatalogModel>();
|
|
403
|
+
for (const model of goModels) {
|
|
404
|
+
if (model.provider !== COMBO_NAMESPACE
|
|
405
|
+
|| model.nativeAlias !== true
|
|
406
|
+
|| typeof model.alias !== "string"
|
|
407
|
+
|| model.alias.includes("/")) continue;
|
|
408
|
+
if (nativeAliasesBySlug.has(model.alias)) {
|
|
409
|
+
collisionSkipped.add(model);
|
|
410
|
+
if (!slugAliasCollisionWarnings.has(model.alias)) {
|
|
411
|
+
slugAliasCollisionWarnings.add(model.alias);
|
|
412
|
+
console.warn(
|
|
413
|
+
`[Remodex] native combo alias collision on "${model.alias}": keeping the first configured combo and omitting later duplicates from the catalog.`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
nativeAliasesBySlug.set(model.alias, model);
|
|
419
|
+
}
|
|
420
|
+
const comboPublicSlugs = new Set(goModels
|
|
421
|
+
.filter(model => model.provider === COMBO_NAMESPACE)
|
|
422
|
+
.map(catalogModelSlug));
|
|
423
|
+
for (const slug of gptSlugs) {
|
|
424
|
+
const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
|
|
425
|
+
if (rank.has(slug)) native.priority = rank.get(slug)!;
|
|
426
|
+
nativeEntries.push(native);
|
|
427
|
+
const nativeAlias = nativeAliasesBySlug.get(slug);
|
|
428
|
+
if (!nativeAlias || collisionSkipped.has(nativeAlias)) {
|
|
429
|
+
if (!suppressedBareNativeSlugs.has(slug)) out.push(native);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const routed = deriveEntry(
|
|
433
|
+
template,
|
|
434
|
+
slug,
|
|
435
|
+
`Routed via Remodex → ${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
|
|
436
|
+
5,
|
|
437
|
+
nativeAlias,
|
|
438
|
+
exactComboSlugs,
|
|
439
|
+
);
|
|
440
|
+
routed.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
441
|
+
const rankHit = rank.get(slug) ?? rank.get(`${nativeAlias.provider}/${nativeAlias.id}`);
|
|
442
|
+
if (rankHit !== undefined) routed.priority = rankHit * priorityStride;
|
|
443
|
+
else if (accountSelectors.length > 0) routed.priority = 1_000 + (typeof routed.priority === "number" ? routed.priority : 5);
|
|
444
|
+
out.push(routed);
|
|
445
|
+
emittedNativeAliases.add(nativeAlias);
|
|
446
|
+
emittedNativeAliasSlugs.add(slug);
|
|
447
|
+
}
|
|
448
|
+
for (const [selectorIndex, selector] of accountSelectors.entries()) {
|
|
449
|
+
for (const [nativeIndex, native] of nativeEntries.entries()) {
|
|
450
|
+
const nativeSlug = String(native.slug);
|
|
451
|
+
if (disabledNativeAccountSlugs.has(nativeSlug)) continue;
|
|
452
|
+
const e = JSON.parse(JSON.stringify(native)) as RawEntry;
|
|
453
|
+
const catalogSlug = `${selector}/${nativeSlug}`;
|
|
454
|
+
e.slug = catalogSlug;
|
|
455
|
+
e.display_name = accountBoundNativeDisplayName(selector, native);
|
|
456
|
+
// Codex ignores this Remodex extension; preserve the native comp_hash unchanged.
|
|
457
|
+
e.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
458
|
+
const exactRank = rank.get(catalogSlug);
|
|
459
|
+
// A bare featured id belongs to the compatibility combo once shadowed. Exact
|
|
460
|
+
// account-qualified picks still rank normally, but the account clone must not
|
|
461
|
+
// inherit the bare alias rank and consume another top spawn_agent slot.
|
|
462
|
+
const inheritedRank = emittedNativeAliasSlugs.has(nativeSlug) ? undefined : rank.get(nativeSlug);
|
|
463
|
+
const featuredRank = exactRank ?? inheritedRank;
|
|
464
|
+
e.priority = featuredRank !== undefined
|
|
465
|
+
? featuredRank * priorityStride + selectorIndex
|
|
466
|
+
: ((featured?.length ?? 0) + nativeIndex) * accountSelectors.length + selectorIndex;
|
|
467
|
+
e.visibility = "list";
|
|
468
|
+
out.push(e);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
for (const m of goModels) {
|
|
472
|
+
if (collisionSkipped.has(m) || emittedNativeAliases.has(m)) continue;
|
|
473
|
+
const slug = catalogModelSlug(m);
|
|
474
|
+
if (m.provider !== COMBO_NAMESPACE && comboPublicSlugs.has(slug)) {
|
|
475
|
+
warnComboMasqueradeCollisionOnce(slug);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
// Provider rows use the one-slash slug codec; combo aliases intentionally override that
|
|
479
|
+
// public slug and may be bare.
|
|
480
|
+
const e = deriveEntry(
|
|
481
|
+
template,
|
|
482
|
+
slug,
|
|
483
|
+
`Routed via Remodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
|
|
484
|
+
5,
|
|
485
|
+
m,
|
|
486
|
+
exactComboSlugs,
|
|
487
|
+
);
|
|
488
|
+
if (m.provider === COMBO_NAMESPACE && m.nativeAlias === true && !slug.includes("/")) {
|
|
489
|
+
e.opencodex_catalog_kind = CODEX_NATIVE_ALIAS_CATALOG_KIND;
|
|
490
|
+
}
|
|
491
|
+
// Featured picks may be stored raw (legacy) or encoded — honor both.
|
|
492
|
+
const rankHit = rank.get(slug) ?? rank.get(`${m.provider}/${m.id}`);
|
|
493
|
+
if (rankHit !== undefined) e.priority = rankHit * priorityStride;
|
|
494
|
+
else if (accountSelectors.length > 0) {
|
|
495
|
+
// Keep the generated account rows together in Codex's priority-sorted flat picker.
|
|
496
|
+
e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
|
|
497
|
+
}
|
|
498
|
+
out.push(e);
|
|
499
|
+
}
|
|
500
|
+
// Central capability override (phase 120.4): the advertised flag must match the implemented WS
|
|
501
|
+
// endpoint. Overrides both the routed strip (normalizeRoutedCatalogEntry) and any native template
|
|
502
|
+
// leak (deriveEntry clones the template as-is for native slugs).
|
|
503
|
+
for (const entry of out) {
|
|
504
|
+
if (wsEnabled) entry.supports_websockets = true;
|
|
505
|
+
else {
|
|
506
|
+
delete entry.supports_websockets;
|
|
507
|
+
// Snapshot-backed native entries carry prefer_websockets: never advertise a preference
|
|
508
|
+
// for an endpoint ocx has disabled.
|
|
509
|
+
delete entry.prefer_websockets;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function resetCatalogRuntimeStateForTests(): void {
|
|
516
|
+
resetBundledCatalogCacheForTests();
|
|
517
|
+
lastDropWarnSignature.clear();
|
|
518
|
+
openAiApiCollisionWarnings.clear();
|
|
519
|
+
comboCatalogWarningSignatures.clear();
|
|
520
|
+
slugAliasCollisionWarnings.clear();
|
|
521
|
+
comboMasqueradeCollisionWarnings.clear();
|
|
522
|
+
comboUnrestorableShadowWarnings.clear();
|
|
523
|
+
accountSelectorShadowCollisionWarnings.clear();
|
|
524
|
+
clearLastComboCatalogOmissions();
|
|
525
|
+
clearModelCache(undefined, "eviction");
|
|
526
|
+
clearGatherRoutedModelsInflight();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function orderForSubagents(goModels: CatalogModel[], featured?: string[]): CatalogModel[] {
|
|
530
|
+
if (!featured || featured.length === 0) return goModels;
|
|
531
|
+
const rank = new Map(featured.map((id, i) => [id, i]));
|
|
532
|
+
// Featured picks may be stored raw (legacy) or encoded — match both forms.
|
|
533
|
+
const rankOf = (m: CatalogModel) =>
|
|
534
|
+
(m.alias ? rank.get(m.alias) : undefined)
|
|
535
|
+
?? rank.get(`${m.provider}/${m.id}`)
|
|
536
|
+
?? rank.get(routedSlug(m.provider, m.id))
|
|
537
|
+
?? Number.MAX_SAFE_INTEGER;
|
|
538
|
+
return [...goModels].sort((a, b) => {
|
|
539
|
+
return rankOf(a) - rankOf(b);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* True when an existing catalog row was authored by Remodex routing (#855).
|
|
545
|
+
* Every generated routed row — current full-slug form, the June–July 2026
|
|
546
|
+
* provider-name form, and legacy combo aliases — carries the stable
|
|
547
|
+
* description prefix `Routed via Remodex → ` (or the legacy Remodex prefix);
|
|
548
|
+
* foreign rows from Cursor or
|
|
549
|
+
* user tooling do not. `owned_by` cannot serve as the signal (upstream
|
|
550
|
+
* ownership), and `comp_hash` defaults to "opencodex" for every normalized
|
|
551
|
+
* row.
|
|
552
|
+
*/
|
|
553
|
+
function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
554
|
+
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
555
|
+
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
556
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
557
|
+
return slug.includes("/") && (
|
|
558
|
+
desc.startsWith("Routed via Remodex → ")
|
|
559
|
+
|| desc.startsWith("Routed via opencodex → ")
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function recoverableNativeSlug(entry: RawEntry): string | null {
|
|
564
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
565
|
+
return SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
566
|
+
&& !isNativeAliasCatalogEntry(entry)
|
|
567
|
+
&& entry.owned_by !== COMBO_NAMESPACE
|
|
568
|
+
? slug
|
|
569
|
+
: null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Append missing supported native rows from trusted catalog sources only. */
|
|
573
|
+
export function mergeCatalogModelsWithNativeRecovery(
|
|
574
|
+
primaryCatalogModels: readonly RawEntry[],
|
|
575
|
+
nativeRecoverySources: readonly (readonly RawEntry[])[],
|
|
576
|
+
): RawEntry[] {
|
|
577
|
+
const merged = [...primaryCatalogModels];
|
|
578
|
+
const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
|
|
579
|
+
const slug = recoverableNativeSlug(entry);
|
|
580
|
+
return slug === null ? [] : [slug];
|
|
581
|
+
}));
|
|
582
|
+
for (const source of nativeRecoverySources) {
|
|
583
|
+
for (const entry of source) {
|
|
584
|
+
const slug = recoverableNativeSlug(entry);
|
|
585
|
+
if (slug === null || recoveredNativeSlugs.has(slug)) continue;
|
|
586
|
+
merged.push(structuredClone(entry) as RawEntry);
|
|
587
|
+
recoveredNativeSlugs.add(slug);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return merged;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export interface ObservedCatalogMergePolicy {
|
|
594
|
+
/** Required observed/fixed set; the core merge never consults ambient catalog state. */
|
|
595
|
+
readonly nativeBackfillSlugs: readonly string[];
|
|
596
|
+
/** Whether unsupported OpenAI-family bare rows survive the merge. */
|
|
597
|
+
readonly unsupportedNativeEntries: "preserve" | "drop";
|
|
598
|
+
/** Whether merge-policy collision/preservation warnings belong to this caller's flow. */
|
|
599
|
+
readonly warningPolicy: "emit" | "suppress";
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Content policy shared by every writer of the canonical Codex model catalog. */
|
|
603
|
+
export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
|
|
604
|
+
Pick<ObservedCatalogMergePolicy, "nativeBackfillSlugs" | "unsupportedNativeEntries">
|
|
605
|
+
> = Object.freeze({
|
|
606
|
+
nativeBackfillSlugs: Object.freeze([...NATIVE_OPENAI_MODELS]),
|
|
607
|
+
unsupportedNativeEntries: "drop",
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
export interface ObservedCatalogMergeInput {
|
|
611
|
+
readonly catalogModels: readonly RawEntry[];
|
|
612
|
+
readonly baselineCatalogModels: readonly RawEntry[];
|
|
613
|
+
readonly routedEntries: readonly RawEntry[];
|
|
614
|
+
readonly baseline: ReadonlyMap<string, number>;
|
|
615
|
+
readonly featured: readonly string[];
|
|
616
|
+
readonly wsEnabled: boolean;
|
|
617
|
+
readonly template: RawEntry | null;
|
|
618
|
+
readonly disabledModels: ReadonlySet<string>;
|
|
619
|
+
readonly selectedModelsByProvider: ReadonlyMap<string, ReadonlySet<string>>;
|
|
620
|
+
readonly gatheredProviderNames: ReadonlySet<string>;
|
|
621
|
+
readonly degradedProviderNames: ReadonlySet<string>;
|
|
622
|
+
readonly legacyCustomModelSlugs: ReadonlySet<string>;
|
|
623
|
+
readonly multiAgentMode: MultiAgentMode;
|
|
624
|
+
readonly multiAgentV2Enabled: boolean;
|
|
625
|
+
readonly exactComboSlugs: ReadonlySet<string>;
|
|
626
|
+
readonly hasPhysicalComboProvider: boolean;
|
|
627
|
+
readonly includeNativeOpenAi: boolean;
|
|
628
|
+
readonly accountBoundEntries: readonly RawEntry[];
|
|
629
|
+
readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
|
|
630
|
+
readonly policy: ObservedCatalogMergePolicy;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Deterministically merge one fully observed catalog state.
|
|
635
|
+
*
|
|
636
|
+
* Every non-catalog input is explicit so evidence-bound convergence cannot
|
|
637
|
+
* accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
|
|
638
|
+
*/
|
|
639
|
+
export function mergeCatalogEntriesFromObservedState({
|
|
640
|
+
catalogModels,
|
|
641
|
+
baselineCatalogModels,
|
|
642
|
+
routedEntries,
|
|
643
|
+
baseline,
|
|
644
|
+
featured,
|
|
645
|
+
wsEnabled,
|
|
646
|
+
template,
|
|
647
|
+
disabledModels,
|
|
648
|
+
selectedModelsByProvider,
|
|
649
|
+
gatheredProviderNames,
|
|
650
|
+
degradedProviderNames,
|
|
651
|
+
legacyCustomModelSlugs,
|
|
652
|
+
multiAgentMode,
|
|
653
|
+
multiAgentV2Enabled,
|
|
654
|
+
exactComboSlugs,
|
|
655
|
+
hasPhysicalComboProvider,
|
|
656
|
+
includeNativeOpenAi,
|
|
657
|
+
accountBoundEntries,
|
|
658
|
+
suppressedBareNativeSlugs = new Set(),
|
|
659
|
+
policy,
|
|
660
|
+
}: ObservedCatalogMergeInput): RawEntry[] {
|
|
661
|
+
// Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
|
|
662
|
+
// the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
|
|
663
|
+
const detachedCatalogModels = catalogModels.map(entry => structuredClone(entry) as RawEntry);
|
|
664
|
+
const detachedBaselineCatalogModels = baselineCatalogModels
|
|
665
|
+
.map(entry => structuredClone(entry) as RawEntry);
|
|
666
|
+
const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
|
|
667
|
+
const detachedAccountBoundEntries = accountBoundEntries
|
|
668
|
+
.map(entry => structuredClone(entry) as RawEntry);
|
|
669
|
+
const disabledModelKeys = new Set([...disabledModels].map(slugEquivalenceKey));
|
|
670
|
+
const legacyCustomModelKeys = new Set(
|
|
671
|
+
[...legacyCustomModelSlugs].map(slugEquivalenceKey),
|
|
672
|
+
);
|
|
673
|
+
const selectedModelKeysByProvider = new Map([...selectedModelsByProvider].map(([provider, models]) => (
|
|
674
|
+
[provider, new Set([...models].map(model => slugEquivalenceKey(routedSlug(provider, model))))] as const
|
|
675
|
+
)));
|
|
676
|
+
const freshAccountKeys = new Set(detachedAccountBoundEntries.flatMap(entry => (
|
|
677
|
+
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
678
|
+
)));
|
|
679
|
+
const wouldSurviveUnreplaced = (entry: RawEntry): boolean => {
|
|
680
|
+
if (entry.owned_by === COMBO_NAMESPACE
|
|
681
|
+
|| trustedAccountBoundNativeCatalogSlug(entry) !== undefined
|
|
682
|
+
|| entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
683
|
+
|| isOcxAuthoredRoutedEntry(entry)
|
|
684
|
+
|| typeof entry.slug !== "string") return false;
|
|
685
|
+
const slug = entry.slug;
|
|
686
|
+
if (!slug.includes("/")) {
|
|
687
|
+
if (!includeNativeOpenAi || policy.nativeBackfillSlugs.includes(slug)) return false;
|
|
688
|
+
return policy.unsupportedNativeEntries === "preserve" || !isUnsupportedOpenAiNativeSlug(slug);
|
|
689
|
+
}
|
|
690
|
+
if (isRoutedModelCompatibilityExcluded(slug)) return false;
|
|
691
|
+
if (!hasPhysicalComboProvider && slug.startsWith(`${COMBO_NAMESPACE}/`)) return false;
|
|
692
|
+
const key = slugEquivalenceKey(slug);
|
|
693
|
+
if (freshAccountKeys.has(key)) return false;
|
|
694
|
+
if (disabledModelKeys.has(key)) return false;
|
|
695
|
+
const slash = slug.indexOf("/");
|
|
696
|
+
const provider = slug.slice(0, slash);
|
|
697
|
+
const selected = selectedModelKeysByProvider.get(provider);
|
|
698
|
+
if (selected !== undefined && !selected.has(key)) return false;
|
|
699
|
+
return !gatheredProviderNames.has(provider) || degradedProviderNames.has(provider);
|
|
700
|
+
};
|
|
701
|
+
const validRoutedEntries = detachedRoutedEntries.filter(entry => {
|
|
702
|
+
return !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
703
|
+
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
704
|
+
});
|
|
705
|
+
const restorableCatalogKeys = new Set(detachedBaselineCatalogModels.flatMap(entry => (
|
|
706
|
+
wouldSurviveUnreplaced(entry) && typeof entry.slug === "string"
|
|
707
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
708
|
+
: []
|
|
709
|
+
)));
|
|
710
|
+
const unrestorableCatalogKeys = new Set(detachedCatalogModels.flatMap(entry => {
|
|
711
|
+
if (!wouldSurviveUnreplaced(entry) || typeof entry.slug !== "string") return [];
|
|
712
|
+
const key = slugEquivalenceKey(entry.slug);
|
|
713
|
+
return restorableCatalogKeys.has(key) ? [] : [key];
|
|
714
|
+
}));
|
|
715
|
+
const admittedRoutedEntries = validRoutedEntries.filter(entry => {
|
|
716
|
+
if (!isExactComboCatalogEntry(entry, exactComboSlugs)) return true;
|
|
717
|
+
const slug = entry.slug as string;
|
|
718
|
+
const key = slugEquivalenceKey(slug);
|
|
719
|
+
if (!unrestorableCatalogKeys.has(key)) return true;
|
|
720
|
+
if (policy.warningPolicy === "emit") warnComboUnrestorableShadowOnce(slug);
|
|
721
|
+
return false;
|
|
722
|
+
});
|
|
723
|
+
// A fresh non-custom row authoritatively resolves a historically ambiguous slug as a normal
|
|
724
|
+
// provider model. Persist that classification so the durable deletion evidence cannot remove
|
|
725
|
+
// the legitimate row during a later degraded refresh.
|
|
726
|
+
for (const entry of admittedRoutedEntries) {
|
|
727
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
728
|
+
if (!slug
|
|
729
|
+
|| entry.opencodex_catalog_kind !== undefined
|
|
730
|
+
|| entry.owned_by === COMBO_NAMESPACE
|
|
731
|
+
|| !isOcxAuthoredRoutedEntry(entry)
|
|
732
|
+
|| !legacyCustomModelKeys.has(slugEquivalenceKey(slug))) continue;
|
|
733
|
+
entry.opencodex_catalog_kind = CODEX_PROVIDER_MODEL_CATALOG_KIND;
|
|
734
|
+
}
|
|
735
|
+
const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
|
|
736
|
+
isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
737
|
+
&& typeof entry.description === "string"
|
|
738
|
+
&& (
|
|
739
|
+
entry.description.startsWith(`Routed via Remodex → ${COMBO_NAMESPACE} (`)
|
|
740
|
+
|| entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
|
|
741
|
+
)
|
|
742
|
+
)));
|
|
743
|
+
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
744
|
+
const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
745
|
+
typeof entry.slug === "string" ? [slugEquivalenceKey(entry.slug)] : []
|
|
746
|
+
)));
|
|
747
|
+
const freshEquivalent = (slug: string): boolean => (
|
|
748
|
+
freshEquivalentKeys.has(slugEquivalenceKey(slug))
|
|
749
|
+
);
|
|
750
|
+
const freshBareComboAliases = new Set(admittedRoutedEntries.flatMap(entry => (
|
|
751
|
+
typeof entry.slug === "string"
|
|
752
|
+
&& !entry.slug.includes("/")
|
|
753
|
+
&& entry.owned_by === COMBO_NAMESPACE
|
|
754
|
+
? [entry.slug]
|
|
755
|
+
: []
|
|
756
|
+
)));
|
|
757
|
+
const staleComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
758
|
+
typeof entry.slug === "string"
|
|
759
|
+
&& entry.owned_by === COMBO_NAMESPACE
|
|
760
|
+
&& !freshEquivalent(entry.slug)
|
|
761
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
762
|
+
: []
|
|
763
|
+
)));
|
|
764
|
+
const currentNonComboKeys = new Set(detachedCatalogModels.flatMap(entry => (
|
|
765
|
+
entry.owned_by !== COMBO_NAMESPACE && typeof entry.slug === "string"
|
|
766
|
+
? [slugEquivalenceKey(entry.slug)]
|
|
767
|
+
: []
|
|
768
|
+
)));
|
|
769
|
+
const restoredComboShadows = detachedBaselineCatalogModels.filter(entry => {
|
|
770
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
771
|
+
if (!slug || entry.owned_by === COMBO_NAMESPACE) return false;
|
|
772
|
+
const key = slugEquivalenceKey(slug);
|
|
773
|
+
return staleComboKeys.has(key) && !currentNonComboKeys.has(key);
|
|
774
|
+
});
|
|
775
|
+
const catalogModelsForMerge = [...detachedCatalogModels, ...restoredComboShadows];
|
|
776
|
+
const nativePriority = (slug: string, fallback: unknown): number => {
|
|
777
|
+
const base = baseline.get(slug)
|
|
778
|
+
?? (typeof fallback === "number" ? fallback : 9);
|
|
779
|
+
if (rank.has(slug)) return rank.get(slug)!;
|
|
780
|
+
return featured.length > 0 ? Math.max(base, featured.length + 100) : base;
|
|
781
|
+
};
|
|
782
|
+
const nativeSourceEntries = includeNativeOpenAi
|
|
783
|
+
? catalogModelsForMerge
|
|
784
|
+
.filter(m => typeof m.slug === "string"
|
|
785
|
+
&& !(m.slug as string).includes("/")
|
|
786
|
+
&& m.owned_by !== COMBO_NAMESPACE
|
|
787
|
+
&& (policy.unsupportedNativeEntries === "preserve"
|
|
788
|
+
|| !isUnsupportedOpenAiNativeSlug(m.slug as string)))
|
|
789
|
+
.map(m => {
|
|
790
|
+
const slug = m.slug as string;
|
|
791
|
+
// Fallback-quality entries (ocx synthesis / codex-rs model_info fallback: display_name
|
|
792
|
+
// stamped with the bare slug) are upgraded to the pinned upstream snapshot entry so a
|
|
793
|
+
// previously synthesized ladder (e.g. luna advertising ultra) self-heals on sync. A
|
|
794
|
+
// genuine catalog entry (real display name) is preserved untouched.
|
|
795
|
+
if (shouldUpgradeToUpstreamEntry(m)) {
|
|
796
|
+
const upstream = upstreamNativeEntry(slug)!;
|
|
797
|
+
const finished = finishUpstreamNativeEntry(upstream, 9);
|
|
798
|
+
finished.priority = nativePriority(slug, upstream.priority);
|
|
799
|
+
return finished;
|
|
800
|
+
}
|
|
801
|
+
const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
|
|
802
|
+
// Older natives kept from disk still need the mock top tiers (max + ultra always
|
|
803
|
+
// for subagent max spawns; wire-clamped to the model's real top rung).
|
|
804
|
+
if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
|
|
805
|
+
return preserved;
|
|
806
|
+
})
|
|
807
|
+
: [];
|
|
808
|
+
const native = nativeSourceEntries.filter(entry =>
|
|
809
|
+
typeof entry.slug !== "string"
|
|
810
|
+
|| (!freshBareComboAliases.has(entry.slug) && !suppressedBareNativeSlugs.has(entry.slug))
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
// Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
|
|
814
|
+
// routed provider exposing the same id can never delete the native OpenAI/Codex base row.
|
|
815
|
+
// Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
|
|
816
|
+
const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
817
|
+
if (includeNativeOpenAi) {
|
|
818
|
+
for (const slug of policy.nativeBackfillSlugs) {
|
|
819
|
+
if (nativeSlugs.has(slug) || freshBareComboAliases.has(slug) || suppressedBareNativeSlugs.has(slug)) continue;
|
|
820
|
+
nativeSlugs.add(slug);
|
|
821
|
+
const entry = deriveEntry(
|
|
822
|
+
template ? JSON.parse(JSON.stringify(template)) : null,
|
|
823
|
+
slug,
|
|
824
|
+
"OpenAI native model (Codex OAuth passthrough).",
|
|
825
|
+
nativePriority(slug, upstreamNativeEntry(slug)?.priority),
|
|
826
|
+
);
|
|
827
|
+
entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority);
|
|
828
|
+
native.push(entry);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const nativeSourceBySlug = new Map([...nativeSourceEntries, ...native].flatMap(entry =>
|
|
833
|
+
typeof entry.slug === "string" ? [[entry.slug, entry] as const] : []
|
|
834
|
+
));
|
|
835
|
+
const alignedAccountBoundEntries = detachedAccountBoundEntries.map(entry => {
|
|
836
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
837
|
+
const source = nativeSlug === undefined ? undefined : nativeSourceBySlug.get(nativeSlug);
|
|
838
|
+
if (!source) return entry;
|
|
839
|
+
const aligned = JSON.parse(JSON.stringify(source)) as RawEntry;
|
|
840
|
+
aligned.slug = entry.slug;
|
|
841
|
+
aligned.display_name = entry.display_name;
|
|
842
|
+
aligned.priority = entry.priority;
|
|
843
|
+
aligned.visibility = "list";
|
|
844
|
+
aligned.opencodex_catalog_kind = CODEX_ACCOUNT_BOUND_CATALOG_KIND;
|
|
845
|
+
return aligned;
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const freshSlugs = new Set(
|
|
849
|
+
admittedRoutedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
850
|
+
);
|
|
851
|
+
const existingRoutedEntries = catalogModelsForMerge.filter(m =>
|
|
852
|
+
typeof m.slug === "string"
|
|
853
|
+
&& (m.slug.includes("/") || isNativeAliasCatalogEntry(m))
|
|
854
|
+
&& trustedAccountBoundNativeCatalogSlug(m) === undefined
|
|
855
|
+
);
|
|
856
|
+
const preservedRoutedEntries = existingRoutedEntries.filter(entry => {
|
|
857
|
+
const slug = entry.slug as string;
|
|
858
|
+
if (freshEquivalent(slug)) return false;
|
|
859
|
+
if (isNativeAliasCatalogEntry(entry)) return exactComboSlugs.has(slug);
|
|
860
|
+
// Current custom rows are always regenerated from config, even while provider discovery is
|
|
861
|
+
// degraded. A marked row absent from the fresh projection is therefore an intentional delete.
|
|
862
|
+
if (entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND) return false;
|
|
863
|
+
// Before custom rows had a marker, a config deletion could otherwise be mistaken for a
|
|
864
|
+
// provider outage. Only explicit save-boundary evidence may classify an unmarked Remodex
|
|
865
|
+
// row; foreign and future-marked rows fail closed and remain preserved.
|
|
866
|
+
if (entry.opencodex_catalog_kind === undefined
|
|
867
|
+
&& entry.owned_by !== COMBO_NAMESPACE
|
|
868
|
+
&& isOcxAuthoredRoutedEntry(entry)
|
|
869
|
+
&& legacyCustomModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
870
|
+
const provider = slug.slice(0, slug.indexOf("/"));
|
|
871
|
+
if (gatheredProviderNames.has(provider)) {
|
|
872
|
+
// A provider-local degraded observation preserves only that namespace. Authoritative empty
|
|
873
|
+
// catalogs and successful removals still delete stale rows even when another provider fails.
|
|
874
|
+
return degradedProviderNames.has(provider);
|
|
875
|
+
}
|
|
876
|
+
// Deleted/disabled providers cannot retain Remodex-authored ghosts. Foreign catalog rows
|
|
877
|
+
// remain outside provider ownership and survive unless a fresh row replaces their exact slug.
|
|
878
|
+
return !isOcxAuthoredRoutedEntry(entry);
|
|
879
|
+
});
|
|
880
|
+
let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
|
|
881
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
882
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
883
|
+
if (!slug.includes("/")) return true;
|
|
884
|
+
if (disabledModelKeys.has(slugEquivalenceKey(slug))) return false;
|
|
885
|
+
// Provider allowlists own provider rows, not a current combo's public alias. Exempt only an
|
|
886
|
+
// identity from this gather's generated combo projection: provider discovery may supply a
|
|
887
|
+
// spoofed `owned_by`, and persisted combo-shaped rows are not fresh authority.
|
|
888
|
+
if (freshExactComboEntries.has(entry)) return true;
|
|
889
|
+
const slash = slug.indexOf("/");
|
|
890
|
+
const provider = slug.slice(0, slash);
|
|
891
|
+
const selected = selectedModelKeysByProvider.get(provider);
|
|
892
|
+
return selected === undefined || selected.has(slugEquivalenceKey(slug));
|
|
893
|
+
});
|
|
894
|
+
if (!hasPhysicalComboProvider) {
|
|
895
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
896
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
897
|
+
const comboOwned = slug.startsWith(`${COMBO_NAMESPACE}/`) || entry.owned_by === COMBO_NAMESPACE;
|
|
898
|
+
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
899
|
+
return !comboOwned || freshSlugs.has(slug) || retainedNativeAlias;
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
903
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
904
|
+
const retainedNativeAlias = isNativeAliasCatalogEntry(entry) && exactComboSlugs.has(slug);
|
|
905
|
+
return retainedNativeAlias
|
|
906
|
+
|| !isExactComboCatalogEntry(entry, exactComboSlugs)
|
|
907
|
+
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
908
|
+
});
|
|
909
|
+
// Reapply final catalog policy to rows preserved from disk. Those rows bypass
|
|
910
|
+
// gatherRoutedModels, so filtering only the freshly gathered list can resurrect an excluded id.
|
|
911
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry =>
|
|
912
|
+
typeof entry.slug !== "string" || !isRoutedModelCompatibilityExcluded(entry.slug)
|
|
913
|
+
);
|
|
914
|
+
const accountBoundSlugs = new Set(alignedAccountBoundEntries.flatMap(entry =>
|
|
915
|
+
typeof entry.slug === "string" ? [entry.slug] : []
|
|
916
|
+
));
|
|
917
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
918
|
+
if (typeof entry.slug !== "string" || !accountBoundSlugs.has(entry.slug)) return true;
|
|
919
|
+
if (freshSlugs.has(entry.slug) && policy.warningPolicy === "emit") {
|
|
920
|
+
warnAccountSelectorShadowedProviderOnce(entry.slug);
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
});
|
|
924
|
+
const finalRoutedEntrySet = new Set(finalRoutedEntries);
|
|
925
|
+
const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
|
|
926
|
+
if (!finalRoutedEntrySet.has(entry)) return false;
|
|
927
|
+
const slug = entry.slug as string;
|
|
928
|
+
const provider = slug.slice(0, slug.indexOf("/"));
|
|
929
|
+
return gatheredProviderNames.has(provider) && degradedProviderNames.has(provider);
|
|
930
|
+
}).length;
|
|
931
|
+
if (degradedPreservedCount > 0 && policy.warningPolicy === "emit") {
|
|
932
|
+
console.warn(`[Remodex] catalog sync: provider discovery degraded; preserving ${degradedPreservedCount} existing routed entr${degradedPreservedCount === 1 ? "y" : "ies"} on disk.`);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
|
|
936
|
+
const mergedEntries = [...native, ...managedEntries].map(m => {
|
|
937
|
+
const normalized = normalizeServiceTiers(m);
|
|
938
|
+
if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized);
|
|
939
|
+
const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
|
|
940
|
+
const e = ensureStrictCatalogFields(normalized, {
|
|
941
|
+
preserveExactInputModalities: exactCombo,
|
|
942
|
+
isRouted: finalRoutedEntrySet.has(m),
|
|
943
|
+
});
|
|
944
|
+
// Native rows may need the compatibility max rung for older Codex clients. Routed rows are
|
|
945
|
+
// different: their supported_reasoning_levels are the provider's capability contract and
|
|
946
|
+
// must remain exact (for example qwen may support none/minimal but not max). Adding a
|
|
947
|
+
// synthetic max here would make the Desktop picker disagree with /api/models and could send
|
|
948
|
+
// an effort the provider rejects.
|
|
949
|
+
const preserveRoutedLadder = finalRoutedEntrySet.has(m)
|
|
950
|
+
&& typeof m.slug === "string"
|
|
951
|
+
&& m.slug.includes("/")
|
|
952
|
+
&& !m.slug.startsWith(`${COMBO_NAMESPACE}/`);
|
|
953
|
+
if (!exactCombo && !preserveRoutedLadder) {
|
|
954
|
+
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
955
|
+
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
956
|
+
: [];
|
|
957
|
+
if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
|
|
958
|
+
levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
|
|
959
|
+
?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
|
|
960
|
+
e.supported_reasoning_levels = levels;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (wsEnabled) e.supports_websockets = true;
|
|
964
|
+
else {
|
|
965
|
+
delete e.supports_websockets;
|
|
966
|
+
// Match buildCatalogEntries: never advertise a websocket preference while WS is off.
|
|
967
|
+
delete e.prefer_websockets;
|
|
968
|
+
}
|
|
969
|
+
return e;
|
|
970
|
+
});
|
|
971
|
+
// Native enable/disable runs as the LAST pass so the upstream-upgrade branch above can never
|
|
972
|
+
// clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
|
|
973
|
+
// only their generated account row.
|
|
974
|
+
const versionedEntries = applyMultiAgentMode(
|
|
975
|
+
applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0),
|
|
976
|
+
multiAgentMode,
|
|
977
|
+
multiAgentV2Enabled,
|
|
978
|
+
);
|
|
979
|
+
for (const entry of versionedEntries) {
|
|
980
|
+
const kind = entry.opencodex_catalog_kind;
|
|
981
|
+
if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
|
|
982
|
+
&& kind !== CODEX_CUSTOM_MODEL_CATALOG_KIND
|
|
983
|
+
&& kind !== CODEX_PROVIDER_MODEL_CATALOG_KIND) continue;
|
|
984
|
+
// Canonicalize extension-field order after every normalizer. This keeps an unchanged catalog
|
|
985
|
+
// byte-idempotent whether an owned row was freshly built or retained from the prior pass.
|
|
986
|
+
delete entry.opencodex_catalog_kind;
|
|
987
|
+
entry.opencodex_catalog_kind = kind;
|
|
988
|
+
}
|
|
989
|
+
return versionedEntries;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Merge retained-sync rows using the process-observed Codex feature state. */
|
|
993
|
+
export function mergeCatalogEntriesForSync(
|
|
994
|
+
catalogModels: RawEntry[],
|
|
995
|
+
routedEntries: RawEntry[],
|
|
996
|
+
baseline: Map<string, number>,
|
|
997
|
+
featured: string[],
|
|
998
|
+
wsEnabled: boolean,
|
|
999
|
+
_goIds: Set<string> = new Set(),
|
|
1000
|
+
template: RawEntry | null = null,
|
|
1001
|
+
disabledModels: ReadonlySet<string> = new Set(),
|
|
1002
|
+
gatheredProviderNames?: Set<string>,
|
|
1003
|
+
multiAgentMode: MultiAgentMode = "default",
|
|
1004
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
1005
|
+
hasPhysicalComboProvider = false,
|
|
1006
|
+
includeNativeOpenAi = true,
|
|
1007
|
+
accountBoundEntries: readonly RawEntry[] = [],
|
|
1008
|
+
legacyCustomModelSlugs: ReadonlySet<string> = new Set(),
|
|
1009
|
+
suppressedBareNativeSlugs: ReadonlySet<string> = new Set(
|
|
1010
|
+
routedEntries.flatMap(entry => (
|
|
1011
|
+
isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
|
|
1012
|
+
)),
|
|
1013
|
+
),
|
|
1014
|
+
): RawEntry[] {
|
|
1015
|
+
// Retained for source compatibility with the original helper contract. Raw provider ids must
|
|
1016
|
+
// not suppress same-named native rows; actual admitted combo entries own that decision now.
|
|
1017
|
+
void _goIds;
|
|
1018
|
+
const effectiveGatheredProviderNames = gatheredProviderNames ?? new Set(
|
|
1019
|
+
routedEntries.flatMap(entry => {
|
|
1020
|
+
// A slashed combo alias is not evidence that its public prefix is an authoritative provider
|
|
1021
|
+
// namespace. Treating it as one would let the combo replace an unrestorable foreign row.
|
|
1022
|
+
if (isExactComboCatalogEntry(entry, exactComboSlugs)) return [];
|
|
1023
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1024
|
+
const slash = slug.indexOf("/");
|
|
1025
|
+
return slash > 0 ? [slug.slice(0, slash)] : [];
|
|
1026
|
+
}),
|
|
1027
|
+
);
|
|
1028
|
+
return mergeCatalogEntriesFromObservedState({
|
|
1029
|
+
catalogModels,
|
|
1030
|
+
baselineCatalogModels: [],
|
|
1031
|
+
routedEntries,
|
|
1032
|
+
baseline,
|
|
1033
|
+
featured,
|
|
1034
|
+
wsEnabled,
|
|
1035
|
+
template,
|
|
1036
|
+
disabledModels,
|
|
1037
|
+
selectedModelsByProvider: new Map(),
|
|
1038
|
+
gatheredProviderNames: effectiveGatheredProviderNames,
|
|
1039
|
+
degradedProviderNames: new Set(),
|
|
1040
|
+
legacyCustomModelSlugs,
|
|
1041
|
+
multiAgentMode,
|
|
1042
|
+
multiAgentV2Enabled: isMultiAgentV2Enabled(),
|
|
1043
|
+
exactComboSlugs,
|
|
1044
|
+
hasPhysicalComboProvider,
|
|
1045
|
+
includeNativeOpenAi,
|
|
1046
|
+
accountBoundEntries,
|
|
1047
|
+
suppressedBareNativeSlugs,
|
|
1048
|
+
policy: {
|
|
1049
|
+
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1050
|
+
warningPolicy: "emit",
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
interface RetainedCatalogSyncRead {
|
|
1056
|
+
readonly catalogPath: string;
|
|
1057
|
+
readonly catalog: RawCatalog;
|
|
1058
|
+
readonly onDiskCatalog: RawCatalog | null;
|
|
1059
|
+
readonly evidence: string;
|
|
1060
|
+
/**
|
|
1061
|
+
* Process-local epochs, baselined AFTER our own gather rather than with the
|
|
1062
|
+
* filesystem bytes above. See `retainedCatalogProcessEvidence`.
|
|
1063
|
+
*/
|
|
1064
|
+
readonly processEvidence: string;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
interface RetainedCatalogSyncResult {
|
|
1068
|
+
added: number;
|
|
1069
|
+
path: string;
|
|
1070
|
+
catalogWritten: boolean;
|
|
1071
|
+
comboOmissions: ComboCatalogOmission[];
|
|
1072
|
+
/** `desired_disabled` observed under K after the provider await; nothing was written. */
|
|
1073
|
+
skippedReason?: "desired_disabled";
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
interface RetainedCatalogSyncWrite {
|
|
1077
|
+
readonly config: OcxConfig;
|
|
1078
|
+
readonly goModels: CatalogModel[];
|
|
1079
|
+
readonly providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
1080
|
+
readonly comboOmissions: ComboCatalogOmission[];
|
|
1081
|
+
readonly read: RetainedCatalogSyncRead;
|
|
1082
|
+
readonly permit: CatalogWritePermit;
|
|
1083
|
+
readonly owningCodexHome: string;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function optionalFileBytes(path: string): string | null {
|
|
1087
|
+
try {
|
|
1088
|
+
return readFileSync(path).toString("base64");
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") return null;
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function loadCatalogForRetainedSync(path: string): RawCatalog | null {
|
|
1096
|
+
const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
|
|
1097
|
+
if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
|
|
1098
|
+
const active = readCatalog(path);
|
|
1099
|
+
// A valid configured custom file remains the content authority even when it has no bare native
|
|
1100
|
+
// template. The null-template builder is deliberate; a stale backup must not replace active
|
|
1101
|
+
// custom root metadata merely because the current file contains only routed rows.
|
|
1102
|
+
if (active && (!isDefaultCatalogPath(path) || findNativeTemplate(active))) return active;
|
|
1103
|
+
return readCatalog(catalogBackupPathFor(path))
|
|
1104
|
+
?? (isDefaultCatalogPath(path) ? readCatalog(legacyCatalogBackupPath()) : null)
|
|
1105
|
+
?? readCatalog(activeCodexModelsCachePath())
|
|
1106
|
+
?? active;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function retainedCatalogSyncEvidence(
|
|
1110
|
+
config: OcxConfig,
|
|
1111
|
+
catalogPath: string,
|
|
1112
|
+
catalog: RawCatalog,
|
|
1113
|
+
): string {
|
|
1114
|
+
return JSON.stringify({
|
|
1115
|
+
config,
|
|
1116
|
+
catalogPath,
|
|
1117
|
+
catalog,
|
|
1118
|
+
catalogBytes: optionalFileBytes(catalogPath),
|
|
1119
|
+
hashedBackupBytes: optionalFileBytes(catalogBackupPathFor(catalogPath)),
|
|
1120
|
+
legacyBackupBytes: isDefaultCatalogPath(catalogPath)
|
|
1121
|
+
? optionalFileBytes(legacyCatalogBackupPath()) : null,
|
|
1122
|
+
modelsCacheBytes: optionalFileBytes(activeCodexModelsCachePath()),
|
|
1123
|
+
// The persisted runtime selection is a pre-await filesystem input, not a
|
|
1124
|
+
// process epoch: another PROCESS can move runtime authority by rewriting this
|
|
1125
|
+
// file, and that move is invisible to our in-process memo. Recorded PRESENT or
|
|
1126
|
+
// ABSENT, because its absence is what makes the resolver fall back.
|
|
1127
|
+
runtimeStateBytes: optionalFileBytes(codexRuntimeStatePath()),
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* The bundled-template half of the same evidence, observed separately.
|
|
1133
|
+
*
|
|
1134
|
+
* The runtime process memo is deliberately NOT here, and that exclusion took three
|
|
1135
|
+
* attempts to get honest. Gathering resolves the Codex runtime lazily and under its
|
|
1136
|
+
* own cache key, so this path cannot pre-settle that memo: baselining it before the
|
|
1137
|
+
* await always detected our own side effect and refused every write, and baselining
|
|
1138
|
+
* it after the await captured a runtime that ANOTHER process had moved as though it
|
|
1139
|
+
* were ours — a catalog prepared from R1 committing after authority reached R2.
|
|
1140
|
+
*
|
|
1141
|
+
* Runtime authority is covered where it is actually durable instead: the persisted
|
|
1142
|
+
* `codex-runtime.json` bytes sit in the pre-await filesystem evidence, PRESENT or
|
|
1143
|
+
* ABSENT, so a cross-process runtime move is caught. What is left uncovered, and is
|
|
1144
|
+
* written down rather than papered over, is a same-process in-memory runtime swap
|
|
1145
|
+
* that never touches that file — WP11 owns the lock that makes that case decidable.
|
|
1146
|
+
*/
|
|
1147
|
+
function retainedCatalogProcessEvidence(): string {
|
|
1148
|
+
return JSON.stringify({
|
|
1149
|
+
bundledCatalogCache: bundledCatalogCacheState(),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Capture every local catalog input the retained sync path consults before its
|
|
1155
|
+
* provider await. The exact evidence is compared after K acquisition; a newer
|
|
1156
|
+
* catalog/backup/cache or target selection makes this attempt a no-write.
|
|
1157
|
+
*/
|
|
1158
|
+
function readRetainedCatalogSync(config: OcxConfig): RetainedCatalogSyncRead | null {
|
|
1159
|
+
const catalogPath = readCodexCatalogPath();
|
|
1160
|
+
const catalog = loadCatalogForRetainedSync(catalogPath);
|
|
1161
|
+
if (!catalog) return null;
|
|
1162
|
+
|
|
1163
|
+
// The bundled catalog is a reliable native template on the default path, but it is not the
|
|
1164
|
+
// merge source. Preservation must inspect the file that this sync is about to overwrite;
|
|
1165
|
+
// otherwise an empty/partial provider gather cannot see routed or user-native rows on disk.
|
|
1166
|
+
const onDiskCatalog = readCatalog(catalogPath);
|
|
1167
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, catalog);
|
|
1168
|
+
// `processEvidence` is filled in after the provider await, not here.
|
|
1169
|
+
return { catalogPath, catalog, onDiskCatalog, evidence, processEvidence: "" };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function revalidateRetainedCatalogSync(
|
|
1173
|
+
config: OcxConfig,
|
|
1174
|
+
prepared: RetainedCatalogSyncRead,
|
|
1175
|
+
): RetainedCatalogSyncRead | null {
|
|
1176
|
+
const catalogPath = readCodexCatalogPath();
|
|
1177
|
+
if (catalogPath !== prepared.catalogPath) return null;
|
|
1178
|
+
const evidence = retainedCatalogSyncEvidence(config, catalogPath, prepared.catalog);
|
|
1179
|
+
if (evidence !== prepared.evidence) return null;
|
|
1180
|
+
if (retainedCatalogProcessEvidence() !== prepared.processEvidence) return null;
|
|
1181
|
+
return {
|
|
1182
|
+
catalogPath,
|
|
1183
|
+
catalog: JSON.parse(JSON.stringify(prepared.catalog)) as RawCatalog,
|
|
1184
|
+
onDiskCatalog: readCatalog(catalogPath),
|
|
1185
|
+
evidence,
|
|
1186
|
+
processEvidence: prepared.processEvidence,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
|
|
1191
|
+
if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
|
|
1192
|
+
try {
|
|
1193
|
+
return readFileSync(read.catalogPath, "utf8");
|
|
1194
|
+
} catch {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return catalogHasRoutedEntries(read.catalog)
|
|
1199
|
+
? null
|
|
1200
|
+
: `${JSON.stringify(read.catalog, null, 2)}\n`;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function catalogModelsForMergeWithNativeRecovery(
|
|
1204
|
+
catalogPath: string,
|
|
1205
|
+
catalog: RawCatalog,
|
|
1206
|
+
onDiskCatalog: RawCatalog | null,
|
|
1207
|
+
): RawEntry[] {
|
|
1208
|
+
const primaryCatalogModels = onDiskCatalog?.models ?? catalog.models ?? [];
|
|
1209
|
+
// Native-alias compatibility can omit disabled native rows from the effective catalog because
|
|
1210
|
+
// Desktop's remote allowlist ignores `visibility: "hide"`. Keep current/pristine native recovery
|
|
1211
|
+
// sources beside the on-disk rows so re-enabling a model restores its real metadata. Routed and
|
|
1212
|
+
// user-authored rows still come only from the on-disk catalog.
|
|
1213
|
+
return mergeCatalogModelsWithNativeRecovery(primaryCatalogModels, [
|
|
1214
|
+
catalog.models ?? [],
|
|
1215
|
+
readCatalogBackup(catalogPath)?.models ?? [],
|
|
1216
|
+
]);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function writeRetainedCatalogSync({
|
|
1220
|
+
config,
|
|
1221
|
+
goModels,
|
|
1222
|
+
providerModelOutcomes,
|
|
1223
|
+
comboOmissions,
|
|
1224
|
+
read,
|
|
1225
|
+
permit,
|
|
1226
|
+
owningCodexHome,
|
|
1227
|
+
}: RetainedCatalogSyncWrite): RetainedCatalogSyncResult {
|
|
1228
|
+
const { catalogPath, catalog, onDiskCatalog } = read;
|
|
1229
|
+
const catalogModelsForMerge = catalogModelsForMergeWithNativeRecovery(
|
|
1230
|
+
catalogPath,
|
|
1231
|
+
catalog,
|
|
1232
|
+
onDiskCatalog,
|
|
1233
|
+
);
|
|
1234
|
+
const template = findNativeTemplate(catalog);
|
|
1235
|
+
|
|
1236
|
+
try {
|
|
1237
|
+
// Once-only: preserve the PRISTINE pre-Remodex catalog as the native-priority baseline
|
|
1238
|
+
// (later syncs would otherwise overwrite it with featured-modified priorities).
|
|
1239
|
+
const pristine = pristineCatalogBytes(read);
|
|
1240
|
+
if (pristine !== null) {
|
|
1241
|
+
publishHashedCodexCatalogBackup(permit, owningCodexHome, {
|
|
1242
|
+
path: catalogBackupPathFor(catalogPath),
|
|
1243
|
+
content: pristine,
|
|
1244
|
+
});
|
|
1245
|
+
if (isDefaultCatalogPath(catalogPath)) {
|
|
1246
|
+
publishLegacyCodexCatalogBackup(permit, owningCodexHome, {
|
|
1247
|
+
path: legacyCatalogBackupPath(),
|
|
1248
|
+
content: pristine,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
} catch { /* backup best-effort */ }
|
|
1253
|
+
|
|
1254
|
+
// Hide disabled models from Codex, then feature the chosen subagent models (native OR routed)
|
|
1255
|
+
// by giving them the lowest priority — see buildCatalogEntries for why priority, not array order.
|
|
1256
|
+
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
1257
|
+
const featured = config.subagentModels ?? [];
|
|
1258
|
+
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
1259
|
+
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1260
|
+
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1261
|
+
const suppressedBareNativeSlugs = desktopAllowlistSuppressedNativeSlugs(config);
|
|
1262
|
+
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
1263
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1264
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
1265
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
1266
|
+
? visibleCodexAccountSelectors(config)
|
|
1267
|
+
: [];
|
|
1268
|
+
const wsEnabled = websocketsEnabled(config);
|
|
1269
|
+
const multiAgentV2Enabled = isMultiAgentV2Enabled();
|
|
1270
|
+
const goEntries = buildCatalogEntriesFromObservedState({
|
|
1271
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1272
|
+
gptSlugs: [],
|
|
1273
|
+
goModels: orderedGoModels,
|
|
1274
|
+
featured,
|
|
1275
|
+
wsEnabled,
|
|
1276
|
+
multiAgentMode,
|
|
1277
|
+
exactComboSlugs,
|
|
1278
|
+
accountSelectors,
|
|
1279
|
+
suppressedBareNativeSlugs,
|
|
1280
|
+
disabledNativeAccountSlugs: new Set(),
|
|
1281
|
+
multiAgentV2Enabled,
|
|
1282
|
+
});
|
|
1283
|
+
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
|
|
1284
|
+
// routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
|
|
1285
|
+
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
1286
|
+
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
1287
|
+
const baseline = readNativeBaseline(catalogPath);
|
|
1288
|
+
const gatheredProviderNames = new Set(
|
|
1289
|
+
Object.entries(config.providers ?? {})
|
|
1290
|
+
.filter(([, prov]) => prov.disabled !== true)
|
|
1291
|
+
.map(([name]) => name),
|
|
1292
|
+
);
|
|
1293
|
+
const degradedProviderNames = new Set(
|
|
1294
|
+
providerModelOutcomes
|
|
1295
|
+
.filter(outcome => outcome.state === "degraded")
|
|
1296
|
+
.map(outcome => outcome.provider),
|
|
1297
|
+
);
|
|
1298
|
+
const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
|
|
1299
|
+
Object.entries(config.providers ?? {}).flatMap(([name, provider]) => (
|
|
1300
|
+
provider.disabled !== true
|
|
1301
|
+
&& Array.isArray(provider.selectedModels)
|
|
1302
|
+
&& provider.selectedModels.length > 0
|
|
1303
|
+
? [[name, new Set(provider.selectedModels)] as const]
|
|
1304
|
+
: []
|
|
1305
|
+
)),
|
|
1306
|
+
);
|
|
1307
|
+
// Central WS capability override on the FINAL on-disk catalog (the file Codex reads). Applies to
|
|
1308
|
+
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
1309
|
+
// native template can never leak supports_websockets while the flag is off.
|
|
1310
|
+
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
|
|
1311
|
+
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
|
|
1312
|
+
// providers are configured yet (fresh install / catalog bootstrap tests).
|
|
1313
|
+
const accountBoundEntries = includeAccountBoundNativeOpenAi && accountSelectors.length > 0
|
|
1314
|
+
? buildCatalogEntriesFromObservedState({
|
|
1315
|
+
template: template ? JSON.parse(JSON.stringify(template)) : null,
|
|
1316
|
+
gptSlugs: NATIVE_OPENAI_MODELS,
|
|
1317
|
+
goModels: [],
|
|
1318
|
+
featured,
|
|
1319
|
+
wsEnabled,
|
|
1320
|
+
multiAgentMode,
|
|
1321
|
+
exactComboSlugs,
|
|
1322
|
+
accountSelectors,
|
|
1323
|
+
suppressedBareNativeSlugs,
|
|
1324
|
+
disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))),
|
|
1325
|
+
multiAgentV2Enabled,
|
|
1326
|
+
}).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
|
|
1327
|
+
: [];
|
|
1328
|
+
catalog.models = mergeCatalogEntriesFromObservedState({
|
|
1329
|
+
catalogModels: catalogModelsForMerge,
|
|
1330
|
+
baselineCatalogModels: baselineCatalog?.models ?? [],
|
|
1331
|
+
routedEntries: goEntries,
|
|
1332
|
+
baseline,
|
|
1333
|
+
featured,
|
|
1334
|
+
wsEnabled,
|
|
1335
|
+
template,
|
|
1336
|
+
disabledModels: new Set(config.disabledModels ?? []),
|
|
1337
|
+
selectedModelsByProvider,
|
|
1338
|
+
gatheredProviderNames,
|
|
1339
|
+
degradedProviderNames,
|
|
1340
|
+
legacyCustomModelSlugs: legacyCustomModelCatalogSlugs(config),
|
|
1341
|
+
multiAgentMode,
|
|
1342
|
+
multiAgentV2Enabled,
|
|
1343
|
+
exactComboSlugs,
|
|
1344
|
+
hasPhysicalComboProvider,
|
|
1345
|
+
includeNativeOpenAi,
|
|
1346
|
+
accountBoundEntries,
|
|
1347
|
+
suppressedBareNativeSlugs,
|
|
1348
|
+
policy: {
|
|
1349
|
+
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
1350
|
+
warningPolicy: "emit",
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
// The observed bundled catalog describes native OpenAI rows only. Preserve every routed
|
|
1354
|
+
// provider's exact ladder while still clamping native/account rows for old-client compatibility.
|
|
1355
|
+
clampCatalogModelsToCodexSupport(catalog.models, {}, { preserveRoutedCapabilities: true });
|
|
1356
|
+
|
|
1357
|
+
const catalogContent = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
1358
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1359
|
+
path: catalogPath,
|
|
1360
|
+
content: catalogContent,
|
|
1361
|
+
});
|
|
1362
|
+
// The external Codex app-server projects the generated `opencodex` profile.
|
|
1363
|
+
// Publish the exact same, complete catalog at a stable user-visible path for
|
|
1364
|
+
// that profile without adding model_catalog_json to the user's config.toml.
|
|
1365
|
+
const externalModelsPath = externalCodexModelsPath(owningCodexHome);
|
|
1366
|
+
if (!samePath(externalModelsPath, catalogPath)) {
|
|
1367
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1368
|
+
path: externalModelsPath,
|
|
1369
|
+
content: catalogContent,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
added: goEntries.length + accountBoundEntries.length,
|
|
1374
|
+
path: catalogPath,
|
|
1375
|
+
catalogWritten: true,
|
|
1376
|
+
comboOmissions,
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function visibleAccountReplacementNatives(
|
|
1381
|
+
models: readonly RawEntry[],
|
|
1382
|
+
disabledModels: ReadonlySet<string> | null,
|
|
1383
|
+
): Map<string, boolean> {
|
|
1384
|
+
const replacements = new Map<string, boolean>();
|
|
1385
|
+
for (const entry of models) {
|
|
1386
|
+
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
1387
|
+
if (nativeSlug === undefined || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(nativeSlug)) continue;
|
|
1388
|
+
const exactSlug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1389
|
+
const visible = entry.visibility === "list"
|
|
1390
|
+
|| (disabledModels !== null
|
|
1391
|
+
&& (disabledModels.has(nativeSlug) || disabledModels.has(exactSlug)));
|
|
1392
|
+
replacements.set(nativeSlug, (replacements.get(nativeSlug) ?? true) && visible);
|
|
1393
|
+
}
|
|
1394
|
+
return replacements;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function restoreAccountHiddenBareNatives(
|
|
1398
|
+
entries: readonly RawEntry[],
|
|
1399
|
+
replacementVisibility: ReadonlyMap<string, boolean>,
|
|
1400
|
+
disabledModels: ReadonlySet<string> | null,
|
|
1401
|
+
): RawEntry[] {
|
|
1402
|
+
return entries.map(entry => {
|
|
1403
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1404
|
+
if (
|
|
1405
|
+
entry.visibility !== "hide"
|
|
1406
|
+
|| !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)
|
|
1407
|
+
|| replacementVisibility.get(slug) !== true
|
|
1408
|
+
|| disabledModels === null
|
|
1409
|
+
|| disabledModels.has(slug)
|
|
1410
|
+
) {
|
|
1411
|
+
return entry;
|
|
1412
|
+
}
|
|
1413
|
+
return { ...entry, visibility: "list" };
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function currentDisabledModelsForRestore(): Set<string> | null {
|
|
1418
|
+
try {
|
|
1419
|
+
const diagnostics = readConfigDiagnostics();
|
|
1420
|
+
if (diagnostics.source === "fallback" || diagnostics.error !== null) return null;
|
|
1421
|
+
return new Set(diagnostics.config.disabledModels ?? []);
|
|
1422
|
+
} catch {
|
|
1423
|
+
// An unreadable config cannot safely authorize a visibility change during restore.
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
export async function syncCatalogModels(config: OcxConfig): Promise<RetainedCatalogSyncResult> {
|
|
1429
|
+
const owningCodexHome = getCodexHome();
|
|
1430
|
+
const preflightRead = readRetainedCatalogSync(config);
|
|
1431
|
+
if (preflightRead === null) {
|
|
1432
|
+
return {
|
|
1433
|
+
added: 0,
|
|
1434
|
+
path: readCodexCatalogPath(),
|
|
1435
|
+
catalogWritten: false,
|
|
1436
|
+
comboOmissions: [],
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const comboOmissions: ComboCatalogOmission[] = [];
|
|
1441
|
+
const providerModelOutcomes: CatalogGatherProviderModelOutcome[] = [];
|
|
1442
|
+
// Settle the bundled template, then baseline, and only then await. Reading it
|
|
1443
|
+
// here makes the memo ours before anyone else can move it, so a bundled swap
|
|
1444
|
+
// during the await is an outside change rather than our own side effect.
|
|
1445
|
+
//
|
|
1446
|
+
// The persisted runtime selection is covered by the filesystem evidence above
|
|
1447
|
+
// rather than by a process epoch; see `retainedCatalogProcessEvidence` for why
|
|
1448
|
+
// the in-memory runtime memo cannot be baselined honestly from this path.
|
|
1449
|
+
loadBundledCodexCatalog();
|
|
1450
|
+
const prepared: RetainedCatalogSyncRead = {
|
|
1451
|
+
...preflightRead,
|
|
1452
|
+
evidence: retainedCatalogSyncEvidence(config, preflightRead.catalogPath, preflightRead.catalog),
|
|
1453
|
+
processEvidence: retainedCatalogProcessEvidence(),
|
|
1454
|
+
};
|
|
1455
|
+
const goModels = await gatherRoutedModels(config, {
|
|
1456
|
+
comboOmissions,
|
|
1457
|
+
providerModelOutcomes,
|
|
1458
|
+
});
|
|
1459
|
+
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
|
|
1460
|
+
// Desired state can flip OFF during the provider await above. The catalog
|
|
1461
|
+
// evidence revalidation below cannot see that — intent lives in our config,
|
|
1462
|
+
// not in the catalog files — so the policy is re-read here, under K, right
|
|
1463
|
+
// before the only write. A lost race becomes the discriminated skip instead
|
|
1464
|
+
// of a routed catalog/cache surviving a completed disable.
|
|
1465
|
+
if (!shouldSyncCodexOnStart(loadConfig())) {
|
|
1466
|
+
return {
|
|
1467
|
+
added: 0,
|
|
1468
|
+
path: prepared.catalogPath,
|
|
1469
|
+
catalogWritten: false,
|
|
1470
|
+
comboOmissions,
|
|
1471
|
+
skippedReason: "desired_disabled" as const,
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
const current = revalidateRetainedCatalogSync(config, prepared);
|
|
1475
|
+
if (current === null) return null;
|
|
1476
|
+
return writeRetainedCatalogSync({
|
|
1477
|
+
config,
|
|
1478
|
+
goModels,
|
|
1479
|
+
providerModelOutcomes,
|
|
1480
|
+
comboOmissions,
|
|
1481
|
+
read: current,
|
|
1482
|
+
permit,
|
|
1483
|
+
owningCodexHome,
|
|
1484
|
+
});
|
|
1485
|
+
});
|
|
1486
|
+
if (committed.kind === "completed" && committed.value !== null) return committed.value;
|
|
1487
|
+
return {
|
|
1488
|
+
added: 0,
|
|
1489
|
+
path: prepared.catalogPath,
|
|
1490
|
+
catalogWritten: false,
|
|
1491
|
+
comboOmissions,
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
export function restoreCodexCatalogWithPermit(
|
|
1496
|
+
permit: CatalogWritePermit,
|
|
1497
|
+
owningCodexHome: string,
|
|
1498
|
+
): { removed: number; kept: number; path: string } {
|
|
1499
|
+
const catalogPath = readCodexCatalogPath();
|
|
1500
|
+
const catalog = readCatalog(catalogPath);
|
|
1501
|
+
if (!catalog || !Array.isArray(catalog.models)) return { removed: 0, kept: 0, path: catalogPath };
|
|
1502
|
+
const disabledModels = currentDisabledModelsForRestore();
|
|
1503
|
+
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
1504
|
+
const backup = readCatalogBackup(catalogPath);
|
|
1505
|
+
if (backup && Array.isArray(backup.models)) {
|
|
1506
|
+
const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" && m.slug.includes("/")).length;
|
|
1507
|
+
const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
|
|
1508
|
+
const userNativeAdditions = restoreAccountHiddenBareNatives(
|
|
1509
|
+
(catalog.models ?? []).filter(m =>
|
|
1510
|
+
typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
|
|
1511
|
+
),
|
|
1512
|
+
replacementVisibility,
|
|
1513
|
+
disabledModels,
|
|
1514
|
+
);
|
|
1515
|
+
const restored = {
|
|
1516
|
+
...backup,
|
|
1517
|
+
models: [...backup.models, ...userNativeAdditions],
|
|
1518
|
+
};
|
|
1519
|
+
const restoredContent = `${JSON.stringify(restored, null, 2)}\n`;
|
|
1520
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1521
|
+
path: catalogPath,
|
|
1522
|
+
content: restoredContent,
|
|
1523
|
+
});
|
|
1524
|
+
const externalModelsPath = externalCodexModelsPath(owningCodexHome);
|
|
1525
|
+
if (!samePath(externalModelsPath, catalogPath)) {
|
|
1526
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1527
|
+
path: externalModelsPath,
|
|
1528
|
+
content: restoredContent,
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
return { removed, kept: restored.models.length, path: catalogPath };
|
|
1532
|
+
}
|
|
1533
|
+
const before = catalog.models.length;
|
|
1534
|
+
const native = restoreAccountHiddenBareNatives(
|
|
1535
|
+
catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/"))),
|
|
1536
|
+
replacementVisibility,
|
|
1537
|
+
disabledModels,
|
|
1538
|
+
);
|
|
1539
|
+
const removed = before - native.length;
|
|
1540
|
+
catalog.models = native;
|
|
1541
|
+
const restoredContent = `${JSON.stringify(catalog, null, 2)}\n`;
|
|
1542
|
+
if (removed > 0) {
|
|
1543
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1544
|
+
path: catalogPath,
|
|
1545
|
+
content: restoredContent,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
const externalModelsPath = externalCodexModelsPath(owningCodexHome);
|
|
1549
|
+
if (!samePath(externalModelsPath, catalogPath)) {
|
|
1550
|
+
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
1551
|
+
path: externalModelsPath,
|
|
1552
|
+
content: restoredContent,
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
return { removed, kept: native.length, path: catalogPath };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
export function restoreCodexCatalog(): { removed: number; kept: number; path: string } {
|
|
1559
|
+
const owningCodexHome = getCodexHome();
|
|
1560
|
+
const outcome = withCatalogWriteSerialization(
|
|
1561
|
+
owningCodexHome,
|
|
1562
|
+
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
1563
|
+
);
|
|
1564
|
+
return outcome.kind === "completed"
|
|
1565
|
+
? outcome.value
|
|
1566
|
+
: { removed: 0, kept: 0, path: readCodexCatalogPath() };
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|
|
1570
|
+
export function invalidateCodexModelsCacheWithPermit(
|
|
1571
|
+
permit: CatalogWritePermit,
|
|
1572
|
+
owningCodexHome: string,
|
|
1573
|
+
): boolean {
|
|
1574
|
+
try {
|
|
1575
|
+
// This permit is a REACQUISITION: refreshCodexModelCatalog's commit released
|
|
1576
|
+
// K before this rewrite runs, so the commit-path desired-state check cannot
|
|
1577
|
+
// cover it. A disable landing in that gap must not be overwritten by a
|
|
1578
|
+
// routed cache write — re-read intent under this permit, same as the commit.
|
|
1579
|
+
if (!shouldSyncCodexOnStart(loadConfig())) return false;
|
|
1580
|
+
const catalogPath = readCodexCatalogPath();
|
|
1581
|
+
if (!existsSync(catalogPath)) return false;
|
|
1582
|
+
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
|
|
1583
|
+
const models = catalog.models ?? catalog;
|
|
1584
|
+
const wrapper = {
|
|
1585
|
+
fetched_at: "2000-01-01T00:00:00Z",
|
|
1586
|
+
client_version: "0.0.0",
|
|
1587
|
+
models,
|
|
1588
|
+
};
|
|
1589
|
+
replaceCodexModelsCache(permit, owningCodexHome, {
|
|
1590
|
+
path: activeCodexModelsCachePath(),
|
|
1591
|
+
content: `${JSON.stringify(wrapper, null, 2)}\n`,
|
|
1592
|
+
});
|
|
1593
|
+
return true;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
export function invalidateCodexModelsCache(): boolean {
|
|
1600
|
+
const owningCodexHome = getCodexHome();
|
|
1601
|
+
const outcome = withCatalogWriteSerialization(
|
|
1602
|
+
owningCodexHome,
|
|
1603
|
+
permit => invalidateCodexModelsCacheWithPermit(permit, owningCodexHome),
|
|
1604
|
+
);
|
|
1605
|
+
return outcome.kind === "completed" && outcome.value;
|
|
1606
|
+
}
|