@iislee/opencodex 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +471 -0
- package/src/service.ts +2554 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft SIGTERM-before-/F is NOT available on Windows (process.kill is TerminateProcess).
|
|
3
|
+
* Graceful drain is stopProxyGracefully(); this module only resets leftover TCBs after hard kill.
|
|
4
|
+
*/
|
|
5
|
+
import { dlopen, ptr, type Pointer } from "bun:ffi";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
export type TcpQuad = {
|
|
9
|
+
localAddr: string;
|
|
10
|
+
localPort: number;
|
|
11
|
+
remoteAddr: string;
|
|
12
|
+
remotePort: number;
|
|
13
|
+
state: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Parse netstat -ano rows whose local address uses `port`. Exported for tests. */
|
|
17
|
+
export function parseTcpQuadsForLocalPort(output: string, port: number): TcpQuad[] {
|
|
18
|
+
const rows: TcpQuad[] = [];
|
|
19
|
+
const portSuffix = `:${port}`;
|
|
20
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
21
|
+
const line = rawLine.trim();
|
|
22
|
+
if (!/^TCP\b/i.test(line)) continue;
|
|
23
|
+
const parts = line.split(/\s+/);
|
|
24
|
+
if (parts.length < 4) continue;
|
|
25
|
+
const local = parts[1]!;
|
|
26
|
+
const remote = parts[2]!;
|
|
27
|
+
const state = parts[3]!;
|
|
28
|
+
if (!local.endsWith(portSuffix) && !local.endsWith(`]:${port}`)) continue;
|
|
29
|
+
const localAddr = stripPort(local, port);
|
|
30
|
+
const remoteParsed = splitHostPort(remote);
|
|
31
|
+
if (!localAddr || !remoteParsed) continue;
|
|
32
|
+
rows.push({
|
|
33
|
+
localAddr,
|
|
34
|
+
localPort: port,
|
|
35
|
+
remoteAddr: remoteParsed.host,
|
|
36
|
+
remotePort: remoteParsed.port,
|
|
37
|
+
state,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return rows;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stripPort(addr: string, port: number): string | null {
|
|
44
|
+
const suffix = `:${port}`;
|
|
45
|
+
if (addr.endsWith(suffix)) return addr.slice(0, -suffix.length).replace(/^\[|\]$/g, "") || "0.0.0.0";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function splitHostPort(addr: string): { host: string; port: number } | null {
|
|
50
|
+
if (addr === "0.0.0.0:0" || addr === "*:*" || addr === "[::]:0") {
|
|
51
|
+
return { host: "0.0.0.0", port: 0 };
|
|
52
|
+
}
|
|
53
|
+
const m = /^(?:\[([^\]]+)\]|([^:]+)):(\d+)$/.exec(addr);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
return { host: (m[1] ?? m[2] ?? "0.0.0.0").replace(/^::ffff:/i, ""), port: Number(m[3]) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ipv4ToWinUint32(addr: string): number | null {
|
|
59
|
+
const host = addr.replace(/^::ffff:/i, "");
|
|
60
|
+
// Refuse bare IPv6 — SetTcpEntry is IPv4-only; coercing "::"/"::1" to 0 would
|
|
61
|
+
// miss the real TCB and can hit an unrelated IPv4 wildcard row.
|
|
62
|
+
if (isBareIpv6Address(host)) return null;
|
|
63
|
+
if (host === "0.0.0.0" || host === "*") return 0;
|
|
64
|
+
const parts = host.split(".").map(Number);
|
|
65
|
+
if (parts.length !== 4 || parts.some(n => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
66
|
+
return (parts[0]! | (parts[1]! << 8) | (parts[2]! << 16) | (parts[3]! << 24)) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True for bare IPv6 (including :: / ::1), false for dotted IPv4 and IPv4-mapped. */
|
|
70
|
+
export function isBareIpv6Address(addr: string): boolean {
|
|
71
|
+
const host = String(addr || "").replace(/^\[|\]$/g, "").replace(/^::ffff:/i, "");
|
|
72
|
+
if (!host) return false;
|
|
73
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return false;
|
|
74
|
+
return host.includes(":");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type WindowsTcpDropResult = {
|
|
78
|
+
/** Successful SetTcpEntry(DELETE_TCB) calls for IPv4 rows. */
|
|
79
|
+
dropped: number;
|
|
80
|
+
/** IPv6 (or unparseable) rows skipped — never coerced into IPv4 wildcards. */
|
|
81
|
+
skippedIpv6: number;
|
|
82
|
+
/**
|
|
83
|
+
* SetTcpEntry returned ERROR_ACCESS_DENIED (5) or the UAC-non-elevated code (317).
|
|
84
|
+
* On a normal (non-admin) update worker this is expected — ghost LISTEN rows must
|
|
85
|
+
* clear by waiting for the OS, not by SetTcpEntry.
|
|
86
|
+
*/
|
|
87
|
+
accessDenied: number;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
function htons(port: number): number {
|
|
91
|
+
return (((port & 0xff) << 8) | ((port >> 8) & 0xff)) >>> 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type SetTcpEntryFn = (row: Pointer) => number;
|
|
95
|
+
let setTcpEntryFn: SetTcpEntryFn | null | undefined;
|
|
96
|
+
|
|
97
|
+
function loadSetTcpEntry(): SetTcpEntryFn | null {
|
|
98
|
+
if (setTcpEntryFn !== undefined) return setTcpEntryFn;
|
|
99
|
+
if (process.platform !== "win32") {
|
|
100
|
+
setTcpEntryFn = null;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const lib = dlopen("iphlpapi.dll", {
|
|
105
|
+
SetTcpEntry: { args: ["ptr"], returns: "u32" },
|
|
106
|
+
});
|
|
107
|
+
setTcpEntryFn = (row: Pointer) => lib.symbols.SetTcpEntry(row) as number;
|
|
108
|
+
} catch {
|
|
109
|
+
setTcpEntryFn = null;
|
|
110
|
+
}
|
|
111
|
+
return setTcpEntryFn;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readNetstatAno(): string {
|
|
115
|
+
const netstat = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\netstat.exe`;
|
|
116
|
+
const cmd = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\cmd.exe`;
|
|
117
|
+
try {
|
|
118
|
+
return execFileSync(cmd, ["/d", "/c", `chcp 437>nul & "${netstat}" -ano -p tcp`], {
|
|
119
|
+
encoding: "utf-8",
|
|
120
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
121
|
+
timeout: 5000,
|
|
122
|
+
windowsHide: true,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
return execFileSync(netstat, ["-ano", "-p", "tcp"], {
|
|
126
|
+
encoding: "utf-8",
|
|
127
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
128
|
+
timeout: 4000,
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Force-delete IPv4 TCP rows bound to `localPort` via SetTcpEntry(DELETE_TCB).
|
|
136
|
+
* Does not kill foreign processes — only resets sockets so the listen port can bind again.
|
|
137
|
+
* Bare IPv6 rows (`::1`, `::`, etc.) are skipped (not coerced into IPv4 wildcards);
|
|
138
|
+
* IPv6 TCB reclamation is unsupported on this path.
|
|
139
|
+
*/
|
|
140
|
+
export function dropWindowsTcpRowsForLocalPort(port: number): WindowsTcpDropResult {
|
|
141
|
+
if (process.platform !== "win32" || !Number.isFinite(port) || port <= 0) {
|
|
142
|
+
return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
|
|
143
|
+
}
|
|
144
|
+
const setTcpEntry = loadSetTcpEntry();
|
|
145
|
+
if (!setTcpEntry) return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
|
|
146
|
+
|
|
147
|
+
let output = "";
|
|
148
|
+
try {
|
|
149
|
+
output = readNetstatAno();
|
|
150
|
+
} catch {
|
|
151
|
+
return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rows = parseTcpQuadsForLocalPort(output, Math.trunc(port));
|
|
155
|
+
let dropped = 0;
|
|
156
|
+
let skippedIpv6 = 0;
|
|
157
|
+
let accessDenied = 0;
|
|
158
|
+
for (const row of rows) {
|
|
159
|
+
const localDw = ipv4ToWinUint32(row.localAddr);
|
|
160
|
+
const remoteDw = ipv4ToWinUint32(row.remoteAddr);
|
|
161
|
+
if (localDw === null || remoteDw === null) {
|
|
162
|
+
if (isBareIpv6Address(row.localAddr) || isBareIpv6Address(row.remoteAddr)) {
|
|
163
|
+
skippedIpv6 += 1;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const buf = new ArrayBuffer(20);
|
|
168
|
+
const view = new DataView(buf);
|
|
169
|
+
view.setUint32(0, 12, true); // MIB_TCP_STATE_DELETE_TCB
|
|
170
|
+
view.setUint32(4, localDw, true);
|
|
171
|
+
view.setUint32(8, htons(row.localPort), true);
|
|
172
|
+
view.setUint32(12, remoteDw, true);
|
|
173
|
+
view.setUint32(16, htons(row.remotePort), true);
|
|
174
|
+
try {
|
|
175
|
+
const rc = setTcpEntry(ptr(buf));
|
|
176
|
+
if (rc === 0) dropped += 1;
|
|
177
|
+
// 5 = ERROR_ACCESS_DENIED, 317 = non-elevated SetTcpEntry (MSDN).
|
|
178
|
+
else if (rc === 5 || rc === 317) accessDenied += 1;
|
|
179
|
+
} catch {
|
|
180
|
+
/* keep going */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { dropped, skippedIpv6, accessDenied };
|
|
184
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { durableBunPath } from "../lib/bun-runtime";
|
|
4
|
+
import type { WindowsTrayStatus } from "../tray/windows";
|
|
5
|
+
|
|
6
|
+
export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
|
|
7
|
+
|
|
8
|
+
/** Run blocking registry/PowerShell waits outside the proxy event loop. */
|
|
9
|
+
export function runWindowsTrayAction(action: WindowsTrayAction): Promise<WindowsTrayStatus> {
|
|
10
|
+
if (action === "status") {
|
|
11
|
+
return import("../tray/windows").then(({ getWindowsTrayStatusAsync }) => getWindowsTrayStatusAsync());
|
|
12
|
+
}
|
|
13
|
+
const bun = durableBunPath();
|
|
14
|
+
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
execFile(bun, [cli, "tray", action, "--json"], {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
env: process.env,
|
|
19
|
+
timeout: 30_000,
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
maxBuffer: 256 * 1024,
|
|
22
|
+
}, (error, stdout, stderr) => {
|
|
23
|
+
const lines = stdout.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
24
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(lines[index]) as WindowsTrayStatus | { error?: unknown };
|
|
27
|
+
if ("supported" in parsed && typeof parsed.supported === "boolean") {
|
|
28
|
+
resolve(parsed);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const failure = parsed as { error?: unknown };
|
|
32
|
+
if (typeof failure.error === "string") {
|
|
33
|
+
reject(new Error(failure.error));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
} catch { /* scan earlier lines */ }
|
|
37
|
+
}
|
|
38
|
+
reject(new Error(error?.message || stderr.trim() || "Windows tray action returned no status"));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import type { ServerWebSocket } from "bun";
|
|
2
|
+
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
3
|
+
import type { CodexAuthContext } from "../codex/auth-context";
|
|
4
|
+
import { headersForCodexAuthContext } from "../codex/auth-context";
|
|
5
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
6
|
+
import type { DataPlaneAdmission } from "./auth-cors";
|
|
7
|
+
import type { AdmissionReservation } from "../lib/admission";
|
|
8
|
+
|
|
9
|
+
const OPEN = 1;
|
|
10
|
+
type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
|
|
11
|
+
type ResponsesPayloadObserver = (payload: string) => void;
|
|
12
|
+
const SAFE_RESPONSE_HEADER_EXACT = new Set([
|
|
13
|
+
"retry-after",
|
|
14
|
+
"x-request-id",
|
|
15
|
+
"openai-request-id",
|
|
16
|
+
"x-codex-turn-state",
|
|
17
|
+
"openai-model",
|
|
18
|
+
"x-models-etag",
|
|
19
|
+
"x-reasoning-included",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export interface WsData {
|
|
23
|
+
headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
|
|
24
|
+
/**
|
|
25
|
+
* Resolved once at the handshake. Auth is handshake-time only on this path, so
|
|
26
|
+
* the per-frame log contexts have no request headers left to re-resolve from.
|
|
27
|
+
* Optional like every other member here: a socket object can exist before the
|
|
28
|
+
* handshake fills it, and an unattributed frame is preferable to a fabricated
|
|
29
|
+
* attribution.
|
|
30
|
+
*/
|
|
31
|
+
admission?: DataPlaneAdmission;
|
|
32
|
+
authContext?: CodexAuthContext; // last resolved account decision for observability/registry cleanup
|
|
33
|
+
cancel?: () => void; // cancels the in-flight stream reader/fetch
|
|
34
|
+
turnId?: number; // monotonically increasing per socket; prevents stale frames after replacement turns
|
|
35
|
+
/** Discriminator: Responses reframing vs transparent live/realtime sideband relay. */
|
|
36
|
+
kind?: "responses" | "live-sideband";
|
|
37
|
+
liveUpstream?: WebSocket;
|
|
38
|
+
liveUpstreamUrl?: string;
|
|
39
|
+
liveUpstreamHeaders?: Record<string, string>;
|
|
40
|
+
livePending?: Array<string | Buffer>;
|
|
41
|
+
liveOpened?: boolean;
|
|
42
|
+
admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the Responses WebSocket upgrade payload.
|
|
47
|
+
*
|
|
48
|
+
* Extracted so the handshake's contract is testable: `server.upgrade` hands its
|
|
49
|
+
* `data` straight to the socket, and a client has no way to read `ws.data` back.
|
|
50
|
+
* A test that only asserts "the socket opened" would still pass if the admission
|
|
51
|
+
* were dropped from the payload, so the payload itself is what gets asserted.
|
|
52
|
+
*/
|
|
53
|
+
export function buildResponsesWsData(headers: Headers, admission: DataPlaneAdmission, admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>): WsData {
|
|
54
|
+
// Auth is handshake-time only on this path: the per-frame contexts have no
|
|
55
|
+
// request headers left to re-resolve from, so the decision rides along here.
|
|
56
|
+
return { headers, admission, ...(admissionLease ? { admissionLease } : {}) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class WsSendDroppedError extends Error {
|
|
60
|
+
constructor() {
|
|
61
|
+
super("websocket send dropped the message");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function selectForwardHeaders(
|
|
66
|
+
headers: Headers,
|
|
67
|
+
codexOverride?: { accessToken: string; chatgptAccountId: string },
|
|
68
|
+
): Headers {
|
|
69
|
+
const selected = new Headers();
|
|
70
|
+
for (const name of FORWARD_HEADERS) {
|
|
71
|
+
const value = headers.get(name);
|
|
72
|
+
if (value) selected.set(name, value);
|
|
73
|
+
}
|
|
74
|
+
if (codexOverride) {
|
|
75
|
+
selected.set("authorization", `Bearer ${codexOverride.accessToken}`);
|
|
76
|
+
selected.set("chatgpt-account-id", codexOverride.chatgptAccountId);
|
|
77
|
+
}
|
|
78
|
+
return selected;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function selectForwardHeadersForAuthContext(headers: Headers, ctx: CodexAuthContext): Headers {
|
|
82
|
+
return headersForCodexAuthContext(headers, ctx);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function safeResponseHeaders(headers: Headers): Record<string, string> {
|
|
86
|
+
const out: Record<string, string> = {};
|
|
87
|
+
for (const [name, value] of headers) {
|
|
88
|
+
const lower = name.toLowerCase();
|
|
89
|
+
if (
|
|
90
|
+
SAFE_RESPONSE_HEADER_EXACT.has(lower) ||
|
|
91
|
+
lower.startsWith("x-ratelimit-") ||
|
|
92
|
+
/^x-codex(?:-[a-z0-9-]+)?-(primary|secondary|tertiary)-(used-percent|window-minutes|reset-at)$/.test(lower) ||
|
|
93
|
+
/^x-codex(?:-[a-z0-9-]+)?-limit-name$/.test(lower)
|
|
94
|
+
) {
|
|
95
|
+
out[lower] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildWarmupCompletionFrames(frame: Record<string, unknown>): string[] {
|
|
102
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
103
|
+
const baseResponse: Record<string, unknown> = {
|
|
104
|
+
id: "",
|
|
105
|
+
object: "response",
|
|
106
|
+
created_at: createdAt,
|
|
107
|
+
model: typeof frame.model === "string" ? frame.model : undefined,
|
|
108
|
+
output: [],
|
|
109
|
+
};
|
|
110
|
+
return [
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
type: "response.created",
|
|
113
|
+
sequence_number: 0,
|
|
114
|
+
response: { ...baseResponse, status: "in_progress" },
|
|
115
|
+
}),
|
|
116
|
+
JSON.stringify({
|
|
117
|
+
type: "response.completed",
|
|
118
|
+
sequence_number: 1,
|
|
119
|
+
response: { ...baseResponse, status: "completed" },
|
|
120
|
+
}),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function sendTextFrame(ws: ServerWebSocket<WsData>, payload: string): void {
|
|
125
|
+
if (ws.readyState !== OPEN) throw new WsSendDroppedError();
|
|
126
|
+
const result = ws.send(payload);
|
|
127
|
+
if (result === 0) throw new WsSendDroppedError();
|
|
128
|
+
// Bun returns -1 when queued with backpressure. That is accepted; a later 0 is the hard failure.
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function sendJsonFrame(ws: ServerWebSocket<WsData>, payload: Record<string, unknown>): void {
|
|
132
|
+
sendTextFrame(ws, JSON.stringify(payload));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function buildWsErrorFrame(
|
|
136
|
+
status: number,
|
|
137
|
+
error: Record<string, unknown>,
|
|
138
|
+
headers?: Headers,
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
return {
|
|
141
|
+
type: "error",
|
|
142
|
+
status,
|
|
143
|
+
error,
|
|
144
|
+
headers: headers ? safeResponseHeaders(headers) : {},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseSseBlock(block: string): string | null {
|
|
149
|
+
const data: string[] = [];
|
|
150
|
+
for (const line of block.split(/\r?\n/)) {
|
|
151
|
+
if (line.startsWith("data:")) {
|
|
152
|
+
const value = line.slice(5);
|
|
153
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return data.length > 0 ? data.join("\n") : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function nextSseBlock(buffer: string): { block: string; rest: string } | null {
|
|
160
|
+
const match = buffer.match(/\r?\n\r?\n/);
|
|
161
|
+
if (!match || match.index === undefined) return null;
|
|
162
|
+
return {
|
|
163
|
+
block: buffer.slice(0, match.index),
|
|
164
|
+
rest: buffer.slice(match.index + match[0].length),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function payloadType(payload: string): string | null {
|
|
169
|
+
try {
|
|
170
|
+
const json = JSON.parse(payload) as { type?: unknown };
|
|
171
|
+
return typeof json.type === "string" ? json.type : null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function terminalStatusFromType(type: string): ResponsesTerminalStatus | null {
|
|
178
|
+
switch (type) {
|
|
179
|
+
case "response.completed":
|
|
180
|
+
return "completed";
|
|
181
|
+
case "response.failed":
|
|
182
|
+
return "failed";
|
|
183
|
+
case "response.incomplete":
|
|
184
|
+
return "incomplete";
|
|
185
|
+
default:
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function protocolError(message: string): Record<string, unknown> {
|
|
191
|
+
return {
|
|
192
|
+
type: "protocol_error",
|
|
193
|
+
code: "websocket_protocol_error",
|
|
194
|
+
message,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendProtocolError(ws: ServerWebSocket<WsData>, status: number, message: string): void {
|
|
199
|
+
sendJsonFrame(ws, buildWsErrorFrame(status, protocolError(message)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function pumpResponsesSseToWebSocket(
|
|
203
|
+
ws: ServerWebSocket<WsData>,
|
|
204
|
+
sseStream: ReadableStream<Uint8Array>,
|
|
205
|
+
options: {
|
|
206
|
+
isCurrent?: () => boolean;
|
|
207
|
+
onTerminal?: ResponsesTerminalReporter;
|
|
208
|
+
onSsePayload?: ResponsesPayloadObserver;
|
|
209
|
+
} = {},
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const reader = sseStream.getReader();
|
|
212
|
+
const isCurrent = options.isCurrent ?? (() => true);
|
|
213
|
+
let clientCancelled = false;
|
|
214
|
+
let terminalReported = false;
|
|
215
|
+
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
216
|
+
if (terminalReported || clientCancelled || !isCurrent()) return;
|
|
217
|
+
terminalReported = true;
|
|
218
|
+
options.onTerminal?.(status);
|
|
219
|
+
};
|
|
220
|
+
const cancel = () => {
|
|
221
|
+
clientCancelled = true;
|
|
222
|
+
void reader.cancel().catch(() => {});
|
|
223
|
+
};
|
|
224
|
+
ws.data.cancel = cancel;
|
|
225
|
+
|
|
226
|
+
const decoder = new TextDecoder();
|
|
227
|
+
let buffer = "";
|
|
228
|
+
let terminalSeen = false;
|
|
229
|
+
|
|
230
|
+
const handlePayload = (payload: string): boolean => {
|
|
231
|
+
if (!isCurrent()) return true;
|
|
232
|
+
if (payload === "[DONE]") return false;
|
|
233
|
+
try {
|
|
234
|
+
options.onSsePayload?.(payload);
|
|
235
|
+
} catch {
|
|
236
|
+
/* payload observation must not affect WebSocket delivery */
|
|
237
|
+
}
|
|
238
|
+
const type = payloadType(payload);
|
|
239
|
+
if (!type) {
|
|
240
|
+
reportTerminal("incomplete");
|
|
241
|
+
sendProtocolError(ws, 502, "Invalid JSON payload in upstream SSE frame");
|
|
242
|
+
terminalSeen = true;
|
|
243
|
+
void reader.cancel().catch(() => {});
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (terminalSeen) return true;
|
|
247
|
+
sendTextFrame(ws, payload);
|
|
248
|
+
const terminalStatus = terminalStatusFromType(type);
|
|
249
|
+
if (terminalStatus) {
|
|
250
|
+
reportTerminal(terminalStatus);
|
|
251
|
+
terminalSeen = true;
|
|
252
|
+
void reader.cancel().catch(() => {});
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
while (!terminalSeen) {
|
|
260
|
+
const { done, value } = await reader.read();
|
|
261
|
+
if (done) break;
|
|
262
|
+
buffer += decoder.decode(value, { stream: true });
|
|
263
|
+
let next: { block: string; rest: string } | null;
|
|
264
|
+
while ((next = nextSseBlock(buffer))) {
|
|
265
|
+
buffer = next.rest;
|
|
266
|
+
const payload = parseSseBlock(next.block);
|
|
267
|
+
if (payload && handlePayload(payload)) break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
buffer += decoder.decode();
|
|
271
|
+
if (!terminalSeen && buffer.trim()) {
|
|
272
|
+
const payload = parseSseBlock(buffer);
|
|
273
|
+
if (payload) handlePayload(payload);
|
|
274
|
+
}
|
|
275
|
+
if (!terminalSeen && isCurrent() && !clientCancelled) {
|
|
276
|
+
reportTerminal("incomplete");
|
|
277
|
+
sendProtocolError(ws, 502, "Upstream stream ended before response terminal event");
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (!terminalSeen && isCurrent() && ws.readyState === OPEN) {
|
|
281
|
+
if (!(err instanceof WsSendDroppedError)) reportTerminal("incomplete");
|
|
282
|
+
sendProtocolError(ws, 502, err instanceof Error ? err.message : String(err));
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
if (ws.data.cancel === cancel) ws.data.cancel = undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function sendResponsesJsonAsEvents(
|
|
290
|
+
ws: ServerWebSocket<WsData>,
|
|
291
|
+
response: Record<string, unknown>,
|
|
292
|
+
onTerminal?: ResponsesTerminalReporter,
|
|
293
|
+
onPayload?: ResponsesPayloadObserver,
|
|
294
|
+
): void {
|
|
295
|
+
const sendObservedFrame = (payload: Record<string, unknown>) => {
|
|
296
|
+
const text = JSON.stringify(payload);
|
|
297
|
+
try {
|
|
298
|
+
onPayload?.(text);
|
|
299
|
+
} catch {
|
|
300
|
+
/* payload observation must not affect WebSocket delivery */
|
|
301
|
+
}
|
|
302
|
+
sendTextFrame(ws, text);
|
|
303
|
+
};
|
|
304
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
305
|
+
sendObservedFrame({
|
|
306
|
+
type: "response.created",
|
|
307
|
+
response: { ...response, status: "in_progress", output: [] },
|
|
308
|
+
});
|
|
309
|
+
output.forEach((item, outputIndex) => {
|
|
310
|
+
sendObservedFrame({
|
|
311
|
+
type: "response.output_item.done",
|
|
312
|
+
output_index: outputIndex,
|
|
313
|
+
item,
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
const finalStatus = response.status === "failed" || response.status === "incomplete"
|
|
317
|
+
? response.status
|
|
318
|
+
: "completed";
|
|
319
|
+
sendObservedFrame({
|
|
320
|
+
type: `response.${finalStatus}` as "response.completed" | "response.failed" | "response.incomplete",
|
|
321
|
+
response: { ...response, status: finalStatus },
|
|
322
|
+
});
|
|
323
|
+
onTerminal?.(finalStatus);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function errorPayloadFromText(text: string): Record<string, unknown> {
|
|
327
|
+
try {
|
|
328
|
+
const json = JSON.parse(text) as { error?: unknown };
|
|
329
|
+
if (json.error && typeof json.error === "object" && !Array.isArray(json.error)) {
|
|
330
|
+
return json.error as Record<string, unknown>;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
/* fall through */
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
type: "upstream_error",
|
|
337
|
+
message: text ? text.slice(0, 500) : "Upstream request failed",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function sendResponseToWebSocket(
|
|
342
|
+
ws: ServerWebSocket<WsData>,
|
|
343
|
+
response: Response,
|
|
344
|
+
isCurrent: () => boolean,
|
|
345
|
+
options: {
|
|
346
|
+
onTerminal?: ResponsesTerminalReporter;
|
|
347
|
+
onSsePayload?: ResponsesPayloadObserver;
|
|
348
|
+
} = {},
|
|
349
|
+
): Promise<void> {
|
|
350
|
+
if (!isCurrent()) {
|
|
351
|
+
await response.body?.cancel().catch(() => {});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
const text = await response.text().catch(() => "");
|
|
357
|
+
if (!isCurrent()) return;
|
|
358
|
+
sendJsonFrame(ws, buildWsErrorFrame(response.status, errorPayloadFromText(text), response.headers));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
363
|
+
if (!response.body) {
|
|
364
|
+
options.onTerminal?.("incomplete");
|
|
365
|
+
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
366
|
+
type: "protocol_error",
|
|
367
|
+
code: "websocket_protocol_error",
|
|
368
|
+
message: `Unexpected successful upstream response without a body (${response.status})`,
|
|
369
|
+
}, response.headers));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (contentType.includes("text/event-stream")) {
|
|
374
|
+
await pumpResponsesSseToWebSocket(ws, response.body, {
|
|
375
|
+
isCurrent,
|
|
376
|
+
onTerminal: options.onTerminal,
|
|
377
|
+
onSsePayload: options.onSsePayload,
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (contentType.includes("application/json")) {
|
|
383
|
+
const text = await response.text();
|
|
384
|
+
if (!isCurrent()) return;
|
|
385
|
+
const json = JSON.parse(text) as Record<string, unknown>;
|
|
386
|
+
sendResponsesJsonAsEvents(ws, json, options.onTerminal, options.onSsePayload);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const { prefix, stream } = await readBoundedPrefix(response.body);
|
|
391
|
+
if (!isCurrent()) {
|
|
392
|
+
await stream.cancel().catch(() => {});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (looksLikeSse(prefix)) {
|
|
396
|
+
await pumpResponsesSseToWebSocket(ws, stream, {
|
|
397
|
+
isCurrent,
|
|
398
|
+
onTerminal: options.onTerminal,
|
|
399
|
+
onSsePayload: options.onSsePayload,
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const text = await new Response(stream).text();
|
|
405
|
+
if (!isCurrent()) return;
|
|
406
|
+
const trimmed = text.trim();
|
|
407
|
+
if (trimmed.startsWith("{")) {
|
|
408
|
+
const json = JSON.parse(trimmed) as Record<string, unknown>;
|
|
409
|
+
sendResponsesJsonAsEvents(ws, json, options.onTerminal, options.onSsePayload);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
options.onTerminal?.("incomplete");
|
|
414
|
+
sendJsonFrame(ws, buildWsErrorFrame(502, {
|
|
415
|
+
type: "protocol_error",
|
|
416
|
+
code: "websocket_protocol_error",
|
|
417
|
+
message: `Unexpected successful non-SSE upstream response (${contentType || "missing content-type"})`,
|
|
418
|
+
}, response.headers));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function readBoundedPrefix(
|
|
422
|
+
body: ReadableStream<Uint8Array>,
|
|
423
|
+
maxBytes = 4096,
|
|
424
|
+
): Promise<{ prefix: Uint8Array; stream: ReadableStream<Uint8Array> }> {
|
|
425
|
+
const reader = body.getReader();
|
|
426
|
+
const chunks: Uint8Array[] = [];
|
|
427
|
+
let remainder: Uint8Array | undefined;
|
|
428
|
+
let total = 0;
|
|
429
|
+
while (total < maxBytes) {
|
|
430
|
+
const { done, value } = await reader.read();
|
|
431
|
+
if (done) break;
|
|
432
|
+
const take = Math.min(value.byteLength, maxBytes - total);
|
|
433
|
+
if (take > 0) {
|
|
434
|
+
chunks.push(value.slice(0, take));
|
|
435
|
+
total += take;
|
|
436
|
+
}
|
|
437
|
+
if (take < value.byteLength) {
|
|
438
|
+
remainder = value.slice(take);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const prefix = new Uint8Array(total);
|
|
443
|
+
let offset = 0;
|
|
444
|
+
for (const chunk of chunks) {
|
|
445
|
+
prefix.set(chunk, offset);
|
|
446
|
+
offset += chunk.byteLength;
|
|
447
|
+
}
|
|
448
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
449
|
+
start(controller) {
|
|
450
|
+
if (prefix.byteLength > 0) controller.enqueue(prefix);
|
|
451
|
+
if (remainder && remainder.byteLength > 0) controller.enqueue(remainder);
|
|
452
|
+
},
|
|
453
|
+
async pull(controller) {
|
|
454
|
+
const { done, value } = await reader.read();
|
|
455
|
+
if (done) {
|
|
456
|
+
controller.close();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
controller.enqueue(value);
|
|
460
|
+
},
|
|
461
|
+
cancel(reason) {
|
|
462
|
+
return reader.cancel(reason);
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
return { prefix, stream };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function looksLikeSse(prefix: Uint8Array): boolean {
|
|
469
|
+
const text = new TextDecoder().decode(prefix);
|
|
470
|
+
return /^\s*(event:|data:)/.test(text);
|
|
471
|
+
}
|