@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -2,15 +2,23 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import type { CatalogModel } from "../../codex/catalog";
|
|
4
4
|
import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
|
+
import { clearGatherRoutedModelsInflight, fetchProviderModels } from "../../codex/catalog/provider-fetch";
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_SUBAGENT_MODELS,
|
|
8
|
+
adoptPersistedProviderIntoLiveConfig,
|
|
7
9
|
codexAutoStartEnabled,
|
|
8
10
|
hasOwnProvider,
|
|
9
11
|
isValidProviderName,
|
|
10
12
|
multiAgentGuidanceEnabled,
|
|
13
|
+
nonBlankStringArrayConfigError,
|
|
14
|
+
normalizeNonBlankStringArray,
|
|
11
15
|
providerBaseUrlConfigError,
|
|
12
16
|
providerHeadersConfigError,
|
|
17
|
+
requestPacingConfigError,
|
|
18
|
+
readConfigAdmissionSnapshot,
|
|
13
19
|
saveConfigPreservingClaudeCode,
|
|
20
|
+
upstreamHttpVersionConfigError,
|
|
21
|
+
withConfigMutationLockSync,
|
|
14
22
|
} from "../../config";
|
|
15
23
|
import {
|
|
16
24
|
clearLoginState,
|
|
@@ -21,13 +29,15 @@ import {
|
|
|
21
29
|
submitManualLoginCode,
|
|
22
30
|
upsertOAuthProvider,
|
|
23
31
|
} from "../../oauth";
|
|
24
|
-
import {
|
|
32
|
+
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
25
33
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
26
34
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
27
|
-
import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
|
|
35
|
+
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
36
|
+
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
37
|
+
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
28
38
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
29
|
-
import { deriveProviderPresets } from "../../providers/derive";
|
|
30
|
-
import { providerCodexAccountMode } from "../../providers/registry";
|
|
39
|
+
import { deriveProviderPresets, enrichProviderFromRegistry } from "../../providers/derive";
|
|
40
|
+
import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
31
41
|
import {
|
|
32
42
|
extractModelEnvelopeRows,
|
|
33
43
|
extractProviderModelItems,
|
|
@@ -35,12 +45,19 @@ import {
|
|
|
35
45
|
resolveProviderModelDiscovery,
|
|
36
46
|
} from "../../providers/model-discovery";
|
|
37
47
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
38
|
-
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
48
|
+
import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
49
|
+
import { clearKeyCooldowns } from "../../providers/key-failover";
|
|
50
|
+
import { providerRequestPacingStatus } from "../../providers/request-pacing";
|
|
39
51
|
import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
40
52
|
import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
|
|
41
53
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
42
54
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
43
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
clearModelCache,
|
|
57
|
+
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
58
|
+
getProviderDiscoveryStatus,
|
|
59
|
+
getProviderLiveModelCount,
|
|
60
|
+
} from "../../codex/model-cache";
|
|
44
61
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
45
62
|
import { resolveCodexHomeDir } from "../../codex/home";
|
|
46
63
|
import { readUsageEntries } from "../../usage/log";
|
|
@@ -63,36 +80,420 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
|
|
|
63
80
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
64
81
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
65
82
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
83
|
+
import { providerServiceTierConfigError } from "./provider-capability-config";
|
|
66
84
|
import { applySystemEnvToggle } from "../system-env";
|
|
85
|
+
import {
|
|
86
|
+
LOCAL_PROVIDER_RELOAD_NAME_HEADER,
|
|
87
|
+
LOCAL_PROVIDER_RELOAD_PATH,
|
|
88
|
+
} from "../../lib/local-provider-reload-contract";
|
|
89
|
+
import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
|
|
90
|
+
import {
|
|
91
|
+
XAI_RESPONSES_OPT_IN_MODELS,
|
|
92
|
+
xaiResponsesOptInState,
|
|
93
|
+
} from "../../providers/xai-responses-opt-in";
|
|
67
94
|
|
|
68
95
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
69
96
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
70
97
|
import type { ManagementContext } from "./context";
|
|
71
98
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
72
99
|
|
|
100
|
+
type ProviderPatchApplication =
|
|
101
|
+
| { error: string }
|
|
102
|
+
| {
|
|
103
|
+
next: OcxProviderConfig;
|
|
104
|
+
touched: boolean;
|
|
105
|
+
editorTouched: boolean;
|
|
106
|
+
enablingOpenAi: boolean;
|
|
107
|
+
headersTouched: boolean;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
|
|
112
|
+
* for validation and again inside the config mutation lock against the newest provider,
|
|
113
|
+
* so a concurrent PATCH cannot be erased by a save of a stale snapshot. Only synchronous
|
|
114
|
+
* checks live here; the async destination probe stays in the route.
|
|
115
|
+
*/
|
|
116
|
+
function applyProviderPatchFields(
|
|
117
|
+
name: string,
|
|
118
|
+
provider: OcxProviderConfig,
|
|
119
|
+
rawBody: Record<string, unknown>,
|
|
120
|
+
keys: string[],
|
|
121
|
+
config: OcxConfig,
|
|
122
|
+
): ProviderPatchApplication {
|
|
123
|
+
const next: OcxProviderConfig = { ...provider };
|
|
124
|
+
let touched = false;
|
|
125
|
+
let headersTouched = false;
|
|
126
|
+
|
|
127
|
+
if (Object.hasOwn(rawBody, "disabled")) {
|
|
128
|
+
if (typeof rawBody.disabled !== "boolean") return { error: "disabled must be a boolean" };
|
|
129
|
+
if (rawBody.disabled && name === config.defaultProvider) {
|
|
130
|
+
return { error: "cannot disable the default provider; set another default first" };
|
|
131
|
+
}
|
|
132
|
+
next.disabled = rawBody.disabled;
|
|
133
|
+
touched = true;
|
|
134
|
+
}
|
|
135
|
+
if (Object.hasOwn(rawBody, "adapter")) {
|
|
136
|
+
if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return { error: "adapter must be a non-empty string" };
|
|
137
|
+
next.adapter = rawBody.adapter.trim();
|
|
138
|
+
touched = true;
|
|
139
|
+
}
|
|
140
|
+
if (Object.hasOwn(rawBody, "baseUrl")) {
|
|
141
|
+
if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return { error: "baseUrl must be a non-empty string" };
|
|
142
|
+
next.baseUrl = rawBody.baseUrl.trim();
|
|
143
|
+
touched = true;
|
|
144
|
+
}
|
|
145
|
+
if (Object.hasOwn(rawBody, "defaultModel")) {
|
|
146
|
+
if (typeof rawBody.defaultModel !== "string") return { error: "defaultModel must be a string" };
|
|
147
|
+
const dm = rawBody.defaultModel.trim();
|
|
148
|
+
if (dm) next.defaultModel = dm;
|
|
149
|
+
else delete next.defaultModel;
|
|
150
|
+
touched = true;
|
|
151
|
+
}
|
|
152
|
+
if (Object.hasOwn(rawBody, "authMode")) {
|
|
153
|
+
if (typeof rawBody.authMode !== "string") return { error: "authMode must be a string" };
|
|
154
|
+
const mode = rawBody.authMode.trim();
|
|
155
|
+
if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
|
|
156
|
+
next.authMode = mode;
|
|
157
|
+
touched = true;
|
|
158
|
+
} else if (mode === "") {
|
|
159
|
+
delete next.authMode;
|
|
160
|
+
touched = true;
|
|
161
|
+
} else {
|
|
162
|
+
return { error: "authMode must be key, forward, oauth, or local" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (Object.hasOwn(rawBody, "apiKeyTransport")) {
|
|
166
|
+
const transport = rawBody.apiKeyTransport;
|
|
167
|
+
if (transport === "x-api-key" || transport === "bearer") {
|
|
168
|
+
next.apiKeyTransport = transport;
|
|
169
|
+
touched = true;
|
|
170
|
+
} else if (transport === "") {
|
|
171
|
+
delete next.apiKeyTransport;
|
|
172
|
+
touched = true;
|
|
173
|
+
} else {
|
|
174
|
+
return { error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (Object.hasOwn(rawBody, "note")) {
|
|
178
|
+
if (typeof rawBody.note !== "string") return { error: "note must be a string" };
|
|
179
|
+
const note = rawBody.note.trim();
|
|
180
|
+
if (note) next.note = note;
|
|
181
|
+
else delete next.note;
|
|
182
|
+
touched = true;
|
|
183
|
+
}
|
|
184
|
+
if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
|
|
185
|
+
if (typeof rawBody.allowPrivateNetwork !== "boolean") return { error: "allowPrivateNetwork must be a boolean" };
|
|
186
|
+
next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
|
|
187
|
+
touched = true;
|
|
188
|
+
}
|
|
189
|
+
if (Object.hasOwn(rawBody, "liveModels")) {
|
|
190
|
+
if (typeof rawBody.liveModels !== "boolean") return { error: "liveModels must be a boolean" };
|
|
191
|
+
next.liveModels = rawBody.liveModels;
|
|
192
|
+
touched = true;
|
|
193
|
+
}
|
|
194
|
+
if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
|
|
195
|
+
if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
|
|
196
|
+
if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
|
|
197
|
+
return { error: "xaiResponsesOptIn must be a boolean" };
|
|
198
|
+
}
|
|
199
|
+
const modelAdapters = { ...(next.modelAdapters ?? {}) };
|
|
200
|
+
for (const model of XAI_RESPONSES_OPT_IN_MODELS) {
|
|
201
|
+
if (rawBody.xaiResponsesOptIn) modelAdapters[model] = "openai-responses";
|
|
202
|
+
else delete modelAdapters[model];
|
|
203
|
+
}
|
|
204
|
+
if (Object.keys(modelAdapters).length > 0) next.modelAdapters = modelAdapters;
|
|
205
|
+
else delete next.modelAdapters;
|
|
206
|
+
touched = true;
|
|
207
|
+
}
|
|
208
|
+
if (Object.hasOwn(rawBody, "requestPacing")) {
|
|
209
|
+
const value = rawBody.requestPacing;
|
|
210
|
+
if (value === null) {
|
|
211
|
+
delete next.requestPacing;
|
|
212
|
+
} else {
|
|
213
|
+
if (!isPlainRecord(value)) return { error: "requestPacing must be a plain object or null" };
|
|
214
|
+
const pacingError = requestPacingConfigError(value);
|
|
215
|
+
if (pacingError) return { error: pacingError };
|
|
216
|
+
// `requestPacingConfigError` is the runtime narrowing boundary above; keep the
|
|
217
|
+
// assertion explicit because a generic plain record cannot express `enabled`.
|
|
218
|
+
next.requestPacing = structuredClone(value) as unknown as OcxProviderConfig["requestPacing"];
|
|
219
|
+
}
|
|
220
|
+
touched = true;
|
|
221
|
+
}
|
|
222
|
+
if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
|
|
223
|
+
const value = rawBody.upstreamHttpVersion;
|
|
224
|
+
if (value === null || value === "") {
|
|
225
|
+
delete next.upstreamHttpVersion;
|
|
226
|
+
} else {
|
|
227
|
+
const versionError = upstreamHttpVersionConfigError(value);
|
|
228
|
+
if (versionError) return { error: versionError };
|
|
229
|
+
// `upstreamHttpVersionConfigError` is the shared write boundary; the assertion is
|
|
230
|
+
// explicit because the incoming value is an unknown JSON scalar.
|
|
231
|
+
next.upstreamHttpVersion = value as OcxProviderConfig["upstreamHttpVersion"];
|
|
232
|
+
}
|
|
233
|
+
touched = true;
|
|
234
|
+
}
|
|
235
|
+
// The Models page edits the catalog hints in place; keep them on the existing
|
|
236
|
+
// provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
|
|
237
|
+
if (Object.hasOwn(rawBody, "contextWindow")) {
|
|
238
|
+
const value = rawBody.contextWindow;
|
|
239
|
+
if (value === null) {
|
|
240
|
+
delete next.contextWindow;
|
|
241
|
+
// `Number.isInteger(1e100)` is true, so an integer check alone admits a value that
|
|
242
|
+
// serializes into the catalog as an enormous number and can make Codex reject the whole
|
|
243
|
+
// file. Safe-integer is the real bound for something that ends up in a JSON int field.
|
|
244
|
+
} else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
|
|
245
|
+
next.contextWindow = value;
|
|
246
|
+
} else {
|
|
247
|
+
return { error: "contextWindow must be a positive safe integer or null" };
|
|
248
|
+
}
|
|
249
|
+
touched = true;
|
|
250
|
+
}
|
|
251
|
+
if (Object.hasOwn(rawBody, "modelContextWindows")) {
|
|
252
|
+
const value = rawBody.modelContextWindows;
|
|
253
|
+
if (value === null) {
|
|
254
|
+
delete next.modelContextWindows;
|
|
255
|
+
} else {
|
|
256
|
+
if (!isPlainRecord(value)) return { error: "modelContextWindows must be a plain object or null" };
|
|
257
|
+
const windows: Record<string, number> = { ...(next.modelContextWindows ?? {}) };
|
|
258
|
+
for (const [model, window] of Object.entries(value)) {
|
|
259
|
+
if (!model.trim()) return { error: "modelContextWindows keys must be nonblank model ids" };
|
|
260
|
+
if (window === null) {
|
|
261
|
+
delete windows[model];
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0) {
|
|
265
|
+
return { error: "modelContextWindows values must be positive safe integers or null" };
|
|
266
|
+
}
|
|
267
|
+
windows[model] = window;
|
|
268
|
+
}
|
|
269
|
+
if (Object.keys(windows).length > 0) next.modelContextWindows = windows;
|
|
270
|
+
else delete next.modelContextWindows;
|
|
271
|
+
}
|
|
272
|
+
touched = true;
|
|
273
|
+
}
|
|
274
|
+
if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
|
|
275
|
+
const value = rawBody.modelSupportsServiceTier;
|
|
276
|
+
if (value === null) {
|
|
277
|
+
delete next.modelSupportsServiceTier;
|
|
278
|
+
} else {
|
|
279
|
+
if (!isPlainRecord(value)) return { error: "modelSupportsServiceTier must be a plain object or null" };
|
|
280
|
+
const capabilities: Record<string, boolean> = { ...(next.modelSupportsServiceTier ?? {}) };
|
|
281
|
+
for (const [model, supported] of Object.entries(value)) {
|
|
282
|
+
if (!model.trim()) return { error: "modelSupportsServiceTier keys must be nonblank model ids" };
|
|
283
|
+
if (supported === null) {
|
|
284
|
+
delete capabilities[model];
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (typeof supported !== "boolean") {
|
|
288
|
+
return { error: "modelSupportsServiceTier values must be booleans or null" };
|
|
289
|
+
}
|
|
290
|
+
capabilities[model] = supported;
|
|
291
|
+
}
|
|
292
|
+
if (Object.keys(capabilities).length > 0) next.modelSupportsServiceTier = capabilities;
|
|
293
|
+
else delete next.modelSupportsServiceTier;
|
|
294
|
+
}
|
|
295
|
+
touched = true;
|
|
296
|
+
}
|
|
297
|
+
if (Object.hasOwn(rawBody, "noStructuredOutputModels")) {
|
|
298
|
+
const value = rawBody.noStructuredOutputModels;
|
|
299
|
+
if (value === null) {
|
|
300
|
+
delete next.noStructuredOutputModels;
|
|
301
|
+
} else {
|
|
302
|
+
const error = nonBlankStringArrayConfigError(value, "noStructuredOutputModels");
|
|
303
|
+
if (error) return { error };
|
|
304
|
+
const models = normalizeNonBlankStringArray(value as string[]);
|
|
305
|
+
if (models.length > 0) next.noStructuredOutputModels = models;
|
|
306
|
+
else delete next.noStructuredOutputModels;
|
|
307
|
+
}
|
|
308
|
+
touched = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// headers is the one object-valued field in the mask. PATCH semantics merge it
|
|
312
|
+
// shallowly into the existing block so a single fingerprint header can be added
|
|
313
|
+
// without wiping the rest; null or an empty object clears user-managed headers.
|
|
314
|
+
if (Object.hasOwn(rawBody, "headers")) {
|
|
315
|
+
const headersValue = rawBody.headers;
|
|
316
|
+
if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
|
|
317
|
+
// Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
|
|
318
|
+
// is not user-managed: restoring it keeps the upstream transport intact after a
|
|
319
|
+
// clear instead of deleting the whole block.
|
|
320
|
+
const entry = getProviderRegistryEntry(name);
|
|
321
|
+
if (entry?.staticHeaders && providerMatchesRegistryTransport(name, next)) {
|
|
322
|
+
next.headers = { ...entry.staticHeaders };
|
|
323
|
+
} else {
|
|
324
|
+
delete next.headers;
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
if (!isPlainRecord(headersValue)) return { error: "headers must be an object" };
|
|
328
|
+
const headersError = providerHeadersConfigError(headersValue);
|
|
329
|
+
if (headersError) return { error: headersError };
|
|
330
|
+
// Header names are case-insensitive on the wire. Drop any existing key whose
|
|
331
|
+
// lowercase name collides with an incoming one, or Headers normalization would
|
|
332
|
+
// send a combined "x-custom: v1, v2" value upstream.
|
|
333
|
+
const incoming = new Map(
|
|
334
|
+
Object.entries(headersValue as Record<string, string>).map(([key, value]) => [key.toLowerCase(), [key, value] as const]),
|
|
335
|
+
);
|
|
336
|
+
const merged: Record<string, string> = {};
|
|
337
|
+
for (const [key, value] of Object.entries(next.headers ?? {})) {
|
|
338
|
+
if (!incoming.has(key.toLowerCase())) merged[key] = value;
|
|
339
|
+
}
|
|
340
|
+
for (const [key, value] of incoming.values()) merged[key] = value;
|
|
341
|
+
next.headers = merged;
|
|
342
|
+
}
|
|
343
|
+
touched = true;
|
|
344
|
+
headersTouched = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!touched) return { error: "no recognized fields to update" };
|
|
348
|
+
|
|
349
|
+
// A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
|
|
350
|
+
// routing eligibility, not the provider shape. Re-enabling `openai` is different — a
|
|
351
|
+
// malformed disabled row must not come back online unchanged, so canonicalize/reject
|
|
352
|
+
// against the same built-in gate used by mode PATCH and POST.
|
|
353
|
+
const editorTouched = keys.some(key => key !== "disabled");
|
|
354
|
+
const enablingOpenAi = name === "openai"
|
|
355
|
+
&& Object.hasOwn(rawBody, "disabled")
|
|
356
|
+
&& rawBody.disabled === false
|
|
357
|
+
&& provider.disabled === true;
|
|
358
|
+
if (!editorTouched && enablingOpenAi) {
|
|
359
|
+
if (!isCanonicalOpenAiForwardProvider(next)) {
|
|
360
|
+
return { error: "provider openai must be the canonical built-in provider" };
|
|
361
|
+
}
|
|
362
|
+
// Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
|
|
363
|
+
// accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
|
|
364
|
+
next.baseUrl = CODEX_FORWARD_BASE_URL;
|
|
365
|
+
// Fill missing mode so a disabled canonical row becomes a complete live openai entry.
|
|
366
|
+
if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
|
|
367
|
+
next.codexAccountMode = "pool";
|
|
368
|
+
}
|
|
369
|
+
if (next.disabled === false) delete next.disabled;
|
|
370
|
+
// Canonical openai never uses private-network opt-in; drop a stale flag that
|
|
371
|
+
// was ignored for the DNS probe so it cannot linger on the live row.
|
|
372
|
+
delete next.allowPrivateNetwork;
|
|
373
|
+
}
|
|
374
|
+
return { next, touched, editorTouched, enablingOpenAi, headersTouched };
|
|
375
|
+
}
|
|
376
|
+
|
|
73
377
|
export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
74
|
-
const { req, url, config, deps,
|
|
378
|
+
const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
75
379
|
|
|
76
380
|
if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
|
|
77
381
|
const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
|
|
78
382
|
return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
|
|
79
383
|
}
|
|
80
384
|
|
|
385
|
+
if (url.pathname === "/api/provider-request-pacing" && req.method === "GET") {
|
|
386
|
+
const name = url.searchParams.get("name")?.trim();
|
|
387
|
+
if (name) {
|
|
388
|
+
if (!isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
|
|
389
|
+
return jsonResponse({ error: "unknown provider" }, 404);
|
|
390
|
+
}
|
|
391
|
+
return jsonResponse(providerRequestPacingStatus(name, config.providers[name]!));
|
|
392
|
+
}
|
|
393
|
+
return jsonResponse(Object.fromEntries(
|
|
394
|
+
Object.entries(config.providers).map(([providerName, provider]) => [
|
|
395
|
+
providerName,
|
|
396
|
+
providerRequestPacingStatus(providerName, provider),
|
|
397
|
+
]),
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
|
|
81
401
|
if (url.pathname === "/api/providers" && req.method === "GET") {
|
|
82
402
|
return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
|
|
83
403
|
name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
|
|
84
404
|
hasApiKey: !!p.apiKey,
|
|
405
|
+
// Presence only (#959 review): header names and values never leave the process.
|
|
406
|
+
hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
|
|
85
407
|
allowPrivateNetwork: p.allowPrivateNetwork === true,
|
|
86
408
|
liveModels: p.liveModels !== false,
|
|
409
|
+
requestPacing: p.requestPacing,
|
|
87
410
|
models: p.models ?? [],
|
|
411
|
+
contextWindow: p.contextWindow,
|
|
412
|
+
modelContextWindows: p.modelContextWindows,
|
|
413
|
+
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
414
|
+
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
415
|
+
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
88
416
|
authMode: p.authMode,
|
|
89
417
|
apiKeyTransport: p.apiKeyTransport,
|
|
90
418
|
disabled: p.disabled === true,
|
|
91
419
|
codexAccountMode: providerCodexAccountMode(name, p),
|
|
420
|
+
...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
|
|
92
421
|
discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
|
|
93
422
|
})));
|
|
94
423
|
}
|
|
95
424
|
|
|
425
|
+
if (url.pathname === LOCAL_PROVIDER_RELOAD_PATH && req.method === "POST") {
|
|
426
|
+
if (principal !== "local-provider-reload-capability") {
|
|
427
|
+
return jsonResponse({ error: "provider reload capability required" }, 403);
|
|
428
|
+
}
|
|
429
|
+
const name = req.headers.get(LOCAL_PROVIDER_RELOAD_NAME_HEADER) ?? "";
|
|
430
|
+
if (!isValidProviderName(name)) return jsonResponse({ error: "invalid provider reload target" }, 400);
|
|
431
|
+
|
|
432
|
+
const admitted = readConfigAdmissionSnapshot();
|
|
433
|
+
if (
|
|
434
|
+
admitted.kind !== "read"
|
|
435
|
+
|| admitted.diagnostics.source !== "file"
|
|
436
|
+
|| admitted.diagnostics.error !== null
|
|
437
|
+
) {
|
|
438
|
+
return jsonResponse({ error: "provider reload source unavailable" }, 409);
|
|
439
|
+
}
|
|
440
|
+
const diskConfig = admitted.diagnostics.config;
|
|
441
|
+
if (!hasOwnProvider(diskConfig.providers, name)) {
|
|
442
|
+
return jsonResponse({ error: "provider reload target unavailable" }, 404);
|
|
443
|
+
}
|
|
444
|
+
const provider = diskConfig.providers[name]!;
|
|
445
|
+
const providerError = providerManagementConfigError(name, provider);
|
|
446
|
+
if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
|
|
447
|
+
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
448
|
+
diskConfig.codexAccountNamespaces,
|
|
449
|
+
name,
|
|
450
|
+
);
|
|
451
|
+
if (namespaceCollision) return jsonResponse({ error: "provider reload target conflicts with routing" }, 409);
|
|
452
|
+
const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(provider);
|
|
453
|
+
const resolvedError = await providerDestinationResolvedError(name, provider, { allowBenchmarkAddresses });
|
|
454
|
+
if (resolvedError) return jsonResponse({ error: "provider reload target rejected" }, 409);
|
|
455
|
+
|
|
456
|
+
// Destination validation awaits DNS. A cooperating writer holds the same SQLite
|
|
457
|
+
// mutation lock, so the final exact-byte check and live adoption happen as one
|
|
458
|
+
// synchronous authority decision. The route does not save or reserialize disk.
|
|
459
|
+
let currentDiskConfig: OcxConfig | null = null;
|
|
460
|
+
let sourceChanged = false;
|
|
461
|
+
withConfigMutationLockSync(() => {
|
|
462
|
+
const current = readConfigAdmissionSnapshot();
|
|
463
|
+
if (
|
|
464
|
+
current.kind !== "read"
|
|
465
|
+
|| current.diagnostics.source !== "file"
|
|
466
|
+
|| current.diagnostics.error !== null
|
|
467
|
+
|| current.contentSha256 !== admitted.contentSha256
|
|
468
|
+
) {
|
|
469
|
+
sourceChanged = true;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
currentDiskConfig = current.diagnostics.config;
|
|
473
|
+
adoptPersistedProviderIntoLiveConfig(
|
|
474
|
+
config,
|
|
475
|
+
name,
|
|
476
|
+
current.diagnostics.config.providers[name]!,
|
|
477
|
+
current.diagnostics.config,
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
if (sourceChanged || currentDiskConfig === null) {
|
|
481
|
+
return jsonResponse({ error: "provider reload source changed" }, 409);
|
|
482
|
+
}
|
|
483
|
+
reconcileLiveStateStores();
|
|
484
|
+
// The complete disk snapshot owns display overlays, including providers that this
|
|
485
|
+
// live routing instance deliberately does not adopt.
|
|
486
|
+
refreshUserCostOverlays(currentDiskConfig);
|
|
487
|
+
clearGatherRoutedModelsInflight();
|
|
488
|
+
(deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
|
|
489
|
+
clearAccountQuotaCache(name);
|
|
490
|
+
clearKeyCooldowns(name);
|
|
491
|
+
clearModelCache(name);
|
|
492
|
+
if (name === "openai") (deps.clearThreadAccountMap ?? clearThreadAccountMap)();
|
|
493
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
494
|
+
return jsonResponse({ success: true, name, catalogRefresh });
|
|
495
|
+
}
|
|
496
|
+
|
|
96
497
|
// Add (or overwrite) a single provider. Merges into the live in-memory config and
|
|
97
498
|
// persists — existing providers' real keys are never round-tripped (unlike PUT /api/config,
|
|
98
499
|
// which would re-save the masked keys from GET). Live routing picks it up immediately.
|
|
@@ -102,7 +503,13 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
102
503
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
103
504
|
const providerError = providerManagementConfigError(name, body.provider);
|
|
104
505
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
506
|
+
const serviceTierError = providerServiceTierConfigError(name, body.provider);
|
|
507
|
+
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
105
508
|
const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
|
|
509
|
+
// PATCH already clears on null; POST persisted the body as submitted, so a `null` here
|
|
510
|
+
// reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
|
|
511
|
+
// "clear" means everywhere else.
|
|
512
|
+
if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
|
|
106
513
|
if (!name || !prov?.adapter || !prov?.baseUrl) {
|
|
107
514
|
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
|
|
108
515
|
}
|
|
@@ -128,12 +535,44 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
128
535
|
}
|
|
129
536
|
// Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
|
|
130
537
|
// doesn't send — merge it in so the sidecars are gated correctly.
|
|
538
|
+
// Sample request ownership BEFORE enrichment. Enrichment fills absent fields from the
|
|
539
|
+
// registry seed, after which "the client omitted this" and "the registry supplied it" are
|
|
540
|
+
// indistinguishable — so a carry-over guard written as `prov.x === undefined` after this
|
|
541
|
+
// call can never fire.
|
|
542
|
+
const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
|
|
543
|
+
const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
|
|
544
|
+
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
131
545
|
enrichProviderFromCatalog(name, prov);
|
|
132
546
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
133
547
|
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
134
548
|
// let the (possibly new) apiKey join the pool as the active entry.
|
|
135
549
|
const existingPool = config.providers[name]?.apiKeyPool;
|
|
136
550
|
if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
|
|
551
|
+
// The same rule applies to user-configured price overlays: the dashboard's
|
|
552
|
+
// add/edit form does not send modelCosts, so an overwrite must not silently
|
|
553
|
+
// erase hand-edited per-model prices from Logs/Usage estimates.
|
|
554
|
+
const existingCosts = config.providers[name]?.modelCosts;
|
|
555
|
+
if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
|
|
556
|
+
// ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
|
|
557
|
+
// has no member for either field, so the add/edit form structurally cannot send them:
|
|
558
|
+
// absence in the request means "not carried", never "the user deleted it". Deletion goes
|
|
559
|
+
// through PATCH with an explicit null (#1409).
|
|
560
|
+
const existing = config.providers[name];
|
|
561
|
+
if (!submittedRequestPacing && existing?.requestPacing) {
|
|
562
|
+
prov.requestPacing = structuredClone(existing.requestPacing);
|
|
563
|
+
}
|
|
564
|
+
if (!submittedContextWindow && existing?.contextWindow !== undefined) {
|
|
565
|
+
prov.contextWindow = existing.contextWindow;
|
|
566
|
+
}
|
|
567
|
+
if (existing?.modelContextWindows) {
|
|
568
|
+
// When the client did send a map, its keys win and the user's other keys survive. When
|
|
569
|
+
// it did not, the stored value is the user's map alone: merging the registry seed in
|
|
570
|
+
// would persist seed keys into user config as a side effect of an unrelated save, and
|
|
571
|
+
// router.ts already fills registry values beneath user entries at resolve time.
|
|
572
|
+
prov.modelContextWindows = submittedModelContextWindows
|
|
573
|
+
? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
|
|
574
|
+
: { ...existing.modelContextWindows };
|
|
575
|
+
}
|
|
137
576
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
|
|
138
577
|
if (body.setDefault === true) config.defaultProvider = name;
|
|
139
578
|
save(config);
|
|
@@ -144,8 +583,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
144
583
|
}
|
|
145
584
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
146
585
|
clearModelCache(name);
|
|
147
|
-
await
|
|
148
|
-
return jsonResponse({ success: true, name });
|
|
586
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
587
|
+
return jsonResponse({ success: true, name, catalogRefresh });
|
|
149
588
|
}
|
|
150
589
|
|
|
151
590
|
if (url.pathname === "/api/providers" && req.method === "PATCH") {
|
|
@@ -213,106 +652,19 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
213
652
|
if (Object.hasOwn(rawBody, "apiKey")) {
|
|
214
653
|
return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
|
|
215
654
|
}
|
|
216
|
-
const
|
|
217
|
-
|
|
655
|
+
const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
|
|
656
|
+
if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
|
|
657
|
+
const next = applied.next;
|
|
218
658
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (rawBody.disabled && name === config.defaultProvider) {
|
|
222
|
-
return jsonResponse({ error: "cannot disable the default provider; set another default first" }, 400);
|
|
223
|
-
}
|
|
224
|
-
next.disabled = rawBody.disabled;
|
|
225
|
-
touched = true;
|
|
226
|
-
}
|
|
227
|
-
if (Object.hasOwn(rawBody, "adapter")) {
|
|
228
|
-
if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return jsonResponse({ error: "adapter must be a non-empty string" }, 400);
|
|
229
|
-
next.adapter = rawBody.adapter.trim();
|
|
230
|
-
touched = true;
|
|
231
|
-
}
|
|
232
|
-
if (Object.hasOwn(rawBody, "baseUrl")) {
|
|
233
|
-
if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return jsonResponse({ error: "baseUrl must be a non-empty string" }, 400);
|
|
234
|
-
next.baseUrl = rawBody.baseUrl.trim();
|
|
235
|
-
touched = true;
|
|
236
|
-
}
|
|
237
|
-
if (Object.hasOwn(rawBody, "defaultModel")) {
|
|
238
|
-
if (typeof rawBody.defaultModel !== "string") return jsonResponse({ error: "defaultModel must be a string" }, 400);
|
|
239
|
-
const dm = rawBody.defaultModel.trim();
|
|
240
|
-
if (dm) next.defaultModel = dm;
|
|
241
|
-
else delete next.defaultModel;
|
|
242
|
-
touched = true;
|
|
243
|
-
}
|
|
244
|
-
if (Object.hasOwn(rawBody, "authMode")) {
|
|
245
|
-
if (typeof rawBody.authMode !== "string") return jsonResponse({ error: "authMode must be a string" }, 400);
|
|
246
|
-
const mode = rawBody.authMode.trim();
|
|
247
|
-
if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
|
|
248
|
-
next.authMode = mode;
|
|
249
|
-
touched = true;
|
|
250
|
-
} else if (mode === "") {
|
|
251
|
-
delete next.authMode;
|
|
252
|
-
touched = true;
|
|
253
|
-
} else {
|
|
254
|
-
return jsonResponse({ error: "authMode must be key, forward, oauth, or local" }, 400);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (Object.hasOwn(rawBody, "apiKeyTransport")) {
|
|
258
|
-
const transport = rawBody.apiKeyTransport;
|
|
259
|
-
if (transport === "x-api-key" || transport === "bearer") {
|
|
260
|
-
next.apiKeyTransport = transport;
|
|
261
|
-
touched = true;
|
|
262
|
-
} else if (transport === "") {
|
|
263
|
-
delete next.apiKeyTransport;
|
|
264
|
-
touched = true;
|
|
265
|
-
} else {
|
|
266
|
-
return jsonResponse({ error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" }, 400);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (Object.hasOwn(rawBody, "note")) {
|
|
270
|
-
if (typeof rawBody.note !== "string") return jsonResponse({ error: "note must be a string" }, 400);
|
|
271
|
-
const note = rawBody.note.trim();
|
|
272
|
-
if (note) next.note = note;
|
|
273
|
-
else delete next.note;
|
|
274
|
-
touched = true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
|
|
278
|
-
if (typeof rawBody.allowPrivateNetwork !== "boolean") return jsonResponse({ error: "allowPrivateNetwork must be a boolean" }, 400);
|
|
279
|
-
next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
|
|
280
|
-
touched = true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (Object.hasOwn(rawBody, "liveModels")) {
|
|
284
|
-
if (typeof rawBody.liveModels !== "boolean") return jsonResponse({ error: "liveModels must be a boolean" }, 400);
|
|
285
|
-
next.liveModels = rawBody.liveModels;
|
|
286
|
-
touched = true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (!touched) return jsonResponse({ error: "no recognized fields to update" }, 400);
|
|
290
|
-
|
|
291
|
-
// A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
|
|
292
|
-
// routing eligibility, not the provider shape. Re-enabling `openai` is different — a
|
|
293
|
-
// malformed disabled row must not come back online unchanged, so canonicalize/reject
|
|
294
|
-
// against the same built-in gate used by mode PATCH and POST.
|
|
295
|
-
const editorTouched = keys.some(key => key !== "disabled");
|
|
296
|
-
const enablingOpenAi = name === "openai"
|
|
297
|
-
&& Object.hasOwn(rawBody, "disabled")
|
|
298
|
-
&& rawBody.disabled === false
|
|
299
|
-
&& config.providers[name]?.disabled === true;
|
|
300
|
-
if (editorTouched) {
|
|
659
|
+
const pacingOnly = keys.every(key => key === "requestPacing");
|
|
660
|
+
if (applied.editorTouched && !pacingOnly) {
|
|
301
661
|
const providerError = providerManagementConfigError(name, next);
|
|
302
662
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
663
|
+
const serviceTierError = providerServiceTierConfigError(name, next);
|
|
664
|
+
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
303
665
|
const resolvedError = await providerDestinationResolvedError(name, next);
|
|
304
666
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
305
|
-
} else if (enablingOpenAi) {
|
|
306
|
-
if (!isCanonicalOpenAiForwardProvider(next)) {
|
|
307
|
-
return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
|
|
308
|
-
}
|
|
309
|
-
// Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
|
|
310
|
-
// accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
|
|
311
|
-
next.baseUrl = CODEX_FORWARD_BASE_URL;
|
|
312
|
-
// Fill missing mode so a disabled canonical row becomes a complete live openai entry.
|
|
313
|
-
if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
|
|
314
|
-
next.codexAccountMode = "pool";
|
|
315
|
-
}
|
|
667
|
+
} else if (applied.enablingOpenAi) {
|
|
316
668
|
// Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
|
|
317
669
|
// allowPrivateNetwork on this path — it must not bypass the built-in guard.
|
|
318
670
|
const resolvedError = await providerDestinationResolvedError(
|
|
@@ -321,26 +673,180 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
321
673
|
{ allowBenchmarkAddresses: true },
|
|
322
674
|
);
|
|
323
675
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
324
|
-
if (next.disabled === false) delete next.disabled;
|
|
325
|
-
// Canonical openai never uses private-network opt-in; drop a stale flag that
|
|
326
|
-
// was ignored for the DNS probe so it cannot linger on the live row.
|
|
327
|
-
delete next.allowPrivateNetwork;
|
|
328
676
|
}
|
|
329
677
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
678
|
+
// The live config is shared and the destination probe above is awaited. Re-apply the
|
|
679
|
+
// mask onto the newest provider under the mutation lock right before saving, so two
|
|
680
|
+
// concurrent PATCHes updating different fields/headers both survive instead of the
|
|
681
|
+
// later save clobbering the earlier snapshot.
|
|
682
|
+
let replayError: string | undefined;
|
|
683
|
+
withConfigMutationLockSync(() => {
|
|
684
|
+
const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
|
|
685
|
+
if ("error" in replay) {
|
|
686
|
+
replayError = replay.error;
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (replay.editorTouched && !pacingOnly) {
|
|
690
|
+
const syncError = providerManagementConfigError(name, replay.next);
|
|
691
|
+
if (syncError) {
|
|
692
|
+
replayError = syncError;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const serviceTierError = providerServiceTierConfigError(name, replay.next);
|
|
696
|
+
if (serviceTierError) {
|
|
697
|
+
replayError = serviceTierError;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
} else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
|
|
701
|
+
replayError = "provider openai must be the canonical built-in provider";
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// A PATCH that managed headers owns the resulting block: the clear path restores
|
|
705
|
+
// registry static headers, so exact-match stripping must not erase them again.
|
|
706
|
+
config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
|
|
707
|
+
saveConfigPreservingClaudeCode(config);
|
|
708
|
+
});
|
|
709
|
+
if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
|
|
333
710
|
reconcileLiveStateStores();
|
|
334
|
-
if (editorTouched) {
|
|
711
|
+
if (applied.editorTouched && !pacingOnly) {
|
|
335
712
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
336
713
|
clearModelCache(name);
|
|
337
714
|
}
|
|
338
|
-
await
|
|
715
|
+
const catalogRefresh = pacingOnly ? null : await convergeCodexCatalog();
|
|
339
716
|
return jsonResponse({
|
|
340
717
|
success: true,
|
|
341
718
|
name,
|
|
342
719
|
disabled: config.providers[name]!.disabled === true,
|
|
343
720
|
hasApiKey: !!config.providers[name]!.apiKey,
|
|
721
|
+
...(name === "xai"
|
|
722
|
+
? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
|
|
723
|
+
: {}),
|
|
724
|
+
catalogRefresh,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Force-refresh one provider's live model list: drop the per-provider TTL cache (and any
|
|
729
|
+
// in-flight multi-provider gather that could re-serve the old payload), re-fetch, then best-
|
|
730
|
+
// effort rebuild the Codex catalog so the Models page and Codex pickers pick up the new rows.
|
|
731
|
+
if (url.pathname === "/api/providers/refresh-models" && req.method === "POST") {
|
|
732
|
+
const name = url.searchParams.get("name")?.trim();
|
|
733
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
|
|
734
|
+
return jsonResponse({ error: "unknown provider" }, 404);
|
|
735
|
+
}
|
|
736
|
+
const raw = config.providers[name]!;
|
|
737
|
+
if (raw.disabled) {
|
|
738
|
+
return jsonResponse({
|
|
739
|
+
ok: false,
|
|
740
|
+
provider: name,
|
|
741
|
+
models: [],
|
|
742
|
+
count: 0,
|
|
743
|
+
error: "Provider is disabled",
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const prov = { ...raw };
|
|
747
|
+
enrichProviderFromRegistry(name, prov);
|
|
748
|
+
const started = Date.now();
|
|
749
|
+
|
|
750
|
+
if (prov.authMode === "forward") {
|
|
751
|
+
return jsonResponse({
|
|
752
|
+
ok: true,
|
|
753
|
+
provider: name,
|
|
754
|
+
models: [],
|
|
755
|
+
count: 0,
|
|
756
|
+
source: "forward",
|
|
757
|
+
latencyMs: Date.now() - started,
|
|
758
|
+
message: "Passthrough provider has no upstream /models catalog.",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (prov.liveModels === false) {
|
|
763
|
+
const models = [...(prov.models ?? [])];
|
|
764
|
+
if (models.length === 0 && prov.defaultModel) models.push(prov.defaultModel);
|
|
765
|
+
return jsonResponse({
|
|
766
|
+
ok: true,
|
|
767
|
+
provider: name,
|
|
768
|
+
models,
|
|
769
|
+
count: models.length,
|
|
770
|
+
source: "static",
|
|
771
|
+
latencyMs: Date.now() - started,
|
|
772
|
+
message: "Live discovery is off; returning configured/static models only.",
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
clearModelCache(name);
|
|
777
|
+
clearGatherRoutedModelsInflight();
|
|
778
|
+
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
779
|
+
const catalogModels = await fetchProviderModels(
|
|
780
|
+
name,
|
|
781
|
+
prov,
|
|
782
|
+
ttlMs,
|
|
783
|
+
providerContextCap(config, name),
|
|
784
|
+
);
|
|
785
|
+
const models = catalogModels.map(model => model.id);
|
|
786
|
+
const discovery = getProviderDiscoveryStatus(name);
|
|
787
|
+
const liveModelCount = getProviderLiveModelCount(name);
|
|
788
|
+
const latencyMs = Date.now() - started;
|
|
789
|
+
|
|
790
|
+
if (discovery?.status === "failed") {
|
|
791
|
+
const error = discovery.reason === "http"
|
|
792
|
+
? `upstream /models returned ${discovery.httpStatus}`
|
|
793
|
+
: discovery.reason === "blocked"
|
|
794
|
+
? "upstream /models blocked by destination policy"
|
|
795
|
+
: discovery.reason === "invalid_response"
|
|
796
|
+
? "upstream /models returned an unexpected response"
|
|
797
|
+
: discovery.reason === "network"
|
|
798
|
+
? "upstream /models network error"
|
|
799
|
+
: "provider model discovery failed";
|
|
800
|
+
// Do not rewrite providers[name].models from a failed probe — keep the last good seed.
|
|
801
|
+
await convergeCodexCatalog();
|
|
802
|
+
return jsonResponse({
|
|
803
|
+
ok: false,
|
|
804
|
+
provider: name,
|
|
805
|
+
models,
|
|
806
|
+
count: models.length,
|
|
807
|
+
source: "fallback",
|
|
808
|
+
persisted: false,
|
|
809
|
+
discovery,
|
|
810
|
+
...(liveModelCount !== undefined ? { liveModelCount } : {}),
|
|
811
|
+
latencyMs,
|
|
812
|
+
error,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Live success: write the discovered ids into the provider config so the Models tab,
|
|
817
|
+
// Settings default-model picker, and static fallback all see the same list after restart.
|
|
818
|
+
// Only discovery.status === "ok" means upstream actually answered; "configured" without a
|
|
819
|
+
// live status is the no-token / no-fetch path and must not clobber an existing models seed.
|
|
820
|
+
let persisted = false;
|
|
821
|
+
if (discovery?.status === "ok") {
|
|
822
|
+
const existing = config.providers[name]!;
|
|
823
|
+
if (models.length > 0) existing.models = models;
|
|
824
|
+
else delete existing.models;
|
|
825
|
+
// Drop allowlist entries that no longer exist upstream so selectedModels cannot hide
|
|
826
|
+
// every newly discovered row while pointing at dead ids.
|
|
827
|
+
if (Array.isArray(existing.selectedModels) && existing.selectedModels.length > 0) {
|
|
828
|
+
const liveIds = new Set(models);
|
|
829
|
+
const kept = existing.selectedModels.filter(id => liveIds.has(id));
|
|
830
|
+
if (kept.length > 0) existing.selectedModels = kept;
|
|
831
|
+
else delete existing.selectedModels;
|
|
832
|
+
}
|
|
833
|
+
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
834
|
+
save(config);
|
|
835
|
+
reconcileLiveStateStores();
|
|
836
|
+
persisted = true;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
await convergeCodexCatalog();
|
|
840
|
+
return jsonResponse({
|
|
841
|
+
ok: true,
|
|
842
|
+
provider: name,
|
|
843
|
+
models,
|
|
844
|
+
count: models.length,
|
|
845
|
+
source: discovery?.status === "ok" ? "live" : "configured",
|
|
846
|
+
persisted,
|
|
847
|
+
...(discovery ? { discovery } : {}),
|
|
848
|
+
...(liveModelCount !== undefined ? { liveModelCount } : {}),
|
|
849
|
+
latencyMs,
|
|
344
850
|
});
|
|
345
851
|
}
|
|
346
852
|
|
|
@@ -370,19 +876,54 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
370
876
|
// credential resolution/network access for providers such as Antigravity.
|
|
371
877
|
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
372
878
|
}
|
|
373
|
-
const {
|
|
374
|
-
const
|
|
879
|
+
const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
880
|
+
const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
881
|
+
const snapshot = antigravity
|
|
882
|
+
? await getValidAccessTokenSnapshot(name).catch(() => undefined)
|
|
883
|
+
: undefined;
|
|
884
|
+
const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
|
|
375
885
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
376
886
|
return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
|
|
377
887
|
}
|
|
378
|
-
|
|
888
|
+
if (prov.adapter === "cursor") {
|
|
889
|
+
const started = Date.now();
|
|
890
|
+
const live = await fetchCursorUsableModels({
|
|
891
|
+
apiKey: apiKey ?? "",
|
|
892
|
+
baseUrl: prov.baseUrl,
|
|
893
|
+
});
|
|
894
|
+
const latencyMs = Date.now() - started;
|
|
895
|
+
if (!live.ok) {
|
|
896
|
+
return jsonResponse({
|
|
897
|
+
ok: false,
|
|
898
|
+
latencyMs,
|
|
899
|
+
error: `cursor discovery ${live.error}${live.detail ? `: ${live.detail}` : ""}`,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
return jsonResponse({
|
|
903
|
+
ok: true,
|
|
904
|
+
latencyMs,
|
|
905
|
+
models: live.models.length,
|
|
906
|
+
message: `Connected. ${live.models.length} models.`,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
const project = prov.project ?? snapshot?.projectId;
|
|
910
|
+
if (antigravity && !project) {
|
|
911
|
+
return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
|
|
912
|
+
}
|
|
913
|
+
const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
|
|
379
914
|
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
380
915
|
const started = Date.now();
|
|
381
916
|
try {
|
|
382
|
-
const res =
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
917
|
+
const res = method === "POST"
|
|
918
|
+
? await providerOutboundPost(name, prov, modelsUrl, {
|
|
919
|
+
headers,
|
|
920
|
+
body: JSON.stringify({ project }),
|
|
921
|
+
signal: AbortSignal.timeout(8000),
|
|
922
|
+
})
|
|
923
|
+
: await providerOutboundGet(name, prov, modelsUrl, {
|
|
924
|
+
headers,
|
|
925
|
+
signal: AbortSignal.timeout(8000),
|
|
926
|
+
});
|
|
386
927
|
const latencyMs = Date.now() - started;
|
|
387
928
|
const redirectError = await providerRedirectError(res, modelsUrl);
|
|
388
929
|
if (redirectError) {
|
|
@@ -398,7 +939,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
398
939
|
} catch {
|
|
399
940
|
// Best-effort release for non-conforming response streams.
|
|
400
941
|
}
|
|
401
|
-
return jsonResponse({ ok: false, latencyMs, error: `upstream
|
|
942
|
+
return jsonResponse({ ok: false, latencyMs, error: `upstream model discovery returned ${res.status}` });
|
|
402
943
|
}
|
|
403
944
|
const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
|
|
404
945
|
if (!bounded.ok) {
|
|
@@ -406,20 +947,26 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
406
947
|
ok: false,
|
|
407
948
|
latencyMs,
|
|
408
949
|
error: bounded.reason === "response_too_large"
|
|
409
|
-
|
|
410
|
-
|
|
950
|
+
? `upstream model discovery exceeded the ${discovery.maxResponseBytes}-byte response limit`
|
|
951
|
+
: "upstream model discovery returned invalid JSON",
|
|
411
952
|
});
|
|
412
953
|
}
|
|
954
|
+
const ccaModels = antigravity ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels) : undefined;
|
|
955
|
+
if (antigravity && !ccaModels) {
|
|
956
|
+
return jsonResponse({ ok: false, latencyMs, error: "upstream CCA model discovery returned an unexpected shape" });
|
|
957
|
+
}
|
|
413
958
|
// OpenAI-style lists (and Together top-level arrays) use the same validation/dedupe/filter
|
|
414
959
|
// as catalog discovery. Google's /v1beta/models uses `models[].name` and remains a
|
|
415
960
|
// connectivity-only count because it is not an authoritative catalog source.
|
|
416
961
|
const record = bounded.value !== null && typeof bounded.value === "object" && !Array.isArray(bounded.value)
|
|
417
962
|
? bounded.value as Record<string, unknown>
|
|
418
963
|
: undefined;
|
|
419
|
-
const extracted =
|
|
964
|
+
const extracted = ccaModels
|
|
965
|
+
? undefined
|
|
966
|
+
: Array.isArray(bounded.value) || Array.isArray(record?.data)
|
|
420
967
|
? extractProviderModelItems(bounded.value, discovery)
|
|
421
968
|
: extractModelEnvelopeRows(bounded.value, discovery.maxModels, ["models"]);
|
|
422
|
-
if (!extracted.ok) {
|
|
969
|
+
if (extracted && !extracted.ok) {
|
|
423
970
|
return jsonResponse({
|
|
424
971
|
ok: false,
|
|
425
972
|
latencyMs,
|
|
@@ -428,7 +975,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
428
975
|
: "upstream /models returned an unexpected shape",
|
|
429
976
|
});
|
|
430
977
|
}
|
|
431
|
-
const models = "items" in extracted ? extracted
|
|
978
|
+
const models = ccaModels?.length ?? ("items" in extracted! ? extracted!.items.length : extracted!.rows.length);
|
|
432
979
|
return jsonResponse({
|
|
433
980
|
ok: true,
|
|
434
981
|
latencyMs,
|
|
@@ -479,13 +1026,21 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
479
1026
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
480
1027
|
if (fallbackDefault) config.defaultProvider = fallbackDefault;
|
|
481
1028
|
delete config.providers[name];
|
|
1029
|
+
const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
|
|
1030
|
+
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
482
1031
|
setProviderContextCap(config, name, false);
|
|
483
1032
|
save(config);
|
|
1033
|
+
await replaceProviderAccountSet(name, null);
|
|
484
1034
|
reconcileLiveStateStores();
|
|
485
1035
|
const { clearModelCache: clearCache } = await import("../../codex/model-cache");
|
|
486
1036
|
clearCache(name);
|
|
487
|
-
await
|
|
488
|
-
return jsonResponse({
|
|
1037
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
1038
|
+
return jsonResponse({
|
|
1039
|
+
success: true,
|
|
1040
|
+
...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}),
|
|
1041
|
+
...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
|
|
1042
|
+
catalogRefresh,
|
|
1043
|
+
});
|
|
489
1044
|
}
|
|
490
1045
|
|
|
491
1046
|
if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
|
|
@@ -493,27 +1048,94 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
493
1048
|
}
|
|
494
1049
|
|
|
495
1050
|
if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
|
|
496
|
-
let
|
|
497
|
-
try {
|
|
1051
|
+
let rawBody: unknown;
|
|
1052
|
+
try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1053
|
+
// Reject non-object payloads (e.g. `{"provider": true}` or a bare array) before any
|
|
1054
|
+
// property access, with the route's consistent 400 response.
|
|
1055
|
+
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider-context-caps body must be a plain object" }, 400);
|
|
1056
|
+
const body = rawBody as { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
|
|
498
1057
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
499
1058
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
500
|
-
const respond = () => jsonResponse({
|
|
1059
|
+
const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
|
|
1060
|
+
ok: true,
|
|
1061
|
+
cap: DEFAULT_PROVIDER_CONTEXT_CAP,
|
|
1062
|
+
value: globalContextCapValue(config),
|
|
1063
|
+
caps: providerContextCaps(config),
|
|
1064
|
+
catalogRefresh,
|
|
1065
|
+
});
|
|
501
1066
|
|
|
502
|
-
//
|
|
1067
|
+
// Reject malformed and mixed payloads before branch selection: `provider` and `enabled`
|
|
1068
|
+
// must appear together with their expected types, and provider updates cannot be
|
|
1069
|
+
// combined with `setAll` (which would otherwise be silently ignored).
|
|
1070
|
+
const hasProviderFields = Object.hasOwn(body, "provider") || Object.hasOwn(body, "enabled");
|
|
1071
|
+
if (hasProviderFields) {
|
|
1072
|
+
if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
|
|
1073
|
+
return jsonResponse({ error: "provider and enabled are required together" }, 400);
|
|
1074
|
+
}
|
|
1075
|
+
if (Object.hasOwn(body, "setAll")) {
|
|
1076
|
+
return jsonResponse({ error: "setAll cannot be combined with provider updates" }, 400);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Branch 1: per-provider toggle (checked first: a per-provider request may carry an
|
|
1081
|
+
// explicit `value`, which must never fall through to the global-value branch). Enable
|
|
1082
|
+
// writes the current global default unless an explicit per-provider value is supplied;
|
|
1083
|
+
// that value is never copied to other providers.
|
|
1084
|
+
if (typeof body.provider === "string" && typeof body.enabled === "boolean") {
|
|
1085
|
+
const provider = body.provider.trim();
|
|
1086
|
+
if (!isValidProviderName(provider)) {
|
|
1087
|
+
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
1088
|
+
}
|
|
1089
|
+
if (!hasOwnProvider(config.providers, provider)) {
|
|
1090
|
+
return jsonResponse({ error: "unknown provider" }, 404);
|
|
1091
|
+
}
|
|
1092
|
+
// Validate a supplied per-provider value before mutating anything: it must be a
|
|
1093
|
+
// finite number, and after flooring it must still be >= 1 — a value like 0.5 would
|
|
1094
|
+
// otherwise floor to 0 and silently fall back to the global default.
|
|
1095
|
+
if (body.value !== undefined && (typeof body.value !== "number" || !Number.isFinite(body.value))) {
|
|
1096
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
1097
|
+
}
|
|
1098
|
+
const perProviderValue = typeof body.value === "number" ? Math.floor(body.value) : undefined;
|
|
1099
|
+
if (perProviderValue !== undefined && perProviderValue < 1) {
|
|
1100
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
1101
|
+
}
|
|
1102
|
+
setProviderContextCap(config, provider, body.enabled, perProviderValue);
|
|
1103
|
+
save(config);
|
|
1104
|
+
reconcileLiveStateStores();
|
|
1105
|
+
clearModelCache(provider);
|
|
1106
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
1107
|
+
return respond(catalogRefresh);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Branch 2: set the global cap value. When `setAll` accompanies it (dashboard "apply to
|
|
1111
|
+
// every routed provider" toggle), re-point every enabled provider; otherwise keep each
|
|
1112
|
+
// provider's own cap value and only change the default for future toggles.
|
|
503
1113
|
if (body.value !== undefined) {
|
|
504
|
-
|
|
1114
|
+
// Same normalization as the per-provider branch: floor before validating so a value
|
|
1115
|
+
// that floors to zero is rejected rather than silently stored.
|
|
1116
|
+
if (typeof body.value !== "number" || !Number.isFinite(body.value)) {
|
|
505
1117
|
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
506
1118
|
}
|
|
1119
|
+
const normalizedValue = Math.floor(body.value);
|
|
1120
|
+
if (normalizedValue < 1) {
|
|
1121
|
+
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
1122
|
+
}
|
|
1123
|
+
if (body.setAll !== undefined && typeof body.setAll !== "boolean") {
|
|
1124
|
+
return jsonResponse({ error: "setAll must be a boolean" }, 400);
|
|
1125
|
+
}
|
|
507
1126
|
const affected = Object.keys(providerContextCaps(config));
|
|
508
|
-
|
|
1127
|
+
const applyToAll = body.setAll === true;
|
|
1128
|
+
setGlobalContextCapValue(config, normalizedValue, applyToAll);
|
|
509
1129
|
save(config);
|
|
510
1130
|
reconcileLiveStateStores();
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
1131
|
+
if (applyToAll) {
|
|
1132
|
+
for (const provider of affected) clearModelCache(provider);
|
|
1133
|
+
}
|
|
1134
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
1135
|
+
return respond(catalogRefresh);
|
|
514
1136
|
}
|
|
515
1137
|
|
|
516
|
-
// Branch
|
|
1138
|
+
// Branch 3: enable/clear the cap for every provider at once.
|
|
517
1139
|
if (body.setAll !== undefined) {
|
|
518
1140
|
if (typeof body.setAll !== "boolean") {
|
|
519
1141
|
return jsonResponse({ error: "setAll must be a boolean" }, 400);
|
|
@@ -524,27 +1146,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
524
1146
|
save(config);
|
|
525
1147
|
reconcileLiveStateStores();
|
|
526
1148
|
for (const provider of new Set([...before, ...names])) clearModelCache(provider);
|
|
527
|
-
await
|
|
528
|
-
return respond();
|
|
1149
|
+
const catalogRefresh = await convergeCodexCatalog();
|
|
1150
|
+
return respond(catalogRefresh);
|
|
529
1151
|
}
|
|
530
1152
|
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
|
|
534
|
-
}
|
|
535
|
-
const provider = body.provider.trim();
|
|
536
|
-
if (!isValidProviderName(provider)) {
|
|
537
|
-
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
538
|
-
}
|
|
539
|
-
if (!hasOwnProvider(config.providers, provider)) {
|
|
540
|
-
return jsonResponse({ error: "unknown provider" }, 404);
|
|
541
|
-
}
|
|
542
|
-
setProviderContextCap(config, provider, body.enabled);
|
|
543
|
-
save(config);
|
|
544
|
-
reconcileLiveStateStores();
|
|
545
|
-
clearModelCache(provider);
|
|
546
|
-
await refreshCodexCatalogBestEffort();
|
|
547
|
-
return respond();
|
|
1153
|
+
// Unrecognized payload: reject rather than silently succeeding.
|
|
1154
|
+
return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
|
|
548
1155
|
}
|
|
549
1156
|
|
|
550
1157
|
// Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
|