@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
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import type { Message, ToolDefinition } from "../../types.js";
|
|
4
4
|
import {
|
|
5
|
+
isThinkingEnabledOnWire,
|
|
5
6
|
mapNeutralToolChoice,
|
|
6
7
|
OpenAIChatCompletionsProvider,
|
|
7
8
|
} from "../chat-completions-provider.js";
|
|
@@ -23,11 +24,17 @@ const USER_MESSAGE: Message[] = [
|
|
|
23
24
|
* Stub the SDK client so we can capture the outgoing chat.completions.create
|
|
24
25
|
* params without hitting the network.
|
|
25
26
|
*/
|
|
26
|
-
function stubChatProvider(
|
|
27
|
+
function stubChatProvider(
|
|
28
|
+
options?: ConstructorParameters<typeof OpenAIChatCompletionsProvider>[2],
|
|
29
|
+
): {
|
|
27
30
|
provider: OpenAIChatCompletionsProvider;
|
|
28
31
|
requests: Array<Record<string, unknown>>;
|
|
29
32
|
} {
|
|
30
|
-
const provider = new OpenAIChatCompletionsProvider(
|
|
33
|
+
const provider = new OpenAIChatCompletionsProvider(
|
|
34
|
+
"test-key",
|
|
35
|
+
"test-model",
|
|
36
|
+
options,
|
|
37
|
+
);
|
|
31
38
|
const requests: Array<Record<string, unknown>> = [];
|
|
32
39
|
(provider as unknown as { client: unknown }).client = {
|
|
33
40
|
chat: {
|
|
@@ -49,6 +56,23 @@ function stubChatProvider(): {
|
|
|
49
56
|
return { provider, requests };
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
describe("isThinkingEnabledOnWire", () => {
|
|
60
|
+
test("detects flat reasoning_effort and nested reasoning flags", () => {
|
|
61
|
+
expect(isThinkingEnabledOnWire({})).toBe(false);
|
|
62
|
+
expect(isThinkingEnabledOnWire({ reasoning_effort: "none" })).toBe(false);
|
|
63
|
+
expect(isThinkingEnabledOnWire({ reasoning_effort: "high" })).toBe(true);
|
|
64
|
+
expect(isThinkingEnabledOnWire({ reasoning: { effort: "none" } })).toBe(
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
expect(isThinkingEnabledOnWire({ reasoning: { effort: "medium" } })).toBe(
|
|
68
|
+
true,
|
|
69
|
+
);
|
|
70
|
+
expect(isThinkingEnabledOnWire({ reasoning: { enabled: true } })).toBe(
|
|
71
|
+
true,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
52
76
|
describe("mapNeutralToolChoice (chat-completions wire format)", () => {
|
|
53
77
|
// Each neutral tool_choice variant maps to its OpenAI-compatible form.
|
|
54
78
|
test("maps the Anthropic-shaped union to OpenAI's tool_choice values", () => {
|
|
@@ -144,4 +168,103 @@ describe("OpenAIChatCompletionsProvider tool_choice wiring", () => {
|
|
|
144
168
|
expect(requests[0].tools).toBeUndefined();
|
|
145
169
|
expect(requests[0].tool_choice).toBeUndefined();
|
|
146
170
|
});
|
|
171
|
+
|
|
172
|
+
// `"auto"` is the chat-completions default when tools are present. Sending it
|
|
173
|
+
// alongside reasoning_effort 400s DeepSeek thinking mode, so it is omitted.
|
|
174
|
+
test("omits redundant tool_choice auto when thinking is on the wire", async () => {
|
|
175
|
+
const { provider, requests } = stubChatProvider();
|
|
176
|
+
|
|
177
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
178
|
+
tools: TOOLS,
|
|
179
|
+
config: { tool_choice: { type: "auto" }, effort: "high" },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(requests[0].reasoning_effort).toBe("high");
|
|
183
|
+
expect(requests[0].tool_choice).toBeUndefined();
|
|
184
|
+
expect(requests[0].tools).toBeDefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("omits redundant tool_choice auto when nested reasoning.enabled is set", async () => {
|
|
188
|
+
const { provider, requests } = stubChatProvider({
|
|
189
|
+
extraCreateParams: { reasoning: { enabled: true } },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
193
|
+
tools: TOOLS,
|
|
194
|
+
config: { tool_choice: { type: "auto" } },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(requests[0].tool_choice).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("forwards tool_choice auto when thinking is off", async () => {
|
|
201
|
+
const { provider, requests } = stubChatProvider();
|
|
202
|
+
|
|
203
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
204
|
+
tools: TOOLS,
|
|
205
|
+
config: { tool_choice: { type: "auto" } },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(requests[0].tool_choice).toBe("auto");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Catalog providers (Fireworks, Together) honor `none` with thinking.
|
|
212
|
+
test("forwards tool_choice none with thinking when omitToolChoiceWhenReasoning is off", async () => {
|
|
213
|
+
const { provider, requests } = stubChatProvider();
|
|
214
|
+
|
|
215
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
216
|
+
tools: TOOLS,
|
|
217
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(requests[0].reasoning_effort).toBe("high");
|
|
221
|
+
expect(requests[0].tool_choice).toBe("none");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("forwards a forced tool_choice with thinking when omitToolChoiceWhenReasoning is off", async () => {
|
|
225
|
+
const { provider, requests } = stubChatProvider();
|
|
226
|
+
|
|
227
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
228
|
+
tools: TOOLS,
|
|
229
|
+
config: { tool_choice: { type: "tool", name: "bash" }, effort: "high" },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(requests[0].tool_choice).toEqual({
|
|
233
|
+
type: "function",
|
|
234
|
+
function: { name: "bash" },
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("omits every tool_choice in thinking mode when omitToolChoiceWhenReasoning is on", async () => {
|
|
239
|
+
const { provider, requests } = stubChatProvider({
|
|
240
|
+
omitToolChoiceWhenReasoning: true,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
244
|
+
tools: TOOLS,
|
|
245
|
+
config: { tool_choice: { type: "none" }, effort: "high" },
|
|
246
|
+
});
|
|
247
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
248
|
+
tools: TOOLS,
|
|
249
|
+
config: { tool_choice: { type: "tool", name: "bash" }, effort: "high" },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(requests[0].reasoning_effort).toBe("high");
|
|
253
|
+
expect(requests[0].tool_choice).toBeUndefined();
|
|
254
|
+
expect(requests[1].tool_choice).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("still forwards tool_choice when omitToolChoiceWhenReasoning is on but thinking is off", async () => {
|
|
258
|
+
const { provider, requests } = stubChatProvider({
|
|
259
|
+
omitToolChoiceWhenReasoning: true,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await provider.sendMessage(USER_MESSAGE, {
|
|
263
|
+
tools: TOOLS,
|
|
264
|
+
config: { tool_choice: { type: "none" } },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(requests[0].reasoning_effort).toBeUndefined();
|
|
268
|
+
expect(requests[0].tool_choice).toBe("none");
|
|
269
|
+
});
|
|
147
270
|
});
|
|
@@ -162,14 +162,19 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
162
162
|
parseThinkTags?: boolean;
|
|
163
163
|
/** Wire field used to replay prior assistant thinking on multi-turn requests.
|
|
164
164
|
* DeepSeek/Fireworks use `"reasoning_content"`; OpenRouter uses `"reasoning"`.
|
|
165
|
-
* When unset, thinking blocks are dropped from outbound assistant messages.
|
|
165
|
+
* When unset, thinking blocks are dropped from outbound assistant messages.
|
|
166
|
+
* When set, the field is included only if there is thinking to replay, so a
|
|
167
|
+
* standard Chat Completions endpoint does not see an extra key on ordinary
|
|
168
|
+
* tool-call turns. DeepSeek thinking mode that requires the field even when
|
|
169
|
+
* empty is handled by a one-shot retry. */
|
|
166
170
|
assistantReasoningField?: "reasoning" | "reasoning_content";
|
|
167
171
|
/** Backfill a non-empty placeholder for assistant turns that would otherwise
|
|
168
172
|
* serialize with neither `content` nor `tool_calls` (e.g. reasoning-only
|
|
169
|
-
* turns). Off by default; enabled for
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
+
* turns, or a Stop mid-stream before any text). Off by default; enabled for
|
|
174
|
+
* OpenRouter, Vercel AI Gateway, LiteLLM, and custom `openai-compatible`
|
|
175
|
+
* endpoints, whose downstream providers (e.g. DeepSeek, vLLM, Portkey)
|
|
176
|
+
* reject such messages with `Invalid assistant message: content or
|
|
177
|
+
* tool_calls must be set`. See {@link EMPTY_ASSISTANT_TURN_PLACEHOLDER}. */
|
|
173
178
|
backfillEmptyAssistantContent?: boolean;
|
|
174
179
|
/** Present object-typed tool params to the model as JSON-string params and
|
|
175
180
|
* decode them back to objects on the response. Works around models whose
|
|
@@ -177,6 +182,13 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
177
182
|
* with minimax-m3 on Fireworks). Off by default; scalars/arrays unaffected.
|
|
178
183
|
* See {@link coerceObjectParamsToJsonString}. */
|
|
179
184
|
coerceObjectArgsToJsonString?: boolean;
|
|
185
|
+
/** Drop `tool_choice` when thinking/reasoning is on the wire. Strict
|
|
186
|
+
* OpenAI-compatible reasoning upstreams (DeepSeek thinking mode) reject any
|
|
187
|
+
* explicit `tool_choice` with `Thinking mode does not support this
|
|
188
|
+
* tool_choice`. Off by default so catalog providers that honor the combo
|
|
189
|
+
* (Fireworks, Together) keep sending `none` / forced choices. Enabled for
|
|
190
|
+
* the generic `openai-compatible` adapter, whose upstream is unknown. */
|
|
191
|
+
omitToolChoiceWhenReasoning?: boolean;
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
const log = getLogger("chat-completions");
|
|
@@ -219,12 +231,29 @@ export function clampReasoningEffort(
|
|
|
219
231
|
: value;
|
|
220
232
|
}
|
|
221
233
|
|
|
234
|
+
/** Human-readable text from an OpenAI-compatible error, including wrapped
|
|
235
|
+
* upstream detail (OpenRouter `metadata.raw`). Used by the one-shot
|
|
236
|
+
* compatibility retries so a generic SDK wrapper message cannot hide the
|
|
237
|
+
* real reason. */
|
|
238
|
+
function openaiCompatErrorHaystack(error: unknown): string {
|
|
239
|
+
return error instanceof OpenAI.APIError
|
|
240
|
+
? normalizedErrorText(normalizeOpenAIAPIError(error))
|
|
241
|
+
: error instanceof Error
|
|
242
|
+
? error.message
|
|
243
|
+
: String(error);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isClientErrorStatus(error: unknown): boolean {
|
|
247
|
+
const status = (error as { status?: unknown }).status;
|
|
248
|
+
return typeof status === "number" && status >= 400 && status < 500;
|
|
249
|
+
}
|
|
250
|
+
|
|
222
251
|
/**
|
|
223
252
|
* True when the request carried an explicit reasoning opt-out (`"none"` sent
|
|
224
253
|
* as flat `reasoning_effort` or nested `reasoning.effort`) and the provider
|
|
225
254
|
* rejected it with a 4xx that names the reasoning field. Reasoning-only
|
|
226
255
|
* models (e.g. DeepSeek R1) reject the opt-out rather than ignore it; that
|
|
227
|
-
* one case is worth a single retry with the reasoning params stripped
|
|
256
|
+
* one case is worth a single retry with the reasoning params stripped:
|
|
228
257
|
* model-default reasoning beats a hard failure.
|
|
229
258
|
*/
|
|
230
259
|
function isReasoningOptOutRejection(error: unknown, params: unknown): boolean {
|
|
@@ -240,22 +269,217 @@ function isReasoningOptOutRejection(error: unknown, params: unknown): boolean {
|
|
|
240
269
|
if (!optedOut) {
|
|
241
270
|
return false;
|
|
242
271
|
}
|
|
243
|
-
|
|
244
|
-
|
|
272
|
+
if (!isClientErrorStatus(error)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
return /reasoning/i.test(openaiCompatErrorHaystack(error));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* True when thinking/reasoning is active on the outbound chat-completions
|
|
280
|
+
* body: a non-`"none"` `reasoning_effort`, a nested `reasoning.effort`, or
|
|
281
|
+
* `reasoning.enabled: true` (OpenRouter / Vercel AI Gateway).
|
|
282
|
+
*/
|
|
283
|
+
export function isThinkingEnabledOnWire(params: unknown): boolean {
|
|
284
|
+
const p = params as {
|
|
285
|
+
reasoning_effort?: unknown;
|
|
286
|
+
reasoning?: { effort?: unknown; enabled?: unknown } | null;
|
|
287
|
+
};
|
|
288
|
+
const nested = p.reasoning;
|
|
289
|
+
if (nested && typeof nested === "object") {
|
|
290
|
+
if (nested.enabled === true) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (typeof nested.effort === "string" && nested.effort !== "none") {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return (
|
|
298
|
+
typeof p.reasoning_effort === "string" && p.reasoning_effort !== "none"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* True when the request sent an explicit `tool_choice` and the provider
|
|
304
|
+
* rejected it because thinking/reasoning mode forbids that parameter.
|
|
305
|
+
* DeepSeek thinking mode 400s with `Thinking mode does not support this
|
|
306
|
+
* tool_choice` for any explicit value, including `"auto"` and `"none"`.
|
|
307
|
+
* One retry without `tool_choice` lets the same provider succeed instead of
|
|
308
|
+
* failing over to a different backend.
|
|
309
|
+
*/
|
|
310
|
+
function isThinkingModeToolChoiceRejection(
|
|
311
|
+
error: unknown,
|
|
312
|
+
params: unknown,
|
|
313
|
+
): boolean {
|
|
314
|
+
const p = params as { tool_choice?: unknown };
|
|
315
|
+
if (p.tool_choice === undefined) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (!isClientErrorStatus(error)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
return /does not support this tool_choice/i.test(
|
|
322
|
+
openaiCompatErrorHaystack(error),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
type AssistantReasoningExtras = {
|
|
327
|
+
reasoning?: string;
|
|
328
|
+
reasoning_content?: string;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function assistantReasoningExtras(
|
|
332
|
+
msg: OpenAI.Chat.Completions.ChatCompletionMessageParam,
|
|
333
|
+
): AssistantReasoningExtras | null {
|
|
334
|
+
if (msg.role !== "assistant") {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return msg as OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam &
|
|
338
|
+
AssistantReasoningExtras;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function paramsMessages(
|
|
342
|
+
params: unknown,
|
|
343
|
+
): OpenAI.Chat.Completions.ChatCompletionMessageParam[] | undefined {
|
|
344
|
+
const messages = (params as { messages?: unknown }).messages;
|
|
345
|
+
return Array.isArray(messages)
|
|
346
|
+
? (messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[])
|
|
347
|
+
: undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function messagesCarryAssistantReasoningField(params: unknown): boolean {
|
|
351
|
+
const messages = paramsMessages(params);
|
|
352
|
+
if (!messages) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return messages.some((msg) => {
|
|
356
|
+
const extra = assistantReasoningExtras(msg);
|
|
357
|
+
return (
|
|
358
|
+
extra !== null &&
|
|
359
|
+
(extra.reasoning !== undefined || extra.reasoning_content !== undefined)
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function assistantMessagesNeedReasoningContentBackfill(
|
|
365
|
+
params: unknown,
|
|
366
|
+
): boolean {
|
|
367
|
+
const messages = paramsMessages(params);
|
|
368
|
+
if (!messages) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
return messages.some((msg) => {
|
|
372
|
+
const extra = assistantReasoningExtras(msg);
|
|
373
|
+
return (
|
|
374
|
+
extra !== null &&
|
|
375
|
+
extra.reasoning_content === undefined &&
|
|
376
|
+
extra.reasoning === undefined
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function stripAssistantReasoningFields(params: unknown): boolean {
|
|
382
|
+
const messages = paramsMessages(params);
|
|
383
|
+
if (!messages) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
let stripped = false;
|
|
387
|
+
for (const msg of messages) {
|
|
388
|
+
const extra = assistantReasoningExtras(msg);
|
|
389
|
+
if (extra === null) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (extra.reasoning_content !== undefined) {
|
|
393
|
+
delete extra.reasoning_content;
|
|
394
|
+
stripped = true;
|
|
395
|
+
}
|
|
396
|
+
if (extra.reasoning !== undefined) {
|
|
397
|
+
delete extra.reasoning;
|
|
398
|
+
stripped = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return stripped;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function backfillEmptyReasoningContent(params: unknown): boolean {
|
|
405
|
+
const messages = paramsMessages(params);
|
|
406
|
+
if (!messages) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
let added = false;
|
|
410
|
+
for (const msg of messages) {
|
|
411
|
+
const extra = assistantReasoningExtras(msg);
|
|
412
|
+
if (extra === null) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
extra.reasoning_content !== undefined ||
|
|
417
|
+
extra.reasoning !== undefined
|
|
418
|
+
) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
extra.reasoning_content = "";
|
|
422
|
+
added = true;
|
|
423
|
+
}
|
|
424
|
+
return added;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function haystackNamesAssistantReasoningField(haystack: string): boolean {
|
|
428
|
+
if (/reasoning_content/i.test(haystack)) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return /\breasoning\b/i.test(haystack) && !/reasoning_effort/i.test(haystack);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* True when thinking-mode requires `reasoning_content` on subsequent
|
|
436
|
+
* assistant messages and this request omitted it. DeepSeek 400s with
|
|
437
|
+
* `The reasoning_content in the thinking mode must be passed back to the API`.
|
|
438
|
+
* One retry with an empty string on those assistant messages satisfies the
|
|
439
|
+
* presence check without putting the extra key on every custom-endpoint turn.
|
|
440
|
+
*/
|
|
441
|
+
function isMissingReasoningContentRejection(
|
|
442
|
+
error: unknown,
|
|
443
|
+
params: unknown,
|
|
444
|
+
): boolean {
|
|
445
|
+
if (!isClientErrorStatus(error)) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const haystack = openaiCompatErrorHaystack(error);
|
|
449
|
+
if (
|
|
450
|
+
!/reasoning_content/i.test(haystack) ||
|
|
451
|
+
!/must be passed back/i.test(haystack)
|
|
452
|
+
) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
return assistantMessagesNeedReasoningContentBackfill(params);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* True when the request included an assistant `reasoning` / `reasoning_content`
|
|
460
|
+
* extra and the provider rejected it as an unknown message property. One retry
|
|
461
|
+
* without those extras lets a strict Chat Completions schema succeed.
|
|
462
|
+
*/
|
|
463
|
+
function isUnknownAssistantReasoningFieldRejection(
|
|
464
|
+
error: unknown,
|
|
465
|
+
params: unknown,
|
|
466
|
+
): boolean {
|
|
467
|
+
if (!isClientErrorStatus(error)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
if (!messagesCarryAssistantReasoningField(params)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const haystack = openaiCompatErrorHaystack(error);
|
|
474
|
+
if (/must be passed back/i.test(haystack)) {
|
|
245
475
|
return false;
|
|
246
476
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
error instanceof OpenAI.APIError
|
|
254
|
-
? normalizedErrorText(normalizeOpenAIAPIError(error))
|
|
255
|
-
: error instanceof Error
|
|
256
|
-
? error.message
|
|
257
|
-
: String(error);
|
|
258
|
-
return /reasoning/i.test(haystack);
|
|
477
|
+
if (!haystackNamesAssistantReasoningField(haystack)) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
return /unknown|unexpected|unrecognized|additional propert|extra (?:field|property)|not (?:a )?valid|invalid (?:argument|parameter|field|property)/i.test(
|
|
481
|
+
haystack,
|
|
482
|
+
);
|
|
259
483
|
}
|
|
260
484
|
|
|
261
485
|
/**
|
|
@@ -334,6 +558,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
334
558
|
| undefined;
|
|
335
559
|
private backfillEmptyAssistantContent: boolean;
|
|
336
560
|
private coerceObjectArgsToJsonString: boolean;
|
|
561
|
+
private omitToolChoiceWhenReasoning: boolean;
|
|
337
562
|
|
|
338
563
|
constructor(
|
|
339
564
|
apiKey: string,
|
|
@@ -363,6 +588,8 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
363
588
|
options.backfillEmptyAssistantContent ?? false;
|
|
364
589
|
this.coerceObjectArgsToJsonString =
|
|
365
590
|
options.coerceObjectArgsToJsonString ?? false;
|
|
591
|
+
this.omitToolChoiceWhenReasoning =
|
|
592
|
+
options.omitToolChoiceWhenReasoning ?? false;
|
|
366
593
|
}
|
|
367
594
|
|
|
368
595
|
get defaultModel(): string {
|
|
@@ -469,11 +696,24 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
469
696
|
|
|
470
697
|
// Honor a caller-supplied tool_choice (e.g. `{ type: "none" }` to force
|
|
471
698
|
// a text-only answer, or `{ type: "tool", name }` for a forced call).
|
|
472
|
-
// Only meaningful when tools are present
|
|
699
|
+
// Only meaningful when tools are present: OpenAI rejects a named or
|
|
473
700
|
// "required" choice with no tools.
|
|
701
|
+
//
|
|
702
|
+
// Strict reasoning upstreams (DeepSeek thinking mode) reject any
|
|
703
|
+
// explicit tool_choice, including `"auto"` (the API default when tools
|
|
704
|
+
// are present) and `"none"`. Omit `"auto"` whenever thinking is on the
|
|
705
|
+
// wire. Catalog providers that honor `none` / forced choices still
|
|
706
|
+
// receive them; the generic openai-compatible adapter drops every
|
|
707
|
+
// explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
|
|
474
708
|
const toolChoice = mapNeutralToolChoice(configObj?.tool_choice);
|
|
475
709
|
if (toolChoice !== undefined) {
|
|
476
|
-
|
|
710
|
+
const thinkingOn = isThinkingEnabledOnWire(params);
|
|
711
|
+
const skipAutoDefault = thinkingOn && toolChoice === "auto";
|
|
712
|
+
const skipAllChoices =
|
|
713
|
+
thinkingOn && this.omitToolChoiceWhenReasoning;
|
|
714
|
+
if (!skipAutoDefault && !skipAllChoices) {
|
|
715
|
+
params.tool_choice = toolChoice;
|
|
716
|
+
}
|
|
477
717
|
}
|
|
478
718
|
}
|
|
479
719
|
|
|
@@ -573,20 +813,54 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
573
813
|
try {
|
|
574
814
|
stream = await createStream();
|
|
575
815
|
} catch (error) {
|
|
576
|
-
if (
|
|
816
|
+
if (isReasoningOptOutRejection(error, params)) {
|
|
817
|
+
log.warn(
|
|
818
|
+
{
|
|
819
|
+
provider: this.name,
|
|
820
|
+
model: modelOverride ?? this.model,
|
|
821
|
+
error: error instanceof Error ? error.message : String(error),
|
|
822
|
+
},
|
|
823
|
+
"Model rejected the explicit reasoning opt-out; retrying without reasoning params",
|
|
824
|
+
);
|
|
825
|
+
delete params.reasoning_effort;
|
|
826
|
+
delete (params as unknown as Record<string, unknown>).reasoning;
|
|
827
|
+
stream = await createStream();
|
|
828
|
+
} else if (isThinkingModeToolChoiceRejection(error, params)) {
|
|
829
|
+
log.warn(
|
|
830
|
+
{
|
|
831
|
+
provider: this.name,
|
|
832
|
+
model: modelOverride ?? this.model,
|
|
833
|
+
error: error instanceof Error ? error.message : String(error),
|
|
834
|
+
},
|
|
835
|
+
"Upstream rejected tool_choice in thinking mode; retrying without tool_choice",
|
|
836
|
+
);
|
|
837
|
+
delete params.tool_choice;
|
|
838
|
+
stream = await createStream();
|
|
839
|
+
} else if (isMissingReasoningContentRejection(error, params)) {
|
|
840
|
+
log.warn(
|
|
841
|
+
{
|
|
842
|
+
provider: this.name,
|
|
843
|
+
model: modelOverride ?? this.model,
|
|
844
|
+
error: error instanceof Error ? error.message : String(error),
|
|
845
|
+
},
|
|
846
|
+
"Upstream requires reasoning_content round-trip; retrying with empty field on assistant messages",
|
|
847
|
+
);
|
|
848
|
+
backfillEmptyReasoningContent(params);
|
|
849
|
+
stream = await createStream();
|
|
850
|
+
} else if (isUnknownAssistantReasoningFieldRejection(error, params)) {
|
|
851
|
+
log.warn(
|
|
852
|
+
{
|
|
853
|
+
provider: this.name,
|
|
854
|
+
model: modelOverride ?? this.model,
|
|
855
|
+
error: error instanceof Error ? error.message : String(error),
|
|
856
|
+
},
|
|
857
|
+
"Upstream rejected assistant reasoning field; retrying without it",
|
|
858
|
+
);
|
|
859
|
+
stripAssistantReasoningFields(params);
|
|
860
|
+
stream = await createStream();
|
|
861
|
+
} else {
|
|
577
862
|
throw error;
|
|
578
863
|
}
|
|
579
|
-
log.warn(
|
|
580
|
-
{
|
|
581
|
-
provider: this.name,
|
|
582
|
-
model: modelOverride ?? this.model,
|
|
583
|
-
error: error instanceof Error ? error.message : String(error),
|
|
584
|
-
},
|
|
585
|
-
"Model rejected the explicit reasoning opt-out; retrying without reasoning params",
|
|
586
|
-
);
|
|
587
|
-
delete params.reasoning_effort;
|
|
588
|
-
delete (params as unknown as Record<string, unknown>).reasoning;
|
|
589
|
-
stream = await createStream();
|
|
590
864
|
}
|
|
591
865
|
|
|
592
866
|
for await (const chunk of stream) {
|
|
@@ -1095,6 +1369,14 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1095
1369
|
content: textParts.length > 0 ? textParts.join("") : null,
|
|
1096
1370
|
};
|
|
1097
1371
|
|
|
1372
|
+
if (toolCalls.length > 0) {
|
|
1373
|
+
result.tool_calls = toolCalls;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Include the configured wire field only when there is thinking to replay.
|
|
1377
|
+
// Ordinary tool-call turns omit it so a strict Chat Completions schema
|
|
1378
|
+
// does not reject an extra key. Empty-field presence for DeepSeek is a
|
|
1379
|
+
// one-shot retry, not the default serialization.
|
|
1098
1380
|
if (reasoningParts.length > 0 && this.assistantReasoningField) {
|
|
1099
1381
|
(
|
|
1100
1382
|
result as OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam & {
|
|
@@ -1104,15 +1386,13 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1104
1386
|
)[this.assistantReasoningField] = reasoningParts.join("");
|
|
1105
1387
|
}
|
|
1106
1388
|
|
|
1107
|
-
if (toolCalls.length > 0) {
|
|
1108
|
-
result.tool_calls = toolCalls;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
1389
|
// An assistant message must carry `content` or `tool_calls`. A turn with
|
|
1112
|
-
// neither (e.g. reasoning-only
|
|
1113
|
-
// no tool calls, which strict OpenAI-compatible
|
|
1114
|
-
// lives in a separate field and does not
|
|
1115
|
-
// providers that need it (OpenRouter
|
|
1390
|
+
// neither (e.g. reasoning-only, or a Stop before any text) would serialize
|
|
1391
|
+
// to null/empty content with no tool calls, which strict OpenAI-compatible
|
|
1392
|
+
// backends reject. Reasoning lives in a separate field and does not
|
|
1393
|
+
// satisfy this constraint. Scoped to providers that need it (OpenRouter,
|
|
1394
|
+
// Vercel AI Gateway, LiteLLM, openai-compatible) via
|
|
1395
|
+
// `backfillEmptyAssistantContent`.
|
|
1116
1396
|
if (
|
|
1117
1397
|
this.backfillEmptyAssistantContent &&
|
|
1118
1398
|
!result.tool_calls &&
|
|
@@ -33,6 +33,13 @@ export interface RouteInvokeParams {
|
|
|
33
33
|
readonly url: string;
|
|
34
34
|
/** Request header entries as `[name, value]` pairs (preserves duplicates). */
|
|
35
35
|
readonly headers: ReadonlyArray<readonly [string, string]>;
|
|
36
|
+
/**
|
|
37
|
+
* Manifest name of the plugin whose namespace the route falls in, absent for
|
|
38
|
+
* a workspace route. The host marks it as the plugin in context for the
|
|
39
|
+
* handler's execution, so plugin-scoped host APIs behave the same whether the
|
|
40
|
+
* handler runs in the host or in-thread on the daemon.
|
|
41
|
+
*/
|
|
42
|
+
readonly pluginName?: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
package/src/routes/worker.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { createServer, type Server, type Socket } from "node:net";
|
|
|
24
24
|
import type { IpcEnvelope } from "@vellumai/ipc-server-utils";
|
|
25
25
|
import { IpcFrameReader, writeMessage } from "@vellumai/ipc-server-utils";
|
|
26
26
|
|
|
27
|
+
import { runInPluginContext } from "../plugins/plugin-execution-context.js";
|
|
27
28
|
import { disableStreamSeqStamping } from "../runtime/assistant-stream-state.js";
|
|
28
29
|
import {
|
|
29
30
|
evictRouteSourceTree,
|
|
@@ -126,26 +127,46 @@ async function handleInvoke(
|
|
|
126
127
|
evictRouteSourceTree(sourceRootForHandler(params.filePath));
|
|
127
128
|
lastHandlerMtime.set(params.filePath, params.mtimeMs);
|
|
128
129
|
}
|
|
129
|
-
const mod = await importRouteModule(params.filePath);
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// A plugin's own routes execute as that plugin, matching the daemon's
|
|
132
|
+
// in-thread path, so plugin-scoped host APIs a handler reaches
|
|
133
|
+
// (`resolveCredential`, `indexDocument`) scope to the owning plugin rather
|
|
134
|
+
// than falling through to their unscoped branch. The context covers the
|
|
135
|
+
// import as well as the call: a route module can reach a scoped API at
|
|
136
|
+
// evaluation time, and it is imported once per mtime.
|
|
137
|
+
const serve = async (): Promise<
|
|
138
|
+
{ ok: true; response: Response } | { ok: false; allowed: string[] }
|
|
139
|
+
> => {
|
|
140
|
+
const mod = await importRouteModule(params.filePath);
|
|
141
|
+
const handler = mod[params.method];
|
|
142
|
+
if (typeof handler !== "function") {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
allowed: HTTP_METHODS.filter((m) => typeof mod[m] === "function"),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const request = reconstructRequest(params, body);
|
|
149
|
+
const response = (await (handler as (req: Request) => unknown)(
|
|
150
|
+
request,
|
|
151
|
+
)) as Response;
|
|
152
|
+
return { ok: true, response };
|
|
153
|
+
};
|
|
154
|
+
const outcome = await (params.pluginName
|
|
155
|
+
? runInPluginContext(params.pluginName, serve)
|
|
156
|
+
: serve());
|
|
157
|
+
|
|
158
|
+
if (!outcome.ok) {
|
|
134
159
|
replyResult(
|
|
135
160
|
socket,
|
|
136
161
|
id,
|
|
137
162
|
405,
|
|
138
|
-
allowed.length ? [["allow", allowed.join(", ")]] : [],
|
|
163
|
+
outcome.allowed.length ? [["allow", outcome.allowed.join(", ")]] : [],
|
|
139
164
|
null,
|
|
140
165
|
);
|
|
141
166
|
return;
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
const
|
|
145
|
-
const response = (await (handler as (req: Request) => unknown)(
|
|
146
|
-
request,
|
|
147
|
-
)) as Response;
|
|
148
|
-
|
|
169
|
+
const { response } = outcome;
|
|
149
170
|
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
150
171
|
const headers: [string, string][] = [];
|
|
151
172
|
response.headers.forEach((value, name) => {
|