@vellumai/assistant 0.11.4 → 0.11.5-staging.2
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/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -294,6 +294,53 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
test("round-trips reasoning_content on assistant messages that carry tool_calls", async () => {
|
|
298
|
+
const { provider, requests } = stubProvider(
|
|
299
|
+
[
|
|
300
|
+
{
|
|
301
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
302
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
await provider.sendMessage([
|
|
309
|
+
{
|
|
310
|
+
role: "assistant",
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: "thinking",
|
|
314
|
+
thinking: "need the search tool",
|
|
315
|
+
signature: "",
|
|
316
|
+
},
|
|
317
|
+
{ type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const params = requests[0] as {
|
|
323
|
+
messages: Array<{
|
|
324
|
+
role: string;
|
|
325
|
+
content: string | null;
|
|
326
|
+
reasoning_content?: string;
|
|
327
|
+
tool_calls?: unknown;
|
|
328
|
+
}>;
|
|
329
|
+
};
|
|
330
|
+
expect(params.messages[0]).toEqual({
|
|
331
|
+
role: "assistant",
|
|
332
|
+
content: null,
|
|
333
|
+
reasoning_content: "need the search tool",
|
|
334
|
+
tool_calls: [
|
|
335
|
+
{
|
|
336
|
+
id: "call_1",
|
|
337
|
+
type: "function",
|
|
338
|
+
function: { name: "search", arguments: JSON.stringify({ q: "x" }) },
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
297
344
|
test("uses reasoning field for OpenRouter-style round-trip", async () => {
|
|
298
345
|
const { provider, requests } = stubProvider(
|
|
299
346
|
[
|
|
@@ -438,18 +485,88 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
438
485
|
};
|
|
439
486
|
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
440
487
|
// Backfill defaults off, so providers that tolerate null assistant content
|
|
441
|
-
// (e.g.
|
|
488
|
+
// (e.g. Fireworks, Together) are unaffected unless they opt in.
|
|
442
489
|
expect(assistantMsg.content).toBeNull();
|
|
443
490
|
});
|
|
444
491
|
|
|
445
|
-
test("
|
|
446
|
-
|
|
492
|
+
test("backfills placeholder when thinking is dropped and no text was emitted", async () => {
|
|
493
|
+
// Custom openai-compatible endpoints do not set assistantReasoningField, so
|
|
494
|
+
// a Stop during thinking serializes to { role: "assistant", content: null }
|
|
495
|
+
// unless the backfill guard runs.
|
|
496
|
+
const { provider, requests } = stubProvider(
|
|
497
|
+
[
|
|
498
|
+
{
|
|
499
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
500
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
{ backfillEmptyAssistantContent: true },
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
await provider.sendMessage([
|
|
507
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
447
508
|
{
|
|
448
|
-
|
|
449
|
-
|
|
509
|
+
role: "assistant",
|
|
510
|
+
content: [
|
|
511
|
+
{
|
|
512
|
+
type: "thinking",
|
|
513
|
+
thinking: "aborted mid-thought",
|
|
514
|
+
signature: "",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
450
517
|
},
|
|
451
518
|
]);
|
|
452
519
|
|
|
520
|
+
const params = requests[0] as {
|
|
521
|
+
messages: Array<{
|
|
522
|
+
role: string;
|
|
523
|
+
content: string | null;
|
|
524
|
+
reasoning?: string;
|
|
525
|
+
reasoning_content?: string;
|
|
526
|
+
tool_calls?: unknown;
|
|
527
|
+
}>;
|
|
528
|
+
};
|
|
529
|
+
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
530
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
531
|
+
expect(assistantMsg.tool_calls).toBeUndefined();
|
|
532
|
+
expect(assistantMsg.reasoning).toBeUndefined();
|
|
533
|
+
expect(assistantMsg.reasoning_content).toBeUndefined();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("backfills placeholder for an empty aborted assistant turn", async () => {
|
|
537
|
+
const { provider, requests } = stubProvider(
|
|
538
|
+
[
|
|
539
|
+
{
|
|
540
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
541
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
{ backfillEmptyAssistantContent: true },
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await provider.sendMessage([
|
|
548
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
549
|
+
{ role: "assistant", content: [] },
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
const params = requests[0] as {
|
|
553
|
+
messages: Array<{ role: string; content: string | null }>;
|
|
554
|
+
};
|
|
555
|
+
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
556
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("does not backfill content when tool calls are present", async () => {
|
|
560
|
+
const { provider, requests } = stubProvider(
|
|
561
|
+
[
|
|
562
|
+
{
|
|
563
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
564
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
{ backfillEmptyAssistantContent: true },
|
|
568
|
+
);
|
|
569
|
+
|
|
453
570
|
await provider.sendMessage([
|
|
454
571
|
{
|
|
455
572
|
role: "assistant",
|
|
@@ -460,12 +577,18 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
460
577
|
]);
|
|
461
578
|
|
|
462
579
|
const params = requests[0] as {
|
|
463
|
-
messages: Array<{
|
|
580
|
+
messages: Array<{
|
|
581
|
+
role: string;
|
|
582
|
+
content: string | null;
|
|
583
|
+
reasoning_content?: string;
|
|
584
|
+
}>;
|
|
464
585
|
};
|
|
465
586
|
// Tool-call-only assistant messages keep null content (preferred by
|
|
466
587
|
// Anthropic-proxy/Bedrock backends); the placeholder is only for the
|
|
467
|
-
// neither-content-nor-tool_calls case.
|
|
588
|
+
// neither-content-nor-tool_calls case. The reasoning field stays omitted
|
|
589
|
+
// when assistantReasoningField is unset.
|
|
468
590
|
expect(params.messages[0].content).toBeNull();
|
|
591
|
+
expect(params.messages[0].reasoning_content).toBeUndefined();
|
|
469
592
|
});
|
|
470
593
|
|
|
471
594
|
test("forwards config.top_p onto the request body", async () => {
|
|
@@ -535,52 +658,153 @@ describe("OpenAIChatCompletionsProvider reasoning parsing", () => {
|
|
|
535
658
|
};
|
|
536
659
|
expect(params.messages[0].reasoning_content).toBe("deepseek thinking");
|
|
537
660
|
});
|
|
538
|
-
});
|
|
539
661
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
"test-key",
|
|
547
|
-
"test-model",
|
|
548
|
-
);
|
|
549
|
-
const requests: unknown[] = [];
|
|
550
|
-
const pending = [...errors];
|
|
551
|
-
(provider as unknown as { client: unknown }).client = {
|
|
552
|
-
chat: {
|
|
553
|
-
completions: {
|
|
554
|
-
create: async (params: unknown) => {
|
|
555
|
-
// Snapshot: the fallback mutates `params` between attempts.
|
|
556
|
-
requests.push(JSON.parse(JSON.stringify(params)));
|
|
557
|
-
const error = pending.shift();
|
|
558
|
-
if (error !== undefined) {
|
|
559
|
-
throw error;
|
|
560
|
-
}
|
|
561
|
-
return makeStream(chunks);
|
|
562
|
-
},
|
|
662
|
+
test("omits empty reasoning_content on tool-call turns even when the field is set", async () => {
|
|
663
|
+
const { provider, requests } = stubProvider(
|
|
664
|
+
[
|
|
665
|
+
{
|
|
666
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
667
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
563
668
|
},
|
|
669
|
+
],
|
|
670
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
await provider.sendMessage([
|
|
674
|
+
{
|
|
675
|
+
role: "assistant",
|
|
676
|
+
content: [
|
|
677
|
+
{ type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
|
|
678
|
+
],
|
|
564
679
|
},
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
const params = requests[0] as {
|
|
683
|
+
messages: Array<{
|
|
684
|
+
role: string;
|
|
685
|
+
content: string | null;
|
|
686
|
+
tool_calls?: unknown;
|
|
687
|
+
reasoning_content?: string;
|
|
688
|
+
}>;
|
|
565
689
|
};
|
|
566
|
-
|
|
567
|
-
|
|
690
|
+
const assistantMsg = params.messages[0];
|
|
691
|
+
expect(assistantMsg.tool_calls).toEqual([
|
|
692
|
+
{
|
|
693
|
+
id: "call_1",
|
|
694
|
+
type: "function",
|
|
695
|
+
function: { name: "search", arguments: JSON.stringify({ q: "x" }) },
|
|
696
|
+
},
|
|
697
|
+
]);
|
|
698
|
+
expect(assistantMsg.reasoning_content).toBeUndefined();
|
|
699
|
+
});
|
|
568
700
|
|
|
569
|
-
|
|
570
|
-
{
|
|
571
|
-
|
|
572
|
-
|
|
701
|
+
test("omits empty reasoning on tool-call turns for the OpenRouter-style field", async () => {
|
|
702
|
+
const { provider, requests } = stubProvider(
|
|
703
|
+
[
|
|
704
|
+
{
|
|
705
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
706
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
{ assistantReasoningField: "reasoning" },
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
await provider.sendMessage([
|
|
713
|
+
{
|
|
714
|
+
role: "assistant",
|
|
715
|
+
content: [
|
|
716
|
+
{ type: "tool_use", id: "call_1", name: "search", input: { q: "x" } },
|
|
717
|
+
],
|
|
718
|
+
},
|
|
719
|
+
]);
|
|
720
|
+
|
|
721
|
+
const params = requests[0] as {
|
|
722
|
+
messages: Array<{
|
|
723
|
+
role: string;
|
|
724
|
+
reasoning?: string;
|
|
725
|
+
reasoning_content?: string;
|
|
726
|
+
}>;
|
|
727
|
+
};
|
|
728
|
+
expect(params.messages[0].reasoning).toBeUndefined();
|
|
729
|
+
expect(params.messages[0].reasoning_content).toBeUndefined();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("omits empty reasoning_content on text-only turns even when the field is set", async () => {
|
|
733
|
+
const { provider, requests } = stubProvider(
|
|
734
|
+
[
|
|
735
|
+
{
|
|
736
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
737
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
await provider.sendMessage([
|
|
744
|
+
{
|
|
745
|
+
role: "assistant",
|
|
746
|
+
content: [{ type: "text", text: "plain reply" }],
|
|
747
|
+
},
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
const params = requests[0] as {
|
|
751
|
+
messages: Array<{
|
|
752
|
+
role: string;
|
|
753
|
+
content: string | null;
|
|
754
|
+
reasoning_content?: string;
|
|
755
|
+
}>;
|
|
756
|
+
};
|
|
757
|
+
expect(params.messages[0].content).toBe("plain reply");
|
|
758
|
+
expect(params.messages[0].reasoning_content).toBeUndefined();
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
function stubProviderWithErrors(
|
|
763
|
+
errors: unknown[],
|
|
764
|
+
chunks: MockChunk[],
|
|
765
|
+
options?: OpenAIChatCompletionsProviderOptions,
|
|
766
|
+
): { provider: OpenAIChatCompletionsProvider; requests: unknown[] } {
|
|
767
|
+
const provider = new OpenAIChatCompletionsProvider(
|
|
768
|
+
"test-key",
|
|
769
|
+
"test-model",
|
|
770
|
+
options,
|
|
771
|
+
);
|
|
772
|
+
const requests: unknown[] = [];
|
|
773
|
+
const pending = [...errors];
|
|
774
|
+
(provider as unknown as { client: unknown }).client = {
|
|
775
|
+
chat: {
|
|
776
|
+
completions: {
|
|
777
|
+
create: async (params: unknown) => {
|
|
778
|
+
// Snapshot: the fallback mutates `params` between attempts.
|
|
779
|
+
requests.push(JSON.parse(JSON.stringify(params)));
|
|
780
|
+
const error = pending.shift();
|
|
781
|
+
if (error !== undefined) {
|
|
782
|
+
throw error;
|
|
783
|
+
}
|
|
784
|
+
return makeStream(chunks);
|
|
785
|
+
},
|
|
786
|
+
},
|
|
573
787
|
},
|
|
574
|
-
|
|
788
|
+
};
|
|
789
|
+
return { provider, requests };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const OK_CHUNKS: MockChunk[] = [
|
|
793
|
+
{
|
|
794
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
795
|
+
usage: { prompt_tokens: 1, completion_tokens: 1 },
|
|
796
|
+
},
|
|
797
|
+
];
|
|
575
798
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
799
|
+
function rejection(message: string, status = 400): Error {
|
|
800
|
+
return Object.assign(new Error(message), { status });
|
|
801
|
+
}
|
|
579
802
|
|
|
803
|
+
describe("reasoning opt-out rejection fallback", () => {
|
|
580
804
|
test("retries once without reasoning params when a model rejects the explicit opt-out", async () => {
|
|
581
805
|
const { provider, requests } = stubProviderWithErrors(
|
|
582
806
|
[rejection("reasoning_effort 'none' is not supported for this model")],
|
|
583
|
-
|
|
807
|
+
OK_CHUNKS,
|
|
584
808
|
);
|
|
585
809
|
|
|
586
810
|
const response = await provider.sendMessage(
|
|
@@ -626,7 +850,7 @@ describe("reasoning opt-out rejection fallback", () => {
|
|
|
626
850
|
// can only fire off the normalized upstream detail.
|
|
627
851
|
expect(/reasoning/i.test(wrapped.message)).toBe(false);
|
|
628
852
|
|
|
629
|
-
const { provider, requests } = stubProviderWithErrors([wrapped],
|
|
853
|
+
const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS);
|
|
630
854
|
|
|
631
855
|
const response = await provider.sendMessage(
|
|
632
856
|
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
@@ -651,7 +875,7 @@ describe("reasoning opt-out rejection fallback", () => {
|
|
|
651
875
|
test("does not retry when the request did not opt out of reasoning", async () => {
|
|
652
876
|
const { provider, requests } = stubProviderWithErrors(
|
|
653
877
|
[rejection("reasoning_effort is invalid")],
|
|
654
|
-
|
|
878
|
+
OK_CHUNKS,
|
|
655
879
|
);
|
|
656
880
|
|
|
657
881
|
await expect(
|
|
@@ -666,7 +890,7 @@ describe("reasoning opt-out rejection fallback", () => {
|
|
|
666
890
|
test("does not retry a 4xx that does not name reasoning", async () => {
|
|
667
891
|
const { provider, requests } = stubProviderWithErrors(
|
|
668
892
|
[rejection("invalid api key")],
|
|
669
|
-
|
|
893
|
+
OK_CHUNKS,
|
|
670
894
|
);
|
|
671
895
|
|
|
672
896
|
await expect(
|
|
@@ -681,7 +905,7 @@ describe("reasoning opt-out rejection fallback", () => {
|
|
|
681
905
|
test("does not retry server errors", async () => {
|
|
682
906
|
const { provider, requests } = stubProviderWithErrors(
|
|
683
907
|
[rejection("reasoning backend unavailable", 500)],
|
|
684
|
-
|
|
908
|
+
OK_CHUNKS,
|
|
685
909
|
);
|
|
686
910
|
|
|
687
911
|
await expect(
|
|
@@ -694,6 +918,312 @@ describe("reasoning opt-out rejection fallback", () => {
|
|
|
694
918
|
});
|
|
695
919
|
});
|
|
696
920
|
|
|
921
|
+
describe("thinking-mode tool_choice rejection fallback", () => {
|
|
922
|
+
test("retries once without tool_choice when thinking mode rejects it", async () => {
|
|
923
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
924
|
+
[rejection("Thinking mode does not support this tool_choice")],
|
|
925
|
+
OK_CHUNKS,
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const response = await provider.sendMessage(
|
|
929
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
930
|
+
{
|
|
931
|
+
tools: [
|
|
932
|
+
{
|
|
933
|
+
name: "bash",
|
|
934
|
+
description: "Run a shell command",
|
|
935
|
+
input_schema: { type: "object", properties: {} },
|
|
936
|
+
},
|
|
937
|
+
],
|
|
938
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
expect(requests).toHaveLength(2);
|
|
943
|
+
const first = requests[0] as {
|
|
944
|
+
tool_choice?: string;
|
|
945
|
+
reasoning_effort?: string;
|
|
946
|
+
};
|
|
947
|
+
const second = requests[1] as {
|
|
948
|
+
tool_choice?: string;
|
|
949
|
+
reasoning_effort?: string;
|
|
950
|
+
};
|
|
951
|
+
expect(first.tool_choice).toBe("none");
|
|
952
|
+
expect(first.reasoning_effort).toBe("high");
|
|
953
|
+
expect(second.tool_choice).toBeUndefined();
|
|
954
|
+
expect(second.reasoning_effort).toBe("high");
|
|
955
|
+
const text = response.content.find((b) => b.type === "text") as
|
|
956
|
+
| { type: "text"; text: string }
|
|
957
|
+
| undefined;
|
|
958
|
+
expect(text?.text).toBe("ok");
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test("retries once for an OpenRouter-wrapped thinking-mode tool_choice rejection", async () => {
|
|
962
|
+
const wrapped = new OpenAI.APIError(
|
|
963
|
+
400,
|
|
964
|
+
{
|
|
965
|
+
code: 400,
|
|
966
|
+
message: "Provider returned error",
|
|
967
|
+
metadata: {
|
|
968
|
+
raw: "[invalid_request_error] Thinking mode does not support this tool_choice",
|
|
969
|
+
provider_name: "deepseek",
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
undefined,
|
|
973
|
+
new Headers(),
|
|
974
|
+
);
|
|
975
|
+
expect(/tool_choice/i.test(wrapped.message)).toBe(false);
|
|
976
|
+
|
|
977
|
+
const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS);
|
|
978
|
+
|
|
979
|
+
await provider.sendMessage(
|
|
980
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
981
|
+
{
|
|
982
|
+
tools: [
|
|
983
|
+
{
|
|
984
|
+
name: "bash",
|
|
985
|
+
description: "Run a shell command",
|
|
986
|
+
input_schema: { type: "object", properties: {} },
|
|
987
|
+
},
|
|
988
|
+
],
|
|
989
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
expect(requests).toHaveLength(2);
|
|
994
|
+
expect((requests[0] as { tool_choice?: string }).tool_choice).toBe("none");
|
|
995
|
+
expect((requests[1] as { tool_choice?: string }).tool_choice).toBeUndefined();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("does not retry a 4xx that does not name tool_choice", async () => {
|
|
999
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1000
|
+
[rejection("invalid api key")],
|
|
1001
|
+
OK_CHUNKS,
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
await expect(
|
|
1005
|
+
provider.sendMessage(
|
|
1006
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1007
|
+
{
|
|
1008
|
+
tools: [
|
|
1009
|
+
{
|
|
1010
|
+
name: "bash",
|
|
1011
|
+
description: "Run a shell command",
|
|
1012
|
+
input_schema: { type: "object", properties: {} },
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
1016
|
+
},
|
|
1017
|
+
),
|
|
1018
|
+
).rejects.toThrow();
|
|
1019
|
+
expect(requests).toHaveLength(1);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
test("does not retry thinking-mode tool_choice 500s", async () => {
|
|
1023
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1024
|
+
[rejection("Thinking mode does not support this tool_choice", 500)],
|
|
1025
|
+
OK_CHUNKS,
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
await expect(
|
|
1029
|
+
provider.sendMessage(
|
|
1030
|
+
[{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
|
1031
|
+
{
|
|
1032
|
+
tools: [
|
|
1033
|
+
{
|
|
1034
|
+
name: "bash",
|
|
1035
|
+
description: "Run a shell command",
|
|
1036
|
+
input_schema: { type: "object", properties: {} },
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
1040
|
+
},
|
|
1041
|
+
),
|
|
1042
|
+
).rejects.toThrow();
|
|
1043
|
+
expect(requests).toHaveLength(1);
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
describe("missing reasoning_content rejection fallback", () => {
|
|
1048
|
+
const toolCallHistory = [
|
|
1049
|
+
{
|
|
1050
|
+
role: "assistant" as const,
|
|
1051
|
+
content: [
|
|
1052
|
+
{
|
|
1053
|
+
type: "tool_use" as const,
|
|
1054
|
+
id: "call_1",
|
|
1055
|
+
name: "search",
|
|
1056
|
+
input: { q: "x" },
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
test("retries once with empty reasoning_content when thinking mode requires it", async () => {
|
|
1063
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1064
|
+
[
|
|
1065
|
+
rejection(
|
|
1066
|
+
"The reasoning_content in the thinking mode must be passed back to the API",
|
|
1067
|
+
),
|
|
1068
|
+
],
|
|
1069
|
+
OK_CHUNKS,
|
|
1070
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
const response = await provider.sendMessage(toolCallHistory);
|
|
1074
|
+
|
|
1075
|
+
expect(requests).toHaveLength(2);
|
|
1076
|
+
const first = requests[0] as {
|
|
1077
|
+
messages: Array<{ reasoning_content?: string }>;
|
|
1078
|
+
};
|
|
1079
|
+
const second = requests[1] as {
|
|
1080
|
+
messages: Array<{ reasoning_content?: string }>;
|
|
1081
|
+
};
|
|
1082
|
+
expect(first.messages[0].reasoning_content).toBeUndefined();
|
|
1083
|
+
expect(second.messages[0].reasoning_content).toBe("");
|
|
1084
|
+
const text = response.content.find((b) => b.type === "text") as
|
|
1085
|
+
| { type: "text"; text: string }
|
|
1086
|
+
| undefined;
|
|
1087
|
+
expect(text?.text).toBe("ok");
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("retries once for an OpenRouter-wrapped missing reasoning_content rejection", async () => {
|
|
1091
|
+
const wrapped = new OpenAI.APIError(
|
|
1092
|
+
400,
|
|
1093
|
+
{
|
|
1094
|
+
code: 400,
|
|
1095
|
+
message: "Provider returned error",
|
|
1096
|
+
metadata: {
|
|
1097
|
+
raw: "[invalid_request_error] The reasoning_content in the thinking mode must be passed back to the API",
|
|
1098
|
+
provider_name: "deepseek",
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
undefined,
|
|
1102
|
+
new Headers(),
|
|
1103
|
+
);
|
|
1104
|
+
expect(/reasoning_content/i.test(wrapped.message)).toBe(false);
|
|
1105
|
+
|
|
1106
|
+
const { provider, requests } = stubProviderWithErrors([wrapped], OK_CHUNKS, {
|
|
1107
|
+
assistantReasoningField: "reasoning_content",
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
await provider.sendMessage(toolCallHistory);
|
|
1111
|
+
|
|
1112
|
+
expect(requests).toHaveLength(2);
|
|
1113
|
+
expect(
|
|
1114
|
+
(requests[0] as { messages: Array<{ reasoning_content?: string }> })
|
|
1115
|
+
.messages[0].reasoning_content,
|
|
1116
|
+
).toBeUndefined();
|
|
1117
|
+
expect(
|
|
1118
|
+
(requests[1] as { messages: Array<{ reasoning_content?: string }> })
|
|
1119
|
+
.messages[0].reasoning_content,
|
|
1120
|
+
).toBe("");
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test("does not retry a 4xx that does not require reasoning_content round-trip", async () => {
|
|
1124
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1125
|
+
[rejection("invalid api key")],
|
|
1126
|
+
OK_CHUNKS,
|
|
1127
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
await expect(provider.sendMessage(toolCallHistory)).rejects.toThrow();
|
|
1131
|
+
expect(requests).toHaveLength(1);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test("does not retry missing reasoning_content 500s", async () => {
|
|
1135
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1136
|
+
[
|
|
1137
|
+
rejection(
|
|
1138
|
+
"The reasoning_content in the thinking mode must be passed back to the API",
|
|
1139
|
+
500,
|
|
1140
|
+
),
|
|
1141
|
+
],
|
|
1142
|
+
OK_CHUNKS,
|
|
1143
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
await expect(provider.sendMessage(toolCallHistory)).rejects.toThrow();
|
|
1147
|
+
expect(requests).toHaveLength(1);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
describe("unknown assistant reasoning field rejection fallback", () => {
|
|
1152
|
+
const thinkingHistory = [
|
|
1153
|
+
{
|
|
1154
|
+
role: "assistant" as const,
|
|
1155
|
+
content: [
|
|
1156
|
+
{ type: "thinking" as const, thinking: "hidden chain", signature: "" },
|
|
1157
|
+
{ type: "text" as const, text: "answer" },
|
|
1158
|
+
],
|
|
1159
|
+
},
|
|
1160
|
+
];
|
|
1161
|
+
|
|
1162
|
+
test("retries once without reasoning_content when a strict schema rejects it", async () => {
|
|
1163
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1164
|
+
[
|
|
1165
|
+
rejection(
|
|
1166
|
+
"Additional properties are not allowed ('reasoning_content' was unexpected)",
|
|
1167
|
+
),
|
|
1168
|
+
],
|
|
1169
|
+
OK_CHUNKS,
|
|
1170
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
const response = await provider.sendMessage(thinkingHistory);
|
|
1174
|
+
|
|
1175
|
+
expect(requests).toHaveLength(2);
|
|
1176
|
+
const first = requests[0] as {
|
|
1177
|
+
messages: Array<{ reasoning_content?: string; content: string | null }>;
|
|
1178
|
+
};
|
|
1179
|
+
const second = requests[1] as {
|
|
1180
|
+
messages: Array<{ reasoning_content?: string; content: string | null }>;
|
|
1181
|
+
};
|
|
1182
|
+
expect(first.messages[0].reasoning_content).toBe("hidden chain");
|
|
1183
|
+
expect(second.messages[0].reasoning_content).toBeUndefined();
|
|
1184
|
+
expect(second.messages[0].content).toBe("answer");
|
|
1185
|
+
const text = response.content.find((b) => b.type === "text") as
|
|
1186
|
+
| { type: "text"; text: string }
|
|
1187
|
+
| undefined;
|
|
1188
|
+
expect(text?.text).toBe("ok");
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
test("does not strip reasoning_content on a must-be-passed-back error", async () => {
|
|
1192
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1193
|
+
[
|
|
1194
|
+
rejection(
|
|
1195
|
+
"The reasoning_content in the thinking mode must be passed back to the API",
|
|
1196
|
+
),
|
|
1197
|
+
],
|
|
1198
|
+
OK_CHUNKS,
|
|
1199
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
|
|
1203
|
+
expect(requests).toHaveLength(1);
|
|
1204
|
+
expect(
|
|
1205
|
+
(requests[0] as { messages: Array<{ reasoning_content?: string }> })
|
|
1206
|
+
.messages[0].reasoning_content,
|
|
1207
|
+
).toBe("hidden chain");
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
test("does not retry unknown-field 500s", async () => {
|
|
1211
|
+
const { provider, requests } = stubProviderWithErrors(
|
|
1212
|
+
[
|
|
1213
|
+
rejection(
|
|
1214
|
+
"Additional properties are not allowed ('reasoning_content' was unexpected)",
|
|
1215
|
+
500,
|
|
1216
|
+
),
|
|
1217
|
+
],
|
|
1218
|
+
OK_CHUNKS,
|
|
1219
|
+
{ assistantReasoningField: "reasoning_content" },
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
await expect(provider.sendMessage(thinkingHistory)).rejects.toThrow();
|
|
1223
|
+
expect(requests).toHaveLength(1);
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
|
|
697
1227
|
describe("OpenAIChatCompletionsProvider cache usage parsing", () => {
|
|
698
1228
|
test("maps prompt_tokens_details cache fields into usage", async () => {
|
|
699
1229
|
// prompt_tokens is the inclusive total; the cached subset surfaces as
|