@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
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,1619 @@
|
|
|
1
|
+
import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUsage } from "./types";
|
|
2
|
+
import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
|
|
3
|
+
import { encodeCompactionSummary } from "./responses/compaction";
|
|
4
|
+
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
5
|
+
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
6
|
+
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
7
|
+
import {
|
|
8
|
+
isTranslatorBudgetExceededError,
|
|
9
|
+
releaseTranslatedEvent,
|
|
10
|
+
type TranslatorBudget,
|
|
11
|
+
type TranslatorBufferKind,
|
|
12
|
+
} from "./lib/translator-budget";
|
|
13
|
+
|
|
14
|
+
function uuid(): string {
|
|
15
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
19
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
23
|
+
// input_tokens_details / output_tokens_details are ALWAYS emitted (zero defaults):
|
|
24
|
+
// strict Responses clients deserialize them as required fields — grok-build's pinned
|
|
25
|
+
// async-openai fork (rev 95b52ebd, response_usage.rs) has non-Option InputTokenDetails/
|
|
26
|
+
// OutputTokenDetails, so omitting them turns a successful turn into a hard exit after
|
|
27
|
+
// response.completed ("missing field `input_tokens_details`", verified live 2026-07-23).
|
|
28
|
+
if (!usage) {
|
|
29
|
+
return {
|
|
30
|
+
input_tokens: 0,
|
|
31
|
+
output_tokens: 0,
|
|
32
|
+
total_tokens: 0,
|
|
33
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
34
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// inputTokens is already inclusive of cache read/write (types.ts convention). Stateful
|
|
38
|
+
// providers may report an absolute active-context checkpoint separately from their
|
|
39
|
+
// per-attempt usage. Split that checkpoint into input + output without adding output twice.
|
|
40
|
+
const inputTokens = usage.contextTotalTokens !== undefined
|
|
41
|
+
? Math.max(0, usage.contextTotalTokens - usage.outputTokens)
|
|
42
|
+
: usage.inputTokens;
|
|
43
|
+
const out: Record<string, unknown> = {
|
|
44
|
+
input_tokens: inputTokens,
|
|
45
|
+
output_tokens: usage.outputTokens,
|
|
46
|
+
total_tokens: usage.contextTotalTokens !== undefined
|
|
47
|
+
? usage.contextTotalTokens
|
|
48
|
+
: usageDisplayTotalTokens(usage) ?? inputTokens + usage.outputTokens,
|
|
49
|
+
};
|
|
50
|
+
// cached_tokens carries cache READS only, matching OpenAI semantics, and is always present
|
|
51
|
+
// (zero default) for strict clients. Clamp to inputTokens so a provider's absolute
|
|
52
|
+
// checkpoint can never report more cache reads than input.
|
|
53
|
+
const inputDetails: Record<string, number> = {
|
|
54
|
+
cached_tokens: Math.min(usage.cachedInputTokens ?? 0, inputTokens),
|
|
55
|
+
};
|
|
56
|
+
if (usage.cacheCreationInputTokens !== undefined) {
|
|
57
|
+
const cacheRead = inputDetails.cached_tokens ?? 0;
|
|
58
|
+
inputDetails.cache_write_tokens = Math.min(
|
|
59
|
+
usage.cacheCreationInputTokens,
|
|
60
|
+
Math.max(0, inputTokens - cacheRead),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
out.input_tokens_details = inputDetails;
|
|
64
|
+
out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function responseError(status: number, type: string, message: string): OcxErrorPayload {
|
|
69
|
+
return classifyError(status, type, message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether assembled function-call arguments are usable JSON.
|
|
74
|
+
* An empty buffer is valid (no-arg tools send no deltas). Non-empty must parse —
|
|
75
|
+
* once fragments have been streamed to the client they cannot be repaired the way
|
|
76
|
+
* non-stream adapters degrade a bad payload to `{}`.
|
|
77
|
+
*/
|
|
78
|
+
function toolCallArgumentsUsable(args: string): boolean {
|
|
79
|
+
const trimmed = args.trim();
|
|
80
|
+
if (!trimmed) return true;
|
|
81
|
+
try {
|
|
82
|
+
JSON.parse(trimmed);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
|
|
90
|
+
if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
|
|
91
|
+
return adapterFailureFromMessage(event.message);
|
|
92
|
+
}
|
|
93
|
+
const fallback = adapterFailureFromMessage(event.message);
|
|
94
|
+
let httpStatus = event.status ?? fallback.httpStatus;
|
|
95
|
+
const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
|
|
96
|
+
if (event.errorType !== undefined) error.type = event.errorType;
|
|
97
|
+
if (event.code !== undefined) error.code = event.code;
|
|
98
|
+
// Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
|
|
99
|
+
if (isCyberPolicyCode(error.code) || isCyberPolicyCode(event.code)) {
|
|
100
|
+
error.code = CYBER_POLICY_ERROR_CODE;
|
|
101
|
+
error.type = "invalid_request_error";
|
|
102
|
+
httpStatus = 400;
|
|
103
|
+
}
|
|
104
|
+
return { httpStatus, error };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { adapterFailureFromMessage } from "./lib/errors";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the native `WebSearchAction::Search` payload from the queries that ran. codex-rs prefers a
|
|
111
|
+
* non-empty `query` over `queries` for the cell label, and only renders "<first> ..." when `query`
|
|
112
|
+
* is absent and `queries.len() > 1`. So a single query → `{ query }`; multiple → `{ queries }` with
|
|
113
|
+
* no singular `query`, so Codex shows the native plural ellipsis. Empty → `{ query: "" }`.
|
|
114
|
+
*/
|
|
115
|
+
function webSearchAction(queries: string[]): Record<string, unknown> {
|
|
116
|
+
if (queries.length <= 1) return { type: "search", query: queries[0] ?? "" };
|
|
117
|
+
return { type: "search", queries };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface OutputItem {
|
|
121
|
+
type: string;
|
|
122
|
+
id: string;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
|
|
127
|
+
|
|
128
|
+
export function bridgeToResponsesSSE(
|
|
129
|
+
events: AsyncIterable<AdapterEvent>,
|
|
130
|
+
modelId: string,
|
|
131
|
+
toolNsMap?: Map<string, { namespace: string; name: string }>,
|
|
132
|
+
freeformToolNames?: Set<string>,
|
|
133
|
+
toolSearchToolNames?: Set<string>,
|
|
134
|
+
onCancel?: () => void,
|
|
135
|
+
heartbeatMs = 2_000,
|
|
136
|
+
options?: {
|
|
137
|
+
responseId?: string;
|
|
138
|
+
stallTimeoutSec?: number;
|
|
139
|
+
hideThinkingSummary?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Remote compaction v2 turn: accumulate all assistant text and, on done, emit ONE synthetic
|
|
142
|
+
* `{type:"compaction", encrypted_content:"ocx1:"+base64(text)}` output item before
|
|
143
|
+
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
144
|
+
*/
|
|
145
|
+
compaction?: boolean;
|
|
146
|
+
/** One-shot: first non-empty text/thinking/raw-reasoning delta observed (WP4 TTFT). */
|
|
147
|
+
onFirstOutput?: () => void;
|
|
148
|
+
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
149
|
+
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
150
|
+
/**
|
|
151
|
+
* Raw adapter-reported usage at the terminal event, BEFORE wire normalization.
|
|
152
|
+
* responsesUsage() always emits token-detail objects with zero defaults for strict
|
|
153
|
+
* clients (grok-build), which makes the wire unusable as a provenance source: the
|
|
154
|
+
* request log must not read synthetic zeros as measured cache/reasoning numbers
|
|
155
|
+
* (cache_detail_missing would be silently suppressed). Callers set logCtx.usage
|
|
156
|
+
* from this callback instead of re-parsing the bridged SSE.
|
|
157
|
+
*/
|
|
158
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
159
|
+
translatorBudget?: TranslatorBudget;
|
|
160
|
+
/**
|
|
161
|
+
* Test seam for the wire/stall beat loop. Production omits this and uses the
|
|
162
|
+
* global timers; injecting here must not change scheduling semantics.
|
|
163
|
+
*/
|
|
164
|
+
timers?: {
|
|
165
|
+
setInterval: (handler: () => void, ms: number) => unknown;
|
|
166
|
+
clearInterval: (id: unknown) => void;
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
): ReadableStream<Uint8Array> {
|
|
170
|
+
const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
|
|
171
|
+
const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
|
|
172
|
+
// Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
|
|
173
|
+
// function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
|
|
174
|
+
const freeformInput = (args: string): string => {
|
|
175
|
+
try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
|
|
176
|
+
return args;
|
|
177
|
+
};
|
|
178
|
+
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
179
|
+
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
180
|
+
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
181
|
+
// buffers get their string value progressively unescaped; anything else streams raw.
|
|
182
|
+
const FREEFORM_WRAP_PREFIX = '{"input":"';
|
|
183
|
+
const freeformPartialInput = (args: string): string => {
|
|
184
|
+
if (!args.startsWith(FREEFORM_WRAP_PREFIX)) return args;
|
|
185
|
+
const body = args.slice(FREEFORM_WRAP_PREFIX.length);
|
|
186
|
+
let out = "";
|
|
187
|
+
for (let i = 0; i < body.length; i++) {
|
|
188
|
+
const c = body[i];
|
|
189
|
+
if (c === '"') break; // unescaped closing quote: value complete
|
|
190
|
+
if (c === "\\") {
|
|
191
|
+
const n = body[i + 1];
|
|
192
|
+
if (n === undefined) break; // escape split across chunks: wait for more
|
|
193
|
+
i++;
|
|
194
|
+
if (n === "n") out += "\n";
|
|
195
|
+
else if (n === "t") out += "\t";
|
|
196
|
+
else if (n === "r") out += "\r";
|
|
197
|
+
else if (n === "u") {
|
|
198
|
+
const hex = body.slice(i + 1, i + 5);
|
|
199
|
+
if (hex.length === 4 && /^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 4; }
|
|
200
|
+
else break; // incomplete \uXXXX: wait for more
|
|
201
|
+
} else out += n; // \" \\ \/ etc.
|
|
202
|
+
} else out += c;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
// tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
|
|
207
|
+
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
208
|
+
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
209
|
+
};
|
|
210
|
+
const encoder = new TextEncoder();
|
|
211
|
+
const budget = options?.translatorBudget;
|
|
212
|
+
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
213
|
+
const appendString = (
|
|
214
|
+
previous: string,
|
|
215
|
+
previousBytes: number,
|
|
216
|
+
fragment: string,
|
|
217
|
+
kind: TranslatorBufferKind,
|
|
218
|
+
callId?: string,
|
|
219
|
+
): { value: string; bytes: number } => {
|
|
220
|
+
const fragmentBytes = bytesOf(fragment);
|
|
221
|
+
const nextBytes = previousBytes + fragmentBytes;
|
|
222
|
+
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
223
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
224
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
225
|
+
try {
|
|
226
|
+
const value = previous + fragment;
|
|
227
|
+
reservation.commitRetained();
|
|
228
|
+
budget.releaseRetained(previousBytes, scope);
|
|
229
|
+
return { value, bytes: nextBytes };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
reservation.release();
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const replaceRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
|
|
236
|
+
const nextBytes = bytesOf(next);
|
|
237
|
+
if (!budget) return nextBytes;
|
|
238
|
+
const reservation = budget.reserveTransient(nextBytes, { kind });
|
|
239
|
+
reservation.commitRetained();
|
|
240
|
+
budget.releaseRetained(previousBytes, { kind });
|
|
241
|
+
return nextBytes;
|
|
242
|
+
};
|
|
243
|
+
const chargeValue = (value: unknown, kind: TranslatorBufferKind): number => {
|
|
244
|
+
const bytes = bytesOf(JSON.stringify(value));
|
|
245
|
+
budget?.chargeRetained(bytes, { kind });
|
|
246
|
+
return bytes;
|
|
247
|
+
};
|
|
248
|
+
const responseId = options?.responseId ?? `resp_${uuid()}`;
|
|
249
|
+
let seq = 0;
|
|
250
|
+
// Set once the client is gone (cancel) or an enqueue throws on a torn-down controller, so we
|
|
251
|
+
// never enqueue again and never throw a second time inside start() — the RC2 double-throw that
|
|
252
|
+
// otherwise surfaced as proxy-side stream noise on every client disconnect.
|
|
253
|
+
let closed = false;
|
|
254
|
+
let clientCancelled = false;
|
|
255
|
+
let terminalReported = false;
|
|
256
|
+
const reportTerminal = (status: ResponsesTerminalStatus) => {
|
|
257
|
+
if (terminalReported || clientCancelled || closed) return;
|
|
258
|
+
terminalReported = true;
|
|
259
|
+
try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
|
|
260
|
+
};
|
|
261
|
+
// RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
|
|
262
|
+
// eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
|
|
263
|
+
// (responses.rs `_ => Ok(None)`). Emit a parser-ignored `response.heartbeat` whenever the
|
|
264
|
+
// *wire* has been silent, even if invisible adapter heartbeats are still flowing (web-search
|
|
265
|
+
// buffering + raw-byte progress). Upstream activity only resets the stall watchdog.
|
|
266
|
+
let upstreamActivity = false;
|
|
267
|
+
let wireActivity = false;
|
|
268
|
+
let beat: unknown;
|
|
269
|
+
let controller: ReadableStreamDefaultController<Uint8Array>;
|
|
270
|
+
let emittedFrames = 0;
|
|
271
|
+
let gated = false;
|
|
272
|
+
let stepping = false;
|
|
273
|
+
let terminateForTranslatorOverflow: ((error: unknown) => void) | undefined;
|
|
274
|
+
const emit = (name: string, data: Record<string, unknown>) => {
|
|
275
|
+
if (closed) return;
|
|
276
|
+
wireActivity = true;
|
|
277
|
+
try {
|
|
278
|
+
const frameText = sseEvent(name, { type: name, sequence_number: seq++, ...data });
|
|
279
|
+
const frameBytes = bytesOf(frameText);
|
|
280
|
+
const reservation = budget?.reserveTransient(frameBytes, { kind: "live_transient" });
|
|
281
|
+
const frame = encoder.encode(frameText);
|
|
282
|
+
reservation?.commitRetained();
|
|
283
|
+
controller.enqueue(frame);
|
|
284
|
+
budget?.releaseRetained(frameBytes, { kind: "live_transient" });
|
|
285
|
+
emittedFrames++;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
288
|
+
terminateForTranslatorOverflow?.(error);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
closed = true;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const emitDone = () => {
|
|
295
|
+
if (closed) return;
|
|
296
|
+
try {
|
|
297
|
+
const done = "data: [DONE]\n\n";
|
|
298
|
+
const doneBytes = bytesOf(done);
|
|
299
|
+
const reservation = budget?.reserveTransient(doneBytes, { kind: "live_transient" });
|
|
300
|
+
const frame = encoder.encode(done);
|
|
301
|
+
reservation?.commitRetained();
|
|
302
|
+
controller.enqueue(frame);
|
|
303
|
+
budget?.releaseRetained(doneBytes, { kind: "live_transient" });
|
|
304
|
+
emittedFrames++;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
307
|
+
terminateForTranslatorOverflow?.(error);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
closed = true;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
315
|
+
let outputIndex = 0;
|
|
316
|
+
const finishedItems: OutputItem[] = [];
|
|
317
|
+
const retainFinishedItem = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
|
|
318
|
+
const itemBytes = bytesOf(JSON.stringify(item));
|
|
319
|
+
const reservation = budget?.reserveTransient(itemBytes, { kind });
|
|
320
|
+
finishedItems.push(item);
|
|
321
|
+
reservation?.commitRetained();
|
|
322
|
+
if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const responseSnapshot = (status: string, output: OutputItem[], endTurn?: boolean) => ({
|
|
326
|
+
id: responseId, object: "response", created_at: createdAt,
|
|
327
|
+
status, model: modelId, output, usage: null,
|
|
328
|
+
...(endTurn !== undefined ? { end_turn: endTurn } : {}),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
|
|
332
|
+
let stallTicks = 0;
|
|
333
|
+
const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
|
|
334
|
+
const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
|
|
335
|
+
|
|
336
|
+
let currentMsg: { itemId: string; outputIndex: number; text: string; textBytes: number; phase?: OcxMessagePhase } | null = null;
|
|
337
|
+
let currentReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
|
|
338
|
+
let currentRawReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
|
|
339
|
+
// Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
|
|
340
|
+
// block; redacted blocks are opaque payloads replayed verbatim. Attached to the reasoning
|
|
341
|
+
// item as an ocxr1 encrypted_content envelope on close. hiddenThinkingText collects the
|
|
342
|
+
// suppressed text under hideThinkingSummary so the signed text still round-trips.
|
|
343
|
+
let pendingSignature: string | undefined;
|
|
344
|
+
let pendingSignatureBytes = 0;
|
|
345
|
+
let pendingRedacted: string[] = [];
|
|
346
|
+
let hiddenThinkingText = "";
|
|
347
|
+
let hiddenThinkingBytes = 0;
|
|
348
|
+
const takeReasoningEnvelope = (hiddenText?: string): string | undefined => {
|
|
349
|
+
if (!pendingSignature && pendingRedacted.length === 0) return undefined;
|
|
350
|
+
const envelope: ReasoningEnvelope = {};
|
|
351
|
+
if (pendingSignature) envelope.sig = pendingSignature;
|
|
352
|
+
if (pendingRedacted.length > 0) envelope.red = pendingRedacted;
|
|
353
|
+
if (hiddenText) envelope.txt = hiddenText;
|
|
354
|
+
const previousBytes = pendingSignatureBytes
|
|
355
|
+
+ pendingRedacted.reduce((sum, value) => sum + bytesOf(value), 0)
|
|
356
|
+
+ (hiddenText ? hiddenThinkingBytes : 0);
|
|
357
|
+
const encoded = encodeReasoningEnvelope(envelope);
|
|
358
|
+
const reservation = budget?.reserveTransient(bytesOf(encoded), { kind: "reasoning" });
|
|
359
|
+
pendingSignature = undefined;
|
|
360
|
+
pendingSignatureBytes = 0;
|
|
361
|
+
pendingRedacted = [];
|
|
362
|
+
reservation?.commitRetained();
|
|
363
|
+
budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
364
|
+
return encoded;
|
|
365
|
+
};
|
|
366
|
+
// hideThinkingSummary path: no visible reasoning item exists, but a signed thinking block
|
|
367
|
+
// must still round-trip — emit an envelope-only reasoning item (empty summary, no text leak).
|
|
368
|
+
const flushHiddenReasoningEnvelope = () => {
|
|
369
|
+
const encrypted = takeReasoningEnvelope(hiddenThinkingText || undefined);
|
|
370
|
+
hiddenThinkingText = "";
|
|
371
|
+
hiddenThinkingBytes = 0;
|
|
372
|
+
if (!encrypted) return;
|
|
373
|
+
const itemId = `rs_${uuid()}`;
|
|
374
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
375
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
376
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
377
|
+
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
378
|
+
outputIndex++;
|
|
379
|
+
};
|
|
380
|
+
// hideThinkingSummary for RAW reasoning (openai-chat reasoning_content, kiro tags): no
|
|
381
|
+
// visible reasoning item is emitted — the app renders nothing, so tool cells keep grouping
|
|
382
|
+
// like native models — but the text still round-trips in a txt-only ocxr1 envelope so
|
|
383
|
+
// preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
|
|
384
|
+
// encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
|
|
385
|
+
let hiddenRawReasoningText = "";
|
|
386
|
+
let hiddenRawReasoningBytes = 0;
|
|
387
|
+
const flushHiddenRawReasoning = () => {
|
|
388
|
+
if (!hiddenRawReasoningText) return;
|
|
389
|
+
const previousBytes = hiddenRawReasoningBytes;
|
|
390
|
+
const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
|
|
391
|
+
const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
|
|
392
|
+
hiddenRawReasoningText = "";
|
|
393
|
+
hiddenRawReasoningBytes = 0;
|
|
394
|
+
reservation?.commitRetained();
|
|
395
|
+
budget?.releaseRetained(previousBytes, { kind: "reasoning" });
|
|
396
|
+
const itemId = `rs_${uuid()}`;
|
|
397
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
|
|
398
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
399
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
400
|
+
retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
|
|
401
|
+
outputIndex++;
|
|
402
|
+
};
|
|
403
|
+
// Full assistant text of a compaction turn (across message boundaries) — becomes the
|
|
404
|
+
// synthetic compaction item's payload on done.
|
|
405
|
+
let compactionText = "";
|
|
406
|
+
let compactionTextBytes = 0;
|
|
407
|
+
let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
|
|
408
|
+
// Open native web-search cell (between begin and end). Holds the output index allocated on
|
|
409
|
+
// begin so the matching done reuses it; closed as `failed` if the stream terminates early.
|
|
410
|
+
let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
|
|
411
|
+
// Sources from completed web searches, awaiting the next assistant message. Attached as
|
|
412
|
+
// url_citation annotations on that message (the desktop app's Sources chip), then cleared so
|
|
413
|
+
// they bind to exactly one message. Deduped by URL across multiple searches in the turn.
|
|
414
|
+
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
415
|
+
const takeWebAnnotations = (): { type: string; url: string; title?: string; start_index: number; end_index: number }[] => {
|
|
416
|
+
if (pendingWebSources.length === 0) return [];
|
|
417
|
+
const anns = pendingWebSources.map(s => ({
|
|
418
|
+
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
419
|
+
}));
|
|
420
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
421
|
+
const annotationBytes = bytesOf(JSON.stringify(anns));
|
|
422
|
+
const reservation = budget?.reserveTransient(annotationBytes, { kind: "retained_collectors" });
|
|
423
|
+
pendingWebSources = [];
|
|
424
|
+
reservation?.commitRetained();
|
|
425
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
426
|
+
return anns;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const closeCurrentMessage = (inferredPhase?: OcxMessagePhase) => {
|
|
430
|
+
if (!currentMsg) return;
|
|
431
|
+
// Chat Completions has no message-phase field. Keep its live item provisional, then
|
|
432
|
+
// classify it only when the next adapter event proves whether this text led into more
|
|
433
|
+
// work or completed the turn. Explicit adapter phases always outrank this inference.
|
|
434
|
+
const phase = currentMsg.phase ?? inferredPhase;
|
|
435
|
+
// Bind any pending web-search citations to this assistant message (then they clear).
|
|
436
|
+
const annotations = takeWebAnnotations();
|
|
437
|
+
// Finalize the text part (Responses protocol). Without these .done events Codex never
|
|
438
|
+
// commits the content part and renders the message as truncated / cut off.
|
|
439
|
+
emit("response.output_text.done", {
|
|
440
|
+
item_id: currentMsg.itemId, output_index: currentMsg.outputIndex, content_index: 0, text: currentMsg.text,
|
|
441
|
+
});
|
|
442
|
+
emit("response.content_part.done", {
|
|
443
|
+
item_id: currentMsg.itemId, output_index: currentMsg.outputIndex, content_index: 0,
|
|
444
|
+
part: { type: "output_text", text: currentMsg.text, annotations },
|
|
445
|
+
});
|
|
446
|
+
const item = {
|
|
447
|
+
type: "message", id: currentMsg.itemId, status: "completed", role: "assistant",
|
|
448
|
+
content: [{ type: "output_text", text: currentMsg.text, annotations }],
|
|
449
|
+
...(phase ? { phase } : {}),
|
|
450
|
+
};
|
|
451
|
+
emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
|
|
452
|
+
retainFinishedItem(item as OutputItem, currentMsg.textBytes + bytesOf(JSON.stringify(annotations)));
|
|
453
|
+
outputIndex++;
|
|
454
|
+
currentMsg = null;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const closeCurrentReasoning = () => {
|
|
458
|
+
if (!currentReasoning) return;
|
|
459
|
+
emit("response.reasoning_summary_text.done", {
|
|
460
|
+
item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0, text: currentReasoning.text,
|
|
461
|
+
});
|
|
462
|
+
emit("response.reasoning_summary_part.done", {
|
|
463
|
+
item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0,
|
|
464
|
+
part: { type: "summary_text", text: currentReasoning.text },
|
|
465
|
+
});
|
|
466
|
+
const encrypted = takeReasoningEnvelope();
|
|
467
|
+
const item = {
|
|
468
|
+
type: "reasoning", id: currentReasoning.itemId,
|
|
469
|
+
summary: [{ type: "summary_text", text: currentReasoning.text }],
|
|
470
|
+
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
471
|
+
};
|
|
472
|
+
emit("response.output_item.done", { output_index: currentReasoning.outputIndex, item });
|
|
473
|
+
retainFinishedItem(item as OutputItem, currentReasoning.textBytes + bytesOf(encrypted ?? ""), "reasoning");
|
|
474
|
+
outputIndex++;
|
|
475
|
+
currentReasoning = null;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const closeCurrentRawReasoning = () => {
|
|
479
|
+
if (!currentRawReasoning) return;
|
|
480
|
+
const item = {
|
|
481
|
+
type: "reasoning", id: currentRawReasoning.itemId, summary: [],
|
|
482
|
+
content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
|
|
483
|
+
};
|
|
484
|
+
emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
|
|
485
|
+
retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
|
|
486
|
+
outputIndex++;
|
|
487
|
+
currentRawReasoning = null;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const closeCurrentToolCall = () => {
|
|
491
|
+
if (!currentToolCall) return;
|
|
492
|
+
// Empty input (no-arg tools like computer_use get_app_state / list_apps) must serialize as
|
|
493
|
+
// "{}", never "" — Codex echoes the call back as a function_call next turn, and JSON.parse("")
|
|
494
|
+
// would 400 the whole session ("invalid JSON arguments"), poisoning all later turns.
|
|
495
|
+
const argsStr = currentToolCall.args || "{}";
|
|
496
|
+
// Finalize streamed function-call arguments so Codex commits the call (incl. MCP / computer_use).
|
|
497
|
+
if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
|
|
498
|
+
emit("response.function_call_arguments.done", {
|
|
499
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex, arguments: argsStr,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
if (currentToolCall.freeform) {
|
|
503
|
+
emit("response.custom_tool_call_input.done", {
|
|
504
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
505
|
+
input: freeformInput(currentToolCall.args),
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const item = currentToolCall.toolSearch
|
|
509
|
+
? {
|
|
510
|
+
type: "tool_search_call", id: currentToolCall.itemId,
|
|
511
|
+
call_id: currentToolCall.callId, execution: "client",
|
|
512
|
+
arguments: parseArgsObj(currentToolCall.args), status: "completed",
|
|
513
|
+
}
|
|
514
|
+
: currentToolCall.freeform
|
|
515
|
+
? {
|
|
516
|
+
type: "custom_tool_call", id: currentToolCall.itemId,
|
|
517
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
518
|
+
input: freeformInput(currentToolCall.args), status: "completed",
|
|
519
|
+
}
|
|
520
|
+
: {
|
|
521
|
+
type: "function_call", id: currentToolCall.itemId,
|
|
522
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
523
|
+
arguments: argsStr, status: "completed",
|
|
524
|
+
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
525
|
+
};
|
|
526
|
+
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
527
|
+
retainFinishedItem(item as OutputItem);
|
|
528
|
+
budget?.closeCall(currentToolCall.callId);
|
|
529
|
+
outputIndex++;
|
|
530
|
+
currentToolCall = null;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Terminal-error / incomplete path for an open tool call (#765 remainder).
|
|
534
|
+
// Closing via closeCurrentToolCall() would emit function_call_arguments.done and
|
|
535
|
+
// status:"completed" BEFORE response.failed — the client still sees an issued call.
|
|
536
|
+
// Cancel instead: no *.done argument frames, status:"incomplete" (same pattern as an
|
|
537
|
+
// in-flight web_search_call closing as "failed"). Args still serialize as "{}" when
|
|
538
|
+
// empty so echoed items cannot poison the next turn with JSON.parse("").
|
|
539
|
+
const failCurrentToolCall = () => {
|
|
540
|
+
if (!currentToolCall) return;
|
|
541
|
+
const argsStr = currentToolCall.args || "{}";
|
|
542
|
+
const item = currentToolCall.toolSearch
|
|
543
|
+
? {
|
|
544
|
+
type: "tool_search_call", id: currentToolCall.itemId,
|
|
545
|
+
call_id: currentToolCall.callId, execution: "client",
|
|
546
|
+
arguments: parseArgsObj(currentToolCall.args), status: "incomplete",
|
|
547
|
+
}
|
|
548
|
+
: currentToolCall.freeform
|
|
549
|
+
? {
|
|
550
|
+
type: "custom_tool_call", id: currentToolCall.itemId,
|
|
551
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
552
|
+
input: freeformInput(currentToolCall.args), status: "incomplete",
|
|
553
|
+
}
|
|
554
|
+
: {
|
|
555
|
+
type: "function_call", id: currentToolCall.itemId,
|
|
556
|
+
call_id: currentToolCall.callId, name: currentToolCall.name,
|
|
557
|
+
arguments: argsStr, status: "incomplete",
|
|
558
|
+
...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
|
|
559
|
+
};
|
|
560
|
+
emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
|
|
561
|
+
retainFinishedItem(item as OutputItem);
|
|
562
|
+
budget?.closeCall(currentToolCall.callId);
|
|
563
|
+
outputIndex++;
|
|
564
|
+
currentToolCall = null;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const abortCurrentToolCallForTranslatorOverflow = () => {
|
|
568
|
+
if (!currentToolCall) return;
|
|
569
|
+
budget?.closeCall(currentToolCall.callId);
|
|
570
|
+
currentToolCall = null;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Finalize an open web-search cell. `status` is "completed" on a normal end, or "failed" when
|
|
574
|
+
// the stream terminates (error/incomplete) while a search was still in flight, so Codex never
|
|
575
|
+
// leaves a "Searching the web" spinner spinning forever.
|
|
576
|
+
// `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
|
|
577
|
+
// downstream translators (claude outbound) can fill web_search_tool_result content.
|
|
578
|
+
const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
|
|
579
|
+
if (!currentWebSearch) return;
|
|
580
|
+
const item = {
|
|
581
|
+
type: "web_search_call", id: currentWebSearch.itemId, status,
|
|
582
|
+
action: webSearchAction(queries),
|
|
583
|
+
...(sources && sources.length > 0 ? { sources } : {}),
|
|
584
|
+
};
|
|
585
|
+
emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
|
|
586
|
+
retainFinishedItem(item as OutputItem);
|
|
587
|
+
outputIndex++;
|
|
588
|
+
currentWebSearch = null;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// RC1: guarantee the Responses stream always ends with exactly one terminal event. Set true
|
|
592
|
+
// when a done/error/catch terminal is emitted; if the adapter generator returns without one
|
|
593
|
+
// we synthesize a terminal below, so Codex never hits the parser's
|
|
594
|
+
// "stream closed before response.completed" (responses.rs) -> ApiError::Stream.
|
|
595
|
+
// That synthesized terminal is response.incomplete with reason "adapter_eof", NOT
|
|
596
|
+
// response.completed: a generator that returns without a terminal event is a truncated
|
|
597
|
+
// stream, and reporting it as a clean finish is the failure mode this whole path exists
|
|
598
|
+
// to avoid. The comment said "completed" long after the code stopped doing that.
|
|
599
|
+
let terminated = false;
|
|
600
|
+
let firstOutputReported = false;
|
|
601
|
+
const reportFirstOutput = (event: AdapterEvent): void => {
|
|
602
|
+
if (firstOutputReported) return;
|
|
603
|
+
const nonEmpty = event.type === "text_delta"
|
|
604
|
+
? event.text.length > 0
|
|
605
|
+
: event.type === "thinking_delta"
|
|
606
|
+
? event.thinking.length > 0
|
|
607
|
+
: event.type === "reasoning_raw_delta"
|
|
608
|
+
? event.text.length > 0
|
|
609
|
+
: false;
|
|
610
|
+
if (!nonEmpty) return;
|
|
611
|
+
firstOutputReported = true;
|
|
612
|
+
try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
|
|
613
|
+
};
|
|
614
|
+
const it = events[Symbol.asyncIterator]();
|
|
615
|
+
let iteratorStarted = false;
|
|
616
|
+
let iteratorReturned = false;
|
|
617
|
+
let upstreamDone = false;
|
|
618
|
+
const returnIterator = () => {
|
|
619
|
+
if (iteratorReturned) return;
|
|
620
|
+
iteratorReturned = true;
|
|
621
|
+
const finishReturn = () => {
|
|
622
|
+
try {
|
|
623
|
+
void it.return?.()?.catch(() => {});
|
|
624
|
+
} catch {
|
|
625
|
+
/* synchronous iterator cleanup failure is also best-effort */
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
// Async-generator return() before the first next() does not enter the generator, so its
|
|
629
|
+
// finally blocks cannot cancel prepared upstream bodies. The cancel hook has already
|
|
630
|
+
// aborted the turn; bootstrap one cleanup step, then close the iterator without awaiting it.
|
|
631
|
+
if (!iteratorStarted) {
|
|
632
|
+
iteratorStarted = true;
|
|
633
|
+
try {
|
|
634
|
+
void it.next().then(finishReturn, () => {}).catch(() => {});
|
|
635
|
+
} catch {
|
|
636
|
+
/* synchronous iterator start failure is also best-effort */
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
finishReturn();
|
|
641
|
+
};
|
|
642
|
+
let upstreamCancelled = false;
|
|
643
|
+
const cancelUpstreamOnce = () => {
|
|
644
|
+
if (upstreamCancelled) return;
|
|
645
|
+
upstreamCancelled = true;
|
|
646
|
+
try { onCancel?.(); } catch { /* cancellation must not strand the client stream */ }
|
|
647
|
+
returnIterator();
|
|
648
|
+
};
|
|
649
|
+
let handlingTranslatorOverflow = false;
|
|
650
|
+
terminateForTranslatorOverflow = _error => {
|
|
651
|
+
if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
|
|
652
|
+
handlingTranslatorOverflow = true;
|
|
653
|
+
abortCurrentToolCallForTranslatorOverflow();
|
|
654
|
+
currentWebSearch = null;
|
|
655
|
+
const failure = adapterFailureFromEvent({
|
|
656
|
+
type: "error",
|
|
657
|
+
status: 502,
|
|
658
|
+
errorType: "upstream_error",
|
|
659
|
+
code: "translation_buffer_limit",
|
|
660
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
661
|
+
}).error;
|
|
662
|
+
const failedFrame = sseEvent("response.failed", {
|
|
663
|
+
type: "response.failed",
|
|
664
|
+
sequence_number: seq++,
|
|
665
|
+
response: {
|
|
666
|
+
...responseSnapshot("failed", finishedItems),
|
|
667
|
+
error: failure,
|
|
668
|
+
last_error: failure,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
try {
|
|
672
|
+
controller.enqueue(encoder.encode(failedFrame));
|
|
673
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
674
|
+
emittedFrames += 2;
|
|
675
|
+
} catch {
|
|
676
|
+
/* client already tore down the stream */
|
|
677
|
+
}
|
|
678
|
+
reportTerminal("failed");
|
|
679
|
+
terminated = true;
|
|
680
|
+
cancelUpstreamOnce();
|
|
681
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
682
|
+
beat = undefined;
|
|
683
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
684
|
+
closed = true;
|
|
685
|
+
gated = true;
|
|
686
|
+
stepping = false;
|
|
687
|
+
};
|
|
688
|
+
const step = async () => {
|
|
689
|
+
if (stepping || closed) return;
|
|
690
|
+
stepping = true;
|
|
691
|
+
gated = false;
|
|
692
|
+
const emittedAtStart = emittedFrames;
|
|
693
|
+
try {
|
|
694
|
+
while (!terminated && !closed && emittedFrames === emittedAtStart) {
|
|
695
|
+
iteratorStarted = true;
|
|
696
|
+
const next = await it.next();
|
|
697
|
+
if (next.done) { upstreamDone = true; break; }
|
|
698
|
+
const event = next.value;
|
|
699
|
+
let terminalEvent = false;
|
|
700
|
+
// Invisible adapter heartbeats (and buffered web-search progress) count as upstream
|
|
701
|
+
// liveness only — they must not suppress wire keepalives that re-arm Codex idle timers.
|
|
702
|
+
upstreamActivity = true;
|
|
703
|
+
stallTicks = 0;
|
|
704
|
+
reportFirstOutput(event);
|
|
705
|
+
// Compaction turns emit ONLY the synthetic compaction item + response.completed. The
|
|
706
|
+
// summary text is accumulated silently: emitting it as a normal assistant message would
|
|
707
|
+
// duplicate the summary if this response is ever replayed via previous_response_id
|
|
708
|
+
// expansion (rememberResponseState stores input + output). Codex ignores extra items but
|
|
709
|
+
// its compaction UI renders nothing mid-turn, so nothing is lost visually.
|
|
710
|
+
if (options?.compaction) {
|
|
711
|
+
if (event.type === "text_delta") {
|
|
712
|
+
({ value: compactionText, bytes: compactionTextBytes } = appendString(
|
|
713
|
+
compactionText,
|
|
714
|
+
compactionTextBytes,
|
|
715
|
+
event.text,
|
|
716
|
+
"retained_collectors",
|
|
717
|
+
));
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
|
|
721
|
+
}
|
|
722
|
+
switch (event.type) {
|
|
723
|
+
case "assistant_boundary": {
|
|
724
|
+
// A guarded continuation starts a fresh assistant output item while keeping the
|
|
725
|
+
// intermediate, suspicious text in the same Responses turn.
|
|
726
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
727
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
728
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
729
|
+
flushHiddenRawReasoning();
|
|
730
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
731
|
+
flushHiddenReasoningEnvelope();
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case "text_delta": {
|
|
735
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
736
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
737
|
+
flushHiddenRawReasoning();
|
|
738
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
739
|
+
// Only flush on an explicit phase change. A later delta that omits `phase` must
|
|
740
|
+
// keep appending to the current message rather than wiping the earlier phase.
|
|
741
|
+
if (currentMsg && event.phase !== undefined && currentMsg.phase !== event.phase) {
|
|
742
|
+
closeCurrentMessage("commentary");
|
|
743
|
+
}
|
|
744
|
+
if (!currentMsg) {
|
|
745
|
+
const itemId = `msg_${uuid()}`;
|
|
746
|
+
const item = {
|
|
747
|
+
type: "message", id: itemId, status: "in_progress", role: "assistant",
|
|
748
|
+
content: [] as { type: string; text: string; annotations: never[] }[],
|
|
749
|
+
...(event.phase ? { phase: event.phase } : {}),
|
|
750
|
+
};
|
|
751
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
752
|
+
emit("response.content_part.added", {
|
|
753
|
+
item_id: itemId, output_index: outputIndex, content_index: 0,
|
|
754
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
755
|
+
});
|
|
756
|
+
currentMsg = { itemId, outputIndex, text: "", textBytes: 0, ...(event.phase ? { phase: event.phase } : {}) };
|
|
757
|
+
}
|
|
758
|
+
({ value: currentMsg.text, bytes: currentMsg.textBytes } = appendString(
|
|
759
|
+
currentMsg.text,
|
|
760
|
+
currentMsg.textBytes,
|
|
761
|
+
event.text,
|
|
762
|
+
"retained_collectors",
|
|
763
|
+
));
|
|
764
|
+
emit("response.output_text.delta", {
|
|
765
|
+
item_id: currentMsg.itemId, output_index: currentMsg.outputIndex,
|
|
766
|
+
content_index: 0, delta: event.text,
|
|
767
|
+
});
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
case "thinking_delta": {
|
|
771
|
+
if (options?.hideThinkingSummary) {
|
|
772
|
+
({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
|
|
773
|
+
hiddenThinkingText,
|
|
774
|
+
hiddenThinkingBytes,
|
|
775
|
+
event.thinking,
|
|
776
|
+
"reasoning",
|
|
777
|
+
));
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
781
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
782
|
+
flushHiddenRawReasoning();
|
|
783
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
784
|
+
if (!currentReasoning) {
|
|
785
|
+
const itemId = `rs_${uuid()}`;
|
|
786
|
+
const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
|
|
787
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
788
|
+
emit("response.reasoning_summary_part.added", {
|
|
789
|
+
item_id: itemId, output_index: outputIndex, summary_index: 0,
|
|
790
|
+
part: { type: "summary_text", text: "" },
|
|
791
|
+
});
|
|
792
|
+
currentReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
|
|
793
|
+
}
|
|
794
|
+
({ value: currentReasoning.text, bytes: currentReasoning.textBytes } = appendString(
|
|
795
|
+
currentReasoning.text,
|
|
796
|
+
currentReasoning.textBytes,
|
|
797
|
+
event.thinking,
|
|
798
|
+
"reasoning",
|
|
799
|
+
));
|
|
800
|
+
emit("response.reasoning_summary_text.delta", {
|
|
801
|
+
item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex,
|
|
802
|
+
summary_index: 0, delta: event.thinking,
|
|
803
|
+
});
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "thinking_signature": {
|
|
807
|
+
pendingSignatureBytes = replaceRetainedString(pendingSignatureBytes, event.signature, "reasoning");
|
|
808
|
+
pendingSignature = event.signature;
|
|
809
|
+
// Signature arrives at the end of the thinking block. With a visible reasoning item
|
|
810
|
+
// open, closeCurrentReasoning attaches the envelope; hidden/suppressed blocks flush
|
|
811
|
+
// an envelope-only reasoning item now.
|
|
812
|
+
if (!currentReasoning) flushHiddenReasoningEnvelope();
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case "redacted_thinking": {
|
|
816
|
+
budget?.chargeRetained(bytesOf(event.data), { kind: "reasoning" });
|
|
817
|
+
pendingRedacted.push(event.data);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
case "reasoning_raw_delta": {
|
|
821
|
+
if (options?.hideThinkingSummary) {
|
|
822
|
+
({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
|
|
823
|
+
hiddenRawReasoningText,
|
|
824
|
+
hiddenRawReasoningBytes,
|
|
825
|
+
event.text,
|
|
826
|
+
"reasoning",
|
|
827
|
+
));
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
831
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
832
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
833
|
+
if (!currentRawReasoning) {
|
|
834
|
+
const itemId = `rs_${uuid()}`;
|
|
835
|
+
const item = { type: "reasoning", id: itemId, summary: [] as never[], content: [] as { type: string; text: string }[] };
|
|
836
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
837
|
+
currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
|
|
838
|
+
}
|
|
839
|
+
({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
|
|
840
|
+
currentRawReasoning.text,
|
|
841
|
+
currentRawReasoning.textBytes,
|
|
842
|
+
event.text,
|
|
843
|
+
"reasoning",
|
|
844
|
+
));
|
|
845
|
+
emit("response.reasoning_text.delta", {
|
|
846
|
+
item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
|
|
847
|
+
content_index: 0, delta: event.text,
|
|
848
|
+
});
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
case "tool_call_start": {
|
|
852
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
853
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
854
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
855
|
+
flushHiddenRawReasoning();
|
|
856
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
857
|
+
const mapped = toolNsMap?.get(event.name);
|
|
858
|
+
const realName = mapped?.name ?? event.name;
|
|
859
|
+
const ns = mapped?.namespace;
|
|
860
|
+
const toolSearch = toolSearchToolNames?.has(realName) ?? false;
|
|
861
|
+
const freeform = !toolSearch && (freeformToolNames?.has(realName) ?? false);
|
|
862
|
+
const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
|
|
863
|
+
const item = toolSearch
|
|
864
|
+
? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
|
|
865
|
+
: freeform
|
|
866
|
+
? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
|
|
867
|
+
: { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
|
|
868
|
+
emit("response.output_item.added", { output_index: outputIndex, item });
|
|
869
|
+
currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch };
|
|
870
|
+
budget?.openCall(event.id);
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case "tool_call_delta": {
|
|
874
|
+
if (currentToolCall) {
|
|
875
|
+
({ value: currentToolCall.args, bytes: currentToolCall.argsBytes } = appendString(
|
|
876
|
+
currentToolCall.args,
|
|
877
|
+
currentToolCall.argsBytes,
|
|
878
|
+
event.arguments,
|
|
879
|
+
"tool_args",
|
|
880
|
+
currentToolCall.callId,
|
|
881
|
+
));
|
|
882
|
+
if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
|
|
883
|
+
emit("response.function_call_arguments.delta", {
|
|
884
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
885
|
+
delta: event.arguments,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (currentToolCall.freeform) {
|
|
889
|
+
// Hold while the buffer is still an ambiguous prefix of the JSON wrapper,
|
|
890
|
+
// then stream only the unwrapped input suffix (never rewind on mode flips).
|
|
891
|
+
if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
|
|
892
|
+
const full = freeformPartialInput(currentToolCall.args);
|
|
893
|
+
const emitted = currentToolCall.inputEmitted ?? "";
|
|
894
|
+
if (full.startsWith(emitted) && full.length > emitted.length) {
|
|
895
|
+
emit("response.custom_tool_call_input.delta", {
|
|
896
|
+
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
897
|
+
delta: full.slice(emitted.length),
|
|
898
|
+
});
|
|
899
|
+
currentToolCall.inputEmitted = full;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
case "tool_call_end": {
|
|
907
|
+
// Fragments already streamed cannot be repaired. Refuse to complete a function call
|
|
908
|
+
// whose assembled arguments do not parse — cancel the item and fail the turn so the
|
|
909
|
+
// client never sees status:"completed" for unusable args (#765 stream remainder).
|
|
910
|
+
if (
|
|
911
|
+
currentToolCall
|
|
912
|
+
&& !currentToolCall.freeform
|
|
913
|
+
&& !currentToolCall.toolSearch
|
|
914
|
+
&& !toolCallArgumentsUsable(currentToolCall.args)
|
|
915
|
+
) {
|
|
916
|
+
failCurrentToolCall();
|
|
917
|
+
const failure = responseError(
|
|
918
|
+
502,
|
|
919
|
+
"upstream_error",
|
|
920
|
+
"upstream stream produced malformed tool call arguments",
|
|
921
|
+
);
|
|
922
|
+
emit("response.failed", {
|
|
923
|
+
response: {
|
|
924
|
+
...responseSnapshot("failed", finishedItems),
|
|
925
|
+
error: failure,
|
|
926
|
+
last_error: failure,
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
reportTerminal("failed");
|
|
930
|
+
terminalEvent = true;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
closeCurrentToolCall();
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
case "web_search_call_begin": {
|
|
937
|
+
// Open the native search cell so Codex shows the "Searching the web" spinner WHILE the
|
|
938
|
+
// sidecar runs. Close any other open item first, allocate this item's output index, and
|
|
939
|
+
// hold it open until the matching `web_search_call_end` (or a terminal close).
|
|
940
|
+
if (currentMsg) closeCurrentMessage("commentary");
|
|
941
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
942
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
943
|
+
flushHiddenRawReasoning();
|
|
944
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
945
|
+
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
946
|
+
const wsItemId = `ws_${uuid()}`;
|
|
947
|
+
emit("response.output_item.added", {
|
|
948
|
+
output_index: outputIndex,
|
|
949
|
+
item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
|
|
950
|
+
});
|
|
951
|
+
currentWebSearch = { itemId: wsItemId, eventId: event.id, outputIndex };
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
case "web_search_call_end": {
|
|
955
|
+
// The sidecar resolved — finalize the cell as "Searched <query>". If no begin opened
|
|
956
|
+
// (defensive), synthesize the added frame first so the done has a matching item.
|
|
957
|
+
if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
|
|
958
|
+
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
959
|
+
const wsItemId2 = `ws_${uuid()}`;
|
|
960
|
+
emit("response.output_item.added", {
|
|
961
|
+
output_index: outputIndex,
|
|
962
|
+
item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
|
|
963
|
+
});
|
|
964
|
+
currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
|
|
965
|
+
}
|
|
966
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, event.sources);
|
|
967
|
+
// Queue this search's sources for the next assistant message (dedup by URL).
|
|
968
|
+
if (event.sources) {
|
|
969
|
+
const seen = new Set(pendingWebSources.map(s => s.url));
|
|
970
|
+
for (const s of event.sources) {
|
|
971
|
+
if (!seen.has(s.url)) {
|
|
972
|
+
seen.add(s.url);
|
|
973
|
+
chargeValue(s, "tool_search_sources");
|
|
974
|
+
pendingWebSources.push(s);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case "done": {
|
|
981
|
+
if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
|
|
982
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
983
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
984
|
+
flushHiddenRawReasoning();
|
|
985
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
986
|
+
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
987
|
+
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
988
|
+
// need their envelope-only reasoning item so the blocks replay next turn.
|
|
989
|
+
flushHiddenReasoningEnvelope();
|
|
990
|
+
if (options?.compaction) {
|
|
991
|
+
// Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
|
|
992
|
+
const item = {
|
|
993
|
+
type: "compaction", id: `cmp_${uuid()}`,
|
|
994
|
+
encrypted_content: encodeCompactionSummary(compactionText),
|
|
995
|
+
};
|
|
996
|
+
emit("response.output_item.done", { output_index: outputIndex, item });
|
|
997
|
+
retainFinishedItem(item as OutputItem, compactionTextBytes);
|
|
998
|
+
outputIndex++;
|
|
999
|
+
}
|
|
1000
|
+
if (event.stopReason === "max_tokens" || event.stopReason === "content_filter") {
|
|
1001
|
+
// Upstream stopped before a normal completion. Surface as incomplete so the
|
|
1002
|
+
// client can distinguish a truncated/filtered turn from a finished one.
|
|
1003
|
+
const response = {
|
|
1004
|
+
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
1005
|
+
usage: responsesUsage(event.usage),
|
|
1006
|
+
incomplete_details: {
|
|
1007
|
+
reason: event.stopReason === "max_tokens" ? "max_output_tokens" : "content_filter",
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
1010
|
+
// Cache max-output partials so previous_response_id replay can continue them;
|
|
1011
|
+
// rememberResponseState rejects content-filtered incomplete responses.
|
|
1012
|
+
options?.onCompletedResponse?.(response, event.providerState);
|
|
1013
|
+
options?.onUsage?.(event.usage);
|
|
1014
|
+
emit("response.incomplete", { response });
|
|
1015
|
+
reportTerminal("incomplete");
|
|
1016
|
+
} else {
|
|
1017
|
+
const response = { ...responseSnapshot("completed", finishedItems, event.endTurn), usage: responsesUsage(event.usage) };
|
|
1018
|
+
options?.onCompletedResponse?.(response, event.providerState);
|
|
1019
|
+
options?.onUsage?.(event.usage);
|
|
1020
|
+
emit("response.completed", {
|
|
1021
|
+
response,
|
|
1022
|
+
});
|
|
1023
|
+
reportTerminal("completed");
|
|
1024
|
+
}
|
|
1025
|
+
terminalEvent = true;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
case "incomplete": {
|
|
1029
|
+
if (currentMsg) closeCurrentMessage();
|
|
1030
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
1031
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1032
|
+
flushHiddenRawReasoning();
|
|
1033
|
+
if (currentToolCall) failCurrentToolCall();
|
|
1034
|
+
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1035
|
+
flushHiddenReasoningEnvelope();
|
|
1036
|
+
options?.onUsage?.(event.usage);
|
|
1037
|
+
emit("response.incomplete", {
|
|
1038
|
+
response: {
|
|
1039
|
+
...responseSnapshot("incomplete", finishedItems, event.endTurn),
|
|
1040
|
+
usage: responsesUsage(event.usage),
|
|
1041
|
+
incomplete_details: {
|
|
1042
|
+
reason: event.reason,
|
|
1043
|
+
...(event.message ? { message: event.message } : {}),
|
|
1044
|
+
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
reportTerminal("incomplete");
|
|
1049
|
+
terminalEvent = true;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
case "error": {
|
|
1053
|
+
if (event.code === "translation_buffer_limit") {
|
|
1054
|
+
terminateForTranslatorOverflow(event);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (currentMsg) closeCurrentMessage();
|
|
1058
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
1059
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1060
|
+
flushHiddenRawReasoning();
|
|
1061
|
+
if (currentToolCall) failCurrentToolCall();
|
|
1062
|
+
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1063
|
+
const failure = adapterFailureFromEvent(event);
|
|
1064
|
+
if (event.usage) options?.onUsage?.(event.usage);
|
|
1065
|
+
emit("response.failed", {
|
|
1066
|
+
response: {
|
|
1067
|
+
...responseSnapshot("failed", finishedItems),
|
|
1068
|
+
// Partial consumption from a mid-stream upstream failure: surfaced so the request
|
|
1069
|
+
// log can record real tokens instead of usageStatus "unreported" with 0.
|
|
1070
|
+
...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
|
|
1071
|
+
error: failure.error,
|
|
1072
|
+
last_error: failure.error,
|
|
1073
|
+
...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
reportTerminal("failed");
|
|
1077
|
+
terminalEvent = true;
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (terminalEvent) {
|
|
1082
|
+
cancelUpstreamOnce();
|
|
1083
|
+
terminated = true;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
1089
|
+
terminateForTranslatorOverflow(err);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (!terminated) {
|
|
1093
|
+
flushHiddenRawReasoning();
|
|
1094
|
+
if (currentToolCall) failCurrentToolCall();
|
|
1095
|
+
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1096
|
+
emit("response.failed", {
|
|
1097
|
+
response: {
|
|
1098
|
+
...responseSnapshot("failed", finishedItems),
|
|
1099
|
+
error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
|
|
1100
|
+
last_error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
reportTerminal("failed");
|
|
1104
|
+
cancelUpstreamOnce();
|
|
1105
|
+
terminated = true;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (!terminated && !upstreamDone) {
|
|
1110
|
+
gated = true;
|
|
1111
|
+
stepping = false;
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (beat !== undefined) { clearBeatInterval(beat); beat = undefined; }
|
|
1115
|
+
|
|
1116
|
+
if (!terminated) {
|
|
1117
|
+
// The adapter generator ended without an explicit done/error event. Mark as incomplete
|
|
1118
|
+
// rather than completed so Codex can distinguish a clean finish from a truncated stream.
|
|
1119
|
+
if (currentMsg) closeCurrentMessage();
|
|
1120
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
1121
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1122
|
+
flushHiddenRawReasoning();
|
|
1123
|
+
if (currentToolCall) failCurrentToolCall();
|
|
1124
|
+
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1125
|
+
options?.onUsage?.(undefined);
|
|
1126
|
+
emit("response.incomplete", {
|
|
1127
|
+
response: {
|
|
1128
|
+
...responseSnapshot("incomplete", finishedItems),
|
|
1129
|
+
usage: responsesUsage(undefined),
|
|
1130
|
+
incomplete_details: { reason: "adapter_eof" },
|
|
1131
|
+
},
|
|
1132
|
+
});
|
|
1133
|
+
reportTerminal("incomplete");
|
|
1134
|
+
terminated = true;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
emitDone();
|
|
1138
|
+
try {
|
|
1139
|
+
controller.close();
|
|
1140
|
+
} catch {
|
|
1141
|
+
/* already closed (e.g. client cancelled) */
|
|
1142
|
+
}
|
|
1143
|
+
closed = true;
|
|
1144
|
+
gated = true;
|
|
1145
|
+
stepping = false;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
const startStream = () => {
|
|
1149
|
+
emit("response.created", { response: responseSnapshot("in_progress", []) });
|
|
1150
|
+
// The default ReadableStream strategy has HWM=1. Once one event's frames fill that
|
|
1151
|
+
// queue, pull stepping pauses; no custom FIFO or queuing strategy is layered on top.
|
|
1152
|
+
gated = true;
|
|
1153
|
+
beat = setBeatInterval(() => {
|
|
1154
|
+
if (closed || gated) return;
|
|
1155
|
+
if (upstreamActivity) {
|
|
1156
|
+
upstreamActivity = false;
|
|
1157
|
+
stallTicks = 0;
|
|
1158
|
+
} else if (++stallTicks >= maxStallTicks) {
|
|
1159
|
+
if (currentMsg) closeCurrentMessage();
|
|
1160
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
1161
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
1162
|
+
flushHiddenRawReasoning();
|
|
1163
|
+
if (currentToolCall) failCurrentToolCall();
|
|
1164
|
+
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1165
|
+
emit("response.incomplete", {
|
|
1166
|
+
response: {
|
|
1167
|
+
...responseSnapshot("incomplete", finishedItems),
|
|
1168
|
+
incomplete_details: { reason: "upstream_stall_timeout" },
|
|
1169
|
+
},
|
|
1170
|
+
});
|
|
1171
|
+
reportTerminal("incomplete");
|
|
1172
|
+
cancelUpstreamOnce();
|
|
1173
|
+
terminated = true;
|
|
1174
|
+
emitDone();
|
|
1175
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
1176
|
+
beat = undefined;
|
|
1177
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
1178
|
+
closed = true;
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
// Wire silence is independent of upstream adapter heartbeats.
|
|
1182
|
+
if (wireActivity) {
|
|
1183
|
+
wireActivity = false;
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
try {
|
|
1187
|
+
controller.enqueue(heartbeatFrame);
|
|
1188
|
+
emittedFrames++;
|
|
1189
|
+
} catch {
|
|
1190
|
+
closed = true;
|
|
1191
|
+
}
|
|
1192
|
+
}, heartbeatMs);
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
return new ReadableStream<Uint8Array>({
|
|
1196
|
+
start(streamController) {
|
|
1197
|
+
controller = streamController;
|
|
1198
|
+
startStream();
|
|
1199
|
+
},
|
|
1200
|
+
pull() {
|
|
1201
|
+
return step();
|
|
1202
|
+
},
|
|
1203
|
+
cancel() {
|
|
1204
|
+
// Client (Codex) disconnected. Stop emitting and let the caller abort the upstream fetch so a
|
|
1205
|
+
// cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
|
|
1206
|
+
clientCancelled = true;
|
|
1207
|
+
closed = true;
|
|
1208
|
+
if (beat !== undefined) clearBeatInterval(beat);
|
|
1209
|
+
cancelUpstreamOnce();
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export function buildResponseJSON(
|
|
1215
|
+
events: AdapterEvent[],
|
|
1216
|
+
modelId: string,
|
|
1217
|
+
options?: {
|
|
1218
|
+
hideThinkingSummary?: boolean;
|
|
1219
|
+
toolNsMap?: Map<string, { namespace: string; name: string }>;
|
|
1220
|
+
freeformToolNames?: Set<string>;
|
|
1221
|
+
toolSearchToolNames?: Set<string>;
|
|
1222
|
+
/** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
|
|
1223
|
+
compaction?: boolean;
|
|
1224
|
+
onProviderState?: (state: OcxProviderContinuationState) => void;
|
|
1225
|
+
/** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
|
|
1226
|
+
onUsage?: (usage: OcxUsage | undefined) => void;
|
|
1227
|
+
translatorBudget?: TranslatorBudget;
|
|
1228
|
+
},
|
|
1229
|
+
): Record<string, unknown> {
|
|
1230
|
+
const responseId = `resp_${uuid()}`;
|
|
1231
|
+
const output: OutputItem[] = [];
|
|
1232
|
+
const budget = options?.translatorBudget;
|
|
1233
|
+
const encoder = new TextEncoder();
|
|
1234
|
+
const bytesOf = (value: string): number => Buffer.byteLength(value);
|
|
1235
|
+
const appendBatchString = (
|
|
1236
|
+
previous: string,
|
|
1237
|
+
previousBytes: number,
|
|
1238
|
+
fragment: string,
|
|
1239
|
+
kind: TranslatorBufferKind,
|
|
1240
|
+
callId?: string,
|
|
1241
|
+
): { value: string; bytes: number } => {
|
|
1242
|
+
const nextBytes = previousBytes + bytesOf(fragment);
|
|
1243
|
+
if (!budget) return { value: previous + fragment, bytes: nextBytes };
|
|
1244
|
+
const scope = { kind, ...(callId ? { callId } : {}) };
|
|
1245
|
+
const reservation = budget.reserveTransient(nextBytes, scope);
|
|
1246
|
+
try {
|
|
1247
|
+
const value = previous + fragment;
|
|
1248
|
+
reservation.commitRetained();
|
|
1249
|
+
budget.releaseRetained(previousBytes, scope);
|
|
1250
|
+
return { value, bytes: nextBytes };
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
reservation.release();
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
const replaceBatchRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
|
|
1257
|
+
const nextBytes = bytesOf(next);
|
|
1258
|
+
if (!budget) return nextBytes;
|
|
1259
|
+
const reservation = budget.reserveTransient(nextBytes, { kind });
|
|
1260
|
+
reservation.commitRetained();
|
|
1261
|
+
budget.releaseRetained(previousBytes, { kind });
|
|
1262
|
+
return nextBytes;
|
|
1263
|
+
};
|
|
1264
|
+
const pushOutput = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
|
|
1265
|
+
const reservation = budget?.reserveTransient(bytesOf(JSON.stringify(item)), { kind });
|
|
1266
|
+
output.push(item);
|
|
1267
|
+
reservation?.commitRetained();
|
|
1268
|
+
if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
|
|
1269
|
+
};
|
|
1270
|
+
let usage: OcxUsage | undefined;
|
|
1271
|
+
let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
|
|
1272
|
+
let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
|
|
1273
|
+
let endTurn: boolean | undefined;
|
|
1274
|
+
let stopReason: string | undefined;
|
|
1275
|
+
let cleanDone = false;
|
|
1276
|
+
let compactionText = "";
|
|
1277
|
+
let compactionTextBytes = 0;
|
|
1278
|
+
|
|
1279
|
+
let currentText = "";
|
|
1280
|
+
let currentTextBytes = 0;
|
|
1281
|
+
let currentTextPhase: OcxMessagePhase | undefined;
|
|
1282
|
+
let currentSummaryReasoning = "";
|
|
1283
|
+
let currentSummaryReasoningBytes = 0;
|
|
1284
|
+
let currentRawReasoning = "";
|
|
1285
|
+
let currentRawReasoningBytes = 0;
|
|
1286
|
+
// Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
|
|
1287
|
+
let batchSignature: string | undefined;
|
|
1288
|
+
let batchSignatureBytes = 0;
|
|
1289
|
+
let batchRedacted: string[] = [];
|
|
1290
|
+
let batchRedactedBytes = 0;
|
|
1291
|
+
let currentToolCallId = "";
|
|
1292
|
+
let currentToolCallName = "";
|
|
1293
|
+
let currentToolCallArgs = "";
|
|
1294
|
+
let currentToolCallArgsBytes = 0;
|
|
1295
|
+
// Web-search citations awaiting the next assistant message (attached as url_citation annotations).
|
|
1296
|
+
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
1297
|
+
|
|
1298
|
+
const freeformInput = (args: string): string => {
|
|
1299
|
+
try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
|
|
1300
|
+
return args;
|
|
1301
|
+
};
|
|
1302
|
+
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
1303
|
+
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
const flushText = (inferredPhase?: OcxMessagePhase) => {
|
|
1307
|
+
if (!currentText) return;
|
|
1308
|
+
const phase = currentTextPhase ?? inferredPhase;
|
|
1309
|
+
const annotations = pendingWebSources.map(s => ({
|
|
1310
|
+
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
1311
|
+
}));
|
|
1312
|
+
pendingWebSources = [];
|
|
1313
|
+
const item = {
|
|
1314
|
+
type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
|
|
1315
|
+
content: [{ type: "output_text", text: currentText, annotations }],
|
|
1316
|
+
...(phase ? { phase } : {}),
|
|
1317
|
+
} as OutputItem;
|
|
1318
|
+
pushOutput(item, currentTextBytes);
|
|
1319
|
+
currentText = "";
|
|
1320
|
+
currentTextBytes = 0;
|
|
1321
|
+
currentTextPhase = undefined;
|
|
1322
|
+
};
|
|
1323
|
+
const flushSummaryReasoning = () => {
|
|
1324
|
+
if (!currentSummaryReasoning && !batchSignature && batchRedacted.length === 0) return;
|
|
1325
|
+
const envelope: ReasoningEnvelope = {};
|
|
1326
|
+
if (batchSignature) envelope.sig = batchSignature;
|
|
1327
|
+
if (batchRedacted.length > 0) envelope.red = batchRedacted;
|
|
1328
|
+
const hidden = options?.hideThinkingSummary === true;
|
|
1329
|
+
if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
|
|
1330
|
+
const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope) : undefined;
|
|
1331
|
+
const sourceBytes = currentSummaryReasoningBytes + batchSignatureBytes + batchRedactedBytes;
|
|
1332
|
+
batchSignature = undefined;
|
|
1333
|
+
batchSignatureBytes = 0;
|
|
1334
|
+
batchRedacted = [];
|
|
1335
|
+
batchRedactedBytes = 0;
|
|
1336
|
+
if (hidden && !encrypted) {
|
|
1337
|
+
budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
|
|
1338
|
+
currentSummaryReasoning = "";
|
|
1339
|
+
currentSummaryReasoningBytes = 0;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const item = {
|
|
1343
|
+
type: "reasoning", id: `rs_${uuid()}`,
|
|
1344
|
+
summary: !hidden && currentSummaryReasoning ? [{ type: "summary_text", text: currentSummaryReasoning }] : [],
|
|
1345
|
+
...(encrypted ? { encrypted_content: encrypted } : {}),
|
|
1346
|
+
} as OutputItem;
|
|
1347
|
+
pushOutput(item, sourceBytes, "reasoning");
|
|
1348
|
+
currentSummaryReasoning = "";
|
|
1349
|
+
currentSummaryReasoningBytes = 0;
|
|
1350
|
+
};
|
|
1351
|
+
const flushRawReasoning = () => {
|
|
1352
|
+
if (!currentRawReasoning) return;
|
|
1353
|
+
if (options?.hideThinkingSummary === true) {
|
|
1354
|
+
// Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
|
|
1355
|
+
pushOutput({
|
|
1356
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
1357
|
+
encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
|
|
1358
|
+
}, currentRawReasoningBytes, "reasoning");
|
|
1359
|
+
currentRawReasoning = "";
|
|
1360
|
+
currentRawReasoningBytes = 0;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
pushOutput({
|
|
1364
|
+
type: "reasoning", id: `rs_${uuid()}`, summary: [],
|
|
1365
|
+
content: [{ type: "reasoning_text", text: currentRawReasoning }],
|
|
1366
|
+
}, currentRawReasoningBytes, "reasoning");
|
|
1367
|
+
currentRawReasoning = "";
|
|
1368
|
+
currentRawReasoningBytes = 0;
|
|
1369
|
+
};
|
|
1370
|
+
const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
|
|
1371
|
+
if (!currentToolCallId) return;
|
|
1372
|
+
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1373
|
+
const realName = mapped?.name ?? currentToolCallName;
|
|
1374
|
+
const ns = mapped?.namespace;
|
|
1375
|
+
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1376
|
+
const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
|
|
1377
|
+
if (toolSearch) {
|
|
1378
|
+
pushOutput({
|
|
1379
|
+
type: "tool_search_call", id: `tsc_${uuid()}`,
|
|
1380
|
+
call_id: currentToolCallId, execution: "client",
|
|
1381
|
+
arguments: parseArgsObj(currentToolCallArgs), status,
|
|
1382
|
+
});
|
|
1383
|
+
} else if (freeform) {
|
|
1384
|
+
pushOutput({
|
|
1385
|
+
type: "custom_tool_call", id: `ctc_${uuid()}`,
|
|
1386
|
+
call_id: currentToolCallId, name: realName,
|
|
1387
|
+
input: freeformInput(currentToolCallArgs), status,
|
|
1388
|
+
});
|
|
1389
|
+
} else {
|
|
1390
|
+
pushOutput({
|
|
1391
|
+
type: "function_call", id: `fc_${uuid()}`,
|
|
1392
|
+
call_id: currentToolCallId, name: realName,
|
|
1393
|
+
arguments: currentToolCallArgs || "{}", status,
|
|
1394
|
+
...(ns ? { namespace: ns } : {}),
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
budget?.closeCall(currentToolCallId);
|
|
1398
|
+
currentToolCallId = "";
|
|
1399
|
+
currentToolCallName = "";
|
|
1400
|
+
currentToolCallArgs = "";
|
|
1401
|
+
currentToolCallArgsBytes = 0;
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
for (const e of events) {
|
|
1405
|
+
switch (e.type) {
|
|
1406
|
+
case "assistant_boundary":
|
|
1407
|
+
flushText("commentary");
|
|
1408
|
+
flushSummaryReasoning();
|
|
1409
|
+
flushRawReasoning();
|
|
1410
|
+
flushToolCall();
|
|
1411
|
+
break;
|
|
1412
|
+
case "text_delta":
|
|
1413
|
+
// Only flush on an explicit phase change. A later delta that omits `phase` must keep
|
|
1414
|
+
// appending under the previously established phase.
|
|
1415
|
+
if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
|
|
1416
|
+
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1417
|
+
if (currentRawReasoning) flushRawReasoning();
|
|
1418
|
+
if (currentToolCallId) flushToolCall();
|
|
1419
|
+
// Compaction turns keep the summary out of normal message output (replay dedup — see
|
|
1420
|
+
// bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
|
|
1421
|
+
if (options?.compaction) {
|
|
1422
|
+
({ value: compactionText, bytes: compactionTextBytes } = appendBatchString(
|
|
1423
|
+
compactionText, compactionTextBytes, e.text, "retained_collectors",
|
|
1424
|
+
));
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
if (e.phase !== undefined) currentTextPhase = e.phase;
|
|
1428
|
+
({ value: currentText, bytes: currentTextBytes } = appendBatchString(
|
|
1429
|
+
currentText, currentTextBytes, e.text, "retained_collectors",
|
|
1430
|
+
));
|
|
1431
|
+
}
|
|
1432
|
+
break;
|
|
1433
|
+
case "thinking_delta":
|
|
1434
|
+
if (currentText) flushText("commentary");
|
|
1435
|
+
if (currentRawReasoning) flushRawReasoning();
|
|
1436
|
+
if (currentToolCallId) flushToolCall();
|
|
1437
|
+
{
|
|
1438
|
+
({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
|
|
1439
|
+
currentSummaryReasoning, currentSummaryReasoningBytes, e.thinking, "reasoning",
|
|
1440
|
+
));
|
|
1441
|
+
}
|
|
1442
|
+
break;
|
|
1443
|
+
case "thinking_signature":
|
|
1444
|
+
// End of the current thinking block — flush it WITH the signature envelope so the
|
|
1445
|
+
// block/signature pairing survives multi-block turns.
|
|
1446
|
+
batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
|
|
1447
|
+
batchSignature = e.signature;
|
|
1448
|
+
flushSummaryReasoning();
|
|
1449
|
+
break;
|
|
1450
|
+
case "redacted_thinking":
|
|
1451
|
+
{
|
|
1452
|
+
const dataBytes = bytesOf(e.data);
|
|
1453
|
+
budget?.chargeRetained(dataBytes, { kind: "reasoning" });
|
|
1454
|
+
batchRedactedBytes += dataBytes;
|
|
1455
|
+
}
|
|
1456
|
+
batchRedacted.push(e.data);
|
|
1457
|
+
break;
|
|
1458
|
+
case "reasoning_raw_delta":
|
|
1459
|
+
if (currentText) flushText("commentary");
|
|
1460
|
+
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1461
|
+
if (currentToolCallId) flushToolCall();
|
|
1462
|
+
{
|
|
1463
|
+
({ value: currentRawReasoning, bytes: currentRawReasoningBytes } = appendBatchString(
|
|
1464
|
+
currentRawReasoning, currentRawReasoningBytes, e.text, "reasoning",
|
|
1465
|
+
));
|
|
1466
|
+
}
|
|
1467
|
+
break;
|
|
1468
|
+
case "tool_call_start":
|
|
1469
|
+
if (currentText) flushText("commentary");
|
|
1470
|
+
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1471
|
+
if (currentRawReasoning) flushRawReasoning();
|
|
1472
|
+
flushToolCall();
|
|
1473
|
+
currentToolCallId = e.id;
|
|
1474
|
+
budget?.openCall(e.id);
|
|
1475
|
+
currentToolCallName = e.name;
|
|
1476
|
+
currentToolCallArgs = "";
|
|
1477
|
+
currentToolCallArgsBytes = 0;
|
|
1478
|
+
break;
|
|
1479
|
+
case "tool_call_delta":
|
|
1480
|
+
{
|
|
1481
|
+
({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
|
|
1482
|
+
currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
|
|
1483
|
+
));
|
|
1484
|
+
}
|
|
1485
|
+
break;
|
|
1486
|
+
case "tool_call_end":
|
|
1487
|
+
if (!toolCallArgumentsUsable(currentToolCallArgs) && currentToolCallId) {
|
|
1488
|
+
// Mirror the streaming path: refuse to complete unusable arguments.
|
|
1489
|
+
const mapped = options?.toolNsMap?.get(currentToolCallName);
|
|
1490
|
+
const realName = mapped?.name ?? currentToolCallName;
|
|
1491
|
+
const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
|
|
1492
|
+
const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
|
|
1493
|
+
if (!freeform && !toolSearch) {
|
|
1494
|
+
flushToolCall("incomplete");
|
|
1495
|
+
errorEvent = {
|
|
1496
|
+
type: "error",
|
|
1497
|
+
message: "upstream stream produced malformed tool call arguments",
|
|
1498
|
+
status: 502,
|
|
1499
|
+
errorType: "upstream_error",
|
|
1500
|
+
};
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
flushToolCall();
|
|
1505
|
+
break;
|
|
1506
|
+
case "web_search_call_begin":
|
|
1507
|
+
// Batch/non-streaming output has no in_progress phase to animate — the search cell is a
|
|
1508
|
+
// single finalized item, emitted on `end`. Begin is a no-op here.
|
|
1509
|
+
break;
|
|
1510
|
+
case "web_search_call_end":
|
|
1511
|
+
if (currentText) flushText("commentary");
|
|
1512
|
+
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1513
|
+
if (currentRawReasoning) flushRawReasoning();
|
|
1514
|
+
flushToolCall();
|
|
1515
|
+
pushOutput({
|
|
1516
|
+
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
|
|
1517
|
+
action: webSearchAction(e.queries),
|
|
1518
|
+
...(e.sources && e.sources.length > 0 ? { sources: e.sources } : {}),
|
|
1519
|
+
});
|
|
1520
|
+
if (e.sources) {
|
|
1521
|
+
const seen = new Set(pendingWebSources.map(s => s.url));
|
|
1522
|
+
for (const s of e.sources) {
|
|
1523
|
+
if (!seen.has(s.url)) {
|
|
1524
|
+
seen.add(s.url);
|
|
1525
|
+
budget?.chargeRetained(bytesOf(JSON.stringify(s)), { kind: "tool_search_sources" });
|
|
1526
|
+
pendingWebSources.push(s);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
break;
|
|
1531
|
+
case "error":
|
|
1532
|
+
errorEvent = e;
|
|
1533
|
+
usage = e.usage ?? usage;
|
|
1534
|
+
break;
|
|
1535
|
+
case "incomplete":
|
|
1536
|
+
incompleteEvent = e;
|
|
1537
|
+
endTurn = e.endTurn;
|
|
1538
|
+
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
1539
|
+
break;
|
|
1540
|
+
case "done":
|
|
1541
|
+
usage = e.usage;
|
|
1542
|
+
endTurn = e.endTurn;
|
|
1543
|
+
cleanDone = e.stopReason === undefined;
|
|
1544
|
+
if (e.providerState) options?.onProviderState?.(e.providerState);
|
|
1545
|
+
// Match streaming: max_tokens and content_filter both terminate as incomplete.
|
|
1546
|
+
if (e.stopReason === "max_tokens" || e.stopReason === "content_filter") stopReason = e.stopReason;
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1549
|
+
if (budget) releaseTranslatedEvent(e, budget);
|
|
1550
|
+
}
|
|
1551
|
+
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
1552
|
+
flushSummaryReasoning();
|
|
1553
|
+
flushRawReasoning();
|
|
1554
|
+
// Open tool call on a failed/incomplete turn must not land as status:"completed".
|
|
1555
|
+
if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent ? "incomplete" : "completed");
|
|
1556
|
+
// A truncated turn must never be installed as replacement history: emit the
|
|
1557
|
+
// compaction item only when the turn actually completed (#422).
|
|
1558
|
+
if (
|
|
1559
|
+
options?.compaction
|
|
1560
|
+
&& !errorEvent
|
|
1561
|
+
&& !incompleteEvent
|
|
1562
|
+
&& stopReason !== "max_tokens"
|
|
1563
|
+
&& stopReason !== "content_filter"
|
|
1564
|
+
) {
|
|
1565
|
+
pushOutput({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) }, compactionTextBytes);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
|
|
1569
|
+
const status = errorEvent
|
|
1570
|
+
? "failed"
|
|
1571
|
+
: incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
|
|
1572
|
+
? "incomplete"
|
|
1573
|
+
: "completed";
|
|
1574
|
+
options?.onUsage?.(incompleteEvent?.usage ?? usage);
|
|
1575
|
+
return {
|
|
1576
|
+
id: responseId, object: "response",
|
|
1577
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
1578
|
+
status,
|
|
1579
|
+
model: modelId, output,
|
|
1580
|
+
...(endTurn !== undefined ? { end_turn: endTurn } : {}),
|
|
1581
|
+
...(failure ? { error: failure.error, last_error: failure.error } : {}),
|
|
1582
|
+
...(errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
|
|
1583
|
+
...(incompleteEvent ? {
|
|
1584
|
+
incomplete_details: {
|
|
1585
|
+
reason: incompleteEvent.reason,
|
|
1586
|
+
...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
|
|
1587
|
+
...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
|
|
1588
|
+
},
|
|
1589
|
+
} : stopReason === "max_tokens" ? {
|
|
1590
|
+
incomplete_details: { reason: "max_output_tokens" },
|
|
1591
|
+
} : stopReason === "content_filter" ? {
|
|
1592
|
+
incomplete_details: { reason: "content_filter" },
|
|
1593
|
+
} : {}),
|
|
1594
|
+
usage: responsesUsage(incompleteEvent?.usage ?? usage),
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
export function formatErrorResponse(
|
|
1599
|
+
status: number,
|
|
1600
|
+
type: string,
|
|
1601
|
+
message: string,
|
|
1602
|
+
options?: { code?: string | null; retryAfter?: string | null },
|
|
1603
|
+
): Response {
|
|
1604
|
+
const error = classifyError(status, type, message);
|
|
1605
|
+
if (isCyberPolicyCode(options?.code)) {
|
|
1606
|
+
error.code = CYBER_POLICY_ERROR_CODE;
|
|
1607
|
+
error.type = "invalid_request_error";
|
|
1608
|
+
}
|
|
1609
|
+
const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
|
|
1610
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
1611
|
+
const retryAfter = options?.retryAfter?.trim();
|
|
1612
|
+
if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
|
|
1613
|
+
headers.set("Retry-After", retryAfter);
|
|
1614
|
+
}
|
|
1615
|
+
return new Response(JSON.stringify({ error }), {
|
|
1616
|
+
status: finalStatus,
|
|
1617
|
+
headers,
|
|
1618
|
+
});
|
|
1619
|
+
}
|