@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,1126 @@
|
|
|
1
|
+
import { markActivity } from "../lib/sidecar-tracker";
|
|
2
|
+
import {
|
|
3
|
+
buildWarmupCompletionFrames,
|
|
4
|
+
buildWsErrorFrame,
|
|
5
|
+
selectForwardHeaders,
|
|
6
|
+
sendJsonFrame,
|
|
7
|
+
buildResponsesWsData,
|
|
8
|
+
sendResponseToWebSocket,
|
|
9
|
+
sendTextFrame,
|
|
10
|
+
type WsData,
|
|
11
|
+
} from "./ws-bridge";
|
|
12
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_SUBAGENT_MODELS,
|
|
15
|
+
applyProxyEnv,
|
|
16
|
+
armClaudeCodeBaseline,
|
|
17
|
+
loadConfig,
|
|
18
|
+
saveConfig,
|
|
19
|
+
websocketsEnabled,
|
|
20
|
+
} from "../config";
|
|
21
|
+
import { reconcileOAuthProviders } from "../oauth";
|
|
22
|
+
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
23
|
+
import { startMemoryWatchdog } from "./memory-watchdog";
|
|
24
|
+
import {
|
|
25
|
+
reconcileLiveStateStores,
|
|
26
|
+
setLiveStateStoreConfig,
|
|
27
|
+
} from "../lib/state-store-registrations";
|
|
28
|
+
import { startStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
29
|
+
import {
|
|
30
|
+
configureAppOwnedMemoryBudget,
|
|
31
|
+
enforceAppOwnedMemoryBudget,
|
|
32
|
+
resolveAppOwnedMemoryBudgetBytes,
|
|
33
|
+
} from "../lib/app-owned-memory";
|
|
34
|
+
import {
|
|
35
|
+
registerAppOwnedMemorySweepFallback,
|
|
36
|
+
registerDefaultAppOwnedMemoryStores,
|
|
37
|
+
registerDefaultAppOwnedObservedBuffers,
|
|
38
|
+
} from "../lib/app-owned-memory-stores";
|
|
39
|
+
import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
|
|
40
|
+
import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
|
|
41
|
+
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
42
|
+
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
43
|
+
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
44
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
45
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
46
|
+
import type { StorageCleanupPolicy } from "../types";
|
|
47
|
+
import {
|
|
48
|
+
CodexAccountCooldownError,
|
|
49
|
+
cooldownErrorMessage,
|
|
50
|
+
} from "../codex/auth-context";
|
|
51
|
+
export {
|
|
52
|
+
clearThreadAccountMap,
|
|
53
|
+
formatCodexProviderForLog,
|
|
54
|
+
resolveCodexAccountForThread,
|
|
55
|
+
} from "../codex/routing";
|
|
56
|
+
import { formatCodexProviderForLog } from "../codex/routing";
|
|
57
|
+
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
58
|
+
import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
|
|
59
|
+
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
|
|
60
|
+
export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
|
|
61
|
+
export { resolveAdapter } from "./adapter-resolve";
|
|
62
|
+
import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
|
|
63
|
+
import {
|
|
64
|
+
drainAndShutdown,
|
|
65
|
+
getActiveTurnCount,
|
|
66
|
+
isDraining,
|
|
67
|
+
registerTurn,
|
|
68
|
+
setServerRef,
|
|
69
|
+
trackStreamLifetime,
|
|
70
|
+
tryAdmitTurn,
|
|
71
|
+
unregisterTurn,
|
|
72
|
+
type ActiveTurnLease,
|
|
73
|
+
} from "./lifecycle";
|
|
74
|
+
export {
|
|
75
|
+
drainAndShutdown,
|
|
76
|
+
getActiveTurnCount,
|
|
77
|
+
isDraining,
|
|
78
|
+
isRecyclingForExit,
|
|
79
|
+
markRecyclingForExit,
|
|
80
|
+
registerTurn,
|
|
81
|
+
trackStreamLifetime,
|
|
82
|
+
unregisterTurn,
|
|
83
|
+
} from "./lifecycle";
|
|
84
|
+
import {
|
|
85
|
+
addFinalRequestLog,
|
|
86
|
+
hydrateRequestLogsFromDisk,
|
|
87
|
+
httpStatusForRequestLogTerminal,
|
|
88
|
+
httpStatusForTerminalStatus,
|
|
89
|
+
inspectResponseLogSsePayload,
|
|
90
|
+
nextRequestLogId,
|
|
91
|
+
recordFirstOutput,
|
|
92
|
+
type RequestLogContext,
|
|
93
|
+
type RequestLogEntry,
|
|
94
|
+
} from "./request-log";
|
|
95
|
+
export {
|
|
96
|
+
addFinalRequestLog,
|
|
97
|
+
filterRequestLogs,
|
|
98
|
+
hydrateRequestLogsFromDisk,
|
|
99
|
+
httpStatusForTerminalStatus,
|
|
100
|
+
httpStatusFromTerminalError,
|
|
101
|
+
nextRequestLogId,
|
|
102
|
+
requestLogErrorCode,
|
|
103
|
+
requestLogSpeedLabel,
|
|
104
|
+
usageFromResponsesPayload,
|
|
105
|
+
type RequestLogContext,
|
|
106
|
+
type RequestLogEntry,
|
|
107
|
+
} from "./request-log";
|
|
108
|
+
import {
|
|
109
|
+
consumeForInspection,
|
|
110
|
+
relaySseWithHeartbeat,
|
|
111
|
+
relayWithAbort,
|
|
112
|
+
responseWithDeferredRequestLog,
|
|
113
|
+
sanitizePassthroughHeaders,
|
|
114
|
+
} from "./relay";
|
|
115
|
+
export {
|
|
116
|
+
consumeForInspection,
|
|
117
|
+
relaySseWithFailedTail,
|
|
118
|
+
relaySseWithHeartbeat,
|
|
119
|
+
relayWithAbort,
|
|
120
|
+
responseWithDeferredRequestLog,
|
|
121
|
+
sanitizePassthroughHeaders,
|
|
122
|
+
} from "./relay";
|
|
123
|
+
import {
|
|
124
|
+
assertServerAuthConfig,
|
|
125
|
+
corsHeaders,
|
|
126
|
+
managementCorsHeaders,
|
|
127
|
+
isAllowedRequestOrigin,
|
|
128
|
+
isAllowedManagementOrigin,
|
|
129
|
+
isApiAuthRequired,
|
|
130
|
+
isLoopbackHostname,
|
|
131
|
+
jsonResponse,
|
|
132
|
+
admissionFields,
|
|
133
|
+
resolveApiAuth,
|
|
134
|
+
resolveResponsesApiAuth,
|
|
135
|
+
safeConfigDTO,
|
|
136
|
+
setCorsOrigin,
|
|
137
|
+
withCors,
|
|
138
|
+
withManagementCors,
|
|
139
|
+
} from "./auth-cors";
|
|
140
|
+
export {
|
|
141
|
+
assertServerAuthConfig,
|
|
142
|
+
corsHeaders,
|
|
143
|
+
hasValidApiAuth,
|
|
144
|
+
isApiAuthRequired,
|
|
145
|
+
isLoopbackHostname,
|
|
146
|
+
jsonResponse,
|
|
147
|
+
safeConfigDTO,
|
|
148
|
+
} from "./auth-cors";
|
|
149
|
+
import { disableResponsesRequestTimeout, handleResponses, handleResponsesCompact } from "./responses";
|
|
150
|
+
export { disableResponsesRequestTimeout, linkAbortSignal } from "./responses";
|
|
151
|
+
import { handleClaudeCountTokens, handleClaudeMessages } from "./claude-messages";
|
|
152
|
+
import { handleChatCompletions } from "./chat-completions";
|
|
153
|
+
import { anthropicErrorResponse } from "../claude/outbound";
|
|
154
|
+
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
155
|
+
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
156
|
+
import { handleImages } from "./images";
|
|
157
|
+
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
158
|
+
import { handleSearch } from "./search";
|
|
159
|
+
import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
|
|
160
|
+
import { initializeManagementAuthState, issueGuiSession, requireManagementAuth } from "./management-auth";
|
|
161
|
+
|
|
162
|
+
const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
163
|
+
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
164
|
+
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
165
|
+
|
|
166
|
+
function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
|
|
167
|
+
try {
|
|
168
|
+
ws.data.liveUpstream?.close(code, reason);
|
|
169
|
+
} catch {
|
|
170
|
+
/* upstream already gone */
|
|
171
|
+
}
|
|
172
|
+
ws.data.liveUpstream = undefined;
|
|
173
|
+
ws.data.livePending = undefined;
|
|
174
|
+
try {
|
|
175
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
176
|
+
ws.close(code, reason);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
/* client already gone */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
184
|
+
const url = ws.data.liveUpstreamUrl;
|
|
185
|
+
if (!url) {
|
|
186
|
+
closeLiveSideband(ws, 1011, "missing upstream");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
let upstream: WebSocket;
|
|
190
|
+
try {
|
|
191
|
+
// Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
|
|
192
|
+
upstream = new WebSocket(url, { headers: ws.data.liveUpstreamHeaders ?? {} } as unknown as string[]);
|
|
193
|
+
} catch {
|
|
194
|
+
closeLiveSideband(ws, 1011, "upstream connect failed");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
ws.data.liveUpstream = upstream;
|
|
198
|
+
ws.data.cancel = () => {
|
|
199
|
+
try {
|
|
200
|
+
upstream.close(1000, "client closed");
|
|
201
|
+
} catch {
|
|
202
|
+
/* ignore */
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
upstream.addEventListener("open", () => {
|
|
207
|
+
ws.data.liveOpened = true;
|
|
208
|
+
const pending = ws.data.livePending ?? [];
|
|
209
|
+
ws.data.livePending = undefined;
|
|
210
|
+
for (const frame of pending) {
|
|
211
|
+
try {
|
|
212
|
+
upstream.send(frame);
|
|
213
|
+
} catch {
|
|
214
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
upstream.addEventListener("message", (event) => {
|
|
220
|
+
try {
|
|
221
|
+
logLiveSidebandFrame("u2c", event.data);
|
|
222
|
+
if (typeof event.data === "string") ws.send(event.data);
|
|
223
|
+
else if (event.data instanceof ArrayBuffer) ws.send(event.data);
|
|
224
|
+
else if (ArrayBuffer.isView(event.data)) {
|
|
225
|
+
ws.send(event.data.buffer.slice(event.data.byteOffset, event.data.byteOffset + event.data.byteLength));
|
|
226
|
+
} else ws.send(event.data as Buffer);
|
|
227
|
+
} catch {
|
|
228
|
+
closeLiveSideband(ws, 1011, "client send failed");
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
upstream.addEventListener("close", (event) => {
|
|
232
|
+
try {
|
|
233
|
+
ws.close(event.code || 1000, event.reason || "");
|
|
234
|
+
} catch {
|
|
235
|
+
/* ignore */
|
|
236
|
+
}
|
|
237
|
+
ws.data.liveUpstream = undefined;
|
|
238
|
+
});
|
|
239
|
+
upstream.addEventListener("error", () => {
|
|
240
|
+
closeLiveSideband(ws, 1011, "upstream error");
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
|
|
245
|
+
// "../src/server" import surface stable for tests/callers.
|
|
246
|
+
|
|
247
|
+
// Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
|
|
248
|
+
|
|
249
|
+
// Source invariant for tests/passthrough-abort.test.ts after the pure module split:
|
|
250
|
+
// if (isEventStream && upstreamResponse.body) {
|
|
251
|
+
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
252
|
+
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
253
|
+
// #314 gated shape: win32 no-rewrite traffic follows runtime/config policy; darwin no-rewrite
|
|
254
|
+
// traffic requires explicit config-eager opt-in (`auto` always stays tee on darwin). Default OFF
|
|
255
|
+
// on the bundled known-bad runtime; policy lives in 260731_macos_rss_retention phase 100.
|
|
256
|
+
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
257
|
+
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
258
|
+
// new Response(eagerBody,
|
|
259
|
+
// Default shape (tee + background inspection):
|
|
260
|
+
// upstreamResponse.body.tee()
|
|
261
|
+
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
262
|
+
// process.platform === "win32"
|
|
263
|
+
// && !needsClientRewrite
|
|
264
|
+
// ? nativeBody
|
|
265
|
+
// relaySseWithFailedTail(repairedBody, upstream)
|
|
266
|
+
// new Response(clientBody
|
|
267
|
+
// markNativePassthroughSseResponse
|
|
268
|
+
// const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
269
|
+
// function responseWithDeferredRequestLog
|
|
270
|
+
// isNativePassthroughSseResponse(response)
|
|
271
|
+
// trackSseForRequestLog(
|
|
272
|
+
// export function relaySseWithHeartbeat
|
|
273
|
+
|
|
274
|
+
export function startServer(port?: number) {
|
|
275
|
+
const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
|
|
276
|
+
setLiveStateStoreConfig(config);
|
|
277
|
+
applyProxyEnv(config);
|
|
278
|
+
assertServerAuthConfig(config);
|
|
279
|
+
const managementAuth = initializeManagementAuthState(config);
|
|
280
|
+
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
281
|
+
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
282
|
+
reconcileOAuthProviders(config);
|
|
283
|
+
reconcileLiveStateStores();
|
|
284
|
+
// Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
|
|
285
|
+
// even [], is left alone so GUI removals persist.
|
|
286
|
+
if (config.subagentModels === undefined) {
|
|
287
|
+
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
288
|
+
saveConfig(config);
|
|
289
|
+
}
|
|
290
|
+
// authMode migration (devlog 260726_claude_auth_auto/015): before "auto" existed,
|
|
291
|
+
// choosing Subscription DELETED the key, so a pre-upgrade block with no authMode is
|
|
292
|
+
// indistinguishable from "never chose". Pin those to subscription once so an upgrade
|
|
293
|
+
// never silently moves a deliberate subscriber onto proxy.
|
|
294
|
+
if (runClaudeAuthModeMigration(config)) saveConfig(config);
|
|
295
|
+
// Sidecar model migration (KST 2026-07-10 06:00 = UTC 2026-07-09 21:00): auto-migrate the old
|
|
296
|
+
// gpt-5.4-mini default to gpt-5.6-luna for both search and vision sidecars. Only touches configs
|
|
297
|
+
// still on the old default — explicit user choices are preserved.
|
|
298
|
+
{
|
|
299
|
+
const SIDECAR_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0); // July 9 21:00 UTC = KST July 10 06:00
|
|
300
|
+
if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
|
|
301
|
+
let migrated = false;
|
|
302
|
+
if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
|
|
303
|
+
config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
|
|
304
|
+
migrated = true;
|
|
305
|
+
}
|
|
306
|
+
if (config.visionSidecar?.model === "gpt-5.4-mini") {
|
|
307
|
+
config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
|
|
308
|
+
migrated = true;
|
|
309
|
+
}
|
|
310
|
+
if (migrated) saveConfig(config);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
invalidateCodexModelsCache();
|
|
314
|
+
// Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
|
|
315
|
+
// the server can serve a request, and AFTER the startup migrations above — those run
|
|
316
|
+
// against a config nobody else holds and are the documented exception to the save
|
|
317
|
+
// boundary, so the baseline should reflect what they wrote. Arming is eager on
|
|
318
|
+
// purpose: a lazy "arm on first save" loses exactly the hand edit made before that
|
|
319
|
+
// first save, which is the case the guard exists for.
|
|
320
|
+
armClaudeCodeBaseline(config);
|
|
321
|
+
// usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
|
|
322
|
+
// /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
|
|
323
|
+
hydrateRequestLogsFromDisk();
|
|
324
|
+
// #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
|
|
325
|
+
// startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
|
|
326
|
+
startMemoryWatchdog();
|
|
327
|
+
registerDefaultAppOwnedMemoryStores();
|
|
328
|
+
registerDefaultAppOwnedObservedBuffers();
|
|
329
|
+
registerAppOwnedMemorySweepFallback();
|
|
330
|
+
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
331
|
+
enforceAppOwnedMemoryBudget();
|
|
332
|
+
startStateStoreSweeper();
|
|
333
|
+
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
334
|
+
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
335
|
+
// Heavy work runs in a Worker via the single-flight job controller.
|
|
336
|
+
// Keep live config.policy in sync when background runs advance nextRun/lastRun.
|
|
337
|
+
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
338
|
+
config.storageCleanupPolicy = policy;
|
|
339
|
+
};
|
|
340
|
+
setStorageCleanupPolicyLiveSink(applyPolicy);
|
|
341
|
+
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
342
|
+
startStorageCleanupScheduler();
|
|
343
|
+
|
|
344
|
+
const listenPort = port ?? config.port ?? 10100;
|
|
345
|
+
setCorsOrigin(listenPort);
|
|
346
|
+
|
|
347
|
+
// Canonicalize an explicit "localhost" bind to IPv4 so it matches the injected base_url (which
|
|
348
|
+
// resolves localhost→127.0.0.1): on Windows `localhost` resolves ::1-first, but the injected URL
|
|
349
|
+
// is 127.0.0.1, so binding literal "localhost" would reintroduce the F4 refusal. Wildcards
|
|
350
|
+
// (0.0.0.0/::) and specific hosts are left untouched so intentional exposure is preserved.
|
|
351
|
+
const configuredHost = config.hostname?.trim();
|
|
352
|
+
const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
|
|
353
|
+
|
|
354
|
+
// Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
|
|
355
|
+
// the server_is_overloaded code so clients can back off, but always return a JSON envelope.
|
|
356
|
+
function drainingResponse(req: Request): Response {
|
|
357
|
+
const response = formatErrorResponse(503, "server_error", "Service shutting down");
|
|
358
|
+
const headers = new Headers(response.headers);
|
|
359
|
+
for (const [name, value] of Object.entries(corsHeaders(req, config))) {
|
|
360
|
+
headers.set(name, value);
|
|
361
|
+
}
|
|
362
|
+
headers.set("Retry-After", "5");
|
|
363
|
+
return new Response(response.body, { status: 503, headers });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function serverBusyResponse(req: Request, resource: string): Response {
|
|
367
|
+
return withCors(new Response(JSON.stringify({
|
|
368
|
+
error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
|
|
369
|
+
}), {
|
|
370
|
+
status: 503,
|
|
371
|
+
headers: { "Content-Type": "application/json", "Retry-After": "1" },
|
|
372
|
+
}), req, config);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runAdmittedHttpTurn(req: Request, work: (lease: ActiveTurnLease) => Promise<Response>): Promise<Response> {
|
|
376
|
+
const lease = tryAdmitTurn();
|
|
377
|
+
if (!lease) return serverBusyResponse(req, "active turns");
|
|
378
|
+
let response: Response;
|
|
379
|
+
try {
|
|
380
|
+
response = await work(lease);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
lease.release();
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
if (!lease.isTransferred()) {
|
|
386
|
+
lease.release();
|
|
387
|
+
}
|
|
388
|
+
return response;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const server: Server<WsData> = Bun.serve<WsData>({
|
|
392
|
+
port: listenPort,
|
|
393
|
+
hostname: bindHost,
|
|
394
|
+
idleTimeout: 255,
|
|
395
|
+
async fetch(req, requestServer): Promise<Response> {
|
|
396
|
+
const url = new URL(req.url);
|
|
397
|
+
markActivity(`${req.method} ${url.pathname}`);
|
|
398
|
+
|
|
399
|
+
if (req.method === "OPTIONS") {
|
|
400
|
+
const managementPreflight = url.pathname.startsWith("/api/");
|
|
401
|
+
const allowed = managementPreflight
|
|
402
|
+
? isAllowedManagementOrigin(req, config)
|
|
403
|
+
: isAllowedRequestOrigin(req, config);
|
|
404
|
+
if (!allowed) {
|
|
405
|
+
return new Response(null, { status: 403, headers: corsHeaders() });
|
|
406
|
+
}
|
|
407
|
+
return new Response(null, {
|
|
408
|
+
status: 204,
|
|
409
|
+
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, config),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
|
|
414
|
+
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
415
|
+
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
416
|
+
if (isDraining()) {
|
|
417
|
+
return drainingResponse(req);
|
|
418
|
+
}
|
|
419
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
420
|
+
if (!admission) {
|
|
421
|
+
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
422
|
+
}
|
|
423
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
424
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
|
|
425
|
+
}
|
|
426
|
+
// WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
|
|
427
|
+
// so under Design B it always tries the WS transport first. When the feature is off, reject
|
|
428
|
+
// the upgrade with 426 — codex-rs maps a connect-time UPGRADE_REQUIRED to a clean
|
|
429
|
+
// session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
|
|
430
|
+
// surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
|
|
431
|
+
if (!websocketsEnabled(config)) {
|
|
432
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, config);
|
|
433
|
+
}
|
|
434
|
+
const websocketLease = tryReserveCodexWebSocket();
|
|
435
|
+
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets");
|
|
436
|
+
if (server.upgrade(req, {
|
|
437
|
+
data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
|
|
438
|
+
})) return undefined as unknown as Response;
|
|
439
|
+
websocketLease.release();
|
|
440
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
444
|
+
// service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
|
|
445
|
+
return jsonResponse({ status: "ok", service: "opencodex", version: VERSION, uptime: process.uptime(), pid: process.pid, port: listenPort }, 200, req, config);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (url.pathname.startsWith("/api/")) {
|
|
449
|
+
const apiAuthError = requireManagementAuth(req, managementAuth, config);
|
|
450
|
+
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
|
|
451
|
+
const mgmtResponse = await handleManagementAPI(req, url, config);
|
|
452
|
+
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
453
|
+
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (url.pathname === "/v1/models" && req.method === "GET") {
|
|
457
|
+
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
458
|
+
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
459
|
+
// remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
|
|
460
|
+
const admission = resolveApiAuth(req, config);
|
|
461
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
462
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
463
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
464
|
+
}
|
|
465
|
+
let goModels;
|
|
466
|
+
try {
|
|
467
|
+
goModels = await fetchAllModels(config);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error instanceof CatalogGatherBusyError) {
|
|
470
|
+
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
471
|
+
status: 503,
|
|
472
|
+
headers: { "content-type": "application/json", "Retry-After": "1" },
|
|
473
|
+
}), req, config);
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
478
|
+
const nativeSlugs = nativeOpenAiSlugs();
|
|
479
|
+
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
480
|
+
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
481
|
+
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
482
|
+
// Anthropic-style headers; 003 G1-G8 + devlog 131). Entries use the official
|
|
483
|
+
// ModelInfo shape incl. capabilities (effort ladder / thinking) — Desktop 3P can
|
|
484
|
+
// only learn capabilities through discovery, and Claude Code 2.1.207 strips the
|
|
485
|
+
// extra fields (backward-safe). Ids are the claude-opus-4-8-{code} Desktop
|
|
486
|
+
// aliases; legacy claude-ocx-* ids keep decoding via resolveAlias. Detection:
|
|
487
|
+
// anthropic-version header (Claude Code sends it) or explicit ?flavor=anthropic.
|
|
488
|
+
// Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
|
|
489
|
+
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
490
|
+
|| url.searchParams.get("flavor") === "anthropic";
|
|
491
|
+
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
492
|
+
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, config);
|
|
493
|
+
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
494
|
+
buildDesktop3pRegistry(
|
|
495
|
+
[...desktopVisibleNativeSlugs(config)],
|
|
496
|
+
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
497
|
+
config.claudeCode?.desktopProfile,
|
|
498
|
+
);
|
|
499
|
+
const { buildAnthropicModelInfos } = await import("../claude/model-info");
|
|
500
|
+
const { resolveAutoContext } = await import("../claude/context-windows");
|
|
501
|
+
const { activeDesktop3pAlias } = await import("../claude/desktop-3p");
|
|
502
|
+
// Per-surface id family (devlog 050): explicit ?ids= wins; otherwise the
|
|
503
|
+
// Claude Code CLI discovery UA (`claude-code/<version>`, binary n_()) gets
|
|
504
|
+
// readable claude-ocx ids and every other client (Desktop 3P) keeps the
|
|
505
|
+
// hashed family its config was written with. Unknown UA -> hashed (safe).
|
|
506
|
+
const idsParam = url.searchParams.get("ids");
|
|
507
|
+
const idStyle = idsParam === "cli"
|
|
508
|
+
? "readable" as const
|
|
509
|
+
: idsParam === "desktop"
|
|
510
|
+
? "desktop3p" as const
|
|
511
|
+
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
512
|
+
const data = buildAnthropicModelInfos([...desktopVisibleNativeSlugs(config)], goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias);
|
|
513
|
+
return jsonResponse({ data }, 200, req, config);
|
|
514
|
+
}
|
|
515
|
+
if (url.searchParams.has("client_version")) {
|
|
516
|
+
// Codex client → Codex catalog shape: native gpt + namespaced routed models,
|
|
517
|
+
// cloned from a native template so required fields (base_instructions, etc.) are present.
|
|
518
|
+
// Pass the subagent picks so featured models lead by priority (matches the on-disk file).
|
|
519
|
+
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
520
|
+
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
521
|
+
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
522
|
+
const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config));
|
|
523
|
+
return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
|
|
524
|
+
}
|
|
525
|
+
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
526
|
+
// (pure availability list — disabled natives are omitted entirely).
|
|
527
|
+
// Grok Build discovers models through this endpoint too, and its model picker only
|
|
528
|
+
// enables /effort for entries that advertise the reasoning ladder in the Grok model
|
|
529
|
+
// catalog shape (supports_reasoning_effort + reasoning_efforts[]). The Codex catalog
|
|
530
|
+
// branch above already carries the same ladders, so mirror them here — native rows
|
|
531
|
+
// from the upstream snapshot, routed rows from the configured provider tiers. The
|
|
532
|
+
// default uses the same canonical fallback as the Codex catalog resolver
|
|
533
|
+
// (configured default, then medium, then high, then the first tier). Extra fields
|
|
534
|
+
// are ignored by plain OpenAI clients.
|
|
535
|
+
const grokEffortOption = (value: string, isDefault: boolean) => ({
|
|
536
|
+
value,
|
|
537
|
+
label: `${value[0].toUpperCase()}${value.slice(1)} Effort`,
|
|
538
|
+
...(isDefault ? { default: true } : {}),
|
|
539
|
+
});
|
|
540
|
+
const grokEffortFields = (efforts: string[], configuredDefault?: string) => {
|
|
541
|
+
if (efforts.length === 0) return {};
|
|
542
|
+
const defaultEffort = configuredDefault && efforts.includes(configuredDefault)
|
|
543
|
+
? configuredDefault
|
|
544
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
|
|
545
|
+
return {
|
|
546
|
+
supports_reasoning_effort: true,
|
|
547
|
+
reasoning_effort: defaultEffort,
|
|
548
|
+
reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
const data = [
|
|
552
|
+
...visibleNativeSlugs(config).map(id => ({
|
|
553
|
+
id,
|
|
554
|
+
object: "model",
|
|
555
|
+
created: 0,
|
|
556
|
+
owned_by: "openai",
|
|
557
|
+
...grokEffortFields(nativeReasoningEfforts(id), nativeDefaultReasoningEffort(id)),
|
|
558
|
+
})),
|
|
559
|
+
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
|
|
560
|
+
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
561
|
+
object: "model",
|
|
562
|
+
created: 0,
|
|
563
|
+
owned_by: m.owned_by ?? m.provider,
|
|
564
|
+
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
565
|
+
})),
|
|
566
|
+
];
|
|
567
|
+
return jsonResponse({ object: "list", data }, 200, req, config);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
|
|
571
|
+
// Must be matched BEFORE the /v1/responses POST branch never sees it (distinct path) and
|
|
572
|
+
// before the /v1/* 404 guard below.
|
|
573
|
+
if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
|
|
574
|
+
if (isDraining()) {
|
|
575
|
+
return drainingResponse(req);
|
|
576
|
+
}
|
|
577
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
578
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
579
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
580
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
581
|
+
}
|
|
582
|
+
const start = Date.now();
|
|
583
|
+
const requestId = nextRequestLogId(start);
|
|
584
|
+
const logCtx: RequestLogContext = {
|
|
585
|
+
model: "unknown",
|
|
586
|
+
provider: "unknown",
|
|
587
|
+
...admissionFields(admission),
|
|
588
|
+
inboundProtocol: "responses",
|
|
589
|
+
};
|
|
590
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
591
|
+
let response: Response;
|
|
592
|
+
try {
|
|
593
|
+
response = await handleResponsesCompact(req, config, logCtx);
|
|
594
|
+
} catch {
|
|
595
|
+
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
596
|
+
}
|
|
597
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
598
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
599
|
+
return withCors(response, req, config);
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (
|
|
604
|
+
req.method === "POST"
|
|
605
|
+
&& (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits")
|
|
606
|
+
) {
|
|
607
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
608
|
+
if (isDraining()) {
|
|
609
|
+
return drainingResponse(req);
|
|
610
|
+
}
|
|
611
|
+
const admission = resolveApiAuth(req, config);
|
|
612
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
613
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
614
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
615
|
+
}
|
|
616
|
+
const start = Date.now();
|
|
617
|
+
const requestId = nextRequestLogId(start);
|
|
618
|
+
const logCtx: RequestLogContext = {
|
|
619
|
+
model: "image_gen",
|
|
620
|
+
provider: "unknown",
|
|
621
|
+
...admissionFields(admission),
|
|
622
|
+
};
|
|
623
|
+
const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
|
|
624
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
625
|
+
const response = await handleImages(req, config, endpoint, logCtx);
|
|
626
|
+
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
627
|
+
return withCors(response, req, config);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
|
|
632
|
+
const admission = resolveApiAuth(req, config);
|
|
633
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
634
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
635
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
636
|
+
}
|
|
637
|
+
const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
|
|
638
|
+
const { resolveArtifactPath } = await import("../images/artifacts");
|
|
639
|
+
const artifactPath = resolveArtifactPath(id);
|
|
640
|
+
if (!artifactPath) {
|
|
641
|
+
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, config);
|
|
642
|
+
}
|
|
643
|
+
const file = Bun.file(artifactPath);
|
|
644
|
+
const ext = artifactPath.split(".").pop()?.toLowerCase();
|
|
645
|
+
const contentType =
|
|
646
|
+
ext === "png" ? "image/png"
|
|
647
|
+
: ext === "jpg" || ext === "jpeg" ? "image/jpeg"
|
|
648
|
+
: ext === "webp" ? "image/webp"
|
|
649
|
+
: ext === "gif" ? "image/gif"
|
|
650
|
+
: "application/octet-stream";
|
|
651
|
+
return withCors(new Response(file, {
|
|
652
|
+
status: 200,
|
|
653
|
+
headers: {
|
|
654
|
+
"content-type": contentType,
|
|
655
|
+
"cache-control": "private, max-age=3600",
|
|
656
|
+
"x-content-type-options": "nosniff",
|
|
657
|
+
},
|
|
658
|
+
}), req, config);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
662
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
663
|
+
if (isDraining()) {
|
|
664
|
+
return drainingResponse(req);
|
|
665
|
+
}
|
|
666
|
+
const admission = resolveApiAuth(req, config);
|
|
667
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
668
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
669
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
670
|
+
}
|
|
671
|
+
const start = Date.now();
|
|
672
|
+
const requestId = nextRequestLogId(start);
|
|
673
|
+
const logCtx: RequestLogContext = {
|
|
674
|
+
model: "web_search",
|
|
675
|
+
provider: "unknown",
|
|
676
|
+
...admissionFields(admission),
|
|
677
|
+
};
|
|
678
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
679
|
+
const response = await handleSearch(req, config, logCtx);
|
|
680
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
681
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
682
|
+
return withCors(response, req, config);
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
687
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
688
|
+
if (isDraining()) {
|
|
689
|
+
return drainingResponse(req);
|
|
690
|
+
}
|
|
691
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
692
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
693
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
694
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
695
|
+
}
|
|
696
|
+
const start = Date.now();
|
|
697
|
+
const requestId = nextRequestLogId(start);
|
|
698
|
+
const logCtx: RequestLogContext = {
|
|
699
|
+
model: "unknown",
|
|
700
|
+
provider: "unknown",
|
|
701
|
+
...admissionFields(admission),
|
|
702
|
+
inboundProtocol: "responses",
|
|
703
|
+
};
|
|
704
|
+
let logged = false;
|
|
705
|
+
const finalizeNativePassthroughLog = (
|
|
706
|
+
status: number,
|
|
707
|
+
meta: { terminalStatus?: ResponsesTerminalStatus; closeReason: "terminal" | "client_cancel" },
|
|
708
|
+
) => {
|
|
709
|
+
if (logged) return;
|
|
710
|
+
logged = true;
|
|
711
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
712
|
+
};
|
|
713
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => {
|
|
714
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
715
|
+
turnAdmissionLease,
|
|
716
|
+
abortSignal: req.signal,
|
|
717
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
718
|
+
onNativePassthroughTerminal: status => {
|
|
719
|
+
finalizeNativePassthroughLog(httpStatusForTerminalStatus(status), {
|
|
720
|
+
terminalStatus: status,
|
|
721
|
+
closeReason: "terminal",
|
|
722
|
+
});
|
|
723
|
+
},
|
|
724
|
+
onNativePassthroughCancel: () => {
|
|
725
|
+
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, config);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Anthropic Messages inbound (Claude Code). count_tokens FIRST (longer path).
|
|
733
|
+
// Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
|
|
734
|
+
if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
|
|
735
|
+
if (isDraining()) {
|
|
736
|
+
return drainingResponse(req);
|
|
737
|
+
}
|
|
738
|
+
const admission = resolveApiAuth(req, config);
|
|
739
|
+
if (!admission) {
|
|
740
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
|
|
741
|
+
}
|
|
742
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
743
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, config);
|
|
744
|
+
}
|
|
745
|
+
return runAdmittedHttpTurn(req, async () => withCors(await handleClaudeCountTokens(req, config), req, config));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
749
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
750
|
+
if (isDraining()) {
|
|
751
|
+
return drainingResponse(req);
|
|
752
|
+
}
|
|
753
|
+
const admission = resolveApiAuth(req, config);
|
|
754
|
+
if (!admission) {
|
|
755
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, config);
|
|
756
|
+
}
|
|
757
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
758
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, config);
|
|
759
|
+
}
|
|
760
|
+
const start = Date.now();
|
|
761
|
+
const requestId = nextRequestLogId(start);
|
|
762
|
+
const logCtx: RequestLogContext = {
|
|
763
|
+
model: "unknown",
|
|
764
|
+
provider: "unknown",
|
|
765
|
+
...admissionFields(admission),
|
|
766
|
+
inboundProtocol: "messages",
|
|
767
|
+
};
|
|
768
|
+
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
769
|
+
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
770
|
+
// translated Anthropic stream here.
|
|
771
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
772
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
773
|
+
req,
|
|
774
|
+
config,
|
|
775
|
+
));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
// OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
|
|
780
|
+
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
781
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
782
|
+
if (isDraining()) {
|
|
783
|
+
return drainingResponse(req);
|
|
784
|
+
}
|
|
785
|
+
const admission = resolveResponsesApiAuth(req, config);
|
|
786
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
787
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
788
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
789
|
+
}
|
|
790
|
+
const start = Date.now();
|
|
791
|
+
const requestId = nextRequestLogId(start);
|
|
792
|
+
const logCtx: RequestLogContext = {
|
|
793
|
+
model: "unknown",
|
|
794
|
+
provider: "unknown",
|
|
795
|
+
...admissionFields(admission),
|
|
796
|
+
inboundProtocol: "chat",
|
|
797
|
+
};
|
|
798
|
+
return runAdmittedHttpTurn(req, async turnAdmissionLease => withCors(
|
|
799
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease }),
|
|
800
|
+
req,
|
|
801
|
+
config,
|
|
802
|
+
));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// ChatGPT / Codex App voice (GPT‑Live / Frameless Bidi) + OpenAI Realtime call-create.
|
|
806
|
+
// Clients hit either /v1/live (Frameless App) or /v1/realtime/calls (codex RealtimeCallClient /
|
|
807
|
+
// public Realtime API). Sideband WS joins are handled just below.
|
|
808
|
+
if (
|
|
809
|
+
req.method === "POST"
|
|
810
|
+
&& (url.pathname === "/v1/live" || url.pathname === "/v1/realtime/calls")
|
|
811
|
+
) {
|
|
812
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
813
|
+
if (isDraining()) {
|
|
814
|
+
return drainingResponse(req);
|
|
815
|
+
}
|
|
816
|
+
const admission = resolveApiAuth(req, config);
|
|
817
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
818
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
819
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
820
|
+
}
|
|
821
|
+
const start = Date.now();
|
|
822
|
+
const requestId = nextRequestLogId(start);
|
|
823
|
+
const logCtx: RequestLogContext = {
|
|
824
|
+
model: "gpt-live",
|
|
825
|
+
provider: "unknown",
|
|
826
|
+
...admissionFields(admission),
|
|
827
|
+
};
|
|
828
|
+
return runAdmittedHttpTurn(req, async () => {
|
|
829
|
+
const response = await handleLive(req, config, logCtx);
|
|
830
|
+
addFinalRequestLog(
|
|
831
|
+
requestId,
|
|
832
|
+
start,
|
|
833
|
+
logCtx,
|
|
834
|
+
response.status,
|
|
835
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
836
|
+
);
|
|
837
|
+
return withCors(response, req, config);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Voice / Realtime sideband WebSocket: Frameless joins /v1/live/{callId}; Realtime v1 joins
|
|
842
|
+
// /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Transparent bidirectional relay.
|
|
843
|
+
const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
844
|
+
? parseLiveSidebandTarget(url.pathname, url.searchParams)
|
|
845
|
+
: null;
|
|
846
|
+
if (liveSidebandTarget) {
|
|
847
|
+
if (isDraining()) {
|
|
848
|
+
return drainingResponse(req);
|
|
849
|
+
}
|
|
850
|
+
const admission = resolveApiAuth(req, config);
|
|
851
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, config);
|
|
852
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
853
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, config);
|
|
854
|
+
}
|
|
855
|
+
const start = Date.now();
|
|
856
|
+
const requestId = nextRequestLogId(start);
|
|
857
|
+
const logCtx: RequestLogContext = {
|
|
858
|
+
model: "gpt-live",
|
|
859
|
+
provider: "unknown",
|
|
860
|
+
...admissionFields(admission),
|
|
861
|
+
};
|
|
862
|
+
const resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget);
|
|
863
|
+
if (resolved instanceof Response) {
|
|
864
|
+
addFinalRequestLog(requestId, start, logCtx, resolved.status);
|
|
865
|
+
return withCors(resolved, req, config);
|
|
866
|
+
}
|
|
867
|
+
addFinalRequestLog(requestId, start, logCtx, 101);
|
|
868
|
+
if (server.upgrade(req, {
|
|
869
|
+
data: {
|
|
870
|
+
kind: "live-sideband",
|
|
871
|
+
liveUpstreamUrl: resolved.upstreamWsUrl,
|
|
872
|
+
liveUpstreamHeaders: resolved.headers,
|
|
873
|
+
livePending: [],
|
|
874
|
+
liveOpened: false,
|
|
875
|
+
} satisfies WsData,
|
|
876
|
+
})) return undefined as unknown as Response;
|
|
877
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
|
|
881
|
+
// GUI static handler (extensionless paths would get index.html with HTTP 200 and codex-rs
|
|
882
|
+
// endpoint clients — memories/*, realtime/* — would surface confusing
|
|
883
|
+
// serde decode errors instead of a clean not-found).
|
|
884
|
+
if (url.pathname.startsWith("/v1/")) {
|
|
885
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
|
|
889
|
+
? issueGuiSession(req, config, managementAuth)
|
|
890
|
+
: null;
|
|
891
|
+
const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
|
|
892
|
+
if (guiFile) return guiFile;
|
|
893
|
+
if (url.pathname === "/" && req.method === "GET") {
|
|
894
|
+
return jsonResponse(rootFallbackPayload());
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
898
|
+
},
|
|
899
|
+
websocket: {
|
|
900
|
+
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
901
|
+
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
902
|
+
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
903
|
+
// Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
|
|
904
|
+
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
905
|
+
open(ws: ServerWebSocket<WsData>) {
|
|
906
|
+
if (ws.data.kind === "live-sideband") {
|
|
907
|
+
attachLiveSidebandUpstream(ws);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!ws.data.admissionLease) {
|
|
911
|
+
ws.close(1013, "server busy");
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
ws.data.admissionLease.bind(ws);
|
|
915
|
+
registerCodexWebSocket(ws);
|
|
916
|
+
},
|
|
917
|
+
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
918
|
+
if (ws.data.kind === "live-sideband") {
|
|
919
|
+
logLiveSidebandFrame("c2u", raw);
|
|
920
|
+
const upstream = ws.data.liveUpstream;
|
|
921
|
+
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
922
|
+
const pending = ws.data.livePending ?? (ws.data.livePending = []);
|
|
923
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) {
|
|
924
|
+
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
pending.push(raw);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (upstream.readyState !== WebSocket.OPEN) {
|
|
931
|
+
closeLiveSideband(ws, 1011, "upstream not open");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
upstream.send(raw);
|
|
936
|
+
} catch {
|
|
937
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const rawBytes = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength;
|
|
942
|
+
if (rawBytes > MAX_WS_FRAME_BYTES) {
|
|
943
|
+
sendJsonFrame(ws, buildWsErrorFrame(413, {
|
|
944
|
+
type: "invalid_request_error",
|
|
945
|
+
message: "WebSocket response.create frame is too large",
|
|
946
|
+
}));
|
|
947
|
+
ws.close(1009, "message too large");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
let frame: Record<string, unknown>;
|
|
951
|
+
try {
|
|
952
|
+
frame = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as Record<string, unknown>;
|
|
953
|
+
} catch {
|
|
954
|
+
return; // text-only contract; ignore unparseable frames
|
|
955
|
+
}
|
|
956
|
+
if (frame.type === "response.processed") return; // ack — no-op
|
|
957
|
+
if (frame.type !== "response.create") return;
|
|
958
|
+
markActivity("ws response.create");
|
|
959
|
+
|
|
960
|
+
ws.data.cancel?.();
|
|
961
|
+
const turnId = (ws.data.turnId ?? 0) + 1;
|
|
962
|
+
ws.data.turnId = turnId;
|
|
963
|
+
const isCurrent = () => ws.data.turnId === turnId;
|
|
964
|
+
const turnAbort = new AbortController();
|
|
965
|
+
const cancelTurn = () => {
|
|
966
|
+
turnAbort.abort("websocket turn superseded or closed");
|
|
967
|
+
};
|
|
968
|
+
ws.data.cancel = cancelTurn;
|
|
969
|
+
// A socket may carry several response.create frames. Clear the previous
|
|
970
|
+
// account before resolving this frame so a failed Multi resolution cannot
|
|
971
|
+
// leave stale invalidation ownership behind.
|
|
972
|
+
updateCodexWebSocketAuthContext(ws, undefined);
|
|
973
|
+
|
|
974
|
+
if (frame.generate === false) {
|
|
975
|
+
for (const payload of buildWarmupCompletionFrames(frame)) {
|
|
976
|
+
if (!isCurrent()) return;
|
|
977
|
+
sendTextFrame(ws, payload);
|
|
978
|
+
}
|
|
979
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const turnAdmissionLease = tryAdmitTurn();
|
|
984
|
+
if (!turnAdmissionLease) {
|
|
985
|
+
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
986
|
+
type: "server_error",
|
|
987
|
+
code: "server_busy",
|
|
988
|
+
message: "active turns capacity reached",
|
|
989
|
+
retryable: true,
|
|
990
|
+
}, new Headers({ "Retry-After": "1" })));
|
|
991
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const payload: Record<string, unknown> = { ...frame };
|
|
996
|
+
delete payload.type;
|
|
997
|
+
turnAdmissionLease.bindAbortController(turnAbort);
|
|
998
|
+
void (async () => {
|
|
999
|
+
const start = Date.now();
|
|
1000
|
+
const requestId = nextRequestLogId(start);
|
|
1001
|
+
// Resolved once at the handshake — a frame has no request headers left
|
|
1002
|
+
// to re-resolve from. Optional on WsData like every other member, so
|
|
1003
|
+
// narrow rather than assume: an unattributed frame is preferable to a
|
|
1004
|
+
// fabricated attribution.
|
|
1005
|
+
const wsAdmission = ws.data.admission;
|
|
1006
|
+
const logCtx: RequestLogContext = {
|
|
1007
|
+
model: "unknown",
|
|
1008
|
+
provider: "unknown",
|
|
1009
|
+
...(wsAdmission ? admissionFields(wsAdmission) : {}),
|
|
1010
|
+
inboundProtocol: "responses",
|
|
1011
|
+
};
|
|
1012
|
+
let logged = false;
|
|
1013
|
+
const finalizeLog = (
|
|
1014
|
+
status: number,
|
|
1015
|
+
meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
1016
|
+
) => {
|
|
1017
|
+
if (logged) return;
|
|
1018
|
+
logged = true;
|
|
1019
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1020
|
+
};
|
|
1021
|
+
const baseHeaders = ws.data.headers ?? new Headers();
|
|
1022
|
+
const fwd = new Headers({ "content-type": "application/json" });
|
|
1023
|
+
baseHeaders.forEach((value, key) => fwd.set(key, value));
|
|
1024
|
+
const req = new Request("http://localhost/v1/responses", {
|
|
1025
|
+
method: "POST",
|
|
1026
|
+
headers: fwd,
|
|
1027
|
+
body: JSON.stringify({ ...payload, stream: true }),
|
|
1028
|
+
});
|
|
1029
|
+
try {
|
|
1030
|
+
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1031
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
1032
|
+
forceEmptyResponseId: true,
|
|
1033
|
+
abortSignal: turnAbort.signal,
|
|
1034
|
+
turnAdmissionLease,
|
|
1035
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1036
|
+
onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
|
|
1037
|
+
recordTerminalOutcomes: false,
|
|
1038
|
+
setTerminalOutcomeRecorder: recorder => {
|
|
1039
|
+
terminalRecorder = recorder;
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
1043
|
+
onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
1044
|
+
onTerminal: status => {
|
|
1045
|
+
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
1046
|
+
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
1047
|
+
terminalStatus: status,
|
|
1048
|
+
closeReason: "terminal",
|
|
1049
|
+
});
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
if (!logged) finalizeLog(turnAbort.signal.aborted ? 499 : response.status);
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
if (!isCurrent()) return;
|
|
1055
|
+
try {
|
|
1056
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
1057
|
+
finalizeLog(429);
|
|
1058
|
+
// Codex Desktop rides this WS transport, so it must carry the same
|
|
1059
|
+
// actionable text as HTTP; a frame has no headers, hence message-only.
|
|
1060
|
+
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
1061
|
+
type: "rate_limit_error",
|
|
1062
|
+
message: cooldownErrorMessage(err),
|
|
1063
|
+
}));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
finalizeLog(502);
|
|
1067
|
+
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
1068
|
+
type: "proxy_error",
|
|
1069
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1070
|
+
}));
|
|
1071
|
+
} catch {
|
|
1072
|
+
/* socket already gone or send dropped */
|
|
1073
|
+
}
|
|
1074
|
+
} finally {
|
|
1075
|
+
turnAdmissionLease.release();
|
|
1076
|
+
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|
|
1077
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
1078
|
+
}
|
|
1079
|
+
})();
|
|
1080
|
+
},
|
|
1081
|
+
close(ws: ServerWebSocket<WsData>) {
|
|
1082
|
+
if (ws.data.kind === "live-sideband") {
|
|
1083
|
+
ws.data.cancel?.();
|
|
1084
|
+
ws.data.liveUpstream = undefined;
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
unregisterCodexWebSocket(ws);
|
|
1088
|
+
ws.data.admissionLease?.release();
|
|
1089
|
+
ws.data.admissionLease = undefined;
|
|
1090
|
+
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
1091
|
+
},
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
setServerRef(server);
|
|
1096
|
+
const actualPort = server.port ?? listenPort;
|
|
1097
|
+
setCorsOrigin(actualPort);
|
|
1098
|
+
|
|
1099
|
+
console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
|
|
1100
|
+
console.log(` POST /v1/responses → provider translation`);
|
|
1101
|
+
console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
|
|
1102
|
+
console.log(` GET /healthz → health check`);
|
|
1103
|
+
console.log(` GET /api/* → management API`);
|
|
1104
|
+
console.log(` GET / → GUI dashboard`);
|
|
1105
|
+
|
|
1106
|
+
// Prime pool-account quota in the background so the rotation engine has real
|
|
1107
|
+
// usage scores from the first routing decision, even when the dashboard is
|
|
1108
|
+
// never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
|
|
1109
|
+
// listener, and a blocked network silently no-ops (see Phase 30 diagnostics).
|
|
1110
|
+
const openAiProvider = config.providers.openai;
|
|
1111
|
+
if (
|
|
1112
|
+
openAiProvider
|
|
1113
|
+
&& openAiProvider.disabled !== true
|
|
1114
|
+
&& isCanonicalOpenAiForwardProvider(openAiProvider)
|
|
1115
|
+
&& providerCodexAccountMode("openai", openAiProvider) === "pool"
|
|
1116
|
+
) {
|
|
1117
|
+
import("../codex/auth-api")
|
|
1118
|
+
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
|
|
1119
|
+
.catch(() => {});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1123
|
+
scheduleStorageCleanupStartupRun();
|
|
1124
|
+
|
|
1125
|
+
return server;
|
|
1126
|
+
}
|