@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,1049 @@
|
|
|
1
|
+
import type { ResponsesTerminalStatus } from "../bridge";
|
|
2
|
+
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
3
|
+
import { isUsageDebugEnabled } from "../usage/debug";
|
|
4
|
+
import {
|
|
5
|
+
addRequestLog,
|
|
6
|
+
addFinalRequestLog,
|
|
7
|
+
httpStatusForRequestLogTerminal,
|
|
8
|
+
inspectResponseLogJson,
|
|
9
|
+
inspectResponseLogSsePayload,
|
|
10
|
+
inspectResponseLogSsePayloadParsed,
|
|
11
|
+
recordFirstOutput,
|
|
12
|
+
type RequestLogContext,
|
|
13
|
+
type RequestLogEntry,
|
|
14
|
+
} from "./request-log";
|
|
15
|
+
|
|
16
|
+
const nativePassthroughSseResponses = new WeakSet<Response>();
|
|
17
|
+
const eagerRelaySseResponses = new WeakSet<Response>();
|
|
18
|
+
|
|
19
|
+
export const MAX_INSPECTION_SSE_FRAME_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
export const MAX_COMPLETED_OUTPUT_ITEMS = 256;
|
|
21
|
+
export const MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
export const MAX_TAIL_ERROR_MESSAGE_CHARS = 512;
|
|
23
|
+
|
|
24
|
+
export type InspectionCounters = {
|
|
25
|
+
frameBufferHighWaterBytes: number;
|
|
26
|
+
completedItemsMaxCount: number;
|
|
27
|
+
frameCapOverflows: number;
|
|
28
|
+
itemCapEvictions: number;
|
|
29
|
+
postCancelDrainStops: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const inspectionCounters: InspectionCounters = {
|
|
33
|
+
frameBufferHighWaterBytes: 0,
|
|
34
|
+
completedItemsMaxCount: 0,
|
|
35
|
+
frameCapOverflows: 0,
|
|
36
|
+
itemCapEvictions: 0,
|
|
37
|
+
postCancelDrainStops: 0,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function getInspectionCounters(): InspectionCounters {
|
|
41
|
+
return { ...inspectionCounters };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resetInspectionCountersForTest(): void {
|
|
45
|
+
inspectionCounters.frameBufferHighWaterBytes = 0;
|
|
46
|
+
inspectionCounters.completedItemsMaxCount = 0;
|
|
47
|
+
inspectionCounters.frameCapOverflows = 0;
|
|
48
|
+
inspectionCounters.itemCapEvictions = 0;
|
|
49
|
+
inspectionCounters.postCancelDrainStops = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function relayWithAbort(
|
|
53
|
+
body: ReadableStream<Uint8Array> | null,
|
|
54
|
+
upstream: AbortController,
|
|
55
|
+
onClientGone?: (reason?: unknown) => void,
|
|
56
|
+
): ReadableStream<Uint8Array> | null {
|
|
57
|
+
if (!body) return null;
|
|
58
|
+
const reader = body.getReader();
|
|
59
|
+
return new ReadableStream<Uint8Array>({
|
|
60
|
+
async pull(controller) {
|
|
61
|
+
try {
|
|
62
|
+
const { done, value } = await reader.read();
|
|
63
|
+
if (done) {
|
|
64
|
+
controller.close();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
controller.enqueue(value);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
try { controller.error(err); } catch { /* already torn down */ }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
cancel(reason) {
|
|
73
|
+
// A tee caller may transfer abort ownership to its bounded inspection pump.
|
|
74
|
+
if (onClientGone) onClientGone(reason);
|
|
75
|
+
else upstream.abort(reason);
|
|
76
|
+
reader.cancel(reason).catch(() => {});
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildFailedTailPayload(err: unknown): string {
|
|
82
|
+
const translatorOverflow = isTranslatorBudgetExceededError(err);
|
|
83
|
+
const message = (translatorOverflow
|
|
84
|
+
? "upstream translation buffer exceeded the safe limit"
|
|
85
|
+
: `Upstream stream terminated unexpectedly: ${err instanceof Error ? err.message : String(err)}`)
|
|
86
|
+
.slice(0, MAX_TAIL_ERROR_MESSAGE_CHARS);
|
|
87
|
+
const failure = {
|
|
88
|
+
type: "upstream_error",
|
|
89
|
+
code: translatorOverflow ? "translation_buffer_limit" : "upstream_reset",
|
|
90
|
+
message,
|
|
91
|
+
};
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
type: "response.failed",
|
|
94
|
+
response: { status: "failed", error: failure, last_error: failure },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Relay a passthrough SSE body like relayWithAbort, but convert a MID-STREAM failure (upstream
|
|
100
|
+
* reset after headers) into a clean terminal: any partial block is closed off, then a synthetic
|
|
101
|
+
* `response.failed` event and `data: [DONE]` are emitted and the stream closes. Without this the
|
|
102
|
+
* client sees a raw socket teardown with no terminal SSE event. Deliberately NOT a resend: the
|
|
103
|
+
* upstream already committed the request (duplicate-completion risk — same policy as cursor's
|
|
104
|
+
* committed=non-replayable transport retry).
|
|
105
|
+
*/
|
|
106
|
+
export function relaySseWithFailedTail(
|
|
107
|
+
body: ReadableStream<Uint8Array>,
|
|
108
|
+
upstream: AbortController,
|
|
109
|
+
onClientGone?: (reason?: unknown) => void,
|
|
110
|
+
): ReadableStream<Uint8Array> {
|
|
111
|
+
const reader = body.getReader();
|
|
112
|
+
const encoder = new TextEncoder();
|
|
113
|
+
return new ReadableStream<Uint8Array>({
|
|
114
|
+
async pull(controller) {
|
|
115
|
+
try {
|
|
116
|
+
const { done, value } = await reader.read();
|
|
117
|
+
if (done) {
|
|
118
|
+
controller.close();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
controller.enqueue(value);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const payload = buildFailedTailPayload(err);
|
|
124
|
+
try {
|
|
125
|
+
// Leading blank line terminates a partial SSE block so the failed frame parses cleanly.
|
|
126
|
+
controller.enqueue(encoder.encode(`\n\nevent: response.failed\ndata: ${payload}\n\ndata: [DONE]\n\n`));
|
|
127
|
+
controller.close();
|
|
128
|
+
} catch { /* client already torn down */ }
|
|
129
|
+
upstream.abort();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
cancel(reason) {
|
|
133
|
+
if (onClientGone) onClientGone(reason);
|
|
134
|
+
else upstream.abort(reason);
|
|
135
|
+
reader.cancel(reason).catch(() => {});
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function nextSseBlock(buffer: string): { block: string; rest: string } | null {
|
|
141
|
+
const match = buffer.match(/\r?\n\r?\n/);
|
|
142
|
+
if (!match || match.index === undefined) return null;
|
|
143
|
+
return {
|
|
144
|
+
block: buffer.slice(0, match.index),
|
|
145
|
+
rest: buffer.slice(match.index + match[0].length),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function sseDataPayload(block: string): string | null {
|
|
150
|
+
const data: string[] = [];
|
|
151
|
+
for (const line of block.split(/\r?\n/)) {
|
|
152
|
+
if (!line.startsWith("data:")) continue;
|
|
153
|
+
const value = line.slice(5);
|
|
154
|
+
data.push(value.startsWith(" ") ? value.slice(1) : value);
|
|
155
|
+
}
|
|
156
|
+
return data.length > 0 ? data.join("\n") : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function terminalStatusFromSsePayload(payload: string): ResponsesTerminalStatus | null {
|
|
160
|
+
if (payload === "[DONE]") return null;
|
|
161
|
+
try {
|
|
162
|
+
return terminalStatusFromParsed(JSON.parse(payload));
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True when a native Responses SSE payload carries the FIRST kind of non-empty model output. */
|
|
169
|
+
export function isFirstOutputSsePayload(payload: string | null): boolean {
|
|
170
|
+
if (!payload || payload === "[DONE]") return false;
|
|
171
|
+
try {
|
|
172
|
+
return firstOutputFromParsed(JSON.parse(payload));
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function firstOutputFromParsed(parsed: unknown): boolean {
|
|
179
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
|
|
180
|
+
const event = parsed as { type?: unknown; delta?: unknown };
|
|
181
|
+
return (event.type === "response.output_text.delta"
|
|
182
|
+
|| event.type === "response.reasoning_summary_text.delta"
|
|
183
|
+
|| event.type === "response.reasoning_text.delta")
|
|
184
|
+
&& typeof event.delta === "string"
|
|
185
|
+
&& event.delta.length > 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createFirstOutputReporter(onFirstOutput?: () => void): {
|
|
189
|
+
payload: (payload: string | null) => void;
|
|
190
|
+
parsed: (parsed: unknown) => void;
|
|
191
|
+
} {
|
|
192
|
+
let reported = false;
|
|
193
|
+
const report = (isFirst: boolean) => {
|
|
194
|
+
if (reported || !isFirst) return;
|
|
195
|
+
reported = true;
|
|
196
|
+
try { onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
payload: payload => report(isFirstOutputSsePayload(payload)),
|
|
200
|
+
parsed: parsed => report(firstOutputFromParsed(parsed)),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function terminalStatusFromParsed(parsed: unknown): ResponsesTerminalStatus | null {
|
|
205
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
206
|
+
switch ((parsed as { type?: unknown }).type) {
|
|
207
|
+
case "response.completed":
|
|
208
|
+
return "completed";
|
|
209
|
+
case "response.failed":
|
|
210
|
+
return "failed";
|
|
211
|
+
case "response.incomplete":
|
|
212
|
+
return "incomplete";
|
|
213
|
+
default:
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Extract the response object from a `response.completed` SSE payload, or null. */
|
|
219
|
+
export function completedResponseFromSsePayload(payload: string): { id?: unknown; output?: unknown; status?: unknown } | null {
|
|
220
|
+
if (payload === "[DONE]") return null;
|
|
221
|
+
try {
|
|
222
|
+
const json = JSON.parse(payload) as { type?: unknown; response?: unknown };
|
|
223
|
+
return completedResponseFromParsedEvent(json);
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Extract the response object from an already-parsed `response.completed` event, or null. */
|
|
230
|
+
export function completedResponseFromParsedEvent(
|
|
231
|
+
json: unknown,
|
|
232
|
+
): { id?: unknown; output?: unknown; status?: unknown } | null {
|
|
233
|
+
if (!json || typeof json !== "object" || Array.isArray(json)
|
|
234
|
+
|| (json as { type?: unknown }).type !== "response.completed") return null;
|
|
235
|
+
const response = (json as { response?: unknown }).response;
|
|
236
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return null;
|
|
237
|
+
return response as { id?: unknown; output?: unknown; status?: unknown };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function trackSseForRequestLog(
|
|
241
|
+
body: ReadableStream<Uint8Array>,
|
|
242
|
+
onTerminal: (status: ResponsesTerminalStatus) => void,
|
|
243
|
+
onCancel: () => void,
|
|
244
|
+
logCtx?: RequestLogContext,
|
|
245
|
+
onFirstOutput?: () => void,
|
|
246
|
+
): ReadableStream<Uint8Array> {
|
|
247
|
+
const reader = body.getReader();
|
|
248
|
+
const decoder = new TextDecoder();
|
|
249
|
+
let buffer = "";
|
|
250
|
+
let terminalReported = false;
|
|
251
|
+
const reportFirstOutput = createFirstOutputReporter(onFirstOutput);
|
|
252
|
+
|
|
253
|
+
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
254
|
+
if (terminalReported) return;
|
|
255
|
+
terminalReported = true;
|
|
256
|
+
onTerminal(status);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const inspectPayload = (payload: string | null) => {
|
|
260
|
+
if (!payload) return;
|
|
261
|
+
if (logCtx) inspectResponseLogSsePayload(logCtx, payload);
|
|
262
|
+
reportFirstOutput.payload(payload);
|
|
263
|
+
const status = terminalStatusFromSsePayload(payload);
|
|
264
|
+
if (status) reportTerminal(status);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const inspectChunk = (value: Uint8Array) => {
|
|
268
|
+
buffer += decoder.decode(value, { stream: true });
|
|
269
|
+
let next: { block: string; rest: string } | null;
|
|
270
|
+
while ((next = nextSseBlock(buffer))) {
|
|
271
|
+
buffer = next.rest;
|
|
272
|
+
inspectPayload(sseDataPayload(next.block));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return new ReadableStream<Uint8Array>({
|
|
277
|
+
async pull(controller) {
|
|
278
|
+
try {
|
|
279
|
+
const { done, value } = await reader.read();
|
|
280
|
+
if (done) {
|
|
281
|
+
buffer += decoder.decode();
|
|
282
|
+
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
283
|
+
if (!terminalReported) reportTerminal("incomplete");
|
|
284
|
+
controller.close();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
inspectChunk(value);
|
|
288
|
+
controller.enqueue(value);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (!terminalReported) reportTerminal("incomplete");
|
|
291
|
+
try { controller.error(err); } catch { /* already torn down */ }
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
cancel(reason) {
|
|
295
|
+
onCancel();
|
|
296
|
+
reader.cancel(reason).catch(() => {});
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function responseWithDeferredRequestLog(
|
|
302
|
+
response: Response,
|
|
303
|
+
requestId: string,
|
|
304
|
+
start: number,
|
|
305
|
+
logCtx: RequestLogContext,
|
|
306
|
+
addLog: (entry: RequestLogEntry) => void = addRequestLog,
|
|
307
|
+
): Response {
|
|
308
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
309
|
+
if (isUsageDebugEnabled() && !logCtx.usageDebugContentType && contentType) {
|
|
310
|
+
logCtx.usageDebugContentType = contentType;
|
|
311
|
+
}
|
|
312
|
+
if (isNativePassthroughSseResponse(response)) {
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
if (!response.body || !contentType.includes("text/event-stream")) {
|
|
316
|
+
if (response.body && (contentType.includes("application/json") || response.status >= 400)) {
|
|
317
|
+
const finalizeJsonLog = async () => {
|
|
318
|
+
const text = await response.text();
|
|
319
|
+
// Non-JSON error bodies: inspect/log only a bounded prefix (the stored
|
|
320
|
+
// upstreamError is 500 chars anyway); the FULL text is still forwarded to the
|
|
321
|
+
// client below, unchanged. JSON bodies keep full inspection (usage parsing).
|
|
322
|
+
const isJson = contentType.includes("application/json");
|
|
323
|
+
inspectResponseLogJson(logCtx, isJson ? text : text.slice(0, 8192));
|
|
324
|
+
addFinalRequestLog(requestId, start, logCtx, response.status, { closeReason: "non_stream" }, addLog);
|
|
325
|
+
return text;
|
|
326
|
+
};
|
|
327
|
+
const body = new ReadableStream<Uint8Array>({
|
|
328
|
+
async start(controller) {
|
|
329
|
+
try {
|
|
330
|
+
controller.enqueue(new TextEncoder().encode(await finalizeJsonLog()));
|
|
331
|
+
controller.close();
|
|
332
|
+
} catch (err) {
|
|
333
|
+
addFinalRequestLog(requestId, start, logCtx, 502, { closeReason: "non_stream" }, addLog);
|
|
334
|
+
try { controller.error(err); } catch { /* already torn down */ }
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return new Response(body, {
|
|
339
|
+
status: response.status,
|
|
340
|
+
statusText: response.statusText,
|
|
341
|
+
headers: response.headers,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (isUsageDebugEnabled() && logCtx.usageDebugBodyKind === undefined) {
|
|
345
|
+
logCtx.usageDebugBodyKind = response.body ? "other" : "none";
|
|
346
|
+
}
|
|
347
|
+
addFinalRequestLog(requestId, start, logCtx, response.status, { closeReason: "non_stream" }, addLog);
|
|
348
|
+
return response;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let logged = false;
|
|
352
|
+
const body = trackSseForRequestLog(
|
|
353
|
+
response.body,
|
|
354
|
+
status => {
|
|
355
|
+
if (logged) return;
|
|
356
|
+
logged = true;
|
|
357
|
+
addFinalRequestLog(requestId, start, logCtx, httpStatusForRequestLogTerminal(status, logCtx), {
|
|
358
|
+
terminalStatus: status,
|
|
359
|
+
closeReason: "terminal",
|
|
360
|
+
}, addLog);
|
|
361
|
+
},
|
|
362
|
+
() => {
|
|
363
|
+
if (logged) return;
|
|
364
|
+
logged = true;
|
|
365
|
+
addFinalRequestLog(requestId, start, logCtx, 499, { closeReason: "client_cancel" }, addLog);
|
|
366
|
+
},
|
|
367
|
+
logCtx,
|
|
368
|
+
() => recordFirstOutput(logCtx, start),
|
|
369
|
+
);
|
|
370
|
+
return new Response(body, {
|
|
371
|
+
status: response.status,
|
|
372
|
+
statusText: response.statusText,
|
|
373
|
+
headers: response.headers,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function markNativePassthroughSseResponse(response: Response): Response {
|
|
378
|
+
nativePassthroughSseResponses.add(response);
|
|
379
|
+
return response;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function isNativePassthroughSseResponse(response: Response): boolean {
|
|
383
|
+
return nativePassthroughSseResponses.has(response);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function markEagerRelaySseResponse(response: Response): Response {
|
|
387
|
+
eagerRelaySseResponses.add(response);
|
|
388
|
+
return response;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Test-only path identity seam; runtime behavior must not branch on this marker. */
|
|
392
|
+
export function isEagerRelaySseResponse(response: Response): boolean {
|
|
393
|
+
return eagerRelaySseResponses.has(response);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function relaySseWithHeartbeat(
|
|
397
|
+
body: ReadableStream<Uint8Array> | null,
|
|
398
|
+
upstream: AbortController,
|
|
399
|
+
heartbeatMs = 15_000,
|
|
400
|
+
onTerminal?: (status: ResponsesTerminalStatus) => void,
|
|
401
|
+
options?: { onStart?: () => void; onDone?: () => void },
|
|
402
|
+
): ReadableStream<Uint8Array> | null {
|
|
403
|
+
if (!body) return null;
|
|
404
|
+
const reader = body.getReader();
|
|
405
|
+
const decoder = new TextDecoder();
|
|
406
|
+
const heartbeat = new TextEncoder().encode(": opencodex keepalive\n\n");
|
|
407
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
408
|
+
let closed = false;
|
|
409
|
+
let clientCancelled = false;
|
|
410
|
+
let terminalReported = false;
|
|
411
|
+
let buffer = "";
|
|
412
|
+
|
|
413
|
+
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
414
|
+
if (terminalReported || clientCancelled || closed) return;
|
|
415
|
+
terminalReported = true;
|
|
416
|
+
onTerminal?.(status);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const inspectPayload = (payload: string | null) => {
|
|
420
|
+
if (!payload) return;
|
|
421
|
+
const status = terminalStatusFromSsePayload(payload);
|
|
422
|
+
if (status) reportTerminal(status);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const inspectChunk = (value: Uint8Array) => {
|
|
426
|
+
buffer += decoder.decode(value, { stream: true });
|
|
427
|
+
let next: { block: string; rest: string } | null;
|
|
428
|
+
while ((next = nextSseBlock(buffer))) {
|
|
429
|
+
buffer = next.rest;
|
|
430
|
+
inspectPayload(sseDataPayload(next.block));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const cleanup = () => {
|
|
435
|
+
if (closed) return;
|
|
436
|
+
closed = true;
|
|
437
|
+
if (timer) clearInterval(timer);
|
|
438
|
+
timer = undefined;
|
|
439
|
+
options?.onDone?.();
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return new ReadableStream<Uint8Array>({
|
|
443
|
+
start(controller) {
|
|
444
|
+
options?.onStart?.();
|
|
445
|
+
timer = setInterval(() => {
|
|
446
|
+
if (closed) return;
|
|
447
|
+
try {
|
|
448
|
+
controller.enqueue(heartbeat);
|
|
449
|
+
} catch {
|
|
450
|
+
cleanup();
|
|
451
|
+
}
|
|
452
|
+
}, heartbeatMs);
|
|
453
|
+
},
|
|
454
|
+
async pull(controller) {
|
|
455
|
+
try {
|
|
456
|
+
const { done, value } = await reader.read();
|
|
457
|
+
if (done) {
|
|
458
|
+
buffer += decoder.decode();
|
|
459
|
+
if (buffer.trim()) inspectPayload(sseDataPayload(buffer));
|
|
460
|
+
if (!terminalReported && !clientCancelled) reportTerminal("incomplete");
|
|
461
|
+
cleanup();
|
|
462
|
+
controller.close();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
inspectChunk(value);
|
|
466
|
+
controller.enqueue(value);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
if (!clientCancelled) reportTerminal("incomplete");
|
|
469
|
+
cleanup();
|
|
470
|
+
try { controller.error(err); } catch { /* already torn down */ }
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
cancel(reason) {
|
|
474
|
+
clientCancelled = true;
|
|
475
|
+
cleanup();
|
|
476
|
+
upstream.abort(reason);
|
|
477
|
+
reader.cancel(reason).catch(() => {});
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Background-consume an SSE stream purely for terminal-outcome inspection (quota tracking).
|
|
484
|
+
* Does not produce output; safe to ignore errors (the client-facing stream is separate).
|
|
485
|
+
*/
|
|
486
|
+
export type SseInspector = {
|
|
487
|
+
/** Feed one upstream chunk through the SSE scanning state machine. */
|
|
488
|
+
feed(chunk: Uint8Array): void;
|
|
489
|
+
/** Flush the decoder + trailing unterminated buffer (upstream cleanly done). */
|
|
490
|
+
finish(): void;
|
|
491
|
+
/** Drop every retained frame/item reference without parsing. Idempotent. */
|
|
492
|
+
dispose(): void;
|
|
493
|
+
/** True once a protocol terminal was detected and reported. */
|
|
494
|
+
reported(): boolean;
|
|
495
|
+
/** True once any protocol terminal was parsed, including metadata-only inspectors. */
|
|
496
|
+
terminalSeen(): boolean;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export type SseInspectorHandlers = {
|
|
500
|
+
onTerminal?: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void;
|
|
501
|
+
logCtx?: RequestLogContext;
|
|
502
|
+
onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void;
|
|
503
|
+
onFirstOutput?: () => void;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
type CompletedOutputItem = { item: unknown; sourceBytes: number };
|
|
507
|
+
|
|
508
|
+
function delimiterLengthAt(
|
|
509
|
+
index: number,
|
|
510
|
+
length: number,
|
|
511
|
+
byteAt: (index: number) => number,
|
|
512
|
+
): number | 0 | undefined {
|
|
513
|
+
const first = byteAt(index);
|
|
514
|
+
if (first === 10) {
|
|
515
|
+
if (index + 1 >= length) return undefined;
|
|
516
|
+
const second = byteAt(index + 1);
|
|
517
|
+
if (second === 10) return 2;
|
|
518
|
+
if (second !== 13) return 0;
|
|
519
|
+
if (index + 2 >= length) return undefined;
|
|
520
|
+
return byteAt(index + 2) === 10 ? 3 : 0;
|
|
521
|
+
}
|
|
522
|
+
if (first !== 13) return 0;
|
|
523
|
+
if (index + 1 >= length) return undefined;
|
|
524
|
+
if (byteAt(index + 1) !== 10) return 0;
|
|
525
|
+
if (index + 2 >= length) return undefined;
|
|
526
|
+
const third = byteAt(index + 2);
|
|
527
|
+
if (third === 10) return 3;
|
|
528
|
+
if (third !== 13) return 0;
|
|
529
|
+
if (index + 3 >= length) return undefined;
|
|
530
|
+
return byteAt(index + 3) === 10 ? 4 : 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function joinedBytes(slices: readonly Uint8Array[], byteLength: number): Uint8Array {
|
|
534
|
+
if (slices.length === 1 && slices[0]!.byteLength === byteLength) return slices[0]!;
|
|
535
|
+
const joined = new Uint8Array(byteLength);
|
|
536
|
+
let offset = 0;
|
|
537
|
+
for (const slice of slices) {
|
|
538
|
+
joined.set(slice, offset);
|
|
539
|
+
offset += slice.byteLength;
|
|
540
|
+
}
|
|
541
|
+
return joined;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Per-chunk SSE inspection state machine shared by consumeForInspection,
|
|
546
|
+
* consumeForResponseLogMetadata, and the eager bounded relay (relay-eager.ts).
|
|
547
|
+
*
|
|
548
|
+
* Extraction-fidelity invariants (devlog/_plan/260723_win_mem_safestream/020):
|
|
549
|
+
* - logCtx SSE inspection is gated on !reported; in the metadata configuration
|
|
550
|
+
* (no onTerminal) `reported` stays permanently false, which reproduces the
|
|
551
|
+
* metadata consumer's unconditional inspection through the same gate.
|
|
552
|
+
* - finish() skips the trailing-buffer scan once reported, while per-block
|
|
553
|
+
* onCompletedResponse continues firing after reported — an intentional
|
|
554
|
+
* asymmetry inherited from consumeForInspection.
|
|
555
|
+
* - logCtx.transportPhase/terminalSource are mutated BEFORE onTerminal fires.
|
|
556
|
+
* - Synthetic terminals (incomplete / failed-502) are the CALLER's decision:
|
|
557
|
+
* the caller owns `cancelled` state and reads `reported()` to decide.
|
|
558
|
+
*/
|
|
559
|
+
export function createSseInspector(handlers: SseInspectorHandlers): SseInspector {
|
|
560
|
+
let decoder: TextDecoder | null = new TextDecoder();
|
|
561
|
+
let reported = false;
|
|
562
|
+
let sawTerminal = false;
|
|
563
|
+
let disposed = false;
|
|
564
|
+
let delimiterTail = new Uint8Array(0);
|
|
565
|
+
let candidateSlices: Uint8Array[] = [];
|
|
566
|
+
let candidateBytes = 0;
|
|
567
|
+
let discardingOversizedFrame = false;
|
|
568
|
+
const reportFirstOutput = createFirstOutputReporter(handlers.onFirstOutput);
|
|
569
|
+
// Allocate reconstruction state only for persistence-capable inspectors.
|
|
570
|
+
const completedItemsByOutputIndex = handlers.onCompletedResponse
|
|
571
|
+
? new Map<number, CompletedOutputItem>()
|
|
572
|
+
: null;
|
|
573
|
+
let aggregateItemBytes = 0;
|
|
574
|
+
let reconstructionTainted = false;
|
|
575
|
+
|
|
576
|
+
const clearFrameState = (): void => {
|
|
577
|
+
delimiterTail = new Uint8Array(0);
|
|
578
|
+
candidateSlices = [];
|
|
579
|
+
candidateBytes = 0;
|
|
580
|
+
discardingOversizedFrame = false;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const clearCompletedItems = (): void => {
|
|
584
|
+
completedItemsByOutputIndex?.clear();
|
|
585
|
+
aggregateItemBytes = 0;
|
|
586
|
+
reconstructionTainted = false;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const dispose = (): void => {
|
|
590
|
+
if (disposed) return;
|
|
591
|
+
disposed = true;
|
|
592
|
+
decoder = null;
|
|
593
|
+
clearFrameState();
|
|
594
|
+
clearCompletedItems();
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const retainCandidateSlice = (slice: Uint8Array): void => {
|
|
598
|
+
if (slice.byteLength === 0 || discardingOversizedFrame) return;
|
|
599
|
+
const nextBytes = candidateBytes + slice.byteLength;
|
|
600
|
+
inspectionCounters.frameBufferHighWaterBytes = Math.max(
|
|
601
|
+
inspectionCounters.frameBufferHighWaterBytes,
|
|
602
|
+
Math.min(nextBytes, MAX_INSPECTION_SSE_FRAME_BYTES),
|
|
603
|
+
);
|
|
604
|
+
if (nextBytes > MAX_INSPECTION_SSE_FRAME_BYTES) {
|
|
605
|
+
candidateSlices = [];
|
|
606
|
+
candidateBytes = 0;
|
|
607
|
+
discardingOversizedFrame = true;
|
|
608
|
+
inspectionCounters.frameCapOverflows += 1;
|
|
609
|
+
// The rejected frame may have carried an output item we will never see;
|
|
610
|
+
// any later empty-output terminal must not synthesize a partial replay
|
|
611
|
+
// from the surviving map entries (same taint rule as item eviction).
|
|
612
|
+
reconstructionTainted = true;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// `subarray()` aliases the upstream chunk's backing buffer. Copy only the
|
|
616
|
+
// live candidate bytes so a tiny trailing frame cannot pin a multi-MiB
|
|
617
|
+
// chunk whose preceding frames have already been consumed.
|
|
618
|
+
candidateSlices.push(slice.slice());
|
|
619
|
+
candidateBytes = nextBytes;
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const retainCompletedItem = (index: number, item: unknown, sourceBytes: number): void => {
|
|
623
|
+
const previous = completedItemsByOutputIndex!.get(index);
|
|
624
|
+
if (previous) {
|
|
625
|
+
aggregateItemBytes -= previous.sourceBytes;
|
|
626
|
+
completedItemsByOutputIndex!.delete(index);
|
|
627
|
+
}
|
|
628
|
+
if (sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
|
|
629
|
+
reconstructionTainted = true;
|
|
630
|
+
inspectionCounters.itemCapEvictions += 1;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
completedItemsByOutputIndex!.set(index, { item, sourceBytes });
|
|
634
|
+
aggregateItemBytes += sourceBytes;
|
|
635
|
+
while (completedItemsByOutputIndex!.size > MAX_COMPLETED_OUTPUT_ITEMS
|
|
636
|
+
|| aggregateItemBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
|
|
637
|
+
let highestIndex = -1;
|
|
638
|
+
for (const retainedIndex of completedItemsByOutputIndex!.keys()) {
|
|
639
|
+
if (retainedIndex > highestIndex) highestIndex = retainedIndex;
|
|
640
|
+
}
|
|
641
|
+
const evicted = completedItemsByOutputIndex!.get(highestIndex);
|
|
642
|
+
if (!evicted) break;
|
|
643
|
+
completedItemsByOutputIndex!.delete(highestIndex);
|
|
644
|
+
aggregateItemBytes -= evicted.sourceBytes;
|
|
645
|
+
reconstructionTainted = true;
|
|
646
|
+
inspectionCounters.itemCapEvictions += 1;
|
|
647
|
+
}
|
|
648
|
+
inspectionCounters.completedItemsMaxCount = Math.max(
|
|
649
|
+
inspectionCounters.completedItemsMaxCount,
|
|
650
|
+
completedItemsByOutputIndex!.size,
|
|
651
|
+
);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const scanPayload = (payload: string | null, sourceBytes: number): void => {
|
|
655
|
+
if (!payload) return;
|
|
656
|
+
let parsed: unknown | undefined;
|
|
657
|
+
if (payload !== "[DONE]") {
|
|
658
|
+
try {
|
|
659
|
+
parsed = JSON.parse(payload);
|
|
660
|
+
} catch {
|
|
661
|
+
/* malformed SSE payloads remain best-effort/no-throw */
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (!reported && handlers.logCtx) {
|
|
665
|
+
inspectResponseLogSsePayloadParsed(handlers.logCtx, payload, parsed);
|
|
666
|
+
}
|
|
667
|
+
reportFirstOutput.parsed(parsed);
|
|
668
|
+
const status = terminalStatusFromParsed(parsed);
|
|
669
|
+
if (status) sawTerminal = true;
|
|
670
|
+
if (!reported && handlers.onTerminal && status) {
|
|
671
|
+
try {
|
|
672
|
+
reported = true;
|
|
673
|
+
if (handlers.logCtx) {
|
|
674
|
+
handlers.logCtx.transportPhase = "terminal_sse";
|
|
675
|
+
handlers.logCtx.terminalSource = "upstream";
|
|
676
|
+
}
|
|
677
|
+
handlers.onTerminal(status);
|
|
678
|
+
} finally {
|
|
679
|
+
if (status === "failed" || status === "incomplete") clearCompletedItems();
|
|
680
|
+
}
|
|
681
|
+
} else if (status === "failed" || status === "incomplete") {
|
|
682
|
+
clearCompletedItems();
|
|
683
|
+
}
|
|
684
|
+
if (handlers.onCompletedResponse) {
|
|
685
|
+
type ParsedSseEvent = { type?: unknown; output_index?: unknown; item?: unknown; response?: unknown };
|
|
686
|
+
const parsedEvent = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
687
|
+
? parsed as ParsedSseEvent
|
|
688
|
+
: null;
|
|
689
|
+
const doneItem = parsedEvent?.type === "response.output_item.done" ? parsedEvent.item : undefined;
|
|
690
|
+
if (parsedEvent
|
|
691
|
+
&& doneItem !== undefined
|
|
692
|
+
&& Number.isInteger(parsedEvent.output_index)
|
|
693
|
+
&& (parsedEvent.output_index as number) >= 0
|
|
694
|
+
&& typeof doneItem === "object"
|
|
695
|
+
&& doneItem !== null
|
|
696
|
+
&& !Array.isArray(doneItem)
|
|
697
|
+
&& typeof (doneItem as { type?: unknown }).type === "string") {
|
|
698
|
+
retainCompletedItem(parsedEvent.output_index as number, doneItem, sourceBytes);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
let response = completedResponseFromParsedEvent(parsedEvent);
|
|
702
|
+
if (response) {
|
|
703
|
+
// Authoritative output is a NON-EMPTY ARRAY only. Anything else
|
|
704
|
+
// (missing, null, scalar, object) keeps the historical backfill
|
|
705
|
+
// behavior so a malformed terminal cannot reach rememberResponseState
|
|
706
|
+
// and destroy continuation state (review C1-2).
|
|
707
|
+
const hasAuthoritativeOutput = Array.isArray(response.output)
|
|
708
|
+
&& response.output.length > 0;
|
|
709
|
+
if (!hasAuthoritativeOutput && reconstructionTainted) {
|
|
710
|
+
clearCompletedItems();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!hasAuthoritativeOutput && completedItemsByOutputIndex!.size > 0) {
|
|
714
|
+
response = {
|
|
715
|
+
...response,
|
|
716
|
+
output: [...completedItemsByOutputIndex!.entries()]
|
|
717
|
+
.sort(([left], [right]) => left - right)
|
|
718
|
+
.map(([, retained]) => retained.item),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
handlers.onCompletedResponse(response);
|
|
723
|
+
} finally {
|
|
724
|
+
clearCompletedItems();
|
|
725
|
+
}
|
|
726
|
+
} else if (parsedEvent?.type === "response.completed") {
|
|
727
|
+
clearCompletedItems();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const completeCandidate = (): void => {
|
|
733
|
+
if (discardingOversizedFrame) {
|
|
734
|
+
discardingOversizedFrame = false;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const sourceBytes = candidateBytes;
|
|
738
|
+
const frame = joinedBytes(candidateSlices, sourceBytes);
|
|
739
|
+
candidateSlices = [];
|
|
740
|
+
candidateBytes = 0;
|
|
741
|
+
if (reported && !handlers.onCompletedResponse) return;
|
|
742
|
+
const decoded = decoder!.decode(frame);
|
|
743
|
+
scanPayload(sseDataPayload(decoded), sourceBytes);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const scanChunk = (chunk: Uint8Array): void => {
|
|
747
|
+
const previousTail = delimiterTail;
|
|
748
|
+
delimiterTail = new Uint8Array(0);
|
|
749
|
+
const tailLength = previousTail.byteLength;
|
|
750
|
+
const totalLength = tailLength + chunk.byteLength;
|
|
751
|
+
const byteAt = (index: number): number => index < tailLength
|
|
752
|
+
? previousTail[index]!
|
|
753
|
+
: chunk[index - tailLength]!;
|
|
754
|
+
const retainRange = (start: number, end: number): void => {
|
|
755
|
+
if (end <= start || discardingOversizedFrame) return;
|
|
756
|
+
if (start < tailLength) {
|
|
757
|
+
retainCandidateSlice(previousTail.subarray(start, Math.min(end, tailLength)));
|
|
758
|
+
}
|
|
759
|
+
if (end > tailLength) {
|
|
760
|
+
retainCandidateSlice(chunk.subarray(Math.max(0, start - tailLength), end - tailLength));
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
let index = 0;
|
|
764
|
+
let retainedThrough = 0;
|
|
765
|
+
while (index < totalLength) {
|
|
766
|
+
const delimiterLength = delimiterLengthAt(index, totalLength, byteAt);
|
|
767
|
+
if (delimiterLength === undefined) break;
|
|
768
|
+
if (delimiterLength > 0) {
|
|
769
|
+
retainRange(retainedThrough, index);
|
|
770
|
+
completeCandidate();
|
|
771
|
+
index += delimiterLength;
|
|
772
|
+
retainedThrough = index;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
index += 1;
|
|
776
|
+
}
|
|
777
|
+
retainRange(retainedThrough, index);
|
|
778
|
+
if (index < totalLength) {
|
|
779
|
+
delimiterTail = new Uint8Array(totalLength - index);
|
|
780
|
+
for (let offset = 0; offset < delimiterTail.byteLength; offset += 1) {
|
|
781
|
+
delimiterTail[offset] = byteAt(index + offset);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
feed(chunk) {
|
|
788
|
+
if (!disposed) scanChunk(chunk);
|
|
789
|
+
},
|
|
790
|
+
finish() {
|
|
791
|
+
if (disposed) return;
|
|
792
|
+
try {
|
|
793
|
+
retainCandidateSlice(delimiterTail);
|
|
794
|
+
delimiterTail = new Uint8Array(0);
|
|
795
|
+
if (!discardingOversizedFrame && candidateBytes > 0 && !reported) {
|
|
796
|
+
const sourceBytes = candidateBytes;
|
|
797
|
+
const decoded = decoder!.decode(joinedBytes(candidateSlices, sourceBytes));
|
|
798
|
+
scanPayload(decoded.trim() ? sseDataPayload(decoded) : null, sourceBytes);
|
|
799
|
+
}
|
|
800
|
+
} finally {
|
|
801
|
+
clearFrameState();
|
|
802
|
+
clearCompletedItems();
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
dispose,
|
|
806
|
+
reported: () => reported,
|
|
807
|
+
terminalSeen: () => sawTerminal,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export type InspectionDrainBounds = { ms: number; bytes: number };
|
|
812
|
+
|
|
813
|
+
export type InspectionConsumerOptions = {
|
|
814
|
+
clientGoneSignal?: AbortSignal;
|
|
815
|
+
drainBounds?: Partial<InspectionDrainBounds>;
|
|
816
|
+
upstream?: AbortController;
|
|
817
|
+
now?: () => number;
|
|
818
|
+
/** Test seam for proving both public consumers dispose their owned inspector. */
|
|
819
|
+
inspectorFactory?: (handlers: SseInspectorHandlers) => SseInspector;
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const DEFAULT_INSPECTION_DRAIN_MS = 15_000;
|
|
823
|
+
const DEFAULT_INSPECTION_DRAIN_BYTES = 32 * 1024 * 1024;
|
|
824
|
+
type InspectionPumpOptions = InspectionConsumerOptions & {
|
|
825
|
+
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
826
|
+
inspector: SseInspector;
|
|
827
|
+
signal?: AbortSignal;
|
|
828
|
+
onDone?: () => void;
|
|
829
|
+
onCancel?: () => void;
|
|
830
|
+
onCleanEof?: () => void;
|
|
831
|
+
onReadError?: () => void;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
function startBoundedInspectionPump(options: InspectionPumpOptions): void {
|
|
835
|
+
const { reader, inspector, signal, clientGoneSignal } = options;
|
|
836
|
+
let cancelled = false;
|
|
837
|
+
let clientGone = false;
|
|
838
|
+
let clientGoneReason: unknown;
|
|
839
|
+
let drainedBytes = 0;
|
|
840
|
+
let drainDeadline = Number.POSITIVE_INFINITY;
|
|
841
|
+
let drainTimer: ReturnType<typeof setTimeout> | undefined;
|
|
842
|
+
let drainStopped = false;
|
|
843
|
+
const drainMs = options.drainBounds?.ms ?? DEFAULT_INSPECTION_DRAIN_MS;
|
|
844
|
+
const drainBytes = options.drainBounds?.bytes ?? DEFAULT_INSPECTION_DRAIN_BYTES;
|
|
845
|
+
const now = options.now ?? Date.now;
|
|
846
|
+
let cancelFired = false;
|
|
847
|
+
const fireCancel = () => {
|
|
848
|
+
if (cancelFired) return;
|
|
849
|
+
cancelFired = true;
|
|
850
|
+
options.onCancel?.();
|
|
851
|
+
};
|
|
852
|
+
const markClientGone = () => {
|
|
853
|
+
if (clientGone || cancelled) return;
|
|
854
|
+
clientGone = true;
|
|
855
|
+
clientGoneReason = clientGoneSignal?.reason;
|
|
856
|
+
drainDeadline = now() + drainMs;
|
|
857
|
+
if (inspector.terminalSeen() || drainMs <= 0 || drainBytes <= 0) {
|
|
858
|
+
stopDrain();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
// Do not unref: on Bun/Windows a pending `reader.read()` can be the only
|
|
862
|
+
// wake source; an unref'd timer may never run, so a silent post-cancel
|
|
863
|
+
// drain (time bound, no bytes) hangs the suite until the job timeout.
|
|
864
|
+
drainTimer = setTimeout(stopDrain, drainMs);
|
|
865
|
+
};
|
|
866
|
+
// Ends the bounded drain by cancelling the reader: the pending read settles
|
|
867
|
+
// and the pump loop observes `drainStopped`. Deliberately NOT a shared
|
|
868
|
+
// Promise.race companion — racing every read against one pending promise
|
|
869
|
+
// retains O(chunk-count) reactions on long streams (review C1-1), the exact
|
|
870
|
+
// retention class this phase removes.
|
|
871
|
+
const stopDrain = () => {
|
|
872
|
+
if (drainStopped || cancelled) return;
|
|
873
|
+
drainStopped = true;
|
|
874
|
+
reader.cancel(clientGoneReason).catch(() => {});
|
|
875
|
+
};
|
|
876
|
+
const abortImmediately = () => {
|
|
877
|
+
if (cancelled) return;
|
|
878
|
+
cancelled = true;
|
|
879
|
+
reader.cancel(signal?.reason).catch(() => {});
|
|
880
|
+
fireCancel();
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
if (signal?.aborted) {
|
|
884
|
+
cancelled = true;
|
|
885
|
+
reader.cancel(signal.reason).catch(() => {});
|
|
886
|
+
inspector.dispose();
|
|
887
|
+
fireCancel();
|
|
888
|
+
options.onDone?.();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
signal?.addEventListener("abort", abortImmediately, { once: true });
|
|
892
|
+
clientGoneSignal?.addEventListener("abort", markClientGone, { once: true });
|
|
893
|
+
if (clientGoneSignal?.aborted) markClientGone();
|
|
894
|
+
|
|
895
|
+
const pump = async () => {
|
|
896
|
+
let clientGoneWithoutTerminal = false;
|
|
897
|
+
let boundEndedDrain = false;
|
|
898
|
+
try {
|
|
899
|
+
for (;;) {
|
|
900
|
+
const { done, value } = await reader.read();
|
|
901
|
+
if (drainStopped) {
|
|
902
|
+
// stopDrain() cancelled the reader; the settled read is the wake-up.
|
|
903
|
+
clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
904
|
+
boundEndedDrain = clientGoneWithoutTerminal;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (done) {
|
|
908
|
+
inspector.finish();
|
|
909
|
+
if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
910
|
+
else if (!cancelled) options.onCleanEof?.();
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
if (!clientGone) {
|
|
914
|
+
inspector.feed(value);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (now() >= drainDeadline) {
|
|
918
|
+
clientGoneWithoutTerminal = true;
|
|
919
|
+
boundEndedDrain = true;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
const remainingBytes = Math.max(0, drainBytes - drainedBytes);
|
|
923
|
+
const inspectedValue = value.byteLength > remainingBytes
|
|
924
|
+
? value.subarray(0, remainingBytes)
|
|
925
|
+
: value;
|
|
926
|
+
if (inspectedValue.byteLength > 0) inspector.feed(inspectedValue);
|
|
927
|
+
drainedBytes += inspectedValue.byteLength;
|
|
928
|
+
if (inspector.terminalSeen()) break;
|
|
929
|
+
if (value.byteLength > remainingBytes
|
|
930
|
+
|| drainedBytes >= drainBytes
|
|
931
|
+
|| now() >= drainDeadline) {
|
|
932
|
+
clientGoneWithoutTerminal = true;
|
|
933
|
+
boundEndedDrain = true;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
} catch {
|
|
938
|
+
if (clientGone) clientGoneWithoutTerminal = !inspector.terminalSeen();
|
|
939
|
+
else if (!cancelled) options.onReadError?.();
|
|
940
|
+
} finally {
|
|
941
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
942
|
+
signal?.removeEventListener("abort", abortImmediately);
|
|
943
|
+
clientGoneSignal?.removeEventListener("abort", markClientGone);
|
|
944
|
+
if (clientGone) {
|
|
945
|
+
if (boundEndedDrain) inspectionCounters.postCancelDrainStops += 1;
|
|
946
|
+
if (clientGoneWithoutTerminal) fireCancel();
|
|
947
|
+
options.upstream?.abort(clientGoneReason);
|
|
948
|
+
reader.cancel(clientGoneReason).catch(() => {});
|
|
949
|
+
}
|
|
950
|
+
inspector.dispose();
|
|
951
|
+
options.onDone?.();
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
void pump();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
export function consumeForInspection(
|
|
958
|
+
body: ReadableStream<Uint8Array>,
|
|
959
|
+
onTerminal: (status: ResponsesTerminalStatus, httpStatusOverride?: number) => void,
|
|
960
|
+
signal?: AbortSignal,
|
|
961
|
+
onDone?: () => void,
|
|
962
|
+
logCtx?: RequestLogContext,
|
|
963
|
+
onCancel?: () => void,
|
|
964
|
+
onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void,
|
|
965
|
+
onFirstOutput?: () => void,
|
|
966
|
+
options?: InspectionConsumerOptions,
|
|
967
|
+
): void {
|
|
968
|
+
const reader = body.getReader();
|
|
969
|
+
const inspector = (options?.inspectorFactory ?? createSseInspector)({
|
|
970
|
+
onTerminal,
|
|
971
|
+
logCtx,
|
|
972
|
+
onCompletedResponse,
|
|
973
|
+
onFirstOutput,
|
|
974
|
+
});
|
|
975
|
+
startBoundedInspectionPump({
|
|
976
|
+
...options,
|
|
977
|
+
reader,
|
|
978
|
+
inspector,
|
|
979
|
+
signal,
|
|
980
|
+
onDone,
|
|
981
|
+
onCancel,
|
|
982
|
+
onCleanEof: () => {
|
|
983
|
+
if (!inspector.reported()) {
|
|
984
|
+
if (logCtx) logCtx.terminalSource = "synthetic";
|
|
985
|
+
onTerminal("incomplete");
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
onReadError: () => {
|
|
989
|
+
// Upstream read failure after HTTP 200 (mid-stream socket reset) is not a
|
|
990
|
+
// protocol `response.incomplete` terminal. Report a synthetic 502 so account
|
|
991
|
+
// health treats it as transient; abort-driven client cancellation still wins.
|
|
992
|
+
if (!inspector.reported()) {
|
|
993
|
+
if (logCtx) {
|
|
994
|
+
logCtx.transportPhase = "mid_stream";
|
|
995
|
+
logCtx.terminalSource = "synthetic";
|
|
996
|
+
}
|
|
997
|
+
onTerminal("failed", 502);
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function consumeForResponseLogMetadata(
|
|
1004
|
+
body: ReadableStream<Uint8Array>,
|
|
1005
|
+
logCtx: RequestLogContext,
|
|
1006
|
+
signal?: AbortSignal,
|
|
1007
|
+
onDone?: () => void,
|
|
1008
|
+
onCompletedResponse?: (response: { id?: unknown; output?: unknown; status?: unknown }) => void,
|
|
1009
|
+
onFirstOutput?: () => void,
|
|
1010
|
+
options?: InspectionConsumerOptions,
|
|
1011
|
+
): void {
|
|
1012
|
+
const reader = body.getReader();
|
|
1013
|
+
// No onTerminal → the inspector's `reported` gate stays permanently false,
|
|
1014
|
+
// reproducing this consumer's unconditional logCtx inspection.
|
|
1015
|
+
const inspector = (options?.inspectorFactory ?? createSseInspector)({
|
|
1016
|
+
logCtx,
|
|
1017
|
+
onCompletedResponse,
|
|
1018
|
+
onFirstOutput,
|
|
1019
|
+
});
|
|
1020
|
+
startBoundedInspectionPump({ ...options, reader, inspector, signal, onDone });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Bun's fetch auto-decompresses the response body but leaves the upstream `content-encoding`
|
|
1025
|
+
* (and a now-stale `content-length`) on `response.headers`. Relaying those with the already-decoded
|
|
1026
|
+
* body makes the caller (Codex) double-decode / truncate → "stream error" on every gpt passthrough.
|
|
1027
|
+
* Drop encoding + hop-by-hop headers; relay everything else (content-type, etc.) verbatim.
|
|
1028
|
+
*/
|
|
1029
|
+
export function sanitizePassthroughHeaders(upstream: Headers): Headers {
|
|
1030
|
+
const DROP = new Set([
|
|
1031
|
+
"content-encoding",
|
|
1032
|
+
"content-length",
|
|
1033
|
+
"transfer-encoding",
|
|
1034
|
+
"connection",
|
|
1035
|
+
"keep-alive",
|
|
1036
|
+
"proxy-authenticate",
|
|
1037
|
+
"proxy-authorization",
|
|
1038
|
+
"set-cookie",
|
|
1039
|
+
"set-cookie2",
|
|
1040
|
+
"te",
|
|
1041
|
+
"trailer",
|
|
1042
|
+
"upgrade",
|
|
1043
|
+
]);
|
|
1044
|
+
const out = new Headers();
|
|
1045
|
+
upstream.forEach((value, key) => {
|
|
1046
|
+
if (!DROP.has(key.toLowerCase())) out.set(key, value);
|
|
1047
|
+
});
|
|
1048
|
+
return out;
|
|
1049
|
+
}
|