@iislee/opencodex 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -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 +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +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/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.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/gemini.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-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/kiro.svg +14 -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 +1 -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 +102 -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 +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -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 +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -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 +1214 -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 +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -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 +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -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 +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -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/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -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 +381 -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 +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -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 +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -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 +829 -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/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 +202 -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 +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -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/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -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 +514 -0
- package/src/lib/winsw.ts +375 -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/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -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 +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -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 +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -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 +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -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/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -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 +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -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 +471 -0
- package/src/service.ts +2554 -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 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -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 +164 -0
- package/src/usage/log.ts +658 -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 +125 -0
- package/src/vision/index.ts +467 -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 +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Codex parses a catalog entry's `input_modalities` as a closed enum, and one out-of-enum
|
|
6
|
+
* value makes it reject the ENTIRE catalog file — plugins, apps and MCP servers all stop
|
|
7
|
+
* loading over one model's metadata (#759).
|
|
8
|
+
*
|
|
9
|
+
* The catalog writer normalizes on the way out, but a rejected value stored here would still
|
|
10
|
+
* be handed back to the GUI and CLI as if it were real, and the offline `ocx models add` path
|
|
11
|
+
* already refuses it. Validate at ingress so all three paths agree.
|
|
12
|
+
*/
|
|
13
|
+
const ALLOWED_INPUT_MODALITIES = new Set(["text", "image", "audio"]);
|
|
14
|
+
|
|
15
|
+
function readInputModalities(raw: unknown): { values?: string[]; error?: string } {
|
|
16
|
+
if (raw === undefined) return {};
|
|
17
|
+
if (!Array.isArray(raw)) return { error: "inputModalities must be an array" };
|
|
18
|
+
// Reject non-strings rather than filtering them out. Dropping them silently accepted a
|
|
19
|
+
// malformed POST and, worse, let a PUT of `[42]` clear the stored modalities while
|
|
20
|
+
// answering 200 — the opposite of the contract this validator exists to state. An empty
|
|
21
|
+
// array stays valid: that is how `ocx models edit --modalities -` clears the field.
|
|
22
|
+
const rejected: string[] = [];
|
|
23
|
+
for (const value of raw) {
|
|
24
|
+
if (typeof value !== "string") return { error: "inputModalities must contain only strings" };
|
|
25
|
+
if (!ALLOWED_INPUT_MODALITIES.has(value)) rejected.push(value);
|
|
26
|
+
}
|
|
27
|
+
if (rejected.length > 0) {
|
|
28
|
+
return { error: `unsupported input modality: ${rejected.join(", ")} (allowed: text, image, audio)` };
|
|
29
|
+
}
|
|
30
|
+
return { values: raw as string[] };
|
|
31
|
+
}
|
|
32
|
+
import type { CatalogModel } from "../../codex/catalog";
|
|
33
|
+
import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
34
|
+
import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
|
|
35
|
+
import { getProviderLiveModelCount } from "../../codex/model-cache";
|
|
36
|
+
import {
|
|
37
|
+
DEFAULT_SUBAGENT_MODELS,
|
|
38
|
+
codexAutoStartEnabled,
|
|
39
|
+
hasOwnProvider,
|
|
40
|
+
isValidProviderName,
|
|
41
|
+
multiAgentGuidanceEnabled,
|
|
42
|
+
providerBaseUrlConfigError,
|
|
43
|
+
providerHeadersConfigError,
|
|
44
|
+
saveConfigPreservingClaudeCode,
|
|
45
|
+
} from "../../config";
|
|
46
|
+
import {
|
|
47
|
+
clearLoginState,
|
|
48
|
+
getLoginStatus,
|
|
49
|
+
isPublicOAuthProvider,
|
|
50
|
+
listOAuthProviders,
|
|
51
|
+
startLoginFlow,
|
|
52
|
+
submitManualLoginCode,
|
|
53
|
+
upsertOAuthProvider,
|
|
54
|
+
} from "../../oauth";
|
|
55
|
+
import { removeCredential } from "../../oauth/store";
|
|
56
|
+
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
57
|
+
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
58
|
+
import { deriveProviderPresets } from "../../providers/derive";
|
|
59
|
+
import { providerCodexAccountMode } from "../../providers/registry";
|
|
60
|
+
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
61
|
+
import { COMBO_NAMESPACE, comboModelId, comboPublicModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
62
|
+
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
63
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
64
|
+
import { clearThreadAccountMap } from "../../codex/routing";
|
|
65
|
+
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
66
|
+
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
67
|
+
import { resolveCodexHomeDir } from "../../codex/home";
|
|
68
|
+
import { readUsageEntries } from "../../usage/log";
|
|
69
|
+
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
70
|
+
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
71
|
+
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
72
|
+
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
73
|
+
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
74
|
+
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
75
|
+
import {
|
|
76
|
+
clearDebugSettings,
|
|
77
|
+
clearDebugSetting,
|
|
78
|
+
getDebugSettings,
|
|
79
|
+
setDebugSettings,
|
|
80
|
+
type DebugFlag,
|
|
81
|
+
} from "../../lib/debug-settings";
|
|
82
|
+
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
83
|
+
import { drainAndShutdown } from "../lifecycle";
|
|
84
|
+
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
85
|
+
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
86
|
+
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
87
|
+
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
|
|
88
|
+
import { applySystemEnvToggle } from "../system-env";
|
|
89
|
+
import {
|
|
90
|
+
EXPORT_CLIENTS,
|
|
91
|
+
EXPORT_CLIENT_IDS,
|
|
92
|
+
OPENCODE_PROVIDER_ID,
|
|
93
|
+
buildClientConfig,
|
|
94
|
+
isExportClientId,
|
|
95
|
+
opencodeProxyBaseUrl,
|
|
96
|
+
} from "../../clients/config-export";
|
|
97
|
+
import type {
|
|
98
|
+
ExportClientId,
|
|
99
|
+
ExportModel,
|
|
100
|
+
OpencodeGeneratedConfig,
|
|
101
|
+
PiGeneratedConfig,
|
|
102
|
+
} from "../../clients/config-export";
|
|
103
|
+
|
|
104
|
+
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
105
|
+
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
106
|
+
import type { ManagementContext } from "./context";
|
|
107
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
|
|
111
|
+
* that model plus the identity/visibility fields this boundary computes for every row
|
|
112
|
+
* regardless of source. `disabled` is always present; the rest vary by row origin.
|
|
113
|
+
*/
|
|
114
|
+
type ManagementModelRow = Partial<CatalogModel> & {
|
|
115
|
+
provider: string;
|
|
116
|
+
id: string;
|
|
117
|
+
namespaced: string;
|
|
118
|
+
disabled: boolean;
|
|
119
|
+
native?: boolean;
|
|
120
|
+
custom?: boolean;
|
|
121
|
+
customId?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
|
|
126
|
+
* models the GUI's Models tab shows — including this function's `disabled` computation,
|
|
127
|
+
* which the export core (src/clients/config-export.ts) deliberately does not perform.
|
|
128
|
+
*/
|
|
129
|
+
async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
|
|
130
|
+
const models = await fetchAllModels(config);
|
|
131
|
+
const disabled = new Set(config.disabledModels ?? []);
|
|
132
|
+
// Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
|
|
133
|
+
// from the static supported set so a disabled model stays listed and re-enableable.
|
|
134
|
+
const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
|
|
135
|
+
provider: "openai",
|
|
136
|
+
id: row.slug,
|
|
137
|
+
namespaced: row.slug,
|
|
138
|
+
disabled: row.disabled,
|
|
139
|
+
native: true,
|
|
140
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
141
|
+
}));
|
|
142
|
+
const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
|
|
143
|
+
const namespaced = routedSlug(cm.provider, cm.modelId);
|
|
144
|
+
return {
|
|
145
|
+
provider: cm.provider,
|
|
146
|
+
id: cm.modelId,
|
|
147
|
+
namespaced,
|
|
148
|
+
disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
|
|
149
|
+
custom: true,
|
|
150
|
+
customId: cm.id,
|
|
151
|
+
displayName: cm.displayName,
|
|
152
|
+
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
|
|
153
|
+
...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
const publicModels = uniqueCatalogModelsForPublicList(models);
|
|
157
|
+
const comboNamespaced = new Set(
|
|
158
|
+
publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
|
|
159
|
+
);
|
|
160
|
+
const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
|
|
161
|
+
// Custom metadata wins when a physical live/static row resolves to the same Codex-facing
|
|
162
|
+
// slug, while a combo keeps the same precedence it has in routing and /v1/models.
|
|
163
|
+
const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
|
|
164
|
+
const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
|
|
165
|
+
// Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
|
|
166
|
+
const namespaced = catalogModelSlug(m);
|
|
167
|
+
if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
|
|
168
|
+
const contextCap = providerContextCap(config, m.provider);
|
|
169
|
+
return {
|
|
170
|
+
...m,
|
|
171
|
+
namespaced,
|
|
172
|
+
disabled: [...disabled].some(stored => (
|
|
173
|
+
stored === namespaced || slugEquals(stored, m.provider, m.id)
|
|
174
|
+
)),
|
|
175
|
+
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
176
|
+
};
|
|
177
|
+
}).filter((row): row is ManagementModelRow => row !== null);
|
|
178
|
+
return [...native, ...dedupedRouted, ...visibleCustomModels];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** `/api/models` row → the narrower input the client-config serializers accept. */
|
|
182
|
+
function toExportModel(row: ManagementModelRow): ExportModel {
|
|
183
|
+
return {
|
|
184
|
+
namespaced: row.namespaced,
|
|
185
|
+
provider: row.provider,
|
|
186
|
+
id: row.id,
|
|
187
|
+
...(row.native ? { native: true } : {}),
|
|
188
|
+
...(row.displayName ? { displayName: row.displayName } : {}),
|
|
189
|
+
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
190
|
+
...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Counts read back off the SERIALIZED document rather than recomputed from the input rows.
|
|
196
|
+
* `modelsWithoutLimits` drives a GUI line claiming "these models ship without limits", so it
|
|
197
|
+
* has to describe the bytes the user actually receives — a parallel reimplementation of the
|
|
198
|
+
* core's "authoritative context window" rule would be free to drift from it silently.
|
|
199
|
+
*/
|
|
200
|
+
function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
201
|
+
if (client === "opencode") {
|
|
202
|
+
const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
|
|
203
|
+
const entries = Object.values(models);
|
|
204
|
+
return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
|
|
205
|
+
}
|
|
206
|
+
const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
|
|
207
|
+
return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
211
|
+
const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
|
|
212
|
+
// A handler persists the exact config object passed in. Production defaults to
|
|
213
|
+
// the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
|
|
214
|
+
// bypass this seam with a dynamic config import — doing so replaced a user's
|
|
215
|
+
// ~/.opencodex/config.json with the `existing-uuid` test fixture.
|
|
216
|
+
const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
|
|
217
|
+
|
|
218
|
+
if (url.pathname === "/api/catalog" && req.method === "GET") {
|
|
219
|
+
const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
|
|
220
|
+
const catalog = readCatalog(readCodexCatalogPath());
|
|
221
|
+
if (!catalog) return jsonResponse({ error: "catalog not found" }, 404, req, config);
|
|
222
|
+
const headers: Record<string, string> = {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
...corsHeaders(req, config),
|
|
225
|
+
};
|
|
226
|
+
const { loadPersistedCodexRuntime } = await import("../../codex/runtime");
|
|
227
|
+
const version = loadPersistedCodexRuntime()?.selectedVersion;
|
|
228
|
+
if (version) headers["x-opencodex-codex-version"] = version;
|
|
229
|
+
return new Response(JSON.stringify(catalog), { status: 200, headers });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
233
|
+
return jsonResponse(await listManagementModelRows(config));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Client config document for OpenCode / Pi, built from the SAME function `ocx export`
|
|
238
|
+
* calls, so the bytes a user downloads here and the bytes they pipe from the CLI cannot
|
|
239
|
+
* disagree. Read-only: this route never writes the user's client config.
|
|
240
|
+
*/
|
|
241
|
+
if (url.pathname === "/api/client-config" && req.method === "GET") {
|
|
242
|
+
const requested = url.searchParams.get("client")?.trim() ?? "";
|
|
243
|
+
if (!isExportClientId(requested)) {
|
|
244
|
+
return jsonResponse(
|
|
245
|
+
{ error: `client must be one of: ${EXPORT_CLIENT_IDS.join(", ")}` },
|
|
246
|
+
400,
|
|
247
|
+
req,
|
|
248
|
+
config,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const spec = EXPORT_CLIENTS[requested];
|
|
252
|
+
let rows: ManagementModelRow[];
|
|
253
|
+
try {
|
|
254
|
+
rows = await listManagementModelRows(config);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// A partial or empty `models` block reads as a valid config while offering nothing,
|
|
257
|
+
// so a catalog failure is surfaced as unavailable rather than serialized. The
|
|
258
|
+
// catalog-busy error keeps its own 503 from handleManagementAPI.
|
|
259
|
+
if (error instanceof CatalogGatherBusyError) throw error;
|
|
260
|
+
return jsonResponse(
|
|
261
|
+
{ error: `model catalog unavailable: ${error instanceof Error ? error.message : String(error)}` },
|
|
262
|
+
503,
|
|
263
|
+
req,
|
|
264
|
+
config,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
// The export core does not filter visibility — it serializes what it is given. A model the
|
|
268
|
+
// user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
|
|
269
|
+
// client a selector the proxy refuses to route.
|
|
270
|
+
const models = rows.filter(row => !row.disabled).map(toExportModel);
|
|
271
|
+
const document = buildClientConfig(requested, {
|
|
272
|
+
baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
|
|
273
|
+
models,
|
|
274
|
+
config,
|
|
275
|
+
});
|
|
276
|
+
return jsonResponse({
|
|
277
|
+
client: spec.id,
|
|
278
|
+
filename: spec.filename,
|
|
279
|
+
destination: spec.destination(process.env),
|
|
280
|
+
apiKeyEnv: spec.apiKeyEnv,
|
|
281
|
+
exportHint: spec.exportHint,
|
|
282
|
+
...summarizeExportedModels(requested, document),
|
|
283
|
+
config: document,
|
|
284
|
+
}, 200, req, config);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Enable/disable models: which routed models Codex sees. PUT hides them from the catalog +
|
|
288
|
+
// /v1/models and invalidates Codex's 5-min models cache so it applies on the next turn.
|
|
289
|
+
if (url.pathname === "/api/disabled-models" && req.method === "PUT") {
|
|
290
|
+
let body: { models?: unknown };
|
|
291
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
292
|
+
const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
|
|
293
|
+
config.disabledModels = disabled;
|
|
294
|
+
persistConfig(config);
|
|
295
|
+
await refreshCodexCatalogBestEffort();
|
|
296
|
+
return jsonResponse({ ok: true, disabled });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// One user-facing visibility switch spans two persisted filters: a provider allowlist and the
|
|
300
|
+
// shared blocklist. Keep the update atomic so an interrupted request cannot expose a half-applied
|
|
301
|
+
// state. Native rows only use the blocklist; routed/custom rows also join a non-empty allowlist.
|
|
302
|
+
if (url.pathname === "/api/model-visibility" && req.method === "PUT") {
|
|
303
|
+
let parsedBody: unknown;
|
|
304
|
+
try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
305
|
+
if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid model visibility request" }, 400);
|
|
306
|
+
const body = parsedBody;
|
|
307
|
+
const scope = body.scope === "models" || body.scope === "provider" ? body.scope : null;
|
|
308
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
309
|
+
if (!scope || !provider || !isValidProviderName(provider) || typeof body.enabled !== "boolean" || !Array.isArray(body.targets)) {
|
|
310
|
+
return jsonResponse({ error: "invalid model visibility request" }, 400);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
|
|
314
|
+
const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
|
|
315
|
+
if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
|
|
316
|
+
return jsonResponse({ error: "unknown model visibility provider" }, 400);
|
|
317
|
+
}
|
|
318
|
+
const supportedNative = new Set(nativeModelRows(config).map(row => row.slug));
|
|
319
|
+
const targets: Array<{ id: string; native: boolean }> = [];
|
|
320
|
+
const seen = new Set<string>();
|
|
321
|
+
for (const value of body.targets) {
|
|
322
|
+
if (!isPlainRecord(value) || typeof value.id !== "string" || (value.native !== undefined && typeof value.native !== "boolean")) {
|
|
323
|
+
return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
324
|
+
}
|
|
325
|
+
const id = value.id.trim();
|
|
326
|
+
const native = value.native === true;
|
|
327
|
+
if (!id || (provider === "openai") !== native || (native && !supportedNative.has(id))) {
|
|
328
|
+
return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
329
|
+
}
|
|
330
|
+
const key = `${native ? "native" : "routed"}:${id}`;
|
|
331
|
+
if (!seen.has(key)) {
|
|
332
|
+
seen.add(key);
|
|
333
|
+
targets.push({ id, native });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
|
|
337
|
+
|
|
338
|
+
const knownComboSelectors = new Set(
|
|
339
|
+
Object.entries(config.combos ?? {}).flatMap(([id, combo]) => [
|
|
340
|
+
comboModelId(id),
|
|
341
|
+
comboPublicModelId(id, combo),
|
|
342
|
+
]),
|
|
343
|
+
);
|
|
344
|
+
const targetComboSelectors = new Map<string, Set<string>>();
|
|
345
|
+
if (isVirtualComboNamespace) {
|
|
346
|
+
for (const target of targets) {
|
|
347
|
+
const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
|
|
348
|
+
if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
|
|
349
|
+
targetComboSelectors.set(target.id, new Set([comboModelId(target.id), comboPublicModelId(target.id, combo)]));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
|
|
353
|
+
? stored === target.id
|
|
354
|
+
: isVirtualComboNamespace
|
|
355
|
+
? targetComboSelectors.get(target.id)!.has(stored)
|
|
356
|
+
: slugEquals(stored, provider, target.id);
|
|
357
|
+
|
|
358
|
+
let disabled = [...new Set(config.disabledModels ?? [])];
|
|
359
|
+
if (body.enabled) {
|
|
360
|
+
if (scope === "provider") {
|
|
361
|
+
if (providerConfig && !isVirtualComboNamespace) delete providerConfig.selectedModels;
|
|
362
|
+
if (isVirtualComboNamespace) {
|
|
363
|
+
disabled = disabled.filter(stored => !knownComboSelectors.has(stored));
|
|
364
|
+
} else {
|
|
365
|
+
const nativeIds = provider === "openai"
|
|
366
|
+
? disabledNativeSlugs({ disabledModels: disabled })
|
|
367
|
+
: new Set<string>();
|
|
368
|
+
disabled = disabled.filter(stored => (
|
|
369
|
+
knownComboSelectors.has(stored)
|
|
370
|
+
|| (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
|
|
371
|
+
));
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
if (!isVirtualComboNamespace && providerConfig?.selectedModels && providerConfig.selectedModels.length > 0) {
|
|
375
|
+
const additions = targets.filter(target => !target.native).map(target => target.id);
|
|
376
|
+
providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
|
|
377
|
+
}
|
|
378
|
+
disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
for (const target of targets) {
|
|
382
|
+
const canonical = target.native
|
|
383
|
+
? target.id
|
|
384
|
+
: isVirtualComboNamespace
|
|
385
|
+
? comboModelId(target.id)
|
|
386
|
+
: routedSlug(provider, target.id);
|
|
387
|
+
const alreadyDisabled = disabled.some(stored => matchesTarget(stored, target));
|
|
388
|
+
if (!alreadyDisabled) disabled.push(canonical);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
config.disabledModels = disabled;
|
|
393
|
+
persistConfig(config);
|
|
394
|
+
await refreshCodexCatalogBestEffort();
|
|
395
|
+
return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (url.pathname === "/api/custom-models" && req.method === "GET") {
|
|
399
|
+
return jsonResponse(config.customModels ?? []);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (url.pathname === "/api/custom-models" && req.method === "POST") {
|
|
403
|
+
let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
|
|
404
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
405
|
+
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
406
|
+
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
407
|
+
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
408
|
+
if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
409
|
+
if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
|
|
410
|
+
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
|
|
411
|
+
const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
|
|
412
|
+
if (displayName?.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
|
|
413
|
+
const contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
|
|
414
|
+
const modalities = readInputModalities(body.inputModalities);
|
|
415
|
+
if (modalities.error) return jsonResponse({ error: modalities.error }, 400);
|
|
416
|
+
const inputModalities = modalities.values;
|
|
417
|
+
const existing = config.customModels ?? [];
|
|
418
|
+
const newSlug = routedSlug(provider, modelId);
|
|
419
|
+
if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
|
|
420
|
+
return jsonResponse({ error: "duplicate model" }, 409);
|
|
421
|
+
}
|
|
422
|
+
const entry: OcxCustomModel = {
|
|
423
|
+
id: randomUUID(),
|
|
424
|
+
provider,
|
|
425
|
+
modelId,
|
|
426
|
+
...(displayName ? { displayName } : {}),
|
|
427
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
428
|
+
...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
|
|
429
|
+
addedAt: new Date().toISOString(),
|
|
430
|
+
};
|
|
431
|
+
config.customModels = [...existing, entry];
|
|
432
|
+
persistConfig(config);
|
|
433
|
+
await refreshCodexCatalogBestEffort();
|
|
434
|
+
return jsonResponse(entry, 201);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
438
|
+
if (customPutMatch && req.method === "PUT") {
|
|
439
|
+
let id: string;
|
|
440
|
+
try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
441
|
+
let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
|
|
442
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
443
|
+
const list = config.customModels ?? [];
|
|
444
|
+
const idx = list.findIndex(cm => cm.id === id);
|
|
445
|
+
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
446
|
+
const cm = { ...list[idx] };
|
|
447
|
+
if (typeof body.modelId === "string" && body.modelId.trim()) {
|
|
448
|
+
if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
449
|
+
cm.modelId = body.modelId.trim();
|
|
450
|
+
}
|
|
451
|
+
if (body.displayName !== undefined) {
|
|
452
|
+
const dn = typeof body.displayName === "string" ? body.displayName.trim() : "";
|
|
453
|
+
if (dn.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
|
|
454
|
+
cm.displayName = dn || undefined;
|
|
455
|
+
}
|
|
456
|
+
if (body.contextWindow !== undefined) {
|
|
457
|
+
cm.contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
|
|
458
|
+
}
|
|
459
|
+
if (body.inputModalities !== undefined) {
|
|
460
|
+
const edited = readInputModalities(body.inputModalities);
|
|
461
|
+
if (edited.error) return jsonResponse({ error: edited.error }, 400);
|
|
462
|
+
cm.inputModalities = edited.values && edited.values.length > 0 ? edited.values : undefined;
|
|
463
|
+
}
|
|
464
|
+
const updatedSlug = routedSlug(cm.provider, cm.modelId);
|
|
465
|
+
if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
|
|
466
|
+
return jsonResponse({ error: "duplicate model" }, 409);
|
|
467
|
+
}
|
|
468
|
+
list[idx] = cm;
|
|
469
|
+
config.customModels = list;
|
|
470
|
+
persistConfig(config);
|
|
471
|
+
await refreshCodexCatalogBestEffort();
|
|
472
|
+
return jsonResponse(cm);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
|
|
476
|
+
if (customDelMatch && req.method === "DELETE") {
|
|
477
|
+
let id: string;
|
|
478
|
+
try { id = decodeURIComponent(customDelMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
|
|
479
|
+
const list = config.customModels ?? [];
|
|
480
|
+
const idx = list.findIndex(cm => cm.id === id);
|
|
481
|
+
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
482
|
+
list.splice(idx, 1);
|
|
483
|
+
config.customModels = list.length > 0 ? list : undefined;
|
|
484
|
+
persistConfig(config);
|
|
485
|
+
await refreshCodexCatalogBestEffort();
|
|
486
|
+
return jsonResponse({ ok: true });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
|
|
490
|
+
// only those ids ship to Codex's catalog / /v1/models. GET returns the CURRENT selection plus the
|
|
491
|
+
// FULL available set per provider (unfiltered — the picker needs everything to choose from).
|
|
492
|
+
if (url.pathname === "/api/selected-models" && req.method === "GET") {
|
|
493
|
+
const models = await fetchAllModels(config);
|
|
494
|
+
const available: Record<string, string[]> = {};
|
|
495
|
+
for (const m of models) (available[m.provider] ??= []).push(m.id);
|
|
496
|
+
const selected: Record<string, string[]> = {};
|
|
497
|
+
// Live-catalog provenance. The GUI cannot infer this by subtracting known custom ids: an id
|
|
498
|
+
// that is both custom and discovered would make a real live catalog look custom-only.
|
|
499
|
+
const liveModelCounts: Record<string, number> = {};
|
|
500
|
+
for (const [name, prov] of Object.entries(config.providers)) {
|
|
501
|
+
if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
|
|
502
|
+
const liveCount = getProviderLiveModelCount(name);
|
|
503
|
+
if (liveCount !== undefined) liveModelCounts[name] = liveCount;
|
|
504
|
+
}
|
|
505
|
+
return jsonResponse({ selected, available, liveModelCounts });
|
|
506
|
+
}
|
|
507
|
+
if (url.pathname === "/api/selected-models" && req.method === "PUT") {
|
|
508
|
+
let body: { provider?: unknown; models?: unknown };
|
|
509
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
510
|
+
const provider = typeof body.provider === "string" ? body.provider : "";
|
|
511
|
+
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
512
|
+
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|
|
513
|
+
}
|
|
514
|
+
const models = Array.isArray(body.models)
|
|
515
|
+
? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
|
|
516
|
+
: [];
|
|
517
|
+
// Empty list clears the allowlist (provider reverts to exposing all models).
|
|
518
|
+
if (models.length > 0) config.providers[provider].selectedModels = models;
|
|
519
|
+
else delete config.providers[provider].selectedModels;
|
|
520
|
+
persistConfig(config);
|
|
521
|
+
await refreshCodexCatalogBestEffort();
|
|
522
|
+
return jsonResponse({ ok: true, provider, selected: models });
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
}
|