@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5
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/bun.lock +20 -0
- package/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/openapi.yaml +196 -56
- package/package.json +4 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +42 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +164 -78
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/code-search-tool.test.ts +585 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/config-loader-backfill.test.ts +123 -10
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +19 -24
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +134 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/exploration-drift-hook.test.ts +3 -2
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
- package/src/__tests__/guardian-dispatch.test.ts +140 -1
- package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +290 -8
- package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +471 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
- package/src/__tests__/mcp-health-check.test.ts +2 -1
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/mtime-cache.test.ts +275 -5
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +191 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/openai-provider.test.ts +22 -12
- package/src/__tests__/openai-responses-provider.test.ts +12 -2
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +21 -83
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/plugin-pipeline.test.ts +96 -0
- package/src/__tests__/plugin-route-contribution.test.ts +6 -19
- package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
- package/src/__tests__/plugin-types.test.ts +2 -4
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-error-scenarios.test.ts +5 -4
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +4 -1
- package/src/__tests__/relay-server.test.ts +758 -32
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +49 -15
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -6
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +398 -1
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +216 -20
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +154 -0
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +164 -1
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/events/conversation-notice.ts +26 -0
- package/src/api/index.ts +19 -1
- package/src/api/responses/llm-request-log-entry.ts +29 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/api/surfaces.ts +39 -3
- package/src/approvals/guardian-channel-delivery.ts +30 -0
- package/src/approvals/guardian-expiry-notifier.ts +148 -0
- package/src/approvals/guardian-request-resolvers.ts +17 -15
- package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +14 -9
- package/src/calls/inbound-trust-reader.ts +24 -2
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +188 -51
- package/src/calls/relay-setup-router.ts +47 -17
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/channels/types.ts +10 -20
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +242 -0
- package/src/cli/commands/plugins.ts +125 -13
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/cli/lib/publish-plugin.ts +633 -0
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/bundled-skills/subagent/SKILL.md +17 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/feature-flag-registry.json +5 -13
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/loader.ts +38 -5
- package/src/config/prune-seeded-callsite-defaults.ts +110 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +30 -34
- package/src/config/skills.ts +27 -5
- package/src/config/sync-gated-profiles.ts +12 -16
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -55
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/contacts/member-write-relay.ts +183 -0
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/assistant-attachments.ts +27 -4
- package/src/daemon/conversation-agent-loop.ts +28 -0
- package/src/daemon/conversation-notices.ts +60 -0
- package/src/daemon/conversation-process.ts +35 -16
- package/src/daemon/conversation-surfaces.ts +111 -38
- package/src/daemon/conversation-tool-setup.ts +71 -30
- package/src/daemon/conversation.ts +23 -1
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +17 -41
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +67 -26
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/lifecycle.ts +16 -0
- package/src/daemon/mcp-reload-service.ts +10 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/message-types/conversations.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +12 -12
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/hooks/hook-loader.ts +341 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
- package/src/mcp/client.ts +15 -1
- package/src/mcp/mcp-auth-orchestrator.ts +6 -1
- package/src/mcp/mcp-header-store.ts +134 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -8
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +129 -61
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +37 -503
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +104 -0
- package/src/memory/llm-request-log-store.ts +26 -1
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +14 -8
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +106 -10
- package/src/memory/migrations/validate-migration-state.ts +105 -67
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +37 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +1294 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +204 -0
- package/src/memory/worker-process.ts +89 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/provider-types.ts +8 -0
- package/src/messaging/providers/slack/binding-metadata.ts +60 -0
- package/src/notifications/__tests__/broadcaster.test.ts +8 -8
- package/src/notifications/__tests__/connected-channels.test.ts +86 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +53 -25
- package/src/notifications/emit-signal.ts +34 -15
- package/src/onboarding/checkin-event.test.ts +222 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +12 -12
- package/src/plugin-api/types.ts +12 -14
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
- package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/external-plugin-loader.ts +2 -2
- package/src/plugins/mtime-cache.ts +186 -330
- package/src/plugins/pipeline.ts +111 -13
- package/src/plugins/registry.ts +59 -16
- package/src/plugins/surface-import.ts +121 -0
- package/src/plugins/types.ts +7 -9
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +45 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
- package/src/providers/openai/api-error-normalization.ts +270 -0
- package/src/providers/openai/chat-completions-provider.ts +37 -83
- package/src/providers/openai/responses-provider.ts +50 -46
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +10 -0
- package/src/providers/ratelimit.ts +10 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +10 -0
- package/src/providers/types.ts +22 -0
- package/src/providers/usage-tracking.ts +10 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
- package/src/runtime/access-request-helper.ts +18 -39
- package/src/runtime/actor-trust-resolver.ts +46 -19
- package/src/runtime/anchored-guardian.test.ts +109 -0
- package/src/runtime/anchored-guardian.ts +86 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +69 -8
- package/src/runtime/invite-redemption-service.ts +213 -187
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +85 -13
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
- package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +48 -57
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-query-routes.ts +72 -0
- package/src/runtime/routes/conversation-routes.ts +84 -85
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +16 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +83 -0
- package/src/runtime/routes/mcp-auth-routes.ts +171 -19
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +42 -56
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +13 -58
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +111 -40
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +254 -33
- package/src/subagent/types.ts +11 -4
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/executor.ts +4 -4
- package/src/tools/filesystem/search.ts +543 -0
- package/src/tools/registry.ts +38 -0
- package/src/tools/shared/filesystem/path-policy.ts +12 -5
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/tool-manifest.ts +3 -0
- package/src/tools/types.ts +131 -25
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/errors.ts +26 -1
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +19 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workflows/library.test.ts +140 -0
- package/src/workflows/library.ts +82 -28
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
- package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -93
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
- package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type OpenAI from "openai";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
captureRawErrorBodyFetch,
|
|
7
|
+
formatNormalizedOpenAIAPIError,
|
|
8
|
+
normalizeOpenAIAPIError,
|
|
9
|
+
} from "../api-error-normalization.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stand-in for an `OpenAI.APIError`. The real SDK constructor stringifies the
|
|
13
|
+
* body into `.message` and renders unparseable bodies as "(no body)" — exactly
|
|
14
|
+
* the lossy behavior we work around by capturing the raw body separately — so
|
|
15
|
+
* the tests pass the raw body explicitly the way the provider does.
|
|
16
|
+
*/
|
|
17
|
+
function apiError(
|
|
18
|
+
status: number,
|
|
19
|
+
opts: { message?: string; sdkBody?: unknown; headers?: Headers } = {},
|
|
20
|
+
): InstanceType<typeof OpenAI.APIError> {
|
|
21
|
+
return {
|
|
22
|
+
status,
|
|
23
|
+
message: opts.message ?? `${status} status code (no body)`,
|
|
24
|
+
headers: opts.headers ?? new Headers(),
|
|
25
|
+
error: opts.sdkBody,
|
|
26
|
+
} as unknown as InstanceType<typeof OpenAI.APIError>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("normalizeOpenAIAPIError", () => {
|
|
30
|
+
test("surfaces Django {detail} bodies the SDK would drop", () => {
|
|
31
|
+
const n = normalizeOpenAIAPIError(
|
|
32
|
+
apiError(400),
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
detail: "Model 'MiniMax-M3' is not yet supported on Vellum.",
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
expect(n.message).toBe(
|
|
38
|
+
"Model 'MiniMax-M3' is not yet supported on Vellum.",
|
|
39
|
+
);
|
|
40
|
+
expect(formatNormalizedOpenAIAPIError("Together AI", 400, n)).toBe(
|
|
41
|
+
"Together AI API error (400): Model 'MiniMax-M3' is not yet supported on Vellum.",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("extracts OpenAI-shaped error metadata", () => {
|
|
46
|
+
const n = normalizeOpenAIAPIError(
|
|
47
|
+
apiError(401),
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
error: {
|
|
50
|
+
message: "Invalid API key provided",
|
|
51
|
+
code: "invalid_api_key",
|
|
52
|
+
type: "invalid_request_error",
|
|
53
|
+
param: "api_key",
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
expect(n).toMatchObject({
|
|
58
|
+
message: "Invalid API key provided",
|
|
59
|
+
apiErrorCode: "invalid_api_key",
|
|
60
|
+
apiErrorType: "invalid_request_error",
|
|
61
|
+
apiErrorParam: "api_key",
|
|
62
|
+
});
|
|
63
|
+
expect(formatNormalizedOpenAIAPIError("OpenAI", 401, n)).toContain(
|
|
64
|
+
"code=invalid_api_key; type=invalid_request_error; param=api_key",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("promotes OpenRouter metadata.raw over the generic wrapper message", () => {
|
|
69
|
+
const n = normalizeOpenAIAPIError(
|
|
70
|
+
apiError(400, {
|
|
71
|
+
sdkBody: {
|
|
72
|
+
error: {
|
|
73
|
+
code: 400,
|
|
74
|
+
message: "Provider returned error",
|
|
75
|
+
metadata: {
|
|
76
|
+
raw: "messages.4: tool_use_id must reference a prior tool_use block",
|
|
77
|
+
provider_name: "Anthropic",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
expect(n.message).toBe(
|
|
84
|
+
"messages.4: tool_use_id must reference a prior tool_use block",
|
|
85
|
+
);
|
|
86
|
+
expect(n.detail).toBe("provider=Anthropic");
|
|
87
|
+
expect(n.apiErrorCode).toBe("400");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("prefers the captured raw body over the SDK's parsed error", () => {
|
|
91
|
+
// SDK collapsed the body; raw body still has the real detail.
|
|
92
|
+
const n = normalizeOpenAIAPIError(
|
|
93
|
+
apiError(400, { sdkBody: {} }),
|
|
94
|
+
JSON.stringify({ detail: "managed proxy rejected model" }),
|
|
95
|
+
);
|
|
96
|
+
expect(n.message).toBe("managed proxy rejected model");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("falls back to the SDK-parsed error when the captured JSON was truncated", () => {
|
|
100
|
+
// encodeCapturedBody slices oversized bodies, which can leave an invalid
|
|
101
|
+
// JSON prefix. Rather than render that fragment (and lose metadata), fall
|
|
102
|
+
// back to the body the SDK already parsed from the full response.
|
|
103
|
+
const truncated =
|
|
104
|
+
'{"error":{"message":"too long","code":"context_length_exceeded","type":"invalid_request_error","param":"messag';
|
|
105
|
+
const n = normalizeOpenAIAPIError(
|
|
106
|
+
apiError(400, {
|
|
107
|
+
sdkBody: {
|
|
108
|
+
error: {
|
|
109
|
+
message: "too long",
|
|
110
|
+
code: "context_length_exceeded",
|
|
111
|
+
type: "invalid_request_error",
|
|
112
|
+
param: "messages",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
truncated,
|
|
117
|
+
);
|
|
118
|
+
expect(n.message).toBe("too long");
|
|
119
|
+
expect(n.apiErrorCode).toBe("context_length_exceeded");
|
|
120
|
+
expect(n.apiErrorType).toBe("invalid_request_error");
|
|
121
|
+
expect(n.apiErrorParam).toBe("messages");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("uses a plain-text raw body when JSON parsing fails", () => {
|
|
125
|
+
const n = normalizeOpenAIAPIError(apiError(502), "upstream timeout");
|
|
126
|
+
expect(n.message).toBe("upstream timeout");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("reads the upstream request id from headers", () => {
|
|
130
|
+
const n = normalizeOpenAIAPIError(
|
|
131
|
+
apiError(400, { headers: new Headers({ "x-request-id": "req_abc123" }) }),
|
|
132
|
+
JSON.stringify({ detail: "bad request" }),
|
|
133
|
+
);
|
|
134
|
+
expect(n.requestId).toBe("req_abc123");
|
|
135
|
+
expect(formatNormalizedOpenAIAPIError("OpenAI", 400, n)).toContain(
|
|
136
|
+
"request_id=req_abc123",
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("falls back to x-openrouter-request-id", () => {
|
|
141
|
+
const n = normalizeOpenAIAPIError(
|
|
142
|
+
apiError(400, {
|
|
143
|
+
headers: new Headers({ "x-openrouter-request-id": "gen-or-xyz" }),
|
|
144
|
+
}),
|
|
145
|
+
JSON.stringify({ detail: "bad request" }),
|
|
146
|
+
);
|
|
147
|
+
expect(n.requestId).toBe("gen-or-xyz");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("truncates very long details with an ellipsis", () => {
|
|
151
|
+
const n = normalizeOpenAIAPIError(
|
|
152
|
+
apiError(400),
|
|
153
|
+
JSON.stringify({ detail: "X".repeat(5000) }),
|
|
154
|
+
);
|
|
155
|
+
expect(n.message.length).toBeLessThanOrEqual(2001);
|
|
156
|
+
expect(n.message.endsWith("…")).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("degrades to the status-stripped SDK message when there is no body", () => {
|
|
160
|
+
const n = normalizeOpenAIAPIError(
|
|
161
|
+
apiError(500, { message: "500 Internal Server Error" }),
|
|
162
|
+
);
|
|
163
|
+
expect(n.message).toBe("Internal Server Error");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("falls back to 'Request failed' for the SDK '(no body)' sentinel", () => {
|
|
167
|
+
const n = normalizeOpenAIAPIError(
|
|
168
|
+
apiError(500, { message: "500 status code (no body)" }),
|
|
169
|
+
);
|
|
170
|
+
expect(n.message).toBe("Request failed");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("surfaces the verbatim captured body as rawBody for downstream persistence", () => {
|
|
174
|
+
const raw = JSON.stringify({ detail: "model gone", extra: "kept" });
|
|
175
|
+
const n = normalizeOpenAIAPIError(apiError(400), raw);
|
|
176
|
+
// Extracted message is the clean detail...
|
|
177
|
+
expect(n.message).toBe("model gone");
|
|
178
|
+
// ...but the raw body is carried verbatim so the Raw tab can show it all.
|
|
179
|
+
expect(n.rawBody).toBe(raw);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("keeps sibling code/type/param when the body is {error: <string>}", () => {
|
|
183
|
+
const n = normalizeOpenAIAPIError(
|
|
184
|
+
apiError(400),
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
error: "bad request",
|
|
187
|
+
code: "context_length_exceeded",
|
|
188
|
+
type: "invalid_request_error",
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
expect(n.message).toBe("bad request");
|
|
192
|
+
expect(n.apiErrorCode).toBe("context_length_exceeded");
|
|
193
|
+
expect(n.apiErrorType).toBe("invalid_request_error");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("captureRawErrorBodyFetch", () => {
|
|
198
|
+
// The fetch wrapper stashes the dropped body in a WeakMap keyed by the
|
|
199
|
+
// response's headers object — which the SDK passes through to
|
|
200
|
+
// APIError.headers — so normalize() recovers it with no explicit rawBody
|
|
201
|
+
// argument and nothing is written to a (loggable) header.
|
|
202
|
+
function fakeFetch(body: string, status: number): typeof globalThis.fetch {
|
|
203
|
+
return (async () =>
|
|
204
|
+
new Response(body, { status })) as unknown as typeof globalThis.fetch;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function errFor(res: Response, status: number) {
|
|
208
|
+
return {
|
|
209
|
+
status,
|
|
210
|
+
message: `${status} status code (no body)`,
|
|
211
|
+
headers: res.headers,
|
|
212
|
+
error: undefined,
|
|
213
|
+
} as unknown as InstanceType<typeof import("openai").APIError>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
test("recovers a non-2xx body into normalize() via the headers WeakMap", async () => {
|
|
217
|
+
const original = globalThis.fetch;
|
|
218
|
+
globalThis.fetch = fakeFetch(JSON.stringify({ detail: "model gone" }), 400);
|
|
219
|
+
try {
|
|
220
|
+
const res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
221
|
+
// No rawBody arg: normalize recovers the body keyed by res.headers alone.
|
|
222
|
+
expect(normalizeOpenAIAPIError(errFor(res, 400)).message).toBe(
|
|
223
|
+
"model gone",
|
|
224
|
+
);
|
|
225
|
+
} finally {
|
|
226
|
+
globalThis.fetch = original;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("never writes the captured body to a header", async () => {
|
|
231
|
+
const original = globalThis.fetch;
|
|
232
|
+
globalThis.fetch = fakeFetch(JSON.stringify({ detail: "secret" }), 400);
|
|
233
|
+
try {
|
|
234
|
+
const res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
235
|
+
const headerNames = [...res.headers.keys()];
|
|
236
|
+
expect(headerNames.some((h) => h.includes("vellum"))).toBe(false);
|
|
237
|
+
} finally {
|
|
238
|
+
globalThis.fetch = original;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("passes OK responses through untouched", async () => {
|
|
243
|
+
const original = globalThis.fetch;
|
|
244
|
+
globalThis.fetch = fakeFetch("ok", 200);
|
|
245
|
+
try {
|
|
246
|
+
const res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
247
|
+
expect(await res.text()).toBe("ok");
|
|
248
|
+
} finally {
|
|
249
|
+
globalThis.fetch = original;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("recovers (truncated) detail for oversized terminal errors", async () => {
|
|
254
|
+
const big = "y".repeat(40_000);
|
|
255
|
+
const original = globalThis.fetch;
|
|
256
|
+
globalThis.fetch = fakeFetch(JSON.stringify({ detail: big }), 400);
|
|
257
|
+
try {
|
|
258
|
+
const res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
259
|
+
const n = normalizeOpenAIAPIError(errFor(res, 400));
|
|
260
|
+
// The captured body is bounded (16 KB) and the message re-truncated to
|
|
261
|
+
// MAX_DETAIL_CHARS, so an oversized error can't balloon either.
|
|
262
|
+
expect(n.message).toContain("yyyy");
|
|
263
|
+
expect(n.message.length).toBeLessThanOrEqual(2001);
|
|
264
|
+
} finally {
|
|
265
|
+
globalThis.fetch = original;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("does not drain SDK-retryable bodies (408/409/429/5xx)", async () => {
|
|
270
|
+
const original = globalThis.fetch;
|
|
271
|
+
// Mirror the full SDK retry predicate, not just 429/5xx.
|
|
272
|
+
for (const status of [408, 409, 429, 500, 503]) {
|
|
273
|
+
globalThis.fetch = fakeFetch(
|
|
274
|
+
JSON.stringify({ detail: "retry me" }),
|
|
275
|
+
status,
|
|
276
|
+
);
|
|
277
|
+
try {
|
|
278
|
+
const res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
279
|
+
// Pass-through: nothing captured, and the body is still readable by the
|
|
280
|
+
// SDK (we never consumed it).
|
|
281
|
+
expect(normalizeOpenAIAPIError(errFor(res, status)).message).not.toBe(
|
|
282
|
+
"retry me",
|
|
283
|
+
);
|
|
284
|
+
expect(res.status).toBe(status);
|
|
285
|
+
expect(await res.text()).toContain("retry me");
|
|
286
|
+
} finally {
|
|
287
|
+
globalThis.fetch = original;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("honors the x-should-retry header override", async () => {
|
|
293
|
+
const original = globalThis.fetch;
|
|
294
|
+
const withHeader = (
|
|
295
|
+
status: number,
|
|
296
|
+
value: string,
|
|
297
|
+
): typeof globalThis.fetch =>
|
|
298
|
+
(async () =>
|
|
299
|
+
new Response(JSON.stringify({ detail: "header says so" }), {
|
|
300
|
+
status,
|
|
301
|
+
headers: { "x-should-retry": value },
|
|
302
|
+
})) as unknown as typeof globalThis.fetch;
|
|
303
|
+
try {
|
|
304
|
+
// x-should-retry:true on an otherwise-terminal 400 → SDK retries → skip.
|
|
305
|
+
globalThis.fetch = withHeader(400, "true");
|
|
306
|
+
let res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
307
|
+
expect(normalizeOpenAIAPIError(errFor(res, 400)).message).not.toBe(
|
|
308
|
+
"header says so",
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// x-should-retry:false on a 500 → SDK won't retry → capture it.
|
|
312
|
+
globalThis.fetch = withHeader(500, "false");
|
|
313
|
+
res = await captureRawErrorBodyFetch("https://x/v1/chat", {});
|
|
314
|
+
expect(normalizeOpenAIAPIError(errFor(res, 500)).message).toBe(
|
|
315
|
+
"header says so",
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
globalThis.fetch = original;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalized view of an OpenAI-compatible `APIError`. The SDK reads
|
|
5
|
+
* `error.message` and renders bodies it can't parse as "(no body)", so
|
|
6
|
+
* Django `{"detail": "..."}` payloads from the managed runtime proxy and
|
|
7
|
+
* OpenRouter's nested `error.metadata.raw` get dropped. We capture the raw
|
|
8
|
+
* non-2xx body in a `fetch` wrapper and reconstruct the useful fields here.
|
|
9
|
+
*/
|
|
10
|
+
export interface NormalizedOpenAIAPIError {
|
|
11
|
+
message: string;
|
|
12
|
+
detail?: string;
|
|
13
|
+
requestId?: string;
|
|
14
|
+
apiErrorCode?: string;
|
|
15
|
+
apiErrorType?: string;
|
|
16
|
+
apiErrorParam?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The captured raw upstream non-2xx body, verbatim (possibly truncated to
|
|
19
|
+
* MAX_CAPTURED_BODY_CHARS). Carried so callers can persist the actual
|
|
20
|
+
* provider payload for the inspector's Raw tab instead of only the
|
|
21
|
+
* extracted fields. Absent for retryable (429/5xx) errors, whose bodies
|
|
22
|
+
* `captureRawErrorBodyFetch` intentionally doesn't drain.
|
|
23
|
+
*/
|
|
24
|
+
rawBody?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_DETAIL_CHARS = 2000;
|
|
28
|
+
const REQUEST_ID_HEADERS = [
|
|
29
|
+
"x-request-id",
|
|
30
|
+
"x-openrouter-request-id",
|
|
31
|
+
"openai-request-id",
|
|
32
|
+
"x-amzn-requestid",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
// The OpenAI SDK keeps only the `.error` key of a parsed JSON error body and
|
|
36
|
+
// discards the rest, so a Django-proxy `{ "detail": … }` payload never reaches
|
|
37
|
+
// the thrown APIError. captureRawErrorBodyFetch stashes the raw body in a
|
|
38
|
+
// WeakMap keyed by the response's headers object — which the SDK passes through
|
|
39
|
+
// to `APIError.headers` by reference — so it stays correlated to its own
|
|
40
|
+
// request, with no shared provider state for concurrent calls to clobber. A
|
|
41
|
+
// WeakMap (rather than a synthetic header) keeps the body out of the SDK's
|
|
42
|
+
// debug header logging and is reclaimed automatically with the response.
|
|
43
|
+
const capturedErrorBodies = new WeakMap<object, string>();
|
|
44
|
+
// Bound the stored body so a huge HTML error page can't balloon memory; the
|
|
45
|
+
// message is re-truncated to MAX_DETAIL_CHARS downstream anyway.
|
|
46
|
+
const MAX_CAPTURED_BODY_CHARS = 16_384;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SDK `fetch` option that captures non-2xx response bodies. Install via the
|
|
50
|
+
* OpenAI client `fetch` option; safe to share across requests and instances
|
|
51
|
+
* because the captured body is keyed per-response in a WeakMap, not stored on
|
|
52
|
+
* any shared field.
|
|
53
|
+
*/
|
|
54
|
+
export const captureRawErrorBodyFetch = async (
|
|
55
|
+
url: RequestInfo | URL,
|
|
56
|
+
init?: RequestInit,
|
|
57
|
+
): Promise<Response> => {
|
|
58
|
+
const res = await globalThis.fetch(url, init);
|
|
59
|
+
if (res.ok) return res;
|
|
60
|
+
// Don't drain bodies the SDK will retry: reading a large or slow upstream
|
|
61
|
+
// error page on every attempt would delay those retries and buffer the whole
|
|
62
|
+
// body. We still capture terminal (non-retryable) errors — that's where the
|
|
63
|
+
// actionable upstream detail lives (unsupported model, invalid key, malformed
|
|
64
|
+
// request, etc.).
|
|
65
|
+
if (sdkWillRetry(res)) return res;
|
|
66
|
+
// clone() so reading the body leaves the SDK's own read of `res` intact.
|
|
67
|
+
const body = await res
|
|
68
|
+
.clone()
|
|
69
|
+
.text()
|
|
70
|
+
.catch(() => undefined);
|
|
71
|
+
if (!body) return res;
|
|
72
|
+
capturedErrorBodies.set(
|
|
73
|
+
res.headers,
|
|
74
|
+
body.length > MAX_CAPTURED_BODY_CHARS
|
|
75
|
+
? body.slice(0, MAX_CAPTURED_BODY_CHARS)
|
|
76
|
+
: body,
|
|
77
|
+
);
|
|
78
|
+
return res;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mirror the OpenAI SDK's `shouldRetry` predicate so this wrapper never drains
|
|
83
|
+
* a body the SDK is about to retry. The SDK retries more than 429/5xx: an
|
|
84
|
+
* explicit `x-should-retry` header (which also overrides 429/5xx to *not*
|
|
85
|
+
* retry), plus 408 (request timeout) and 409 (lock timeout). Keep in sync with
|
|
86
|
+
* `openai/client.js` `shouldRetry`.
|
|
87
|
+
*/
|
|
88
|
+
function sdkWillRetry(res: Response): boolean {
|
|
89
|
+
const shouldRetryHeader = res.headers.get("x-should-retry");
|
|
90
|
+
if (shouldRetryHeader === "true") return true;
|
|
91
|
+
if (shouldRetryHeader === "false") return false;
|
|
92
|
+
return (
|
|
93
|
+
res.status === 408 ||
|
|
94
|
+
res.status === 409 ||
|
|
95
|
+
res.status === 429 ||
|
|
96
|
+
res.status >= 500
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeOpenAIAPIError(
|
|
101
|
+
error: InstanceType<typeof OpenAI.APIError>,
|
|
102
|
+
rawBody: string | undefined = readCapturedErrorBody(error.headers),
|
|
103
|
+
): NormalizedOpenAIAPIError {
|
|
104
|
+
// Prefer the captured raw body (intact upstream payload) over the SDK's
|
|
105
|
+
// already-parsed `error.error`, which may have collapsed the detail. But if
|
|
106
|
+
// the captured body didn't parse as a JSON object — e.g. it was truncated
|
|
107
|
+
// past MAX_CAPTURED_BODY_CHARS into an invalid prefix — fall back to the
|
|
108
|
+
// SDK's parsed object so we don't drop code/type/param it already extracted.
|
|
109
|
+
const parsedRaw = parseBody(rawBody);
|
|
110
|
+
const sdkError = (error as { error?: unknown }).error;
|
|
111
|
+
const parsed = asRecord(parsedRaw) ?? sdkError ?? parsedRaw;
|
|
112
|
+
const body = extractBody(parsed);
|
|
113
|
+
|
|
114
|
+
const message =
|
|
115
|
+
body.message ||
|
|
116
|
+
stripLeadingStatus(error.message ?? "", error.status) ||
|
|
117
|
+
"Request failed";
|
|
118
|
+
|
|
119
|
+
const out: NormalizedOpenAIAPIError = { message };
|
|
120
|
+
if (body.detail && body.detail !== message) out.detail = body.detail;
|
|
121
|
+
const code = body.apiErrorCode ?? scalar((error as { code?: unknown }).code);
|
|
122
|
+
const type = body.apiErrorType ?? scalar((error as { type?: unknown }).type);
|
|
123
|
+
const param =
|
|
124
|
+
body.apiErrorParam ?? scalar((error as { param?: unknown }).param);
|
|
125
|
+
if (code) out.apiErrorCode = code;
|
|
126
|
+
if (type) out.apiErrorType = type;
|
|
127
|
+
if (param) out.apiErrorParam = param;
|
|
128
|
+
const requestId = readHeader(error.headers);
|
|
129
|
+
if (requestId) out.requestId = requestId;
|
|
130
|
+
if (rawBody) out.rawBody = rawBody;
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function formatNormalizedOpenAIAPIError(
|
|
135
|
+
providerLabel: string,
|
|
136
|
+
status: number | undefined,
|
|
137
|
+
n: NormalizedOpenAIAPIError,
|
|
138
|
+
): string {
|
|
139
|
+
const statusLabel =
|
|
140
|
+
typeof status === "number" ? String(status) : "unknown status";
|
|
141
|
+
const extras = [
|
|
142
|
+
n.detail,
|
|
143
|
+
n.apiErrorCode && `code=${n.apiErrorCode}`,
|
|
144
|
+
n.apiErrorType && `type=${n.apiErrorType}`,
|
|
145
|
+
n.apiErrorParam && `param=${n.apiErrorParam}`,
|
|
146
|
+
n.requestId && `request_id=${n.requestId}`,
|
|
147
|
+
].filter((v): v is string => Boolean(v));
|
|
148
|
+
const suffix = extras.length > 0 ? ` [${extras.join("; ")}]` : "";
|
|
149
|
+
return `${providerLabel} API error (${statusLabel}): ${n.message}${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface BodyDetails {
|
|
153
|
+
message?: string;
|
|
154
|
+
detail?: string;
|
|
155
|
+
apiErrorCode?: string;
|
|
156
|
+
apiErrorType?: string;
|
|
157
|
+
apiErrorParam?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function extractBody(body: unknown): BodyDetails {
|
|
161
|
+
if (typeof body === "string") return { message: trunc(body.trim()) };
|
|
162
|
+
const rec = asRecord(body);
|
|
163
|
+
if (!rec) return {};
|
|
164
|
+
|
|
165
|
+
// OpenAI/OpenRouter nest under `error`; Django puts `detail` at the top.
|
|
166
|
+
// A plain `error: "string"` is the whole message but may still carry
|
|
167
|
+
// sibling code/type/param, so fall through to the metadata extraction below.
|
|
168
|
+
const err = asRecord(rec.error) ?? rec;
|
|
169
|
+
|
|
170
|
+
let message =
|
|
171
|
+
str(rec.error) ?? str(err.message) ?? str(err.detail) ?? str(rec.detail);
|
|
172
|
+
let detail: string | undefined;
|
|
173
|
+
|
|
174
|
+
// OpenRouter: the real downstream error lives in metadata.raw while the
|
|
175
|
+
// top-level message is a generic "Provider returned error".
|
|
176
|
+
const meta = asRecord(err.metadata);
|
|
177
|
+
const raw = str(meta?.raw);
|
|
178
|
+
const provider = str(meta?.provider_name);
|
|
179
|
+
if (raw && message && /^provider returned error$/i.test(message)) {
|
|
180
|
+
message = raw;
|
|
181
|
+
} else if (raw && raw !== message) {
|
|
182
|
+
detail = raw;
|
|
183
|
+
}
|
|
184
|
+
if (provider) {
|
|
185
|
+
detail = detail
|
|
186
|
+
? `${detail}; provider=${provider}`
|
|
187
|
+
: `provider=${provider}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...(message ? { message: trunc(message) } : {}),
|
|
192
|
+
...(detail ? { detail: trunc(detail) } : {}),
|
|
193
|
+
...withScalar("apiErrorCode", err.code ?? rec.code),
|
|
194
|
+
...withScalar("apiErrorType", err.type ?? rec.type),
|
|
195
|
+
...withScalar("apiErrorParam", err.param ?? rec.param),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseBody(raw: string | undefined): unknown {
|
|
200
|
+
const trimmed = raw?.trim();
|
|
201
|
+
if (!trimmed) return undefined;
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(trimmed);
|
|
204
|
+
} catch {
|
|
205
|
+
return trimmed; // non-JSON body (HTML error page, plain text)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stripLeadingStatus(
|
|
210
|
+
message: string,
|
|
211
|
+
status: number | undefined,
|
|
212
|
+
): string {
|
|
213
|
+
const trimmed = message.trim();
|
|
214
|
+
// SDK sentinel for an unparseable/empty body — carries no signal, so let the
|
|
215
|
+
// caller fall back to "Request failed" rather than surface SDK phrasing.
|
|
216
|
+
if (/^\d*\s*status code \(no body\)$/i.test(trimmed)) return "";
|
|
217
|
+
if (typeof status !== "number") return trimmed;
|
|
218
|
+
return trimmed.replace(new RegExp(`^${status}\\s+`), "").trim() || trimmed;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function trunc(s: string): string {
|
|
222
|
+
return s.length > MAX_DETAIL_CHARS ? `${s.slice(0, MAX_DETAIL_CHARS)}…` : s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function withScalar(key: keyof BodyDetails, value: unknown): BodyDetails {
|
|
226
|
+
const s = scalar(value);
|
|
227
|
+
return s ? { [key]: s } : {};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function scalar(value: unknown): string | undefined {
|
|
231
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
232
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function str(value: unknown): string | undefined {
|
|
237
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
241
|
+
return value && typeof value === "object"
|
|
242
|
+
? (value as Record<string, unknown>)
|
|
243
|
+
: undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readHeader(headers: unknown): string | undefined {
|
|
247
|
+
return readHeaderValue(headers, REQUEST_ID_HEADERS);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function readHeaderValue(
|
|
251
|
+
headers: unknown,
|
|
252
|
+
names: readonly string[],
|
|
253
|
+
): string | undefined {
|
|
254
|
+
if (!headers) return undefined;
|
|
255
|
+
const get = (headers as { get?: unknown }).get;
|
|
256
|
+
const getter = typeof get === "function" ? get.bind(headers) : undefined;
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const raw = getter
|
|
259
|
+
? (getter(name) as string | null)
|
|
260
|
+
: (asRecord(headers)?.[name] ?? asRecord(headers)?.[name.toLowerCase()]);
|
|
261
|
+
if (typeof raw === "string" && raw.length > 0) return raw;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readCapturedErrorBody(headers: unknown): string | undefined {
|
|
267
|
+
return headers && typeof headers === "object"
|
|
268
|
+
? capturedErrorBodies.get(headers)
|
|
269
|
+
: undefined;
|
|
270
|
+
}
|