@vellumai/assistant 0.4.46 → 0.4.49
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/ARCHITECTURE.md +7 -7
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/architecture/security.md +5 -5
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +35 -25
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +239 -26
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +111 -7
- package/src/__tests__/credential-vault-unit.test.ts +287 -54
- package/src/__tests__/credential-vault.test.ts +406 -12
- package/src/__tests__/credentials-cli.test.ts +82 -6
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +38 -25
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +3 -3
- package/src/__tests__/skills.test.ts +3 -12
- package/src/__tests__/slack-channel-config.test.ts +76 -11
- package/src/__tests__/slack-share-routes.test.ts +17 -14
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -20
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +8 -10
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +4 -8
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -1
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +46 -15
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +110 -23
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/cli.ts +3 -2
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +76 -56
- package/src/daemon/handlers/config-telegram.ts +53 -24
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +39 -63
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +20 -11
- package/src/daemon/session.ts +139 -16
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/providers/index.ts +2 -1
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/instrument.ts +15 -1
- package/src/logfire.ts +16 -5
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +26 -3
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/guardian.ts +1 -1
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +19 -13
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +283 -122
- package/src/messaging/providers/gmail/people-client.ts +32 -24
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
- package/src/messaging/providers/whatsapp/adapter.ts +17 -11
- package/src/messaging/registry.ts +2 -31
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +537 -0
- package/src/oauth/byo-connection.ts +128 -0
- package/src/oauth/connect-orchestrator.ts +139 -56
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +58 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +192 -0
- package/src/oauth/platform-connection.ts +111 -0
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +8 -4
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +70 -45
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -27
- package/src/runtime/AGENTS.md +17 -0
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +17 -10
- package/src/runtime/http-types.ts +2 -3
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -6
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +113 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -8
- package/src/security/credential-key.ts +14 -0
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +145 -43
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +5 -4
- package/src/tools/credentials/metadata-store.ts +22 -74
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +139 -151
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/registry.ts +2 -7
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +62 -8
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +0 -5
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +94 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1020
- package/src/daemon/ride-shotgun-handler.ts +0 -567
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
- package/src/runtime/telegram-streaming-delivery.ts +0 -383
- package/src/tools/computer-use/request-computer-control.ts +0 -61
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { parseImageDimensions } from "../context/image-dimensions.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper: build a Buffer of given bytes and return its base64 encoding.
|
|
9
|
+
*/
|
|
10
|
+
function toBase64(bytes: number[]): string {
|
|
11
|
+
return Buffer.from(bytes).toString("base64");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal valid PNG IHDR: 8-byte signature + 13-byte IHDR chunk.
|
|
16
|
+
* Width = 320, Height = 240.
|
|
17
|
+
*/
|
|
18
|
+
function minimalPngHeader(width: number, height: number): number[] {
|
|
19
|
+
const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
20
|
+
// IHDR chunk: length (13 = 0x0000000D), "IHDR", width(4), height(4), bitDepth, colorType, compression, filter, interlace
|
|
21
|
+
const ihdrLength = [0x00, 0x00, 0x00, 0x0d];
|
|
22
|
+
const ihdrType = [0x49, 0x48, 0x44, 0x52]; // "IHDR"
|
|
23
|
+
const w = [
|
|
24
|
+
(width >> 24) & 0xff,
|
|
25
|
+
(width >> 16) & 0xff,
|
|
26
|
+
(width >> 8) & 0xff,
|
|
27
|
+
width & 0xff,
|
|
28
|
+
];
|
|
29
|
+
const h = [
|
|
30
|
+
(height >> 24) & 0xff,
|
|
31
|
+
(height >> 16) & 0xff,
|
|
32
|
+
(height >> 8) & 0xff,
|
|
33
|
+
height & 0xff,
|
|
34
|
+
];
|
|
35
|
+
const rest = [0x08, 0x06, 0x00, 0x00, 0x00]; // bit depth, color type RGBA, compression, filter, interlace
|
|
36
|
+
const crc = [0x00, 0x00, 0x00, 0x00]; // dummy CRC (not validated by parser)
|
|
37
|
+
return [...sig, ...ihdrLength, ...ihdrType, ...w, ...h, ...rest, ...crc];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Minimal valid JPEG with SOF0 marker.
|
|
42
|
+
* Structure: SOI + APP0 (short) + SOF0 with given dimensions.
|
|
43
|
+
*/
|
|
44
|
+
function minimalJpegHeader(width: number, height: number): number[] {
|
|
45
|
+
const soi = [0xff, 0xd8]; // Start of image
|
|
46
|
+
// APP0 marker (JFIF) - minimal
|
|
47
|
+
const app0 = [
|
|
48
|
+
0xff,
|
|
49
|
+
0xe0, // APP0 marker
|
|
50
|
+
0x00,
|
|
51
|
+
0x10, // length = 16
|
|
52
|
+
0x4a,
|
|
53
|
+
0x46,
|
|
54
|
+
0x49,
|
|
55
|
+
0x46,
|
|
56
|
+
0x00, // "JFIF\0"
|
|
57
|
+
0x01,
|
|
58
|
+
0x01, // version 1.1
|
|
59
|
+
0x00, // aspect ratio units
|
|
60
|
+
0x00,
|
|
61
|
+
0x01, // X density
|
|
62
|
+
0x00,
|
|
63
|
+
0x01, // Y density
|
|
64
|
+
0x00,
|
|
65
|
+
0x00, // no thumbnail
|
|
66
|
+
];
|
|
67
|
+
// SOF0 marker
|
|
68
|
+
const sof0 = [
|
|
69
|
+
0xff,
|
|
70
|
+
0xc0, // SOF0 marker
|
|
71
|
+
0x00,
|
|
72
|
+
0x0b, // length = 11
|
|
73
|
+
0x08, // precision = 8 bits
|
|
74
|
+
(height >> 8) & 0xff,
|
|
75
|
+
height & 0xff, // height
|
|
76
|
+
(width >> 8) & 0xff,
|
|
77
|
+
width & 0xff, // width
|
|
78
|
+
0x03, // number of components
|
|
79
|
+
0x01,
|
|
80
|
+
0x11,
|
|
81
|
+
0x00, // Y component
|
|
82
|
+
];
|
|
83
|
+
return [...soi, ...app0, ...sof0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Minimal valid GIF89a header with given dimensions.
|
|
88
|
+
*/
|
|
89
|
+
function minimalGifHeader(width: number, height: number): number[] {
|
|
90
|
+
// "GIF89a"
|
|
91
|
+
const sig = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61];
|
|
92
|
+
const w = [width & 0xff, (width >> 8) & 0xff]; // little-endian uint16
|
|
93
|
+
const h = [height & 0xff, (height >> 8) & 0xff]; // little-endian uint16
|
|
94
|
+
return [...sig, ...w, ...h, 0x00, 0x00]; // pad to 12 bytes
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Minimal valid WebP VP8 (lossy) header with given dimensions.
|
|
99
|
+
*/
|
|
100
|
+
function minimalWebpVP8Header(width: number, height: number): number[] {
|
|
101
|
+
const riff = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
|
102
|
+
const fileSize = [0x00, 0x00, 0x00, 0x00]; // dummy file size
|
|
103
|
+
const webp = [0x57, 0x45, 0x42, 0x50]; // "WEBP"
|
|
104
|
+
const vp8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
|
105
|
+
const chunkSize = [0x00, 0x00, 0x00, 0x00]; // dummy chunk size
|
|
106
|
+
// VP8 bitstream header (bytes 20-25): frame tag + start code
|
|
107
|
+
const frameTag = [0x9d, 0x01, 0x2a]; // key frame tag bytes
|
|
108
|
+
const padding = [0x00, 0x00, 0x00]; // padding to reach offset 26
|
|
109
|
+
// Width at byte 26 (LE uint16), height at byte 28 (LE uint16)
|
|
110
|
+
const w = [width & 0xff, (width >> 8) & 0x3f]; // little-endian uint16, upper bits masked
|
|
111
|
+
const h = [height & 0xff, (height >> 8) & 0x3f]; // little-endian uint16, upper bits masked
|
|
112
|
+
return [
|
|
113
|
+
...riff,
|
|
114
|
+
...fileSize,
|
|
115
|
+
...webp,
|
|
116
|
+
...vp8,
|
|
117
|
+
...chunkSize,
|
|
118
|
+
...frameTag,
|
|
119
|
+
...padding,
|
|
120
|
+
...w,
|
|
121
|
+
...h,
|
|
122
|
+
0x00,
|
|
123
|
+
0x00,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Minimal valid WebP VP8L (lossless) header with given dimensions.
|
|
129
|
+
*/
|
|
130
|
+
function minimalWebpVP8LHeader(width: number, height: number): number[] {
|
|
131
|
+
const riff = [0x52, 0x49, 0x46, 0x46];
|
|
132
|
+
const fileSize = [0x00, 0x00, 0x00, 0x00];
|
|
133
|
+
const webp = [0x57, 0x45, 0x42, 0x50];
|
|
134
|
+
const vp8l = [0x56, 0x50, 0x38, 0x4c]; // "VP8L"
|
|
135
|
+
const chunkSize = [0x00, 0x00, 0x00, 0x00];
|
|
136
|
+
// Signature byte at offset 20
|
|
137
|
+
const sigByte = [0x2f];
|
|
138
|
+
// At offset 21: LE uint32 encoding width-1 in bits 0-13 and height-1 in bits 14-27
|
|
139
|
+
const bits = ((width - 1) & 0x3fff) | (((height - 1) & 0x3fff) << 14);
|
|
140
|
+
const bitsBytes = [
|
|
141
|
+
bits & 0xff,
|
|
142
|
+
(bits >> 8) & 0xff,
|
|
143
|
+
(bits >> 16) & 0xff,
|
|
144
|
+
(bits >> 24) & 0xff,
|
|
145
|
+
];
|
|
146
|
+
return [
|
|
147
|
+
...riff,
|
|
148
|
+
...fileSize,
|
|
149
|
+
...webp,
|
|
150
|
+
...vp8l,
|
|
151
|
+
...chunkSize,
|
|
152
|
+
...sigByte,
|
|
153
|
+
...bitsBytes,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Minimal valid WebP VP8X (extended) header with given dimensions.
|
|
159
|
+
*/
|
|
160
|
+
function minimalWebpVP8XHeader(width: number, height: number): number[] {
|
|
161
|
+
const riff = [0x52, 0x49, 0x46, 0x46];
|
|
162
|
+
const fileSize = [0x00, 0x00, 0x00, 0x00];
|
|
163
|
+
const webp = [0x57, 0x45, 0x42, 0x50];
|
|
164
|
+
const vp8x = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
|
165
|
+
const chunkSize = [0x0a, 0x00, 0x00, 0x00]; // chunk size = 10
|
|
166
|
+
const flags = [0x00, 0x00, 0x00, 0x00]; // flags (bytes 20-23)
|
|
167
|
+
// Width-1 as LE uint24 at offset 24
|
|
168
|
+
const w1 = width - 1;
|
|
169
|
+
const wBytes = [w1 & 0xff, (w1 >> 8) & 0xff, (w1 >> 16) & 0xff];
|
|
170
|
+
// Height-1 as LE uint24 at offset 27
|
|
171
|
+
const h1 = height - 1;
|
|
172
|
+
const hBytes = [h1 & 0xff, (h1 >> 8) & 0xff, (h1 >> 16) & 0xff];
|
|
173
|
+
return [
|
|
174
|
+
...riff,
|
|
175
|
+
...fileSize,
|
|
176
|
+
...webp,
|
|
177
|
+
...vp8x,
|
|
178
|
+
...chunkSize,
|
|
179
|
+
...flags,
|
|
180
|
+
...wBytes,
|
|
181
|
+
...hBytes,
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
describe("parseImageDimensions", () => {
|
|
186
|
+
describe("PNG", () => {
|
|
187
|
+
it("extracts dimensions from a valid PNG header", () => {
|
|
188
|
+
const base64 = toBase64(minimalPngHeader(320, 240));
|
|
189
|
+
const result = parseImageDimensions(base64, "image/png");
|
|
190
|
+
expect(result).toEqual({ width: 320, height: 240 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("extracts dimensions from a large PNG", () => {
|
|
194
|
+
const base64 = toBase64(minimalPngHeader(3840, 2160));
|
|
195
|
+
const result = parseImageDimensions(base64, "image/png");
|
|
196
|
+
expect(result).toEqual({ width: 3840, height: 2160 });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns null for truncated PNG data", () => {
|
|
200
|
+
const bytes = minimalPngHeader(320, 240);
|
|
201
|
+
const truncated = toBase64(bytes.slice(0, 10));
|
|
202
|
+
expect(parseImageDimensions(truncated, "image/png")).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns null for corrupt PNG signature", () => {
|
|
206
|
+
const bytes = minimalPngHeader(320, 240);
|
|
207
|
+
bytes[0] = 0x00; // corrupt signature
|
|
208
|
+
expect(parseImageDimensions(toBase64(bytes), "image/png")).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("JPEG", () => {
|
|
213
|
+
it("extracts dimensions from a valid JPEG with SOF0", () => {
|
|
214
|
+
const base64 = toBase64(minimalJpegHeader(640, 480));
|
|
215
|
+
const result = parseImageDimensions(base64, "image/jpeg");
|
|
216
|
+
expect(result).toEqual({ width: 640, height: 480 });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("extracts dimensions from a JPEG with SOF2 (progressive)", () => {
|
|
220
|
+
const bytes = minimalJpegHeader(800, 600);
|
|
221
|
+
// Change SOF0 (0xC0) to SOF2 (0xC2)
|
|
222
|
+
const sof0Idx = bytes.indexOf(0xc0, 2);
|
|
223
|
+
bytes[sof0Idx] = 0xc2;
|
|
224
|
+
const result = parseImageDimensions(toBase64(bytes), "image/jpeg");
|
|
225
|
+
expect(result).toEqual({ width: 800, height: 600 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("returns null for truncated JPEG data", () => {
|
|
229
|
+
const truncated = toBase64([0xff, 0xd8, 0xff, 0xc0]);
|
|
230
|
+
expect(parseImageDimensions(truncated, "image/jpeg")).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("returns null for corrupt JPEG (missing SOI)", () => {
|
|
234
|
+
const bytes = minimalJpegHeader(640, 480);
|
|
235
|
+
bytes[0] = 0x00;
|
|
236
|
+
expect(parseImageDimensions(toBase64(bytes), "image/jpeg")).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("GIF", () => {
|
|
241
|
+
it("extracts dimensions from a valid GIF89a header", () => {
|
|
242
|
+
const base64 = toBase64(minimalGifHeader(100, 50));
|
|
243
|
+
const result = parseImageDimensions(base64, "image/gif");
|
|
244
|
+
expect(result).toEqual({ width: 100, height: 50 });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("extracts dimensions from GIF87a header", () => {
|
|
248
|
+
const bytes = minimalGifHeader(256, 128);
|
|
249
|
+
bytes[4] = 0x37; // Change '9' to '7' for GIF87a — signature check is GIF8 only
|
|
250
|
+
bytes[5] = 0x61;
|
|
251
|
+
const result = parseImageDimensions(toBase64(bytes), "image/gif");
|
|
252
|
+
expect(result).toEqual({ width: 256, height: 128 });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("returns null for truncated GIF data", () => {
|
|
256
|
+
const truncated = toBase64([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
|
|
257
|
+
expect(parseImageDimensions(truncated, "image/gif")).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("returns null for corrupt GIF signature", () => {
|
|
261
|
+
const bytes = minimalGifHeader(100, 50);
|
|
262
|
+
bytes[0] = 0x00;
|
|
263
|
+
expect(parseImageDimensions(toBase64(bytes), "image/gif")).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("WebP", () => {
|
|
268
|
+
it("extracts dimensions from a VP8 (lossy) WebP", () => {
|
|
269
|
+
const base64 = toBase64(minimalWebpVP8Header(400, 300));
|
|
270
|
+
const result = parseImageDimensions(base64, "image/webp");
|
|
271
|
+
expect(result).toEqual({ width: 400, height: 300 });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("extracts dimensions from a VP8L (lossless) WebP", () => {
|
|
275
|
+
const base64 = toBase64(minimalWebpVP8LHeader(500, 250));
|
|
276
|
+
const result = parseImageDimensions(base64, "image/webp");
|
|
277
|
+
expect(result).toEqual({ width: 500, height: 250 });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("extracts dimensions from a VP8X (extended) WebP", () => {
|
|
281
|
+
const base64 = toBase64(minimalWebpVP8XHeader(1920, 1080));
|
|
282
|
+
const result = parseImageDimensions(base64, "image/webp");
|
|
283
|
+
expect(result).toEqual({ width: 1920, height: 1080 });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("returns null for truncated WebP data", () => {
|
|
287
|
+
const truncated = toBase64([
|
|
288
|
+
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00,
|
|
289
|
+
]);
|
|
290
|
+
expect(parseImageDimensions(truncated, "image/webp")).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns null for corrupt RIFF signature", () => {
|
|
294
|
+
const bytes = minimalWebpVP8Header(400, 300);
|
|
295
|
+
bytes[0] = 0x00;
|
|
296
|
+
expect(parseImageDimensions(toBase64(bytes), "image/webp")).toBeNull();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("unknown media type", () => {
|
|
301
|
+
it("returns null for unsupported media type", () => {
|
|
302
|
+
expect(parseImageDimensions("AAAA", "image/bmp")).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("returns null for non-image media type", () => {
|
|
306
|
+
expect(parseImageDimensions("AAAA", "application/pdf")).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("empty/invalid data", () => {
|
|
311
|
+
it("returns null for empty base64 string", () => {
|
|
312
|
+
expect(parseImageDimensions("", "image/png")).toBeNull();
|
|
313
|
+
expect(parseImageDimensions("", "image/jpeg")).toBeNull();
|
|
314
|
+
expect(parseImageDimensions("", "image/gif")).toBeNull();
|
|
315
|
+
expect(parseImageDimensions("", "image/webp")).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("real image file", () => {
|
|
320
|
+
it("parses dimensions from an actual PNG file in the repo", () => {
|
|
321
|
+
const pngPath = join(
|
|
322
|
+
import.meta.dir,
|
|
323
|
+
"../../..",
|
|
324
|
+
"clients/chrome-extension/icons/icon16.png",
|
|
325
|
+
);
|
|
326
|
+
const pngData = readFileSync(pngPath);
|
|
327
|
+
const base64 = pngData.toString("base64");
|
|
328
|
+
const result = parseImageDimensions(base64, "image/png");
|
|
329
|
+
expect(result).toEqual({ width: 16, height: 16 });
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
@@ -104,7 +104,7 @@ describe("token estimator", () => {
|
|
|
104
104
|
expect(largeFileTokens - smallFileTokens).toBeGreaterThan(1000);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
test("does not count file base64 payload for OpenAI
|
|
107
|
+
test("does not count file base64 payload for OpenAI-style file fallback", () => {
|
|
108
108
|
const sharedSource = {
|
|
109
109
|
type: "base64" as const,
|
|
110
110
|
filename: "report.pdf",
|
|
@@ -130,6 +130,47 @@ describe("token estimator", () => {
|
|
|
130
130
|
expect(largeFileTokens).toBe(smallFileTokens);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test("estimates Anthropic PDF tokens from file size", () => {
|
|
134
|
+
// ~14.8 MB PDF => ~20M base64 chars
|
|
135
|
+
const base64Length = 20_000_000;
|
|
136
|
+
const tokens = estimateContentBlockTokens(
|
|
137
|
+
{
|
|
138
|
+
type: "file",
|
|
139
|
+
source: {
|
|
140
|
+
type: "base64",
|
|
141
|
+
filename: "large-report.pdf",
|
|
142
|
+
media_type: "application/pdf",
|
|
143
|
+
data: "a".repeat(base64Length),
|
|
144
|
+
},
|
|
145
|
+
extracted_text: "",
|
|
146
|
+
},
|
|
147
|
+
{ providerName: "anthropic" },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Raw bytes = 20_000_000 * 3/4 = 15_000_000
|
|
151
|
+
// Estimated tokens = 15_000_000 * 0.016 = 240_000 (plus overhead)
|
|
152
|
+
expect(tokens).toBeGreaterThan(200_000);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("Anthropic PDF minimum is one page", () => {
|
|
156
|
+
const tokens = estimateContentBlockTokens(
|
|
157
|
+
{
|
|
158
|
+
type: "file",
|
|
159
|
+
source: {
|
|
160
|
+
type: "base64",
|
|
161
|
+
filename: "tiny.pdf",
|
|
162
|
+
media_type: "application/pdf",
|
|
163
|
+
data: "a".repeat(16),
|
|
164
|
+
},
|
|
165
|
+
extracted_text: "",
|
|
166
|
+
},
|
|
167
|
+
{ providerName: "anthropic" },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Should be at least ANTHROPIC_PDF_MIN_TOKENS (1600) plus overhead
|
|
171
|
+
expect(tokens).toBeGreaterThanOrEqual(1600);
|
|
172
|
+
});
|
|
173
|
+
|
|
133
174
|
test("does not count non-inline file base64 payload for Gemini", () => {
|
|
134
175
|
const sharedSource = {
|
|
135
176
|
type: "base64" as const,
|
|
@@ -156,21 +197,163 @@ describe("token estimator", () => {
|
|
|
156
197
|
expect(largeFileTokens).toBe(smallFileTokens);
|
|
157
198
|
});
|
|
158
199
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
data: "a".repeat(60_000),
|
|
200
|
+
// Non-Anthropic providers use base64 payload size for image estimation
|
|
201
|
+
test("scales image token estimate with base64 payload size (non-Anthropic)", () => {
|
|
202
|
+
const smallImageTokens = estimateContentBlockTokens(
|
|
203
|
+
{
|
|
204
|
+
type: "image",
|
|
205
|
+
source: {
|
|
206
|
+
type: "base64",
|
|
207
|
+
media_type: "image/png",
|
|
208
|
+
data: "a".repeat(64),
|
|
209
|
+
},
|
|
170
210
|
},
|
|
171
|
-
|
|
211
|
+
{ providerName: "openai" },
|
|
212
|
+
);
|
|
213
|
+
const largeImageTokens = estimateContentBlockTokens(
|
|
214
|
+
{
|
|
215
|
+
type: "image",
|
|
216
|
+
source: {
|
|
217
|
+
type: "base64",
|
|
218
|
+
media_type: "image/png",
|
|
219
|
+
data: "a".repeat(60_000),
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{ providerName: "openai" },
|
|
223
|
+
);
|
|
172
224
|
|
|
173
225
|
expect(largeImageTokens).toBeGreaterThan(smallImageTokens);
|
|
174
226
|
expect(largeImageTokens - smallImageTokens).toBeGreaterThan(1000);
|
|
175
227
|
});
|
|
228
|
+
|
|
229
|
+
test("estimates Anthropic image tokens from dimensions, not base64 size", () => {
|
|
230
|
+
// Build a minimal valid PNG header encoding 1920x1080 dimensions.
|
|
231
|
+
// PNG header: 8-byte signature + 4-byte IHDR length + 4-byte "IHDR" + 4-byte width + 4-byte height = 24 bytes minimum
|
|
232
|
+
const pngHeader = Buffer.alloc(24);
|
|
233
|
+
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
|
234
|
+
pngHeader[0] = 0x89;
|
|
235
|
+
pngHeader[1] = 0x50;
|
|
236
|
+
pngHeader[2] = 0x4e;
|
|
237
|
+
pngHeader[3] = 0x47;
|
|
238
|
+
pngHeader[4] = 0x0d;
|
|
239
|
+
pngHeader[5] = 0x0a;
|
|
240
|
+
pngHeader[6] = 0x1a;
|
|
241
|
+
pngHeader[7] = 0x0a;
|
|
242
|
+
// IHDR chunk length (13 bytes)
|
|
243
|
+
pngHeader.writeUInt32BE(13, 8);
|
|
244
|
+
// "IHDR"
|
|
245
|
+
pngHeader[12] = 0x49;
|
|
246
|
+
pngHeader[13] = 0x48;
|
|
247
|
+
pngHeader[14] = 0x44;
|
|
248
|
+
pngHeader[15] = 0x52;
|
|
249
|
+
// Width: 1920
|
|
250
|
+
pngHeader.writeUInt32BE(1920, 16);
|
|
251
|
+
// Height: 1080
|
|
252
|
+
pngHeader.writeUInt32BE(1080, 20);
|
|
253
|
+
|
|
254
|
+
// Pad with ~200 KB of data to simulate a real screenshot payload
|
|
255
|
+
const padding = Buffer.alloc(200_000, 0x42);
|
|
256
|
+
const fullPayload = Buffer.concat([pngHeader, padding]);
|
|
257
|
+
const base64Data = fullPayload.toString("base64");
|
|
258
|
+
|
|
259
|
+
const anthropicTokens = estimateContentBlockTokens(
|
|
260
|
+
{
|
|
261
|
+
type: "image",
|
|
262
|
+
source: { type: "base64", media_type: "image/png", data: base64Data },
|
|
263
|
+
},
|
|
264
|
+
{ providerName: "anthropic" },
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// 1920x1080 scaled to fit 1568x1568: scale = 1568/1920 = 0.8167
|
|
268
|
+
// scaledWidth = round(1920 * 0.8167) = 1568, scaledHeight = round(1080 * 0.8167) = 882
|
|
269
|
+
// tokens = ceil(1568 * 882 / 750) = ceil(1843.968) = ~1844
|
|
270
|
+
// With IMAGE_BLOCK_OVERHEAD_TOKENS and media_type overhead, still well under 5000
|
|
271
|
+
expect(anthropicTokens).toBeLessThan(5_000);
|
|
272
|
+
|
|
273
|
+
// Verify it's NOT using base64 size (which would be ~50,000+ tokens)
|
|
274
|
+
const nonAnthropicTokens = estimateContentBlockTokens(
|
|
275
|
+
{
|
|
276
|
+
type: "image",
|
|
277
|
+
source: { type: "base64", media_type: "image/png", data: base64Data },
|
|
278
|
+
},
|
|
279
|
+
{ providerName: "openai" },
|
|
280
|
+
);
|
|
281
|
+
expect(nonAnthropicTokens).toBeGreaterThan(50_000);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("falls back to max tokens when Anthropic image dimensions can't be parsed", () => {
|
|
285
|
+
// Corrupted base64 that won't parse as a valid image header
|
|
286
|
+
const corruptedData = Buffer.from(
|
|
287
|
+
"not-a-valid-image-header-at-all",
|
|
288
|
+
).toString("base64");
|
|
289
|
+
|
|
290
|
+
const tokens = estimateContentBlockTokens(
|
|
291
|
+
{
|
|
292
|
+
type: "image",
|
|
293
|
+
source: {
|
|
294
|
+
type: "base64",
|
|
295
|
+
media_type: "image/png",
|
|
296
|
+
data: corruptedData,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{ providerName: "anthropic" },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Should fall back to ANTHROPIC_IMAGE_MAX_TOKENS (~3,277)
|
|
303
|
+
// The total will include IMAGE_BLOCK_OVERHEAD_TOKENS + media_type overhead,
|
|
304
|
+
// but the max is applied at the outer Math.max(IMAGE_BLOCK_TOKENS, ...) level
|
|
305
|
+
// ANTHROPIC_IMAGE_MAX_TOKENS = ceil(1568*1568/750) = 3277
|
|
306
|
+
// Total = max(1024, 16 + ceil(9/4) + 3277) = max(1024, 3296) = 3296
|
|
307
|
+
expect(tokens).toBeGreaterThanOrEqual(3_277);
|
|
308
|
+
expect(tokens).toBeLessThan(4_000);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("Anthropic image tokens are the same for same-dimension images regardless of payload size", () => {
|
|
312
|
+
// Build two PNG headers with the same dimensions (800x600) but different payload sizes
|
|
313
|
+
function makePng(
|
|
314
|
+
width: number,
|
|
315
|
+
height: number,
|
|
316
|
+
paddingSize: number,
|
|
317
|
+
): string {
|
|
318
|
+
const header = Buffer.alloc(24);
|
|
319
|
+
header[0] = 0x89;
|
|
320
|
+
header[1] = 0x50;
|
|
321
|
+
header[2] = 0x4e;
|
|
322
|
+
header[3] = 0x47;
|
|
323
|
+
header[4] = 0x0d;
|
|
324
|
+
header[5] = 0x0a;
|
|
325
|
+
header[6] = 0x1a;
|
|
326
|
+
header[7] = 0x0a;
|
|
327
|
+
header.writeUInt32BE(13, 8);
|
|
328
|
+
header[12] = 0x49;
|
|
329
|
+
header[13] = 0x48;
|
|
330
|
+
header[14] = 0x44;
|
|
331
|
+
header[15] = 0x52;
|
|
332
|
+
header.writeUInt32BE(width, 16);
|
|
333
|
+
header.writeUInt32BE(height, 20);
|
|
334
|
+
const padding = Buffer.alloc(paddingSize, 0x42);
|
|
335
|
+
return Buffer.concat([header, padding]).toString("base64");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const smallPayload = makePng(800, 600, 1_000);
|
|
339
|
+
const largePayload = makePng(800, 600, 200_000);
|
|
340
|
+
|
|
341
|
+
const smallTokens = estimateContentBlockTokens(
|
|
342
|
+
{
|
|
343
|
+
type: "image",
|
|
344
|
+
source: { type: "base64", media_type: "image/png", data: smallPayload },
|
|
345
|
+
},
|
|
346
|
+
{ providerName: "anthropic" },
|
|
347
|
+
);
|
|
348
|
+
const largeTokens = estimateContentBlockTokens(
|
|
349
|
+
{
|
|
350
|
+
type: "image",
|
|
351
|
+
source: { type: "base64", media_type: "image/png", data: largePayload },
|
|
352
|
+
},
|
|
353
|
+
{ providerName: "anthropic" },
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// For Anthropic, same dimensions should produce the same estimate
|
|
357
|
+
expect(largeTokens).toBe(smallTokens);
|
|
358
|
+
});
|
|
176
359
|
});
|