@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +622 -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 +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -29,8 +29,48 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
|
|
|
29
29
|
}
|
|
30
30
|
return { values: raw as string[] };
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom-row reasoning ladder. Labels are validated against the Codex ladder (low..ultra)
|
|
35
|
+
* exactly like provider `modelReasoningEfforts` values; unknown labels would otherwise
|
|
36
|
+
* surface in a catalog the upstream never accepts. An empty array is meaningful (explicit
|
|
37
|
+
* "no reasoning" hides the effort control) and must be preserved, not cleared.
|
|
38
|
+
*/
|
|
39
|
+
function readReasoningEfforts(raw: unknown): { values?: string[]; error?: string } {
|
|
40
|
+
if (raw === undefined) return {};
|
|
41
|
+
if (!Array.isArray(raw)) return { error: "reasoningEfforts must be an array" };
|
|
42
|
+
const rejected: string[] = [];
|
|
43
|
+
const values: string[] = [];
|
|
44
|
+
for (const value of raw) {
|
|
45
|
+
if (typeof value !== "string") return { error: "reasoningEfforts must contain only strings" };
|
|
46
|
+
if (!isDeclaredReasoningEffort(value)) { rejected.push(value); continue; }
|
|
47
|
+
if (!values.includes(value)) values.push(value);
|
|
48
|
+
}
|
|
49
|
+
if (rejected.length > 0) {
|
|
50
|
+
return { error: `unsupported reasoning effort: ${rejected.join(", ")} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
|
|
51
|
+
}
|
|
52
|
+
// Canonical order: the catalog writes supported_reasoning_levels in input order and the
|
|
53
|
+
// fallback default picks the first entry, so a caller-chosen order must not leak through.
|
|
54
|
+
return { values: canonicalizeReasoningEfforts(values) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Default effort must be a ladder member that the declared ladder actually includes. */
|
|
58
|
+
function readDefaultReasoningEffort(raw: unknown, efforts: string[] | undefined): { value?: string; error?: string } {
|
|
59
|
+
if (raw === undefined) return {};
|
|
60
|
+
if (raw === null) return { value: undefined };
|
|
61
|
+
if (typeof raw !== "string" || !isDeclaredReasoningEffort(raw)) {
|
|
62
|
+
return { error: "defaultReasoningEffort must be one of: none, minimal, low, medium, high, xhigh, max, ultra" };
|
|
63
|
+
}
|
|
64
|
+
if (efforts === undefined || efforts.length === 0) {
|
|
65
|
+
return { error: "defaultReasoningEffort requires a non-empty reasoningEfforts ladder" };
|
|
66
|
+
}
|
|
67
|
+
if (!efforts.includes(raw)) {
|
|
68
|
+
return { error: `defaultReasoningEffort "${raw}" is not in the declared reasoningEfforts ladder` };
|
|
69
|
+
}
|
|
70
|
+
return { value: raw };
|
|
71
|
+
}
|
|
32
72
|
import type { CatalogModel } from "../../codex/catalog";
|
|
33
|
-
import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
73
|
+
import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
34
74
|
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
35
75
|
import { getProviderLiveModelCount } from "../../codex/model-cache";
|
|
36
76
|
import {
|
|
@@ -57,8 +97,11 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
57
97
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
58
98
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
59
99
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
60
|
-
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
61
|
-
import {
|
|
100
|
+
import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
101
|
+
import { knownModelIdsForProvider } from "../../router";
|
|
102
|
+
import { effectiveModelAliases, MODEL_ALIAS_PATTERN } from "../../providers/default-aliases";
|
|
103
|
+
import { comboPublicModelId } from "../../combos/types";
|
|
104
|
+
import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
62
105
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
63
106
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
64
107
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
@@ -71,6 +114,7 @@ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summa
|
|
|
71
114
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
72
115
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
73
116
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
117
|
+
import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../../reasoning-effort";
|
|
74
118
|
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
75
119
|
import {
|
|
76
120
|
clearDebugSettings,
|
|
@@ -87,10 +131,11 @@ import type { PersistedUsageAttempt } from "../../usage/log";
|
|
|
87
131
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
|
|
88
132
|
import { applySystemEnvToggle } from "../system-env";
|
|
89
133
|
import {
|
|
134
|
+
ClientPathError,
|
|
90
135
|
EXPORT_CLIENTS,
|
|
91
136
|
EXPORT_CLIENT_IDS,
|
|
92
137
|
OPENCODE_PROVIDER_ID,
|
|
93
|
-
|
|
138
|
+
buildClientConfigText,
|
|
94
139
|
isExportClientId,
|
|
95
140
|
opencodeProxyBaseUrl,
|
|
96
141
|
} from "../../clients/config-export";
|
|
@@ -104,92 +149,14 @@ import type {
|
|
|
104
149
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
105
150
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
106
151
|
import type { ManagementContext } from "./context";
|
|
152
|
+
import { listManagementModelRows, loadExportModels } from "./model-rows";
|
|
107
153
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
type ManagementModelRow = Partial<CatalogModel> & {
|
|
115
|
-
provider: string;
|
|
116
|
-
id: string;
|
|
117
|
-
namespaced: string;
|
|
118
|
-
disabled: boolean;
|
|
119
|
-
native?: boolean;
|
|
120
|
-
custom?: boolean;
|
|
121
|
-
customId?: string;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
|
|
126
|
-
* models the GUI's Models tab shows — including this function's `disabled` computation,
|
|
127
|
-
* which the export core (src/clients/config-export.ts) deliberately does not perform.
|
|
128
|
-
*/
|
|
129
|
-
async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
|
|
130
|
-
const models = await fetchAllModels(config);
|
|
131
|
-
const disabled = new Set(config.disabledModels ?? []);
|
|
132
|
-
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
133
|
-
// from the static supported set so a disabled model stays listed and re-enableable.
|
|
134
|
-
const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
|
|
135
|
-
provider: "openai",
|
|
136
|
-
id: row.slug,
|
|
137
|
-
namespaced: row.slug,
|
|
138
|
-
disabled: row.disabled,
|
|
139
|
-
native: true,
|
|
140
|
-
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
141
|
-
}));
|
|
142
|
-
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
143
|
-
const namespaced = routedSlug(cm.provider, cm.modelId);
|
|
144
|
-
return {
|
|
145
|
-
provider: cm.provider,
|
|
146
|
-
id: cm.modelId,
|
|
147
|
-
namespaced,
|
|
148
|
-
disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
|
|
149
|
-
custom: true,
|
|
150
|
-
customId: cm.id,
|
|
151
|
-
displayName: cm.displayName,
|
|
152
|
-
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
153
|
-
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
154
|
-
};
|
|
155
|
-
});
|
|
156
|
-
const publicModels = uniqueCatalogModelsForPublicList(models);
|
|
157
|
-
const comboNamespaced = new Set(
|
|
158
|
-
publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
|
|
159
|
-
);
|
|
160
|
-
const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
|
|
161
|
-
// Custom metadata wins when a physical live/static row resolves to the same Codex-facing
|
|
162
|
-
// slug, while a combo keeps the same precedence it has in routing and /v1/models.
|
|
163
|
-
const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
|
|
164
|
-
const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
|
|
165
|
-
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
166
|
-
const namespaced = catalogModelSlug(m);
|
|
167
|
-
if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
|
|
168
|
-
const contextCap = providerContextCap(config, m.provider);
|
|
169
|
-
return {
|
|
170
|
-
...m,
|
|
171
|
-
namespaced,
|
|
172
|
-
disabled: [...disabled].some(stored => (
|
|
173
|
-
stored === namespaced || slugEquals(stored, m.provider, m.id)
|
|
174
|
-
)),
|
|
175
|
-
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
176
|
-
};
|
|
177
|
-
}).filter((row): row is ManagementModelRow => row !== null);
|
|
178
|
-
return [...native, ...dedupedRouted, ...visibleCustomModels];
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** `/api/models` row → the narrower input the client-config serializers accept. */
|
|
182
|
-
function toExportModel(row: ManagementModelRow): ExportModel {
|
|
183
|
-
return {
|
|
184
|
-
namespaced: row.namespaced,
|
|
185
|
-
provider: row.provider,
|
|
186
|
-
id: row.id,
|
|
187
|
-
...(row.native ? { native: true } : {}),
|
|
188
|
-
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
189
|
-
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
190
|
-
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
154
|
+
import {
|
|
155
|
+
hasModelPreset,
|
|
156
|
+
markModelPresetDiverged,
|
|
157
|
+
materializeModelPreset,
|
|
158
|
+
modelPresetFor,
|
|
159
|
+
} from "../../providers/model-presets";
|
|
193
160
|
|
|
194
161
|
/**
|
|
195
162
|
* Counts read back off the SERIALIZED document rather than recomputed from the input rows.
|
|
@@ -198,23 +165,172 @@ function toExportModel(row: ManagementModelRow): ExportModel {
|
|
|
198
165
|
* core's "authoritative context window" rule would be free to drift from it silently.
|
|
199
166
|
*/
|
|
200
167
|
function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
|
|
207
|
-
return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
|
|
168
|
+
// Each client counts its own document shape. The previous branch assumed
|
|
169
|
+
// "anything that is not OpenCode must be Pi", which silently misread the
|
|
170
|
+
// moment a third client existed.
|
|
171
|
+
return EXPORT_CLIENTS[client].summarize(document);
|
|
208
172
|
}
|
|
209
173
|
|
|
210
174
|
export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
211
|
-
const { req, url, config, deps,
|
|
175
|
+
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
212
176
|
// A handler persists the exact config object passed in. Production defaults to
|
|
213
177
|
// the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
|
|
214
178
|
// bypass this seam with a dynamic config import — doing so replaced a user's
|
|
215
179
|
// ~/.opencodex/config.json with the `existing-uuid` test fixture.
|
|
216
180
|
const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
|
|
217
181
|
|
|
182
|
+
if (url.pathname === "/api/model-discovery" && req.method === "GET") {
|
|
183
|
+
const providers = Object.fromEntries(Object.entries(config.providers).map(([name, provider]) => [
|
|
184
|
+
name, provider.newModelPolicy ?? "inherit",
|
|
185
|
+
]));
|
|
186
|
+
const recentArrivals = Object.fromEntries(Object.entries(config.modelDiscovery?.recentArrivals ?? {}).map(([name, rows]) => [
|
|
187
|
+
name,
|
|
188
|
+
rows.map(row => ({
|
|
189
|
+
...row,
|
|
190
|
+
state: (config.disabledModels ?? []).some(slug => slugEquals(slug, name, row.id))
|
|
191
|
+
? "auto-disabled" : "enabled",
|
|
192
|
+
})),
|
|
193
|
+
]));
|
|
194
|
+
const baselineCounts = Object.fromEntries(Object.entries(config.modelDiscovery?.knownModels ?? {}).map(([name, baseline]) => [
|
|
195
|
+
name, baseline.ids.length,
|
|
196
|
+
]));
|
|
197
|
+
return jsonResponse({
|
|
198
|
+
policy: config.modelDiscovery?.newModelPolicy ?? "on", providers, recentArrivals, baselineCounts,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (url.pathname === "/api/model-discovery" && req.method === "PUT") {
|
|
203
|
+
let body: { policy?: unknown; provider?: unknown };
|
|
204
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
205
|
+
if (body.policy !== "on" && body.policy !== "off") return jsonResponse({ error: "policy must be on or off" }, 400);
|
|
206
|
+
const provider = typeof body.provider === "string" && body.provider.trim() ? body.provider.trim() : null;
|
|
207
|
+
let baselineBootstrapped = false;
|
|
208
|
+
if (provider) {
|
|
209
|
+
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
210
|
+
config.providers[provider].newModelPolicy = body.policy;
|
|
211
|
+
} else {
|
|
212
|
+
const wasAbsent = config.modelDiscovery?.newModelPolicy === undefined;
|
|
213
|
+
config.modelDiscovery ??= {};
|
|
214
|
+
config.modelDiscovery.newModelPolicy = body.policy;
|
|
215
|
+
if (body.policy === "off" && wasAbsent) {
|
|
216
|
+
const models = await fetchAllModels(config);
|
|
217
|
+
const known = config.modelDiscovery.knownModels ??= {};
|
|
218
|
+
const at = new Date().toISOString();
|
|
219
|
+
for (const name of Object.keys(config.providers)) {
|
|
220
|
+
known[name] ??= { ids: [...new Set(models.filter(m => m.provider === name).map(m => m.id))].sort(), removed: [], updatedAt: at };
|
|
221
|
+
}
|
|
222
|
+
baselineBootstrapped = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
persistConfig(config);
|
|
226
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
227
|
+
return jsonResponse({ ok: true, policy: body.policy, provider, ...(baselineBootstrapped ? { baselineBootstrapped } : {}), catalogRefresh });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (url.pathname === "/api/model-discovery/acknowledge" && req.method === "POST") {
|
|
231
|
+
let body: { provider?: unknown; ids?: unknown };
|
|
232
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
233
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
234
|
+
if (!provider || !Array.isArray(body.ids) || body.ids.some(id => typeof id !== "string")) {
|
|
235
|
+
return jsonResponse({ error: "provider and string ids are required" }, 400);
|
|
236
|
+
}
|
|
237
|
+
const acknowledged = new Set(body.ids as string[]);
|
|
238
|
+
const recent = config.modelDiscovery?.recentArrivals;
|
|
239
|
+
if (recent?.[provider]) recent[provider] = recent[provider].filter(row => !acknowledged.has(row.id));
|
|
240
|
+
persistConfig(config);
|
|
241
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
242
|
+
return jsonResponse({ ok: true, provider, acknowledged: [...acknowledged], catalogRefresh });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (url.pathname === "/api/aliases" && req.method === "GET") {
|
|
246
|
+
const providers: Record<string, string> = {};
|
|
247
|
+
const models: Record<string, Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }>> = {};
|
|
248
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
249
|
+
if (provider.alias) providers[name] = provider.alias;
|
|
250
|
+
const known = knownModelIdsForProvider(name, provider, config);
|
|
251
|
+
const knownSet = new Set(known);
|
|
252
|
+
const rows: Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }> = {};
|
|
253
|
+
for (const [id, value] of effectiveModelAliases(config, provider, new Set([...known, ...Object.keys(provider.modelAliases ?? {})]))) {
|
|
254
|
+
rows[id] = { ...value, ...(!knownSet.has(id) ? { stale: true } : {}) };
|
|
255
|
+
}
|
|
256
|
+
if (Object.keys(rows).length) models[name] = rows;
|
|
257
|
+
}
|
|
258
|
+
return jsonResponse({ providers, models, defaults: {
|
|
259
|
+
global: config.defaultModelAliases ?? false,
|
|
260
|
+
providers: Object.fromEntries(Object.entries(config.providers).filter(([, p]) => p.defaultAliases !== undefined).map(([n, p]) => [n, p.defaultAliases])),
|
|
261
|
+
} });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const providerAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/alias$/);
|
|
265
|
+
if (providerAliasMatch && req.method === "PUT") {
|
|
266
|
+
const name = decodeURIComponent(providerAliasMatch[1]!);
|
|
267
|
+
// `keys` is not a provider name: `/api/providers/keys/alias` is the API-KEY POOL's rename
|
|
268
|
+
// endpoint (oauth-account-routes.ts), and model routes are dispatched BEFORE it. Without
|
|
269
|
+
// this guard the alias route matched `name = "keys"`, found no such provider, and returned
|
|
270
|
+
// 404 for every key-pool rename.
|
|
271
|
+
if (name === "keys") return null;
|
|
272
|
+
const provider = config.providers[name];
|
|
273
|
+
if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
|
|
274
|
+
let raw: unknown;
|
|
275
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
276
|
+
if (!isPlainRecord(raw) || (raw.alias !== null && typeof raw.alias !== "string")) return jsonResponse({ error: "alias must be a string or null" }, 400, req, config);
|
|
277
|
+
const alias = typeof raw.alias === "string" ? raw.alias.trim() : null;
|
|
278
|
+
if (alias && !isValidProviderName(alias)) return jsonResponse({ error: "invalid provider alias" }, 400, req, config);
|
|
279
|
+
const lower = alias?.toLowerCase();
|
|
280
|
+
const collision = lower && Object.entries(config.providers).find(([other, p]) =>
|
|
281
|
+
other !== name && (other.toLowerCase() === lower || p.alias?.toLowerCase() === lower));
|
|
282
|
+
const comboCollision = lower && Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower);
|
|
283
|
+
const accountCollision = lower && Object.keys(config.codexAccountNamespaces ?? {}).find(value => value.toLowerCase() === lower);
|
|
284
|
+
if (collision || comboCollision || accountCollision) return jsonResponse({ error: `alias conflicts with '${collision?.[0] ?? comboCollision?.[0] ?? accountCollision}'` }, 409, req, config);
|
|
285
|
+
if (alias) provider.alias = alias; else delete provider.alias;
|
|
286
|
+
persistConfig(config);
|
|
287
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
288
|
+
return jsonResponse({ ok: true, provider: name, alias, catalogRefresh });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const modelAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-aliases$/);
|
|
292
|
+
if (modelAliasMatch && req.method === "PUT") {
|
|
293
|
+
const name = decodeURIComponent(modelAliasMatch[1]!);
|
|
294
|
+
if (name === "keys") return null;
|
|
295
|
+
const provider = config.providers[name];
|
|
296
|
+
if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
|
|
297
|
+
let raw: unknown;
|
|
298
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
299
|
+
if (!isPlainRecord(raw) || (raw.set !== undefined && !isPlainRecord(raw.set)) || (raw.remove !== undefined && !Array.isArray(raw.remove))) return jsonResponse({ error: "invalid model alias update" }, 400, req, config);
|
|
300
|
+
const next = { ...(provider.modelAliases ?? {}) };
|
|
301
|
+
for (const id of (raw.remove ?? []) as unknown[]) if (typeof id === "string") delete next[id];
|
|
302
|
+
const conflicts: Array<{ alias: string; heldBy: string }> = [];
|
|
303
|
+
const known = knownModelIdsForProvider(name, provider, config);
|
|
304
|
+
for (const [id, value] of Object.entries((raw.set ?? {}) as Record<string, unknown>)) {
|
|
305
|
+
if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value)) return jsonResponse({ error: `invalid model alias for '${id}'` }, 400, req, config);
|
|
306
|
+
const lower = value.toLowerCase();
|
|
307
|
+
const heldBy = Object.entries(next).find(([other, alias]) => other !== id && alias.toLowerCase() === lower)?.[0]
|
|
308
|
+
?? known.find(native => native.toLowerCase() === lower)
|
|
309
|
+
?? Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower)?.[0];
|
|
310
|
+
if (heldBy || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) conflicts.push({ alias: value, heldBy: heldBy ?? "native OpenAI family" });
|
|
311
|
+
else next[id] = value;
|
|
312
|
+
}
|
|
313
|
+
if (conflicts.length) return jsonResponse({ error: "model alias collision", conflicts }, 409, req, config);
|
|
314
|
+
provider.modelAliases = next;
|
|
315
|
+
persistConfig(config);
|
|
316
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
317
|
+
return jsonResponse({ ok: true, aliases: next, catalogRefresh });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (url.pathname === "/api/default-aliases" && req.method === "PUT") {
|
|
321
|
+
let raw: unknown;
|
|
322
|
+
try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
323
|
+
if (!isPlainRecord(raw) || typeof raw.enabled !== "boolean" || (raw.provider !== undefined && typeof raw.provider !== "string")) return jsonResponse({ error: "enabled must be boolean" }, 400, req, config);
|
|
324
|
+
if (typeof raw.provider === "string") {
|
|
325
|
+
const provider = config.providers[raw.provider];
|
|
326
|
+
if (!provider) return jsonResponse({ error: `provider '${raw.provider}' not found` }, 404, req, config);
|
|
327
|
+
provider.defaultAliases = raw.enabled;
|
|
328
|
+
} else config.defaultModelAliases = raw.enabled;
|
|
329
|
+
persistConfig(config);
|
|
330
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
331
|
+
return jsonResponse({ ok: true, catalogRefresh });
|
|
332
|
+
}
|
|
333
|
+
|
|
218
334
|
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
219
335
|
const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
|
|
220
336
|
const catalog = readCatalog(readCodexCatalogPath());
|
|
@@ -249,9 +365,33 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
249
365
|
);
|
|
250
366
|
}
|
|
251
367
|
const spec = EXPORT_CLIENTS[requested];
|
|
252
|
-
|
|
368
|
+
// Resolved before the catalog load on purpose. A refused override is a
|
|
369
|
+
// property of the request, not of the catalog: validating it afterwards
|
|
370
|
+
// let a busy or failing catalog answer 503 first, so a user with a
|
|
371
|
+
// relative override never saw the message that says how to fix it — and
|
|
372
|
+
// the route did the enumeration work anyway for input it was going to
|
|
373
|
+
// reject.
|
|
374
|
+
let destination: string;
|
|
253
375
|
try {
|
|
254
|
-
|
|
376
|
+
destination = spec.destination(process.env);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
// A client's own environment override can name a path the resolver
|
|
379
|
+
// refuses — a relative value, which this process and the client would
|
|
380
|
+
// resolve against different working directories. That is a
|
|
381
|
+
// user-correctable configuration error, not a server fault, so it leaves
|
|
382
|
+
// this boundary as a bounded 400 instead of escaping handleManagementAPI
|
|
383
|
+
// as a generic 500 and stripping the message that says how to fix it.
|
|
384
|
+
// `integrations/state.ts` and `integrations/writer.ts` already catch the
|
|
385
|
+
// same error on their paths; this route was the one that did not.
|
|
386
|
+
if (!(error instanceof ClientPathError)) throw error;
|
|
387
|
+
return jsonResponse({ error: error.message }, 400, req, config);
|
|
388
|
+
}
|
|
389
|
+
let models: ExportModel[];
|
|
390
|
+
try {
|
|
391
|
+
// The ONE loader every export surface uses. It carries the visibility
|
|
392
|
+
// filter with it, so this route and the integration routes cannot end up
|
|
393
|
+
// telling a client about different models.
|
|
394
|
+
models = await loadExportModels(config);
|
|
255
395
|
} catch (error) {
|
|
256
396
|
// A partial or empty `models` block reads as a valid config while offering nothing,
|
|
257
397
|
// so a catalog failure is surfaced as unavailable rather than serialized. The
|
|
@@ -264,21 +404,24 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
264
404
|
config,
|
|
265
405
|
);
|
|
266
406
|
}
|
|
267
|
-
|
|
268
|
-
// user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
|
|
269
|
-
// client a selector the proxy refuses to route.
|
|
270
|
-
const models = rows.filter(row => !row.disabled).map(toExportModel);
|
|
271
|
-
const document = buildClientConfig(requested, {
|
|
407
|
+
const built = buildClientConfigText(requested, {
|
|
272
408
|
baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
|
|
273
409
|
models,
|
|
274
410
|
config,
|
|
275
411
|
});
|
|
412
|
+
const document = built.document;
|
|
276
413
|
return jsonResponse({
|
|
277
414
|
client: spec.id,
|
|
278
415
|
filename: spec.filename,
|
|
279
|
-
destination
|
|
416
|
+
destination,
|
|
280
417
|
apiKeyEnv: spec.apiKeyEnv,
|
|
281
418
|
exportHint: spec.exportHint,
|
|
419
|
+
// The client's own format and the exact bytes for it. The GUI previously
|
|
420
|
+
// re-serialized `config` as JSON, which is wrong for four of the six
|
|
421
|
+
// clients; `mediaType` also drives the download blob.
|
|
422
|
+
format: built.format,
|
|
423
|
+
mediaType: built.mediaType,
|
|
424
|
+
text: built.text,
|
|
282
425
|
...summarizeExportedModels(requested, document),
|
|
283
426
|
config: document,
|
|
284
427
|
}, 200, req, config);
|
|
@@ -292,8 +435,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
292
435
|
const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
|
|
293
436
|
config.disabledModels = disabled;
|
|
294
437
|
persistConfig(config);
|
|
295
|
-
await
|
|
296
|
-
return jsonResponse({ ok: true, disabled });
|
|
438
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
439
|
+
return jsonResponse({ ok: true, disabled, catalogRefresh });
|
|
297
440
|
}
|
|
298
441
|
|
|
299
442
|
// One user-facing visibility switch spans two persisted filters: a provider allowlist and the
|
|
@@ -315,7 +458,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
315
458
|
if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
|
|
316
459
|
return jsonResponse({ error: "unknown model visibility provider" }, 400);
|
|
317
460
|
}
|
|
318
|
-
const
|
|
461
|
+
const accountNativeQualified = shouldIncludeAccountBoundNativeOpenAi(config)
|
|
462
|
+
? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
|
|
463
|
+
slugs.filter(slug => !nativeModelRows(config).some(row => row.slug === slug)).map(slug => `${selector}/${slug}`))
|
|
464
|
+
: [];
|
|
465
|
+
const supportedNative = new Set([
|
|
466
|
+
...nativeModelRows(config).map(row => row.slug),
|
|
467
|
+
...accountNativeQualified,
|
|
468
|
+
]);
|
|
319
469
|
const targets: Array<{ id: string; native: boolean }> = [];
|
|
320
470
|
const seen = new Set<string>();
|
|
321
471
|
for (const value of body.targets) {
|
|
@@ -336,17 +486,16 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
336
486
|
if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
|
|
337
487
|
|
|
338
488
|
const knownComboSelectors = new Set(
|
|
339
|
-
Object.entries(config.combos ?? {}).flatMap(([id, combo]) =>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
]),
|
|
489
|
+
Object.entries(config.combos ?? {}).flatMap(([id, combo]) => (
|
|
490
|
+
comboDisabledModelSelectors(id, combo)
|
|
491
|
+
)),
|
|
343
492
|
);
|
|
344
493
|
const targetComboSelectors = new Map<string, Set<string>>();
|
|
345
494
|
if (isVirtualComboNamespace) {
|
|
346
495
|
for (const target of targets) {
|
|
347
496
|
const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
|
|
348
497
|
if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
349
|
-
targetComboSelectors.set(target.id, new Set(
|
|
498
|
+
targetComboSelectors.set(target.id, new Set(comboDisabledModelSelectors(target.id, combo)));
|
|
350
499
|
}
|
|
351
500
|
}
|
|
352
501
|
const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
|
|
@@ -365,9 +514,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
365
514
|
const nativeIds = provider === "openai"
|
|
366
515
|
? disabledNativeSlugs({ disabledModels: disabled })
|
|
367
516
|
: new Set<string>();
|
|
517
|
+
const accountNativeIds = provider === "openai" ? new Set(accountNativeQualified) : new Set<string>();
|
|
518
|
+
const nativeAliasSlugs = provider === "openai"
|
|
519
|
+
? configuredNativeAliasSlugs(config)
|
|
520
|
+
: new Set<string>();
|
|
368
521
|
disabled = disabled.filter(stored => (
|
|
369
522
|
knownComboSelectors.has(stored)
|
|
370
|
-
||
|
|
523
|
+
|| nativeAliasSlugs.has(stored)
|
|
524
|
+
|| (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored) && !accountNativeIds.has(stored))
|
|
371
525
|
));
|
|
372
526
|
}
|
|
373
527
|
} else {
|
|
@@ -376,6 +530,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
376
530
|
providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
|
|
377
531
|
}
|
|
378
532
|
disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
|
|
533
|
+
const arrivals = config.modelDiscovery?.recentArrivals?.[provider];
|
|
534
|
+
if (arrivals) config.modelDiscovery!.recentArrivals![provider] = arrivals.filter(row => (
|
|
535
|
+
!targets.some(target => !target.native && target.id === row.id)
|
|
536
|
+
));
|
|
379
537
|
}
|
|
380
538
|
} else {
|
|
381
539
|
for (const target of targets) {
|
|
@@ -391,8 +549,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
391
549
|
|
|
392
550
|
config.disabledModels = disabled;
|
|
393
551
|
persistConfig(config);
|
|
394
|
-
await
|
|
395
|
-
return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
|
|
552
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
553
|
+
return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled, catalogRefresh });
|
|
396
554
|
}
|
|
397
555
|
|
|
398
556
|
if (url.pathname === "/api/custom-models" && req.method === "GET") {
|
|
@@ -400,12 +558,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
400
558
|
}
|
|
401
559
|
|
|
402
560
|
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
403
|
-
let
|
|
404
|
-
try {
|
|
561
|
+
let parsedBody: unknown;
|
|
562
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
563
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
564
|
+
const body = parsedBody;
|
|
405
565
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
406
566
|
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
407
567
|
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
408
|
-
if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
409
568
|
if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
|
|
410
569
|
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
|
|
411
570
|
const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
|
|
@@ -414,11 +573,19 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
414
573
|
const modalities = readInputModalities(body.inputModalities);
|
|
415
574
|
if (modalities.error) return jsonResponse({ error: modalities.error }, 400);
|
|
416
575
|
const inputModalities = modalities.values;
|
|
576
|
+
const reasoning = readReasoningEfforts(body.reasoningEfforts);
|
|
577
|
+
if (reasoning.error) return jsonResponse({ error: reasoning.error }, 400);
|
|
578
|
+
const defaultEffort = readDefaultReasoningEffort(body.defaultReasoningEffort, reasoning.values);
|
|
579
|
+
if (defaultEffort.error) return jsonResponse({ error: defaultEffort.error }, 400);
|
|
417
580
|
const existing = config.customModels ?? [];
|
|
418
581
|
const newSlug = routedSlug(provider, modelId);
|
|
419
582
|
if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
|
|
420
583
|
return jsonResponse({ error: "duplicate model" }, 409);
|
|
421
584
|
}
|
|
585
|
+
const known = knownModelIdsForProvider(provider, config.providers[provider], config);
|
|
586
|
+
if (encodedModelIdCollides(modelId, known)) {
|
|
587
|
+
return jsonResponse({ error: "ambiguous model id" }, 409);
|
|
588
|
+
}
|
|
422
589
|
const entry: OcxCustomModel = {
|
|
423
590
|
id: randomUUID(),
|
|
424
591
|
provider,
|
|
@@ -426,26 +593,29 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
426
593
|
...(displayName ? { displayName } : {}),
|
|
427
594
|
...(contextWindow ? { contextWindow } : {}),
|
|
428
595
|
...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
|
|
596
|
+
...(reasoning.values !== undefined ? { reasoningEfforts: reasoning.values } : {}),
|
|
597
|
+
...(defaultEffort.value ? { defaultReasoningEffort: defaultEffort.value } : {}),
|
|
429
598
|
addedAt: new Date().toISOString(),
|
|
430
599
|
};
|
|
431
600
|
config.customModels = [...existing, entry];
|
|
432
601
|
persistConfig(config);
|
|
433
|
-
await
|
|
434
|
-
return jsonResponse(entry, 201);
|
|
602
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
603
|
+
return jsonResponse({ ...entry, catalogRefresh }, 201);
|
|
435
604
|
}
|
|
436
605
|
|
|
437
606
|
const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
438
607
|
if (customPutMatch && req.method === "PUT") {
|
|
439
608
|
let id: string;
|
|
440
609
|
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
441
|
-
let
|
|
442
|
-
try {
|
|
610
|
+
let parsedBody: unknown;
|
|
611
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
612
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
613
|
+
const body = parsedBody;
|
|
443
614
|
const list = config.customModels ?? [];
|
|
444
615
|
const idx = list.findIndex(cm => cm.id === id);
|
|
445
616
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
446
617
|
const cm = { ...list[idx] };
|
|
447
618
|
if (typeof body.modelId === "string" && body.modelId.trim()) {
|
|
448
|
-
if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
449
619
|
cm.modelId = body.modelId.trim();
|
|
450
620
|
}
|
|
451
621
|
if (body.displayName !== undefined) {
|
|
@@ -461,15 +631,48 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
461
631
|
if (edited.error) return jsonResponse({ error: edited.error }, 400);
|
|
462
632
|
cm.inputModalities = edited.values && edited.values.length > 0 ? edited.values : undefined;
|
|
463
633
|
}
|
|
634
|
+
// `null` clears the stored ladder back to "inherit from the provider row"; `[]` stays
|
|
635
|
+
// stored as an explicit "no reasoning" override. The default effort rides along and is
|
|
636
|
+
// validated against the ladder the row ends up with.
|
|
637
|
+
if (body.reasoningEfforts !== undefined) {
|
|
638
|
+
if (body.reasoningEfforts === null) {
|
|
639
|
+
cm.reasoningEfforts = undefined;
|
|
640
|
+
} else {
|
|
641
|
+
const edited = readReasoningEfforts(body.reasoningEfforts);
|
|
642
|
+
if (edited.error) return jsonResponse({ error: edited.error }, 400);
|
|
643
|
+
cm.reasoningEfforts = edited.values;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (body.defaultReasoningEffort !== undefined) {
|
|
647
|
+
const edited = readDefaultReasoningEffort(body.defaultReasoningEffort, cm.reasoningEfforts);
|
|
648
|
+
if (edited.error) return jsonResponse({ error: edited.error }, 400);
|
|
649
|
+
cm.defaultReasoningEffort = edited.value;
|
|
650
|
+
}
|
|
651
|
+
// Mirror of the POST invariant: a default only survives as a member of the final ladder.
|
|
652
|
+
// Without this, a ladder shrink/clear on a row that was created with a default leaves a
|
|
653
|
+
// stale default that re-applies itself onto the inherited ladder in the generated catalog
|
|
654
|
+
// (the GUI toggle-off path sends only reasoningEfforts, never the default).
|
|
655
|
+
if (cm.defaultReasoningEffort !== undefined) {
|
|
656
|
+
const ladder = cm.reasoningEfforts;
|
|
657
|
+
if (!ladder || ladder.length === 0 || !ladder.includes(cm.defaultReasoningEffort)) {
|
|
658
|
+
cm.defaultReasoningEffort = undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
464
661
|
const updatedSlug = routedSlug(cm.provider, cm.modelId);
|
|
465
662
|
if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
|
|
466
663
|
return jsonResponse({ error: "duplicate model" }, 409);
|
|
467
664
|
}
|
|
665
|
+
const known = knownModelIdsForProvider(cm.provider, config.providers[cm.provider], {
|
|
666
|
+
customModels: list.filter((_, i) => i !== idx),
|
|
667
|
+
});
|
|
668
|
+
if (encodedModelIdCollides(cm.modelId, known)) {
|
|
669
|
+
return jsonResponse({ error: "ambiguous model id" }, 409);
|
|
670
|
+
}
|
|
468
671
|
list[idx] = cm;
|
|
469
672
|
config.customModels = list;
|
|
470
673
|
persistConfig(config);
|
|
471
|
-
await
|
|
472
|
-
return jsonResponse(cm);
|
|
674
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
675
|
+
return jsonResponse({ ...cm, catalogRefresh });
|
|
473
676
|
}
|
|
474
677
|
|
|
475
678
|
const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
@@ -482,15 +685,24 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
482
685
|
list.splice(idx, 1);
|
|
483
686
|
config.customModels = list.length > 0 ? list : undefined;
|
|
484
687
|
persistConfig(config);
|
|
485
|
-
await
|
|
486
|
-
return jsonResponse({ ok: true });
|
|
688
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
689
|
+
return jsonResponse({ ok: true, catalogRefresh });
|
|
487
690
|
}
|
|
488
691
|
|
|
489
692
|
// Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
|
|
490
693
|
// only those ids ship to Codex's catalog / /v1/models. GET returns the CURRENT selection plus the
|
|
491
|
-
// FULL available set per provider
|
|
694
|
+
// FULL available set per provider for the management picker.
|
|
695
|
+
//
|
|
696
|
+
// `fetchAllModels` hides media-generation ids (image/video) from the Codex-facing catalog, but the
|
|
697
|
+
// Providers → Models tab must still list every id the user fetched and saved on the provider
|
|
698
|
+
// (e.g. grok2api's grok-imagine-*). Merge `providers[name].models` on top so those rows are not
|
|
699
|
+
// silently dropped after a successful refresh-models persist.
|
|
700
|
+
//
|
|
701
|
+
// Cold path: prefer cache/configured seeds so the Providers page paints in milliseconds instead
|
|
702
|
+
// of waiting on every upstream `/models` (often multi-second). When any live provider still needs
|
|
703
|
+
// a network probe, kick a background gather and set `refreshing: true` so the GUI can re-poll.
|
|
492
704
|
if (url.pathname === "/api/selected-models" && req.method === "GET") {
|
|
493
|
-
const models = await fetchAllModels(config);
|
|
705
|
+
const models = await fetchAllModels(config, { preferCached: true });
|
|
494
706
|
const available: Record<string, string[]> = {};
|
|
495
707
|
for (const m of models) (available[m.provider] ??= []).push(m.id);
|
|
496
708
|
const selected: Record<string, string[]> = {};
|
|
@@ -501,8 +713,120 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
501
713
|
if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
|
|
502
714
|
const liveCount = getProviderLiveModelCount(name);
|
|
503
715
|
if (liveCount !== undefined) liveModelCounts[name] = liveCount;
|
|
716
|
+
const configured = prov.models;
|
|
717
|
+
if (!Array.isArray(configured) || configured.length === 0) continue;
|
|
718
|
+
const bucket = available[name] ?? (available[name] = []);
|
|
719
|
+
const seen = new Set(bucket);
|
|
720
|
+
for (const id of configured) {
|
|
721
|
+
if (typeof id !== "string" || !id || seen.has(id)) continue;
|
|
722
|
+
seen.add(id);
|
|
723
|
+
bucket.push(id);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Re-check after the local gather: preferCached may have kicked a background flight whose
|
|
727
|
+
// first probe already filled some caches, but `refreshing` still means "more work in flight
|
|
728
|
+
// or still needed" for the GUI re-poll loop.
|
|
729
|
+
const { catalogGatherNeedsLiveRefresh } = await import("../../codex/catalog");
|
|
730
|
+
const refreshing = catalogGatherNeedsLiveRefresh(config);
|
|
731
|
+
return jsonResponse({ selected, available, liveModelCounts, refreshing });
|
|
732
|
+
}
|
|
733
|
+
if (url.pathname === "/api/model-presets" && req.method === "GET") {
|
|
734
|
+
// Preview without applying: rules evaluated against the CURRENT catalog, so the count the
|
|
735
|
+
// user sees is the count they would get.
|
|
736
|
+
const models = await fetchAllModels(config);
|
|
737
|
+
const byProvider = new Map<string, string[]>();
|
|
738
|
+
for (const m of models) {
|
|
739
|
+
const ids = byProvider.get(m.provider) ?? [];
|
|
740
|
+
ids.push(m.id);
|
|
741
|
+
byProvider.set(m.provider, ids);
|
|
742
|
+
}
|
|
743
|
+
const providers: Record<string, unknown> = {};
|
|
744
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
745
|
+
const preset = modelPresetFor(name);
|
|
746
|
+
if (!preset) continue;
|
|
747
|
+
const catalogIds = byProvider.get(name) ?? [];
|
|
748
|
+
const presetIds = materializeModelPreset(name, catalogIds);
|
|
749
|
+
providers[name] = {
|
|
750
|
+
mode: prov.modelPreset?.mode ?? "all",
|
|
751
|
+
...(prov.modelPreset?.appliedVersion !== undefined
|
|
752
|
+
? { appliedVersion: prov.modelPreset.appliedVersion }
|
|
753
|
+
: {}),
|
|
754
|
+
availableVersion: preset.version,
|
|
755
|
+
presetIds,
|
|
756
|
+
presetCount: presetIds.length,
|
|
757
|
+
totalCount: catalogIds.length,
|
|
758
|
+
...(prov.modelPreset?.fallback ? { fallback: prov.modelPreset.fallback } : {}),
|
|
759
|
+
};
|
|
504
760
|
}
|
|
505
|
-
return jsonResponse({
|
|
761
|
+
return jsonResponse({ providers });
|
|
762
|
+
}
|
|
763
|
+
if (url.pathname === "/api/model-presets" && req.method === "PUT") {
|
|
764
|
+
let body: { provider?: unknown; mode?: unknown };
|
|
765
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
766
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
767
|
+
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
768
|
+
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|
|
769
|
+
}
|
|
770
|
+
const mode = body.mode;
|
|
771
|
+
if (mode !== "preset" && mode !== "all" && mode !== "custom") {
|
|
772
|
+
return jsonResponse({ error: "mode must be preset, all, or custom" }, 400);
|
|
773
|
+
}
|
|
774
|
+
const target = config.providers[provider];
|
|
775
|
+
if (mode === "all") {
|
|
776
|
+
// Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
|
|
777
|
+
delete target.selectedModels;
|
|
778
|
+
delete target.modelPreset;
|
|
779
|
+
persistConfig(config);
|
|
780
|
+
return jsonResponse({ ok: true, provider, mode, selected: [], catalogRefresh: await convergeCodexCatalog() });
|
|
781
|
+
}
|
|
782
|
+
if (mode === "custom") {
|
|
783
|
+
// Keep whatever is selected; only the marker changes, so a user can pin their edits
|
|
784
|
+
// without the proxy re-materializing over them.
|
|
785
|
+
target.modelPreset = { ...(target.modelPreset ?? {}), mode: "custom" };
|
|
786
|
+
persistConfig(config);
|
|
787
|
+
return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
|
|
788
|
+
}
|
|
789
|
+
if (!hasModelPreset(provider)) {
|
|
790
|
+
return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
|
|
791
|
+
}
|
|
792
|
+
const models = await fetchAllModels(config);
|
|
793
|
+
const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
|
|
794
|
+
const presetIds = materializeModelPreset(provider, catalogIds);
|
|
795
|
+
const preset = modelPresetFor(provider)!;
|
|
796
|
+
if (presetIds.length === 0) {
|
|
797
|
+
// NEVER write an empty allowlist from a preset: empty means ALL, so it would silently
|
|
798
|
+
// un-curate instead of curating. Keep the previous selection and record the fallback so
|
|
799
|
+
// the next convergence can retry.
|
|
800
|
+
target.modelPreset = {
|
|
801
|
+
mode: "all",
|
|
802
|
+
appliedVersion: preset.version,
|
|
803
|
+
appliedAt: new Date().toISOString(),
|
|
804
|
+
fallback: "preset-empty",
|
|
805
|
+
};
|
|
806
|
+
persistConfig(config);
|
|
807
|
+
return jsonResponse({
|
|
808
|
+
ok: true,
|
|
809
|
+
provider,
|
|
810
|
+
mode: "all",
|
|
811
|
+
fallback: "preset-empty",
|
|
812
|
+
selected: [...(target.selectedModels ?? [])],
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
target.selectedModels = presetIds;
|
|
816
|
+
target.modelPreset = {
|
|
817
|
+
mode: "preset",
|
|
818
|
+
appliedVersion: preset.version,
|
|
819
|
+
appliedAt: new Date().toISOString(),
|
|
820
|
+
};
|
|
821
|
+
persistConfig(config);
|
|
822
|
+
return jsonResponse({
|
|
823
|
+
ok: true,
|
|
824
|
+
provider,
|
|
825
|
+
mode: "preset",
|
|
826
|
+
appliedVersion: preset.version,
|
|
827
|
+
selected: presetIds,
|
|
828
|
+
catalogRefresh: await convergeCodexCatalog(),
|
|
829
|
+
});
|
|
506
830
|
}
|
|
507
831
|
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
508
832
|
let body: { provider?: unknown; models?: unknown };
|
|
@@ -517,9 +841,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
517
841
|
// Empty list clears the allowlist (provider reverts to exposing all models).
|
|
518
842
|
if (models.length > 0) config.providers[provider].selectedModels = models;
|
|
519
843
|
else delete config.providers[provider].selectedModels;
|
|
844
|
+
// Divergence is detected at the WRITE path, not by diffing (#2465): a user edit while the
|
|
845
|
+
// provider is in preset mode makes the selection theirs, and the proxy must never
|
|
846
|
+
// re-materialize over it afterwards.
|
|
847
|
+
markModelPresetDiverged(config.providers[provider]);
|
|
520
848
|
persistConfig(config);
|
|
521
|
-
await
|
|
522
|
-
return jsonResponse({ ok: true, provider, selected: models });
|
|
849
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
850
|
+
return jsonResponse({ ok: true, provider, selected: models, catalogRefresh });
|
|
523
851
|
}
|
|
524
852
|
return null;
|
|
525
853
|
}
|