@remodex/rmx 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +348 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,2267 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { atomicWriteFile, expandUserPath, getConfigDir, resolveEnvValue, websocketsEnabled } from "../../config";
|
|
6
|
+
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
|
+
import {
|
|
8
|
+
clearModelCache,
|
|
9
|
+
clearProviderDiscoveryStatus,
|
|
10
|
+
captureModelCacheGeneration,
|
|
11
|
+
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
12
|
+
getFreshCached,
|
|
13
|
+
getStaleCached,
|
|
14
|
+
isModelsFetchCoolingDown,
|
|
15
|
+
isModelCacheGenerationCurrent,
|
|
16
|
+
markModelsFetchFailure,
|
|
17
|
+
markProviderDiscoveryFailed,
|
|
18
|
+
markProviderDiscoveryOk,
|
|
19
|
+
shouldLogDiscoveryFailure,
|
|
20
|
+
setCached,
|
|
21
|
+
type ProviderModelDiscoveryFailure,
|
|
22
|
+
} from "../model-cache";
|
|
23
|
+
import {
|
|
24
|
+
buildModelsRequest,
|
|
25
|
+
getValidAccessTokenSnapshot,
|
|
26
|
+
observeActiveOAuthAccessToken,
|
|
27
|
+
resolveModelsAuthToken,
|
|
28
|
+
type OAuthActiveTokenObservation,
|
|
29
|
+
} from "../../oauth";
|
|
30
|
+
import type { OcxConfig, OcxProviderConfig, ReasoningControl } from "../../types";
|
|
31
|
+
import { modelInList } from "../../types";
|
|
32
|
+
import { modelSourceVisible } from "../../model-sources";
|
|
33
|
+
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
34
|
+
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
35
|
+
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
36
|
+
import {
|
|
37
|
+
effectiveGoogleMode,
|
|
38
|
+
getProviderRegistryEntry,
|
|
39
|
+
providerMatchesRegistryTransport,
|
|
40
|
+
registryEntryForProviderDestination,
|
|
41
|
+
} from "../../providers/registry";
|
|
42
|
+
import {
|
|
43
|
+
bundledReasoningCapability,
|
|
44
|
+
resolveRoutedModelReasoningCapability,
|
|
45
|
+
type ReasoningCapabilityProvider,
|
|
46
|
+
} from "../../providers/reasoning-capabilities";
|
|
47
|
+
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
|
|
48
|
+
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
49
|
+
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
50
|
+
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
51
|
+
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
52
|
+
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
53
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
54
|
+
import {
|
|
55
|
+
COMBO_NAMESPACE,
|
|
56
|
+
comboModelId,
|
|
57
|
+
getCombo,
|
|
58
|
+
listComboIds,
|
|
59
|
+
targetKey,
|
|
60
|
+
} from "../../combos";
|
|
61
|
+
import type { NormalizedComboConfig } from "../../combos/types";
|
|
62
|
+
import {
|
|
63
|
+
ProviderOutboundPolicyError,
|
|
64
|
+
providerOutboundGet,
|
|
65
|
+
providerOutboundPost,
|
|
66
|
+
providerRedirectError,
|
|
67
|
+
} from "../../lib/provider-outbound";
|
|
68
|
+
import { redactSecretString } from "../../lib/redact";
|
|
69
|
+
import {
|
|
70
|
+
extractProviderModelItems,
|
|
71
|
+
readBoundedDiscoveryJson,
|
|
72
|
+
resolveProviderModelDiscovery,
|
|
73
|
+
type ModelDiscoveryResponseFailure,
|
|
74
|
+
type ProviderModelsApiItem,
|
|
75
|
+
type ResolvedProviderModelDiscovery,
|
|
76
|
+
} from "../../providers/model-discovery";
|
|
77
|
+
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
78
|
+
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
|
|
82
|
+
import type { CatalogModel, CatalogServiceTier } from "./parsing";
|
|
83
|
+
import { disabledNativeSlugs, hasComboTargets, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
|
|
84
|
+
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
85
|
+
import type { ComboCatalogOmission } from "./aggregation";
|
|
86
|
+
import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
87
|
+
import type {
|
|
88
|
+
CatalogAdmissionSnapshot,
|
|
89
|
+
CatalogDiscoveryPolicyField,
|
|
90
|
+
CatalogGatherAuthorityIdentity,
|
|
91
|
+
CatalogProviderDiscoveryPolicySnapshot,
|
|
92
|
+
CatalogProcessLocalEvidence,
|
|
93
|
+
CatalogSourceEvidence,
|
|
94
|
+
CatalogTrustedOpenAiApiPolicySnapshot,
|
|
95
|
+
} from "../convergence-types";
|
|
96
|
+
|
|
97
|
+
export type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
|
|
98
|
+
|
|
99
|
+
/** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
|
|
100
|
+
* Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
|
|
101
|
+
export interface CatalogGatherProviderAuthOutcome {
|
|
102
|
+
readonly provider: string;
|
|
103
|
+
readonly state: OAuthActiveTokenObservation["kind"];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CatalogGatherProviderModelOutcome {
|
|
107
|
+
readonly provider: string;
|
|
108
|
+
readonly state: "authoritative" | "degraded";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface GatherRoutedModelsOptions {
|
|
112
|
+
comboOmissions?: ComboCatalogOmission[];
|
|
113
|
+
providerAuthOutcomes?: CatalogGatherProviderAuthOutcome[];
|
|
114
|
+
/** Flight-local authority of each provider's returned model rows. */
|
|
115
|
+
providerModelOutcomes?: CatalogGatherProviderModelOutcome[];
|
|
116
|
+
/** Internal convergence sink for the immutable policy that produced the returned rows. */
|
|
117
|
+
discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface GatherFlightResult {
|
|
121
|
+
models: CatalogModel[];
|
|
122
|
+
comboOmissions: ComboCatalogOmission[];
|
|
123
|
+
providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
124
|
+
providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[];
|
|
125
|
+
discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface ProviderModelsResult {
|
|
129
|
+
readonly models: CatalogModel[];
|
|
130
|
+
readonly outcome: CatalogGatherProviderModelOutcome;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface ModelsAuthResolution {
|
|
134
|
+
readonly apiKey: string | undefined;
|
|
135
|
+
readonly observed: boolean;
|
|
136
|
+
readonly oauthApiBaseUrl?: string;
|
|
137
|
+
readonly oauthProjectId?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type ModelsAuthResolver =
|
|
141
|
+
| { readonly kind: "refreshing" }
|
|
142
|
+
| {
|
|
143
|
+
readonly kind: "observed";
|
|
144
|
+
readonly resolve: (name: string, provider: OcxProviderConfig) => ModelsAuthResolution;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ModelsAuthResolverFactory = (
|
|
148
|
+
outcomes: CatalogGatherProviderAuthOutcome[],
|
|
149
|
+
) => ModelsAuthResolver;
|
|
150
|
+
|
|
151
|
+
interface CapturedModelsRequest {
|
|
152
|
+
readonly method: "GET" | "POST";
|
|
153
|
+
readonly url: string;
|
|
154
|
+
readonly headersWithoutCredential: Readonly<Record<string, string>>;
|
|
155
|
+
readonly headersWithCredential: Readonly<Record<string, string>>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface CapturedProviderGather {
|
|
159
|
+
readonly name: string;
|
|
160
|
+
/** Persisted/user provider row before registry hydration; required for capability precedence. */
|
|
161
|
+
readonly configuredProvider: OcxProviderConfig;
|
|
162
|
+
/** Registry reasoning snapshot admitted before any outbound await. */
|
|
163
|
+
readonly reasoningRegistry?: ReasoningCapabilityProvider;
|
|
164
|
+
readonly provider: OcxProviderConfig;
|
|
165
|
+
readonly discovery: ResolvedProviderModelDiscovery;
|
|
166
|
+
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
|
|
167
|
+
readonly request: CapturedModelsRequest;
|
|
168
|
+
readonly observedAuth?: ModelsAuthResolution;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface GatherFlightCapture {
|
|
172
|
+
readonly discoveryPolicyIdentity: string;
|
|
173
|
+
readonly authIdentity: string;
|
|
174
|
+
readonly providerGraphIdentity: string;
|
|
175
|
+
readonly discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[];
|
|
176
|
+
readonly providers: readonly CapturedProviderGather[];
|
|
177
|
+
readonly authResolver: ModelsAuthResolver;
|
|
178
|
+
readonly providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[];
|
|
179
|
+
readonly openAiApiPolicy: CatalogTrustedOpenAiApiPolicySnapshot;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface GatherInflightEntry {
|
|
183
|
+
readonly discoveryPolicyIdentity: string;
|
|
184
|
+
/**
|
|
185
|
+
* The credential half of the join decision.
|
|
186
|
+
*
|
|
187
|
+
* `gatherFlightKey`'s fingerprint carries endpoints and model lists but no
|
|
188
|
+
* `authMode`, key or headers, and discovery policy does not carry them either.
|
|
189
|
+
* Two admissions differing ONLY in credential therefore produced the same key
|
|
190
|
+
* and the same policy, so the second joined the first and published rows the
|
|
191
|
+
* old key had fetched — reproduced against the real routes by rotating a key
|
|
192
|
+
* through `/api/providers/keys` mid-flight.
|
|
193
|
+
*
|
|
194
|
+
* Now REDUNDANT with `providerGraphIdentity`, which hashes the whole provider
|
|
195
|
+
* row and therefore covers `apiKey` too: removing this term alone leaves the
|
|
196
|
+
* credential regression green. It is kept deliberately, for two reasons. It
|
|
197
|
+
* covers what the graph cannot — the RESOLVED auth (`observedAuth`) and the
|
|
198
|
+
* final materialized headers, which are derived rather than stored, so an
|
|
199
|
+
* OAuth token that changes while the row is byte-identical still separates
|
|
200
|
+
* admissions. And it states the credential rule where a reader looks for it,
|
|
201
|
+
* instead of leaving it as an emergent property of hashing everything.
|
|
202
|
+
*/
|
|
203
|
+
readonly authIdentity: string;
|
|
204
|
+
/**
|
|
205
|
+
* The whole admitted provider graph, not a chosen subset.
|
|
206
|
+
*
|
|
207
|
+
* `providerCatalogFingerprint` is an ALLOW-LIST, so every field it forgot was
|
|
208
|
+
* silently treated as equivalence: credentials leaked a flight until
|
|
209
|
+
* `authIdentity` landed, and `reasoningEfforts` leaked one after that — both
|
|
210
|
+
* reproduced against real routes. Enumerating fields cannot converge, because
|
|
211
|
+
* the next field added to a provider row inherits the same defect. This
|
|
212
|
+
* identity therefore covers the enriched, frozen provider objects the flight
|
|
213
|
+
* actually gathered from, so a join is refused unless the admissions agree on
|
|
214
|
+
* everything rather than on everything somebody remembered to list.
|
|
215
|
+
*/
|
|
216
|
+
readonly providerGraphIdentity: string;
|
|
217
|
+
readonly promise: Promise<GatherFlightResult>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const gatherInflight = new Map<string, GatherInflightEntry[]>();
|
|
221
|
+
const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32);
|
|
222
|
+
const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`;
|
|
223
|
+
const MAX_CONCURRENT_CATALOG_GATHERS = 8;
|
|
224
|
+
const gatherGate = createAdmissionGate("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
|
|
225
|
+
|
|
226
|
+
export class CatalogGatherBusyError extends ResourceAdmissionError {
|
|
227
|
+
override readonly code = "catalog_busy";
|
|
228
|
+
readonly retryAfterSeconds = 1;
|
|
229
|
+
constructor() {
|
|
230
|
+
super("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS);
|
|
231
|
+
this.name = "CatalogGatherBusyError";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function catalogGatherAdmissionMetrics(): AdmissionMetrics {
|
|
236
|
+
return gatherGate.metrics();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function stableJson(value: unknown): string {
|
|
240
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
241
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
242
|
+
return Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)));
|
|
243
|
+
}
|
|
244
|
+
return nested;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function framed(value: string): string {
|
|
249
|
+
return `${Buffer.byteLength(value, "utf8")}:${value}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function canonicalAuthorityEncoding(value: unknown): string {
|
|
253
|
+
if (value === null) return "null";
|
|
254
|
+
if (value === undefined) return "undefined";
|
|
255
|
+
if (typeof value === "string") return `string${framed(value)}`;
|
|
256
|
+
if (typeof value === "boolean") return value ? "boolean1" : "boolean0";
|
|
257
|
+
if (typeof value === "number") {
|
|
258
|
+
if (!Number.isFinite(value)) throw new TypeError("Catalog authority cannot encode a non-finite number.");
|
|
259
|
+
const encoded = Object.is(value, -0) ? "-0" : String(value);
|
|
260
|
+
return `number${framed(encoded)}`;
|
|
261
|
+
}
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
return `array${value.length}:${value.map(item => framed(canonicalAuthorityEncoding(item))).join("")}`;
|
|
264
|
+
}
|
|
265
|
+
if (typeof value === "object") {
|
|
266
|
+
const record = value as Record<string, unknown>;
|
|
267
|
+
const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
|
|
268
|
+
return `object${keys.length}:${keys.map(key => (
|
|
269
|
+
`${framed(key)}${framed(canonicalAuthorityEncoding(record[key]))}`
|
|
270
|
+
)).join("")}`;
|
|
271
|
+
}
|
|
272
|
+
throw new TypeError(`Catalog authority cannot encode ${typeof value}.`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function keyedGatherIdentity(domain: string, value: unknown): string {
|
|
276
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
277
|
+
.update(framed(domain))
|
|
278
|
+
.update(framed(canonicalAuthorityEncoding(value)))
|
|
279
|
+
.digest("hex");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function keyedGatherBytesIdentity(domain: string, value: Uint8Array): string {
|
|
283
|
+
return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY)
|
|
284
|
+
.update(framed(domain))
|
|
285
|
+
.update(`${value.byteLength}:`)
|
|
286
|
+
.update(value)
|
|
287
|
+
.digest("hex");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createCatalogGatherAuthorityIdentity(
|
|
291
|
+
snapshot: CatalogAdmissionSnapshot,
|
|
292
|
+
sourceEvidence: CatalogSourceEvidence,
|
|
293
|
+
processLocal: CatalogProcessLocalEvidence,
|
|
294
|
+
discoveryPolicies: readonly CatalogProviderDiscoveryPolicySnapshot[],
|
|
295
|
+
): CatalogGatherAuthorityIdentity {
|
|
296
|
+
const sourceEvidenceIdentity = keyedGatherIdentity("catalog-source-evidence-v1", sourceEvidence);
|
|
297
|
+
const processLocalEvidenceIdentity = keyedGatherIdentity("catalog-process-local-v1", processLocal);
|
|
298
|
+
const discoveryPolicyIdentity = keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicies);
|
|
299
|
+
return Object.freeze({
|
|
300
|
+
version: 1 as const,
|
|
301
|
+
authorityId: keyedGatherIdentity("catalog-authority-v1", {
|
|
302
|
+
admittedConfig: snapshot.configIdentity,
|
|
303
|
+
discoveryPolicyIdentity,
|
|
304
|
+
sourceEvidenceIdentity,
|
|
305
|
+
processLocalEvidenceIdentity,
|
|
306
|
+
}),
|
|
307
|
+
admittedConfig: Object.freeze({
|
|
308
|
+
...snapshot.configIdentity,
|
|
309
|
+
generation: Object.freeze({ ...snapshot.configIdentity.generation }),
|
|
310
|
+
}),
|
|
311
|
+
authSnapshotIdentity: keyedGatherIdentity(
|
|
312
|
+
"catalog-auth-v1",
|
|
313
|
+
sourceEvidence.conditional["provider-auth-selection"],
|
|
314
|
+
),
|
|
315
|
+
discoveryPolicyIdentity,
|
|
316
|
+
nativeCatalogSourceIdentity: keyedGatherIdentity(
|
|
317
|
+
"catalog-native-v1",
|
|
318
|
+
sourceEvidence.conditional["native-catalog-selection"],
|
|
319
|
+
),
|
|
320
|
+
sourceEvidenceIdentity,
|
|
321
|
+
processLocalEvidenceIdentity,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function detachedClone<T>(value: T): T {
|
|
326
|
+
if (Array.isArray(value)) return value.map(item => detachedClone(item)) as T;
|
|
327
|
+
if (value && typeof value === "object") {
|
|
328
|
+
const clone: Record<string, unknown> = {};
|
|
329
|
+
for (const key of Object.keys(value)) {
|
|
330
|
+
clone[key] = detachedClone((value as Record<string, unknown>)[key]);
|
|
331
|
+
}
|
|
332
|
+
return clone as T;
|
|
333
|
+
}
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function recursivelyFreeze<T>(value: T): T {
|
|
338
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
339
|
+
for (const nested of Object.values(value as Record<string, unknown>)) recursivelyFreeze(nested);
|
|
340
|
+
return Object.freeze(value);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function detachedFrozen<T>(value: T): T {
|
|
344
|
+
return recursivelyFreeze(detachedClone(value));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function capturedField<T extends object, K extends keyof T>(
|
|
348
|
+
value: T | undefined,
|
|
349
|
+
key: K,
|
|
350
|
+
): CatalogDiscoveryPolicyField<T[K]> {
|
|
351
|
+
if (!value || !Object.hasOwn(value, key)) return Object.freeze({ state: "absent" });
|
|
352
|
+
return detachedFrozen({ state: "present" as const, value: value[key] });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function captureTrustedOpenAiApiPolicy(
|
|
356
|
+
name: string,
|
|
357
|
+
registryTransportMatch: boolean,
|
|
358
|
+
): CatalogTrustedOpenAiApiPolicySnapshot {
|
|
359
|
+
if (name !== OPENAI_API_PROVIDER_ID) return Object.freeze({ state: "unused" });
|
|
360
|
+
if (!registryTransportMatch) return Object.freeze({ state: "transport-mismatch" });
|
|
361
|
+
const entry = getProviderRegistryEntry(name);
|
|
362
|
+
if (!entry?.models) return Object.freeze({ state: "registry-models-absent" });
|
|
363
|
+
return detachedFrozen({
|
|
364
|
+
state: "captured" as const,
|
|
365
|
+
models: entry.models,
|
|
366
|
+
...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}),
|
|
367
|
+
...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}),
|
|
368
|
+
...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}),
|
|
369
|
+
...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function captureModelsRequest(
|
|
374
|
+
name: string,
|
|
375
|
+
provider: OcxProviderConfig,
|
|
376
|
+
observedAuth: ModelsAuthResolution | undefined,
|
|
377
|
+
): CapturedModelsRequest {
|
|
378
|
+
const observed = observedAuth
|
|
379
|
+
? { oauthApiBaseUrl: observedAuth.oauthApiBaseUrl }
|
|
380
|
+
: undefined;
|
|
381
|
+
const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
|
|
382
|
+
const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
|
|
383
|
+
const method = withoutCredential.method ?? "GET";
|
|
384
|
+
if (withoutCredential.url !== withCredential.url || method !== (withCredential.method ?? "GET")) {
|
|
385
|
+
throw new TypeError(`Provider model discovery URL for ${name} depends on credential bytes.`);
|
|
386
|
+
}
|
|
387
|
+
return detachedFrozen({
|
|
388
|
+
method,
|
|
389
|
+
url: withoutCredential.url,
|
|
390
|
+
headersWithoutCredential: withoutCredential.headers,
|
|
391
|
+
headersWithCredential: withCredential.headers,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function captureProviderGather(
|
|
396
|
+
name: string,
|
|
397
|
+
configured: OcxProviderConfig,
|
|
398
|
+
authResolver: ModelsAuthResolver,
|
|
399
|
+
): CapturedProviderGather {
|
|
400
|
+
const namedRegistry = getProviderRegistryEntry(name);
|
|
401
|
+
const reasoningRegistry = namedRegistry && providerMatchesRegistryTransport(name, configured)
|
|
402
|
+
? detachedFrozen(namedRegistry)
|
|
403
|
+
: (() => {
|
|
404
|
+
const destination = registryEntryForProviderDestination(configured);
|
|
405
|
+
return destination ? detachedFrozen(destination) : undefined;
|
|
406
|
+
})();
|
|
407
|
+
const enriched = detachedClone(configured);
|
|
408
|
+
enrichProviderFromRegistry(name, enriched);
|
|
409
|
+
const provider = recursivelyFreeze(enriched);
|
|
410
|
+
const observedAuth = authResolver.kind === "observed"
|
|
411
|
+
&& provider.authMode !== "forward"
|
|
412
|
+
&& provider.liveModels !== false
|
|
413
|
+
? authResolver.resolve(name, provider)
|
|
414
|
+
: undefined;
|
|
415
|
+
const request = captureModelsRequest(name, provider, observedAuth);
|
|
416
|
+
const resolved = resolveProviderModelDiscovery(name, provider);
|
|
417
|
+
const discovery = detachedFrozen({
|
|
418
|
+
...(resolved.spec ? { spec: resolved.spec } : {}),
|
|
419
|
+
maxResponseBytes: resolved.maxResponseBytes,
|
|
420
|
+
maxModels: resolved.maxModels,
|
|
421
|
+
});
|
|
422
|
+
const registryTransportMatch = providerMatchesRegistryTransport(name, provider);
|
|
423
|
+
const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
|
|
424
|
+
const policy = detachedFrozen({
|
|
425
|
+
provider: name,
|
|
426
|
+
registryTransportMatch,
|
|
427
|
+
location: {
|
|
428
|
+
spec: discovery.spec ? "present" as const : "absent" as const,
|
|
429
|
+
url: capturedField(discovery.spec, "url"),
|
|
430
|
+
path: capturedField(discovery.spec, "path"),
|
|
431
|
+
query: capturedField(discovery.spec, "query"),
|
|
432
|
+
},
|
|
433
|
+
finalMethod: request.method,
|
|
434
|
+
finalUrl: request.url,
|
|
435
|
+
filter: capturedField(discovery.spec, "filter"),
|
|
436
|
+
maxResponseBytes: discovery.maxResponseBytes,
|
|
437
|
+
maxModels: discovery.maxModels,
|
|
438
|
+
trustedOpenAiApi,
|
|
439
|
+
});
|
|
440
|
+
return Object.freeze({
|
|
441
|
+
name,
|
|
442
|
+
configuredProvider: detachedFrozen(configured),
|
|
443
|
+
...(reasoningRegistry ? { reasoningRegistry } : {}),
|
|
444
|
+
provider,
|
|
445
|
+
discovery,
|
|
446
|
+
policy,
|
|
447
|
+
request,
|
|
448
|
+
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function captureGatherFlight(
|
|
453
|
+
config: OcxConfig,
|
|
454
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
455
|
+
): GatherFlightCapture {
|
|
456
|
+
const providerAuthOutcomes: CatalogGatherProviderAuthOutcome[] = [];
|
|
457
|
+
const authResolver = createAuthResolver(providerAuthOutcomes);
|
|
458
|
+
const providers = Object.entries(config.providers)
|
|
459
|
+
.filter(([, provider]) => provider.disabled !== true)
|
|
460
|
+
.map(([name, provider]) => captureProviderGather(name, provider, authResolver));
|
|
461
|
+
const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
|
|
462
|
+
return Object.freeze({
|
|
463
|
+
discoveryPolicyIdentity: keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicySnapshots),
|
|
464
|
+
// Credentials are hashed under the same unexported per-process key, never
|
|
465
|
+
// stored or compared in the clear: this value can reach a map key and must
|
|
466
|
+
// not disclose a token. The final headers are included because a static
|
|
467
|
+
// header can carry authority just as an `apiKey` can.
|
|
468
|
+
authIdentity: keyedGatherIdentity("catalog-gather-auth-v1", providers.map(provider => ({
|
|
469
|
+
name: provider.name,
|
|
470
|
+
authMode: provider.provider.authMode ?? null,
|
|
471
|
+
liveModels: provider.provider.liveModels ?? null,
|
|
472
|
+
credential: provider.provider.apiKey ?? null,
|
|
473
|
+
observedAuth: provider.observedAuth ?? null,
|
|
474
|
+
headers: provider.request.headersWithCredential,
|
|
475
|
+
url: provider.request.url,
|
|
476
|
+
}))),
|
|
477
|
+
// Every enriched provider row the flight will gather from, in admission order.
|
|
478
|
+
// Anything that can change a catalog row lives in here by construction.
|
|
479
|
+
providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1",
|
|
480
|
+
providers.map(provider => ({
|
|
481
|
+
name: provider.name,
|
|
482
|
+
// `fetch` is a caller-owned transport executor, not admitted state: the
|
|
483
|
+
// outbound transport honors it so a caller can supply its own HTTP path.
|
|
484
|
+
// It is the one member of a provider row that is legitimately a function,
|
|
485
|
+
// so it is dropped here rather than allowed to break every encode.
|
|
486
|
+
provider: omitProviderTransportExecutor(provider.provider),
|
|
487
|
+
reasoningRegistry: provider.reasoningRegistry ?? null,
|
|
488
|
+
}))),
|
|
489
|
+
discoveryPolicySnapshots,
|
|
490
|
+
providers: Object.freeze(providers),
|
|
491
|
+
authResolver,
|
|
492
|
+
providerAuthOutcomes: Object.freeze([...providerAuthOutcomes]),
|
|
493
|
+
openAiApiPolicy: providers.find(provider => provider.name === OPENAI_API_PROVIDER_ID)?.policy.trustedOpenAiApi
|
|
494
|
+
?? Object.freeze({ state: "unused" as const }),
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Drop the caller-owned transport executor before hashing a provider row.
|
|
500
|
+
*
|
|
501
|
+
* Fails closed on anything ELSE that cannot be encoded: the point of hashing the
|
|
502
|
+
* whole row is that no field escapes the comparison, so a second function member
|
|
503
|
+
* must surface as an encode error rather than being quietly skipped here.
|
|
504
|
+
*/
|
|
505
|
+
function omitProviderTransportExecutor(provider: OcxProviderConfig): Record<string, unknown> {
|
|
506
|
+
const entries = Object.entries(provider).filter(([key]) => key !== "fetch");
|
|
507
|
+
return Object.fromEntries(entries);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function materializeCapturedHeaders(
|
|
511
|
+
request: CapturedModelsRequest,
|
|
512
|
+
apiKey: string | undefined,
|
|
513
|
+
): Record<string, string> {
|
|
514
|
+
const source = apiKey ? request.headersWithCredential : request.headersWithoutCredential;
|
|
515
|
+
return Object.fromEntries(Object.entries(source).map(([name, value]) => [
|
|
516
|
+
name,
|
|
517
|
+
apiKey ? value.split(REQUEST_CREDENTIAL_SENTINEL).join(apiKey) : value,
|
|
518
|
+
]));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record<string, unknown> {
|
|
522
|
+
return {
|
|
523
|
+
n: name,
|
|
524
|
+
// Preserve the persisted tri-state. Registry enrichment may turn an omitted value into
|
|
525
|
+
// `false` while an explicit `true` stays live, so those callers must not share a flight.
|
|
526
|
+
live: prov.liveModels ?? null,
|
|
527
|
+
base: prov.baseUrl ?? "",
|
|
528
|
+
adapter: prov.adapter ?? "",
|
|
529
|
+
models: [...(prov.models ?? [])].sort(),
|
|
530
|
+
selected: [...(prov.selectedModels ?? [])].sort(),
|
|
531
|
+
defaultModel: prov.defaultModel ?? null,
|
|
532
|
+
ctx: prov.contextWindow ?? null,
|
|
533
|
+
ctxW: prov.modelContextWindows ?? null,
|
|
534
|
+
maxIn: prov.modelMaxInputTokens ?? null,
|
|
535
|
+
inMod: prov.modelInputModalities ?? null,
|
|
536
|
+
re: prov.modelReasoningEfforts ?? null,
|
|
537
|
+
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
538
|
+
reqRe: prov.modelReasoningRequired ?? null,
|
|
539
|
+
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
540
|
+
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
541
|
+
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
542
|
+
ptc: prov.parallelToolCalls ?? null,
|
|
543
|
+
gMode: prov.googleMode ?? null,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function gatherFlightKey(config: OcxConfig): string {
|
|
548
|
+
const providers = Object.entries(config.providers)
|
|
549
|
+
.filter(([, prov]) => prov.disabled !== true)
|
|
550
|
+
.map(([name, prov]) => providerCatalogFingerprint(name, prov))
|
|
551
|
+
.sort((a, b) => String(a.n).localeCompare(String(b.n)));
|
|
552
|
+
const assembly = stableJson({
|
|
553
|
+
providers,
|
|
554
|
+
disabledModels: [...(config.disabledModels ?? [])].sort(),
|
|
555
|
+
combos: config.combos ?? {},
|
|
556
|
+
customModels: (config.customModels ?? []).map((cm) => ({
|
|
557
|
+
p: cm.provider,
|
|
558
|
+
m: cm.modelId,
|
|
559
|
+
d: cm.displayName ?? null,
|
|
560
|
+
cw: cm.contextWindow ?? null,
|
|
561
|
+
im: cm.inputModalities ?? null,
|
|
562
|
+
})),
|
|
563
|
+
caps: config.providerContextCaps ?? null,
|
|
564
|
+
});
|
|
565
|
+
const digest = createHash("sha256").update(assembly).digest("hex").slice(0, 16);
|
|
566
|
+
return `${digest}#${config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS}`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */
|
|
570
|
+
export function clearGatherRoutedModelsInflight(): void {
|
|
571
|
+
gatherInflight.clear();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
|
|
575
|
+
const configured = modelRecordValue(prov.modelContextWindows, id) ?? prov.contextWindow;
|
|
576
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function configuredInputModalities(prov: OcxProviderConfig, id: string): string[] | undefined {
|
|
580
|
+
const modalities = modelRecordValue(prov.modelInputModalities, id);
|
|
581
|
+
return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined {
|
|
585
|
+
const configured = modelRecordValue(prov.modelMaxInputTokens, id);
|
|
586
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
|
|
590
|
+
if (!prov) return undefined;
|
|
591
|
+
const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
|
|
592
|
+
if (explicit !== undefined) return explicit;
|
|
593
|
+
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function applyProviderConfigHints(
|
|
597
|
+
name: string,
|
|
598
|
+
prov: OcxProviderConfig,
|
|
599
|
+
model: CatalogModel,
|
|
600
|
+
providerCap?: number,
|
|
601
|
+
configuredProv: OcxProviderConfig = prov,
|
|
602
|
+
reasoningRegistry: ReasoningCapabilityProvider | null | undefined = undefined,
|
|
603
|
+
): CatalogModel {
|
|
604
|
+
const configuredCap = configuredContextWindow(prov, model.id);
|
|
605
|
+
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
|
|
606
|
+
let inputModalities = configuredInputModalities(prov, model.id);
|
|
607
|
+
// Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
|
|
608
|
+
// (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
|
|
609
|
+
// gates attachments client-side on input_modalities, and a text-only entry would block images
|
|
610
|
+
// before the sidecar ever runs ("This model does not support image inputs").
|
|
611
|
+
if (modelInList(prov.noVisionModels, model.id)) {
|
|
612
|
+
const base = inputModalities ?? model.inputModalities ?? ["text"];
|
|
613
|
+
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
614
|
+
}
|
|
615
|
+
const registry = reasoningRegistry === undefined
|
|
616
|
+
? (() => {
|
|
617
|
+
const namedRegistry = getProviderRegistryEntry(name);
|
|
618
|
+
return namedRegistry && providerMatchesRegistryTransport(name, configuredProv)
|
|
619
|
+
? namedRegistry
|
|
620
|
+
: registryEntryForProviderDestination(configuredProv);
|
|
621
|
+
})()
|
|
622
|
+
: reasoningRegistry ?? undefined;
|
|
623
|
+
const reasoning = resolveRoutedModelReasoningCapability(
|
|
624
|
+
model.id,
|
|
625
|
+
configuredProv,
|
|
626
|
+
registry,
|
|
627
|
+
{
|
|
628
|
+
...(model.reasoningControl ? { control: model.reasoningControl } : {}),
|
|
629
|
+
...(model.reasoningEfforts !== undefined ? { efforts: model.reasoningEfforts } : {}),
|
|
630
|
+
...(model.defaultReasoningEffort ? { defaultEffort: model.defaultReasoningEffort } : {}),
|
|
631
|
+
...(typeof model.reasoningRequired === "boolean" ? { required: model.reasoningRequired } : {}),
|
|
632
|
+
},
|
|
633
|
+
bundledReasoningCapability(name, model.id, registry),
|
|
634
|
+
);
|
|
635
|
+
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
636
|
+
const hinted: CatalogModel = {
|
|
637
|
+
...model,
|
|
638
|
+
...(configuredCap !== undefined
|
|
639
|
+
? {
|
|
640
|
+
contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
|
|
641
|
+
? Math.min(model.contextWindow, configuredCap)
|
|
642
|
+
: configuredCap,
|
|
643
|
+
}
|
|
644
|
+
: {}),
|
|
645
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
646
|
+
reasoningControl: reasoning.control,
|
|
647
|
+
...(reasoning.efforts !== undefined ? { reasoningEfforts: reasoning.efforts } : {}),
|
|
648
|
+
...(reasoning.control.kind === "effort" || reasoning.control.kind === "automatic"
|
|
649
|
+
? { reasoningRequired: reasoning.required }
|
|
650
|
+
: {}),
|
|
651
|
+
...(configuredMaxInput !== undefined
|
|
652
|
+
? {
|
|
653
|
+
maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
654
|
+
? Math.min(model.maxInputTokens, configuredMaxInput)
|
|
655
|
+
: configuredMaxInput,
|
|
656
|
+
}
|
|
657
|
+
: {}),
|
|
658
|
+
...(reasoning.defaultEffort ? { defaultReasoningEffort: reasoning.defaultEffort } : {}),
|
|
659
|
+
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
660
|
+
...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
|
|
661
|
+
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
662
|
+
// advertise only on explicit opt-in.
|
|
663
|
+
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
664
|
+
? { parallelToolCalls: true }
|
|
665
|
+
: {}),
|
|
666
|
+
};
|
|
667
|
+
if (reasoning.efforts === undefined) delete hinted.reasoningEfforts;
|
|
668
|
+
if (!reasoning.defaultEffort) delete hinted.defaultReasoningEffort;
|
|
669
|
+
if (reasoning.control.kind !== "effort" && reasoning.control.kind !== "automatic") {
|
|
670
|
+
delete hinted.reasoningRequired;
|
|
671
|
+
}
|
|
672
|
+
const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
|
|
673
|
+
if (providerCap !== undefined && capped !== hinted.contextWindow) {
|
|
674
|
+
return { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true };
|
|
675
|
+
}
|
|
676
|
+
return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function catalogHintsFromProviderConfig(
|
|
680
|
+
name: string,
|
|
681
|
+
prov: OcxProviderConfig,
|
|
682
|
+
id: string,
|
|
683
|
+
contextCap?: number,
|
|
684
|
+
configuredProv: OcxProviderConfig = prov,
|
|
685
|
+
reasoningRegistry: ReasoningCapabilityProvider | null | undefined = undefined,
|
|
686
|
+
): Partial<CatalogModel> {
|
|
687
|
+
const hinted = applyProviderConfigHints(
|
|
688
|
+
name,
|
|
689
|
+
prov,
|
|
690
|
+
{ id, provider: name },
|
|
691
|
+
contextCap,
|
|
692
|
+
configuredProv,
|
|
693
|
+
reasoningRegistry,
|
|
694
|
+
);
|
|
695
|
+
const { provider: _provider, id: _id, ...hints } = hinted;
|
|
696
|
+
return hints;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function applyConfigHintsToCachedModels(
|
|
700
|
+
name: string,
|
|
701
|
+
prov: OcxProviderConfig,
|
|
702
|
+
models: CatalogModel[],
|
|
703
|
+
contextCap?: number,
|
|
704
|
+
configuredProv: OcxProviderConfig = prov,
|
|
705
|
+
reasoningRegistry: ReasoningCapabilityProvider | null | undefined = undefined,
|
|
706
|
+
): CatalogModel[] {
|
|
707
|
+
return models.map(model => applyProviderConfigHints(
|
|
708
|
+
name,
|
|
709
|
+
prov,
|
|
710
|
+
model,
|
|
711
|
+
contextCap,
|
|
712
|
+
configuredProv,
|
|
713
|
+
reasoningRegistry,
|
|
714
|
+
));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Last-resort context window for combo member synthesis when discovery and
|
|
720
|
+
* provider config both omit one. Matches the catalog entry default in
|
|
721
|
+
* `normalizeRoutedCatalogEntry` so incomplete live rows still catalog.
|
|
722
|
+
*/
|
|
723
|
+
const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000;
|
|
724
|
+
|
|
725
|
+
interface ComboCatalogMemberFallback {
|
|
726
|
+
readonly contextWindow?: number;
|
|
727
|
+
readonly inputModalities?: readonly string[];
|
|
728
|
+
readonly reasoningEfforts?: readonly string[];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Resolve a combo target to a catalog member for derivation.
|
|
733
|
+
* Prefer discovery metadata; when the target is missing from the gather map or
|
|
734
|
+
* lacks a positive contextWindow, synthesize from the (registry-enriched)
|
|
735
|
+
* provider config so combos remain catalogued when targets are configured but
|
|
736
|
+
* discovery metadata is incomplete. Disabled providers stay unresolved.
|
|
737
|
+
* When hints still omit contextWindow, prefer known maxInputTokens, else
|
|
738
|
+
* COMBO_MEMBER_CONTEXT_FALLBACK so a live row without ctx does not drop the
|
|
739
|
+
* whole combo from the public catalog.
|
|
740
|
+
*/
|
|
741
|
+
export function resolveComboCatalogMember(
|
|
742
|
+
target: { provider: string; model: string },
|
|
743
|
+
memberByKey: ReadonlyMap<string, CatalogModel>,
|
|
744
|
+
providers: ReadonlyMap<string, OcxProviderConfig>,
|
|
745
|
+
contextCap?: number,
|
|
746
|
+
fallback?: ComboCatalogMemberFallback,
|
|
747
|
+
): CatalogModel | undefined {
|
|
748
|
+
const existing = memberByKey.get(targetKey(target));
|
|
749
|
+
const prov = providers.get(target.provider);
|
|
750
|
+
// Disabled providers never contribute members — even a complete discovery row
|
|
751
|
+
// is unusable for catalog derivation while the provider is off.
|
|
752
|
+
if (prov?.disabled === true) return undefined;
|
|
753
|
+
|
|
754
|
+
const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
|
|
755
|
+
if (!fallback) return member;
|
|
756
|
+
const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
|
|
757
|
+
? member.contextWindow
|
|
758
|
+
: undefined;
|
|
759
|
+
const addMaxInput = contextWindow !== undefined
|
|
760
|
+
&& !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
|
|
761
|
+
const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
|
|
762
|
+
&& fallback.inputModalities !== undefined;
|
|
763
|
+
const addReasoning = member.reasoningControl === undefined
|
|
764
|
+
&& member.reasoningEfforts === undefined
|
|
765
|
+
&& fallback.reasoningEfforts !== undefined;
|
|
766
|
+
if (!addMaxInput && !addModalities && !addReasoning) return member;
|
|
767
|
+
return {
|
|
768
|
+
...member,
|
|
769
|
+
...(addMaxInput ? { maxInputTokens: contextWindow } : {}),
|
|
770
|
+
...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
|
|
771
|
+
...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// Complete live/configured rows still honour providerContextCaps so a high
|
|
776
|
+
// discovery window cannot outrun an operator-configured cap. Native-alias
|
|
777
|
+
// fallback metadata may fill only capability gaps; it never raises an explicit
|
|
778
|
+
// discovered/configured context window.
|
|
779
|
+
if (
|
|
780
|
+
existing
|
|
781
|
+
&& typeof existing.contextWindow === "number"
|
|
782
|
+
&& existing.contextWindow > 0
|
|
783
|
+
) {
|
|
784
|
+
const capped = applyProviderContextCap(existing.contextWindow, contextCap);
|
|
785
|
+
if (capped === undefined || capped === existing.contextWindow) {
|
|
786
|
+
return withFallbackMetadata(existing);
|
|
787
|
+
}
|
|
788
|
+
const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0
|
|
789
|
+
? Math.min(existing.maxInputTokens, capped)
|
|
790
|
+
: capped;
|
|
791
|
+
return withFallbackMetadata({
|
|
792
|
+
...existing,
|
|
793
|
+
contextWindow: capped,
|
|
794
|
+
maxInputTokens: maxInput,
|
|
795
|
+
contextCap,
|
|
796
|
+
contextCapped: true as const,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const base: CatalogModel = existing ?? {
|
|
801
|
+
id: target.model,
|
|
802
|
+
provider: target.provider,
|
|
803
|
+
};
|
|
804
|
+
const hinted = prov
|
|
805
|
+
? applyProviderConfigHints(target.provider, prov, base, contextCap)
|
|
806
|
+
: base;
|
|
807
|
+
const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
|
|
808
|
+
? hinted.contextWindow
|
|
809
|
+
: undefined;
|
|
810
|
+
const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
|
|
811
|
+
? hinted.maxInputTokens
|
|
812
|
+
: (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0
|
|
813
|
+
? base.maxInputTokens
|
|
814
|
+
: undefined);
|
|
815
|
+
// Real discovery/config values win. A native alias is the next fallback tier.
|
|
816
|
+
// The generic 128k/text synthesis from #1305 remains the final fallback.
|
|
817
|
+
const fallbackContext = existing || prov ? fallback?.contextWindow : undefined;
|
|
818
|
+
const uncappedContext = hintedContext
|
|
819
|
+
?? knownMaxInput
|
|
820
|
+
?? fallbackContext
|
|
821
|
+
?? (existing || prov ? COMBO_MEMBER_CONTEXT_FALLBACK : undefined);
|
|
822
|
+
if (uncappedContext === undefined) return undefined;
|
|
823
|
+
const usedFallback = hintedContext === undefined;
|
|
824
|
+
const cappedContext = applyProviderContextCap(uncappedContext, contextCap);
|
|
825
|
+
const contextWindow = cappedContext ?? uncappedContext;
|
|
826
|
+
const fallbackCapped = usedFallback
|
|
827
|
+
&& contextCap !== undefined
|
|
828
|
+
&& cappedContext !== undefined
|
|
829
|
+
&& cappedContext !== uncappedContext;
|
|
830
|
+
|
|
831
|
+
const inputModalities = hinted.inputModalities
|
|
832
|
+
?? base.inputModalities
|
|
833
|
+
?? (fallback?.inputModalities ? [...fallback.inputModalities] : undefined)
|
|
834
|
+
?? ["text"];
|
|
835
|
+
const reasoningEfforts = hinted.reasoningControl
|
|
836
|
+
? hinted.reasoningEfforts
|
|
837
|
+
: hinted.reasoningEfforts
|
|
838
|
+
?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined)
|
|
839
|
+
?? base.reasoningEfforts
|
|
840
|
+
?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined);
|
|
841
|
+
const maxInputTokens = knownMaxInput !== undefined
|
|
842
|
+
? Math.min(knownMaxInput, contextWindow)
|
|
843
|
+
: contextWindow;
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
...hinted,
|
|
847
|
+
inputModalities,
|
|
848
|
+
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
849
|
+
contextWindow,
|
|
850
|
+
maxInputTokens,
|
|
851
|
+
...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function isDatedVariantId(liveId: string, configuredId: string): boolean {
|
|
856
|
+
if (!liveId.startsWith(`${configuredId}-`)) return false;
|
|
857
|
+
return /^\d{8}$/.test(liveId.slice(configuredId.length + 1));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export const lastDropWarnSignature = new Map<string, string>();
|
|
861
|
+
let lastWarningReconciledGeneration = 0;
|
|
862
|
+
|
|
863
|
+
export function reconcileProviderFetchWarnings(generation: number): number {
|
|
864
|
+
if (generation <= lastWarningReconciledGeneration) return 0;
|
|
865
|
+
const removed = lastDropWarnSignature.size;
|
|
866
|
+
lastDropWarnSignature.clear();
|
|
867
|
+
lastWarningReconciledGeneration = generation;
|
|
868
|
+
return removed;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
872
|
+
|
|
873
|
+
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
874
|
+
kimi: new Set([
|
|
875
|
+
"k3[1m]",
|
|
876
|
+
"kimi-k2.7-code",
|
|
877
|
+
"kimi-k2.7-code-highspeed",
|
|
878
|
+
"kimi-k2.6",
|
|
879
|
+
"kimi-k2.5",
|
|
880
|
+
]),
|
|
881
|
+
xai: new Set([
|
|
882
|
+
"grok-4.3",
|
|
883
|
+
"grok-4.20-0309-reasoning",
|
|
884
|
+
"grok-4.20-0309-non-reasoning",
|
|
885
|
+
"grok-build-0.1",
|
|
886
|
+
"grok-composer-2.5-fast",
|
|
887
|
+
]),
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds: string[]): void {
|
|
891
|
+
const signature = [...droppedConfiguredIds].sort().join(",");
|
|
892
|
+
if (lastDropWarnSignature.get(name) === signature) return;
|
|
893
|
+
lastDropWarnSignature.set(name, signature);
|
|
894
|
+
console.warn(
|
|
895
|
+
`[Remodex] Provider model discovery for "${name}" omitted configured model ids; dropping them from the authoritative live catalog: ${droppedConfiguredIds.join(", ")}.`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function isGlm52ModelId(id: string): boolean {
|
|
900
|
+
const normalized = id.toLowerCase();
|
|
901
|
+
return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
905
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
906
|
+
? value as Record<string, unknown>
|
|
907
|
+
: undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const MODEL_DISCOVERY_METADATA_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
911
|
+
const OPENROUTER_GATEWAY_REASONING_EFFORTS = [
|
|
912
|
+
"none",
|
|
913
|
+
"minimal",
|
|
914
|
+
"low",
|
|
915
|
+
"medium",
|
|
916
|
+
"high",
|
|
917
|
+
"xhigh",
|
|
918
|
+
"max",
|
|
919
|
+
] as const;
|
|
920
|
+
const NESTED_REASONING_EFFORT_LIST_KEYS = [
|
|
921
|
+
"supported_efforts",
|
|
922
|
+
"supportedEfforts",
|
|
923
|
+
"efforts",
|
|
924
|
+
"levels",
|
|
925
|
+
"supported",
|
|
926
|
+
"options",
|
|
927
|
+
"enum",
|
|
928
|
+
"values",
|
|
929
|
+
] as const;
|
|
930
|
+
|
|
931
|
+
function positiveSafeInteger(...values: unknown[]): number | undefined {
|
|
932
|
+
return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function normalizedMetadataString(raw: string, maxLength: number): string | undefined {
|
|
936
|
+
if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined;
|
|
937
|
+
const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength);
|
|
938
|
+
return normalized || undefined;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): string[] | undefined {
|
|
942
|
+
if (!Array.isArray(value)) return undefined;
|
|
943
|
+
const out: string[] = [];
|
|
944
|
+
const maxInspectedItems = Math.max(maxItems * 8, maxItems);
|
|
945
|
+
for (let i = 0; i < value.length && i < maxInspectedItems; i += 1) {
|
|
946
|
+
const raw = value[i];
|
|
947
|
+
if (typeof raw !== "string") continue;
|
|
948
|
+
const normalized = normalizedMetadataString(raw, maxLength);
|
|
949
|
+
if (normalized && !out.includes(normalized)) out.push(normalized);
|
|
950
|
+
if (out.length >= maxItems) break;
|
|
951
|
+
}
|
|
952
|
+
return out.length > 0 ? out : undefined;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/*
|
|
956
|
+
* Service-tier metadata is provider-owned and arrives through an untrusted
|
|
957
|
+
* `/models` response. Keep the same bounded/string-only posture used for
|
|
958
|
+
* reasoning metadata, but retain an explicit empty array: `[]` is a real
|
|
959
|
+
* capability statement (for example gpt-5.4-mini has no speed selector).
|
|
960
|
+
*/
|
|
961
|
+
const MAX_SERVICE_TIER_ITEMS = 32;
|
|
962
|
+
const MAX_SERVICE_TIER_ID_LENGTH = 64;
|
|
963
|
+
const MAX_SERVICE_TIER_TEXT_LENGTH = 256;
|
|
964
|
+
|
|
965
|
+
function boundedMetadataText(value: unknown, maxLength: number): string | undefined {
|
|
966
|
+
if (typeof value !== "string") return undefined;
|
|
967
|
+
if (
|
|
968
|
+
value.length > maxLength * 4
|
|
969
|
+
|| MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)
|
|
970
|
+
) return undefined;
|
|
971
|
+
const text = value.trim().slice(0, maxLength);
|
|
972
|
+
return text || undefined;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** Canonical catalog/request ids. `fast` is the legacy spelling for priority. */
|
|
976
|
+
function normalizedServiceTierId(value: unknown): string | undefined {
|
|
977
|
+
if (typeof value !== "string") return undefined;
|
|
978
|
+
const normalized = normalizedMetadataString(value, MAX_SERVICE_TIER_ID_LENGTH);
|
|
979
|
+
if (!normalized) return undefined;
|
|
980
|
+
if (normalized === "standard") return "default";
|
|
981
|
+
if (normalized === "fast") return "priority";
|
|
982
|
+
return normalized;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function serviceTierDisplayName(id: string, value: unknown): string {
|
|
986
|
+
return boundedMetadataText(value, MAX_SERVICE_TIER_TEXT_LENGTH)
|
|
987
|
+
?? (id === "default" ? "Standard" : id === "priority" ? "Fast" : id === "ultrafast" ? "Ultrafast" : id);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function normalizedServiceTierList(value: unknown): CatalogServiceTier[] | undefined {
|
|
991
|
+
if (!Array.isArray(value)) return undefined;
|
|
992
|
+
const out: CatalogServiceTier[] = [];
|
|
993
|
+
const seen = new Set<string>();
|
|
994
|
+
// Inspect a bounded number of entries even when all of them are malformed;
|
|
995
|
+
// the returned [] remains meaningful negative evidence.
|
|
996
|
+
for (let index = 0; index < value.length && index < MAX_SERVICE_TIER_ITEMS * 8; index += 1) {
|
|
997
|
+
const raw = value[index];
|
|
998
|
+
const row = plainRecord(raw);
|
|
999
|
+
const id = normalizedServiceTierId(
|
|
1000
|
+
typeof raw === "string"
|
|
1001
|
+
? raw
|
|
1002
|
+
: row?.id ?? row?.value ?? row?.service_tier ?? row?.serviceTier,
|
|
1003
|
+
);
|
|
1004
|
+
if (!id || seen.has(id)) continue;
|
|
1005
|
+
const name = serviceTierDisplayName(
|
|
1006
|
+
id,
|
|
1007
|
+
row?.name ?? row?.display_name ?? row?.displayName ?? row?.label,
|
|
1008
|
+
);
|
|
1009
|
+
const description = boundedMetadataText(
|
|
1010
|
+
row?.description ?? row?.detail,
|
|
1011
|
+
MAX_SERVICE_TIER_TEXT_LENGTH,
|
|
1012
|
+
);
|
|
1013
|
+
out.push({ id, name, ...(description ? { description } : {}) });
|
|
1014
|
+
seen.add(id);
|
|
1015
|
+
if (out.length >= MAX_SERVICE_TIER_ITEMS) break;
|
|
1016
|
+
}
|
|
1017
|
+
return out;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/** Legacy Codex spelling; unlike modern service_tiers, ids remain `fast` here. */
|
|
1021
|
+
function normalizedAdditionalSpeedTierList(value: unknown): string[] | undefined {
|
|
1022
|
+
if (!Array.isArray(value)) return undefined;
|
|
1023
|
+
const out: string[] = [];
|
|
1024
|
+
const seen = new Set<string>();
|
|
1025
|
+
for (let index = 0; index < value.length && index < MAX_SERVICE_TIER_ITEMS * 8; index += 1) {
|
|
1026
|
+
const raw = value[index];
|
|
1027
|
+
const row = plainRecord(raw);
|
|
1028
|
+
const candidate = typeof raw === "string"
|
|
1029
|
+
? raw
|
|
1030
|
+
: row?.id ?? row?.value ?? row?.service_tier ?? row?.serviceTier;
|
|
1031
|
+
const normalized = typeof candidate === "string"
|
|
1032
|
+
? normalizedMetadataString(candidate, MAX_SERVICE_TIER_ID_LENGTH)
|
|
1033
|
+
: undefined;
|
|
1034
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
1035
|
+
out.push(normalized);
|
|
1036
|
+
seen.add(normalized);
|
|
1037
|
+
if (out.length >= MAX_SERVICE_TIER_ITEMS) break;
|
|
1038
|
+
}
|
|
1039
|
+
return out;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function firstExplicitArray(
|
|
1043
|
+
records: readonly (Record<string, unknown> | undefined)[],
|
|
1044
|
+
keys: readonly string[],
|
|
1045
|
+
): unknown[] | undefined {
|
|
1046
|
+
for (const record of records) {
|
|
1047
|
+
if (!record) continue;
|
|
1048
|
+
for (const key of keys) {
|
|
1049
|
+
if (Object.hasOwn(record, key) && Array.isArray(record[key])) return record[key] as unknown[];
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return undefined;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function firstExplicitValue(
|
|
1056
|
+
records: readonly (Record<string, unknown> | undefined)[],
|
|
1057
|
+
keys: readonly string[],
|
|
1058
|
+
): { present: boolean; value: unknown } {
|
|
1059
|
+
for (const record of records) {
|
|
1060
|
+
if (!record) continue;
|
|
1061
|
+
for (const key of keys) {
|
|
1062
|
+
if (Object.hasOwn(record, key)) return { present: true, value: record[key] };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return { present: false, value: undefined };
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
|
|
1069
|
+
const metadata = plainRecord(item.metadata);
|
|
1070
|
+
const metadataCapabilities = metadata?.capabilities;
|
|
1071
|
+
const capabilityRecord = plainRecord(metadataCapabilities) ?? plainRecord(item.capabilities);
|
|
1072
|
+
const out = new Set<string>();
|
|
1073
|
+
for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
|
|
1074
|
+
for (const capability of normalizedStringList(list) ?? []) out.add(capability);
|
|
1075
|
+
}
|
|
1076
|
+
const capabilityFields = capabilityRecord ?? {};
|
|
1077
|
+
let inspectedCapabilityFields = 0;
|
|
1078
|
+
for (const key in capabilityFields) {
|
|
1079
|
+
if (!Object.hasOwn(capabilityFields, key)) continue;
|
|
1080
|
+
inspectedCapabilityFields += 1;
|
|
1081
|
+
if (inspectedCapabilityFields > 256 || out.size >= 32) break;
|
|
1082
|
+
if (capabilityFields[key] === true) {
|
|
1083
|
+
const normalized = normalizedMetadataString(key, 64);
|
|
1084
|
+
if (normalized) out.add(normalized);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
for (const field of ["supports_tools", "supports_tool_calling", "supports_function_calling"] as const) {
|
|
1088
|
+
if (item[field] === true) out.add("tools");
|
|
1089
|
+
}
|
|
1090
|
+
for (const field of ["supports_reasoning", "reasoning"] as const) {
|
|
1091
|
+
if (item[field] === true) out.add("reasoning");
|
|
1092
|
+
}
|
|
1093
|
+
return out.size > 0 ? [...out].filter(Boolean).slice(0, 32) : undefined;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function modelInputModalities(
|
|
1097
|
+
item: ProviderModelsApiItem,
|
|
1098
|
+
capabilities: readonly string[] | undefined,
|
|
1099
|
+
): string[] | undefined {
|
|
1100
|
+
const metadata = plainRecord(item.metadata);
|
|
1101
|
+
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
1102
|
+
const explicit = normalizedStringList(
|
|
1103
|
+
item.input_modalities
|
|
1104
|
+
?? item.modalities
|
|
1105
|
+
?? metadata?.input_modalities
|
|
1106
|
+
?? capabilityRecord?.input_modalities,
|
|
1107
|
+
8,
|
|
1108
|
+
24,
|
|
1109
|
+
)?.filter(value => (
|
|
1110
|
+
// Codex parses `input_modalities` as a closed enum of text | image | audio. A provider that
|
|
1111
|
+
// advertises anything else (zenmux reports "video") must not reach the catalog: Codex rejects
|
|
1112
|
+
// the whole file, so plugins, apps and MCP servers all stop loading over one model's metadata.
|
|
1113
|
+
value === "text" || value === "image" || value === "audio"
|
|
1114
|
+
));
|
|
1115
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
1116
|
+
const architecture = plainRecord(item.architecture);
|
|
1117
|
+
const architectureModality = typeof architecture?.modality === "string"
|
|
1118
|
+
? normalizedMetadataString(architecture.modality, 64)
|
|
1119
|
+
: undefined;
|
|
1120
|
+
if (architectureModality?.includes("->")) {
|
|
1121
|
+
const [rawInput = ""] = architectureModality.split("->");
|
|
1122
|
+
const inferred = rawInput
|
|
1123
|
+
.split("+")
|
|
1124
|
+
.filter(value => value === "text" || value === "image" || value === "audio");
|
|
1125
|
+
if (inferred.length > 0) return [...new Set(inferred)];
|
|
1126
|
+
}
|
|
1127
|
+
if (capabilityRecord?.vision === false) return ["text"];
|
|
1128
|
+
if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
|
|
1129
|
+
return ["text", "image"];
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function normalizedReasoningEffortList(
|
|
1135
|
+
value: unknown,
|
|
1136
|
+
openRouterNullMeansAll = false,
|
|
1137
|
+
): string[] | undefined {
|
|
1138
|
+
if (value === null && openRouterNullMeansAll) {
|
|
1139
|
+
return [...OPENROUTER_GATEWAY_REASONING_EFFORTS];
|
|
1140
|
+
}
|
|
1141
|
+
const record = plainRecord(value);
|
|
1142
|
+
let rawList: unknown[] | undefined = Array.isArray(value) ? value : undefined;
|
|
1143
|
+
if (!rawList && record) {
|
|
1144
|
+
for (const key of NESTED_REASONING_EFFORT_LIST_KEYS) {
|
|
1145
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
1146
|
+
const nested = record[key];
|
|
1147
|
+
if (
|
|
1148
|
+
nested === null
|
|
1149
|
+
&& openRouterNullMeansAll
|
|
1150
|
+
&& (key === "supported_efforts" || key === "supportedEfforts")
|
|
1151
|
+
) {
|
|
1152
|
+
return [...OPENROUTER_GATEWAY_REASONING_EFFORTS];
|
|
1153
|
+
}
|
|
1154
|
+
if (Array.isArray(nested)) {
|
|
1155
|
+
rawList = nested;
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (!rawList) return undefined;
|
|
1161
|
+
const tokens: string[] = [];
|
|
1162
|
+
for (let index = 0; index < rawList.length && index < 64; index += 1) {
|
|
1163
|
+
const row = rawList[index];
|
|
1164
|
+
const raw = typeof row === "string"
|
|
1165
|
+
? row
|
|
1166
|
+
: plainRecord(row)
|
|
1167
|
+
? ["effort", "level", "id", "name", "value"]
|
|
1168
|
+
.map(key => plainRecord(row)?.[key])
|
|
1169
|
+
.find(value => typeof value === "string")
|
|
1170
|
+
: undefined;
|
|
1171
|
+
if (typeof raw !== "string") continue;
|
|
1172
|
+
const normalized = normalizedMetadataString(raw, 24);
|
|
1173
|
+
if (normalized && !tokens.includes(normalized)) tokens.push(normalized);
|
|
1174
|
+
if (tokens.length >= 8) break;
|
|
1175
|
+
}
|
|
1176
|
+
// An explicit empty/unknown list is still useful negative evidence. It must not fall through to
|
|
1177
|
+
// a fabricated generic ladder merely because none of the provider's labels are Codex-safe.
|
|
1178
|
+
return sanitizeCodexReasoningEfforts(tokens) ?? [];
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function firstReasoningEffortList(
|
|
1182
|
+
candidates: readonly {
|
|
1183
|
+
value: unknown;
|
|
1184
|
+
openRouterNullMeansAll?: boolean;
|
|
1185
|
+
}[],
|
|
1186
|
+
): string[] | undefined {
|
|
1187
|
+
for (const candidate of candidates) {
|
|
1188
|
+
const efforts = normalizedReasoningEffortList(
|
|
1189
|
+
candidate.value,
|
|
1190
|
+
candidate.openRouterNullMeansAll === true,
|
|
1191
|
+
);
|
|
1192
|
+
if (efforts !== undefined) return efforts;
|
|
1193
|
+
}
|
|
1194
|
+
return undefined;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function normalizedReasoningEffort(value: unknown): string | undefined {
|
|
1198
|
+
if (typeof value !== "string") return undefined;
|
|
1199
|
+
const normalized = normalizedMetadataString(value, 24);
|
|
1200
|
+
return normalized ? sanitizeCodexReasoningEfforts([normalized])?.[0] : undefined;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function firstBoolean(values: readonly unknown[]): boolean | undefined {
|
|
1204
|
+
return values.find(value => typeof value === "boolean") as boolean | undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function firstReasoningEffort(values: readonly unknown[]): string | undefined {
|
|
1208
|
+
return values.map(normalizedReasoningEffort).find((value): value is string => value !== undefined);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export function catalogHintsFromModelsApiItem(
|
|
1212
|
+
providerName: string,
|
|
1213
|
+
item: ProviderModelsApiItem,
|
|
1214
|
+
capturedMetadataFormat?: ReasoningCapabilityProvider["reasoningMetadataFormat"] | null,
|
|
1215
|
+
): Partial<CatalogModel> {
|
|
1216
|
+
const reasoningMetadataFormat = capturedMetadataFormat === undefined
|
|
1217
|
+
? getProviderRegistryEntry(providerName)?.reasoningMetadataFormat
|
|
1218
|
+
: capturedMetadataFormat ?? undefined;
|
|
1219
|
+
const openRouterMetadata = reasoningMetadataFormat === "openrouter";
|
|
1220
|
+
const metadata = plainRecord(item.metadata);
|
|
1221
|
+
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
1222
|
+
const itemReasoning = plainRecord(item.reasoning);
|
|
1223
|
+
const metadataReasoning = plainRecord(metadata?.reasoning);
|
|
1224
|
+
const capabilityReasoning = plainRecord(capabilityRecord?.reasoning);
|
|
1225
|
+
const effortRecord = plainRecord(capabilityRecord?.reasoning_effort)
|
|
1226
|
+
?? plainRecord(item.reasoning_effort);
|
|
1227
|
+
const limits = plainRecord(metadata?.limits);
|
|
1228
|
+
const contextWindow =
|
|
1229
|
+
positiveSafeInteger(
|
|
1230
|
+
limits?.max_context_length,
|
|
1231
|
+
metadata?.context_length,
|
|
1232
|
+
item.context_length,
|
|
1233
|
+
item.context_size,
|
|
1234
|
+
item.max_model_len,
|
|
1235
|
+
item.max_context_length,
|
|
1236
|
+
);
|
|
1237
|
+
const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
|
|
1238
|
+
const supportsReasoning = firstBoolean([
|
|
1239
|
+
item.supports_reasoning,
|
|
1240
|
+
item.reasoning,
|
|
1241
|
+
metadata?.supports_reasoning,
|
|
1242
|
+
capabilityRecord?.supports_reasoning,
|
|
1243
|
+
capabilityRecord?.reasoning,
|
|
1244
|
+
]);
|
|
1245
|
+
let reasoningEfforts = firstReasoningEffortList([
|
|
1246
|
+
{ value: item.supported_efforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1247
|
+
{ value: item.supportedEfforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1248
|
+
{ value: item.reasoning_efforts },
|
|
1249
|
+
{ value: item.reasoningEfforts },
|
|
1250
|
+
{ value: item.supported_reasoning_efforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1251
|
+
{ value: item.supportedReasoningEfforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1252
|
+
{ value: item.reasoning_levels },
|
|
1253
|
+
{ value: item.reasoningLevels },
|
|
1254
|
+
{ value: item.supported_reasoning_levels },
|
|
1255
|
+
{ value: item.supportedReasoningLevels },
|
|
1256
|
+
{ value: item.reasoning_effort },
|
|
1257
|
+
{ value: itemReasoning, openRouterNullMeansAll: openRouterMetadata },
|
|
1258
|
+
{ value: metadata?.supported_efforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1259
|
+
{ value: metadata?.supportedEfforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1260
|
+
{ value: metadata?.reasoning_efforts },
|
|
1261
|
+
{ value: metadata?.reasoningEfforts },
|
|
1262
|
+
{ value: metadata?.supported_reasoning_efforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1263
|
+
{ value: metadata?.supportedReasoningEfforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1264
|
+
{ value: metadata?.reasoning_levels },
|
|
1265
|
+
{ value: metadata?.reasoningLevels },
|
|
1266
|
+
{ value: metadata?.supported_reasoning_levels },
|
|
1267
|
+
{ value: metadata?.supportedReasoningLevels },
|
|
1268
|
+
{ value: metadataReasoning, openRouterNullMeansAll: openRouterMetadata },
|
|
1269
|
+
{ value: capabilityRecord?.reasoning_efforts },
|
|
1270
|
+
{ value: capabilityRecord?.reasoningEfforts },
|
|
1271
|
+
{ value: capabilityRecord?.supported_reasoning_efforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1272
|
+
{ value: capabilityRecord?.supportedReasoningEfforts, openRouterNullMeansAll: openRouterMetadata },
|
|
1273
|
+
{ value: capabilityReasoning, openRouterNullMeansAll: openRouterMetadata },
|
|
1274
|
+
{ value: capabilityRecord?.reasoning_effort },
|
|
1275
|
+
{ value: effortRecord },
|
|
1276
|
+
]) ?? (supportsReasoning === false ? [] : undefined);
|
|
1277
|
+
const advertisedDefaultReasoningEffort = firstReasoningEffort([
|
|
1278
|
+
item.default_reasoning_effort,
|
|
1279
|
+
item.defaultReasoningEffort,
|
|
1280
|
+
item.default_reasoning_level,
|
|
1281
|
+
item.defaultReasoningLevel,
|
|
1282
|
+
item.default_effort,
|
|
1283
|
+
item.defaultEffort,
|
|
1284
|
+
metadata?.default_reasoning_effort,
|
|
1285
|
+
metadata?.defaultReasoningEffort,
|
|
1286
|
+
metadata?.default_reasoning_level,
|
|
1287
|
+
metadata?.defaultReasoningLevel,
|
|
1288
|
+
metadata?.default_effort,
|
|
1289
|
+
metadata?.defaultEffort,
|
|
1290
|
+
capabilityRecord?.default_reasoning_effort,
|
|
1291
|
+
capabilityRecord?.defaultReasoningEffort,
|
|
1292
|
+
capabilityRecord?.default_reasoning_level,
|
|
1293
|
+
capabilityRecord?.defaultReasoningLevel,
|
|
1294
|
+
capabilityRecord?.default_effort,
|
|
1295
|
+
capabilityRecord?.defaultEffort,
|
|
1296
|
+
itemReasoning?.default_effort,
|
|
1297
|
+
itemReasoning?.defaultEffort,
|
|
1298
|
+
itemReasoning?.default,
|
|
1299
|
+
metadataReasoning?.default_effort,
|
|
1300
|
+
metadataReasoning?.defaultEffort,
|
|
1301
|
+
metadataReasoning?.default,
|
|
1302
|
+
capabilityReasoning?.default_effort,
|
|
1303
|
+
capabilityReasoning?.defaultEffort,
|
|
1304
|
+
capabilityReasoning?.default,
|
|
1305
|
+
effortRecord?.default_effort,
|
|
1306
|
+
effortRecord?.defaultEffort,
|
|
1307
|
+
effortRecord?.default,
|
|
1308
|
+
]);
|
|
1309
|
+
const reasoningRequired = firstBoolean([
|
|
1310
|
+
item.reasoning_required,
|
|
1311
|
+
item.reasoningRequired,
|
|
1312
|
+
item.requires_reasoning,
|
|
1313
|
+
item.requiresReasoning,
|
|
1314
|
+
item.reasoning_mandatory,
|
|
1315
|
+
item.reasoningMandatory,
|
|
1316
|
+
metadata?.reasoning_required,
|
|
1317
|
+
metadata?.reasoningRequired,
|
|
1318
|
+
metadata?.requires_reasoning,
|
|
1319
|
+
metadata?.requiresReasoning,
|
|
1320
|
+
metadata?.reasoning_mandatory,
|
|
1321
|
+
metadata?.reasoningMandatory,
|
|
1322
|
+
capabilityRecord?.reasoning_required,
|
|
1323
|
+
capabilityRecord?.reasoningRequired,
|
|
1324
|
+
capabilityRecord?.requires_reasoning,
|
|
1325
|
+
capabilityRecord?.requiresReasoning,
|
|
1326
|
+
capabilityRecord?.reasoning_mandatory,
|
|
1327
|
+
capabilityRecord?.reasoningMandatory,
|
|
1328
|
+
itemReasoning?.mandatory,
|
|
1329
|
+
itemReasoning?.required,
|
|
1330
|
+
itemReasoning?.always_on,
|
|
1331
|
+
itemReasoning?.alwaysOn,
|
|
1332
|
+
metadataReasoning?.mandatory,
|
|
1333
|
+
metadataReasoning?.required,
|
|
1334
|
+
metadataReasoning?.always_on,
|
|
1335
|
+
metadataReasoning?.alwaysOn,
|
|
1336
|
+
capabilityReasoning?.mandatory,
|
|
1337
|
+
capabilityReasoning?.required,
|
|
1338
|
+
capabilityReasoning?.always_on,
|
|
1339
|
+
capabilityReasoning?.alwaysOn,
|
|
1340
|
+
effortRecord?.mandatory,
|
|
1341
|
+
effortRecord?.required,
|
|
1342
|
+
]);
|
|
1343
|
+
const defaultReasoningEnabled = firstBoolean([
|
|
1344
|
+
item.reasoning_default_enabled,
|
|
1345
|
+
item.reasoningDefaultEnabled,
|
|
1346
|
+
item.default_reasoning_enabled,
|
|
1347
|
+
item.defaultReasoningEnabled,
|
|
1348
|
+
metadata?.reasoning_default_enabled,
|
|
1349
|
+
metadata?.reasoningDefaultEnabled,
|
|
1350
|
+
metadata?.default_reasoning_enabled,
|
|
1351
|
+
metadata?.defaultReasoningEnabled,
|
|
1352
|
+
capabilityRecord?.reasoning_default_enabled,
|
|
1353
|
+
capabilityRecord?.reasoningDefaultEnabled,
|
|
1354
|
+
capabilityRecord?.default_reasoning_enabled,
|
|
1355
|
+
capabilityRecord?.defaultReasoningEnabled,
|
|
1356
|
+
itemReasoning?.default_enabled,
|
|
1357
|
+
itemReasoning?.defaultEnabled,
|
|
1358
|
+
metadataReasoning?.default_enabled,
|
|
1359
|
+
metadataReasoning?.defaultEnabled,
|
|
1360
|
+
capabilityReasoning?.default_enabled,
|
|
1361
|
+
capabilityReasoning?.defaultEnabled,
|
|
1362
|
+
effortRecord?.default_enabled,
|
|
1363
|
+
effortRecord?.defaultEnabled,
|
|
1364
|
+
]);
|
|
1365
|
+
if (reasoningEfforts !== undefined) {
|
|
1366
|
+
if (reasoningRequired === true) {
|
|
1367
|
+
reasoningEfforts = reasoningEfforts.filter(effort => effort !== "none");
|
|
1368
|
+
} else if (
|
|
1369
|
+
openRouterMetadata
|
|
1370
|
+
&& reasoningEfforts.length > 0
|
|
1371
|
+
&& (
|
|
1372
|
+
reasoningRequired === false
|
|
1373
|
+
|| defaultReasoningEnabled === false
|
|
1374
|
+
|| advertisedDefaultReasoningEffort === "none"
|
|
1375
|
+
)
|
|
1376
|
+
) {
|
|
1377
|
+
reasoningEfforts = sanitizeCodexReasoningEfforts(["none", ...reasoningEfforts]) ?? reasoningEfforts;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const selectedDefaultReasoningEffort = defaultReasoningEnabled === false
|
|
1381
|
+
&& reasoningEfforts?.includes("none")
|
|
1382
|
+
? "none"
|
|
1383
|
+
: advertisedDefaultReasoningEffort;
|
|
1384
|
+
const defaultReasoningEffort = selectedDefaultReasoningEffort
|
|
1385
|
+
&& reasoningEfforts?.includes(selectedDefaultReasoningEffort)
|
|
1386
|
+
? selectedDefaultReasoningEffort
|
|
1387
|
+
: undefined;
|
|
1388
|
+
const reasoningObjectPresent = itemReasoning !== undefined
|
|
1389
|
+
|| metadataReasoning !== undefined
|
|
1390
|
+
|| capabilityReasoning !== undefined
|
|
1391
|
+
|| effortRecord !== undefined;
|
|
1392
|
+
const reasoningAdvertised = supportsReasoning === true
|
|
1393
|
+
|| reasoningObjectPresent
|
|
1394
|
+
|| reasoningEfforts !== undefined;
|
|
1395
|
+
const reasoningControl: ReasoningControl | undefined = reasoningEfforts && reasoningEfforts.length > 0
|
|
1396
|
+
? {
|
|
1397
|
+
kind: "effort",
|
|
1398
|
+
efforts: reasoningEfforts,
|
|
1399
|
+
required: reasoningRequired === true,
|
|
1400
|
+
...(defaultReasoningEffort ? { defaultEffort: defaultReasoningEffort } : {}),
|
|
1401
|
+
}
|
|
1402
|
+
: supportsReasoning === false
|
|
1403
|
+
? { kind: "unsupported" }
|
|
1404
|
+
: reasoningAdvertised
|
|
1405
|
+
? openRouterMetadata
|
|
1406
|
+
&& reasoningRequired !== true
|
|
1407
|
+
&& (reasoningRequired === false || defaultReasoningEnabled !== undefined)
|
|
1408
|
+
? {
|
|
1409
|
+
kind: "toggle",
|
|
1410
|
+
...(typeof defaultReasoningEnabled === "boolean"
|
|
1411
|
+
? { defaultEnabled: defaultReasoningEnabled }
|
|
1412
|
+
: {}),
|
|
1413
|
+
}
|
|
1414
|
+
: { kind: "automatic", required: reasoningRequired === true }
|
|
1415
|
+
: undefined;
|
|
1416
|
+
const capabilities = modelCapabilities(item);
|
|
1417
|
+
const inputModalities = modelInputModalities(item, capabilities);
|
|
1418
|
+
// Codex-LB (and compatible Codex model endpoints) place these fields inside
|
|
1419
|
+
// `metadata`, while native Codex model/list responses use the top-level
|
|
1420
|
+
// camelCase spelling. Accept both forms, with an explicitly supplied array
|
|
1421
|
+
// (including `[]`) taking precedence over a legacy fallback list.
|
|
1422
|
+
const tierRecords = [item, metadata];
|
|
1423
|
+
const serviceTierSource = firstExplicitArray(
|
|
1424
|
+
tierRecords,
|
|
1425
|
+
["service_tiers", "serviceTiers"],
|
|
1426
|
+
);
|
|
1427
|
+
const additionalSpeedTierSource = firstExplicitArray(
|
|
1428
|
+
tierRecords,
|
|
1429
|
+
["additional_speed_tiers", "additionalSpeedTiers"],
|
|
1430
|
+
);
|
|
1431
|
+
const defaultServiceTierSource = firstExplicitValue(
|
|
1432
|
+
tierRecords,
|
|
1433
|
+
["default_service_tier", "defaultServiceTier"],
|
|
1434
|
+
);
|
|
1435
|
+
const serviceTiers = normalizedServiceTierList(serviceTierSource);
|
|
1436
|
+
const additionalSpeedTiers = normalizedAdditionalSpeedTierList(additionalSpeedTierSource);
|
|
1437
|
+
const defaultServiceTier = defaultServiceTierSource.present
|
|
1438
|
+
? defaultServiceTierSource.value === null
|
|
1439
|
+
? null
|
|
1440
|
+
: normalizedServiceTierId(defaultServiceTierSource.value) ?? null
|
|
1441
|
+
: undefined;
|
|
1442
|
+
return {
|
|
1443
|
+
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
1444
|
+
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
1445
|
+
...(reasoningControl ? { reasoningControl } : {}),
|
|
1446
|
+
...(reasoningControl?.kind === "effort" ? { reasoningEfforts: reasoningControl.efforts } : {}),
|
|
1447
|
+
...(reasoningControl?.kind === "unsupported" ? { reasoningEfforts: [] } : {}),
|
|
1448
|
+
...(reasoningControl?.kind === "effort" && defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1449
|
+
...(reasoningControl?.kind === "effort" || reasoningControl?.kind === "automatic"
|
|
1450
|
+
? { reasoningRequired: reasoningControl.required }
|
|
1451
|
+
: {}),
|
|
1452
|
+
...(inputModalities ? { inputModalities } : {}),
|
|
1453
|
+
...(capabilities ? { capabilities } : {}),
|
|
1454
|
+
...(serviceTiers !== undefined ? { serviceTiers } : {}),
|
|
1455
|
+
...(additionalSpeedTiers !== undefined ? { additionalSpeedTiers } : {}),
|
|
1456
|
+
...(defaultServiceTierSource.present ? { defaultServiceTier } : {}),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function boundedOwnedBy(value: unknown): string | undefined {
|
|
1461
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256) return undefined;
|
|
1462
|
+
if (MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)) return undefined;
|
|
1463
|
+
return value;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const refreshingModelsAuthResolver: ModelsAuthResolver = { kind: "refreshing" };
|
|
1467
|
+
|
|
1468
|
+
function observedModelsAuthResolver(
|
|
1469
|
+
authStoreBuffer: Uint8Array | null,
|
|
1470
|
+
outcomes: CatalogGatherProviderAuthOutcome[],
|
|
1471
|
+
): ModelsAuthResolver {
|
|
1472
|
+
return {
|
|
1473
|
+
kind: "observed",
|
|
1474
|
+
resolve(name, provider) {
|
|
1475
|
+
if (provider.authMode === "forward") return { apiKey: undefined, observed: true };
|
|
1476
|
+
if (provider.authMode !== "oauth") {
|
|
1477
|
+
return { apiKey: resolveEnvValue(provider.apiKey), observed: true };
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const observation = observeActiveOAuthAccessToken(name, authStoreBuffer);
|
|
1481
|
+
outcomes.push({ provider: name, state: observation.kind });
|
|
1482
|
+
if (observation.kind !== "available") return { apiKey: undefined, observed: true };
|
|
1483
|
+
return {
|
|
1484
|
+
apiKey: observation.snapshot.accessToken,
|
|
1485
|
+
observed: true,
|
|
1486
|
+
...(observation.snapshot.apiBaseUrl ? { oauthApiBaseUrl: observation.snapshot.apiBaseUrl } : {}),
|
|
1487
|
+
...(observation.snapshot.projectId ? { oauthProjectId: observation.snapshot.projectId } : {}),
|
|
1488
|
+
};
|
|
1489
|
+
},
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
async function fetchProviderModelsWithAuth(
|
|
1494
|
+
captured: CapturedProviderGather,
|
|
1495
|
+
ttlMs: number,
|
|
1496
|
+
contextCap: number | undefined,
|
|
1497
|
+
resolveAuth: ModelsAuthResolver,
|
|
1498
|
+
): Promise<ProviderModelsResult> {
|
|
1499
|
+
const { name, configuredProvider, reasoningRegistry, provider: prov, discovery, request } = captured;
|
|
1500
|
+
const observed = (
|
|
1501
|
+
models: CatalogModel[],
|
|
1502
|
+
state: CatalogGatherProviderModelOutcome["state"],
|
|
1503
|
+
): ProviderModelsResult => ({ models, outcome: { provider: name, state } });
|
|
1504
|
+
// Capture before any credential refresh or outbound await. OAuth account changes clear this
|
|
1505
|
+
// generation, so a request started with the former account cannot later publish its result.
|
|
1506
|
+
const cacheGeneration = captureModelCacheGeneration(name);
|
|
1507
|
+
const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
|
|
1508
|
+
if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
|
|
1509
|
+
const seedVertexDefault = prov.adapter === "google"
|
|
1510
|
+
&& prov.googleMode === "vertex"
|
|
1511
|
+
&& (prov.models?.length ?? 0) === 0
|
|
1512
|
+
&& Boolean(prov.defaultModel);
|
|
1513
|
+
const configuredIds = seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : (prov.models ?? []);
|
|
1514
|
+
const configured: CatalogModel[] = configuredIds.map(id => ({
|
|
1515
|
+
id,
|
|
1516
|
+
provider: name,
|
|
1517
|
+
...catalogHintsFromProviderConfig(name, prov, id, contextCap, configuredProvider, reasoningRegistry ?? null),
|
|
1518
|
+
}));
|
|
1519
|
+
// Static catalogs never need an OAuth refresh or an upstream model request. Clear any
|
|
1520
|
+
// discovery failure left by an older live configuration even when the account is logged out.
|
|
1521
|
+
if (prov.liveModels === false) {
|
|
1522
|
+
clearProviderDiscoveryStatus(name);
|
|
1523
|
+
return observed(configured, "authoritative");
|
|
1524
|
+
}
|
|
1525
|
+
const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
|
|
1526
|
+
? prov.authMode === "oauth" && effectiveGoogleMode(name, prov) === "cloud-code-assist"
|
|
1527
|
+
? await getValidAccessTokenSnapshot(name)
|
|
1528
|
+
.then(snapshot => ({
|
|
1529
|
+
apiKey: snapshot.accessToken,
|
|
1530
|
+
observed: false,
|
|
1531
|
+
...(snapshot.projectId ? { oauthProjectId: snapshot.projectId } : {}),
|
|
1532
|
+
}))
|
|
1533
|
+
.catch(() => ({ apiKey: undefined, observed: false }))
|
|
1534
|
+
: { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
|
|
1535
|
+
: resolveAuth.resolve(name, prov));
|
|
1536
|
+
const apiKey = auth.apiKey;
|
|
1537
|
+
// A configured default is a real callable selector and must remain discoverable when a
|
|
1538
|
+
// compatible provider's live /models request fails (issue #308). Keep this separate from the
|
|
1539
|
+
// explicit static list: `liveModels: false` + empty `models[]` intentionally publishes zero
|
|
1540
|
+
// rows, while a failed live discovery may degrade to the default selector.
|
|
1541
|
+
const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic"
|
|
1542
|
+
? configured
|
|
1543
|
+
: [{
|
|
1544
|
+
id: prov.defaultModel,
|
|
1545
|
+
provider: name,
|
|
1546
|
+
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, configuredProvider, reasoningRegistry ?? null),
|
|
1547
|
+
}];
|
|
1548
|
+
const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
|
|
1549
|
+
const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
|
|
1550
|
+
vertexDefaultSeed && !models.some(model => model.id === vertexDefaultSeed.id)
|
|
1551
|
+
? [...models, vertexDefaultSeed]
|
|
1552
|
+
: models
|
|
1553
|
+
);
|
|
1554
|
+
if (prov.adapter === "cursor") {
|
|
1555
|
+
if (!apiKey) return observed(configured, "degraded");
|
|
1556
|
+
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
1557
|
+
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
|
|
1558
|
+
// suffix) but filter the static seed to the bases the account actually has — so models not on the
|
|
1559
|
+
// plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
|
|
1560
|
+
const cachedCursor = getFreshCached(name, ttlMs);
|
|
1561
|
+
if (cachedCursor) {
|
|
1562
|
+
return observed(applyConfigHintsToCachedModels(name, prov, cachedCursor, contextCap, configuredProvider, reasoningRegistry ?? null), "authoritative");
|
|
1563
|
+
}
|
|
1564
|
+
if (isModelsFetchCoolingDown(name)) {
|
|
1565
|
+
const cooling = getStaleCached(name);
|
|
1566
|
+
return observed(
|
|
1567
|
+
cooling ? applyConfigHintsToCachedModels(name, prov, cooling, contextCap, configuredProvider, reasoningRegistry ?? null) : configured,
|
|
1568
|
+
"degraded",
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
|
|
1572
|
+
if (liveResult.ok) {
|
|
1573
|
+
const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
|
|
1574
|
+
const result = available.length > 0 ? available : configured;
|
|
1575
|
+
// Count what discovery actually returned, not the configured rows we fall back to.
|
|
1576
|
+
if (!setCached(name, result, Date.now(), cacheGeneration)) return observed(configured, "degraded");
|
|
1577
|
+
markProviderDiscoveryOk(name, liveResult.models.length);
|
|
1578
|
+
return observed(result, "authoritative");
|
|
1579
|
+
}
|
|
1580
|
+
if (isCurrentCacheGeneration()) {
|
|
1581
|
+
markModelsFetchFailure(name);
|
|
1582
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
1583
|
+
console.warn(
|
|
1584
|
+
`[Remodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
const staleCursor = getStaleCached(name);
|
|
1588
|
+
return observed(
|
|
1589
|
+
staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, contextCap, configuredProvider, reasoningRegistry ?? null) : configured,
|
|
1590
|
+
"degraded",
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
if (prov.authMode === "oauth" && !apiKey) {
|
|
1594
|
+
// No usable token (logged out, or account marked needsReauth). Still surface the
|
|
1595
|
+
// configured static catalog so the GUI Models tab / rail counts are not empty —
|
|
1596
|
+
// matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
|
|
1597
|
+
return observed(configured, "degraded");
|
|
1598
|
+
}
|
|
1599
|
+
const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
1600
|
+
const project = prov.project ?? auth.oauthProjectId;
|
|
1601
|
+
if (cloudCodeAssist && !project) return observed(configured, "degraded");
|
|
1602
|
+
const fresh = getFreshCached(name, ttlMs);
|
|
1603
|
+
if (fresh) {
|
|
1604
|
+
return observed(
|
|
1605
|
+
withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, configuredProvider, reasoningRegistry ?? null)),
|
|
1606
|
+
"authoritative",
|
|
1607
|
+
); // dedups Codex's frequent /v1/models polling within the TTL
|
|
1608
|
+
}
|
|
1609
|
+
if (isModelsFetchCoolingDown(name)) {
|
|
1610
|
+
// A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
|
|
1611
|
+
// fetch timeout on every catalog poll — the dashboard polls this path per page load.
|
|
1612
|
+
const stale = getStaleCached(name);
|
|
1613
|
+
return observed(
|
|
1614
|
+
stale
|
|
1615
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, configuredProvider, reasoningRegistry ?? null))
|
|
1616
|
+
: failedDiscoveryConfigured,
|
|
1617
|
+
"degraded",
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
const url = request.url;
|
|
1621
|
+
const headers = materializeCapturedHeaders(request, apiKey);
|
|
1622
|
+
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
1623
|
+
? "vertex-aiplatform"
|
|
1624
|
+
: "provider-models";
|
|
1625
|
+
const failedDiscoveryFallback = (
|
|
1626
|
+
failure: ProviderModelDiscoveryFailure,
|
|
1627
|
+
): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
|
|
1628
|
+
if (!isCurrentCacheGeneration()) {
|
|
1629
|
+
return { models: failedDiscoveryConfigured, fallback: "configured", shouldLog: false };
|
|
1630
|
+
}
|
|
1631
|
+
// Decide logging BEFORE recording the new status, so we can compare against the prior one and
|
|
1632
|
+
// suppress an identical repeated failure (#395 log flood). The failure stays observable via the
|
|
1633
|
+
// discovery-status API regardless.
|
|
1634
|
+
const shouldLog = shouldLogDiscoveryFailure(name, failure);
|
|
1635
|
+
markModelsFetchFailure(name);
|
|
1636
|
+
markProviderDiscoveryFailed(name, failure);
|
|
1637
|
+
const stale = getStaleCached(name);
|
|
1638
|
+
return {
|
|
1639
|
+
models: stale
|
|
1640
|
+
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, configuredProvider, reasoningRegistry ?? null))
|
|
1641
|
+
: failedDiscoveryConfigured,
|
|
1642
|
+
fallback: stale ? "stale" : "configured",
|
|
1643
|
+
shouldLog,
|
|
1644
|
+
};
|
|
1645
|
+
};
|
|
1646
|
+
try {
|
|
1647
|
+
const res = request.method === "POST"
|
|
1648
|
+
? await providerOutboundPost(name, prov, url, {
|
|
1649
|
+
headers,
|
|
1650
|
+
body: JSON.stringify({ project }),
|
|
1651
|
+
signal: AbortSignal.timeout(8000),
|
|
1652
|
+
})
|
|
1653
|
+
: await providerOutboundGet(name, prov, url, {
|
|
1654
|
+
headers,
|
|
1655
|
+
signal: AbortSignal.timeout(8000),
|
|
1656
|
+
});
|
|
1657
|
+
const redirectError = await providerRedirectError(res, url);
|
|
1658
|
+
if (redirectError) {
|
|
1659
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
1660
|
+
if (shouldLog) {
|
|
1661
|
+
console.warn(
|
|
1662
|
+
`[Remodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
return observed(models, "degraded");
|
|
1666
|
+
}
|
|
1667
|
+
if (!res.ok) {
|
|
1668
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
1669
|
+
if (shouldLog) {
|
|
1670
|
+
console.warn(
|
|
1671
|
+
`[Remodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
return observed(models, "degraded");
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const contentType = (
|
|
1678
|
+
res.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "missing"
|
|
1679
|
+
).slice(0, 80);
|
|
1680
|
+
const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
|
|
1681
|
+
if (!bounded.ok) {
|
|
1682
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1683
|
+
const diagnostic = bounded.reason === "response_too_large"
|
|
1684
|
+
? `exceeded the ${discovery.maxResponseBytes}-byte response limit`
|
|
1685
|
+
: contentType === "application/json" || contentType.endsWith("+json")
|
|
1686
|
+
? "returned invalid JSON in a 2xx response"
|
|
1687
|
+
: "returned a non-JSON 2xx response";
|
|
1688
|
+
if (shouldLog) {
|
|
1689
|
+
console.warn(
|
|
1690
|
+
`[Remodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
return observed(models, "degraded");
|
|
1694
|
+
}
|
|
1695
|
+
const antigravity = cloudCodeAssist
|
|
1696
|
+
? parseAntigravityAvailableModels(bounded.value, discovery.maxModels)
|
|
1697
|
+
: undefined;
|
|
1698
|
+
if (cloudCodeAssist && !antigravity) {
|
|
1699
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1700
|
+
if (shouldLog) {
|
|
1701
|
+
console.warn(
|
|
1702
|
+
`[Remodex] Provider model discovery for "${name}" returned malformed CCA model data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
return observed(models, "degraded");
|
|
1706
|
+
}
|
|
1707
|
+
if (antigravity) {
|
|
1708
|
+
const live = antigravity.map(model => applyProviderConfigHints(name, prov, {
|
|
1709
|
+
id: model.id,
|
|
1710
|
+
provider: name,
|
|
1711
|
+
// CCA omits an exact effort contract. Keep that evidence unknown so a trusted registry or
|
|
1712
|
+
// bundled mapping can fill known models, while genuinely new models remain parameter-free.
|
|
1713
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
1714
|
+
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
|
|
1715
|
+
}, contextCap, configuredProvider, reasoningRegistry ?? null));
|
|
1716
|
+
if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
|
|
1717
|
+
markProviderDiscoveryOk(name, live.length);
|
|
1718
|
+
return observed(live, "authoritative");
|
|
1719
|
+
}
|
|
1720
|
+
const extracted = extractProviderModelItems(bounded.value, discovery);
|
|
1721
|
+
if (!extracted.ok) {
|
|
1722
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1723
|
+
const diagnostic: Record<ModelDiscoveryResponseFailure, string> = {
|
|
1724
|
+
response_too_large: "returned an oversized 2xx response",
|
|
1725
|
+
invalid_json: "returned invalid JSON in a 2xx response",
|
|
1726
|
+
invalid_shape: "returned malformed 2xx data",
|
|
1727
|
+
too_many_models: `exceeded the ${discovery.maxModels}-row model limit`,
|
|
1728
|
+
};
|
|
1729
|
+
if (shouldLog) {
|
|
1730
|
+
console.warn(
|
|
1731
|
+
`[Remodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
return observed(models, "degraded");
|
|
1735
|
+
}
|
|
1736
|
+
const items = extracted.items;
|
|
1737
|
+
const live = items.map(m => {
|
|
1738
|
+
const ownedBy = boundedOwnedBy(m.owned_by);
|
|
1739
|
+
return applyProviderConfigHints(name, prov, {
|
|
1740
|
+
id: m.id,
|
|
1741
|
+
provider: name,
|
|
1742
|
+
...(ownedBy ? { owned_by: ownedBy } : {}),
|
|
1743
|
+
...catalogHintsFromModelsApiItem(
|
|
1744
|
+
name,
|
|
1745
|
+
m,
|
|
1746
|
+
reasoningRegistry?.reasoningMetadataFormat ?? null,
|
|
1747
|
+
),
|
|
1748
|
+
}, contextCap, configuredProvider, reasoningRegistry ?? null);
|
|
1749
|
+
})
|
|
1750
|
+
.filter(m => shouldExposeProviderModel(name, m.id));
|
|
1751
|
+
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
|
|
1752
|
+
// `live`; otherwise configured entries would be reported as discovered ones.
|
|
1753
|
+
const liveModelCount = live.length;
|
|
1754
|
+
const liveIds = new Set(live.map(m => m.id));
|
|
1755
|
+
// Dated-release aliases (Anthropic pattern): older models may appear in the live catalog
|
|
1756
|
+
// ONLY under their dated id (claude-haiku-4-5-20251001) while the config names the
|
|
1757
|
+
// API-valid alias (claude-haiku-4-5). Such aliases are real, callable models — keep them
|
|
1758
|
+
// in the authoritative catalog (alias id, hints from the dated live entry) instead of
|
|
1759
|
+
// dropping them and warning on every poll.
|
|
1760
|
+
const droppedConfiguredIds: string[] = [];
|
|
1761
|
+
for (const m of configured) {
|
|
1762
|
+
if (liveIds.has(m.id)) continue;
|
|
1763
|
+
const dated = live.find(l => isDatedVariantId(l.id, m.id));
|
|
1764
|
+
if (dated) {
|
|
1765
|
+
// Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
|
|
1766
|
+
live.push(applyProviderConfigHints(
|
|
1767
|
+
name,
|
|
1768
|
+
prov,
|
|
1769
|
+
{ ...dated, id: m.id },
|
|
1770
|
+
contextCap,
|
|
1771
|
+
configuredProvider,
|
|
1772
|
+
reasoningRegistry ?? null,
|
|
1773
|
+
));
|
|
1774
|
+
} else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
|
|
1775
|
+
live.push(m);
|
|
1776
|
+
} else {
|
|
1777
|
+
droppedConfiguredIds.push(m.id);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (live.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
|
|
1781
|
+
console.warn(
|
|
1782
|
+
`[Remodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
|
|
1783
|
+
);
|
|
1784
|
+
} else if (droppedConfiguredIds.length > 0
|
|
1785
|
+
&& name !== OPENAI_API_PROVIDER_ID
|
|
1786
|
+
&& !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
|
|
1787
|
+
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
1788
|
+
}
|
|
1789
|
+
if (!setCached(name, live, Date.now(), cacheGeneration)) return observed(configured, "degraded");
|
|
1790
|
+
markProviderDiscoveryOk(name, liveModelCount);
|
|
1791
|
+
return observed(live, "authoritative");
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
if (error instanceof ProviderOutboundPolicyError) {
|
|
1794
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" });
|
|
1795
|
+
if (shouldLog) {
|
|
1796
|
+
console.warn(
|
|
1797
|
+
`[Remodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
return observed(models, "degraded");
|
|
1801
|
+
}
|
|
1802
|
+
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" });
|
|
1803
|
+
if (shouldLog) {
|
|
1804
|
+
console.warn(
|
|
1805
|
+
`[Remodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
return observed(models, "degraded");
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
export async function fetchProviderModels(
|
|
1813
|
+
name: string,
|
|
1814
|
+
prov: OcxProviderConfig,
|
|
1815
|
+
ttlMs: number,
|
|
1816
|
+
contextCap?: number,
|
|
1817
|
+
): Promise<CatalogModel[]> {
|
|
1818
|
+
const captured = captureProviderGather(name, prov, refreshingModelsAuthResolver);
|
|
1819
|
+
return (await fetchProviderModelsWithAuth(
|
|
1820
|
+
captured,
|
|
1821
|
+
ttlMs,
|
|
1822
|
+
contextCap,
|
|
1823
|
+
refreshingModelsAuthResolver,
|
|
1824
|
+
)).models;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
|
|
1828
|
+
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
1829
|
+
return true;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
export function shouldRetainConfiguredProviderModel(providerName: string, modelId: string): boolean {
|
|
1833
|
+
if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
|
|
1834
|
+
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
export function filterCatalogVisibleModels(
|
|
1839
|
+
models: CatalogModel[],
|
|
1840
|
+
config: Pick<OcxConfig, "disabledModels" | "providers" | "modelSourceVisibility">,
|
|
1841
|
+
): CatalogModel[] {
|
|
1842
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
1843
|
+
const allowByProvider = new Map<string, Set<string>>();
|
|
1844
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
1845
|
+
const sel = prov.selectedModels;
|
|
1846
|
+
if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
|
|
1847
|
+
}
|
|
1848
|
+
return models.filter(m => {
|
|
1849
|
+
if (!modelSourceVisible(config, m.provider)) return false;
|
|
1850
|
+
const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
|
|
1851
|
+
// disabledModels may be stored raw (canonical) or encoded (legacy UI writes).
|
|
1852
|
+
for (const stored of disabled) {
|
|
1853
|
+
// Combo management stores the public alias, while canonical `combo/<id>` references
|
|
1854
|
+
// remain valid for backward compatibility through slugEquals below.
|
|
1855
|
+
if (m.alias !== undefined && stored === catalogModelSlug(m) && !nativeAlias) return false;
|
|
1856
|
+
if (slugEquals(stored, m.provider, m.id)) return false;
|
|
1857
|
+
}
|
|
1858
|
+
const allow = allowByProvider.get(m.provider);
|
|
1859
|
+
return !allow || allow.has(m.id);
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
export async function gatherRoutedModels(
|
|
1864
|
+
config: OcxConfig,
|
|
1865
|
+
options?: GatherRoutedModelsOptions,
|
|
1866
|
+
): Promise<CatalogModel[]> {
|
|
1867
|
+
return gatherRoutedModelsWithAuth(
|
|
1868
|
+
config,
|
|
1869
|
+
`refreshing:${gatherFlightKey(config)}`,
|
|
1870
|
+
() => refreshingModelsAuthResolver,
|
|
1871
|
+
options,
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Catalog-gather model discovery using only auth-store bytes already captured by the
|
|
1877
|
+
* filesystem-evidence owner. This entry point never reaches the refreshing resolver.
|
|
1878
|
+
*/
|
|
1879
|
+
export async function gatherRoutedModelsForCatalogGather(
|
|
1880
|
+
config: OcxConfig,
|
|
1881
|
+
evidence: CatalogGatherProviderAuthEvidence,
|
|
1882
|
+
options?: GatherRoutedModelsOptions,
|
|
1883
|
+
): Promise<CatalogModel[]> {
|
|
1884
|
+
const authStoreBuffer = evidence.authStoreBuffer === null
|
|
1885
|
+
? null
|
|
1886
|
+
: Uint8Array.from(evidence.authStoreBuffer);
|
|
1887
|
+
const authIdentity = authStoreBuffer === null
|
|
1888
|
+
? "absent"
|
|
1889
|
+
: keyedGatherBytesIdentity("catalog-observed-auth-v1", authStoreBuffer);
|
|
1890
|
+
return gatherRoutedModelsWithAuth(
|
|
1891
|
+
config,
|
|
1892
|
+
`observed:${authIdentity}:${gatherFlightKey(config)}`,
|
|
1893
|
+
outcomes => observedModelsAuthResolver(authStoreBuffer, outcomes),
|
|
1894
|
+
options,
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async function gatherRoutedModelsWithAuth(
|
|
1899
|
+
config: OcxConfig,
|
|
1900
|
+
key: string,
|
|
1901
|
+
createAuthResolver: ModelsAuthResolverFactory,
|
|
1902
|
+
options?: GatherRoutedModelsOptions,
|
|
1903
|
+
): Promise<CatalogModel[]> {
|
|
1904
|
+
const capture = captureGatherFlight(config, createAuthResolver);
|
|
1905
|
+
const bucket = gatherInflight.get(key) ?? [];
|
|
1906
|
+
let entry = bucket.find(candidate => (
|
|
1907
|
+
candidate.discoveryPolicyIdentity === capture.discoveryPolicyIdentity
|
|
1908
|
+
&& candidate.authIdentity === capture.authIdentity
|
|
1909
|
+
&& candidate.providerGraphIdentity === capture.providerGraphIdentity
|
|
1910
|
+
));
|
|
1911
|
+
if (!entry) {
|
|
1912
|
+
const lease = gatherGate.tryAcquire();
|
|
1913
|
+
if (!lease) throw new CatalogGatherBusyError();
|
|
1914
|
+
// Claim the slot synchronously before any await so same-key callers join this flight.
|
|
1915
|
+
// Distinct authorities retain separate entries even when their legacy bucket matches.
|
|
1916
|
+
let ownedEntry!: GatherInflightEntry;
|
|
1917
|
+
const flight = gatherRoutedModelsUncached(config, capture).finally(() => {
|
|
1918
|
+
const current = gatherInflight.get(key);
|
|
1919
|
+
const index = current?.indexOf(ownedEntry) ?? -1;
|
|
1920
|
+
if (current && index >= 0) current.splice(index, 1);
|
|
1921
|
+
if (current?.length === 0) gatherInflight.delete(key);
|
|
1922
|
+
lease.release();
|
|
1923
|
+
});
|
|
1924
|
+
ownedEntry = Object.freeze({
|
|
1925
|
+
discoveryPolicyIdentity: capture.discoveryPolicyIdentity,
|
|
1926
|
+
authIdentity: capture.authIdentity,
|
|
1927
|
+
providerGraphIdentity: capture.providerGraphIdentity,
|
|
1928
|
+
promise: flight,
|
|
1929
|
+
});
|
|
1930
|
+
bucket.push(ownedEntry);
|
|
1931
|
+
gatherInflight.set(key, bucket);
|
|
1932
|
+
entry = ownedEntry;
|
|
1933
|
+
}
|
|
1934
|
+
const {
|
|
1935
|
+
models,
|
|
1936
|
+
comboOmissions,
|
|
1937
|
+
providerAuthOutcomes,
|
|
1938
|
+
providerModelOutcomes,
|
|
1939
|
+
discoveryPolicySnapshots,
|
|
1940
|
+
} = await entry.promise;
|
|
1941
|
+
if (options?.comboOmissions) {
|
|
1942
|
+
options.comboOmissions.length = 0;
|
|
1943
|
+
options.comboOmissions.push(...comboOmissions);
|
|
1944
|
+
}
|
|
1945
|
+
if (options?.providerAuthOutcomes) {
|
|
1946
|
+
options.providerAuthOutcomes.length = 0;
|
|
1947
|
+
options.providerAuthOutcomes.push(...providerAuthOutcomes);
|
|
1948
|
+
}
|
|
1949
|
+
if (options?.providerModelOutcomes) {
|
|
1950
|
+
options.providerModelOutcomes.length = 0;
|
|
1951
|
+
options.providerModelOutcomes.push(...providerModelOutcomes);
|
|
1952
|
+
}
|
|
1953
|
+
if (options?.discoveryPolicySnapshots) {
|
|
1954
|
+
options.discoveryPolicySnapshots.length = 0;
|
|
1955
|
+
options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots);
|
|
1956
|
+
}
|
|
1957
|
+
return models;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
async function gatherRoutedModelsUncached(
|
|
1961
|
+
config: OcxConfig,
|
|
1962
|
+
capture: GatherFlightCapture,
|
|
1963
|
+
): Promise<GatherFlightResult> {
|
|
1964
|
+
// Flight-local list: joiners copy from the resolved promise, not a process-global last write.
|
|
1965
|
+
const localOmissions: ComboCatalogOmission[] = [];
|
|
1966
|
+
const localProviderAuthOutcomes = capture.providerAuthOutcomes;
|
|
1967
|
+
const resolveAuth = capture.authResolver;
|
|
1968
|
+
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
1969
|
+
// Persisted provider entries can predate newer registry fields (noVisionModels,
|
|
1970
|
+
// modelInputModalities, ...). The ROUTER merges registry seeds at request time
|
|
1971
|
+
// (routedProviderConfig), so the proxy behaves correctly — the catalog listing must see the
|
|
1972
|
+
// same merged view or its advertisements drift from actual proxy behavior (e.g. a
|
|
1973
|
+
// vision-sidecar model advertised text-only, blocking image attachments app-side).
|
|
1974
|
+
// Enrich a CLONE: hydrated defaults must never leak into the persisted config.
|
|
1975
|
+
const activeProviders = capture.providers;
|
|
1976
|
+
const providerResults = await Promise.all(
|
|
1977
|
+
activeProviders.map(provider => fetchProviderModelsWithAuth(
|
|
1978
|
+
provider,
|
|
1979
|
+
ttlMs,
|
|
1980
|
+
providerContextCap(config, provider.name),
|
|
1981
|
+
resolveAuth,
|
|
1982
|
+
)),
|
|
1983
|
+
);
|
|
1984
|
+
const lists = providerResults.map(result => result.models);
|
|
1985
|
+
const apiAugmented = augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
1986
|
+
lists.flat(),
|
|
1987
|
+
config,
|
|
1988
|
+
capture.openAiApiPolicy,
|
|
1989
|
+
);
|
|
1990
|
+
const all = augmentRoutedModelsWithMetadata(apiAugmented, activeProviders.map(provider => provider.name), config.providers, config)
|
|
1991
|
+
// Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
|
|
1992
|
+
// intentionally mirrors Cursor's public model table, including Gemini image preview, so the
|
|
1993
|
+
// exposure decision goes through shouldExposeRoutedModel (single choke point).
|
|
1994
|
+
.filter(shouldExposeRoutedModel);
|
|
1995
|
+
const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model]));
|
|
1996
|
+
// [Decision Log]
|
|
1997
|
+
// - 목적과 의도: 콤보 타겟에 native OpenAI(Codex login) 모델이 포함될 때 카탈로그에서
|
|
1998
|
+
// 누락되는 버그(issue #268)를 수정. "openai" provider는 forward-auth(Codex login
|
|
1999
|
+
// passthrough)이므로 fetchProviderModels가 항상 []를 반환하고, native slugs는
|
|
2000
|
+
// 별도 정적 경로(nativeOpenAiSlugs)로만 노출됨. 따라서 memberByKey에
|
|
2001
|
+
// openai/<slug> 키가 존재하지 않아 콤보가 조용히 drop됨.
|
|
2002
|
+
// - 기존 구현 및 제약 조건: memberByKey는 routed provider /models fetch 결과로만 구성.
|
|
2003
|
+
// - 검토한 주요 대안: (A) native slugs를 all 배열에 직접 push — /v1/models와 온디스크
|
|
2004
|
+
// 카탈로그에서 native 모델이 중복 노출되는 부작용 발생. (B) memberByKey에만 synthetic
|
|
2005
|
+
// CatalogModel을 주입 — 콤보 멤버 해석에만 사용하고 all에는 추가하지 않으므로 기존
|
|
2006
|
+
// 노출 경로에 영향 없음.
|
|
2007
|
+
// - 선택한 방식: (B) — synthetic entries를 memberByKey에만 주입.
|
|
2008
|
+
// - 다른 대안 대신 이 방식을 선택한 이유: 기존 native 모델 노출 경로(/v1/models, 온디스크
|
|
2009
|
+
// 카탈로그 sync, management API)를 전혀 변경하지 않고 콤보 resolution만 수선하기 때문.
|
|
2010
|
+
// - 장점, 단점 및 영향: 장점 — 최소 수정, 기존 경로 무변경. 단점 — synthetic entries의
|
|
2011
|
+
// capability 데이터가 static/upstream snapshot 기반이므로, 사용자가 커스텀 config
|
|
2012
|
+
// 힌트(modelContextWindows 등)로 native 모델의 context window를 오버라이드한 경우
|
|
2013
|
+
// 반영되지 않음. 하지만 nativeOpenAiContextWindow가 이미 config 오버라이드를
|
|
2014
|
+
// 우선시하므로 실제 충돌 가능성은 낮음.
|
|
2015
|
+
if (!hasComboTargets(config)) {
|
|
2016
|
+
// Skip the native slug injection entirely when no combos are configured — avoids
|
|
2017
|
+
// calling nativeOpenAiSlugs() (which reads the live Codex catalog from disk) for
|
|
2018
|
+
// configs that will never need it.
|
|
2019
|
+
} else {
|
|
2020
|
+
const disabled = disabledNativeSlugs(config);
|
|
2021
|
+
const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => {
|
|
2022
|
+
const combo = getCombo(config, id);
|
|
2023
|
+
return combo?.targets.flatMap(target => (
|
|
2024
|
+
target.provider === "openai" ? [target.model] : []
|
|
2025
|
+
)) ?? [];
|
|
2026
|
+
}));
|
|
2027
|
+
for (const slug of nativeOpenAiSlugs()) {
|
|
2028
|
+
// A bare native disable key hides the native row, not a combo that targets it.
|
|
2029
|
+
// Keep synthetic native metadata available to those combos.
|
|
2030
|
+
if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue;
|
|
2031
|
+
const contextWindow = nativeOpenAiContextWindow(slug);
|
|
2032
|
+
if (contextWindow === undefined) continue;
|
|
2033
|
+
const synthetic: CatalogModel = {
|
|
2034
|
+
provider: "openai",
|
|
2035
|
+
id: slug,
|
|
2036
|
+
owned_by: "openai",
|
|
2037
|
+
contextWindow,
|
|
2038
|
+
maxInputTokens: contextWindow,
|
|
2039
|
+
inputModalities: nativeInputModalities(slug),
|
|
2040
|
+
reasoningEfforts: nativeReasoningEfforts(slug),
|
|
2041
|
+
...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
|
|
2042
|
+
};
|
|
2043
|
+
const key = `openai/${slug}`;
|
|
2044
|
+
// Only inject when not already present from a routed provider (an API-key
|
|
2045
|
+
// "openai" provider could shadow the native one).
|
|
2046
|
+
if (!memberByKey.has(key)) memberByKey.set(key, synthetic);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
// Enriched (registry-hydrated) provider clones — shared by combo member synthesis and
|
|
2050
|
+
// custom-model vision-sidecar inheritance so both see the same merged registry view.
|
|
2051
|
+
const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider]));
|
|
2052
|
+
for (const id of listComboIds(config)) {
|
|
2053
|
+
const combo = getCombo(config, id);
|
|
2054
|
+
if (!combo) continue;
|
|
2055
|
+
const nativeContextWindow = combo.nativeAlias && combo.alias
|
|
2056
|
+
? nativeOpenAiContextWindow(combo.alias)
|
|
2057
|
+
: undefined;
|
|
2058
|
+
const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
|
|
2059
|
+
? {
|
|
2060
|
+
contextWindow: nativeContextWindow,
|
|
2061
|
+
inputModalities: nativeInputModalities(combo.alias),
|
|
2062
|
+
reasoningEfforts: nativeReasoningEfforts(combo.alias),
|
|
2063
|
+
}
|
|
2064
|
+
: undefined;
|
|
2065
|
+
const members = combo.targets
|
|
2066
|
+
.map(target => resolveComboCatalogMember(
|
|
2067
|
+
target,
|
|
2068
|
+
memberByKey,
|
|
2069
|
+
enrichedByName,
|
|
2070
|
+
providerContextCap(config, target.provider),
|
|
2071
|
+
nativeAliasFallback,
|
|
2072
|
+
))
|
|
2073
|
+
.filter((member): member is CatalogModel => member !== undefined);
|
|
2074
|
+
const derived = deriveComboCatalogModel(id, combo, members);
|
|
2075
|
+
if (derived) {
|
|
2076
|
+
const nativeDefault = combo.nativeAlias && combo.alias
|
|
2077
|
+
? nativeDefaultReasoningEffort(combo.alias)
|
|
2078
|
+
: undefined;
|
|
2079
|
+
if (combo.defaultEffort === null
|
|
2080
|
+
&& nativeDefault
|
|
2081
|
+
&& derived.reasoningEfforts?.includes(nativeDefault)) {
|
|
2082
|
+
derived.defaultReasoningEffort = nativeDefault;
|
|
2083
|
+
}
|
|
2084
|
+
all.push(derived);
|
|
2085
|
+
}
|
|
2086
|
+
else warnUncataloguedComboOnce(id, combo, members, localOmissions);
|
|
2087
|
+
}
|
|
2088
|
+
replaceLastComboCatalogOmissions(localOmissions);
|
|
2089
|
+
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
2090
|
+
// Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row
|
|
2091
|
+
// with the same slug below, so that row's provider capability metadata is the inheritance source.
|
|
2092
|
+
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
|
|
2093
|
+
const customModels = (config.customModels ?? []).map(cm => {
|
|
2094
|
+
const rawProvider = config.providers[cm.provider];
|
|
2095
|
+
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
|
|
2096
|
+
const base: CatalogModel = {
|
|
2097
|
+
id: cm.modelId,
|
|
2098
|
+
provider: cm.provider,
|
|
2099
|
+
catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
|
|
2100
|
+
// Display-only label: never feeds routing (customModels are keyed by routedSlug below).
|
|
2101
|
+
...(cm.displayName ? { displayName: cm.displayName } : {}),
|
|
2102
|
+
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
2103
|
+
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
2104
|
+
...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
|
|
2105
|
+
};
|
|
2106
|
+
// #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
|
|
2107
|
+
// row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
|
|
2108
|
+
// context, ...) so the generated catalog keeps advertising what the router actually provides.
|
|
2109
|
+
// Explicit custom fields win by construction; this only fills gaps. Without it a
|
|
2110
|
+
// noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one,
|
|
2111
|
+
// which Codex then rejects for spawn_agent with effort "none".
|
|
2112
|
+
const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
|
|
2113
|
+
const merged: CatalogModel = replaced ? {
|
|
2114
|
+
...base,
|
|
2115
|
+
...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
|
|
2116
|
+
...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
|
|
2117
|
+
...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
|
|
2118
|
+
...(base.reasoningControl === undefined && replaced.reasoningControl !== undefined ? { reasoningControl: replaced.reasoningControl } : {}),
|
|
2119
|
+
...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
|
|
2120
|
+
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined ? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
|
|
2121
|
+
...(base.reasoningRequired === undefined && replaced.reasoningRequired !== undefined ? { reasoningRequired: replaced.reasoningRequired } : {}),
|
|
2122
|
+
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
|
|
2123
|
+
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
|
|
2124
|
+
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
|
|
2125
|
+
...(base.serviceTiers === undefined && replaced.serviceTiers !== undefined
|
|
2126
|
+
? { serviceTiers: replaced.serviceTiers.map((tier) => ({ ...tier })) }
|
|
2127
|
+
: {}),
|
|
2128
|
+
...(base.additionalSpeedTiers === undefined && replaced.additionalSpeedTiers !== undefined
|
|
2129
|
+
? { additionalSpeedTiers: [...replaced.additionalSpeedTiers] }
|
|
2130
|
+
: {}),
|
|
2131
|
+
...(base.defaultServiceTier === undefined && replaced.defaultServiceTier !== undefined
|
|
2132
|
+
? { defaultServiceTier: replaced.defaultServiceTier }
|
|
2133
|
+
: {}),
|
|
2134
|
+
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
|
|
2135
|
+
} : base;
|
|
2136
|
+
// Vision-sidecar coverage ONLY: if the custom model is in the enriched provider's
|
|
2137
|
+
// noVisionModels, advertise image input so the Codex app lets images reach the sidecar
|
|
2138
|
+
// (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
|
|
2139
|
+
// user override, so their explicit contextWindow / inputModalities / reasoning fields must be
|
|
2140
|
+
// preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
|
|
2141
|
+
const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
|
|
2142
|
+
if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
|
|
2143
|
+
const current = merged.inputModalities ?? ["text"];
|
|
2144
|
+
if (!current.includes("image")) {
|
|
2145
|
+
return { ...merged, inputModalities: [...current, "image"] };
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return merged;
|
|
2149
|
+
});
|
|
2150
|
+
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
2151
|
+
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
2152
|
+
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
2153
|
+
const providerModelOutcomes = providerResults.map(result => (
|
|
2154
|
+
result.outcome.provider === OPENAI_API_PROVIDER_ID
|
|
2155
|
+
&& capture.openAiApiPolicy.state === "captured"
|
|
2156
|
+
&& capture.openAiApiPolicy.models !== undefined
|
|
2157
|
+
? { provider: result.outcome.provider, state: "authoritative" as const }
|
|
2158
|
+
: result.outcome
|
|
2159
|
+
));
|
|
2160
|
+
return {
|
|
2161
|
+
models: [...deduped, ...customModels],
|
|
2162
|
+
comboOmissions: localOmissions,
|
|
2163
|
+
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
2164
|
+
providerModelOutcomes,
|
|
2165
|
+
discoveryPolicySnapshots: capture.discoveryPolicySnapshots,
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
2170
|
+
models: CatalogModel[],
|
|
2171
|
+
config: OcxConfig,
|
|
2172
|
+
): CatalogModel[] {
|
|
2173
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
2174
|
+
if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models;
|
|
2175
|
+
return augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
2176
|
+
models,
|
|
2177
|
+
config,
|
|
2178
|
+
captureTrustedOpenAiApiPolicy(OPENAI_API_PROVIDER_ID, true),
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
2183
|
+
models: CatalogModel[],
|
|
2184
|
+
config: OcxConfig,
|
|
2185
|
+
policy: CatalogTrustedOpenAiApiPolicySnapshot,
|
|
2186
|
+
): CatalogModel[] {
|
|
2187
|
+
if (policy.state !== "captured" || !policy.models) return models;
|
|
2188
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
2189
|
+
if (!configured || configured.disabled === true) return models;
|
|
2190
|
+
|
|
2191
|
+
const existingById = new Map(
|
|
2192
|
+
models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]),
|
|
2193
|
+
);
|
|
2194
|
+
const trustedRows = policy.models.map((id): CatalogModel => {
|
|
2195
|
+
const officialContext = policy.modelContextWindows?.[id];
|
|
2196
|
+
const officialMaxInput = policy.modelMaxInputTokens?.[id];
|
|
2197
|
+
const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow;
|
|
2198
|
+
const userMaxInput = configured.modelMaxInputTokens?.[id];
|
|
2199
|
+
const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID);
|
|
2200
|
+
const contextWindow = typeof officialContext === "number"
|
|
2201
|
+
? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
|
|
2202
|
+
: undefined;
|
|
2203
|
+
const maxInputTokens = typeof officialMaxInput === "number"
|
|
2204
|
+
? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
|
|
2205
|
+
: undefined;
|
|
2206
|
+
return {
|
|
2207
|
+
provider: OPENAI_API_PROVIDER_ID,
|
|
2208
|
+
id,
|
|
2209
|
+
owned_by: OPENAI_API_PROVIDER_ID,
|
|
2210
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
2211
|
+
...(maxInputTokens ? { maxInputTokens } : {}),
|
|
2212
|
+
...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
|
|
2213
|
+
...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
|
|
2214
|
+
};
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
for (const trusted of trustedRows) {
|
|
2218
|
+
const live = existingById.get(trusted.id);
|
|
2219
|
+
if (!live) continue;
|
|
2220
|
+
const liveSignature = normalizedOpenAiApiSignature(live);
|
|
2221
|
+
const trustedSignature = normalizedOpenAiApiSignature(trusted);
|
|
2222
|
+
if (liveSignature === trustedSignature) continue;
|
|
2223
|
+
const warningKey = `${trusted.provider}/${trusted.id}\n${liveSignature}\n${trustedSignature}`;
|
|
2224
|
+
if (openAiApiCollisionWarnings.has(warningKey)) continue;
|
|
2225
|
+
openAiApiCollisionWarnings.add(warningKey);
|
|
2226
|
+
console.warn(`[Remodex] replacing conflicting live OpenAI API metadata for ${trusted.provider}/${trusted.id} with trusted registry metadata`);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
return [
|
|
2230
|
+
...models.filter(model => model.provider !== OPENAI_API_PROVIDER_ID),
|
|
2231
|
+
...trustedRows,
|
|
2232
|
+
];
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
export function augmentRoutedModelsWithMetadata(
|
|
2236
|
+
models: CatalogModel[],
|
|
2237
|
+
providerNames: string[],
|
|
2238
|
+
providers?: Record<string, OcxProviderConfig>,
|
|
2239
|
+
caps?: Pick<OcxConfig, "providerContextCaps">,
|
|
2240
|
+
): CatalogModel[] {
|
|
2241
|
+
const out = [...models];
|
|
2242
|
+
const seen = new Set(out.map(m => `${m.provider}/${m.id}`));
|
|
2243
|
+
for (const provider of providerNames) {
|
|
2244
|
+
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
2245
|
+
if (providers?.[provider]?.liveModels === false) continue;
|
|
2246
|
+
const jawcodeProvider = resolveMetadataProvider(provider);
|
|
2247
|
+
if (!jawcodeProvider) continue;
|
|
2248
|
+
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
2249
|
+
const key = `${provider}/${meta.id}`;
|
|
2250
|
+
if (seen.has(key)) continue;
|
|
2251
|
+
seen.add(key);
|
|
2252
|
+
const contextCap = caps ? providerContextCap(caps, provider) : undefined;
|
|
2253
|
+
const model: CatalogModel = {
|
|
2254
|
+
provider,
|
|
2255
|
+
id: meta.id,
|
|
2256
|
+
owned_by: provider,
|
|
2257
|
+
...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}),
|
|
2258
|
+
...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}),
|
|
2259
|
+
};
|
|
2260
|
+
out.push({
|
|
2261
|
+
...model,
|
|
2262
|
+
...(providers?.[provider] ? applyProviderConfigHints(provider, providers[provider], model, contextCap) : {}),
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return out;
|
|
2267
|
+
}
|