@iislee/opencodex 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +225 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +471 -0
- package/src/service.ts +2554 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code outbound: internal /v1/responses output -> Anthropic Messages API shapes.
|
|
3
|
+
*
|
|
4
|
+
* Wire contract pinned in devlog/260711_claude_inbound/003_evidence.md (all Tier 2):
|
|
5
|
+
* - SSE order: message_start -> (content_block_start -> deltas -> content_block_stop)*
|
|
6
|
+
* -> message_delta -> message_stop; any number of `ping`.
|
|
7
|
+
* - thinking blocks get thinking_delta(s) then ONE synthetic signature_delta just
|
|
8
|
+
* before content_block_stop (CCR precedent: Claude Code does not verify signatures).
|
|
9
|
+
* - message_delta.usage is cumulative; message_start embeds a full message snapshot.
|
|
10
|
+
* - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
|
|
11
|
+
*/
|
|
12
|
+
import { isTransientUpstreamStatus } from "../lib/upstream-retry";
|
|
13
|
+
import {
|
|
14
|
+
isTranslatorBudgetExceededError,
|
|
15
|
+
TRANSLATOR_MAX_TURN_BYTES,
|
|
16
|
+
TranslatorBudgetExceededError,
|
|
17
|
+
type TranslatorBudget,
|
|
18
|
+
} from "../lib/translator-budget";
|
|
19
|
+
|
|
20
|
+
type Rec = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function isRec(v: unknown): v is Rec {
|
|
23
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function uuid(): string {
|
|
27
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** HTTP status -> Anthropic error taxonomy (010 amendment #4; full official table per devlog 100). */
|
|
31
|
+
export function anthropicErrorType(status: number): string {
|
|
32
|
+
switch (status) {
|
|
33
|
+
case 400: return "invalid_request_error";
|
|
34
|
+
case 401: return "authentication_error";
|
|
35
|
+
case 402: return "billing_error";
|
|
36
|
+
case 403: return "permission_error";
|
|
37
|
+
case 404: return "not_found_error";
|
|
38
|
+
case 409: return "conflict_error";
|
|
39
|
+
case 413: return "request_too_large";
|
|
40
|
+
case 429: return "rate_limit_error";
|
|
41
|
+
case 504: return "timeout_error";
|
|
42
|
+
case 529: return "overloaded_error";
|
|
43
|
+
default: return status >= 500 ? "api_error" : "invalid_request_error";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function anthropicErrorBody(status: number, message: string, type?: string, code?: string): Rec {
|
|
48
|
+
return { type: "error", error: { type: type ?? anthropicErrorType(status), message, ...(code ? { code } : {}) } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function anthropicErrorResponse(status: number, message: string, type?: string, code?: string): Response {
|
|
52
|
+
return new Response(JSON.stringify(anthropicErrorBody(status, message, type, code)), {
|
|
53
|
+
status,
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Responses usage -> Anthropic usage. Responses `input_tokens` is INCLUSIVE of cache
|
|
60
|
+
* read+write (types.ts convention); Anthropic `input_tokens` excludes both, so
|
|
61
|
+
* subtract the full cache detail (devlog 070 — subtracting reads only inflated the
|
|
62
|
+
* non-cached input Claude Code displays by the write share).
|
|
63
|
+
*/
|
|
64
|
+
export function anthropicUsage(usage: unknown, webSearchRequests = 0): Rec {
|
|
65
|
+
const u = isRec(usage) ? usage : {};
|
|
66
|
+
const details = isRec(u.input_tokens_details) ? u.input_tokens_details : {};
|
|
67
|
+
const cached = typeof details.cached_tokens === "number" ? details.cached_tokens : 0;
|
|
68
|
+
const cacheWrite = typeof details.cache_write_tokens === "number" ? details.cache_write_tokens : 0;
|
|
69
|
+
const input = typeof u.input_tokens === "number" ? u.input_tokens : 0;
|
|
70
|
+
const output = typeof u.output_tokens === "number" ? u.output_tokens : 0;
|
|
71
|
+
return {
|
|
72
|
+
input_tokens: Math.max(0, input - cached - cacheWrite),
|
|
73
|
+
output_tokens: output,
|
|
74
|
+
cache_read_input_tokens: cached,
|
|
75
|
+
cache_creation_input_tokens: cacheWrite,
|
|
76
|
+
// Only successful searches are billed/counted (Anthropic contract; Claude Code cost accounting).
|
|
77
|
+
...(webSearchRequests > 0 ? { server_tool_use: { web_search_requests: webSearchRequests } } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sseFrame(name: string, data: Rec): string {
|
|
82
|
+
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
|
|
86
|
+
let nextBytes = previousBytes + Buffer.byteLength(fragment);
|
|
87
|
+
const previousLast = previous.charCodeAt(previous.length - 1);
|
|
88
|
+
const fragmentFirst = fragment.charCodeAt(0);
|
|
89
|
+
if (previousLast >= 0xd800 && previousLast <= 0xdbff
|
|
90
|
+
&& fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
|
|
91
|
+
nextBytes -= 2;
|
|
92
|
+
}
|
|
93
|
+
return nextBytes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Claude Code / Anthropic WebSearch domain filters are optional and mutually exclusive.
|
|
98
|
+
* Empty arrays are rejected ("ambiguous"); both fields together are rejected. Routed models
|
|
99
|
+
* often emit both shapes — sanitize before Claude Code sees the tool_use / server_tool_use
|
|
100
|
+
* input (issue #381).
|
|
101
|
+
*/
|
|
102
|
+
export function isClaudeWebSearchToolName(name: string): boolean {
|
|
103
|
+
const trimmed = name.trim();
|
|
104
|
+
return trimmed === "WebSearch" || /^web_search/i.test(trimmed);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeWebSearchDomainList(value: unknown): string[] | undefined {
|
|
108
|
+
if (!Array.isArray(value)) return undefined;
|
|
109
|
+
const domains = value
|
|
110
|
+
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
|
111
|
+
.map(entry => entry.trim());
|
|
112
|
+
return domains.length > 0 ? domains : undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Strip empty domain filters; if both remain, keep `allowed_domains` and drop `blocked_domains`. */
|
|
116
|
+
export function sanitizeWebSearchInput(input: unknown): Rec {
|
|
117
|
+
const src: Rec = isRec(input) ? { ...input } : {};
|
|
118
|
+
const allowed = normalizeWebSearchDomainList(src.allowed_domains);
|
|
119
|
+
const blocked = normalizeWebSearchDomainList(src.blocked_domains);
|
|
120
|
+
delete src.allowed_domains;
|
|
121
|
+
delete src.blocked_domains;
|
|
122
|
+
if (allowed && blocked) {
|
|
123
|
+
// Prefer allow-list (restrict-to) when a routed model sets both non-empty fields.
|
|
124
|
+
src.allowed_domains = allowed;
|
|
125
|
+
} else if (allowed) {
|
|
126
|
+
src.allowed_domains = allowed;
|
|
127
|
+
} else if (blocked) {
|
|
128
|
+
src.blocked_domains = blocked;
|
|
129
|
+
}
|
|
130
|
+
return src;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Map a Responses `web_search_call` item to its Anthropic pair: the server_tool_use
|
|
135
|
+
* input (query/queries) and the web_search_tool_result content (hits, or the error
|
|
136
|
+
* object when the search failed). Shared by the SSE and JSON translation paths.
|
|
137
|
+
*/
|
|
138
|
+
function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultContent: unknown; completed: boolean } {
|
|
139
|
+
const action = isRec(item.action) ? item.action : {};
|
|
140
|
+
const queries = Array.isArray(action.queries)
|
|
141
|
+
? action.queries.filter((q): q is string => typeof q === "string" && q.length > 0)
|
|
142
|
+
: [];
|
|
143
|
+
const query = typeof action.query === "string" ? action.query : "";
|
|
144
|
+
const input = sanitizeWebSearchInput(
|
|
145
|
+
queries.length > 1 ? { queries } : { query: queries[0] ?? query },
|
|
146
|
+
);
|
|
147
|
+
const completed = item.status !== "failed";
|
|
148
|
+
let resultContent: unknown;
|
|
149
|
+
if (completed) {
|
|
150
|
+
const hits: Rec[] = [];
|
|
151
|
+
if (Array.isArray(item.sources)) {
|
|
152
|
+
for (const s of item.sources) {
|
|
153
|
+
if (isRec(s) && typeof s.url === "string" && s.url.length > 0) {
|
|
154
|
+
hits.push({ type: "web_search_result", title: typeof s.title === "string" ? s.title : "", url: s.url });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
resultContent = hits;
|
|
159
|
+
} else {
|
|
160
|
+
resultContent = { type: "web_search_tool_result_error", error_code: "unavailable" };
|
|
161
|
+
}
|
|
162
|
+
const id = typeof item.id === "string" && item.id.length > 0 ? item.id : `srvtoolu_${uuid()}`;
|
|
163
|
+
return { id, input, resultContent, completed };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function messageSnapshot(model: string): Rec {
|
|
167
|
+
return {
|
|
168
|
+
id: `msg_${uuid()}`,
|
|
169
|
+
type: "message",
|
|
170
|
+
role: "assistant",
|
|
171
|
+
content: [],
|
|
172
|
+
model,
|
|
173
|
+
stop_reason: null,
|
|
174
|
+
stop_sequence: null,
|
|
175
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface OpenBlock {
|
|
180
|
+
kind: "text" | "thinking" | "tool_use";
|
|
181
|
+
index: number;
|
|
182
|
+
/** Responses item_id (tool calls) so output_item.done can match. */
|
|
183
|
+
itemId?: string;
|
|
184
|
+
/** Buffer WebSearch args and emit one sanitized input_json_delta on close (#381). */
|
|
185
|
+
bufferWebSearchArgs?: boolean;
|
|
186
|
+
argsBuf?: string;
|
|
187
|
+
argsBufBytes?: number;
|
|
188
|
+
webSearchArgsEmitted?: boolean;
|
|
189
|
+
callId?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
|
|
193
|
+
export function responsesSseToAnthropicSse(
|
|
194
|
+
upstream: ReadableStream<Uint8Array>,
|
|
195
|
+
model: string,
|
|
196
|
+
opts: { pingIntervalMs?: number; translatorBudget: TranslatorBudget },
|
|
197
|
+
): ReadableStream<Uint8Array> {
|
|
198
|
+
const translatorBudget = opts.translatorBudget;
|
|
199
|
+
const pingIntervalMs = opts?.pingIntervalMs ?? 20_000;
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
let buffer = "";
|
|
203
|
+
let bufferBytes = 0;
|
|
204
|
+
let started = false;
|
|
205
|
+
let terminated = false;
|
|
206
|
+
let cancelled = false;
|
|
207
|
+
let blockIndex = 0;
|
|
208
|
+
let open: OpenBlock | null = null;
|
|
209
|
+
let sawToolUse = false;
|
|
210
|
+
let webSearchRequests = 0;
|
|
211
|
+
let pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
212
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
213
|
+
const utf8SliceBytes = (value: string, start: number, end: number): number => {
|
|
214
|
+
let bytes = 0;
|
|
215
|
+
for (let index = start; index < end; index++) {
|
|
216
|
+
const codePoint = value.codePointAt(index)!;
|
|
217
|
+
if (codePoint <= 0x7f) bytes += 1;
|
|
218
|
+
else if (codePoint <= 0x7ff) bytes += 2;
|
|
219
|
+
else if (codePoint <= 0xffff) bytes += 3;
|
|
220
|
+
else { bytes += 4; index += 1; }
|
|
221
|
+
}
|
|
222
|
+
return bytes;
|
|
223
|
+
};
|
|
224
|
+
const queuedLiveFrameBytes: number[] = [];
|
|
225
|
+
const releaseDeliveredFrame = () => {
|
|
226
|
+
const bytes = queuedLiveFrameBytes.shift();
|
|
227
|
+
if (bytes !== undefined) translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return new ReadableStream<Uint8Array>({
|
|
231
|
+
start(controller) {
|
|
232
|
+
const emit = (name: string, data: Rec) => {
|
|
233
|
+
const frame = encoder.encode(sseFrame(name, data));
|
|
234
|
+
const reservation = translatorBudget.reserveTransient(frame.byteLength, { kind: "live_transient" });
|
|
235
|
+
controller.enqueue(frame);
|
|
236
|
+
reservation.commitRetained();
|
|
237
|
+
queuedLiveFrameBytes.push(frame.byteLength);
|
|
238
|
+
};
|
|
239
|
+
const ensureStarted = () => {
|
|
240
|
+
if (started) return;
|
|
241
|
+
started = true;
|
|
242
|
+
emit("message_start", { type: "message_start", message: messageSnapshot(model) });
|
|
243
|
+
emit("ping", { type: "ping" });
|
|
244
|
+
};
|
|
245
|
+
// Idle keepalive (devlog 100): real Anthropic streams may interleave pings anywhere;
|
|
246
|
+
// synthesizing one during upstream silence protects remote deployments behind
|
|
247
|
+
// LB/NAT idle timeouts and covers slow first tokens. Cheap and spec-legal.
|
|
248
|
+
if (pingIntervalMs > 0) {
|
|
249
|
+
pingTimer = setInterval(() => {
|
|
250
|
+
if (terminated) return;
|
|
251
|
+
try {
|
|
252
|
+
ensureStarted();
|
|
253
|
+
emit("ping", { type: "ping" });
|
|
254
|
+
} catch { /* controller torn down; the read loop is ending anyway */ }
|
|
255
|
+
}, pingIntervalMs);
|
|
256
|
+
}
|
|
257
|
+
const closeOpenBlock = () => {
|
|
258
|
+
if (!open) return;
|
|
259
|
+
if (open.kind === "tool_use" && open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
|
|
260
|
+
// Emit sanitized args even if output_item.done never supplied a full arguments field
|
|
261
|
+
// (finish/fail paths, or deltas-only streams).
|
|
262
|
+
let parsed: unknown = {};
|
|
263
|
+
const rawArgs = open.argsBuf ?? "";
|
|
264
|
+
try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
|
|
265
|
+
emit("content_block_delta", {
|
|
266
|
+
type: "content_block_delta",
|
|
267
|
+
index: open.index,
|
|
268
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
|
|
269
|
+
});
|
|
270
|
+
open.webSearchArgsEmitted = true;
|
|
271
|
+
}
|
|
272
|
+
if (open.kind === "thinking") {
|
|
273
|
+
// Synthetic signature: Claude Code accepts it (003 E6); inbound drops replays anyway.
|
|
274
|
+
emit("content_block_delta", {
|
|
275
|
+
type: "content_block_delta", index: open.index,
|
|
276
|
+
delta: { type: "signature_delta", signature: `ocx${Date.now()}` },
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
emit("content_block_stop", { type: "content_block_stop", index: open.index });
|
|
280
|
+
if (open.callId) translatorBudget.closeCall(open.callId);
|
|
281
|
+
open = null;
|
|
282
|
+
};
|
|
283
|
+
const ensureBlock = (kind: "text" | "thinking") => {
|
|
284
|
+
ensureStarted();
|
|
285
|
+
if (open && open.kind === kind) return;
|
|
286
|
+
closeOpenBlock();
|
|
287
|
+
const index = blockIndex++;
|
|
288
|
+
const contentBlock: Rec = kind === "text"
|
|
289
|
+
? { type: "text", text: "" }
|
|
290
|
+
: { type: "thinking", thinking: "", signature: "" };
|
|
291
|
+
emit("content_block_start", { type: "content_block_start", index, content_block: contentBlock });
|
|
292
|
+
open = { kind, index };
|
|
293
|
+
};
|
|
294
|
+
const finish = (stopReason: string, usage: unknown) => {
|
|
295
|
+
if (terminated) return;
|
|
296
|
+
terminated = true;
|
|
297
|
+
ensureStarted();
|
|
298
|
+
closeOpenBlock();
|
|
299
|
+
emit("message_delta", {
|
|
300
|
+
type: "message_delta",
|
|
301
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
302
|
+
usage: anthropicUsage(usage, webSearchRequests),
|
|
303
|
+
});
|
|
304
|
+
emit("message_stop", { type: "message_stop" });
|
|
305
|
+
};
|
|
306
|
+
// upstreamDerived: transient upstream statuses become overloaded_error so the
|
|
307
|
+
// Anthropic-SDK client retries with backoff; proxy-internal exceptions stay
|
|
308
|
+
// api_error — a deterministic ocx bug must not be masked as retryable
|
|
309
|
+
// (devlog/_plan/260716_claudecode_hardening/020). On win32 mid-stream socket
|
|
310
|
+
// resets reach the reader catch (no failed-tail relay) and stay api_error —
|
|
311
|
+
// same as today, deliberate residual.
|
|
312
|
+
const fail = (status: number, message: string, upstreamDerived = false, code?: string) => {
|
|
313
|
+
if (terminated) return;
|
|
314
|
+
terminated = true;
|
|
315
|
+
if (code === "translation_buffer_limit") {
|
|
316
|
+
if (open?.callId) translatorBudget.closeCall(open.callId);
|
|
317
|
+
open = null;
|
|
318
|
+
// No normal close frames are valid after overflow. Emit exactly one bounded
|
|
319
|
+
// typed terminal without consulting the exhausted budget.
|
|
320
|
+
controller.enqueue(encoder.encode(sseFrame("error", anthropicErrorBody(
|
|
321
|
+
413,
|
|
322
|
+
message,
|
|
323
|
+
"request_too_large",
|
|
324
|
+
"translation_buffer_limit",
|
|
325
|
+
))));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
ensureStarted();
|
|
329
|
+
closeOpenBlock();
|
|
330
|
+
const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
|
|
331
|
+
emit("error", anthropicErrorBody(status, message, type, code));
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleFrame = (eventName: string, data: Rec) => {
|
|
335
|
+
switch (eventName) {
|
|
336
|
+
case "response.created":
|
|
337
|
+
ensureStarted();
|
|
338
|
+
break;
|
|
339
|
+
case "response.heartbeat":
|
|
340
|
+
ensureStarted();
|
|
341
|
+
emit("ping", { type: "ping" });
|
|
342
|
+
break;
|
|
343
|
+
case "response.output_text.delta": {
|
|
344
|
+
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
|
345
|
+
ensureBlock("text");
|
|
346
|
+
emit("content_block_delta", {
|
|
347
|
+
type: "content_block_delta", index: open!.index,
|
|
348
|
+
delta: { type: "text_delta", text: data.delta },
|
|
349
|
+
});
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "response.reasoning_summary_text.delta":
|
|
353
|
+
case "response.reasoning_text.delta": {
|
|
354
|
+
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
|
355
|
+
ensureBlock("thinking");
|
|
356
|
+
emit("content_block_delta", {
|
|
357
|
+
type: "content_block_delta", index: open!.index,
|
|
358
|
+
delta: { type: "thinking_delta", thinking: data.delta },
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "response.output_item.added": {
|
|
363
|
+
const item = isRec(data.item) ? data.item : null;
|
|
364
|
+
if (!item || item.type !== "function_call") break;
|
|
365
|
+
ensureStarted();
|
|
366
|
+
closeOpenBlock();
|
|
367
|
+
sawToolUse = true;
|
|
368
|
+
const index = blockIndex++;
|
|
369
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
370
|
+
const bufferWebSearchArgs = isClaudeWebSearchToolName(name);
|
|
371
|
+
const callId = typeof item.call_id === "string" ? item.call_id : `toolu_${uuid()}`;
|
|
372
|
+
emit("content_block_start", {
|
|
373
|
+
type: "content_block_start", index,
|
|
374
|
+
content_block: {
|
|
375
|
+
type: "tool_use",
|
|
376
|
+
id: callId,
|
|
377
|
+
name,
|
|
378
|
+
input: {},
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
translatorBudget.openCall(callId);
|
|
382
|
+
open = {
|
|
383
|
+
kind: "tool_use",
|
|
384
|
+
index,
|
|
385
|
+
callId,
|
|
386
|
+
itemId: typeof item.id === "string" ? item.id : undefined,
|
|
387
|
+
bufferWebSearchArgs,
|
|
388
|
+
argsBuf: "",
|
|
389
|
+
argsBufBytes: 0,
|
|
390
|
+
webSearchArgsEmitted: false,
|
|
391
|
+
};
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "response.function_call_arguments.delta": {
|
|
395
|
+
if (typeof data.delta !== "string" || data.delta.length === 0) break;
|
|
396
|
+
if (!open || open.kind !== "tool_use") break;
|
|
397
|
+
if (open.bufferWebSearchArgs) {
|
|
398
|
+
const previous = open.argsBuf ?? "";
|
|
399
|
+
const previousBytes = open.argsBufBytes ?? 0;
|
|
400
|
+
const nextBytes = appendedUtf8Bytes(previous, previousBytes, data.delta);
|
|
401
|
+
const scope = {
|
|
402
|
+
kind: "tool_args",
|
|
403
|
+
...(open.callId ? { callId: open.callId } : {}),
|
|
404
|
+
} as const;
|
|
405
|
+
const reservation = translatorBudget.reserveTransient(nextBytes, scope);
|
|
406
|
+
try {
|
|
407
|
+
open.argsBuf = previous + data.delta;
|
|
408
|
+
open.argsBufBytes = nextBytes;
|
|
409
|
+
reservation.commitRetained();
|
|
410
|
+
translatorBudget.releaseRetained(previousBytes, scope);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
reservation.release();
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
emit("content_block_delta", {
|
|
418
|
+
type: "content_block_delta", index: open.index,
|
|
419
|
+
delta: { type: "input_json_delta", partial_json: data.delta },
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "response.output_item.done": {
|
|
424
|
+
const item = isRec(data.item) ? data.item : null;
|
|
425
|
+
if (!item) break;
|
|
426
|
+
// Server-side web search (native passthrough or sidecar bridge): translate the
|
|
427
|
+
// finished call into the Anthropic pair Claude Code natively parses —
|
|
428
|
+
// server_tool_use (query via input_json_delta) + web_search_tool_result.
|
|
429
|
+
// Never marks sawToolUse (stop_reason stays end_turn unless a real tool ran).
|
|
430
|
+
if (item.type === "web_search_call") {
|
|
431
|
+
ensureStarted();
|
|
432
|
+
closeOpenBlock();
|
|
433
|
+
const pair = webSearchPairFromItem(item);
|
|
434
|
+
const toolIndex = blockIndex++;
|
|
435
|
+
emit("content_block_start", {
|
|
436
|
+
type: "content_block_start", index: toolIndex,
|
|
437
|
+
content_block: { type: "server_tool_use", id: pair.id, name: "web_search" },
|
|
438
|
+
});
|
|
439
|
+
emit("content_block_delta", {
|
|
440
|
+
type: "content_block_delta", index: toolIndex,
|
|
441
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(pair.input) },
|
|
442
|
+
});
|
|
443
|
+
emit("content_block_stop", { type: "content_block_stop", index: toolIndex });
|
|
444
|
+
const resultIndex = blockIndex++;
|
|
445
|
+
emit("content_block_start", {
|
|
446
|
+
type: "content_block_start", index: resultIndex,
|
|
447
|
+
content_block: { type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent },
|
|
448
|
+
});
|
|
449
|
+
emit("content_block_stop", { type: "content_block_stop", index: resultIndex });
|
|
450
|
+
if (pair.completed) webSearchRequests++;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
if (!open) break;
|
|
454
|
+
// Close the matching open block (message/reasoning items close implicitly on
|
|
455
|
+
// the next block; function_call items must close here so tool input parses).
|
|
456
|
+
if (open.kind === "tool_use" && item.type === "function_call") {
|
|
457
|
+
if (open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
|
|
458
|
+
const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0
|
|
459
|
+
? item.arguments
|
|
460
|
+
: (open.argsBuf ?? "");
|
|
461
|
+
let parsed: unknown = {};
|
|
462
|
+
try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
|
|
463
|
+
emit("content_block_delta", {
|
|
464
|
+
type: "content_block_delta",
|
|
465
|
+
index: open.index,
|
|
466
|
+
delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
|
|
467
|
+
});
|
|
468
|
+
open.webSearchArgsEmitted = true;
|
|
469
|
+
}
|
|
470
|
+
closeOpenBlock();
|
|
471
|
+
}
|
|
472
|
+
else if (open.kind === "text" && item.type === "message") closeOpenBlock();
|
|
473
|
+
else if (open.kind === "thinking" && item.type === "reasoning") closeOpenBlock();
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case "response.completed": {
|
|
477
|
+
const response = isRec(data.response) ? data.response : {};
|
|
478
|
+
if (response.end_turn === false && !sawToolUse) {
|
|
479
|
+
fail(529, "upstream turn ended without a final answer", true);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
finish(sawToolUse ? "tool_use" : "end_turn", response.usage);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case "response.incomplete": {
|
|
486
|
+
const response = isRec(data.response) ? data.response : {};
|
|
487
|
+
const details = isRec(response.incomplete_details) ? response.incomplete_details : {};
|
|
488
|
+
if (details.reason === "max_output_tokens") {
|
|
489
|
+
finish("max_tokens", response.usage);
|
|
490
|
+
} else if (details.reason === "content_filter") {
|
|
491
|
+
finish("refusal", response.usage);
|
|
492
|
+
} else {
|
|
493
|
+
const message = typeof details.message === "string" && details.message.trim()
|
|
494
|
+
? details.message
|
|
495
|
+
: `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
|
|
496
|
+
fail(529, message, true);
|
|
497
|
+
}
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case "response.failed": {
|
|
501
|
+
const response = isRec(data.response) ? data.response : {};
|
|
502
|
+
const error = isRec(response.error) ? response.error : {};
|
|
503
|
+
const message = typeof error.message === "string" ? error.message : "upstream request failed";
|
|
504
|
+
const code = typeof error.code === "string" ? error.code : undefined;
|
|
505
|
+
if (code === "translation_buffer_limit") {
|
|
506
|
+
throw new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES);
|
|
507
|
+
}
|
|
508
|
+
const status = code === "translation_buffer_limit"
|
|
509
|
+
? 413
|
|
510
|
+
: typeof error.status === "number" ? error.status : 500;
|
|
511
|
+
// status-absent response.failed (relaySseWithFailedTail synthetic tail) defaults
|
|
512
|
+
// to 500, which is in the transient set — the mid-stream reset shape maps to
|
|
513
|
+
// overloaded_error by design.
|
|
514
|
+
fail(
|
|
515
|
+
status,
|
|
516
|
+
message,
|
|
517
|
+
true,
|
|
518
|
+
);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
default:
|
|
522
|
+
break; // web_search_call / custom_tool_call / content_part frames: ignored v1
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
reader = upstream.getReader();
|
|
527
|
+
void (async () => {
|
|
528
|
+
try {
|
|
529
|
+
for (;;) {
|
|
530
|
+
const { done, value } = await reader.read();
|
|
531
|
+
if (done) break;
|
|
532
|
+
const decodedReservation = translatorBudget.reserveTransient(value.byteLength, { kind: "live_transient" });
|
|
533
|
+
let decodedCommitted = false;
|
|
534
|
+
try {
|
|
535
|
+
const fragment = decoder.decode(value, { stream: true });
|
|
536
|
+
decodedReservation.commitRetained();
|
|
537
|
+
decodedCommitted = true;
|
|
538
|
+
const nextBufferBytes = appendedUtf8Bytes(buffer, bufferBytes, fragment);
|
|
539
|
+
const appendReservation = translatorBudget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
|
|
540
|
+
try {
|
|
541
|
+
buffer += fragment;
|
|
542
|
+
appendReservation.commitRetained();
|
|
543
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
544
|
+
bufferBytes = nextBufferBytes;
|
|
545
|
+
} catch (error) {
|
|
546
|
+
appendReservation.release();
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
let sep: number;
|
|
550
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
551
|
+
const rawFrameBytes = utf8SliceBytes(buffer, 0, sep);
|
|
552
|
+
const residualBytes = bufferBytes - rawFrameBytes - 2;
|
|
553
|
+
const rawReservation = translatorBudget.reserveTransient(rawFrameBytes, { kind: "live_transient" });
|
|
554
|
+
let residualReservation: ReturnType<TranslatorBudget["reserveTransient"]> | undefined;
|
|
555
|
+
try {
|
|
556
|
+
residualReservation = translatorBudget.reserveTransient(residualBytes, { kind: "live_transient" });
|
|
557
|
+
const rawFrame = buffer.slice(0, sep);
|
|
558
|
+
const residual = buffer.slice(sep + 2);
|
|
559
|
+
rawReservation.commitRetained();
|
|
560
|
+
residualReservation.commitRetained();
|
|
561
|
+
residualReservation = undefined;
|
|
562
|
+
buffer = residual;
|
|
563
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
564
|
+
bufferBytes = residualBytes;
|
|
565
|
+
try {
|
|
566
|
+
let eventName = "";
|
|
567
|
+
let dataLine = "";
|
|
568
|
+
let dataLineBytes = 0;
|
|
569
|
+
try {
|
|
570
|
+
let lineStart = 0;
|
|
571
|
+
while (lineStart <= rawFrame.length) {
|
|
572
|
+
const newline = rawFrame.indexOf("\n", lineStart);
|
|
573
|
+
const lineEnd = newline === -1 ? rawFrame.length : newline;
|
|
574
|
+
if (rawFrame.startsWith("event: ", lineStart)) {
|
|
575
|
+
eventName = rawFrame.slice(lineStart + 7, lineEnd).trim();
|
|
576
|
+
} else if (rawFrame.startsWith("data: ", lineStart)) {
|
|
577
|
+
const fragmentStart = lineStart + 6;
|
|
578
|
+
const fragmentBytes = utf8SliceBytes(rawFrame, fragmentStart, lineEnd);
|
|
579
|
+
const fragmentReservation = translatorBudget.reserveTransient(fragmentBytes, { kind: "live_transient" });
|
|
580
|
+
let fragmentCommitted = false;
|
|
581
|
+
try {
|
|
582
|
+
const fragment = rawFrame.slice(fragmentStart, lineEnd);
|
|
583
|
+
fragmentReservation.commitRetained();
|
|
584
|
+
fragmentCommitted = true;
|
|
585
|
+
const nextDataLineBytes = appendedUtf8Bytes(dataLine, dataLineBytes, fragment);
|
|
586
|
+
const dataReservation = translatorBudget.reserveTransient(nextDataLineBytes, { kind: "live_transient" });
|
|
587
|
+
try {
|
|
588
|
+
dataLine += fragment;
|
|
589
|
+
dataReservation.commitRetained();
|
|
590
|
+
translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
|
|
591
|
+
dataLineBytes = nextDataLineBytes;
|
|
592
|
+
} catch (error) {
|
|
593
|
+
dataReservation.release();
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
} finally {
|
|
597
|
+
if (fragmentCommitted) {
|
|
598
|
+
translatorBudget.releaseRetained(fragmentBytes, { kind: "live_transient" });
|
|
599
|
+
} else fragmentReservation.release();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (newline === -1) break;
|
|
603
|
+
lineStart = newline + 1;
|
|
604
|
+
}
|
|
605
|
+
if (!dataLine) continue;
|
|
606
|
+
let data: unknown;
|
|
607
|
+
try { data = JSON.parse(dataLine); } catch { continue; }
|
|
608
|
+
if (!isRec(data)) continue;
|
|
609
|
+
// Responses-compatible gateways may omit the optional SSE event field
|
|
610
|
+
// while retaining the event name in the JSON payload's required type.
|
|
611
|
+
const resolvedEventName = eventName || (typeof data.type === "string" ? data.type : "");
|
|
612
|
+
if (!resolvedEventName || terminated) continue;
|
|
613
|
+
handleFrame(resolvedEventName, data);
|
|
614
|
+
} finally {
|
|
615
|
+
translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
translatorBudget.releaseRetained(rawFrameBytes, { kind: "live_transient" });
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
rawReservation.release();
|
|
622
|
+
residualReservation?.release();
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} finally {
|
|
627
|
+
if (decodedCommitted) translatorBudget.releaseRetained(value.byteLength, { kind: "live_transient" });
|
|
628
|
+
else decodedReservation.release();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// EOF without a terminal frame is a TRUNCATION, not success (devlog 100:
|
|
632
|
+
// gateways that close such streams politely hand Claude Code an empty/partial
|
|
633
|
+
// turn with no retryable error — CLIProxyAPI#2189 failure pattern). Fail closed
|
|
634
|
+
// with a mid-stream Anthropic error event so the client can retry.
|
|
635
|
+
if (!cancelled) fail(502, "upstream stream ended before a terminal frame (truncated response)", true);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
638
|
+
try { await reader.cancel(err); } catch { /* already closed */ }
|
|
639
|
+
fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
|
|
640
|
+
} else fail(500, err instanceof Error ? err.message : String(err));
|
|
641
|
+
} finally {
|
|
642
|
+
translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
643
|
+
if (pingTimer !== undefined) clearInterval(pingTimer);
|
|
644
|
+
reader.releaseLock();
|
|
645
|
+
if (!cancelled) controller.close();
|
|
646
|
+
}
|
|
647
|
+
})();
|
|
648
|
+
},
|
|
649
|
+
pull() {
|
|
650
|
+
releaseDeliveredFrame();
|
|
651
|
+
},
|
|
652
|
+
cancel(reason) {
|
|
653
|
+
cancelled = true;
|
|
654
|
+
while (queuedLiveFrameBytes.length > 0) releaseDeliveredFrame();
|
|
655
|
+
if (open?.callId) translatorBudget.closeCall(open.callId);
|
|
656
|
+
if (pingTimer !== undefined) clearInterval(pingTimer);
|
|
657
|
+
return reader?.cancel(reason);
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Non-streaming: /v1/responses JSON -> Anthropic message JSON. */
|
|
663
|
+
export function responsesJsonToAnthropicMessage(json: unknown, model: string): Rec {
|
|
664
|
+
const body = isRec(json) ? json : {};
|
|
665
|
+
const output = Array.isArray(body.output) ? body.output : [];
|
|
666
|
+
const content: Rec[] = [];
|
|
667
|
+
let sawToolUse = false;
|
|
668
|
+
let webSearchRequests = 0;
|
|
669
|
+
|
|
670
|
+
for (const raw of output) {
|
|
671
|
+
if (!isRec(raw)) continue;
|
|
672
|
+
switch (raw.type) {
|
|
673
|
+
case "message": {
|
|
674
|
+
if (!Array.isArray(raw.content)) break;
|
|
675
|
+
for (const part of raw.content) {
|
|
676
|
+
if (isRec(part) && part.type === "output_text" && typeof part.text === "string" && part.text.length > 0) {
|
|
677
|
+
content.push({ type: "text", text: part.text });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
case "reasoning": {
|
|
683
|
+
const parts: string[] = [];
|
|
684
|
+
if (Array.isArray(raw.summary)) {
|
|
685
|
+
for (const s of raw.summary) {
|
|
686
|
+
if (isRec(s) && typeof s.text === "string" && s.text.length > 0) parts.push(s.text);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (Array.isArray(raw.content)) {
|
|
690
|
+
for (const s of raw.content) {
|
|
691
|
+
if (isRec(s) && typeof s.text === "string" && s.text.length > 0) parts.push(s.text);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (parts.length > 0) {
|
|
695
|
+
content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: `ocx${Date.now()}` });
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case "function_call": {
|
|
700
|
+
sawToolUse = true;
|
|
701
|
+
let input: unknown = {};
|
|
702
|
+
if (typeof raw.arguments === "string" && raw.arguments.length > 0) {
|
|
703
|
+
try { input = JSON.parse(raw.arguments); } catch { input = {}; }
|
|
704
|
+
}
|
|
705
|
+
const name = typeof raw.name === "string" ? raw.name : "";
|
|
706
|
+
content.push({
|
|
707
|
+
type: "tool_use",
|
|
708
|
+
id: typeof raw.call_id === "string" ? raw.call_id : `toolu_${uuid()}`,
|
|
709
|
+
name,
|
|
710
|
+
input: isClaudeWebSearchToolName(name) ? sanitizeWebSearchInput(input) : input,
|
|
711
|
+
});
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case "web_search_call": {
|
|
715
|
+
// Server-side search: emit the Anthropic pair. Does NOT set sawToolUse.
|
|
716
|
+
const pair = webSearchPairFromItem(raw);
|
|
717
|
+
content.push({ type: "server_tool_use", id: pair.id, name: "web_search", input: pair.input });
|
|
718
|
+
content.push({ type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent });
|
|
719
|
+
if (pair.completed) webSearchRequests++;
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
default:
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const details = isRec(body.incomplete_details) ? body.incomplete_details : {};
|
|
728
|
+
if (body.status === "incomplete"
|
|
729
|
+
&& details.reason !== "max_output_tokens"
|
|
730
|
+
&& details.reason !== "content_filter") {
|
|
731
|
+
const message = typeof details.message === "string" && details.message.trim()
|
|
732
|
+
? details.message
|
|
733
|
+
: `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
|
|
734
|
+
return anthropicErrorBody(529, message, "overloaded_error");
|
|
735
|
+
}
|
|
736
|
+
if (body.status === "completed" && body.end_turn === false && !sawToolUse) {
|
|
737
|
+
return anthropicErrorBody(529, "upstream turn ended without a final answer", "overloaded_error");
|
|
738
|
+
}
|
|
739
|
+
const stopReason = body.status === "incomplete" && details.reason === "max_output_tokens"
|
|
740
|
+
? "max_tokens"
|
|
741
|
+
: body.status === "incomplete" && details.reason === "content_filter"
|
|
742
|
+
? "refusal"
|
|
743
|
+
: sawToolUse ? "tool_use" : "end_turn";
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
id: `msg_${uuid()}`,
|
|
747
|
+
type: "message",
|
|
748
|
+
role: "assistant",
|
|
749
|
+
content,
|
|
750
|
+
model,
|
|
751
|
+
stop_reason: stopReason,
|
|
752
|
+
stop_sequence: null,
|
|
753
|
+
usage: anthropicUsage(body.usage, webSearchRequests),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Fold an Anthropic SSE stream (our own emission vocabulary) into a message JSON.
|
|
759
|
+
* Used for non-streaming client requests: the internal replay always streams
|
|
760
|
+
* (routed adapters do not support non-stream turns), so the translated stream is
|
|
761
|
+
* aggregated here instead of translating a JSON body.
|
|
762
|
+
*/
|
|
763
|
+
export async function collectAnthropicMessage(
|
|
764
|
+
stream: ReadableStream<Uint8Array>,
|
|
765
|
+
model: string,
|
|
766
|
+
translatorBudget: TranslatorBudget,
|
|
767
|
+
): Promise<Rec> {
|
|
768
|
+
const decoder = new TextDecoder();
|
|
769
|
+
const reader = stream.getReader();
|
|
770
|
+
let buffer = "";
|
|
771
|
+
const content: Rec[] = [];
|
|
772
|
+
let openBlock: Rec | null = null;
|
|
773
|
+
let toolJson = "";
|
|
774
|
+
let stopReason: string | null = "end_turn";
|
|
775
|
+
let usage: Rec = anthropicUsage(undefined);
|
|
776
|
+
let error: Rec | null = null;
|
|
777
|
+
const replaceRetained = (previous: string, next: string, kind: "live_transient" | "retained_collectors") => {
|
|
778
|
+
const reservation = translatorBudget.reserveTransient(Buffer.byteLength(next), { kind });
|
|
779
|
+
reservation.commitRetained();
|
|
780
|
+
translatorBudget.releaseRetained(Buffer.byteLength(previous), { kind });
|
|
781
|
+
return next;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const closeBlock = () => {
|
|
785
|
+
if (!openBlock) return;
|
|
786
|
+
// server_tool_use streams its query via input_json_delta exactly like tool_use (audit F3).
|
|
787
|
+
if (openBlock.type === "tool_use" || openBlock.type === "server_tool_use") {
|
|
788
|
+
try { openBlock.input = toolJson.length > 0 ? JSON.parse(toolJson) : {}; } catch { openBlock.input = {}; }
|
|
789
|
+
}
|
|
790
|
+
translatorBudget.chargeRetained(Buffer.byteLength(JSON.stringify(openBlock)), { kind: "retained_collectors" });
|
|
791
|
+
content.push(openBlock);
|
|
792
|
+
openBlock = null;
|
|
793
|
+
toolJson = "";
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const handle = (name: string, data: Rec) => {
|
|
797
|
+
switch (name) {
|
|
798
|
+
case "content_block_start":
|
|
799
|
+
closeBlock();
|
|
800
|
+
if (isRec(data.content_block)) openBlock = { ...data.content_block };
|
|
801
|
+
break;
|
|
802
|
+
case "content_block_delta": {
|
|
803
|
+
const delta = isRec(data.delta) ? data.delta : {};
|
|
804
|
+
if (!openBlock) break;
|
|
805
|
+
if (delta.type === "text_delta" && typeof delta.text === "string") {
|
|
806
|
+
const previous = typeof openBlock.text === "string" ? openBlock.text : "";
|
|
807
|
+
openBlock.text = replaceRetained(previous, previous + delta.text, "retained_collectors");
|
|
808
|
+
} else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
809
|
+
const previous = typeof openBlock.thinking === "string" ? openBlock.thinking : "";
|
|
810
|
+
openBlock.thinking = replaceRetained(previous, previous + delta.thinking, "retained_collectors");
|
|
811
|
+
} else if (delta.type === "signature_delta" && typeof delta.signature === "string") {
|
|
812
|
+
openBlock.signature = delta.signature;
|
|
813
|
+
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
814
|
+
toolJson = replaceRetained(toolJson, toolJson + delta.partial_json, "retained_collectors");
|
|
815
|
+
}
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
case "content_block_stop":
|
|
819
|
+
closeBlock();
|
|
820
|
+
break;
|
|
821
|
+
case "message_delta": {
|
|
822
|
+
const delta = isRec(data.delta) ? data.delta : {};
|
|
823
|
+
if (typeof delta.stop_reason === "string") stopReason = delta.stop_reason;
|
|
824
|
+
if (isRec(data.usage)) usage = data.usage;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "error":
|
|
828
|
+
error = data;
|
|
829
|
+
break;
|
|
830
|
+
default:
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
for (;;) {
|
|
837
|
+
const { done, value } = await reader.read();
|
|
838
|
+
if (done) break;
|
|
839
|
+
buffer = replaceRetained(buffer, buffer + decoder.decode(value, { stream: true }), "live_transient");
|
|
840
|
+
let sep: number;
|
|
841
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
842
|
+
const rawFrame = buffer.slice(0, sep);
|
|
843
|
+
buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
|
|
844
|
+
let eventName = "";
|
|
845
|
+
let dataLine = "";
|
|
846
|
+
for (const line of rawFrame.split("\n")) {
|
|
847
|
+
if (line.startsWith("event: ")) eventName = line.slice(7).trim();
|
|
848
|
+
else if (line.startsWith("data: ")) dataLine += line.slice(6);
|
|
849
|
+
}
|
|
850
|
+
if (!eventName || !dataLine) continue;
|
|
851
|
+
let data: unknown;
|
|
852
|
+
try { data = JSON.parse(dataLine); } catch { continue; }
|
|
853
|
+
if (isRec(data)) handle(eventName, data);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} finally {
|
|
857
|
+
reader.releaseLock();
|
|
858
|
+
}
|
|
859
|
+
closeBlock();
|
|
860
|
+
|
|
861
|
+
if (error) return error;
|
|
862
|
+
return {
|
|
863
|
+
id: `msg_${uuid()}`,
|
|
864
|
+
type: "message",
|
|
865
|
+
role: "assistant",
|
|
866
|
+
content,
|
|
867
|
+
model,
|
|
868
|
+
stop_reason: stopReason,
|
|
869
|
+
stop_sequence: null,
|
|
870
|
+
usage,
|
|
871
|
+
};
|
|
872
|
+
}
|