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