@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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic image normalization: resize/re-encode images to fit Anthropic's request
|
|
3
|
+
* limits instead of dropping them (devlog/260714_image_normalization_pipeline/020).
|
|
4
|
+
*
|
|
5
|
+
* Age-tier pyramid: newest images keep near-full fidelity, older images become
|
|
6
|
+
* progressively smaller JPEG thumbnails, so a whole session's screenshots stay visible
|
|
7
|
+
* under the request byte budget. An aggregate demotion loop re-encodes the OLDEST
|
|
8
|
+
* not-yet-terminal image one ladder position at a time until the total fits; only when
|
|
9
|
+
* every image is terminal-floored does the guard's Rule 4 (textify) fire as backstop.
|
|
10
|
+
*
|
|
11
|
+
* Runs inside the anthropic adapter's buildRequest BEFORE enforceAnthropicImageLimits,
|
|
12
|
+
* on freshly-built wire messages (in-place mutation is safe: messagesToAnthropicFormat
|
|
13
|
+
* creates new arrays/blocks). Encoding uses Bun.Image (bun >= 1.3.14, probe-verified:
|
|
14
|
+
* decodes JPEG/PNG/WebP/GIF/BMP/TIFF/HEIC/AVIF; corrupt input throws).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
collectImageRefs,
|
|
19
|
+
sniffImageDimensions,
|
|
20
|
+
TOTAL_IMAGE_BASE64_BUDGET,
|
|
21
|
+
type ImageBlockRef,
|
|
22
|
+
} from "./anthropic-image-guard";
|
|
23
|
+
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
24
|
+
|
|
25
|
+
/** One ladder position: dimension cap, JPEG quality attempts, per-image base64 cap. */
|
|
26
|
+
export interface TierSpec {
|
|
27
|
+
maxEdge: number;
|
|
28
|
+
qualities: number[];
|
|
29
|
+
/** Hard per-image base64-length cap at this position; Infinity = terminal (measured size accepted). */
|
|
30
|
+
hardCap: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const KiB = 1024;
|
|
34
|
+
const MiB = 1024 * 1024;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ladder positions 0-5. 0-2 are the age-assigned tiers; 3-5 are demotion floor steps.
|
|
38
|
+
* Terminal (last) accepts its measured output so the aggregate loop always terminates
|
|
39
|
+
* (audit round 2, blocker 1).
|
|
40
|
+
*/
|
|
41
|
+
export const TIER_SPECS: TierSpec[] = [
|
|
42
|
+
{ maxEdge: 2000, qualities: [80, 60, 40, 30], hardCap: 2 * MiB },
|
|
43
|
+
{ maxEdge: 1024, qualities: [70, 50], hardCap: 512 * KiB },
|
|
44
|
+
{ maxEdge: 700, qualities: [60, 40], hardCap: 192 * KiB },
|
|
45
|
+
{ maxEdge: 500, qualities: [40], hardCap: 100 * KiB },
|
|
46
|
+
{ maxEdge: 400, qualities: [30], hardCap: 100 * KiB },
|
|
47
|
+
{ maxEdge: 320, qualities: [25], hardCap: Infinity },
|
|
48
|
+
];
|
|
49
|
+
const TERMINAL_POS = TIER_SPECS.length - 1;
|
|
50
|
+
|
|
51
|
+
/** Newest 6 images ride tier 0, the next 14 tier 1, the rest tier 2 (020 tier table). */
|
|
52
|
+
const TIER0_COUNT = 6;
|
|
53
|
+
const TIER1_COUNT = 14;
|
|
54
|
+
|
|
55
|
+
/** Decode-bomb guards: refuse to decode absurd inputs (020 guards; "extreme values excluded"). */
|
|
56
|
+
export const MAX_INPUT_BASE64_LENGTH = 64 * MiB;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* First-pass worker-pool width. Memory-bound, not CPU-bound: each in-flight item can
|
|
60
|
+
* hold a decoded bitmap, so this bounds peak memory to ~4 decoded images while still
|
|
61
|
+
* overlapping I/O and native-encode threadpool work. Fixed on purpose — a config knob
|
|
62
|
+
* would widen the adapter contract with no demonstrated need.
|
|
63
|
+
*/
|
|
64
|
+
export const IMAGE_NORMALIZE_CONCURRENCY = 4;
|
|
65
|
+
export const MAX_INPUT_PIXELS = 100_000_000;
|
|
66
|
+
|
|
67
|
+
const UNDECODABLE_TEXT = "[image omitted: undecodable or corrupt image data]";
|
|
68
|
+
const BOMB_TEXT = "[image omitted: image too large to process safely]";
|
|
69
|
+
const OVERFLOW_DROP_TEXT = "[image omitted: total image payload exceeded the provider request budget; older images were dropped]";
|
|
70
|
+
|
|
71
|
+
/** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */
|
|
72
|
+
const PASSTHROUGH_MEDIA = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
73
|
+
|
|
74
|
+
export interface NormalizeOptions {
|
|
75
|
+
/** Shift every image's starting ladder position down (413 retry tightening; 030). */
|
|
76
|
+
tierBias?: number;
|
|
77
|
+
/** Test seam: replaces the Bun.Image encode path (audit round 1, blocker 6). */
|
|
78
|
+
encode?: EncodeFn;
|
|
79
|
+
/** Test seam: replaces the pass-through decode validation (C-gate round 1, blocker 1). */
|
|
80
|
+
validate?: ValidateFn;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type EncodeFn = (
|
|
84
|
+
input: Uint8Array,
|
|
85
|
+
spec: TierSpec,
|
|
86
|
+
quality: number,
|
|
87
|
+
) => Promise<{ data: string; mediaType: string }>;
|
|
88
|
+
|
|
89
|
+
/** Proves the payload fully decodes; must throw for corrupt/truncated data. */
|
|
90
|
+
export type ValidateFn = (input: Uint8Array) => Promise<void>;
|
|
91
|
+
|
|
92
|
+
type ProcessResult =
|
|
93
|
+
| { kind: "pass"; b64Length: number }
|
|
94
|
+
| { kind: "encoded"; data: string; mediaType: string }
|
|
95
|
+
| { kind: "failed" };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Byte-weighted LRU over normalized outputs (audit round 1, blocker 2): aggregate cap,
|
|
99
|
+
* not entry count. Entries are immutable snapshots — demotions write NEW tier-suffixed
|
|
100
|
+
* keys, never mutate stored values.
|
|
101
|
+
*/
|
|
102
|
+
export const IMAGE_NORMALIZE_CACHE_MAX_BYTES = 64 * MiB;
|
|
103
|
+
const CACHE_MAX_ENTRIES = 4_096;
|
|
104
|
+
const CACHE_MAX_ENTRY_BYTES = 20 * MiB;
|
|
105
|
+
// "pass" = validated pass-through; "miss" = this position's ladder cannot meet its hard
|
|
106
|
+
// cap for these bytes (skip straight to the next position — C-gate round 2, blocker 1).
|
|
107
|
+
type CacheValue = { data: string; mediaType: string } | "pass" | "miss";
|
|
108
|
+
interface CacheEntry {
|
|
109
|
+
value: CacheValue;
|
|
110
|
+
sizeBytes: number;
|
|
111
|
+
metadataBytes: number;
|
|
112
|
+
storedAt: number;
|
|
113
|
+
}
|
|
114
|
+
interface NormalizeCacheLimits {
|
|
115
|
+
maxBytes: number;
|
|
116
|
+
maxEntries: number;
|
|
117
|
+
maxEntryBytes: number;
|
|
118
|
+
}
|
|
119
|
+
const DEFAULT_CACHE_LIMITS: NormalizeCacheLimits = {
|
|
120
|
+
maxBytes: IMAGE_NORMALIZE_CACHE_MAX_BYTES,
|
|
121
|
+
maxEntries: CACHE_MAX_ENTRIES,
|
|
122
|
+
maxEntryBytes: CACHE_MAX_ENTRY_BYTES,
|
|
123
|
+
};
|
|
124
|
+
const cacheEncoder = new TextEncoder();
|
|
125
|
+
const cache = new Map<string, CacheEntry>();
|
|
126
|
+
let cacheLimits = { ...DEFAULT_CACHE_LIMITS };
|
|
127
|
+
let cacheBytes = 0;
|
|
128
|
+
let cacheMetadataBytes = 0;
|
|
129
|
+
let cacheSentinelEntries = 0;
|
|
130
|
+
let encodeCalls = 0;
|
|
131
|
+
|
|
132
|
+
function cacheEntry(key: string, value: CacheValue): CacheEntry {
|
|
133
|
+
const keyBytes = cacheEncoder.encode(key).byteLength;
|
|
134
|
+
const valueBytes = typeof value === "string"
|
|
135
|
+
? cacheEncoder.encode(value).byteLength
|
|
136
|
+
: cacheEncoder.encode(value.mediaType).byteLength + cacheEncoder.encode(value.data).byteLength;
|
|
137
|
+
const metadataBytes = keyBytes + (typeof value === "string"
|
|
138
|
+
? cacheEncoder.encode(value).byteLength
|
|
139
|
+
: cacheEncoder.encode(value.mediaType).byteLength);
|
|
140
|
+
return { value, sizeBytes: keyBytes + valueBytes, metadataBytes, storedAt: Date.now() };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deleteCacheEntry(key: string): number {
|
|
144
|
+
const entry = cache.get(key);
|
|
145
|
+
if (!entry) return 0;
|
|
146
|
+
cache.delete(key);
|
|
147
|
+
cacheBytes -= entry.sizeBytes;
|
|
148
|
+
cacheMetadataBytes -= entry.metadataBytes;
|
|
149
|
+
if (typeof entry.value === "string") cacheSentinelEntries--;
|
|
150
|
+
return entry.sizeBytes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cachePut(key: string, value: CacheValue): boolean {
|
|
154
|
+
const next = cacheEntry(key, value);
|
|
155
|
+
if (
|
|
156
|
+
next.sizeBytes > cacheLimits.maxEntryBytes
|
|
157
|
+
|| next.sizeBytes > cacheLimits.maxBytes
|
|
158
|
+
|| cacheLimits.maxEntries <= 0
|
|
159
|
+
) return false;
|
|
160
|
+
const existing = cache.get(key);
|
|
161
|
+
if (existing !== undefined) {
|
|
162
|
+
deleteCacheEntry(key); // re-insert refreshes recency and prevents double-count on concurrent misses
|
|
163
|
+
}
|
|
164
|
+
while (cache.size + 1 > cacheLimits.maxEntries || cacheBytes + next.sizeBytes > cacheLimits.maxBytes) {
|
|
165
|
+
const oldest = cache.keys().next().value;
|
|
166
|
+
if (oldest === undefined || deleteCacheEntry(oldest) === 0) return false;
|
|
167
|
+
}
|
|
168
|
+
cache.set(key, next);
|
|
169
|
+
cacheBytes += next.sizeBytes;
|
|
170
|
+
cacheMetadataBytes += next.metadataBytes;
|
|
171
|
+
if (typeof value === "string") cacheSentinelEntries++;
|
|
172
|
+
enforceAppOwnedMemoryBudget();
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Read a cache entry, refreshing its recency (true LRU, C-gate round 1 blocker 5). */
|
|
177
|
+
function cacheGet(key: string): CacheValue | undefined {
|
|
178
|
+
const entry = cache.get(key);
|
|
179
|
+
if (entry !== undefined) {
|
|
180
|
+
cache.delete(key);
|
|
181
|
+
entry.storedAt = Date.now();
|
|
182
|
+
cache.set(key, entry);
|
|
183
|
+
}
|
|
184
|
+
return entry?.value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Test hooks: encoder-invocation counter + cache reset (no production caller). */
|
|
188
|
+
export function getNormalizeStatsForTests(): {
|
|
189
|
+
encodeCalls: number;
|
|
190
|
+
cacheEntries: number;
|
|
191
|
+
cacheBytes: number;
|
|
192
|
+
sentinelEntries: number;
|
|
193
|
+
metadataBytes: number;
|
|
194
|
+
oldestAt: number | null;
|
|
195
|
+
} {
|
|
196
|
+
return {
|
|
197
|
+
encodeCalls,
|
|
198
|
+
cacheEntries: cache.size,
|
|
199
|
+
cacheBytes,
|
|
200
|
+
sentinelEntries: cacheSentinelEntries,
|
|
201
|
+
metadataBytes: cacheMetadataBytes,
|
|
202
|
+
oldestAt: cache.values().next().value?.storedAt ?? null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export function resetNormalizeStateForTests(): void {
|
|
206
|
+
cache.clear();
|
|
207
|
+
cacheBytes = 0;
|
|
208
|
+
cacheMetadataBytes = 0;
|
|
209
|
+
cacheSentinelEntries = 0;
|
|
210
|
+
encodeCalls = 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function setNormalizeCacheLimitsForTests(limits?: Partial<NormalizeCacheLimits>): void {
|
|
214
|
+
resetNormalizeStateForTests();
|
|
215
|
+
cacheLimits = limits ? { ...DEFAULT_CACHE_LIMITS, ...limits } : { ...DEFAULT_CACHE_LIMITS };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function anthropicImageNormalizeRetainedStoreSnapshot(): {
|
|
219
|
+
count: number;
|
|
220
|
+
bytes: number;
|
|
221
|
+
evictableBytes: number;
|
|
222
|
+
pinnedBytes: number;
|
|
223
|
+
oldestAt: number | null;
|
|
224
|
+
} {
|
|
225
|
+
return {
|
|
226
|
+
count: cache.size,
|
|
227
|
+
bytes: cacheBytes,
|
|
228
|
+
evictableBytes: cacheBytes,
|
|
229
|
+
pinnedBytes: 0,
|
|
230
|
+
oldestAt: cache.values().next().value?.storedAt ?? null,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function evictOldestAnthropicImageNormalizeForBudget(): number {
|
|
235
|
+
const oldest = cache.keys().next().value;
|
|
236
|
+
return oldest === undefined ? 0 : deleteCacheEntry(oldest);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Default encoder: Bun.Image resize-to-fit + JPEG at the given quality. */
|
|
240
|
+
const bunImageEncode: EncodeFn = async (input, spec, quality) => {
|
|
241
|
+
const image = new Bun.Image(input);
|
|
242
|
+
const meta = await image.metadata();
|
|
243
|
+
const w = typeof meta.width === "number" ? meta.width : 0;
|
|
244
|
+
const h = typeof meta.height === "number" ? meta.height : 0;
|
|
245
|
+
let pipeline = new Bun.Image(input);
|
|
246
|
+
if (w > spec.maxEdge || h > spec.maxEdge) {
|
|
247
|
+
const scale = spec.maxEdge / Math.max(w, h);
|
|
248
|
+
pipeline = pipeline.resize(Math.max(1, Math.round(w * scale)), Math.max(1, Math.round(h * scale)));
|
|
249
|
+
}
|
|
250
|
+
const out = await pipeline.jpeg({ quality }).toBuffer();
|
|
251
|
+
return { data: Buffer.from(out).toString("base64"), mediaType: "image/jpeg" };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Default pass-through validation: force a full decode (resize forces pixel decoding, a
|
|
256
|
+
* header-only metadata read does not). A sniffable-but-truncated payload must throw here
|
|
257
|
+
* instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1).
|
|
258
|
+
*/
|
|
259
|
+
const bunImageValidate: ValidateFn = async input => {
|
|
260
|
+
await new Bun.Image(input).resize(1, 1).jpeg({ quality: 1 }).toBuffer();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function mediaTypeOf(ref: ImageBlockRef): string {
|
|
264
|
+
const block = ref.container[ref.index] as { source?: { media_type?: unknown } } | undefined;
|
|
265
|
+
const mt = block?.source?.media_type;
|
|
266
|
+
return typeof mt === "string" ? mt.toLowerCase() : "";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function textify(ref: ImageBlockRef, text: string): void {
|
|
270
|
+
ref.container[ref.index] = { type: "text", text };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function replaceImage(ref: ImageBlockRef, data: string, mediaType: string): void {
|
|
274
|
+
ref.container[ref.index] = { type: "image", source: { type: "base64", media_type: mediaType, data } };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function initialPosition(newestFirstIndex: number, bias: number): number {
|
|
278
|
+
const base = newestFirstIndex < TIER0_COUNT ? 0 : newestFirstIndex < TIER0_COUNT + TIER1_COUNT ? 1 : 2;
|
|
279
|
+
return Math.min(base + Math.max(0, bias), TERMINAL_POS);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Process one image at a ladder position: pass through when it already fits the
|
|
284
|
+
* position's caps (Anthropic-native format, dims within maxEdge, size within hardCap —
|
|
285
|
+
* this also exempts possibly-animated GIF/WebP from a lossy re-encode; pass-through is
|
|
286
|
+
* additionally VALIDATED with a full decode once, cached), otherwise walk positions
|
|
287
|
+
* downward encoding until a hard cap is met; terminal accepts measured size.
|
|
288
|
+
* `mediaType` must be the ORIGINAL source media type (cache keys include it — C-gate
|
|
289
|
+
* round 1, blocker 4 — and pass-through eligibility depends on it).
|
|
290
|
+
*/
|
|
291
|
+
async function processAt(
|
|
292
|
+
b64: string,
|
|
293
|
+
startPos: number,
|
|
294
|
+
mediaType: string,
|
|
295
|
+
encode: EncodeFn,
|
|
296
|
+
validate: ValidateFn,
|
|
297
|
+
): Promise<ProcessResult & { pos: number }> {
|
|
298
|
+
const dims = sniffImageDimensions(b64);
|
|
299
|
+
const hash = Bun.hash(b64).toString(36);
|
|
300
|
+
let input: Uint8Array;
|
|
301
|
+
try {
|
|
302
|
+
input = Uint8Array.from(Buffer.from(b64, "base64"));
|
|
303
|
+
} catch {
|
|
304
|
+
return { kind: "failed", pos: startPos };
|
|
305
|
+
}
|
|
306
|
+
for (let pos = startPos; pos <= TERMINAL_POS; pos++) {
|
|
307
|
+
const spec = TIER_SPECS[pos];
|
|
308
|
+
const key = `${hash}:${mediaType}:${pos}`;
|
|
309
|
+
const cached = cacheGet(key);
|
|
310
|
+
if (cached === "pass") return { kind: "pass", b64Length: b64.length, pos };
|
|
311
|
+
if (cached === "miss") continue; // known cap miss: skip to the next position
|
|
312
|
+
if (cached) return { kind: "encoded", data: cached.data, mediaType: cached.mediaType, pos };
|
|
313
|
+
|
|
314
|
+
const fitsDims = dims !== null && dims.width <= spec.maxEdge && dims.height <= spec.maxEdge;
|
|
315
|
+
if (PASSTHROUGH_MEDIA.has(mediaType) && fitsDims && b64.length <= spec.hardCap) {
|
|
316
|
+
try {
|
|
317
|
+
await validate(input); // sniffable-but-truncated data must not ride pass-through
|
|
318
|
+
} catch {
|
|
319
|
+
return { kind: "failed", pos };
|
|
320
|
+
}
|
|
321
|
+
cachePut(key, "pass");
|
|
322
|
+
return { kind: "pass", b64Length: b64.length, pos };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let last: { data: string; mediaType: string } | null = null;
|
|
326
|
+
try {
|
|
327
|
+
for (const quality of spec.qualities) {
|
|
328
|
+
encodeCalls++;
|
|
329
|
+
last = await encode(input, spec, quality);
|
|
330
|
+
if (last.data.length <= spec.hardCap) {
|
|
331
|
+
cachePut(key, last);
|
|
332
|
+
return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// Decode/encode failure: corrupt or unsupported payload (audit round 2, blocker 2).
|
|
337
|
+
return { kind: "failed", pos };
|
|
338
|
+
}
|
|
339
|
+
if (pos === TERMINAL_POS && last) {
|
|
340
|
+
cachePut(key, last);
|
|
341
|
+
return { kind: "encoded", data: last.data, mediaType: last.mediaType, pos };
|
|
342
|
+
}
|
|
343
|
+
// Hard cap missed at this position — remember the miss, continue down the ladder.
|
|
344
|
+
cachePut(key, "miss");
|
|
345
|
+
}
|
|
346
|
+
return { kind: "failed", pos: TERMINAL_POS };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Wire-neutral image handle (devlog/260714_image_normalization_pipeline/050): the core
|
|
351
|
+
* algorithm below normalizes THROUGH this interface so non-Anthropic wire shapes (kiro
|
|
352
|
+
* CodeWhisperer) reuse the exact same tier/cache/demotion machinery. `mediaType` is the
|
|
353
|
+
* canonical lowercased MIME ("image/<format>") — cache identity and pass-through
|
|
354
|
+
* decisions depend on it; wire-specific conversions live inside `replace`.
|
|
355
|
+
*/
|
|
356
|
+
export interface NormalizeTarget {
|
|
357
|
+
base64: string | null;
|
|
358
|
+
mediaType: string;
|
|
359
|
+
replace(data: string, mediaType: string): void;
|
|
360
|
+
drop(note: string): void;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface NormalizeTargetsOptions extends NormalizeOptions {
|
|
364
|
+
/** Total base64 budget across all targets. Default: TOTAL_IMAGE_BASE64_BUDGET. */
|
|
365
|
+
budget?: number;
|
|
366
|
+
/**
|
|
367
|
+
* What to do when every image is terminal-floored and the sum still exceeds budget:
|
|
368
|
+
* "none" (anthropic — the guard's Rule 4 backstop textifies downstream) or "drop"
|
|
369
|
+
* (kiro — no downstream guard exists, so drop OLDEST targets here until it fits).
|
|
370
|
+
*/
|
|
371
|
+
overflowAction?: "none" | "drop";
|
|
372
|
+
/** Only the newest N images are processed (older ones skipped). Default: unlimited. */
|
|
373
|
+
processLimit?: number;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Core normalization over wire-neutral targets (mutates via target callbacks).
|
|
378
|
+
* Null-base64 targets (URL/file sources) pass through untouched.
|
|
379
|
+
*/
|
|
380
|
+
export async function normalizeImageTargets(targets: NormalizeTarget[], options: NormalizeTargetsOptions = {}): Promise<void> {
|
|
381
|
+
if (targets.length === 0) return;
|
|
382
|
+
const encode = options.encode ?? bunImageEncode;
|
|
383
|
+
const validate = options.validate ?? bunImageValidate;
|
|
384
|
+
const bias = options.tierBias ?? 0;
|
|
385
|
+
const budget = options.budget ?? TOTAL_IMAGE_BASE64_BUDGET;
|
|
386
|
+
const overflowAction = options.overflowAction ?? "none";
|
|
387
|
+
const processLimit = options.processLimit ?? Number.POSITIVE_INFINITY;
|
|
388
|
+
const n = targets.length;
|
|
389
|
+
|
|
390
|
+
// sourceB64/sourceMedia are the ORIGINAL input (encode source + cache identity);
|
|
391
|
+
// size always reflects the bytes currently ON the wire for this target (the core is
|
|
392
|
+
// the only mutator, so tracked size cannot drift from reality).
|
|
393
|
+
interface Entry { target: NormalizeTarget; sourceB64: string; sourceMedia: string; pos: number; size: number; done: boolean }
|
|
394
|
+
const entries: (Entry | null)[] = new Array(n).fill(null);
|
|
395
|
+
|
|
396
|
+
// Bounded parallel first pass: a shared index queue with a small fixed worker pool.
|
|
397
|
+
// Unbounded Promise.all across up to `processLimit` (anthropic passes 100) large
|
|
398
|
+
// images would hold that many decoded bitmaps in flight at once — the limit bounds
|
|
399
|
+
// peak memory, not throughput (native encode parallelism lives below this layer).
|
|
400
|
+
// entries[] stays index-addressed, so completion order never affects output order
|
|
401
|
+
// or the sequential demotion loop below.
|
|
402
|
+
let nextIndex = 0;
|
|
403
|
+
let firstError: unknown;
|
|
404
|
+
let failed = false;
|
|
405
|
+
const workerCount = Math.min(IMAGE_NORMALIZE_CONCURRENCY, n);
|
|
406
|
+
const worker = async (): Promise<void> => {
|
|
407
|
+
// A fatal error stops workers from pulling NEW indices; in-flight items settle.
|
|
408
|
+
while (!failed) {
|
|
409
|
+
const i = nextIndex++;
|
|
410
|
+
if (i >= n) return;
|
|
411
|
+
const target = targets[i];
|
|
412
|
+
const b64 = target.base64;
|
|
413
|
+
if (!b64) continue; // URL source: no base64 weight, never touched here.
|
|
414
|
+
const newestFirstIndex = n - 1 - i;
|
|
415
|
+
// Images beyond the processing limit are left untouched (anthropic passes 100:
|
|
416
|
+
// its guard textifies the surplus anyway, so decode/encode work there is waste).
|
|
417
|
+
if (newestFirstIndex >= processLimit) continue;
|
|
418
|
+
if (b64.length > MAX_INPUT_BASE64_LENGTH) {
|
|
419
|
+
target.drop(BOMB_TEXT);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const dims = sniffImageDimensions(b64);
|
|
423
|
+
if (dims && dims.width * dims.height > MAX_INPUT_PIXELS) {
|
|
424
|
+
target.drop(BOMB_TEXT);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const sourceMedia = target.mediaType.toLowerCase();
|
|
428
|
+
const pos = initialPosition(newestFirstIndex, bias);
|
|
429
|
+
const result = await processAt(b64, pos, sourceMedia, encode, validate);
|
|
430
|
+
if (result.kind === "failed") {
|
|
431
|
+
target.drop(UNDECODABLE_TEXT);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
let size = b64.length;
|
|
435
|
+
if (result.kind === "encoded") {
|
|
436
|
+
// Set the failure flag SYNCHRONOUSLY when the wire callback throws: other
|
|
437
|
+
// parked worker continuations may resume before our .catch() runs, and they
|
|
438
|
+
// must not pull new indices after a fatal error (C-gate round 1, blocker 1).
|
|
439
|
+
try {
|
|
440
|
+
target.replace(result.data, result.mediaType);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
if (!failed) {
|
|
443
|
+
failed = true;
|
|
444
|
+
firstError = err;
|
|
445
|
+
}
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
size = result.data.length;
|
|
449
|
+
}
|
|
450
|
+
entries[i] = { target, sourceB64: b64, sourceMedia, pos: result.pos, size, done: result.pos >= TERMINAL_POS };
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker().catch(err => {
|
|
454
|
+
if (!failed) {
|
|
455
|
+
failed = true;
|
|
456
|
+
firstError = err;
|
|
457
|
+
}
|
|
458
|
+
})));
|
|
459
|
+
if (failed) throw firstError;
|
|
460
|
+
|
|
461
|
+
// Aggregate demotion loop (audit rounds 1+3): while the measured total exceeds the
|
|
462
|
+
// budget, demote the OLDEST not-yet-terminal image one position and re-encode.
|
|
463
|
+
let sum = 0;
|
|
464
|
+
for (const e of entries) if (e) sum += e.size;
|
|
465
|
+
while (sum > budget) {
|
|
466
|
+
const entry = entries.find((e): e is Entry => e !== null && !e.done);
|
|
467
|
+
if (!entry) break; // all terminal — overflowAction below decides
|
|
468
|
+
const result = await processAt(entry.sourceB64, entry.pos + 1, entry.sourceMedia, encode, validate);
|
|
469
|
+
if (result.kind === "failed") {
|
|
470
|
+
entry.target.drop(UNDECODABLE_TEXT);
|
|
471
|
+
sum -= entry.size;
|
|
472
|
+
entries[entries.indexOf(entry)] = null;
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
let newSize = entry.size;
|
|
476
|
+
if (result.kind === "encoded") {
|
|
477
|
+
entry.target.replace(result.data, result.mediaType);
|
|
478
|
+
newSize = result.data.length;
|
|
479
|
+
} else {
|
|
480
|
+
newSize = result.b64Length; // pass leaves current bytes (only reachable for never-encoded entries)
|
|
481
|
+
}
|
|
482
|
+
sum += newSize - entry.size;
|
|
483
|
+
entry.size = newSize;
|
|
484
|
+
entry.pos = result.pos;
|
|
485
|
+
entry.done = result.pos >= TERMINAL_POS;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Terminal overflow (050 audit round 1, blocker 3): with no downstream guard, drop
|
|
489
|
+
// OLDEST targets until the sum fits.
|
|
490
|
+
if (overflowAction === "drop") {
|
|
491
|
+
for (let i = 0; i < entries.length && sum > budget; i++) {
|
|
492
|
+
const e = entries[i];
|
|
493
|
+
if (!e) continue;
|
|
494
|
+
e.target.drop(OVERFLOW_DROP_TEXT);
|
|
495
|
+
sum -= e.size;
|
|
496
|
+
entries[i] = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Normalize every base64 image in already-built Anthropic wire messages (mutates in
|
|
503
|
+
* place). URL-source images pass through untouched. See module header for the contract.
|
|
504
|
+
*/
|
|
505
|
+
export async function normalizeAnthropicImages(messages: unknown[], options: NormalizeOptions = {}): Promise<void> {
|
|
506
|
+
const refs = collectImageRefs(messages);
|
|
507
|
+
if (refs.length === 0) return;
|
|
508
|
+
const targets: NormalizeTarget[] = refs.map(ref => ({
|
|
509
|
+
base64: ref.base64,
|
|
510
|
+
mediaType: mediaTypeOf(ref),
|
|
511
|
+
replace: (data: string, mediaType: string) => replaceImage(ref, data, mediaType),
|
|
512
|
+
drop: (note: string) => textify(ref, note),
|
|
513
|
+
}));
|
|
514
|
+
// Anthropic hard-caps 100 images/request and its guard textifies the surplus, so
|
|
515
|
+
// processing beyond the newest 100 is pure waste; terminal overflow stays with the
|
|
516
|
+
// guard's Rule 4 backstop (overflowAction "none").
|
|
517
|
+
await normalizeImageTargets(targets, { ...options, processLimit: 100, overflowAction: "none" });
|
|
518
|
+
}
|