@yansigit/opencodex 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/LICENSE +21 -0
- package/README.md +303 -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 +587 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
- package/gui/dist/assets/index-CGoDO3uO.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/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +108 -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-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +1327 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +121 -0
- package/src/adapters/client-fingerprint.ts +65 -0
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +601 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +288 -0
- package/src/adapters/cursor/discovery.ts +333 -0
- package/src/adapters/cursor/effort-map.ts +151 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +269 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1653 -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 +76 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +547 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +663 -0
- package/src/adapters/cursor/protobuf-events.ts +1381 -0
- package/src/adapters/cursor/protobuf-request.ts +1032 -0
- package/src/adapters/cursor/request-builder.ts +461 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +735 -0
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +79 -0
- package/src/adapters/cursor/types.ts +79 -0
- package/src/adapters/cursor.ts +322 -0
- package/src/adapters/google-antigravity-hosts.ts +48 -0
- package/src/adapters/google-antigravity-replay.ts +827 -0
- package/src/adapters/google-antigravity-tools.ts +105 -0
- package/src/adapters/google-antigravity-wire.ts +141 -0
- package/src/adapters/google-errors.ts +92 -0
- package/src/adapters/google-http.ts +460 -0
- package/src/adapters/google-tool-schema.ts +238 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +1377 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +112 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1936 -0
- package/src/adapters/mimo-free.ts +280 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1980 -0
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1911 -0
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +154 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +1986 -0
- package/src/chat/inbound.ts +319 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +266 -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 +205 -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 +615 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +107 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +578 -0
- package/src/claude/model-info.ts +174 -0
- package/src/claude/outbound.ts +926 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +302 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +737 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +299 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +290 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +213 -0
- package/src/cli/claude.ts +355 -0
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +47 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +127 -0
- package/src/cli/config-command.ts +209 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/dispatch.ts +585 -0
- package/src/cli/doctor.ts +1202 -0
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +213 -0
- package/src/cli/help.ts +101 -0
- package/src/cli/index.ts +973 -0
- package/src/cli/init.ts +211 -0
- package/src/cli/integrations.ts +260 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +245 -0
- package/src/cli/models.ts +422 -0
- package/src/cli/observe.ts +206 -0
- package/src/cli/opencode.ts +588 -0
- package/src/cli/provider-replit.ts +232 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +492 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +422 -0
- package/src/cli/replit-gateway-key-input.ts +138 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +328 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +199 -0
- package/src/cli/v2.ts +268 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1704 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +47 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +544 -0
- package/src/codex/account-usability.ts +43 -0
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/agent-roles-sync.ts +225 -0
- package/src/codex/agent-roles.ts +238 -0
- package/src/codex/app-server-processes.ts +1143 -0
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +2147 -0
- package/src/codex/auth-collision.ts +109 -0
- package/src/codex/auth-context.ts +665 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +436 -0
- package/src/codex/catalog/bundled.ts +549 -0
- package/src/codex/catalog/effort.ts +446 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +664 -0
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +650 -0
- package/src/codex/catalog/provider-fetch.ts +2064 -0
- package/src/codex/catalog/sync.ts +1883 -0
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +14 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1566 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +108 -0
- package/src/codex/history-provider.ts +979 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1733 -0
- package/src/codex/injected-marker.ts +106 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +225 -0
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +68 -0
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +273 -0
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +178 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +492 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +682 -0
- package/src/codex/paths.ts +144 -0
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +425 -0
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +573 -0
- package/src/codex/refresh.ts +62 -0
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +1888 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +2170 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +784 -0
- package/src/codex/sync.ts +319 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +298 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +160 -0
- package/src/combos/index.ts +45 -0
- package/src/combos/request.ts +94 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +398 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +4041 -0
- package/src/fork/register.ts +3 -0
- package/src/generated/compatibility-version.json +3116 -0
- package/src/generated/model-metadata.ts +106 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +530 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +955 -0
- package/src/images/plan.ts +143 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/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 +195 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +346 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +130 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +364 -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 +380 -0
- package/src/lib/errors.ts +406 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/pinned-http.ts +270 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-outbound.ts +210 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +521 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +61 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +119 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +86 -0
- package/src/lib/tool-argument-integers.ts +202 -0
- package/src/lib/translator-budget.ts +400 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +392 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-atomic-replace.ts +156 -0
- package/src/lib/windows-elevation.ts +773 -0
- package/src/lib/windows-secret-acl.ts +854 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +403 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +188 -0
- package/src/oauth/antigravity-routing.ts +151 -0
- package/src/oauth/callback-server.ts +300 -0
- package/src/oauth/chatgpt.ts +161 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +252 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +262 -0
- package/src/oauth/health.ts +407 -0
- package/src/oauth/index.ts +1504 -0
- package/src/oauth/key-providers.ts +124 -0
- package/src/oauth/kimi.ts +227 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +130 -0
- package/src/oauth/log.ts +50 -0
- package/src/oauth/login-cli.ts +223 -0
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +728 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +695 -0
- package/src/providers/antigravity-quota.ts +216 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +74 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +82 -0
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +586 -0
- package/src/providers/fastwire.ts +501 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +449 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/openai-sidecar.ts +243 -0
- package/src/providers/openai-tier-startup.ts +56 -0
- package/src/providers/openai-tiers.ts +423 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +185 -0
- package/src/providers/quota.ts +2345 -0
- package/src/providers/registry.ts +2918 -0
- package/src/providers/replit/constants.ts +27 -0
- package/src/providers/replit/derive.ts +85 -0
- package/src/providers/replit/headers.ts +28 -0
- package/src/providers/replit/origin.ts +55 -0
- package/src/providers/replit/pair-install-response.ts +72 -0
- package/src/providers/replit/probe.ts +199 -0
- package/src/providers/replit/setup.ts +350 -0
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +103 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +148 -0
- package/src/reasoning-effort.ts +183 -0
- package/src/responses/compaction.ts +142 -0
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +838 -0
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +426 -0
- package/src/responses/schema.ts +165 -0
- package/src/responses/spill-store.ts +459 -0
- package/src/responses/state.ts +1433 -0
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +761 -0
- package/src/routing/analytics.ts +378 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +547 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +53 -0
- package/src/server/auth-cors.ts +751 -0
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +442 -0
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +1030 -0
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +190 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +568 -0
- package/src/server/index.ts +1813 -0
- package/src/server/lifecycle.ts +498 -0
- package/src/server/live.ts +717 -0
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +1433 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +193 -0
- package/src/server/management/body.ts +41 -0
- package/src/server/management/combo-routes.ts +263 -0
- package/src/server/management/config-routes.ts +835 -0
- package/src/server/management/context.ts +113 -0
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +586 -0
- package/src/server/management/model-routes.ts +560 -0
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +637 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +1033 -0
- package/src/server/management/replit-provider-routes.ts +86 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +385 -0
- package/src/server/management/shared.ts +286 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +435 -0
- package/src/server/management/system-routes.ts +194 -0
- package/src/server/management/usage-summary-cache.ts +94 -0
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +314 -0
- package/src/server/management-auth.ts +482 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +328 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1209 -0
- package/src/server/request-decompress.ts +239 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1259 -0
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/collaboration.ts +551 -0
- package/src/server/responses/compact.ts +771 -0
- package/src/server/responses/core.ts +5389 -0
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +331 -0
- package/src/server/responses/fetch-helpers.ts +232 -0
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -0
- package/src/server/responses/terminal-guard.ts +251 -0
- package/src/server/responses/upstream-error.ts +53 -0
- package/src/server/responses/ws-upstream.ts +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +25 -0
- package/src/server/search.ts +201 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +315 -0
- package/src/server/startup-health-cache.ts +131 -0
- package/src/server/system-env.ts +484 -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 +472 -0
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +3575 -0
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +757 -0
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +876 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +106 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +415 -0
- package/src/update/job.ts +1887 -0
- package/src/update/notify.ts +263 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -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 +625 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +416 -0
- package/src/usage/log.ts +1223 -0
- package/src/usage/summary.ts +753 -0
- package/src/usage/totals.ts +14 -0
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +189 -0
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +131 -0
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +681 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +195 -0
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/executor.ts +113 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +331 -0
- package/src/web-search/loop.ts +896 -0
- package/src/web-search/parse.ts +315 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/synthetic-tool.ts +47 -0
- package/src/web-search/xai-executor.ts +219 -0
|
@@ -0,0 +1,1813 @@
|
|
|
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
|
+
getConfigDir,
|
|
20
|
+
websocketsEnabled,
|
|
21
|
+
} from "../config";
|
|
22
|
+
import { reconcileOAuthProviders } from "../oauth";
|
|
23
|
+
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
24
|
+
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
25
|
+
import { getCodexHome } from "../codex/paths";
|
|
26
|
+
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
27
|
+
import {
|
|
28
|
+
inspectNativeCodexOwnership,
|
|
29
|
+
type OwnershipInspection,
|
|
30
|
+
} from "../integrations/native/ownership-preflight";
|
|
31
|
+
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
32
|
+
import {
|
|
33
|
+
reconcileLiveStateStores,
|
|
34
|
+
setLiveStateStoreConfig,
|
|
35
|
+
} from "../lib/state-store-registrations";
|
|
36
|
+
import { startUserCostOverlayReconciler } from "../usage/user-cost-overlay-reconciler";
|
|
37
|
+
import {
|
|
38
|
+
configureAppOwnedMemoryBudget,
|
|
39
|
+
enforceAppOwnedMemoryBudget,
|
|
40
|
+
resolveAppOwnedMemoryBudgetBytes,
|
|
41
|
+
} from "../lib/app-owned-memory";
|
|
42
|
+
import {
|
|
43
|
+
registerAppOwnedMemorySweepFallback,
|
|
44
|
+
registerDefaultAppOwnedMemoryStores,
|
|
45
|
+
registerDefaultAppOwnedObservedBuffers,
|
|
46
|
+
} from "../lib/app-owned-memory-stores";
|
|
47
|
+
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
48
|
+
import { activateLab, labActivationRequired } from "../lib/lab-activation";
|
|
49
|
+
import { registerFork } from "../fork/register";
|
|
50
|
+
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
51
|
+
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
52
|
+
import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
|
|
53
|
+
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
54
|
+
import { providerCodexAccountMode } from "../providers/registry";
|
|
55
|
+
import type { StorageCleanupPolicy } from "../types";
|
|
56
|
+
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
57
|
+
import {
|
|
58
|
+
CodexAccountCooldownError,
|
|
59
|
+
cooldownErrorMessage,
|
|
60
|
+
} from "../codex/auth-context";
|
|
61
|
+
import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
|
|
62
|
+
import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex/account-namespaces";
|
|
63
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
64
|
+
import {
|
|
65
|
+
availableAccountGatedNativeModels,
|
|
66
|
+
resolveCodexModelEntitlements,
|
|
67
|
+
} from "../codex/model-entitlements";
|
|
68
|
+
export {
|
|
69
|
+
clearThreadAccountMap,
|
|
70
|
+
formatCodexProviderForLog,
|
|
71
|
+
resolveCodexAccountForThread,
|
|
72
|
+
} from "../codex/routing";
|
|
73
|
+
import { formatCodexProviderForLog } from "../codex/routing";
|
|
74
|
+
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
75
|
+
import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
|
|
76
|
+
import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile, serveSessionBootstrap } from "./gui-static";
|
|
77
|
+
export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
|
|
78
|
+
export { resolveAdapter } from "./adapter-resolve";
|
|
79
|
+
import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
|
|
80
|
+
import {
|
|
81
|
+
drainAndShutdown,
|
|
82
|
+
getActiveTurnCount,
|
|
83
|
+
isDraining,
|
|
84
|
+
registerTurn,
|
|
85
|
+
runListenerShutdown,
|
|
86
|
+
setServerRef,
|
|
87
|
+
trackStreamLifetime,
|
|
88
|
+
tryAdmitTurn,
|
|
89
|
+
unregisterTurn,
|
|
90
|
+
type ActiveTurnLease,
|
|
91
|
+
} from "./lifecycle";
|
|
92
|
+
export {
|
|
93
|
+
drainAndShutdown,
|
|
94
|
+
getActiveTurnCount,
|
|
95
|
+
isDraining,
|
|
96
|
+
isRecyclingForExit,
|
|
97
|
+
markRecyclingForExit,
|
|
98
|
+
registerTurn,
|
|
99
|
+
trackStreamLifetime,
|
|
100
|
+
unregisterTurn,
|
|
101
|
+
} from "./lifecycle";
|
|
102
|
+
import {
|
|
103
|
+
addFinalRequestLog,
|
|
104
|
+
hydrateRequestLogsFromDisk,
|
|
105
|
+
httpStatusForRequestLogTerminal,
|
|
106
|
+
inspectResponseLogSsePayload,
|
|
107
|
+
nextRequestLogId,
|
|
108
|
+
recordFirstOutput,
|
|
109
|
+
type RequestLogContext,
|
|
110
|
+
type RequestLogEntry,
|
|
111
|
+
} from "./request-log";
|
|
112
|
+
export {
|
|
113
|
+
addFinalRequestLog,
|
|
114
|
+
filterRequestLogs,
|
|
115
|
+
hydrateRequestLogsFromDisk,
|
|
116
|
+
httpStatusForTerminalStatus,
|
|
117
|
+
httpStatusFromTerminalError,
|
|
118
|
+
nextRequestLogId,
|
|
119
|
+
requestLogErrorCode,
|
|
120
|
+
requestLogSpeedLabel,
|
|
121
|
+
usageFromResponsesPayload,
|
|
122
|
+
type RequestLogContext,
|
|
123
|
+
type RequestLogEntry,
|
|
124
|
+
} from "./request-log";
|
|
125
|
+
import {
|
|
126
|
+
consumeForInspection,
|
|
127
|
+
relaySseWithHeartbeat,
|
|
128
|
+
relayWithAbort,
|
|
129
|
+
responseWithDeferredRequestLog,
|
|
130
|
+
sanitizePassthroughHeaders,
|
|
131
|
+
} from "./relay";
|
|
132
|
+
export {
|
|
133
|
+
consumeForInspection,
|
|
134
|
+
relaySseWithFailedTail,
|
|
135
|
+
relaySseWithHeartbeat,
|
|
136
|
+
relayWithAbort,
|
|
137
|
+
responseWithDeferredRequestLog,
|
|
138
|
+
sanitizePassthroughHeaders,
|
|
139
|
+
} from "./relay";
|
|
140
|
+
import {
|
|
141
|
+
assertServerAuthConfig,
|
|
142
|
+
corsHeaders,
|
|
143
|
+
managementCorsHeaders,
|
|
144
|
+
isAllowedRequestOrigin,
|
|
145
|
+
isAllowedManagementOrigin,
|
|
146
|
+
isApiAuthRequired,
|
|
147
|
+
isLoopbackHostname,
|
|
148
|
+
jsonResponse,
|
|
149
|
+
admissionFields,
|
|
150
|
+
resolveApiAuth,
|
|
151
|
+
resolveResponsesApiAuth,
|
|
152
|
+
requestPolicyView,
|
|
153
|
+
type RequestPolicyView,
|
|
154
|
+
safeConfigDTO,
|
|
155
|
+
setCorsOrigin,
|
|
156
|
+
withCors,
|
|
157
|
+
withManagementCors,
|
|
158
|
+
} from "./auth-cors";
|
|
159
|
+
export {
|
|
160
|
+
assertServerAuthConfig,
|
|
161
|
+
corsHeaders,
|
|
162
|
+
hasValidApiAuth,
|
|
163
|
+
isApiAuthRequired,
|
|
164
|
+
isLoopbackHostname,
|
|
165
|
+
jsonResponse,
|
|
166
|
+
safeConfigDTO,
|
|
167
|
+
} from "./auth-cors";
|
|
168
|
+
import { disableResponsesRequestTimeout, handleResponses, handleResponsesCompact } from "./responses";
|
|
169
|
+
export { disableResponsesRequestTimeout, linkAbortSignal } from "./responses";
|
|
170
|
+
import { handleClaudeCountTokens, handleClaudeMessages } from "./claude-messages";
|
|
171
|
+
import { handleChatCompletions } from "./chat-completions";
|
|
172
|
+
import { anthropicErrorResponse } from "../claude/outbound";
|
|
173
|
+
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
174
|
+
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
175
|
+
import {
|
|
176
|
+
bindNativeMainStartupLifecycle,
|
|
177
|
+
blockNativeMainStartupForUnownedServiceHome,
|
|
178
|
+
releaseNativeMainStartupLifecycle,
|
|
179
|
+
startNativeMainStartupLifecycle,
|
|
180
|
+
type NativeMainStartupGateDeps,
|
|
181
|
+
type NativeMainStartupLifecycle,
|
|
182
|
+
} from "../codex/native-profile-startup";
|
|
183
|
+
import { handleImages } from "./images";
|
|
184
|
+
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
185
|
+
import { handleSearch } from "./search";
|
|
186
|
+
import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
|
|
187
|
+
import {
|
|
188
|
+
initializeManagementAuthState,
|
|
189
|
+
issueGuiSession,
|
|
190
|
+
managementPrincipal,
|
|
191
|
+
requireManagementAuth,
|
|
192
|
+
type ManagementAuthState,
|
|
193
|
+
} from "./management-auth";
|
|
194
|
+
import {
|
|
195
|
+
LOCAL_ATTESTATION_CHALLENGE_HEADER,
|
|
196
|
+
LOCAL_ATTESTATION_PROOF_HEADER,
|
|
197
|
+
createLocalAttestationProof,
|
|
198
|
+
createLocalAttestationSecret,
|
|
199
|
+
} from "../lib/local-management-attestation";
|
|
200
|
+
import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
|
|
201
|
+
import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
|
|
202
|
+
import { createReadinessGate, type ReadinessGate } from "./readiness";
|
|
203
|
+
|
|
204
|
+
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
205
|
+
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
206
|
+
const LIVE_SIDEBAND_PENDING_MAX = 32;
|
|
207
|
+
const LIVE_SIDEBAND_PENDING_BYTES_MAX = 1024 * 1024;
|
|
208
|
+
const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
|
|
209
|
+
|
|
210
|
+
export function exceedsLiveSidebandFrameByteLimit(frameBytes: number): boolean {
|
|
211
|
+
return frameBytes > MAX_WS_FRAME_BYTES;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function exceedsLiveSidebandPendingByteLimit(pendingBytes: number, incomingBytes: number): boolean {
|
|
215
|
+
return incomingBytes > LIVE_SIDEBAND_PENDING_BYTES_MAX - pendingBytes;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function webSocketFrameBytes(frame: string | ArrayBuffer | ArrayBufferView | Blob | Buffer): number {
|
|
219
|
+
if (typeof frame === "string") return Buffer.byteLength(frame);
|
|
220
|
+
if (frame instanceof ArrayBuffer || ArrayBuffer.isView(frame)) return frame.byteLength;
|
|
221
|
+
return frame.size;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type LiveSidebandPendingEnqueueResult = "queued" | "too-many-frames" | "too-many-bytes";
|
|
225
|
+
|
|
226
|
+
export function enqueueLiveSidebandPendingFrame(
|
|
227
|
+
data: Pick<WsData, "livePending" | "livePendingBytes">,
|
|
228
|
+
frame: string | Buffer,
|
|
229
|
+
frameBytes = webSocketFrameBytes(frame),
|
|
230
|
+
): LiveSidebandPendingEnqueueResult {
|
|
231
|
+
const pending = data.livePending ?? (data.livePending = []);
|
|
232
|
+
if (pending.length >= LIVE_SIDEBAND_PENDING_MAX) return "too-many-frames";
|
|
233
|
+
const pendingBytes = data.livePendingBytes ?? 0;
|
|
234
|
+
if (exceedsLiveSidebandPendingByteLimit(pendingBytes, frameBytes)) return "too-many-bytes";
|
|
235
|
+
pending.push(frame);
|
|
236
|
+
data.livePendingBytes = pendingBytes + frameBytes;
|
|
237
|
+
return "queued";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type LiveSidebandWebSocketFactory = (
|
|
241
|
+
url: string,
|
|
242
|
+
headers: Record<string, string>,
|
|
243
|
+
) => WebSocket;
|
|
244
|
+
|
|
245
|
+
function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
|
|
246
|
+
ws.data.liveTurnAdmissionLease?.release();
|
|
247
|
+
ws.data.liveTurnAdmissionLease = undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Send one live-sideband frame to the upstream socket.
|
|
252
|
+
*
|
|
253
|
+
* Bun's `WebSocket.send` accepts `string | Blob | BufferSource`, but the DOM-lib
|
|
254
|
+
* `Buffer` can be backed by a `SharedArrayBuffer`, which `BufferSource` rejects.
|
|
255
|
+
* `Uint8Array.from` copies into a fresh `ArrayBuffer`-backed view, so a frame
|
|
256
|
+
* arriving from `node:buffer` still round-trips byte-for-byte.
|
|
257
|
+
*/
|
|
258
|
+
function sendUpstreamFrame(upstream: WebSocket, frame: string | Buffer): void {
|
|
259
|
+
if (typeof frame === "string") {
|
|
260
|
+
upstream.send(frame);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
upstream.send(Uint8Array.from(frame));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
|
|
267
|
+
if (upstream && ws.data.liveUpstream !== upstream) return;
|
|
268
|
+
if (ws.data.liveCloseFallback !== undefined) {
|
|
269
|
+
clearTimeout(ws.data.liveCloseFallback);
|
|
270
|
+
ws.data.liveCloseFallback = undefined;
|
|
271
|
+
}
|
|
272
|
+
ws.data.liveUpstream = undefined;
|
|
273
|
+
ws.data.livePending = undefined;
|
|
274
|
+
ws.data.livePendingBytes = undefined;
|
|
275
|
+
ws.data.cancel = undefined;
|
|
276
|
+
releaseLiveSidebandAdmission(ws);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
|
|
280
|
+
if (ws.data.liveCloseFallback !== undefined) return;
|
|
281
|
+
ws.data.liveCloseFallback = setTimeout(() => {
|
|
282
|
+
ws.data.liveCloseFallback = undefined;
|
|
283
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
284
|
+
if (upstream.readyState === WebSocket.CLOSED) {
|
|
285
|
+
finalizeLiveSideband(ws, upstream);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// A close frame was already sent below. Retry once, but never surrender
|
|
289
|
+
// native-main ownership while the authenticated transport remains live.
|
|
290
|
+
try {
|
|
291
|
+
upstream.close(1000, "upstream close timeout");
|
|
292
|
+
} catch {
|
|
293
|
+
/* upstream is already unusable */
|
|
294
|
+
}
|
|
295
|
+
// Some implementations transition synchronously without delivering the
|
|
296
|
+
// close event. That is still an observed CLOSED transport and is safe to
|
|
297
|
+
// finalize. CONNECTING/CLOSING peers keep the lease so profile switching
|
|
298
|
+
// fails at its own bounded drain deadline instead of racing live traffic.
|
|
299
|
+
// The earlier CLOSED check narrowed `readyState` to 0|1|2 in the type
|
|
300
|
+
// system, but the socket can still transition to CLOSED (3) before this
|
|
301
|
+
// fallback fires; the cast keeps the runtime-identical check.
|
|
302
|
+
if ((upstream.readyState as number) === 3) finalizeLiveSideband(ws, upstream);
|
|
303
|
+
}, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
|
|
307
|
+
if (ws.data.liveClosing) return;
|
|
308
|
+
ws.data.liveClosing = true;
|
|
309
|
+
ws.data.livePending = undefined;
|
|
310
|
+
ws.data.livePendingBytes = undefined;
|
|
311
|
+
ws.data.cancel = undefined;
|
|
312
|
+
const upstream = ws.data.liveUpstream;
|
|
313
|
+
// Bun's `WebSocket` type narrows `readyState` to 0|1|2 even though the DOM
|
|
314
|
+
// constant CLOSED is 3; the numeric literal is the runtime-identical check.
|
|
315
|
+
if (!upstream || upstream.readyState === 3) {
|
|
316
|
+
finalizeLiveSideband(ws, upstream);
|
|
317
|
+
} else {
|
|
318
|
+
// The sideband holds a native-main admission lease. Do not release it just
|
|
319
|
+
// because the downstream left: its authenticated upstream remains live
|
|
320
|
+
// until the close event arrives or the transport is observed CLOSED. The
|
|
321
|
+
// bounded fallback only retries close; it does not release ownership.
|
|
322
|
+
armLiveSidebandCloseFallback(ws, upstream);
|
|
323
|
+
try {
|
|
324
|
+
upstream.close(code, reason);
|
|
325
|
+
} catch {
|
|
326
|
+
/* the fallback retries close without releasing ownership */
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
331
|
+
ws.close(code, reason);
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
/* client already gone */
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function attachLiveSidebandUpstream(
|
|
339
|
+
ws: ServerWebSocket<WsData>,
|
|
340
|
+
createWebSocket: LiveSidebandWebSocketFactory = (url, headers) => (
|
|
341
|
+
new WebSocket(url, { headers } as unknown as string[])
|
|
342
|
+
),
|
|
343
|
+
): void {
|
|
344
|
+
const url = ws.data.liveUpstreamUrl;
|
|
345
|
+
if (!url) {
|
|
346
|
+
closeLiveSideband(ws, 1011, "missing upstream");
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
let upstream: WebSocket;
|
|
350
|
+
try {
|
|
351
|
+
// Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
|
|
352
|
+
upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {});
|
|
353
|
+
} catch {
|
|
354
|
+
closeLiveSideband(ws, 1011, "upstream connect failed");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
ws.data.liveUpstream = upstream;
|
|
358
|
+
ws.data.liveClosing = false;
|
|
359
|
+
ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
|
|
360
|
+
|
|
361
|
+
upstream.addEventListener("open", () => {
|
|
362
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
363
|
+
ws.data.liveOpened = true;
|
|
364
|
+
const pending = ws.data.livePending ?? [];
|
|
365
|
+
ws.data.livePending = undefined;
|
|
366
|
+
ws.data.livePendingBytes = undefined;
|
|
367
|
+
for (const frame of pending) {
|
|
368
|
+
try {
|
|
369
|
+
sendUpstreamFrame(upstream, frame);
|
|
370
|
+
} catch {
|
|
371
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
upstream.addEventListener("message", (event) => {
|
|
377
|
+
if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
|
|
378
|
+
try {
|
|
379
|
+
if (exceedsLiveSidebandFrameByteLimit(webSocketFrameBytes(event.data))) {
|
|
380
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
logLiveSidebandFrame("u2c", event.data);
|
|
384
|
+
if (typeof event.data === "string") ws.send(event.data);
|
|
385
|
+
else if (event.data instanceof ArrayBuffer) ws.send(event.data);
|
|
386
|
+
else if (ArrayBuffer.isView(event.data)) {
|
|
387
|
+
ws.send(event.data.buffer.slice(event.data.byteOffset, event.data.byteOffset + event.data.byteLength));
|
|
388
|
+
} else ws.send(event.data as Buffer);
|
|
389
|
+
} catch {
|
|
390
|
+
closeLiveSideband(ws, 1011, "client send failed");
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
upstream.addEventListener("close", (event) => {
|
|
394
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
395
|
+
ws.data.liveClosing = true;
|
|
396
|
+
finalizeLiveSideband(ws, upstream);
|
|
397
|
+
try {
|
|
398
|
+
ws.close(event.code || 1000, event.reason || "");
|
|
399
|
+
} catch {
|
|
400
|
+
/* ignore */
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
upstream.addEventListener("error", () => {
|
|
404
|
+
if (ws.data.liveUpstream !== upstream) return;
|
|
405
|
+
closeLiveSideband(ws, 1011, "upstream error");
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
|
|
410
|
+
// "../src/server" import surface stable for tests/callers.
|
|
411
|
+
|
|
412
|
+
// Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
|
|
413
|
+
|
|
414
|
+
// Source invariant for tests/passthrough-abort.test.ts after the pure module split:
|
|
415
|
+
// if (isEventStream && upstreamResponse.body) {
|
|
416
|
+
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
417
|
+
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
418
|
+
// #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
|
|
419
|
+
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
|
|
420
|
+
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
|
|
421
|
+
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
422
|
+
// Codex upstream WS runtime gating and the forced bounded single-reader branch
|
|
423
|
+
// are owned by responses/ws-upstream.ts and responses/core.ts respectively.
|
|
424
|
+
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
425
|
+
// new Response(eagerBody,
|
|
426
|
+
// Default shape (tee + background inspection):
|
|
427
|
+
// upstreamResponse.body.tee()
|
|
428
|
+
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
429
|
+
// relaySseWithFailedTail(repairedBody, upstream)
|
|
430
|
+
// new Response(clientBody
|
|
431
|
+
// markNativePassthroughSseResponse
|
|
432
|
+
// const body = relayWithAbort(upstreamResponse.body, upstream);
|
|
433
|
+
// function responseWithDeferredRequestLog
|
|
434
|
+
// isNativePassthroughSseResponse(response)
|
|
435
|
+
// trackSseForRequestLog(
|
|
436
|
+
// export function relaySseWithHeartbeat
|
|
437
|
+
|
|
438
|
+
export interface StartServerDeps {
|
|
439
|
+
/** Test-only seam; production always initializes its own management credential state. */
|
|
440
|
+
managementAuthState?: ManagementAuthState;
|
|
441
|
+
/** Test-only route dependencies, forwarded only after management admission succeeds. */
|
|
442
|
+
managementApi?: ManagementApiDeps;
|
|
443
|
+
/** Test-only native-main recovery dependencies; production constructs the normal manager. */
|
|
444
|
+
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
445
|
+
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
446
|
+
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
447
|
+
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
448
|
+
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
449
|
+
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
450
|
+
localAttestationSecret?: string;
|
|
451
|
+
/** Optional readiness gate; a fresh pending gate is created when omitted. */
|
|
452
|
+
readinessGate?: ReadinessGate;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
|
|
456
|
+
try {
|
|
457
|
+
return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
|
|
458
|
+
} catch {
|
|
459
|
+
return {
|
|
460
|
+
ownership: "unknown",
|
|
461
|
+
reason: "service-home ownership inspection failed",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/*
|
|
467
|
+
* #1046. `startServer` rewrites the Codex models cache during boot, and an
|
|
468
|
+
* app-server that started earlier keeps its own in-memory model list. The stale
|
|
469
|
+
* warning is not emitted here: `handleStart` runs a catalog sync moments later,
|
|
470
|
+
* so warning now would read an mtime that write is about to move, and both sites
|
|
471
|
+
* calling the helper independently would warn twice. This records the fact; the
|
|
472
|
+
* CLI start path owns the single decision.
|
|
473
|
+
*
|
|
474
|
+
* A caller that starts a server without `handleStart` (tests, embedded use)
|
|
475
|
+
* deliberately gets no warning — lifecycle diagnostics belong to whoever owns
|
|
476
|
+
* the lifecycle.
|
|
477
|
+
*/
|
|
478
|
+
let startupCacheInvalidationWrote = false;
|
|
479
|
+
|
|
480
|
+
/** #1046: did this process's startup cache invalidation actually write? */
|
|
481
|
+
export function consumeStartupCacheInvalidationWrite(): boolean {
|
|
482
|
+
const wrote = startupCacheInvalidationWrote;
|
|
483
|
+
startupCacheInvalidationWrote = false;
|
|
484
|
+
return wrote;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function warnAgentTaskRecoveryStartup(config: {
|
|
488
|
+
agentTaskRecovery?: { enabled?: boolean };
|
|
489
|
+
}): void {
|
|
490
|
+
if (config.agentTaskRecovery?.enabled !== true) return;
|
|
491
|
+
console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
|
|
492
|
+
console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
|
|
493
|
+
console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
497
|
+
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
498
|
+
const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
|
|
499
|
+
warnAgentTaskRecoveryStartup(config);
|
|
500
|
+
setLiveStateStoreConfig(config);
|
|
501
|
+
applyProxyEnv(config);
|
|
502
|
+
assertServerAuthConfig(config);
|
|
503
|
+
const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
|
|
504
|
+
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
505
|
+
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
506
|
+
// before any request can resolve its physical credential, while health/management/Pool stay live.
|
|
507
|
+
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
508
|
+
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
509
|
+
reconcileOAuthProviders(config);
|
|
510
|
+
reconcileLiveStateStores();
|
|
511
|
+
// Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
|
|
512
|
+
// even [], is left alone so GUI removals persist.
|
|
513
|
+
if (config.subagentModels === undefined) {
|
|
514
|
+
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
515
|
+
saveConfig(config);
|
|
516
|
+
}
|
|
517
|
+
// authMode migration (devlog 260726_claude_auth_auto/015): before "auto" existed,
|
|
518
|
+
// choosing Subscription DELETED the key, so a pre-upgrade block with no authMode is
|
|
519
|
+
// indistinguishable from "never chose". Pin those to subscription once so an upgrade
|
|
520
|
+
// never silently moves a deliberate subscriber onto proxy.
|
|
521
|
+
if (runClaudeAuthModeMigration(config)) saveConfig(config);
|
|
522
|
+
// Sidecar model migration (KST 2026-07-10 06:00 = UTC 2026-07-09 21:00): auto-migrate the old
|
|
523
|
+
// gpt-5.4-mini default to gpt-5.6-luna for both search and vision sidecars. Only touches configs
|
|
524
|
+
// still on the old default — explicit user choices are preserved.
|
|
525
|
+
{
|
|
526
|
+
const SIDECAR_MIGRATION_CUTOFF = Date.UTC(2026, 6, 9, 21, 0); // July 9 21:00 UTC = KST July 10 06:00
|
|
527
|
+
if (Date.now() >= SIDECAR_MIGRATION_CUTOFF) {
|
|
528
|
+
let migrated = false;
|
|
529
|
+
if (config.webSearchSidecar?.model === "gpt-5.4-mini") {
|
|
530
|
+
config.webSearchSidecar = { ...config.webSearchSidecar, model: "gpt-5.6-luna" };
|
|
531
|
+
migrated = true;
|
|
532
|
+
}
|
|
533
|
+
if (config.visionSidecar?.model === "gpt-5.4-mini") {
|
|
534
|
+
config.visionSidecar = { ...config.visionSidecar, model: "gpt-5.6-luna" };
|
|
535
|
+
migrated = true;
|
|
536
|
+
}
|
|
537
|
+
if (migrated) saveConfig(config);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
// Resolve unattended service-home authority before any Codex lock, cache, owner,
|
|
541
|
+
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
542
|
+
// ownership state are non-authority.
|
|
543
|
+
startupCacheInvalidationWrote = false;
|
|
544
|
+
const startupCacheOwnership = inspectStartupOwnership(deps);
|
|
545
|
+
// Startup cache invalidation is best-effort and must never block the server from
|
|
546
|
+
// serving. It now takes K so it cannot race a convergence commit, but both the
|
|
547
|
+
// home resolution and the acquisition can fail on a machine with no Codex home —
|
|
548
|
+
// `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
|
|
549
|
+
// otherwise turn "no Codex installed" into "proxy will not start".
|
|
550
|
+
if (startupCacheOwnership.ownership === "owned") {
|
|
551
|
+
try {
|
|
552
|
+
const startupCodexHome = getCodexHome();
|
|
553
|
+
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
554
|
+
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
555
|
+
// here instead would read a catalog mtime the sync is about to move.
|
|
556
|
+
const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
|
|
557
|
+
invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
|
|
558
|
+
// A refused permit is not a write; only a completed run that returned true is.
|
|
559
|
+
startupCacheInvalidationWrote = outcome.kind === "completed" && outcome.value === true;
|
|
560
|
+
} catch { /* no readable Codex home: nothing to invalidate */ }
|
|
561
|
+
}
|
|
562
|
+
// Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
|
|
563
|
+
// the server can serve a request, and AFTER the startup migrations above — those run
|
|
564
|
+
// against a config nobody else holds and are the documented exception to the save
|
|
565
|
+
// boundary, so the baseline should reflect what they wrote. Arming is eager on
|
|
566
|
+
// purpose: a lazy "arm on first save" loses exactly the hand edit made before that
|
|
567
|
+
// first save, which is the case the guard exists for.
|
|
568
|
+
armClaudeCodeBaseline(config);
|
|
569
|
+
// usage.jsonl already persists every request; rehydrate the in-memory Logs ring so
|
|
570
|
+
// /api/logs (and the GUI) survive `ocx stop` / `ocx start` process restarts.
|
|
571
|
+
hydrateRequestLogsFromDisk();
|
|
572
|
+
registerDefaultAppOwnedMemoryStores();
|
|
573
|
+
registerDefaultAppOwnedObservedBuffers();
|
|
574
|
+
registerAppOwnedMemorySweepFallback();
|
|
575
|
+
configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
|
|
576
|
+
enforceAppOwnedMemoryBudget();
|
|
577
|
+
registerCodexCooldownRecoveryProbeWorker(config);
|
|
578
|
+
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
579
|
+
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
580
|
+
// Heavy work runs in a Worker via the single-flight job controller.
|
|
581
|
+
// Keep live config.policy in sync when background runs advance nextRun/lastRun.
|
|
582
|
+
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
583
|
+
config.storageCleanupPolicy = policy;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const listenPort = port ?? config.port ?? 10100;
|
|
587
|
+
setCorsOrigin(listenPort);
|
|
588
|
+
|
|
589
|
+
// Canonicalize an explicit "localhost" bind to IPv4 so it matches the injected base_url (which
|
|
590
|
+
// resolves localhost→127.0.0.1): on Windows `localhost` resolves ::1-first, but the injected URL
|
|
591
|
+
// is 127.0.0.1, so binding literal "localhost" would reintroduce the F4 refusal. Wildcards
|
|
592
|
+
// (0.0.0.0/::) and specific hosts are left untouched so intentional exposure is preserved.
|
|
593
|
+
const configuredHost = config.hostname?.trim();
|
|
594
|
+
const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
|
|
595
|
+
|
|
596
|
+
// Unauthenticated loopback listener (#1102). Off unless explicitly enabled.
|
|
597
|
+
const loopbackListener = config.unauthenticatedLoopbackListener;
|
|
598
|
+
const loopbackListenerPort = loopbackListener?.enabled ? loopbackListener.port : null;
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Which listener a request arrived on, expressed as the only thing that differs: the bind
|
|
602
|
+
* address the auth and CORS decisions should see.
|
|
603
|
+
*
|
|
604
|
+
* The public listener passes the shared config through untouched, so its behaviour is
|
|
605
|
+
* byte-identical to before. The loopback listener substitutes 127.0.0.1, which is what makes
|
|
606
|
+
* `isApiAuthRequired` return false for it — the same code path a plain loopback bind has
|
|
607
|
+
* always taken, including the Host-header check inside `isAllowedRequestOrigin`.
|
|
608
|
+
*
|
|
609
|
+
* Built per request rather than once per listener so a management-API config change is
|
|
610
|
+
* picked up immediately instead of being frozen at listen time.
|
|
611
|
+
*/
|
|
612
|
+
const publicPolicy = (): RequestPolicyView => config;
|
|
613
|
+
const loopbackPolicy = (): RequestPolicyView => requestPolicyView(config, "127.0.0.1");
|
|
614
|
+
void publicPolicy;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Routes the unauthenticated loopback listener will serve. Everything else 404s.
|
|
618
|
+
*
|
|
619
|
+
* This is an allowlist rather than a filter applied to the public handler, because a filter
|
|
620
|
+
* inverts the failure mode: a route added later would be reachable here by default. The four
|
|
621
|
+
* entries are exactly what a directly-spawned `codex app-server` needs.
|
|
622
|
+
*
|
|
623
|
+
* `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
|
|
624
|
+
* materialization fails or finds no source, `syncCodex` warns and injects with
|
|
625
|
+
* `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
|
|
626
|
+
* through `GET {base_url}/models`. Returning 404 there would leave the picker on its bundled
|
|
627
|
+
* fallback — fixing the direct-spawn host while breaking its model list.
|
|
628
|
+
*/
|
|
629
|
+
function loopbackRouteAllowed(url: URL, req: Request): boolean {
|
|
630
|
+
const path = url.pathname;
|
|
631
|
+
if (path === "/v1/responses") {
|
|
632
|
+
return req.method === "POST" || req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
633
|
+
}
|
|
634
|
+
if (path === "/v1/responses/compact") return req.method === "POST";
|
|
635
|
+
if (path === "/v1/models") return req.method === "GET";
|
|
636
|
+
// Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
|
|
637
|
+
// transport) — a directly-spawned `codex app-server` needs these for desktop
|
|
638
|
+
// voice the same way it needs /v1/responses. WebSocket upgrades only; plain
|
|
639
|
+
// HTTP on these paths stays rejected.
|
|
640
|
+
if (path === "/v1/realtime" || path === "/v1/live") {
|
|
641
|
+
return req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
|
|
647
|
+
// the server_is_overloaded code so clients can back off, but always return a JSON envelope.
|
|
648
|
+
// These two run BEFORE the auth/origin checks, so they need the receiving listener's policy
|
|
649
|
+
// explicitly (#1102). Reaching for the shared `config` here would attach public-policy CORS
|
|
650
|
+
// headers to a 503 on the loopback listener — no model runs and no credential is spent, but
|
|
651
|
+
// it is the one error path that would answer a rebinding origin with its own origin echoed
|
|
652
|
+
// back.
|
|
653
|
+
function drainingResponse(req: Request, policy: RequestPolicyView): Response {
|
|
654
|
+
const response = formatErrorResponse(503, "server_error", "Service shutting down");
|
|
655
|
+
const headers = new Headers(response.headers);
|
|
656
|
+
for (const [name, value] of Object.entries(corsHeaders(req, policy))) {
|
|
657
|
+
headers.set(name, value);
|
|
658
|
+
}
|
|
659
|
+
headers.set("Retry-After", "5");
|
|
660
|
+
return new Response(response.body, { status: 503, headers });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function serverBusyResponse(req: Request, resource: string, policy: RequestPolicyView): Response {
|
|
664
|
+
return withCors(new Response(JSON.stringify({
|
|
665
|
+
error: { type: "server_error", code: "server_busy", message: `${resource} capacity reached` },
|
|
666
|
+
}), {
|
|
667
|
+
status: 503,
|
|
668
|
+
headers: { "Content-Type": "application/json", "Retry-After": "1" },
|
|
669
|
+
}), req, policy);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function runAdmittedHttpTurn(
|
|
673
|
+
req: Request,
|
|
674
|
+
policy: RequestPolicyView,
|
|
675
|
+
work: (lease: ActiveTurnLease) => Promise<Response>,
|
|
676
|
+
): Promise<Response> {
|
|
677
|
+
const lease = tryAdmitTurn();
|
|
678
|
+
if (!lease) return serverBusyResponse(req, "active turns", policy);
|
|
679
|
+
let response: Response;
|
|
680
|
+
try {
|
|
681
|
+
response = await work(lease);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
lease.release();
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
if (!lease.isTransferred()) {
|
|
687
|
+
lease.release();
|
|
688
|
+
}
|
|
689
|
+
return response;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Readiness gate: one PRIVATE controller per startServer invocation, captured
|
|
693
|
+
// by this listener's closure. Starting/failing a second server in the same
|
|
694
|
+
// process can never reset or mutate this gate. handleStart creates the gate,
|
|
695
|
+
// passes it in, and transitions it after the post-startup sync settles. When
|
|
696
|
+
// no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
|
|
697
|
+
const readinessGate = deps.readinessGate ?? createReadinessGate();
|
|
698
|
+
// Actual bound port, filled in after Bun.serve binds so /readyz reports the
|
|
699
|
+
// real ephemeral port for startServer(0). /healthz keeps its existing port
|
|
700
|
+
// field (the requested listenPort) byte-for-byte.
|
|
701
|
+
let boundPort: number | null = null;
|
|
702
|
+
|
|
703
|
+
// Native-main startup ownership creates several SQLite coordination files in
|
|
704
|
+
// CODEX_HOME. When the user has disabled the Codex integration, starting the
|
|
705
|
+
// proxy must not manufacture those Codex artifacts merely to serve other
|
|
706
|
+
// clients; no Codex request can use this lifecycle in that state.
|
|
707
|
+
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
708
|
+
// between the two sites must not widen the service-install race.
|
|
709
|
+
const nativeOwnership = inspectStartupOwnership(deps);
|
|
710
|
+
const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
|
|
711
|
+
? nativeOwnership.ownership === "owned"
|
|
712
|
+
? startNativeMainStartupLifecycle(deps.nativeMainStartup)
|
|
713
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
714
|
+
nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
|
|
715
|
+
// #2108: an `unknown` verdict means the probe could not answer, not that this host
|
|
716
|
+
// is unownable. Hand the fence a way to re-ask so a host that becomes answerable
|
|
717
|
+
// after boot reopens on its own instead of needing `ocx restart`. A `foreign`
|
|
718
|
+
// verdict ignores this by design — that one is a fact, not a question.
|
|
719
|
+
{ reprobe: () => inspectStartupOwnership(deps).ownership },
|
|
720
|
+
)
|
|
721
|
+
: {
|
|
722
|
+
homeId: null,
|
|
723
|
+
settled: Promise.resolve({ status: "ready", homeId: null }),
|
|
724
|
+
release: async () => {},
|
|
725
|
+
};
|
|
726
|
+
let server: Server<WsData>;
|
|
727
|
+
let loopbackServer: Server<WsData> | null = null;
|
|
728
|
+
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
729
|
+
try {
|
|
730
|
+
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
731
|
+
// External `ocx config set` / direct config.json edits run in other
|
|
732
|
+
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
733
|
+
// Started inside the guarded startup transaction so the catch below can
|
|
734
|
+
// release the owner-scoped lease on any listener failure.
|
|
735
|
+
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
736
|
+
const serveOptions = {
|
|
737
|
+
idleTimeout: 255,
|
|
738
|
+
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
739
|
+
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
740
|
+
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
741
|
+
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
742
|
+
// silently when a route is added below.
|
|
743
|
+
if (requestServer === loopbackServer && !loopbackRouteAllowed(new URL(req.url), req)) {
|
|
744
|
+
return withCors(
|
|
745
|
+
formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
|
|
746
|
+
req,
|
|
747
|
+
loopbackPolicy(),
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
// Auth and CORS decisions below read `policy`, not `config`. For the public listener the
|
|
751
|
+
// two are the same object, so its behaviour is unchanged; for the loopback listener the
|
|
752
|
+
// view substitutes 127.0.0.1 as the bind address, which is what routes it through the
|
|
753
|
+
// same code path a plain loopback bind has always taken — Host-header check included.
|
|
754
|
+
// Routing, provider selection and response bodies keep using `config`.
|
|
755
|
+
const policy: RequestPolicyView = requestServer === loopbackServer ? loopbackPolicy() : config;
|
|
756
|
+
const url = new URL(req.url);
|
|
757
|
+
markActivity(`${req.method} ${url.pathname}`);
|
|
758
|
+
|
|
759
|
+
// Readiness is exact-GET on the literal /readyz path. Compare the DECODED
|
|
760
|
+
// pathname so an encoded variant like /readyz%2F (which decodes to
|
|
761
|
+
// /readyz/) cannot bypass the exact-path rejection and reach the GUI
|
|
762
|
+
// fallback (serveGuiFile decodes the pathname and would serve index.html
|
|
763
|
+
// with 200). Malformed percent-sequences fall back to the raw pathname,
|
|
764
|
+
// which still cannot match the exact literal below.
|
|
765
|
+
let readyzPath: string | undefined;
|
|
766
|
+
try {
|
|
767
|
+
const decoded = decodeURIComponent(url.pathname);
|
|
768
|
+
if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
|
|
769
|
+
} catch { /* malformed encoding — not a readiness path */ }
|
|
770
|
+
|
|
771
|
+
if (req.method === "OPTIONS") {
|
|
772
|
+
// /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
|
|
773
|
+
// path) must answer the deterministic JSON 404, never the generic 204
|
|
774
|
+
// preflight response that the SPA fallback would otherwise allow.
|
|
775
|
+
if (readyzPath !== undefined) {
|
|
776
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
777
|
+
}
|
|
778
|
+
const managementPreflight = url.pathname.startsWith("/api/");
|
|
779
|
+
const allowed = managementPreflight
|
|
780
|
+
? isAllowedManagementOrigin(req, config)
|
|
781
|
+
: isAllowedRequestOrigin(req, policy);
|
|
782
|
+
if (!allowed) {
|
|
783
|
+
return new Response(null, { status: 403, headers: corsHeaders() });
|
|
784
|
+
}
|
|
785
|
+
return new Response(null, {
|
|
786
|
+
status: 204,
|
|
787
|
+
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, policy),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
|
|
792
|
+
// handshake-time only, so capture inbound headers and thread them into the pipeline.
|
|
793
|
+
if (url.pathname === "/v1/responses" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
794
|
+
if (isDraining()) {
|
|
795
|
+
return drainingResponse(req, policy);
|
|
796
|
+
}
|
|
797
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
798
|
+
if (!admission) {
|
|
799
|
+
return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
800
|
+
}
|
|
801
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
802
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
803
|
+
}
|
|
804
|
+
// WS transport gate: Codex's built-in `openai` provider hardcodes supports_websockets=true,
|
|
805
|
+
// so under Design B it always tries the WS transport first. When the feature is off, reject
|
|
806
|
+
// the upgrade with 426 — codex-rs maps a connect-time UPGRADE_REQUIRED to a clean
|
|
807
|
+
// session-scoped HTTP fallback (client.rs WebsocketStreamOutcome::FallbackToHttp) instead of
|
|
808
|
+
// surfacing broken-pipe errors from sockets a "disabled" feature would otherwise accept.
|
|
809
|
+
if (!websocketsEnabled(config)) {
|
|
810
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "Responses WebSocket transport is disabled; use HTTP"), req, policy);
|
|
811
|
+
}
|
|
812
|
+
const websocketLease = tryReserveCodexWebSocket();
|
|
813
|
+
if (!websocketLease) return serverBusyResponse(req, "Codex WebSockets", policy);
|
|
814
|
+
// Upgrade on the server that RECEIVED this request, not the captured `server`
|
|
815
|
+
// binding. They are the same object for the public listener, but the
|
|
816
|
+
// unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
|
|
817
|
+
// request to the public server's upgrade would fail or cross sockets.
|
|
818
|
+
if (requestServer.upgrade(req, {
|
|
819
|
+
data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
|
|
820
|
+
})) return undefined as unknown as Response;
|
|
821
|
+
websocketLease.release();
|
|
822
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
826
|
+
// service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
|
|
827
|
+
const healthPort = server.port ?? listenPort;
|
|
828
|
+
const response = jsonResponse({
|
|
829
|
+
status: "ok",
|
|
830
|
+
service: "opencodex",
|
|
831
|
+
version: VERSION,
|
|
832
|
+
uptime: process.uptime(),
|
|
833
|
+
pid: process.pid,
|
|
834
|
+
port: healthPort,
|
|
835
|
+
restartCapability: SYSTEM_RESTART_CAPABILITY_VERSION,
|
|
836
|
+
providerReloadCapability: LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION,
|
|
837
|
+
}, 200, req, policy);
|
|
838
|
+
const challenge = req.headers.get(LOCAL_ATTESTATION_CHALLENGE_HEADER);
|
|
839
|
+
if (challenge) {
|
|
840
|
+
const proof = createLocalAttestationProof(localAttestationSecret, challenge, process.pid, healthPort);
|
|
841
|
+
if (proof) response.headers.set(LOCAL_ATTESTATION_PROOF_HEADER, proof);
|
|
842
|
+
}
|
|
843
|
+
return response;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Readiness: like /healthz this is exact GET and unauthenticated (so a client can
|
|
847
|
+
// back off BEFORE knowing the admission token), but stricter than liveness. The
|
|
848
|
+
// body carries only sanitized identity + the fixed status enum; the sync message,
|
|
849
|
+
// warning text, catalog path, provider output, and account data are never exposed.
|
|
850
|
+
// POST or "/readyz/" must NOT match (exact pathname + GET method): answer them
|
|
851
|
+
// with a JSON 404 here so they can never be silently accepted by the GUI SPA
|
|
852
|
+
// fallback (which would serve index.html with HTTP 200 once gui/dist exists).
|
|
853
|
+
if (readyzPath !== undefined) {
|
|
854
|
+
if (readyzPath !== "/readyz" || req.method !== "GET") {
|
|
855
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
856
|
+
}
|
|
857
|
+
// A draining proxy must never advertise ready: every data-plane branch
|
|
858
|
+
// answers drainingResponse while isDraining() is set, but the one-shot
|
|
859
|
+
// readiness gate is not mutated on shutdown (it is owned by the startup
|
|
860
|
+
// sync). Report pending so `ocx ready --wait` and external supervisors
|
|
861
|
+
// keep polling instead of promoting a proxy that is draining.
|
|
862
|
+
const status = isDraining() ? "pending" : readinessGate.getStatus();
|
|
863
|
+
const body = {
|
|
864
|
+
service: "opencodex",
|
|
865
|
+
version: VERSION,
|
|
866
|
+
uptime: process.uptime(),
|
|
867
|
+
pid: process.pid,
|
|
868
|
+
port: boundPort ?? listenPort,
|
|
869
|
+
status,
|
|
870
|
+
};
|
|
871
|
+
if (status === "ready") {
|
|
872
|
+
return jsonResponse(body, 200, req, policy);
|
|
873
|
+
}
|
|
874
|
+
// Pending/failed: 503 with a conservative Retry-After so well-behaved clients
|
|
875
|
+
// (and `ocx ready --wait`) back off instead of hot-looping.
|
|
876
|
+
const resp = jsonResponse(body, 503, req, policy);
|
|
877
|
+
const headers = new Headers(resp.headers);
|
|
878
|
+
headers.set("Retry-After", "1");
|
|
879
|
+
return new Response(resp.body, { status: 503, headers });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (url.pathname.startsWith("/api/")) {
|
|
883
|
+
const localManagementAuth = {
|
|
884
|
+
attestationSecret: localAttestationSecret,
|
|
885
|
+
pid: process.pid,
|
|
886
|
+
port: boundPort ?? requestServer.port ?? listenPort,
|
|
887
|
+
};
|
|
888
|
+
const apiAuthError = requireManagementAuth(req, managementAuth, config, localManagementAuth);
|
|
889
|
+
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
|
|
890
|
+
// Which credential passed the gate, resolved from the same session table the
|
|
891
|
+
// gate used. Consent-bearing routes need this: request headers are forgeable
|
|
892
|
+
// by anything holding the admin token, the credential is not.
|
|
893
|
+
const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
|
|
894
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, deps.managementApi, principal);
|
|
895
|
+
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
896
|
+
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (url.pathname === "/v1/models" && req.method === "GET") {
|
|
900
|
+
// Model discovery never forwards Authorization upstream, so the broader admission
|
|
901
|
+
// set (Authorization / x-api-key / x-opencodex-api-key) is safe here and required by
|
|
902
|
+
// remote OpenAI-style bearer clients and Claude gateway discovery (anthropic-version).
|
|
903
|
+
const admission = resolveApiAuth(req, policy);
|
|
904
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
905
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
906
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
907
|
+
}
|
|
908
|
+
let goModels;
|
|
909
|
+
let modelEntitlements;
|
|
910
|
+
try {
|
|
911
|
+
[goModels, modelEntitlements] = await Promise.all([
|
|
912
|
+
fetchAllModels(config),
|
|
913
|
+
resolveCodexModelEntitlements(config),
|
|
914
|
+
]);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (error instanceof CatalogGatherBusyError) {
|
|
917
|
+
return withCors(new Response(JSON.stringify({ error: { type: "server_error", code: "catalog_busy", message: error.message } }), {
|
|
918
|
+
status: 503,
|
|
919
|
+
headers: { "content-type": "application/json", "Retry-After": "1" },
|
|
920
|
+
}), req, policy);
|
|
921
|
+
}
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
925
|
+
const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
|
|
926
|
+
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
927
|
+
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
928
|
+
const bareEligibleAccountIds = providerCodexAccountMode(
|
|
929
|
+
OPENAI_CODEX_PROVIDER_ID,
|
|
930
|
+
config.providers[OPENAI_CODEX_PROVIDER_ID],
|
|
931
|
+
) === "direct" ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined;
|
|
932
|
+
const availableBareGatedNativeSlugs = availableAccountGatedNativeModels(
|
|
933
|
+
modelEntitlements,
|
|
934
|
+
bareEligibleAccountIds,
|
|
935
|
+
);
|
|
936
|
+
const availableAccountGatedNativeSlugs = availableAccountGatedNativeModels(modelEntitlements);
|
|
937
|
+
const availableBareNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
938
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
939
|
+
));
|
|
940
|
+
const availableAccountNativeSlugs = NATIVE_OPENAI_MODELS.filter(slug => (
|
|
941
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableAccountGatedNativeSlugs.has(slug)
|
|
942
|
+
));
|
|
943
|
+
const nativeSlugs = includeNativeOpenAi
|
|
944
|
+
? nativeOpenAiSlugs().filter(slug => (
|
|
945
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
946
|
+
))
|
|
947
|
+
: [];
|
|
948
|
+
const disabledNatives = disabledNativeSlugs(config);
|
|
949
|
+
const disabledModels = new Set(config.disabledModels ?? []);
|
|
950
|
+
const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
|
|
951
|
+
const suppressedBareNativeSlugs = new Set([
|
|
952
|
+
...desktopAllowlistSuppressedNativeSlugs(config),
|
|
953
|
+
...[...ACCOUNT_GATED_NATIVE_OPENAI_MODELS].filter(slug => !availableBareGatedNativeSlugs.has(slug)),
|
|
954
|
+
]);
|
|
955
|
+
const accountSelectors = includeAccountBoundNativeOpenAi
|
|
956
|
+
? visibleCodexAccountSelectors(config)
|
|
957
|
+
: [];
|
|
958
|
+
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
959
|
+
const accountNativeSlugsBySelector = includeAccountBoundNativeOpenAi
|
|
960
|
+
? new Map([...accountBoundNativeOpenAiSlugsBySelector(config)].map(([selector, slugs]) => {
|
|
961
|
+
const target = accountTargets.get(selector);
|
|
962
|
+
const accountId = target && isMainCodexAccountTarget(target) ? MAIN_CODEX_ACCOUNT_ID : target;
|
|
963
|
+
const entitled = accountId ? modelEntitlements.modelsByAccount.get(accountId) : undefined;
|
|
964
|
+
const confirmed = accountId ? modelEntitlements.confirmedAccountIds.has(accountId) : false;
|
|
965
|
+
return [selector, slugs.filter(slug => (
|
|
966
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || (confirmed && entitled?.has(slug) === true)
|
|
967
|
+
))] as const;
|
|
968
|
+
}))
|
|
969
|
+
: new Map<string, readonly string[]>();
|
|
970
|
+
const accountNativeSlugs = [...new Set(
|
|
971
|
+
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
972
|
+
)];
|
|
973
|
+
const desktopNativeSlugs = desktopVisibleNativeSlugs(config).filter(slug => (
|
|
974
|
+
!ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(slug) || availableBareGatedNativeSlugs.has(slug)
|
|
975
|
+
));
|
|
976
|
+
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
977
|
+
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
978
|
+
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
979
|
+
// Anthropic-style headers; 003 G1-G8 + devlog 131). Entries use the official
|
|
980
|
+
// ModelInfo shape incl. capabilities (effort ladder / thinking) — Desktop 3P can
|
|
981
|
+
// only learn capabilities through discovery, and Claude Code 2.1.207 strips the
|
|
982
|
+
// extra fields (backward-safe). Ids are the claude-opus-4-8-{code} Desktop
|
|
983
|
+
// aliases; legacy claude-ocx-* ids keep decoding via resolveAlias. Detection:
|
|
984
|
+
// anthropic-version header (Claude Code sends it) or explicit ?flavor=anthropic.
|
|
985
|
+
// Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
|
|
986
|
+
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
987
|
+
|| url.searchParams.get("flavor") === "anthropic";
|
|
988
|
+
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
989
|
+
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
990
|
+
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
991
|
+
buildDesktop3pRegistry(
|
|
992
|
+
desktopNativeSlugs,
|
|
993
|
+
goOrdered.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
994
|
+
config.claudeCode?.desktopProfile,
|
|
995
|
+
);
|
|
996
|
+
const { buildAnthropicModelInfos } = await import("../claude/model-info");
|
|
997
|
+
const { resolveAutoContext } = await import("../claude/context-windows");
|
|
998
|
+
const { activeDesktop3pAlias } = await import("../claude/desktop-3p");
|
|
999
|
+
// Per-surface id family (devlog 050): explicit ?ids= wins; otherwise the
|
|
1000
|
+
// Claude Code CLI discovery UA (`claude-code/<version>`, binary n_()) gets
|
|
1001
|
+
// readable claude-ocx ids and every other client (Desktop 3P) keeps the
|
|
1002
|
+
// hashed family its config was written with. Unknown UA -> hashed (safe).
|
|
1003
|
+
const idsParam = url.searchParams.get("ids");
|
|
1004
|
+
const idStyle = idsParam === "cli"
|
|
1005
|
+
? "readable" as const
|
|
1006
|
+
: idsParam === "desktop"
|
|
1007
|
+
? "desktop3p" as const
|
|
1008
|
+
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
1009
|
+
const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config));
|
|
1010
|
+
return jsonResponse({ data }, 200, req, policy);
|
|
1011
|
+
}
|
|
1012
|
+
if (url.searchParams.has("client_version")) {
|
|
1013
|
+
// Codex client → Codex catalog shape: native gpt + namespaced routed models,
|
|
1014
|
+
// cloned from a native template so required fields (base_instructions, etc.) are present.
|
|
1015
|
+
// Pass the subagent picks so featured models lead by priority (matches the on-disk file).
|
|
1016
|
+
// Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
|
|
1017
|
+
// on-disk sync; codex-rs keeps them out of the picker itself).
|
|
1018
|
+
const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1019
|
+
// Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
|
|
1020
|
+
// newly re-enabled native reappear under each selector before the next sync, while the
|
|
1021
|
+
// no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
|
|
1022
|
+
const catalogNativeSlugs = accountSelectors.length > 0
|
|
1023
|
+
? [...new Set([
|
|
1024
|
+
...availableAccountNativeSlugs,
|
|
1025
|
+
...accountNativeSlugs,
|
|
1026
|
+
])]
|
|
1027
|
+
: nativeSlugs;
|
|
1028
|
+
const entries = buildCatalogEntries(
|
|
1029
|
+
loadCatalogTemplate(),
|
|
1030
|
+
catalogNativeSlugs,
|
|
1031
|
+
goOrdered,
|
|
1032
|
+
config.subagentModels,
|
|
1033
|
+
websocketsEnabled(config),
|
|
1034
|
+
maMode as "v1" | "default" | "v2",
|
|
1035
|
+
exactComboCatalogSlugs(config),
|
|
1036
|
+
accountSelectors,
|
|
1037
|
+
suppressedBareNativeSlugs,
|
|
1038
|
+
new Set(),
|
|
1039
|
+
nativeContextLimits(config),
|
|
1040
|
+
accountNativeSlugs,
|
|
1041
|
+
accountNativeSlugsBySelector,
|
|
1042
|
+
config.keepNativeChatGptOnV1 === true,
|
|
1043
|
+
);
|
|
1044
|
+
return jsonResponse({
|
|
1045
|
+
models: applyNativeVisibility(
|
|
1046
|
+
entries,
|
|
1047
|
+
disabledModels,
|
|
1048
|
+
accountSelectors.length > 0,
|
|
1049
|
+
new Set(accountNativeSlugs),
|
|
1050
|
+
),
|
|
1051
|
+
}, 200, req, policy);
|
|
1052
|
+
}
|
|
1053
|
+
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
1054
|
+
// (pure availability list — disabled natives are omitted entirely).
|
|
1055
|
+
// Grok Build discovers models through this endpoint too, and its model picker only
|
|
1056
|
+
// enables /effort for entries that advertise the reasoning ladder in the Grok model
|
|
1057
|
+
// catalog shape (supports_reasoning_effort + reasoning_efforts[]). The Codex catalog
|
|
1058
|
+
// branch above already carries the same ladders, so mirror them here — native rows
|
|
1059
|
+
// from the upstream snapshot, routed rows from the configured provider tiers. The
|
|
1060
|
+
// default uses the same canonical fallback as the Codex catalog resolver
|
|
1061
|
+
// (configured default, then medium, then high, then the first tier). Extra fields
|
|
1062
|
+
// are ignored by plain OpenAI clients.
|
|
1063
|
+
const grokEffortOption = (value: string, isDefault: boolean) => ({
|
|
1064
|
+
value,
|
|
1065
|
+
label: `${value[0].toUpperCase()}${value.slice(1)} Effort`,
|
|
1066
|
+
...(isDefault ? { default: true } : {}),
|
|
1067
|
+
});
|
|
1068
|
+
const grokEffortFields = (efforts: string[], configuredDefault?: string) => {
|
|
1069
|
+
if (efforts.length === 0) return {};
|
|
1070
|
+
const defaultEffort = configuredDefault && efforts.includes(configuredDefault)
|
|
1071
|
+
? configuredDefault
|
|
1072
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
|
|
1073
|
+
return {
|
|
1074
|
+
supports_reasoning_effort: true,
|
|
1075
|
+
reasoning_effort: defaultEffort,
|
|
1076
|
+
reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
|
|
1077
|
+
};
|
|
1078
|
+
};
|
|
1079
|
+
const nativeModelRow = (id: string, metadataId = id) => ({
|
|
1080
|
+
id,
|
|
1081
|
+
object: "model",
|
|
1082
|
+
created: 0,
|
|
1083
|
+
owned_by: "openai",
|
|
1084
|
+
...grokEffortFields(
|
|
1085
|
+
nativeReasoningEfforts(metadataId),
|
|
1086
|
+
nativeDefaultReasoningEffort(metadataId),
|
|
1087
|
+
),
|
|
1088
|
+
});
|
|
1089
|
+
// Selector-active discovery follows the same complete supported set as the Codex catalog
|
|
1090
|
+
// for both bare and qualified rows. Without selectors, the live catalog continues to own
|
|
1091
|
+
// bare availability.
|
|
1092
|
+
const selectorNativeSlugs = accountSelectors.length > 0
|
|
1093
|
+
? availableBareNativeSlugs.filter(slug => !disabledNatives.has(slug))
|
|
1094
|
+
: [];
|
|
1095
|
+
const bareSelectorNativeSlugs = accountSelectors.length > 0
|
|
1096
|
+
? selectorNativeSlugs
|
|
1097
|
+
: [];
|
|
1098
|
+
const visibleNatives = includeNativeOpenAi
|
|
1099
|
+
? accountSelectors.length > 0
|
|
1100
|
+
? bareSelectorNativeSlugs.filter(slug => !shadowedNativeSlugs.has(slug))
|
|
1101
|
+
: visibleNativeSlugs(config)
|
|
1102
|
+
: [];
|
|
1103
|
+
const visibleAccountNatives = accountSelectors.flatMap(selector =>
|
|
1104
|
+
(accountNativeSlugsBySelector.get(selector) ?? []).filter(metadataId => !disabledNatives.has(metadataId)).flatMap(metadataId => {
|
|
1105
|
+
const id = `${selector}/${metadataId}`;
|
|
1106
|
+
return disabledModels.has(id) ? [] : [{ id, metadataId }];
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
const data = [
|
|
1110
|
+
...visibleNatives.map(id => nativeModelRow(id)),
|
|
1111
|
+
...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
|
|
1112
|
+
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
|
|
1113
|
+
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
1114
|
+
object: "model",
|
|
1115
|
+
created: 0,
|
|
1116
|
+
owned_by: m.owned_by ?? m.provider,
|
|
1117
|
+
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
1118
|
+
})),
|
|
1119
|
+
];
|
|
1120
|
+
return jsonResponse({ object: "list", data }, 200, req, policy);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Remote compaction v1 (codex-rs with Feature::RemoteCompactionV2 off — the default).
|
|
1124
|
+
// Must be matched BEFORE the /v1/responses POST branch never sees it (distinct path) and
|
|
1125
|
+
// before the /v1/* 404 guard below.
|
|
1126
|
+
if (url.pathname === "/v1/responses/compact" && req.method === "POST") {
|
|
1127
|
+
if (isDraining()) {
|
|
1128
|
+
return drainingResponse(req, policy);
|
|
1129
|
+
}
|
|
1130
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1131
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1132
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1133
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1134
|
+
}
|
|
1135
|
+
const start = Date.now();
|
|
1136
|
+
const requestId = nextRequestLogId(start);
|
|
1137
|
+
const logCtx: RequestLogContext = {
|
|
1138
|
+
model: "unknown",
|
|
1139
|
+
provider: "unknown",
|
|
1140
|
+
...admissionFields(admission),
|
|
1141
|
+
inboundProtocol: "responses",
|
|
1142
|
+
};
|
|
1143
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1144
|
+
let response: Response;
|
|
1145
|
+
try {
|
|
1146
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
|
|
1147
|
+
} catch {
|
|
1148
|
+
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
1149
|
+
}
|
|
1150
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
1151
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1152
|
+
return withCors(response, req, policy);
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (
|
|
1157
|
+
req.method === "POST"
|
|
1158
|
+
&& (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits")
|
|
1159
|
+
) {
|
|
1160
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
1161
|
+
if (isDraining()) {
|
|
1162
|
+
return drainingResponse(req, policy);
|
|
1163
|
+
}
|
|
1164
|
+
const admission = resolveApiAuth(req, policy);
|
|
1165
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1166
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1167
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1168
|
+
}
|
|
1169
|
+
const start = Date.now();
|
|
1170
|
+
const requestId = nextRequestLogId(start);
|
|
1171
|
+
const logCtx: RequestLogContext = {
|
|
1172
|
+
model: "image_gen",
|
|
1173
|
+
provider: "unknown",
|
|
1174
|
+
...admissionFields(admission),
|
|
1175
|
+
};
|
|
1176
|
+
const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
|
|
1177
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1178
|
+
const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
|
|
1179
|
+
addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1180
|
+
return withCors(response, req, policy);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
|
|
1185
|
+
const admission = resolveApiAuth(req, policy);
|
|
1186
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1187
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1188
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1189
|
+
}
|
|
1190
|
+
const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
|
|
1191
|
+
const { resolveArtifactPath } = await import("../images/artifacts");
|
|
1192
|
+
const artifactPath = resolveArtifactPath(id);
|
|
1193
|
+
if (!artifactPath) {
|
|
1194
|
+
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, policy);
|
|
1195
|
+
}
|
|
1196
|
+
const file = Bun.file(artifactPath);
|
|
1197
|
+
const ext = artifactPath.split(".").pop()?.toLowerCase();
|
|
1198
|
+
const contentType =
|
|
1199
|
+
ext === "png" ? "image/png"
|
|
1200
|
+
: ext === "jpg" || ext === "jpeg" ? "image/jpeg"
|
|
1201
|
+
: ext === "webp" ? "image/webp"
|
|
1202
|
+
: ext === "gif" ? "image/gif"
|
|
1203
|
+
: "application/octet-stream";
|
|
1204
|
+
return withCors(new Response(file, {
|
|
1205
|
+
status: 200,
|
|
1206
|
+
headers: {
|
|
1207
|
+
"content-type": contentType,
|
|
1208
|
+
"cache-control": "private, max-age=3600",
|
|
1209
|
+
"x-content-type-options": "nosniff",
|
|
1210
|
+
},
|
|
1211
|
+
}), req, policy);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
1215
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
1216
|
+
if (isDraining()) {
|
|
1217
|
+
return drainingResponse(req, policy);
|
|
1218
|
+
}
|
|
1219
|
+
const admission = resolveApiAuth(req, policy);
|
|
1220
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1221
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1222
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1223
|
+
}
|
|
1224
|
+
const start = Date.now();
|
|
1225
|
+
const requestId = nextRequestLogId(start);
|
|
1226
|
+
const logCtx: RequestLogContext = {
|
|
1227
|
+
model: "web_search",
|
|
1228
|
+
provider: "unknown",
|
|
1229
|
+
...admissionFields(admission),
|
|
1230
|
+
};
|
|
1231
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1232
|
+
const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
|
|
1233
|
+
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
1234
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1235
|
+
return withCors(response, req, policy);
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
1240
|
+
if (isDraining()) {
|
|
1241
|
+
return drainingResponse(req, policy);
|
|
1242
|
+
}
|
|
1243
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1244
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1245
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1246
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1247
|
+
}
|
|
1248
|
+
const start = Date.now();
|
|
1249
|
+
const requestId = nextRequestLogId(start);
|
|
1250
|
+
const logCtx: RequestLogContext = {
|
|
1251
|
+
model: "unknown",
|
|
1252
|
+
provider: "unknown",
|
|
1253
|
+
...admissionFields(admission),
|
|
1254
|
+
inboundProtocol: "responses",
|
|
1255
|
+
};
|
|
1256
|
+
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
1257
|
+
let logged = false;
|
|
1258
|
+
const finalizeNativePassthroughLog = (
|
|
1259
|
+
status: number,
|
|
1260
|
+
meta: { terminalStatus?: ResponsesTerminalStatus; closeReason: "terminal" | "client_cancel" },
|
|
1261
|
+
) => {
|
|
1262
|
+
if (logged) return;
|
|
1263
|
+
logged = true;
|
|
1264
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1265
|
+
};
|
|
1266
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1267
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
1268
|
+
turnAdmissionLease,
|
|
1269
|
+
admission,
|
|
1270
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1271
|
+
abortSignal: req.signal,
|
|
1272
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1273
|
+
onNativePassthroughTerminal: status => {
|
|
1274
|
+
finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
1275
|
+
terminalStatus: status,
|
|
1276
|
+
closeReason: "terminal",
|
|
1277
|
+
});
|
|
1278
|
+
},
|
|
1279
|
+
onNativePassthroughCancel: () => {
|
|
1280
|
+
finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
return withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Anthropic Messages inbound (Claude Code). count_tokens FIRST (longer path).
|
|
1288
|
+
// Claude Code posts `/v1/messages?beta=true` — pathname match ignores the query (003 G9).
|
|
1289
|
+
if (url.pathname === "/v1/messages/count_tokens" && req.method === "POST") {
|
|
1290
|
+
if (isDraining()) {
|
|
1291
|
+
return drainingResponse(req, policy);
|
|
1292
|
+
}
|
|
1293
|
+
const admission = resolveApiAuth(req, policy);
|
|
1294
|
+
if (!admission) {
|
|
1295
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
1296
|
+
}
|
|
1297
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1298
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
1299
|
+
}
|
|
1300
|
+
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1301
|
+
await handleClaudeCountTokens(req, config, policy),
|
|
1302
|
+
req,
|
|
1303
|
+
policy,
|
|
1304
|
+
));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (url.pathname === "/v1/messages" && req.method === "POST") {
|
|
1308
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
1309
|
+
if (isDraining()) {
|
|
1310
|
+
return drainingResponse(req, policy);
|
|
1311
|
+
}
|
|
1312
|
+
const admission = resolveApiAuth(req, policy);
|
|
1313
|
+
if (!admission) {
|
|
1314
|
+
return withCors(anthropicErrorResponse(401, "opencodex API key required", "authentication_error"), req, policy);
|
|
1315
|
+
}
|
|
1316
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1317
|
+
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
1318
|
+
}
|
|
1319
|
+
const start = Date.now();
|
|
1320
|
+
const requestId = nextRequestLogId(start);
|
|
1321
|
+
const logCtx: RequestLogContext = {
|
|
1322
|
+
model: "unknown",
|
|
1323
|
+
provider: "unknown",
|
|
1324
|
+
...admissionFields(admission),
|
|
1325
|
+
inboundProtocol: "messages",
|
|
1326
|
+
};
|
|
1327
|
+
// Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
|
|
1328
|
+
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1329
|
+
// translated Anthropic stream here.
|
|
1330
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1331
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
|
|
1332
|
+
req,
|
|
1333
|
+
policy,
|
|
1334
|
+
));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
// OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
|
|
1339
|
+
if (url.pathname === "/v1/chat/completions" && req.method === "POST") {
|
|
1340
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
1341
|
+
if (isDraining()) {
|
|
1342
|
+
return drainingResponse(req, policy);
|
|
1343
|
+
}
|
|
1344
|
+
const admission = resolveResponsesApiAuth(req, policy);
|
|
1345
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1346
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1347
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1348
|
+
}
|
|
1349
|
+
const start = Date.now();
|
|
1350
|
+
const requestId = nextRequestLogId(start);
|
|
1351
|
+
const logCtx: RequestLogContext = {
|
|
1352
|
+
model: "unknown",
|
|
1353
|
+
provider: "unknown",
|
|
1354
|
+
...admissionFields(admission),
|
|
1355
|
+
inboundProtocol: "chat",
|
|
1356
|
+
};
|
|
1357
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1358
|
+
await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
|
|
1359
|
+
req,
|
|
1360
|
+
config,
|
|
1361
|
+
));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// ChatGPT / Codex App voice (GPT‑Live / Frameless Bidi) + OpenAI Realtime call-create.
|
|
1365
|
+
// Clients hit either /v1/live (Frameless App) or /v1/realtime/calls (codex RealtimeCallClient /
|
|
1366
|
+
// public Realtime API). Sideband WS joins are handled just below.
|
|
1367
|
+
if (
|
|
1368
|
+
req.method === "POST"
|
|
1369
|
+
&& (url.pathname === "/v1/live" || url.pathname === "/v1/realtime/calls")
|
|
1370
|
+
) {
|
|
1371
|
+
disableResponsesRequestTimeout(req, requestServer);
|
|
1372
|
+
if (isDraining()) {
|
|
1373
|
+
return drainingResponse(req, policy);
|
|
1374
|
+
}
|
|
1375
|
+
const admission = resolveApiAuth(req, policy);
|
|
1376
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1377
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1378
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1379
|
+
}
|
|
1380
|
+
const start = Date.now();
|
|
1381
|
+
const requestId = nextRequestLogId(start);
|
|
1382
|
+
const logCtx: RequestLogContext = {
|
|
1383
|
+
model: "gpt-live",
|
|
1384
|
+
provider: "unknown",
|
|
1385
|
+
...admissionFields(admission),
|
|
1386
|
+
};
|
|
1387
|
+
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1388
|
+
const response = await handleLive(req, config, logCtx, turnAdmissionLease);
|
|
1389
|
+
addFinalRequestLog(
|
|
1390
|
+
requestId,
|
|
1391
|
+
start,
|
|
1392
|
+
logCtx,
|
|
1393
|
+
response.status,
|
|
1394
|
+
response.status === 499 ? { closeReason: "client_cancel" } : undefined,
|
|
1395
|
+
);
|
|
1396
|
+
return withCors(response, req, policy);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Voice / Realtime WebSocket relay. Sideband joins: Frameless /v1/live/{callId};
|
|
1401
|
+
// Realtime v1 /v1/realtime?call_id= (or /v1/realtime/calls/{callId}). Standalone
|
|
1402
|
+
// sessions (codex-rs thread/realtime/start, WebSocket transport — the desktop voice
|
|
1403
|
+
// path): /v1/realtime?intent=quicksilver&model= and /v1/live?model=.
|
|
1404
|
+
// Transparent bidirectional relay.
|
|
1405
|
+
const liveSidebandTarget = req.headers.get("upgrade")?.toLowerCase() === "websocket"
|
|
1406
|
+
? parseLiveSidebandTarget(url.pathname, url.searchParams, url.search.replace(/^\?/, ""))
|
|
1407
|
+
: null;
|
|
1408
|
+
if (liveSidebandTarget) {
|
|
1409
|
+
if (isDraining()) {
|
|
1410
|
+
return drainingResponse(req, policy);
|
|
1411
|
+
}
|
|
1412
|
+
const admission = resolveApiAuth(req, policy);
|
|
1413
|
+
if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
|
|
1414
|
+
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1415
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "WebSocket upgrade blocked: non-local Origin"), req, policy);
|
|
1416
|
+
}
|
|
1417
|
+
const start = Date.now();
|
|
1418
|
+
const requestId = nextRequestLogId(start);
|
|
1419
|
+
const logCtx: RequestLogContext = {
|
|
1420
|
+
model: "gpt-live",
|
|
1421
|
+
provider: "unknown",
|
|
1422
|
+
...admissionFields(admission),
|
|
1423
|
+
};
|
|
1424
|
+
const turnAdmissionLease = tryAdmitTurn();
|
|
1425
|
+
if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
|
|
1426
|
+
let resolved;
|
|
1427
|
+
try {
|
|
1428
|
+
resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease);
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
turnAdmissionLease.release();
|
|
1431
|
+
throw error;
|
|
1432
|
+
}
|
|
1433
|
+
if (resolved instanceof Response) {
|
|
1434
|
+
turnAdmissionLease.release();
|
|
1435
|
+
addFinalRequestLog(requestId, start, logCtx, resolved.status);
|
|
1436
|
+
return withCors(resolved, req, policy);
|
|
1437
|
+
}
|
|
1438
|
+
addFinalRequestLog(requestId, start, logCtx, 101);
|
|
1439
|
+
if (requestServer.upgrade(req, {
|
|
1440
|
+
data: {
|
|
1441
|
+
kind: "live-sideband",
|
|
1442
|
+
liveUpstreamUrl: resolved.upstreamWsUrl,
|
|
1443
|
+
liveUpstreamHeaders: resolved.headers,
|
|
1444
|
+
livePending: [],
|
|
1445
|
+
livePendingBytes: 0,
|
|
1446
|
+
liveOpened: false,
|
|
1447
|
+
liveTurnAdmissionLease: turnAdmissionLease,
|
|
1448
|
+
} satisfies WsData,
|
|
1449
|
+
})) return undefined as unknown as Response;
|
|
1450
|
+
turnAdmissionLease.release();
|
|
1451
|
+
return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// Data-plane guard: unknown /v1/* paths must fail with JSON 404, never fall through to the
|
|
1455
|
+
// GUI static handler (extensionless paths would get index.html with HTTP 200 and codex-rs
|
|
1456
|
+
// endpoint clients — memories/*, realtime/* — would surface confusing
|
|
1457
|
+
// serde decode errors instead of a clean not-found).
|
|
1458
|
+
if (url.pathname.startsWith("/v1/")) {
|
|
1459
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, policy);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
|
|
1463
|
+
? issueGuiSession(req, config, managementAuth)
|
|
1464
|
+
: null;
|
|
1465
|
+
// Dedicated bootstrap path: answer without requiring a packaged GUI build, so the
|
|
1466
|
+
// Vite dev server can mint an origin-bound loopback session on a fresh checkout.
|
|
1467
|
+
if (url.pathname === "/opencodex-session" && guiSessionCandidate) {
|
|
1468
|
+
return serveSessionBootstrap(guiSessionCandidate);
|
|
1469
|
+
}
|
|
1470
|
+
const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
|
|
1471
|
+
if (guiFile) return guiFile;
|
|
1472
|
+
if (url.pathname === "/" && req.method === "GET") {
|
|
1473
|
+
return jsonResponse(rootFallbackPayload());
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
1477
|
+
},
|
|
1478
|
+
websocket: {
|
|
1479
|
+
maxPayloadLength: MAX_WS_FRAME_BYTES,
|
|
1480
|
+
idleTimeout: WEBSOCKET_IDLE_TIMEOUT_SECONDS,
|
|
1481
|
+
// Responses WebSocket data plane (phase 120.2). Re-frames the same SSE pipeline onto the
|
|
1482
|
+
// socket: parse response.create → run handleResponses unchanged → pump its SSE body as WS
|
|
1483
|
+
// Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
|
|
1484
|
+
// Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
|
|
1485
|
+
open(ws: ServerWebSocket<WsData>) {
|
|
1486
|
+
if (ws.data.kind === "live-sideband") {
|
|
1487
|
+
if (!ws.data.liveTurnAdmissionLease) {
|
|
1488
|
+
closeLiveSideband(ws, 1013, "server busy");
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (!ws.data.admissionLease) {
|
|
1495
|
+
ws.close(1013, "server busy");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
ws.data.admissionLease.bind(ws);
|
|
1499
|
+
registerCodexWebSocket(ws);
|
|
1500
|
+
},
|
|
1501
|
+
message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
|
|
1502
|
+
if (ws.data.kind === "live-sideband") {
|
|
1503
|
+
if (ws.data.liveClosing) return;
|
|
1504
|
+
const rawBytes = webSocketFrameBytes(raw);
|
|
1505
|
+
if (exceedsLiveSidebandFrameByteLimit(rawBytes)) {
|
|
1506
|
+
closeLiveSideband(ws, 1009, "message too large");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
logLiveSidebandFrame("c2u", raw);
|
|
1510
|
+
const upstream = ws.data.liveUpstream;
|
|
1511
|
+
if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
|
|
1512
|
+
const enqueueResult = enqueueLiveSidebandPendingFrame(ws.data, raw, rawBytes);
|
|
1513
|
+
if (enqueueResult === "too-many-frames") {
|
|
1514
|
+
closeLiveSideband(ws, 1009, "too many pending frames");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
if (enqueueResult === "too-many-bytes") {
|
|
1518
|
+
closeLiveSideband(ws, 1009, "too many pending bytes");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (upstream.readyState !== WebSocket.OPEN) {
|
|
1524
|
+
closeLiveSideband(ws, 1011, "upstream not open");
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
try {
|
|
1528
|
+
sendUpstreamFrame(upstream, raw);
|
|
1529
|
+
} catch {
|
|
1530
|
+
closeLiveSideband(ws, 1011, "upstream send failed");
|
|
1531
|
+
}
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const rawBytes = typeof raw === "string" ? Buffer.byteLength(raw) : raw.byteLength;
|
|
1535
|
+
if (rawBytes > MAX_WS_FRAME_BYTES) {
|
|
1536
|
+
sendJsonFrame(ws, buildWsErrorFrame(413, {
|
|
1537
|
+
type: "invalid_request_error",
|
|
1538
|
+
message: "WebSocket response.create frame is too large",
|
|
1539
|
+
}));
|
|
1540
|
+
ws.close(1009, "message too large");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
let frame: Record<string, unknown>;
|
|
1544
|
+
try {
|
|
1545
|
+
frame = JSON.parse(typeof raw === "string" ? raw : raw.toString()) as Record<string, unknown>;
|
|
1546
|
+
} catch {
|
|
1547
|
+
return; // text-only contract; ignore unparseable frames
|
|
1548
|
+
}
|
|
1549
|
+
if (frame.type === "response.processed") return; // ack — no-op
|
|
1550
|
+
if (frame.type !== "response.create") return;
|
|
1551
|
+
markActivity("ws response.create");
|
|
1552
|
+
|
|
1553
|
+
ws.data.cancel?.();
|
|
1554
|
+
const turnId = (ws.data.turnId ?? 0) + 1;
|
|
1555
|
+
ws.data.turnId = turnId;
|
|
1556
|
+
const isCurrent = () => ws.data.turnId === turnId;
|
|
1557
|
+
const turnAbort = new AbortController();
|
|
1558
|
+
const cancelTurn = () => {
|
|
1559
|
+
turnAbort.abort("websocket turn superseded or closed");
|
|
1560
|
+
};
|
|
1561
|
+
ws.data.cancel = cancelTurn;
|
|
1562
|
+
// A socket may carry several response.create frames. Clear the previous
|
|
1563
|
+
// account before resolving this frame so a failed Multi resolution cannot
|
|
1564
|
+
// leave stale invalidation ownership behind.
|
|
1565
|
+
updateCodexWebSocketAuthContext(ws, undefined);
|
|
1566
|
+
|
|
1567
|
+
if (frame.generate === false) {
|
|
1568
|
+
for (const payload of buildWarmupCompletionFrames(frame)) {
|
|
1569
|
+
if (!isCurrent()) return;
|
|
1570
|
+
sendTextFrame(ws, payload);
|
|
1571
|
+
}
|
|
1572
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const turnAdmissionLease = tryAdmitTurn();
|
|
1577
|
+
if (!turnAdmissionLease) {
|
|
1578
|
+
sendJsonFrame(ws, buildWsErrorFrame(503, {
|
|
1579
|
+
type: "server_error",
|
|
1580
|
+
code: "server_busy",
|
|
1581
|
+
message: "active turns capacity reached",
|
|
1582
|
+
retryable: true,
|
|
1583
|
+
}, new Headers({ "Retry-After": "1" })));
|
|
1584
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const payload: Record<string, unknown> = { ...frame };
|
|
1589
|
+
delete payload.type;
|
|
1590
|
+
turnAdmissionLease.bindAbortController(turnAbort);
|
|
1591
|
+
void (async () => {
|
|
1592
|
+
const start = Date.now();
|
|
1593
|
+
const requestId = nextRequestLogId(start);
|
|
1594
|
+
// Resolved once at the handshake — a frame has no request headers left
|
|
1595
|
+
// to re-resolve from. Optional on WsData like every other member, so
|
|
1596
|
+
// narrow rather than assume: an unattributed frame is preferable to a
|
|
1597
|
+
// fabricated attribution.
|
|
1598
|
+
const wsAdmission = ws.data.admission;
|
|
1599
|
+
const logCtx: RequestLogContext = {
|
|
1600
|
+
model: "unknown",
|
|
1601
|
+
provider: "unknown",
|
|
1602
|
+
...(wsAdmission ? admissionFields(wsAdmission) : {}),
|
|
1603
|
+
inboundProtocol: "responses",
|
|
1604
|
+
};
|
|
1605
|
+
let logged = false;
|
|
1606
|
+
const finalizeLog = (
|
|
1607
|
+
status: number,
|
|
1608
|
+
meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
1609
|
+
) => {
|
|
1610
|
+
if (logged) return;
|
|
1611
|
+
logged = true;
|
|
1612
|
+
addFinalRequestLog(requestId, start, logCtx, status, meta);
|
|
1613
|
+
};
|
|
1614
|
+
const baseHeaders = ws.data.headers ?? new Headers();
|
|
1615
|
+
const fwd = new Headers({ "content-type": "application/json" });
|
|
1616
|
+
baseHeaders.forEach((value, key) => fwd.set(key, value));
|
|
1617
|
+
const req = new Request("http://localhost/v1/responses", {
|
|
1618
|
+
method: "POST",
|
|
1619
|
+
headers: fwd,
|
|
1620
|
+
body: JSON.stringify({ ...payload, stream: true }),
|
|
1621
|
+
});
|
|
1622
|
+
try {
|
|
1623
|
+
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
1624
|
+
const response = await handleResponses(req, config, logCtx, {
|
|
1625
|
+
...(wsAdmission ? { admission: wsAdmission } : {}),
|
|
1626
|
+
forceEmptyResponseId: true,
|
|
1627
|
+
inboundTransport: "websocket",
|
|
1628
|
+
abortSignal: turnAbort.signal,
|
|
1629
|
+
turnAdmissionLease,
|
|
1630
|
+
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1631
|
+
onCodexAuthContextResolved: context => updateCodexWebSocketAuthContext(ws, context),
|
|
1632
|
+
recordTerminalOutcomes: false,
|
|
1633
|
+
setTerminalOutcomeRecorder: recorder => {
|
|
1634
|
+
terminalRecorder = recorder;
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
1638
|
+
onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
1639
|
+
onTerminal: status => {
|
|
1640
|
+
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
1641
|
+
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
1642
|
+
terminalStatus: status,
|
|
1643
|
+
closeReason: "terminal",
|
|
1644
|
+
});
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1647
|
+
if (!logged) finalizeLog(turnAbort.signal.aborted ? 499 : response.status);
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
if (!isCurrent()) return;
|
|
1650
|
+
try {
|
|
1651
|
+
if (err instanceof CodexAccountCooldownError) {
|
|
1652
|
+
finalizeLog(429);
|
|
1653
|
+
// Codex Desktop rides this WS transport, so it must carry the same
|
|
1654
|
+
// actionable text as HTTP; a frame has no headers, hence message-only.
|
|
1655
|
+
const accountSelector = typeof payload.model === "string"
|
|
1656
|
+
? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
|
|
1657
|
+
: undefined;
|
|
1658
|
+
sendJsonFrame(ws, buildWsErrorFrame(429, {
|
|
1659
|
+
type: "rate_limit_error",
|
|
1660
|
+
message: cooldownErrorMessage(err, accountSelector),
|
|
1661
|
+
}));
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
finalizeLog(502);
|
|
1665
|
+
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
1666
|
+
type: "proxy_error",
|
|
1667
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1668
|
+
}));
|
|
1669
|
+
} catch {
|
|
1670
|
+
/* socket already gone or send dropped */
|
|
1671
|
+
}
|
|
1672
|
+
} finally {
|
|
1673
|
+
turnAdmissionLease.release();
|
|
1674
|
+
if (!logged && turnAbort.signal.aborted) finalizeLog(499);
|
|
1675
|
+
if (ws.data.cancel === cancelTurn) ws.data.cancel = undefined;
|
|
1676
|
+
}
|
|
1677
|
+
})();
|
|
1678
|
+
},
|
|
1679
|
+
close(ws: ServerWebSocket<WsData>) {
|
|
1680
|
+
if (ws.data.kind === "live-sideband") {
|
|
1681
|
+
closeLiveSideband(ws);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
unregisterCodexWebSocket(ws);
|
|
1685
|
+
ws.data.admissionLease?.release();
|
|
1686
|
+
ws.data.admissionLease = undefined;
|
|
1687
|
+
ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
} as const;
|
|
1691
|
+
|
|
1692
|
+
server = Bun.serve<WsData>({ ...serveOptions, port: listenPort, hostname: bindHost });
|
|
1693
|
+
|
|
1694
|
+
// Both binds are one startup transaction (#1102). If the loopback bind fails after the
|
|
1695
|
+
// public one succeeded, leaving the public listener up would strand it: the CLI's port
|
|
1696
|
+
// retry would read the failure as a public-port conflict and pick a different port,
|
|
1697
|
+
// accumulating listeners. Roll back and rethrow the original error instead.
|
|
1698
|
+
if (loopbackListenerPort !== null) {
|
|
1699
|
+
try {
|
|
1700
|
+
loopbackServer = Bun.serve<WsData>({
|
|
1701
|
+
...serveOptions,
|
|
1702
|
+
port: loopbackListenerPort,
|
|
1703
|
+
hostname: "127.0.0.1",
|
|
1704
|
+
});
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
try {
|
|
1707
|
+
// startServer is synchronous, so this rollback cannot await. Bun begins closing the
|
|
1708
|
+
// listen socket on the call itself; the caller sees the original bind error either
|
|
1709
|
+
// way, and the alternative — leaving the public listener up — is the failure this
|
|
1710
|
+
// rollback exists to prevent.
|
|
1711
|
+
void server.stop(true);
|
|
1712
|
+
} catch {
|
|
1713
|
+
/* the original bind error is the one worth reporting */
|
|
1714
|
+
}
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
userCostOverlayReconciler?.stop();
|
|
1720
|
+
backgroundLifecycle?.releaseAfterFailedStart();
|
|
1721
|
+
void nativeMainLifecycle.release();
|
|
1722
|
+
throw error;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
bindNativeMainStartupLifecycle(server, nativeMainLifecycle);
|
|
1726
|
+
const nativeStop = server.stop.bind(server);
|
|
1727
|
+
const loopbackListenerRef = loopbackServer;
|
|
1728
|
+
Object.defineProperty(server, "stop", {
|
|
1729
|
+
configurable: true,
|
|
1730
|
+
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
1731
|
+
// The orchestration lives in `runListenerShutdown` so its two competing properties —
|
|
1732
|
+
// cleanup completes, failure propagates — are testable without a live socket.
|
|
1733
|
+
await runListenerShutdown(
|
|
1734
|
+
[
|
|
1735
|
+
() => nativeStop(closeActiveConnections),
|
|
1736
|
+
...(loopbackListenerRef
|
|
1737
|
+
? [() => loopbackListenerRef.stop(closeActiveConnections)]
|
|
1738
|
+
: []),
|
|
1739
|
+
async () => {
|
|
1740
|
+
userCostOverlayReconciler?.stop();
|
|
1741
|
+
},
|
|
1742
|
+
],
|
|
1743
|
+
async () => {
|
|
1744
|
+
await backgroundLifecycle.release();
|
|
1745
|
+
await releaseNativeMainStartupLifecycle(server);
|
|
1746
|
+
},
|
|
1747
|
+
);
|
|
1748
|
+
},
|
|
1749
|
+
});
|
|
1750
|
+
setServerRef(server);
|
|
1751
|
+
const actualPort = server.port ?? listenPort;
|
|
1752
|
+
boundPort = actualPort;
|
|
1753
|
+
setCorsOrigin(actualPort);
|
|
1754
|
+
|
|
1755
|
+
console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
|
|
1756
|
+
console.log(` POST /v1/responses → provider translation`);
|
|
1757
|
+
console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
|
|
1758
|
+
console.log(` GET /healthz → health check`);
|
|
1759
|
+
console.log(` GET /api/* → management API`);
|
|
1760
|
+
console.log(` GET / → GUI dashboard`);
|
|
1761
|
+
|
|
1762
|
+
if (loopbackServer) {
|
|
1763
|
+
// Loud on every start, not once at enable time. An operator who inherits a config, or
|
|
1764
|
+
// who forgot, has to be able to see that an unauthenticated surface is live without
|
|
1765
|
+
// reading the file.
|
|
1766
|
+
const loopbackPort = loopbackServer.port ?? loopbackListenerPort;
|
|
1767
|
+
console.warn(`⚠️ Unauthenticated loopback listener active on http://127.0.0.1:${loopbackPort}`);
|
|
1768
|
+
console.warn(` Any local process can use it without a credential — it spends account`);
|
|
1769
|
+
console.warn(` quota and paid provider credentials, and can starve authenticated`);
|
|
1770
|
+
console.warn(` remote clients. Not for shared or multi-tenant hosts.`);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Prime pool-account quota in the background so the rotation engine has real
|
|
1774
|
+
// usage scores from the first routing decision, even when the dashboard is
|
|
1775
|
+
// never opened (the common CLI/WSL case). Fire-and-forget: never blocks the
|
|
1776
|
+
// listener, and a blocked network silently no-ops (see Phase 30 diagnostics).
|
|
1777
|
+
const openAiProvider = config.providers.openai;
|
|
1778
|
+
if (
|
|
1779
|
+
openAiProvider
|
|
1780
|
+
&& openAiProvider.disabled !== true
|
|
1781
|
+
&& isCanonicalOpenAiForwardProvider(openAiProvider)
|
|
1782
|
+
&& providerCodexAccountMode("openai", openAiProvider) === "pool"
|
|
1783
|
+
) {
|
|
1784
|
+
import("../codex/plan-from-token")
|
|
1785
|
+
.then(({ reconcileCodexPlansFromTokens }) => {
|
|
1786
|
+
try {
|
|
1787
|
+
reconcileCodexPlansFromTokens(config);
|
|
1788
|
+
} catch {
|
|
1789
|
+
// Derived plan metadata must not block WHAM priming.
|
|
1790
|
+
}
|
|
1791
|
+
return import("../codex/auth-api");
|
|
1792
|
+
})
|
|
1793
|
+
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
|
|
1794
|
+
.catch(() => {});
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
1798
|
+
backgroundLifecycle.scheduleStartupRun();
|
|
1799
|
+
|
|
1800
|
+
// Compatibility Lab is optional: wire it only for installs that actually use it -- any
|
|
1801
|
+
// routing profile, or automation enabled on disk. This runs synchronously before
|
|
1802
|
+
// startServer returns, in the same turn as Bun.serve, so a policy route can never be
|
|
1803
|
+
// evaluated before its evidence provider is registered. That ordering is load-bearing:
|
|
1804
|
+
// the subagent-fallback chain routes synchronously and has nowhere to await.
|
|
1805
|
+
const labConfigDir = getConfigDir();
|
|
1806
|
+
if (labActivationRequired(config, labConfigDir)) {
|
|
1807
|
+
activateLab(config, labConfigDir);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
registerFork();
|
|
1811
|
+
|
|
1812
|
+
return server;
|
|
1813
|
+
}
|