@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +618 -74
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +152 -29
- package/src/images/plan.ts +23 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +865 -181
- package/src/server/lifecycle.ts +284 -13
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +288 -32
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +9 -3
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
|
-
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
5
|
+
import { atomicWriteFile, expandUserPath, getConfigDir, resolveEnvValue, websocketsEnabled } from "../../config";
|
|
6
6
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
7
|
import {
|
|
8
8
|
clearModelCache,
|
|
9
9
|
clearProviderDiscoveryStatus,
|
|
10
|
+
captureModelCacheGeneration,
|
|
10
11
|
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
11
12
|
getFreshCached,
|
|
12
13
|
getStaleCached,
|
|
13
14
|
isModelsFetchCoolingDown,
|
|
15
|
+
isModelCacheGenerationCurrent,
|
|
14
16
|
markModelsFetchFailure,
|
|
15
17
|
markProviderDiscoveryFailed,
|
|
16
18
|
markProviderDiscoveryOk,
|
|
@@ -18,14 +20,27 @@ import {
|
|
|
18
20
|
setCached,
|
|
19
21
|
type ProviderModelDiscoveryFailure,
|
|
20
22
|
} from "../model-cache";
|
|
21
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildModelsRequest,
|
|
25
|
+
getValidAccessTokenSnapshot,
|
|
26
|
+
observeActiveOAuthAccessToken,
|
|
27
|
+
resolveModelsAuthToken,
|
|
28
|
+
type OAuthActiveTokenObservation,
|
|
29
|
+
} from "../../oauth";
|
|
22
30
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
23
31
|
import { modelInList } from "../../types";
|
|
24
32
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
25
|
-
import {
|
|
33
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
26
34
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
27
|
-
import {
|
|
28
|
-
|
|
35
|
+
import {
|
|
36
|
+
captureFastPolicyAuthority,
|
|
37
|
+
fastPolicyForModel,
|
|
38
|
+
serviceTierSupportFromPolicy,
|
|
39
|
+
} from "../../providers/service-tier";
|
|
40
|
+
import type { FastPolicyAuthority } from "../../providers/fastwire";
|
|
41
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
42
|
+
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
|
|
43
|
+
import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
|
|
29
44
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
30
45
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
31
46
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
@@ -42,6 +57,7 @@ import type { NormalizedComboConfig } from "../../combos/types";
|
|
|
42
57
|
import {
|
|
43
58
|
ProviderOutboundPolicyError,
|
|
44
59
|
providerOutboundGet,
|
|
60
|
+
providerOutboundPost,
|
|
45
61
|
providerRedirectError,
|
|
46
62
|
} from "../../lib/provider-outbound";
|
|
47
63
|
import { redactSecretString } from "../../lib/redact";
|
|
@@ -51,25 +67,175 @@ import {
|
|
|
51
67
|
resolveProviderModelDiscovery,
|
|
52
68
|
type ModelDiscoveryResponseFailure,
|
|
53
69
|
type ProviderModelsApiItem,
|
|
70
|
+
type ResolvedProviderModelDiscovery,
|
|
54
71
|
} from "../../providers/model-discovery";
|
|
55
72
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
56
73
|
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
57
74
|
|
|
58
75
|
|
|
59
|
-
import { JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
76
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
60
77
|
import type { CatalogModel } from "./parsing";
|
|
61
|
-
import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
78
|
+
import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
62
79
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
63
80
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
81
|
+
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
82
|
+
import type {
|
|
83
|
+
CatalogAdmissionSnapshot,
|
|
84
|
+
CatalogDiscoveryPolicyField,
|
|
85
|
+
CatalogGatherAuthorityIdentity,
|
|
86
|
+
CatalogProviderDiscoveryPolicySnapshot,
|
|
87
|
+
CatalogProcessLocalEvidence,
|
|
88
|
+
CatalogSourceEvidence,
|
|
89
|
+
CatalogTrustedOpenAiApiPolicySnapshot,
|
|
90
|
+
} from "../convergence-types";
|
|
91
|
+
|
|
92
|
+
export type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
64
93
|
|
|
65
94
|
/** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
|
|
66
95
|
* Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
|
|
96
|
+
export interface CatalogGatherProviderAuthOutcome {
|
|
97
|
+
readonly provider: string;
|
|
98
|
+
readonly state: OAuthActiveTokenObservation["kind"];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface CatalogGatherProviderModelOutcome {
|
|
102
|
+
readonly provider: string;
|
|
103
|
+
readonly state: "authoritative" | "degraded";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GatherRoutedModelsOptions {
|
|
107
|
+
comboOmissions?: ComboCatalogOmission[];
|
|
108
|
+
providerAuthOutcomes?: CatalogGatherProviderAuthOutcome[];
|
|
109
|
+
/** Flight-local authority of each provider's returned model rows. */
|
|
110
|
+
providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
|
|
111
|
+
/** Internal convergence sink for the immutable policy that produced the returned rows. */
|
|
112
|
+
discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
|
|
113
|
+
/**
|
|
114
|
+
* Management-UI fast path: never block on upstream `/models`.
|
|
115
|
+
* Serves per-provider TTL/stale/configured seeds immediately and, when any live
|
|
116
|
+
* provider still needs a network probe, kicks a background full gather so the
|
|
117
|
+
* next call (or a short GUI re-poll) sees fresh rows. Codex catalog sync and
|
|
118
|
+
* `/v1/models` must omit this — they need the authoritative live set.
|
|
119
|
+
*/
|
|
120
|
+
preferCached?: boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
67
123
|
interface GatherFlightResult {
|
|
68
124
|
models: CatalogModel[];
|
|
69
125
|
comboOmissions: ComboCatalogOmission[];
|
|
126
|
+
providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
127
|
+
providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
128
|
+
discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ProviderModelsResult {
|
|
132
|
+
readonly models: CatalogModel[];
|
|
133
|
+
readonly outcome: CatalogGatherProviderModelOutcome;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface ModelsAuthResolution {
|
|
137
|
+
readonly apiKey: string | undefined;
|
|
138
|
+
readonly observed: boolean;
|
|
139
|
+
readonly oauthApiBaseUrl?: string;
|
|
140
|
+
readonly oauthProjectId?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type ModelsAuthResolver =
|
|
144
|
+
| { readonly kind: "refreshing" }
|
|
145
|
+
| {
|
|
146
|
+
readonly kind: "observed";
|
|
147
|
+
readonly resolve: (name: string, provider: OcxProviderConfig) => ModelsAuthResolution;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type ModelsAuthResolverFactory = (
|
|
151
|
+
outcomes: CatalogGatherProviderAuthOutcome[],
|
|
152
|
+
) => ModelsAuthResolver;
|
|
153
|
+
|
|
154
|
+
interface CapturedModelsRequest {
|
|
155
|
+
readonly method: "GET" | "POST";
|
|
156
|
+
readonly url: string;
|
|
157
|
+
readonly headersWithoutCredential: Readonly<Record<string, string>>;
|
|
158
|
+
readonly headersWithCredential: Readonly<Record<string, string>>;
|
|
70
159
|
}
|
|
71
160
|
|
|
72
|
-
|
|
161
|
+
interface CapturedProviderGather {
|
|
162
|
+
readonly name: string;
|
|
163
|
+
readonly provider: OcxProviderConfig;
|
|
164
|
+
readonly discovery: ResolvedProviderModelDiscovery;
|
|
165
|
+
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
|
|
166
|
+
readonly request: CapturedModelsRequest;
|
|
167
|
+
readonly fastPolicyAuthority: FastPolicyAuthority;
|
|
168
|
+
readonly observedAuth?: ModelsAuthResolution;
|
|
169
|
+
/**
|
|
170
|
+
* Configured model ids this provider must keep even when live discovery omits
|
|
171
|
+
* them — combo targets that are also listed in providers.*.models (OCX-111).
|
|
172
|
+
* Combo-only ids (not in models[]) stay out of the public catalog and are
|
|
173
|
+
* synthesized for combo derivation instead (#1305).
|
|
174
|
+
*/
|
|
175
|
+
readonly retainConfiguredModelIds?: ReadonlySet<string>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface GatherFlightCapture {
|
|
179
|
+
readonly discoveryPolicyIdentity: string;
|
|
180
|
+
readonly authIdentity: string;
|
|
181
|
+
readonly providerGraphIdentity: string;
|
|
182
|
+
readonly discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
183
|
+
readonly providers: readonly CapturedProviderGather[];
|
|
184
|
+
readonly authResolver: ModelsAuthResolver;
|
|
185
|
+
readonly providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
186
|
+
readonly openAiApiPolicy: CatalogTrustedOpenAiApiPolicySnapshot;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface GatherInflightEntry {
|
|
190
|
+
readonly discoveryPolicyIdentity: string;
|
|
191
|
+
/**
|
|
192
|
+
* The credential half of the join decision.
|
|
193
|
+
*
|
|
194
|
+
* `gatherFlightKey`'s fingerprint carries endpoints and model lists but no
|
|
195
|
+
* `authMode`, key or headers, and discovery policy does not carry them either.
|
|
196
|
+
* Two admissions differing ONLY in credential therefore produced the same key
|
|
197
|
+
* and the same policy, so the second joined the first and published rows the
|
|
198
|
+
* old key had fetched — reproduced against the real routes by rotating a key
|
|
199
|
+
* through `/api/providers/keys` mid-flight.
|
|
200
|
+
*
|
|
201
|
+
* Now REDUNDANT with `providerGraphIdentity`, which hashes the whole provider
|
|
202
|
+
* row and therefore covers `apiKey` too: removing this term alone leaves the
|
|
203
|
+
* credential regression green. It is kept deliberately, for two reasons. It
|
|
204
|
+
* covers what the graph cannot — the RESOLVED auth (`observedAuth`) and the
|
|
205
|
+
* final materialized headers, which are derived rather than stored, so an
|
|
206
|
+
* OAuth token that changes while the row is byte-identical still separates
|
|
207
|
+
* admissions. And it states the credential rule where a reader looks for it,
|
|
208
|
+
* instead of leaving it as an emergent property of hashing everything.
|
|
209
|
+
*/
|
|
210
|
+
readonly authIdentity: string;
|
|
211
|
+
/**
|
|
212
|
+
* The whole admitted provider graph, not a chosen subset.
|
|
213
|
+
*
|
|
214
|
+
* `providerCatalogFingerprint` is an ALLOW-LIST, so every field it forgot was
|
|
215
|
+
* silently treated as equivalence: credentials leaked a flight until
|
|
216
|
+
* `authIdentity` landed, and `reasoningEfforts` leaked one after that — both
|
|
217
|
+
* reproduced against real routes. Enumerating fields cannot converge, because
|
|
218
|
+
* the next field added to a provider row inherits the same defect. This
|
|
219
|
+
* identity therefore covers the enriched, frozen provider objects the flight
|
|
220
|
+
* actually gathered from, so a join is refused unless the admissions agree on
|
|
221
|
+
* everything rather than on everything somebody remembered to list.
|
|
222
|
+
*/
|
|
223
|
+
readonly providerGraphIdentity: string;
|
|
224
|
+
readonly promise: Promise<GatherFlightResult>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function withCanonicalOpenAiForwardAuthDefault(
|
|
228
|
+
name: string,
|
|
229
|
+
provider: OcxProviderConfig,
|
|
230
|
+
): OcxProviderConfig {
|
|
231
|
+
if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider;
|
|
232
|
+
const candidate = { ...provider, authMode: "forward" as const };
|
|
233
|
+
return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const gatherInflight = new Map<string, GatherInflightEntry[]>();
|
|
237
|
+
const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32);
|
|
238
|
+
const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`;
|
|
73
239
|
const MAX_CONCURRENT_CATALOG_GATHERS = 8;
|
|
74
240
|
const gatherGate = createAdmissionGate("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
|
|
75
241
|
|
|
@@ -95,6 +261,310 @@ function stableJson(value: unknown): string {
|
|
|
95
261
|
});
|
|
96
262
|
}
|
|
97
263
|
|
|
264
|
+
function framed(value: string): string {
|
|
265
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function canonicalAuthorityEncoding(value: unknown): string {
|
|
269
|
+
if (value === null) return "null";
|
|
270
|
+
if (value === undefined) return "undefined";
|
|
271
|
+
if (typeof value === "string") return `string${framed(value)}`;
|
|
272
|
+
if (typeof value === "boolean") return value ? "boolean1" : "boolean0";
|
|
273
|
+
if (typeof value === "number") {
|
|
274
|
+
if (!Number.isFinite(value)) throw new TypeError("Catalog authority cannot encode a non-finite number.");
|
|
275
|
+
const encoded = Object.is(value, -0) ? "-0" : String(value);
|
|
276
|
+
return `number${framed(encoded)}`;
|
|
277
|
+
}
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
return `array${value.length}:${value.map(item => framed(canonicalAuthorityEncoding(item))).join("")}`;
|
|
280
|
+
}
|
|
281
|
+
if (typeof value === "object") {
|
|
282
|
+
const record = value as Record<string, unknown>;
|
|
283
|
+
const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
|
|
284
|
+
return `object${keys.length}:${keys.map(key => (
|
|
285
|
+
`${framed(key)}${framed(canonicalAuthorityEncoding(record[key]))}`
|
|
286
|
+
)).join("")}`;
|
|
287
|
+
}
|
|
288
|
+
throw new TypeError(`Catalog authority cannot encode ${typeof value}.`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function keyedGatherIdentity(domain: string, value: unknown): string {
|
|
292
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
293
|
+
.update(framed(domain))
|
|
294
|
+
.update(framed(canonicalAuthorityEncoding(value)))
|
|
295
|
+
.digest("hex");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function keyedGatherBytesIdentity(domain: string, value: Uint8Array): string {
|
|
299
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
300
|
+
.update(framed(domain))
|
|
301
|
+
.update(`${value.byteLength}:`)
|
|
302
|
+
.update(value)
|
|
303
|
+
.digest("hex");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function createCatalogGatherAuthorityIdentity(
|
|
307
|
+
snapshot: CatalogAdmissionSnapshot,
|
|
308
|
+
sourceEvidence: CatalogSourceEvidence,
|
|
309
|
+
processLocal: CatalogProcessLocalEvidence,
|
|
310
|
+
discoveryPolicies: readonly CatalogProviderDiscoveryPolicySnapshot[],
|
|
311
|
+
): CatalogGatherAuthorityIdentity {
|
|
312
|
+
const sourceEvidenceIdentity = keyedGatherIdentity("catalog-source-evidence-v1", sourceEvidence);
|
|
313
|
+
const processLocalEvidenceIdentity = keyedGatherIdentity("catalog-process-local-v1", processLocal);
|
|
314
|
+
const discoveryPolicyIdentity = keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicies);
|
|
315
|
+
return Object.freeze({
|
|
316
|
+
version: 1 as const,
|
|
317
|
+
authorityId: keyedGatherIdentity("catalog-authority-v1", {
|
|
318
|
+
admittedConfig: snapshot.configIdentity,
|
|
319
|
+
discoveryPolicyIdentity,
|
|
320
|
+
sourceEvidenceIdentity,
|
|
321
|
+
processLocalEvidenceIdentity,
|
|
322
|
+
}),
|
|
323
|
+
admittedConfig: Object.freeze({
|
|
324
|
+
...snapshot.configIdentity,
|
|
325
|
+
generation: Object.freeze({ ...snapshot.configIdentity.generation }),
|
|
326
|
+
}),
|
|
327
|
+
authSnapshotIdentity: keyedGatherIdentity(
|
|
328
|
+
"catalog-auth-v1",
|
|
329
|
+
sourceEvidence.conditional["provider-auth-selection"],
|
|
330
|
+
),
|
|
331
|
+
discoveryPolicyIdentity,
|
|
332
|
+
nativeCatalogSourceIdentity: keyedGatherIdentity(
|
|
333
|
+
"catalog-native-v1",
|
|
334
|
+
sourceEvidence.conditional["native-catalog-selection"],
|
|
335
|
+
),
|
|
336
|
+
sourceEvidenceIdentity,
|
|
337
|
+
processLocalEvidenceIdentity,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function detachedClone<T>(value: T): T {
|
|
342
|
+
if (Array.isArray(value)) return value.map(item => detachedClone(item)) as T;
|
|
343
|
+
if (value && typeof value === "object") {
|
|
344
|
+
const clone: Record<string, unknown> = {};
|
|
345
|
+
for (const key of Object.keys(value)) {
|
|
346
|
+
clone[key] = detachedClone((value as Record<string, unknown>)[key]);
|
|
347
|
+
}
|
|
348
|
+
return clone as T;
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function recursivelyFreeze<T>(value: T): T {
|
|
354
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
355
|
+
for (const nested of Object.values(value as Record<string, unknown>)) recursivelyFreeze(nested);
|
|
356
|
+
return Object.freeze(value);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function detachedFrozen<T>(value: T): T {
|
|
360
|
+
return recursivelyFreeze(detachedClone(value));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function capturedField<T extends object, K extends keyof T>(
|
|
364
|
+
value: T | undefined,
|
|
365
|
+
key: K,
|
|
366
|
+
): CatalogDiscoveryPolicyField<T[K]> {
|
|
367
|
+
if (!value || !Object.hasOwn(value, key)) return Object.freeze({ state: "absent" });
|
|
368
|
+
return detachedFrozen({ state: "present" as const, value: value[key] });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function captureTrustedOpenAiApiPolicy(
|
|
372
|
+
name: string,
|
|
373
|
+
registryTransportMatch: boolean,
|
|
374
|
+
): CatalogTrustedOpenAiApiPolicySnapshot {
|
|
375
|
+
if (name !== OPENAI_API_PROVIDER_ID) return Object.freeze({ state: "unused" });
|
|
376
|
+
if (!registryTransportMatch) return Object.freeze({ state: "transport-mismatch" });
|
|
377
|
+
const entry = getProviderRegistryEntry(name);
|
|
378
|
+
if (!entry?.models) return Object.freeze({ state: "registry-models-absent" });
|
|
379
|
+
return detachedFrozen({
|
|
380
|
+
state: "captured" as const,
|
|
381
|
+
models: entry.models,
|
|
382
|
+
...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}),
|
|
383
|
+
...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}),
|
|
384
|
+
...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}),
|
|
385
|
+
...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function captureModelsRequest(
|
|
390
|
+
name: string,
|
|
391
|
+
provider: OcxProviderConfig,
|
|
392
|
+
observedAuth: ModelsAuthResolution | undefined,
|
|
393
|
+
): CapturedModelsRequest {
|
|
394
|
+
const observed = observedAuth
|
|
395
|
+
? { oauthApiBaseUrl: observedAuth.oauthApiBaseUrl }
|
|
396
|
+
: undefined;
|
|
397
|
+
const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
|
|
398
|
+
const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
|
|
399
|
+
const method = withoutCredential.method ?? "GET";
|
|
400
|
+
if (withoutCredential.url !== withCredential.url || method !== (withCredential.method ?? "GET")) {
|
|
401
|
+
throw new TypeError(`Provider model discovery URL for ${name} depends on credential bytes.`);
|
|
402
|
+
}
|
|
403
|
+
return detachedFrozen({
|
|
404
|
+
method,
|
|
405
|
+
url: withoutCredential.url,
|
|
406
|
+
headersWithoutCredential: withoutCredential.headers,
|
|
407
|
+
headersWithCredential: withCredential.headers,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function captureProviderGather(
|
|
412
|
+
name: string,
|
|
413
|
+
configured: OcxProviderConfig,
|
|
414
|
+
authResolver: ModelsAuthResolver,
|
|
415
|
+
retainConfiguredModelIds?: ReadonlySet<string>,
|
|
416
|
+
): CapturedProviderGather {
|
|
417
|
+
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
|
|
418
|
+
enrichProviderFromRegistry(name, enriched);
|
|
419
|
+
const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
|
|
420
|
+
const provider = recursivelyFreeze(enriched);
|
|
421
|
+
const fastPolicyAuthority = captureFastPolicyAuthority(
|
|
422
|
+
name,
|
|
423
|
+
provider,
|
|
424
|
+
registryTransportMatch,
|
|
425
|
+
configured,
|
|
426
|
+
);
|
|
427
|
+
const observedAuth = authResolver.kind === "observed"
|
|
428
|
+
&& provider.authMode !== "forward"
|
|
429
|
+
&& provider.liveModels !== false
|
|
430
|
+
? authResolver.resolve(name, provider)
|
|
431
|
+
: undefined;
|
|
432
|
+
const request = captureModelsRequest(name, provider, observedAuth);
|
|
433
|
+
const resolved = resolveProviderModelDiscovery(name, provider);
|
|
434
|
+
const discovery = detachedFrozen({
|
|
435
|
+
...(resolved.spec ? { spec: resolved.spec } : {}),
|
|
436
|
+
maxResponseBytes: resolved.maxResponseBytes,
|
|
437
|
+
maxModels: resolved.maxModels,
|
|
438
|
+
});
|
|
439
|
+
const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
|
|
440
|
+
const policy = detachedFrozen({
|
|
441
|
+
provider: name,
|
|
442
|
+
registryTransportMatch,
|
|
443
|
+
location: {
|
|
444
|
+
spec: discovery.spec ? "present" as const : "absent" as const,
|
|
445
|
+
url: capturedField(discovery.spec, "url"),
|
|
446
|
+
path: capturedField(discovery.spec, "path"),
|
|
447
|
+
query: capturedField(discovery.spec, "query"),
|
|
448
|
+
},
|
|
449
|
+
finalMethod: request.method,
|
|
450
|
+
finalUrl: request.url,
|
|
451
|
+
filter: capturedField(discovery.spec, "filter"),
|
|
452
|
+
maxResponseBytes: discovery.maxResponseBytes,
|
|
453
|
+
maxModels: discovery.maxModels,
|
|
454
|
+
trustedOpenAiApi,
|
|
455
|
+
});
|
|
456
|
+
return Object.freeze({
|
|
457
|
+
name,
|
|
458
|
+
provider,
|
|
459
|
+
discovery,
|
|
460
|
+
policy,
|
|
461
|
+
request,
|
|
462
|
+
fastPolicyAuthority,
|
|
463
|
+
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
|
|
464
|
+
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
|
|
465
|
+
? { retainConfiguredModelIds }
|
|
466
|
+
: {}),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Model ids each provider must retain for combo catalog derivation (OCX-111). */
|
|
471
|
+
export function configuredComboTargetModelsByProvider(
|
|
472
|
+
config: Pick<OcxConfig, "combos">,
|
|
473
|
+
): Map<string, ReadonlySet<string>> {
|
|
474
|
+
const byProvider = new Map<string, Set<string>>();
|
|
475
|
+
for (const id of listComboIds(config)) {
|
|
476
|
+
const combo = getCombo(config, id);
|
|
477
|
+
if (!combo) continue;
|
|
478
|
+
for (const target of combo.targets) {
|
|
479
|
+
let models = byProvider.get(target.provider);
|
|
480
|
+
if (!models) {
|
|
481
|
+
models = new Set();
|
|
482
|
+
byProvider.set(target.provider, models);
|
|
483
|
+
}
|
|
484
|
+
models.add(target.model);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return byProvider;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function captureGatherFlight(
|
|
491
|
+
config: OcxConfig,
|
|
492
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
493
|
+
): GatherFlightCapture {
|
|
494
|
+
const providerAuthOutcomes: CatalogGatherProviderAuthOutcome[] = [];
|
|
495
|
+
const authResolver = createAuthResolver(providerAuthOutcomes);
|
|
496
|
+
const comboTargetsByProvider = configuredComboTargetModelsByProvider(config);
|
|
497
|
+
const providers = Object.entries(config.providers)
|
|
498
|
+
.filter(([, provider]) => provider.disabled !== true)
|
|
499
|
+
.map(([name, provider]) => captureProviderGather(
|
|
500
|
+
name,
|
|
501
|
+
provider,
|
|
502
|
+
authResolver,
|
|
503
|
+
comboTargetsByProvider.get(name),
|
|
504
|
+
));
|
|
505
|
+
const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
discoveryPolicyIdentity: keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicySnapshots),
|
|
508
|
+
// Credentials are hashed under the same unexported per-process key, never
|
|
509
|
+
// stored or compared in the clear: this value can reach a map key and must
|
|
510
|
+
// not disclose a token. The final headers are included because a static
|
|
511
|
+
// header can carry authority just as an `apiKey` can.
|
|
512
|
+
authIdentity: keyedGatherIdentity("catalog-gather-auth-v1", providers.map(provider => ({
|
|
513
|
+
name: provider.name,
|
|
514
|
+
authMode: provider.provider.authMode ?? null,
|
|
515
|
+
liveModels: provider.provider.liveModels ?? null,
|
|
516
|
+
credential: provider.provider.apiKey ?? null,
|
|
517
|
+
observedAuth: provider.observedAuth ?? null,
|
|
518
|
+
headers: provider.request.headersWithCredential,
|
|
519
|
+
url: provider.request.url,
|
|
520
|
+
}))),
|
|
521
|
+
// Every enriched provider row the flight will gather from, in admission order.
|
|
522
|
+
// Anything that can change a catalog row lives in here by construction.
|
|
523
|
+
providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1",
|
|
524
|
+
providers.map(provider => ({
|
|
525
|
+
name: provider.name,
|
|
526
|
+
// `fetch` is a caller-owned transport executor, not admitted state: the
|
|
527
|
+
// outbound transport honors it so a caller can supply its own HTTP path.
|
|
528
|
+
// It is the one member of a provider row that is legitimately a function,
|
|
529
|
+
// so it is dropped here rather than allowed to break every encode.
|
|
530
|
+
provider: omitProviderTransportExecutor(provider.provider),
|
|
531
|
+
fastPolicyAuthority: provider.fastPolicyAuthority,
|
|
532
|
+
// Combo retention is capture-time state, not a provider-row field. Two
|
|
533
|
+
// gathers that share providers but differ in combo targets must not join.
|
|
534
|
+
retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(),
|
|
535
|
+
}))),
|
|
536
|
+
discoveryPolicySnapshots,
|
|
537
|
+
providers: Object.freeze(providers),
|
|
538
|
+
authResolver,
|
|
539
|
+
providerAuthOutcomes: Object.freeze([...providerAuthOutcomes]),
|
|
540
|
+
openAiApiPolicy: providers.find(provider => provider.name === OPENAI_API_PROVIDER_ID)?.policy.trustedOpenAiApi
|
|
541
|
+
?? Object.freeze({ state: "unused" as const }),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Drop the caller-owned transport executor before hashing a provider row.
|
|
547
|
+
*
|
|
548
|
+
* Fails closed on anything ELSE that cannot be encoded: the point of hashing the
|
|
549
|
+
* whole row is that no field escapes the comparison, so a second function member
|
|
550
|
+
* must surface as an encode error rather than being quietly skipped here.
|
|
551
|
+
*/
|
|
552
|
+
function omitProviderTransportExecutor(provider: OcxProviderConfig): Record<string, unknown> {
|
|
553
|
+
const entries = Object.entries(provider).filter(([key]) => key !== "fetch");
|
|
554
|
+
return Object.fromEntries(entries);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function materializeCapturedHeaders(
|
|
558
|
+
request: CapturedModelsRequest,
|
|
559
|
+
apiKey: string | undefined,
|
|
560
|
+
): Record<string, string> {
|
|
561
|
+
const source = apiKey ? request.headersWithCredential : request.headersWithoutCredential;
|
|
562
|
+
return Object.fromEntries(Object.entries(source).map(([name, value]) => [
|
|
563
|
+
name,
|
|
564
|
+
apiKey ? value.split(REQUEST_CREDENTIAL_SENTINEL).join(apiKey) : value,
|
|
565
|
+
]));
|
|
566
|
+
}
|
|
567
|
+
|
|
98
568
|
function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record<string, unknown> {
|
|
99
569
|
return {
|
|
100
570
|
n: name,
|
|
@@ -114,6 +584,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
114
584
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
115
585
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
116
586
|
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
587
|
+
serviceTier: prov.modelSupportsServiceTier ?? null,
|
|
117
588
|
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
118
589
|
ptc: prov.parallelToolCalls ?? null,
|
|
119
590
|
gMode: prov.googleMode ?? null,
|
|
@@ -185,15 +656,23 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
185
656
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
186
657
|
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
187
658
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
659
|
+
const fastPolicy = fastPolicyForModel(prov, model.id, name);
|
|
660
|
+
const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
|
|
661
|
+
const {
|
|
662
|
+
supportsServiceTier: _staleServiceTier,
|
|
663
|
+
fastTierDescription: _staleFastTierDescription,
|
|
664
|
+
...modelWithoutServiceTier
|
|
665
|
+
} = model;
|
|
666
|
+
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
|
|
667
|
+
const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
668
|
+
? model.contextWindow
|
|
669
|
+
: undefined;
|
|
670
|
+
const hintedWindow = discoveredWindow !== undefined
|
|
671
|
+
? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
|
|
672
|
+
: (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
|
|
188
673
|
const hinted = {
|
|
189
|
-
...
|
|
190
|
-
...(
|
|
191
|
-
? {
|
|
192
|
-
contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
193
|
-
? Math.min(model.contextWindow, configuredCap)
|
|
194
|
-
: configuredCap,
|
|
195
|
-
}
|
|
196
|
-
: {}),
|
|
674
|
+
...modelWithoutServiceTier,
|
|
675
|
+
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
197
676
|
...(inputModalities ? { inputModalities } : {}),
|
|
198
677
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
199
678
|
...(configuredMaxInput !== undefined
|
|
@@ -205,12 +684,17 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
205
684
|
: {}),
|
|
206
685
|
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
207
686
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
687
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
688
|
+
...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
|
|
689
|
+
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
690
|
+
: {}),
|
|
208
691
|
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
|
|
209
692
|
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
210
693
|
// advertise only on explicit opt-in.
|
|
211
694
|
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
212
695
|
? { parallelToolCalls: true }
|
|
213
696
|
: {}),
|
|
697
|
+
...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
|
|
214
698
|
};
|
|
215
699
|
const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
|
|
216
700
|
if (providerCap !== undefined && capped !== hinted.contextWindow) {
|
|
@@ -229,6 +713,159 @@ export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderCo
|
|
|
229
713
|
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap));
|
|
230
714
|
}
|
|
231
715
|
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Last-resort context window for combo member synthesis when discovery,
|
|
719
|
+
* provider config, and an enabled Context cap all omit one. Matches the
|
|
720
|
+
* catalog entry default in `normalizeRoutedCatalogEntry` so incomplete live
|
|
721
|
+
* rows still catalog. An enabled Context cap is the operator-facing window,
|
|
722
|
+
* not a clamp on this placeholder.
|
|
723
|
+
*/
|
|
724
|
+
const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000;
|
|
725
|
+
|
|
726
|
+
interface ComboCatalogMemberFallback {
|
|
727
|
+
readonly contextWindow?: number;
|
|
728
|
+
/** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
|
|
729
|
+
readonly maxInputTokens?: number;
|
|
730
|
+
readonly inputModalities?: readonly string[];
|
|
731
|
+
readonly reasoningEfforts?: readonly string[];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Resolve a combo target to a catalog member for derivation.
|
|
736
|
+
* Prefer discovery metadata; when the target is missing from the gather map or
|
|
737
|
+
* lacks a positive contextWindow, synthesize from the (registry-enriched)
|
|
738
|
+
* provider config so combos remain catalogued when targets are configured but
|
|
739
|
+
* discovery metadata is incomplete. Disabled providers stay unresolved.
|
|
740
|
+
* When hints still omit contextWindow, prefer known maxInputTokens, else the
|
|
741
|
+
* enabled Context cap, else COMBO_MEMBER_CONTEXT_FALLBACK so a live row
|
|
742
|
+
* without ctx does not drop the whole combo from the public catalog.
|
|
743
|
+
*/
|
|
744
|
+
export function resolveComboCatalogMember(
|
|
745
|
+
target: { provider: string; model: string },
|
|
746
|
+
memberByKey: ReadonlyMap<string, CatalogModel>,
|
|
747
|
+
providers: ReadonlyMap<string, OcxProviderConfig>,
|
|
748
|
+
contextCap?: number,
|
|
749
|
+
fallback?: ComboCatalogMemberFallback,
|
|
750
|
+
): CatalogModel | undefined {
|
|
751
|
+
const existing = memberByKey.get(targetKey(target));
|
|
752
|
+
const prov = providers.get(target.provider);
|
|
753
|
+
// Disabled providers never contribute members — even a complete discovery row
|
|
754
|
+
// is unusable for catalog derivation while the provider is off.
|
|
755
|
+
if (prov?.disabled === true) return undefined;
|
|
756
|
+
|
|
757
|
+
const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
|
|
758
|
+
if (!fallback) return member;
|
|
759
|
+
const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
|
|
760
|
+
? member.contextWindow
|
|
761
|
+
: undefined;
|
|
762
|
+
const addMaxInput = contextWindow !== undefined
|
|
763
|
+
&& !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
|
|
764
|
+
const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
|
|
765
|
+
&& fallback.inputModalities !== undefined;
|
|
766
|
+
const addReasoning = member.reasoningEfforts === undefined
|
|
767
|
+
&& fallback.reasoningEfforts !== undefined;
|
|
768
|
+
if (!addMaxInput && !addModalities && !addReasoning) return member;
|
|
769
|
+
return {
|
|
770
|
+
...member,
|
|
771
|
+
// Never claim a larger input budget than the window, and prefer the model's own
|
|
772
|
+
// measured ceiling when the fallback carries one.
|
|
773
|
+
...(addMaxInput
|
|
774
|
+
? { maxInputTokens: Math.min(fallback.maxInputTokens ?? contextWindow!, contextWindow!) }
|
|
775
|
+
: {}),
|
|
776
|
+
...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
|
|
777
|
+
...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
|
|
778
|
+
};
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// Complete live/configured rows still honour providerContextCaps so a high
|
|
782
|
+
// discovery window cannot outrun an operator-configured cap. Native-alias
|
|
783
|
+
// fallback metadata may fill only capability gaps; it never raises an explicit
|
|
784
|
+
// discovered/configured context window.
|
|
785
|
+
if (
|
|
786
|
+
existing
|
|
787
|
+
&& typeof existing.contextWindow === "number"
|
|
788
|
+
&& existing.contextWindow > 0
|
|
789
|
+
) {
|
|
790
|
+
const capped = applyProviderContextCap(existing.contextWindow, contextCap);
|
|
791
|
+
if (capped === undefined || capped === existing.contextWindow) {
|
|
792
|
+
return withFallbackMetadata(existing);
|
|
793
|
+
}
|
|
794
|
+
const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0
|
|
795
|
+
? Math.min(existing.maxInputTokens, capped)
|
|
796
|
+
: Math.min(fallback?.maxInputTokens ?? capped, capped);
|
|
797
|
+
return withFallbackMetadata({
|
|
798
|
+
...existing,
|
|
799
|
+
contextWindow: capped,
|
|
800
|
+
maxInputTokens: maxInput,
|
|
801
|
+
contextCap,
|
|
802
|
+
contextCapped: true as const,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const base: CatalogModel = existing ?? {
|
|
807
|
+
id: target.model,
|
|
808
|
+
provider: target.provider,
|
|
809
|
+
};
|
|
810
|
+
const hinted = prov
|
|
811
|
+
? applyProviderConfigHints(target.provider, prov, base, contextCap)
|
|
812
|
+
: base;
|
|
813
|
+
const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
|
|
814
|
+
? hinted.contextWindow
|
|
815
|
+
: undefined;
|
|
816
|
+
const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
|
|
817
|
+
? hinted.maxInputTokens
|
|
818
|
+
: (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0
|
|
819
|
+
? base.maxInputTokens
|
|
820
|
+
: undefined);
|
|
821
|
+
// Kept OUT of knownMaxInput on purpose: that value doubles as a context-window fallback
|
|
822
|
+
// below, and a native alias whose input ceiling (922k) is lower than its window (1.05M)
|
|
823
|
+
// would otherwise shrink the advertised window to the input limit.
|
|
824
|
+
const fallbackMaxInput = existing || prov ? fallback?.maxInputTokens : undefined;
|
|
825
|
+
// Real discovery/config values win. A native alias is the next fallback tier.
|
|
826
|
+
// The generic 128k/text synthesis from #1305 remains the final fallback.
|
|
827
|
+
const fallbackContext = existing || prov ? fallback?.contextWindow : undefined;
|
|
828
|
+
const uncappedContext = hintedContext
|
|
829
|
+
?? knownMaxInput
|
|
830
|
+
?? fallbackContext
|
|
831
|
+
?? (existing || prov ? resolveUnknownRoutedContextWindow(contextCap) : undefined);
|
|
832
|
+
if (uncappedContext === undefined) return undefined;
|
|
833
|
+
// 真发现值才压低。resolveUnknownRoutedContextWindow 已经把 cap 当成窗口填进去了,不能再 min 一次。
|
|
834
|
+
const usedDiscoveredWindow = hintedContext !== undefined || knownMaxInput !== undefined || fallbackContext !== undefined;
|
|
835
|
+
const cappedContext = usedDiscoveredWindow
|
|
836
|
+
? applyProviderContextCap(uncappedContext, contextCap)
|
|
837
|
+
: uncappedContext;
|
|
838
|
+
const contextWindow = cappedContext ?? uncappedContext;
|
|
839
|
+
const fallbackCapped = usedDiscoveredWindow
|
|
840
|
+
&& contextCap !== undefined
|
|
841
|
+
&& cappedContext !== undefined
|
|
842
|
+
&& cappedContext !== uncappedContext;
|
|
843
|
+
|
|
844
|
+
const inputModalities = hinted.inputModalities
|
|
845
|
+
?? base.inputModalities
|
|
846
|
+
?? (fallback?.inputModalities ? [...fallback.inputModalities] : undefined)
|
|
847
|
+
?? ["text"];
|
|
848
|
+
const reasoningEfforts = hinted.reasoningEfforts
|
|
849
|
+
?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined)
|
|
850
|
+
?? base.reasoningEfforts
|
|
851
|
+
?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined);
|
|
852
|
+
// The model's own measured input ceiling still applies when discovery gave us nothing:
|
|
853
|
+
// GPT-5.6 advertises a 1.05M window but refuses input past 922k.
|
|
854
|
+
const effectiveMaxInput = knownMaxInput ?? fallbackMaxInput;
|
|
855
|
+
const maxInputTokens = effectiveMaxInput !== undefined
|
|
856
|
+
? Math.min(effectiveMaxInput, contextWindow)
|
|
857
|
+
: contextWindow;
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
...hinted,
|
|
861
|
+
inputModalities,
|
|
862
|
+
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
863
|
+
contextWindow,
|
|
864
|
+
maxInputTokens,
|
|
865
|
+
...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
232
869
|
export function isDatedVariantId(liveId: string, configuredId: string): boolean {
|
|
233
870
|
if (!liveId.startsWith(`${configuredId}-`)) return false;
|
|
234
871
|
return /^\d{8}$/.test(liveId.slice(configuredId.length + 1));
|
|
@@ -273,10 +910,25 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
|
|
|
273
910
|
);
|
|
274
911
|
}
|
|
275
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Z.AI and Neuralwatt advertise GLM reasoning as a bare boolean, which would otherwise
|
|
915
|
+
* collapse to the four-tier default ladder that omits `max`. These two helpers name the
|
|
916
|
+
* ladder each GLM generation actually honours on the wire.
|
|
917
|
+
*/
|
|
918
|
+
/** GLM-5.2 and its 1M alias: the full five-tier ladder including `max`. */
|
|
276
919
|
export function isGlm52ModelId(id: string): boolean {
|
|
277
|
-
const normalized = id.toLowerCase();
|
|
920
|
+
const normalized = id.trim().toLowerCase();
|
|
278
921
|
return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
|
|
279
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* GLM-5.3 and its 1M alias. 260814: docs.z.ai/devpack/latest-model folds every incoming
|
|
925
|
+
* effort into three effective tiers (low/minimal/light -> low, medium/high -> high,
|
|
926
|
+
* xhigh/max/ultra -> max), so a boolean capability must not be expanded to five rows.
|
|
927
|
+
*/
|
|
928
|
+
export function isGlm53ModelId(id: string): boolean {
|
|
929
|
+
const normalized = id.trim().toLowerCase();
|
|
930
|
+
return normalized === "glm-5.3" || normalized === "glm-5.3[1m]";
|
|
931
|
+
}
|
|
280
932
|
|
|
281
933
|
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
282
934
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -313,7 +965,9 @@ function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): st
|
|
|
313
965
|
function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
|
|
314
966
|
const metadata = plainRecord(item.metadata);
|
|
315
967
|
const metadataCapabilities = metadata?.capabilities;
|
|
316
|
-
const capabilityRecord = plainRecord(metadataCapabilities)
|
|
968
|
+
const capabilityRecord = plainRecord(metadataCapabilities)
|
|
969
|
+
?? plainRecord(item.capabilities)
|
|
970
|
+
?? plainRecord(item.features);
|
|
317
971
|
const out = new Set<string>();
|
|
318
972
|
for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
|
|
319
973
|
for (const capability of normalizedStringList(list) ?? []) out.add(capability);
|
|
@@ -343,7 +997,9 @@ function modelInputModalities(
|
|
|
343
997
|
capabilities: readonly string[] | undefined,
|
|
344
998
|
): string[] | undefined {
|
|
345
999
|
const metadata = plainRecord(item.metadata);
|
|
346
|
-
const capabilityRecord = plainRecord(metadata?.capabilities)
|
|
1000
|
+
const capabilityRecord = plainRecord(metadata?.capabilities)
|
|
1001
|
+
?? plainRecord(item.capabilities)
|
|
1002
|
+
?? plainRecord(item.features);
|
|
347
1003
|
const explicit = normalizedStringList(
|
|
348
1004
|
item.input_modalities
|
|
349
1005
|
?? item.modalities
|
|
@@ -358,8 +1014,26 @@ function modelInputModalities(
|
|
|
358
1014
|
value === "text" || value === "image" || value === "audio"
|
|
359
1015
|
));
|
|
360
1016
|
if (explicit && explicit.length > 0) return explicit;
|
|
1017
|
+
const architecture = plainRecord(item.architecture);
|
|
1018
|
+
const architectureModality = typeof architecture?.modality === "string"
|
|
1019
|
+
? normalizedMetadataString(architecture.modality, 64)
|
|
1020
|
+
: undefined;
|
|
1021
|
+
if (architectureModality?.includes("->")) {
|
|
1022
|
+
const [rawInput = ""] = architectureModality.split("->");
|
|
1023
|
+
const inferred = rawInput
|
|
1024
|
+
.split("+")
|
|
1025
|
+
.filter(value => value === "text" || value === "image" || value === "audio");
|
|
1026
|
+
if (inferred.length > 0) return [...new Set(inferred)];
|
|
1027
|
+
}
|
|
361
1028
|
if (capabilityRecord?.vision === false) return ["text"];
|
|
362
|
-
if (capabilityRecord?.vision === true || capabilities?.some(value =>
|
|
1029
|
+
if (capabilityRecord?.vision === true || capabilities?.some(value => (
|
|
1030
|
+
value === "vision" || value === "image-input" || value === "image_input"
|
|
1031
|
+
// llama.cpp and Ollama-compatible servers report vision as "multimodal" —
|
|
1032
|
+
// it is the only image signal those servers emit (#1797). Mapped to the
|
|
1033
|
+
// closed `text|image` enum rather than passed through: an out-of-enum
|
|
1034
|
+
// modality makes Codex reject the entire catalog file.
|
|
1035
|
+
|| value === "multimodal"
|
|
1036
|
+
))) {
|
|
363
1037
|
return ["text", "image"];
|
|
364
1038
|
}
|
|
365
1039
|
return undefined;
|
|
@@ -377,17 +1051,35 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
377
1051
|
item.context_size,
|
|
378
1052
|
item.max_model_len,
|
|
379
1053
|
item.max_context_length,
|
|
1054
|
+
// llama.cpp reports the served context under `meta`: `n_ctx` is what the
|
|
1055
|
+
// server was actually started with, `n_ctx_train` the model's trained
|
|
1056
|
+
// maximum. Prefer the served value — routing must not promise a window the
|
|
1057
|
+
// running server will refuse. Both come LAST so no provider already
|
|
1058
|
+
// supplying a recognized field changes behavior (#1797).
|
|
1059
|
+
plainRecord(item.meta)?.n_ctx,
|
|
1060
|
+
plainRecord(item.meta)?.n_ctx_train,
|
|
380
1061
|
);
|
|
381
1062
|
const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
|
|
382
|
-
|
|
1063
|
+
// Some OpenAI-compatible catalogs expose the selectable ladder under
|
|
1064
|
+
// `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key.
|
|
1065
|
+
// Treat both as model metadata: otherwise a valid upstream capability disappears
|
|
1066
|
+
// before client exporters (including omp) can advertise it.
|
|
1067
|
+
const reasoningParameters = plainRecord(item.reasoning_parameters)
|
|
1068
|
+
?? plainRecord(metadata?.reasoning_parameters)
|
|
1069
|
+
?? plainRecord(capabilityRecord?.reasoning_parameters);
|
|
1070
|
+
const rawReasoningEfforts = capabilityRecord?.reasoning_effort
|
|
1071
|
+
?? item.reasoning_efforts
|
|
1072
|
+
?? reasoningParameters?.efforts;
|
|
383
1073
|
const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24);
|
|
384
1074
|
const reasoningEfforts = listedReasoningEfforts
|
|
385
1075
|
? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
|
|
386
1076
|
: typeof rawReasoningEfforts === "boolean"
|
|
387
1077
|
? (rawReasoningEfforts
|
|
388
|
-
? ((providerName === "neuralwatt" || providerName === "zai") &&
|
|
389
|
-
? ["low", "
|
|
390
|
-
:
|
|
1078
|
+
? ((providerName === "neuralwatt" || providerName === "zai") && isGlm53ModelId(item.id)
|
|
1079
|
+
? ["low", "high", "max"]
|
|
1080
|
+
: (providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
|
|
1081
|
+
? ["low", "medium", "high", "xhigh", "max"]
|
|
1082
|
+
: ["low", "medium", "high", "xhigh"])
|
|
391
1083
|
: [])
|
|
392
1084
|
: undefined;
|
|
393
1085
|
const capabilities = modelCapabilities(item);
|
|
@@ -407,8 +1099,49 @@ function boundedOwnedBy(value: unknown): string | undefined {
|
|
|
407
1099
|
return value;
|
|
408
1100
|
}
|
|
409
1101
|
|
|
410
|
-
|
|
411
|
-
|
|
1102
|
+
const refreshingModelsAuthResolver: ModelsAuthResolver = { kind: "refreshing" };
|
|
1103
|
+
|
|
1104
|
+
function observedModelsAuthResolver(
|
|
1105
|
+
authStoreBuffer: Uint8Array | null,
|
|
1106
|
+
outcomes: CatalogGatherProviderAuthOutcome[],
|
|
1107
|
+
): ModelsAuthResolver {
|
|
1108
|
+
return {
|
|
1109
|
+
kind: "observed",
|
|
1110
|
+
resolve(name, provider) {
|
|
1111
|
+
if (provider.authMode === "forward") return { apiKey: undefined, observed: true };
|
|
1112
|
+
if (provider.authMode !== "oauth") {
|
|
1113
|
+
return { apiKey: resolveEnvValue(provider.apiKey), observed: true };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const observation = observeActiveOAuthAccessToken(name, authStoreBuffer);
|
|
1117
|
+
outcomes.push({ provider: name, state: observation.kind });
|
|
1118
|
+
if (observation.kind !== "available") return { apiKey: undefined, observed: true };
|
|
1119
|
+
return {
|
|
1120
|
+
apiKey: observation.snapshot.accessToken,
|
|
1121
|
+
observed: true,
|
|
1122
|
+
...(observation.snapshot.apiBaseUrl ? { oauthApiBaseUrl: observation.snapshot.apiBaseUrl } : {}),
|
|
1123
|
+
...(observation.snapshot.projectId ? { oauthProjectId: observation.snapshot.projectId } : {}),
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function fetchProviderModelsWithAuth(
|
|
1130
|
+
captured: CapturedProviderGather,
|
|
1131
|
+
ttlMs: number,
|
|
1132
|
+
contextCap: number | undefined,
|
|
1133
|
+
resolveAuth: ModelsAuthResolver,
|
|
1134
|
+
): Promise<ProviderModelsResult> {
|
|
1135
|
+
const { name, provider: prov, discovery, request } = captured;
|
|
1136
|
+
const observed = (
|
|
1137
|
+
models: CatalogModel[],
|
|
1138
|
+
state: CatalogGatherProviderModelOutcome["state"],
|
|
1139
|
+
): ProviderModelsResult => ({ models, outcome: { provider: name, state } });
|
|
1140
|
+
// Capture before any credential refresh or outbound await. OAuth account changes clear this
|
|
1141
|
+
// generation, so a request started with the former account cannot later publish its result.
|
|
1142
|
+
const cacheGeneration = captureModelCacheGeneration(name);
|
|
1143
|
+
const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
|
|
1144
|
+
if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
|
|
412
1145
|
const seedVertexDefault = prov.adapter === "google"
|
|
413
1146
|
&& prov.googleMode === "vertex"
|
|
414
1147
|
&& (prov.models?.length ?? 0) === 0
|
|
@@ -419,13 +1152,48 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
419
1152
|
provider: name,
|
|
420
1153
|
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
|
|
421
1154
|
}));
|
|
1155
|
+
const withConfiguredRetention = (
|
|
1156
|
+
models: CatalogModel[],
|
|
1157
|
+
options?: { retainComboTargets?: boolean; warnDrops?: boolean },
|
|
1158
|
+
): CatalogModel[] => {
|
|
1159
|
+
const { models: merged, droppedConfiguredIds } = mergeConfiguredModelsIntoLiveCatalog({
|
|
1160
|
+
name,
|
|
1161
|
+
provider: prov,
|
|
1162
|
+
models,
|
|
1163
|
+
configured,
|
|
1164
|
+
retainConfiguredModelIds: captured.retainConfiguredModelIds,
|
|
1165
|
+
contextCap,
|
|
1166
|
+
seedVertexDefault,
|
|
1167
|
+
retainComboTargets: options?.retainComboTargets,
|
|
1168
|
+
});
|
|
1169
|
+
if (
|
|
1170
|
+
options?.warnDrops === true
|
|
1171
|
+
&& droppedConfiguredIds.length > 0
|
|
1172
|
+
&& name !== OPENAI_API_PROVIDER_ID
|
|
1173
|
+
&& !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)
|
|
1174
|
+
) {
|
|
1175
|
+
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
1176
|
+
}
|
|
1177
|
+
return merged;
|
|
1178
|
+
};
|
|
422
1179
|
// Static catalogs never need an OAuth refresh or an upstream model request. Clear any
|
|
423
1180
|
// discovery failure left by an older live configuration even when the account is logged out.
|
|
424
1181
|
if (prov.liveModels === false) {
|
|
425
1182
|
clearProviderDiscoveryStatus(name);
|
|
426
|
-
return configured;
|
|
1183
|
+
return observed(configured, "authoritative");
|
|
427
1184
|
}
|
|
428
|
-
const
|
|
1185
|
+
const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
1186
|
+
? prov.authMode === "oauth" && effectiveGoogleMode(name, prov) === "cloud-code-assist"
|
|
1187
|
+
? await getValidAccessTokenSnapshot(name)
|
|
1188
|
+
.then(snapshot => ({
|
|
1189
|
+
apiKey: snapshot.accessToken,
|
|
1190
|
+
observed: false,
|
|
1191
|
+
...(snapshot.projectId ? { oauthProjectId: snapshot.projectId } : {}),
|
|
1192
|
+
}))
|
|
1193
|
+
.catch(() => ({ apiKey: undefined, observed: false }))
|
|
1194
|
+
: { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
|
|
1195
|
+
: resolveAuth.resolve(name, prov));
|
|
1196
|
+
const apiKey = auth.apiKey;
|
|
429
1197
|
// A configured default is a real callable selector and must remain discoverable when a
|
|
430
1198
|
// compatible provider's live /models request fails (issue #308). Keep this separate from the
|
|
431
1199
|
// explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero
|
|
@@ -444,56 +1212,107 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
444
1212
|
: models
|
|
445
1213
|
);
|
|
446
1214
|
if (prov.adapter === "cursor") {
|
|
447
|
-
if (!apiKey) return configured;
|
|
1215
|
+
if (!apiKey) return observed(configured, "degraded");
|
|
448
1216
|
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
449
1217
|
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
|
|
450
1218
|
// suffix) but filter the static seed to the bases the account actually has — so models not on the
|
|
451
1219
|
// plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
|
|
452
1220
|
const cachedCursor = getFreshCached(name, ttlMs);
|
|
453
|
-
if (cachedCursor)
|
|
1221
|
+
if (cachedCursor) {
|
|
1222
|
+
return observed(
|
|
1223
|
+
withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor)),
|
|
1224
|
+
"authoritative",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
454
1227
|
if (isModelsFetchCoolingDown(name)) {
|
|
455
1228
|
const cooling = getStaleCached(name);
|
|
456
|
-
return
|
|
1229
|
+
return observed(
|
|
1230
|
+
withConfiguredRetention(
|
|
1231
|
+
cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
|
|
1232
|
+
),
|
|
1233
|
+
"degraded",
|
|
1234
|
+
);
|
|
457
1235
|
}
|
|
458
|
-
const
|
|
1236
|
+
const cursorFetch = (prov as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch;
|
|
1237
|
+
const liveResult = await fetchCursorUsableModels({
|
|
1238
|
+
apiKey,
|
|
1239
|
+
baseUrl: prov.baseUrl,
|
|
1240
|
+
upstreamHttpVersion: prov.upstreamHttpVersion,
|
|
1241
|
+
...(cursorFetch ? { fetch: cursorFetch } : {}),
|
|
1242
|
+
});
|
|
459
1243
|
if (liveResult.ok) {
|
|
460
1244
|
const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
|
|
461
1245
|
const result = available.length > 0 ? available : configured;
|
|
462
|
-
//
|
|
1246
|
+
// Cache the discovery-filtered roster without combo retention so a later
|
|
1247
|
+
// gather can re-apply the current capture's retain set on read.
|
|
1248
|
+
const forCache = withConfiguredRetention(result, { retainComboTargets: false });
|
|
1249
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1250
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
1251
|
+
}
|
|
463
1252
|
markProviderDiscoveryOk(name, liveResult.models.length);
|
|
464
|
-
|
|
465
|
-
|
|
1253
|
+
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
1254
|
+
}
|
|
1255
|
+
if (isCurrentCacheGeneration()) {
|
|
1256
|
+
markModelsFetchFailure(name);
|
|
1257
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
1258
|
+
console.warn(
|
|
1259
|
+
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
1260
|
+
);
|
|
466
1261
|
}
|
|
467
|
-
markModelsFetchFailure(name);
|
|
468
|
-
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
469
|
-
console.warn(
|
|
470
|
-
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
471
|
-
);
|
|
472
1262
|
const staleCursor = getStaleCached(name);
|
|
473
|
-
return
|
|
1263
|
+
return observed(
|
|
1264
|
+
withConfiguredRetention(
|
|
1265
|
+
staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured,
|
|
1266
|
+
),
|
|
1267
|
+
"degraded",
|
|
1268
|
+
);
|
|
474
1269
|
}
|
|
475
1270
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
476
1271
|
// No usable token (logged out, or account marked needsReauth). Still surface the
|
|
477
1272
|
// configured static catalog so the GUI Models tab / rail counts are not empty —
|
|
478
1273
|
// matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
|
|
479
|
-
return configured;
|
|
1274
|
+
return observed(configured, "degraded");
|
|
480
1275
|
}
|
|
1276
|
+
const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
1277
|
+
const project = prov.project ?? auth.oauthProjectId;
|
|
1278
|
+
if (cloudCodeAssist && !project) return observed(configured, "degraded");
|
|
481
1279
|
const fresh = getFreshCached(name, ttlMs);
|
|
482
|
-
if (fresh)
|
|
1280
|
+
if (fresh) {
|
|
1281
|
+
return observed(
|
|
1282
|
+
withConfiguredRetention(
|
|
1283
|
+
withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)),
|
|
1284
|
+
),
|
|
1285
|
+
"authoritative",
|
|
1286
|
+
); // dedups Codex's frequent /v1/models polling within the TTL
|
|
1287
|
+
}
|
|
483
1288
|
if (isModelsFetchCoolingDown(name)) {
|
|
484
1289
|
// A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
|
|
485
1290
|
// fetch timeout on every catalog poll — the dashboard polls this path per page load.
|
|
486
1291
|
const stale = getStaleCached(name);
|
|
487
|
-
return
|
|
1292
|
+
return observed(
|
|
1293
|
+
withConfiguredRetention(
|
|
1294
|
+
stale
|
|
1295
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
|
|
1296
|
+
: failedDiscoveryConfigured,
|
|
1297
|
+
),
|
|
1298
|
+
"degraded",
|
|
1299
|
+
);
|
|
488
1300
|
}
|
|
489
|
-
const
|
|
490
|
-
const
|
|
1301
|
+
const url = request.url;
|
|
1302
|
+
const headers = materializeCapturedHeaders(request, apiKey);
|
|
491
1303
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
492
1304
|
? "vertex-aiplatform"
|
|
493
1305
|
: "provider-models";
|
|
494
1306
|
const failedDiscoveryFallback = (
|
|
495
1307
|
failure: ProviderModelDiscoveryFailure,
|
|
496
1308
|
): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
|
|
1309
|
+
if (!isCurrentCacheGeneration()) {
|
|
1310
|
+
return {
|
|
1311
|
+
models: withConfiguredRetention(failedDiscoveryConfigured),
|
|
1312
|
+
fallback: "configured",
|
|
1313
|
+
shouldLog: false,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
497
1316
|
// Decide logging BEFORE recording the new status, so we can compare against the prior one and
|
|
498
1317
|
// suppress an identical repeated failure (#395 log flood). The failure stays observable via the
|
|
499
1318
|
// discovery-status API regardless.
|
|
@@ -502,18 +1321,26 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
502
1321
|
markProviderDiscoveryFailed(name, failure);
|
|
503
1322
|
const stale = getStaleCached(name);
|
|
504
1323
|
return {
|
|
505
|
-
models:
|
|
506
|
-
|
|
507
|
-
|
|
1324
|
+
models: withConfiguredRetention(
|
|
1325
|
+
stale
|
|
1326
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
|
|
1327
|
+
: failedDiscoveryConfigured,
|
|
1328
|
+
),
|
|
508
1329
|
fallback: stale ? "stale" : "configured",
|
|
509
1330
|
shouldLog,
|
|
510
1331
|
};
|
|
511
1332
|
};
|
|
512
1333
|
try {
|
|
513
|
-
const res =
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1334
|
+
const res = request.method === "POST"
|
|
1335
|
+
? await providerOutboundPost(name, prov, url, {
|
|
1336
|
+
headers,
|
|
1337
|
+
body: JSON.stringify({ project }),
|
|
1338
|
+
signal: AbortSignal.timeout(8000),
|
|
1339
|
+
})
|
|
1340
|
+
: await providerOutboundGet(name, prov, url, {
|
|
1341
|
+
headers,
|
|
1342
|
+
signal: AbortSignal.timeout(8000),
|
|
1343
|
+
});
|
|
517
1344
|
const redirectError = await providerRedirectError(res, url);
|
|
518
1345
|
if (redirectError) {
|
|
519
1346
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
@@ -522,7 +1349,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
522
1349
|
`[opencodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
523
1350
|
);
|
|
524
1351
|
}
|
|
525
|
-
return models;
|
|
1352
|
+
return observed(models, "degraded");
|
|
526
1353
|
}
|
|
527
1354
|
if (!res.ok) {
|
|
528
1355
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
@@ -531,7 +1358,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
531
1358
|
`[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
532
1359
|
);
|
|
533
1360
|
}
|
|
534
|
-
return models;
|
|
1361
|
+
return observed(models, "degraded");
|
|
535
1362
|
}
|
|
536
1363
|
|
|
537
1364
|
const contentType = (
|
|
@@ -550,7 +1377,40 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
550
1377
|
`[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
551
1378
|
);
|
|
552
1379
|
}
|
|
553
|
-
return models;
|
|
1380
|
+
return observed(models, "degraded");
|
|
1381
|
+
}
|
|
1382
|
+
const antigravity = cloudCodeAssist
|
|
1383
|
+
? parseAntigravityAvailableModels(bounded.value, discovery.maxModels)
|
|
1384
|
+
: undefined;
|
|
1385
|
+
if (cloudCodeAssist && !antigravity) {
|
|
1386
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1387
|
+
if (shouldLog) {
|
|
1388
|
+
console.warn(
|
|
1389
|
+
`[opencodex] Provider model discovery for "${name}" returned malformed CCA model data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
return observed(models, "degraded");
|
|
1393
|
+
}
|
|
1394
|
+
if (antigravity) {
|
|
1395
|
+
const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
|
|
1396
|
+
id: model.id,
|
|
1397
|
+
provider: name,
|
|
1398
|
+
// CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex
|
|
1399
|
+
// effort-to-wire mapping for a newly discovered model, do not advertise a false ladder.
|
|
1400
|
+
reasoningEfforts: [],
|
|
1401
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
1402
|
+
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
1403
|
+
}, contextCap));
|
|
1404
|
+
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
1405
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1406
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
1407
|
+
}
|
|
1408
|
+
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
|
|
1409
|
+
provider: name,
|
|
1410
|
+
cacheGeneration,
|
|
1411
|
+
});
|
|
1412
|
+
markProviderDiscoveryOk(name, live.length);
|
|
1413
|
+
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
554
1414
|
}
|
|
555
1415
|
const extracted = extractProviderModelItems(bounded.value, discovery);
|
|
556
1416
|
if (!extracted.ok) {
|
|
@@ -566,7 +1426,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
566
1426
|
`[opencodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
567
1427
|
);
|
|
568
1428
|
}
|
|
569
|
-
return models;
|
|
1429
|
+
return observed(models, "degraded");
|
|
570
1430
|
}
|
|
571
1431
|
const items = extracted.items;
|
|
572
1432
|
const live = items.map(m => {
|
|
@@ -582,37 +1442,24 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
582
1442
|
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
|
|
583
1443
|
// `live`; otherwise configured entries would be reported as discovered ones.
|
|
584
1444
|
const liveModelCount = live.length;
|
|
585
|
-
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const dated = live.find(l => isDatedVariantId(l.id, m.id));
|
|
595
|
-
if (dated) {
|
|
596
|
-
// Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
|
|
597
|
-
live.push(applyProviderConfigHints(name, prov, { ...dated, id: m.id }, contextCap));
|
|
598
|
-
} else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
|
|
599
|
-
live.push(m);
|
|
600
|
-
} else {
|
|
601
|
-
droppedConfiguredIds.push(m.id);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (live.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
|
|
1445
|
+
// Dated-release aliases + configured retention (compat allow-list, combo targets,
|
|
1446
|
+
// Vertex default). Cache without combo retention so a later gather re-applies the
|
|
1447
|
+
// current capture's retain set on read (warm-cache OCX-111 / #1308).
|
|
1448
|
+
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
|
|
1449
|
+
const returned = withConfiguredRetention(forCache, { warnDrops: true });
|
|
1450
|
+
const droppedConfiguredIds = configured
|
|
1451
|
+
.map(model => model.id)
|
|
1452
|
+
.filter(id => !returned.some(model => model.id === id));
|
|
1453
|
+
if (returned.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
|
|
605
1454
|
console.warn(
|
|
606
1455
|
`[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
|
|
607
1456
|
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
1457
|
+
}
|
|
1458
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
|
|
1459
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
612
1460
|
}
|
|
613
1461
|
markProviderDiscoveryOk(name, liveModelCount);
|
|
614
|
-
|
|
615
|
-
return live;
|
|
1462
|
+
return observed(returned, "authoritative");
|
|
616
1463
|
} catch (error) {
|
|
617
1464
|
if (error instanceof ProviderOutboundPolicyError) {
|
|
618
1465
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
|
|
@@ -621,7 +1468,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
621
1468
|
`[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
622
1469
|
);
|
|
623
1470
|
}
|
|
624
|
-
return models;
|
|
1471
|
+
return observed(models, "degraded");
|
|
625
1472
|
}
|
|
626
1473
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" });
|
|
627
1474
|
if (shouldLog) {
|
|
@@ -629,10 +1476,25 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
629
1476
|
`[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
630
1477
|
);
|
|
631
1478
|
}
|
|
632
|
-
return models;
|
|
1479
|
+
return observed(models, "degraded");
|
|
633
1480
|
}
|
|
634
1481
|
}
|
|
635
1482
|
|
|
1483
|
+
export async function fetchProviderModels(
|
|
1484
|
+
name: string,
|
|
1485
|
+
prov: OcxProviderConfig,
|
|
1486
|
+
ttlMs: number,
|
|
1487
|
+
contextCap?: number,
|
|
1488
|
+
): Promise<CatalogModel[]> {
|
|
1489
|
+
const captured = captureProviderGather(name, prov, refreshingModelsAuthResolver);
|
|
1490
|
+
return (await fetchProviderModelsWithAuth(
|
|
1491
|
+
captured,
|
|
1492
|
+
ttlMs,
|
|
1493
|
+
contextCap,
|
|
1494
|
+
refreshingModelsAuthResolver,
|
|
1495
|
+
)).models;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
636
1498
|
export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
|
|
637
1499
|
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
638
1500
|
return true;
|
|
@@ -644,6 +1506,60 @@ export function shouldRetainConfiguredProviderModel(providerName: string, modelI
|
|
|
644
1506
|
return false;
|
|
645
1507
|
}
|
|
646
1508
|
|
|
1509
|
+
/**
|
|
1510
|
+
* Fold dated-release aliases and retain configured rows that must survive an
|
|
1511
|
+
* authoritative live roster (compatibility allow-list, combo targets, Vertex
|
|
1512
|
+
* default). Used on every discovery return — live, fresh cache, stale, and
|
|
1513
|
+
* failure fallback — so a warm cache captured before a combo existed still
|
|
1514
|
+
* surfaces the configured target (OCX-111 / #1308).
|
|
1515
|
+
*
|
|
1516
|
+
* Cache writes should pass `retainComboTargets: false` so combo retention is
|
|
1517
|
+
* re-applied on read against the current capture, not frozen into the TTL entry.
|
|
1518
|
+
*/
|
|
1519
|
+
export function mergeConfiguredModelsIntoLiveCatalog(opts: {
|
|
1520
|
+
name: string;
|
|
1521
|
+
provider: OcxProviderConfig;
|
|
1522
|
+
models: readonly CatalogModel[];
|
|
1523
|
+
configured: readonly CatalogModel[];
|
|
1524
|
+
retainConfiguredModelIds?: ReadonlySet<string>;
|
|
1525
|
+
contextCap?: number;
|
|
1526
|
+
seedVertexDefault?: boolean;
|
|
1527
|
+
retainComboTargets?: boolean;
|
|
1528
|
+
}): { models: CatalogModel[]; droppedConfiguredIds: string[] } {
|
|
1529
|
+
const {
|
|
1530
|
+
name,
|
|
1531
|
+
provider: prov,
|
|
1532
|
+
configured,
|
|
1533
|
+
retainConfiguredModelIds,
|
|
1534
|
+
contextCap,
|
|
1535
|
+
seedVertexDefault,
|
|
1536
|
+
retainComboTargets = true,
|
|
1537
|
+
} = opts;
|
|
1538
|
+
const out = [...opts.models];
|
|
1539
|
+
const present = new Set(out.map(model => model.id));
|
|
1540
|
+
const droppedConfiguredIds: string[] = [];
|
|
1541
|
+
for (const candidate of configured) {
|
|
1542
|
+
if (present.has(candidate.id)) continue;
|
|
1543
|
+
const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
|
|
1544
|
+
if (dated) {
|
|
1545
|
+
out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap));
|
|
1546
|
+
present.add(candidate.id);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (
|
|
1550
|
+
seedVertexDefault === true
|
|
1551
|
+
|| shouldRetainConfiguredProviderModel(name, candidate.id)
|
|
1552
|
+
|| (retainComboTargets && retainConfiguredModelIds?.has(candidate.id) === true)
|
|
1553
|
+
) {
|
|
1554
|
+
out.push(candidate);
|
|
1555
|
+
present.add(candidate.id);
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
droppedConfiguredIds.push(candidate.id);
|
|
1559
|
+
}
|
|
1560
|
+
return { models: out, droppedConfiguredIds };
|
|
1561
|
+
}
|
|
1562
|
+
|
|
647
1563
|
export function filterCatalogVisibleModels(
|
|
648
1564
|
models: CatalogModel[],
|
|
649
1565
|
config: Pick<OcxConfig, "disabledModels" | "providers">,
|
|
@@ -655,11 +1571,12 @@ export function filterCatalogVisibleModels(
|
|
|
655
1571
|
if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
|
|
656
1572
|
}
|
|
657
1573
|
return models.filter(m => {
|
|
1574
|
+
const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
|
|
658
1575
|
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
|
|
659
1576
|
for (const stored of disabled) {
|
|
660
1577
|
// Combo management stores the public alias, while canonical `combo/<id>` references
|
|
661
1578
|
// remain valid for backward compatibility through slugEquals below.
|
|
662
|
-
if (m.alias !== undefined && stored === catalogModelSlug(m)) return false;
|
|
1579
|
+
if (m.alias !== undefined && stored === catalogModelSlug(m) && !nativeAlias) return false;
|
|
663
1580
|
if (slugEquals(stored, m.provider, m.id)) return false;
|
|
664
1581
|
}
|
|
665
1582
|
const allow = allowByProvider.get(m.provider);
|
|
@@ -667,37 +1584,261 @@ export function filterCatalogVisibleModels(
|
|
|
667
1584
|
});
|
|
668
1585
|
}
|
|
669
1586
|
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Local-only model list for one provider: fresh TTL cache, else stale last-good,
|
|
1590
|
+
* else configured/static seeds. Never performs network I/O.
|
|
1591
|
+
*
|
|
1592
|
+
* `needsLive` is true when a background discovery should still run (no fresh
|
|
1593
|
+
* cache and not in failure cooldown). Cooling-down providers stay on their
|
|
1594
|
+
* fallback so a dead upstream cannot re-stall every management page load.
|
|
1595
|
+
*/
|
|
1596
|
+
export function peekProviderModelsLocal(
|
|
1597
|
+
name: string,
|
|
1598
|
+
prov: OcxProviderConfig,
|
|
1599
|
+
ttlMs: number,
|
|
1600
|
+
contextCap?: number,
|
|
1601
|
+
): { models: CatalogModel[]; needsLive: boolean } {
|
|
1602
|
+
if (prov.authMode === "forward") return { models: [], needsLive: false };
|
|
1603
|
+
|
|
1604
|
+
const seedVertexDefault = prov.adapter === "google"
|
|
1605
|
+
&& prov.googleMode === "vertex"
|
|
1606
|
+
&& (prov.models?.length ?? 0) === 0
|
|
1607
|
+
&& Boolean(prov.defaultModel);
|
|
1608
|
+
const configuredIds = seedVertexDefault && prov.defaultModel
|
|
1609
|
+
? [prov.defaultModel]
|
|
1610
|
+
: (prov.models ?? []);
|
|
1611
|
+
const configured: CatalogModel[] = configuredIds.map(id => ({
|
|
1612
|
+
id,
|
|
1613
|
+
provider: name,
|
|
1614
|
+
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
|
|
1615
|
+
}));
|
|
1616
|
+
|
|
1617
|
+
if (prov.liveModels === false) {
|
|
1618
|
+
return { models: configured, needsLive: false };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic"
|
|
1622
|
+
? configured
|
|
1623
|
+
: [{
|
|
1624
|
+
id: prov.defaultModel!,
|
|
1625
|
+
provider: name,
|
|
1626
|
+
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel!, contextCap),
|
|
1627
|
+
}];
|
|
1628
|
+
|
|
1629
|
+
const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
|
|
1630
|
+
const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
|
|
1631
|
+
vertexDefaultSeed && !models.some(model => model.id === vertexDefaultSeed.id)
|
|
1632
|
+
? [...models, vertexDefaultSeed]
|
|
1633
|
+
: models
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
const fresh = getFreshCached(name, ttlMs);
|
|
1637
|
+
if (fresh) {
|
|
1638
|
+
return {
|
|
1639
|
+
models: withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)),
|
|
1640
|
+
needsLive: false,
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (isModelsFetchCoolingDown(name)) {
|
|
1645
|
+
const cooling = getStaleCached(name);
|
|
1646
|
+
return {
|
|
1647
|
+
models: cooling
|
|
1648
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, cooling, contextCap))
|
|
1649
|
+
: failedDiscoveryConfigured,
|
|
1650
|
+
needsLive: false,
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const stale = getStaleCached(name);
|
|
1655
|
+
if (stale) {
|
|
1656
|
+
return {
|
|
1657
|
+
models: withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)),
|
|
1658
|
+
// Stale-while-revalidate: paint last-good now, refresh in the background.
|
|
1659
|
+
needsLive: true,
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
return { models: failedDiscoveryConfigured, needsLive: true };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/** True when any enabled live provider still needs a network `/models` probe. */
|
|
1667
|
+
export function catalogGatherNeedsLiveRefresh(config: OcxConfig): boolean {
|
|
1668
|
+
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
1669
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
1670
|
+
if (prov.disabled === true) continue;
|
|
1671
|
+
const enriched = { ...prov };
|
|
1672
|
+
enrichProviderFromRegistry(name, enriched);
|
|
1673
|
+
const { needsLive } = peekProviderModelsLocal(
|
|
1674
|
+
name,
|
|
1675
|
+
enriched,
|
|
1676
|
+
ttlMs,
|
|
1677
|
+
providerContextCap(config, name),
|
|
1678
|
+
);
|
|
1679
|
+
if (needsLive) return true;
|
|
1680
|
+
}
|
|
1681
|
+
return false;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/**
|
|
1685
|
+
* Fire-and-forget full gather. Shared so management fast paths kick one flight
|
|
1686
|
+
* without awaiting it (joiners still share gatherInflight).
|
|
1687
|
+
*/
|
|
1688
|
+
export function kickGatherRoutedModelsBackground(config: OcxConfig): void {
|
|
1689
|
+
void gatherRoutedModels(config).catch(() => {
|
|
1690
|
+
/* discovery status / cooldown already recorded per provider */
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
|
|
670
1694
|
export async function gatherRoutedModels(
|
|
671
1695
|
config: OcxConfig,
|
|
672
|
-
options?:
|
|
1696
|
+
options?: GatherRoutedModelsOptions,
|
|
673
1697
|
): Promise<CatalogModel[]> {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1698
|
+
if (options?.preferCached) {
|
|
1699
|
+
const local = await gatherRoutedModelsLocal(config);
|
|
1700
|
+
if (options.comboOmissions) {
|
|
1701
|
+
options.comboOmissions.length = 0;
|
|
1702
|
+
options.comboOmissions.push(...local.comboOmissions);
|
|
1703
|
+
}
|
|
1704
|
+
if (options.providerAuthOutcomes) {
|
|
1705
|
+
options.providerAuthOutcomes.length = 0;
|
|
1706
|
+
options.providerAuthOutcomes.push(...local.providerAuthOutcomes);
|
|
1707
|
+
}
|
|
1708
|
+
if (options.discoveryPolicySnapshots) {
|
|
1709
|
+
options.discoveryPolicySnapshots.length = 0;
|
|
1710
|
+
options.discoveryPolicySnapshots.push(...local.discoveryPolicySnapshots);
|
|
1711
|
+
}
|
|
1712
|
+
if (local.needsLive) kickGatherRoutedModelsBackground(config);
|
|
1713
|
+
return local.models;
|
|
1714
|
+
}
|
|
1715
|
+
return gatherRoutedModelsWithAuth(
|
|
1716
|
+
config,
|
|
1717
|
+
`refreshing:${gatherFlightKey(config)}`,
|
|
1718
|
+
() => refreshingModelsAuthResolver,
|
|
1719
|
+
options,
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Catalog-gather model discovery using only auth-store bytes already captured by the
|
|
1725
|
+
* filesystem-evidence owner. This entry point never reaches the refreshing resolver.
|
|
1726
|
+
*/
|
|
1727
|
+
export async function gatherRoutedModelsForCatalogGather(
|
|
1728
|
+
config: OcxConfig,
|
|
1729
|
+
evidence: CatalogGatherProviderAuthEvidence,
|
|
1730
|
+
options?: GatherRoutedModelsOptions,
|
|
1731
|
+
): Promise<CatalogModel[]> {
|
|
1732
|
+
const authStoreBuffer = evidence.authStoreBuffer === null
|
|
1733
|
+
? null
|
|
1734
|
+
: Uint8Array.from(evidence.authStoreBuffer);
|
|
1735
|
+
const authIdentity = authStoreBuffer === null
|
|
1736
|
+
? "absent"
|
|
1737
|
+
: keyedGatherBytesIdentity("catalog-observed-auth-v1", authStoreBuffer);
|
|
1738
|
+
return gatherRoutedModelsWithAuth(
|
|
1739
|
+
config,
|
|
1740
|
+
`observed:${authIdentity}:${gatherFlightKey(config)}`,
|
|
1741
|
+
outcomes => observedModelsAuthResolver(authStoreBuffer, outcomes),
|
|
1742
|
+
options,
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async function gatherRoutedModelsWithAuth(
|
|
1747
|
+
config: OcxConfig,
|
|
1748
|
+
key: string,
|
|
1749
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
1750
|
+
options?: GatherRoutedModelsOptions,
|
|
1751
|
+
): Promise<CatalogModel[]> {
|
|
1752
|
+
const capture = captureGatherFlight(config, createAuthResolver);
|
|
1753
|
+
const bucket = gatherInflight.get(key) ?? [];
|
|
1754
|
+
let entry = bucket.find(candidate => (
|
|
1755
|
+
candidate.discoveryPolicyIdentity === capture.discoveryPolicyIdentity
|
|
1756
|
+
&& candidate.authIdentity === capture.authIdentity
|
|
1757
|
+
&& candidate.providerGraphIdentity === capture.providerGraphIdentity
|
|
1758
|
+
));
|
|
1759
|
+
if (!entry) {
|
|
677
1760
|
const lease = gatherGate.tryAcquire();
|
|
678
1761
|
if (!lease) throw new CatalogGatherBusyError();
|
|
679
1762
|
// Claim the slot synchronously before any await so same-key callers join this flight.
|
|
680
|
-
// Distinct
|
|
681
|
-
|
|
682
|
-
|
|
1763
|
+
// Distinct authorities retain separate entries even when their legacy bucket matches.
|
|
1764
|
+
let ownedEntry!: GatherInflightEntry;
|
|
1765
|
+
const flight = gatherRoutedModelsUncached(config, capture).finally(() => {
|
|
1766
|
+
const current = gatherInflight.get(key);
|
|
1767
|
+
const index = current?.indexOf(ownedEntry) ?? -1;
|
|
1768
|
+
if (current && index >= 0) current.splice(index, 1);
|
|
1769
|
+
if (current?.length === 0) gatherInflight.delete(key);
|
|
683
1770
|
lease.release();
|
|
684
1771
|
});
|
|
685
|
-
|
|
686
|
-
|
|
1772
|
+
ownedEntry = Object.freeze({
|
|
1773
|
+
discoveryPolicyIdentity: capture.discoveryPolicyIdentity,
|
|
1774
|
+
authIdentity: capture.authIdentity,
|
|
1775
|
+
providerGraphIdentity: capture.providerGraphIdentity,
|
|
1776
|
+
promise: flight,
|
|
1777
|
+
});
|
|
1778
|
+
bucket.push(ownedEntry);
|
|
1779
|
+
gatherInflight.set(key, bucket);
|
|
1780
|
+
entry = ownedEntry;
|
|
687
1781
|
}
|
|
688
|
-
const {
|
|
1782
|
+
const {
|
|
1783
|
+
models,
|
|
1784
|
+
comboOmissions,
|
|
1785
|
+
providerAuthOutcomes,
|
|
1786
|
+
providerModelOutcomes,
|
|
1787
|
+
discoveryPolicySnapshots,
|
|
1788
|
+
} = await entry.promise;
|
|
689
1789
|
if (options?.comboOmissions) {
|
|
690
1790
|
options.comboOmissions.length = 0;
|
|
691
1791
|
options.comboOmissions.push(...comboOmissions);
|
|
692
1792
|
}
|
|
1793
|
+
if (options?.providerAuthOutcomes) {
|
|
1794
|
+
options.providerAuthOutcomes.length = 0;
|
|
1795
|
+
options.providerAuthOutcomes.push(...providerAuthOutcomes);
|
|
1796
|
+
}
|
|
1797
|
+
if (options?.providerModelOutcomes) {
|
|
1798
|
+
options.providerModelOutcomes.length = 0;
|
|
1799
|
+
options.providerModelOutcomes.push(...providerModelOutcomes);
|
|
1800
|
+
}
|
|
1801
|
+
if (options?.discoveryPolicySnapshots) {
|
|
1802
|
+
options.discoveryPolicySnapshots.length = 0;
|
|
1803
|
+
options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
|
|
1804
|
+
}
|
|
693
1805
|
return models;
|
|
694
1806
|
}
|
|
695
1807
|
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Management fast path: assemble from TTL/stale/configured seeds only.
|
|
1811
|
+
* Never hits the network for `/models`; callers may kick a background gather when needsLive.
|
|
1812
|
+
*/
|
|
1813
|
+
async function gatherRoutedModelsLocal(
|
|
1814
|
+
config: OcxConfig,
|
|
1815
|
+
): Promise<GatherFlightResult & { needsLive: boolean }> {
|
|
1816
|
+
const capture = captureGatherFlight(config, () => refreshingModelsAuthResolver);
|
|
1817
|
+
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
1818
|
+
let needsLive = false;
|
|
1819
|
+
const lists = capture.providers.map(provider => {
|
|
1820
|
+
const peeked = peekProviderModelsLocal(
|
|
1821
|
+
provider.name,
|
|
1822
|
+
provider.provider,
|
|
1823
|
+
ttlMs,
|
|
1824
|
+
providerContextCap(config, provider.name),
|
|
1825
|
+
);
|
|
1826
|
+
if (peeked.needsLive) needsLive = true;
|
|
1827
|
+
return peeked.models;
|
|
1828
|
+
});
|
|
1829
|
+
const assembled = await gatherRoutedModelsUncached(config, capture, lists);
|
|
1830
|
+
return { ...assembled, needsLive };
|
|
1831
|
+
}
|
|
1832
|
+
|
|
696
1833
|
async function gatherRoutedModelsUncached(
|
|
697
1834
|
config: OcxConfig,
|
|
1835
|
+
capture: GatherFlightCapture,
|
|
1836
|
+
precomputedLists?: CatalogModel[][],
|
|
698
1837
|
): Promise<GatherFlightResult> {
|
|
699
1838
|
// Flight-local list: joiners copy from the resolved promise, not a process-global last write.
|
|
700
1839
|
const localOmissions: ComboCatalogOmission[] = [];
|
|
1840
|
+
const localProviderAuthOutcomes = capture.providerAuthOutcomes;
|
|
1841
|
+
const resolveAuth = capture.authResolver;
|
|
701
1842
|
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
702
1843
|
// Persisted provider entries can predate newer registry fields (noVisionModels,
|
|
703
1844
|
// modelInputModalities, ...). The ROUTER merges registry seeds at request time
|
|
@@ -705,18 +1846,28 @@ async function gatherRoutedModelsUncached(
|
|
|
705
1846
|
// same merged view or its advertisements drift from actual proxy behavior (e.g. a
|
|
706
1847
|
// vision-sidecar model advertised text-only, blocking image attachments app-side).
|
|
707
1848
|
// Enrich a CLONE: hydrated defaults must never leak into the persisted config.
|
|
708
|
-
const activeProviders =
|
|
709
|
-
|
|
710
|
-
.map((
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
|
|
1849
|
+
const activeProviders = capture.providers;
|
|
1850
|
+
const providerResults = precomputedLists
|
|
1851
|
+
? activeProviders.map((provider, index) => ({
|
|
1852
|
+
models: precomputedLists[index] ?? [],
|
|
1853
|
+
// Local/SWR path: rows come from cache or config seeds, not a live probe.
|
|
1854
|
+
outcome: { provider: provider.name, state: "authoritative" as const },
|
|
1855
|
+
}))
|
|
1856
|
+
: await Promise.all(
|
|
1857
|
+
activeProviders.map(provider => fetchProviderModelsWithAuth(
|
|
1858
|
+
provider,
|
|
1859
|
+
ttlMs,
|
|
1860
|
+
providerContextCap(config, provider.name),
|
|
1861
|
+
resolveAuth,
|
|
1862
|
+
)),
|
|
1863
|
+
);
|
|
1864
|
+
const lists = providerResults.map(result => result.models);
|
|
1865
|
+
const apiAugmented = augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
1866
|
+
lists.flat(),
|
|
1867
|
+
config,
|
|
1868
|
+
capture.openAiApiPolicy,
|
|
717
1869
|
);
|
|
718
|
-
const
|
|
719
|
-
const all = augmentRoutedModelsWithJawcodeMetadata(apiAugmented, activeProviders.map(([name]) => name), config.providers, config)
|
|
1870
|
+
const all = augmentRoutedModelsWithMetadata(apiAugmented, activeProviders.map(provider => provider.name), config.providers, config)
|
|
720
1871
|
// Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
|
|
721
1872
|
// intentionally mirrors Cursor's public model table, including Gemini image preview, so the
|
|
722
1873
|
// exposure decision goes through shouldExposeRoutedModel (single choke point).
|
|
@@ -747,16 +1898,29 @@ async function gatherRoutedModelsUncached(
|
|
|
747
1898
|
// configs that will never need it.
|
|
748
1899
|
} else {
|
|
749
1900
|
const disabled = disabledNativeSlugs(config);
|
|
1901
|
+
const openaiContextCap = nativeContextLimits(config);
|
|
1902
|
+
const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => {
|
|
1903
|
+
const combo = getCombo(config, id);
|
|
1904
|
+
return combo?.targets.flatMap(target => (
|
|
1905
|
+
target.provider === "openai" ? [target.model] : []
|
|
1906
|
+
)) ?? [];
|
|
1907
|
+
}));
|
|
750
1908
|
for (const slug of nativeOpenAiSlugs()) {
|
|
751
|
-
|
|
752
|
-
|
|
1909
|
+
// A bare native disable key hides the native row, not a combo that targets it.
|
|
1910
|
+
// Keep synthetic native metadata available to those combos.
|
|
1911
|
+
if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue;
|
|
1912
|
+
const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
|
|
753
1913
|
if (contextWindow === undefined) continue;
|
|
754
1914
|
const synthetic: CatalogModel = {
|
|
755
1915
|
provider: "openai",
|
|
756
1916
|
id: slug,
|
|
757
1917
|
owned_by: "openai",
|
|
758
1918
|
contextWindow,
|
|
759
|
-
|
|
1919
|
+
// Input limit, not the total window. These coincide for native GPT-5.6 today (the
|
|
1920
|
+
// advertised 922,000 window is already capped at its measured ceiling), but the two
|
|
1921
|
+
// stay separate fields because routed/API rows of the same family run a wider window.
|
|
1922
|
+
// Falls back to the window for slugs with no separate ceiling.
|
|
1923
|
+
maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
|
|
760
1924
|
inputModalities: nativeInputModalities(slug),
|
|
761
1925
|
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
762
1926
|
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
|
@@ -767,51 +1931,200 @@ async function gatherRoutedModelsUncached(
|
|
|
767
1931
|
if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
|
|
768
1932
|
}
|
|
769
1933
|
}
|
|
1934
|
+
// Enriched (registry-hydrated) provider clones — shared by combo member synthesis and
|
|
1935
|
+
// custom-model vision-sidecar inheritance so both see the same merged registry view.
|
|
1936
|
+
const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider]));
|
|
770
1937
|
for (const id of listComboIds(config)) {
|
|
771
1938
|
const combo = getCombo(config, id);
|
|
772
1939
|
if (!combo) continue;
|
|
1940
|
+
const nativeContextWindow = combo.nativeAlias && combo.alias
|
|
1941
|
+
? nativeOpenAiContextWindow(combo.alias, nativeContextLimits(config))
|
|
1942
|
+
: undefined;
|
|
1943
|
+
const nativeAliasMaxInput = combo.nativeAlias && combo.alias
|
|
1944
|
+
? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
|
|
1945
|
+
? NATIVE_GPT56_MAX_INPUT_TOKENS
|
|
1946
|
+
: nativeOpenAiMaxInputTokens(combo.alias) ?? nativeOpenAiContextWindow(combo.alias))
|
|
1947
|
+
: undefined;
|
|
1948
|
+
const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
|
|
1949
|
+
? {
|
|
1950
|
+
contextWindow: nativeContextWindow,
|
|
1951
|
+
...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}),
|
|
1952
|
+
inputModalities: nativeInputModalities(combo.alias),
|
|
1953
|
+
reasoningEfforts: nativeReasoningEfforts(combo.alias),
|
|
1954
|
+
}
|
|
1955
|
+
: undefined;
|
|
773
1956
|
const members = combo.targets
|
|
774
|
-
.map(target =>
|
|
1957
|
+
.map(target => resolveComboCatalogMember(
|
|
1958
|
+
target,
|
|
1959
|
+
memberByKey,
|
|
1960
|
+
enrichedByName,
|
|
1961
|
+
providerContextCap(config, target.provider),
|
|
1962
|
+
nativeAliasFallback,
|
|
1963
|
+
))
|
|
775
1964
|
.filter((member): member is CatalogModel => member !== undefined);
|
|
776
1965
|
const derived = deriveComboCatalogModel(id, combo, members);
|
|
777
|
-
if (derived)
|
|
1966
|
+
if (derived) {
|
|
1967
|
+
const nativeDefault = combo.nativeAlias && combo.alias
|
|
1968
|
+
? nativeDefaultReasoningEffort(combo.alias)
|
|
1969
|
+
: undefined;
|
|
1970
|
+
if (combo.defaultEffort === null
|
|
1971
|
+
&& nativeDefault
|
|
1972
|
+
&& derived.reasoningEfforts?.includes(nativeDefault)) {
|
|
1973
|
+
derived.defaultReasoningEffort = nativeDefault;
|
|
1974
|
+
}
|
|
1975
|
+
all.push(derived);
|
|
1976
|
+
}
|
|
778
1977
|
else warnUncataloguedComboOnce(id, combo, members, localOmissions);
|
|
779
1978
|
}
|
|
780
1979
|
replaceLastComboCatalogOmissions(localOmissions);
|
|
781
1980
|
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
const
|
|
1981
|
+
// Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row
|
|
1982
|
+
// with the same slug below, so that row's provider capability metadata is the inheritance source.
|
|
1983
|
+
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
|
|
785
1984
|
const customModels = (config.customModels ?? []).map(cm => {
|
|
786
1985
|
const rawProvider = config.providers[cm.provider];
|
|
1986
|
+
const effectiveProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
1987
|
+
// Registry routing backfills an omitted authMode on the built-in OpenAI provider to
|
|
1988
|
+
// forward. Keep the catalog projection on the same contract while still failing closed
|
|
1989
|
+
// for every explicit non-forward mode and every non-canonical endpoint.
|
|
1990
|
+
const providerForCanonicalCheck = rawProvider
|
|
1991
|
+
? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider)
|
|
1992
|
+
: undefined;
|
|
1993
|
+
const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID
|
|
1994
|
+
&& providerForCanonicalCheck !== undefined
|
|
1995
|
+
&& isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)
|
|
1996
|
+
&& isNativeOpenAiCapabilityAliasModel(cm.modelId);
|
|
1997
|
+
const customNativeLimits = {
|
|
1998
|
+
...nativeContextLimits(config),
|
|
1999
|
+
...(typeof cm.contextWindow === "number" && cm.contextWindow > 0
|
|
2000
|
+
? { modelWindows: { ...(nativeContextLimits(config).modelWindows ?? {}), [cm.modelId]: cm.contextWindow } }
|
|
2001
|
+
: {}),
|
|
2002
|
+
};
|
|
2003
|
+
const nativeAliasContextWindow = codexForwardNativeCapabilityAlias
|
|
2004
|
+
? nativeOpenAiContextWindow(cm.modelId, customNativeLimits)
|
|
2005
|
+
: undefined;
|
|
2006
|
+
const customContextWindow = cm.contextWindow
|
|
2007
|
+
? nativeAliasContextWindow !== undefined
|
|
2008
|
+
? nativeAliasContextWindow
|
|
2009
|
+
: cm.contextWindow
|
|
2010
|
+
: nativeAliasContextWindow;
|
|
2011
|
+
const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
|
|
2012
|
+
? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits)
|
|
2013
|
+
: undefined;
|
|
2014
|
+
const customMaxInputTokens = nativeAliasMaxInputTokens !== undefined && customContextWindow !== undefined
|
|
2015
|
+
? Math.min(nativeAliasMaxInputTokens, customContextWindow)
|
|
2016
|
+
: nativeAliasMaxInputTokens;
|
|
2017
|
+
const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
|
|
2018
|
+
? nativeDefaultReasoningEffort(cm.modelId)
|
|
2019
|
+
: undefined;
|
|
787
2020
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
|
|
2021
|
+
const fastPolicy = effectiveProvider
|
|
2022
|
+
? fastPolicyForModel(effectiveProvider, cm.modelId, cm.provider)
|
|
2023
|
+
: undefined;
|
|
2024
|
+
const supportsServiceTier = fastPolicy
|
|
2025
|
+
? serviceTierSupportFromPolicy(fastPolicy)
|
|
2026
|
+
: undefined;
|
|
788
2027
|
const base: CatalogModel = {
|
|
789
2028
|
id: cm.modelId,
|
|
790
2029
|
provider: cm.provider,
|
|
2030
|
+
catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
791
2031
|
// Display-only label: never feeds routing (customModels are keyed by routedSlug below).
|
|
792
|
-
...(cm.displayName
|
|
793
|
-
|
|
794
|
-
|
|
2032
|
+
...(cm.displayName
|
|
2033
|
+
? { displayName: cm.displayName }
|
|
2034
|
+
: codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
|
|
2035
|
+
...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
|
|
2036
|
+
...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
|
|
2037
|
+
...(cm.inputModalities
|
|
2038
|
+
? { inputModalities: cm.inputModalities }
|
|
2039
|
+
: codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
|
|
795
2040
|
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
2041
|
+
// Native-alias defaults apply only where the custom row declares nothing: the explicit
|
|
2042
|
+
// spreads below must win (later in object order), so a stored `[]` stays empty and a
|
|
2043
|
+
// declared ladder is never replaced by the alias's native ladder.
|
|
2044
|
+
...(codexForwardNativeCapabilityAlias
|
|
2045
|
+
? {
|
|
2046
|
+
codexForwardNativeCapabilityAlias: true,
|
|
2047
|
+
parallelToolCalls: nativeParallelToolCalls(cm.modelId),
|
|
2048
|
+
...(Array.isArray(cm.reasoningEfforts)
|
|
2049
|
+
? {}
|
|
2050
|
+
: {
|
|
2051
|
+
reasoningEfforts: nativeReasoningEfforts(cm.modelId),
|
|
2052
|
+
...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
|
|
2053
|
+
}),
|
|
2054
|
+
}
|
|
2055
|
+
: {}),
|
|
2056
|
+
// Explicit custom-row ladder wins over the inherited provider row below: the merge only
|
|
2057
|
+
// gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept
|
|
2058
|
+
// verbatim instead of being replaced by the replaced row's metadata.
|
|
2059
|
+
...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}),
|
|
2060
|
+
...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}),
|
|
2061
|
+
...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
|
|
2062
|
+
...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined
|
|
2063
|
+
? { fastTierDescription: fastPolicy.fastTierDescription }
|
|
2064
|
+
: {}),
|
|
2065
|
+
...(cm.codexToolMode !== undefined
|
|
2066
|
+
? { codexToolMode: cm.codexToolMode }
|
|
2067
|
+
: effectiveProvider?.codexToolMode !== undefined
|
|
2068
|
+
? { codexToolMode: effectiveProvider.codexToolMode }
|
|
2069
|
+
: {}),
|
|
796
2070
|
};
|
|
2071
|
+
// #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
|
|
2072
|
+
// row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
|
|
2073
|
+
// context, ...) so the generated catalog keeps advertising what the router actually provides.
|
|
2074
|
+
// Explicit custom fields win by construction; this only fills gaps. Without it a
|
|
2075
|
+
// noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one,
|
|
2076
|
+
// which Codex then rejects for spawn_agent with effort "none".
|
|
2077
|
+
const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
|
|
2078
|
+
// The final ladder is what the catalog will advertise; the inherited default only rides
|
|
2079
|
+
// along when it is actually a member — otherwise a provider default like "xhigh" would
|
|
2080
|
+
// re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
|
|
2081
|
+
const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
|
|
2082
|
+
const merged: CatalogModel = replaced ? {
|
|
2083
|
+
...base,
|
|
2084
|
+
...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
|
|
2085
|
+
...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
|
|
2086
|
+
...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
|
|
2087
|
+
...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
|
|
2088
|
+
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
|
|
2089
|
+
&& Array.isArray(effectiveLadder) && effectiveLadder.includes(replaced.defaultReasoningEffort)
|
|
2090
|
+
? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
|
|
2091
|
+
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
|
|
2092
|
+
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
|
|
2093
|
+
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
|
|
2094
|
+
...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}),
|
|
2095
|
+
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
2096
|
+
} : base;
|
|
797
2097
|
// Vision-sidecar coverage ONLY: if the custom model is in the enriched provider's
|
|
798
2098
|
// noVisionModels, advertise image input so the Codex app lets images reach the sidecar
|
|
799
2099
|
// (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
|
|
800
2100
|
// user override, so their explicit contextWindow / inputModalities / reasoning fields must be
|
|
801
2101
|
// preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
|
|
802
2102
|
const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
803
|
-
if (enrichedProvider && modelInList(enrichedProvider.noVisionModels,
|
|
804
|
-
const current =
|
|
2103
|
+
if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
|
|
2104
|
+
const current = merged.inputModalities ?? ["text"];
|
|
805
2105
|
if (!current.includes("image")) {
|
|
806
|
-
return { ...
|
|
2106
|
+
return { ...merged, inputModalities: [...current, "image"] };
|
|
807
2107
|
}
|
|
808
2108
|
}
|
|
809
|
-
return
|
|
2109
|
+
return merged;
|
|
810
2110
|
});
|
|
811
2111
|
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
812
2112
|
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
813
2113
|
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
814
|
-
|
|
2114
|
+
const providerModelOutcomes = providerResults.map(result => (
|
|
2115
|
+
result.outcome.provider === OPENAI_API_PROVIDER_ID
|
|
2116
|
+
&& capture.openAiApiPolicy.state === "captured"
|
|
2117
|
+
&& capture.openAiApiPolicy.models !== undefined
|
|
2118
|
+
? { provider: result.outcome.provider, state: "authoritative" as const }
|
|
2119
|
+
: result.outcome
|
|
2120
|
+
));
|
|
2121
|
+
return {
|
|
2122
|
+
models: [...deduped, ...customModels],
|
|
2123
|
+
comboOmissions: localOmissions,
|
|
2124
|
+
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
2125
|
+
providerModelOutcomes,
|
|
2126
|
+
discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
|
|
2127
|
+
};
|
|
815
2128
|
}
|
|
816
2129
|
|
|
817
2130
|
export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
@@ -820,15 +2133,28 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
|
820
2133
|
): CatalogModel[] {
|
|
821
2134
|
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
822
2135
|
if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models;
|
|
823
|
-
|
|
824
|
-
|
|
2136
|
+
return augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
2137
|
+
models,
|
|
2138
|
+
config,
|
|
2139
|
+
captureTrustedOpenAiApiPolicy(OPENAI_API_PROVIDER_ID, true),
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
2144
|
+
models: CatalogModel[],
|
|
2145
|
+
config: OcxConfig,
|
|
2146
|
+
policy: CatalogTrustedOpenAiApiPolicySnapshot,
|
|
2147
|
+
): CatalogModel[] {
|
|
2148
|
+
if (policy.state !== "captured" || !policy.models) return models;
|
|
2149
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
2150
|
+
if (!configured || configured.disabled === true) return models;
|
|
825
2151
|
|
|
826
2152
|
const existingById = new Map(
|
|
827
2153
|
models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]),
|
|
828
2154
|
);
|
|
829
|
-
const trustedRows =
|
|
830
|
-
const officialContext =
|
|
831
|
-
const officialMaxInput =
|
|
2155
|
+
const trustedRows = policy.models.map((id): CatalogModel => {
|
|
2156
|
+
const officialContext = policy.modelContextWindows?.[id];
|
|
2157
|
+
const officialMaxInput = policy.modelMaxInputTokens?.[id];
|
|
832
2158
|
const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow;
|
|
833
2159
|
const userMaxInput = configured.modelMaxInputTokens?.[id];
|
|
834
2160
|
const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID);
|
|
@@ -844,8 +2170,8 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
|
844
2170
|
owned_by: OPENAI_API_PROVIDER_ID,
|
|
845
2171
|
...(contextWindow ? { contextWindow } : {}),
|
|
846
2172
|
...(maxInputTokens ? { maxInputTokens } : {}),
|
|
847
|
-
...(
|
|
848
|
-
...(
|
|
2173
|
+
...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
|
|
2174
|
+
...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
|
|
849
2175
|
};
|
|
850
2176
|
});
|
|
851
2177
|
|
|
@@ -867,7 +2193,7 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
|
867
2193
|
];
|
|
868
2194
|
}
|
|
869
2195
|
|
|
870
|
-
export function
|
|
2196
|
+
export function augmentRoutedModelsWithMetadata(
|
|
871
2197
|
models: CatalogModel[],
|
|
872
2198
|
providerNames: string[],
|
|
873
2199
|
providers?: Record<string, OcxProviderConfig>,
|
|
@@ -878,9 +2204,9 @@ export function augmentRoutedModelsWithJawcodeMetadata(
|
|
|
878
2204
|
for (const provider of providerNames) {
|
|
879
2205
|
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
880
2206
|
if (providers?.[provider]?.liveModels === false) continue;
|
|
881
|
-
const jawcodeProvider =
|
|
2207
|
+
const jawcodeProvider = resolveMetadataProvider(provider);
|
|
882
2208
|
if (!jawcodeProvider) continue;
|
|
883
|
-
for (const meta of
|
|
2209
|
+
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
884
2210
|
const key = `${provider}/${meta.id}`;
|
|
885
2211
|
if (seen.has(key)) continue;
|
|
886
2212
|
seen.add(key);
|