@vellumai/assistant 0.8.3 → 0.8.4
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/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -537,6 +537,55 @@ function formatOrphanedWebSearchResultAsText(block: {
|
|
|
537
537
|
*
|
|
538
538
|
* Builds a fresh result array without mutating the input.
|
|
539
539
|
*/
|
|
540
|
+
/**
|
|
541
|
+
* Find the start index of the active tool-use continuation span at the tail
|
|
542
|
+
* of the formatted message array. Messages from this index onward may contain
|
|
543
|
+
* thinking blocks that must be preserved for Anthropic's tool-use protocol.
|
|
544
|
+
*
|
|
545
|
+
* The active span is the trailing sequence of alternating
|
|
546
|
+
* assistant(tool_use) → user(tool_result) messages. Everything before it is
|
|
547
|
+
* a completed historical turn whose thinking blocks can be safely stripped.
|
|
548
|
+
*
|
|
549
|
+
* Returns `messages.length` when there is no active tool-use continuation
|
|
550
|
+
* (i.e. all messages are historical — strip thinking from everything).
|
|
551
|
+
*/
|
|
552
|
+
function findActiveToolUseContinuationStart(
|
|
553
|
+
messages: Anthropic.MessageParam[],
|
|
554
|
+
): number {
|
|
555
|
+
// Walk backwards from the end. The tail pattern we're looking for is:
|
|
556
|
+
// ... assistant(tool_use) user(tool_result) [assistant(tool_use) user(tool_result)]* ...
|
|
557
|
+
// The last message is typically a user message (the new prompt), so if it
|
|
558
|
+
// doesn't contain tool_result blocks, there's no active continuation.
|
|
559
|
+
let i = messages.length - 1;
|
|
560
|
+
|
|
561
|
+
while (i >= 0) {
|
|
562
|
+
const msg = messages[i];
|
|
563
|
+
if (msg.role === "user") {
|
|
564
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
565
|
+
const hasToolResult = content.some(
|
|
566
|
+
(b) => typeof b !== "string" && isToolResultBlock(b),
|
|
567
|
+
);
|
|
568
|
+
if (!hasToolResult) break;
|
|
569
|
+
// This user message has tool_result — the preceding assistant message
|
|
570
|
+
// should have the matching tool_use and its thinking blocks preserved.
|
|
571
|
+
i--;
|
|
572
|
+
} else if (msg.role === "assistant") {
|
|
573
|
+
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
574
|
+
const hasToolUse = content.some(
|
|
575
|
+
(b) => typeof b !== "string" && isToolUseBlock(b),
|
|
576
|
+
);
|
|
577
|
+
if (!hasToolUse) break;
|
|
578
|
+
// This assistant message has tool_use — it's part of the active span.
|
|
579
|
+
// Check if the preceding user message continues the chain.
|
|
580
|
+
i--;
|
|
581
|
+
} else {
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return i + 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
540
589
|
function ensureToolPairing(
|
|
541
590
|
messages: Anthropic.MessageParam[],
|
|
542
591
|
): Anthropic.MessageParam[] {
|
|
@@ -925,10 +974,32 @@ export class AnthropicProvider implements Provider {
|
|
|
925
974
|
}
|
|
926
975
|
}
|
|
927
976
|
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
977
|
+
// Strip thinking/redacted_thinking blocks from completed historical
|
|
978
|
+
// assistant turns. Anthropic only requires these blocks for active
|
|
979
|
+
// tool-use continuation (the tail span where assistant tool_use is
|
|
980
|
+
// followed by user tool_result). Replaying stale thinking blocks from
|
|
981
|
+
// earlier turns causes 400 errors when the signature is no longer
|
|
982
|
+
// valid (e.g. after a provider/model/profile switch).
|
|
983
|
+
const activeToolUseStart =
|
|
984
|
+
findActiveToolUseContinuationStart(formatted);
|
|
985
|
+
for (let i = 0; i < activeToolUseStart; i++) {
|
|
986
|
+
const msg = formatted[i];
|
|
987
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
988
|
+
const stripped = (
|
|
989
|
+
msg.content as Anthropic.ContentBlockParam[]
|
|
990
|
+
).filter(
|
|
991
|
+
(b) =>
|
|
992
|
+
typeof b === "string" ||
|
|
993
|
+
(b.type !== "thinking" && b.type !== "redacted_thinking"),
|
|
994
|
+
);
|
|
995
|
+
if (stripped.length === 0) {
|
|
996
|
+
stripped.push({
|
|
997
|
+
type: "text" as const,
|
|
998
|
+
text: PLACEHOLDER_BLOCKS_OMITTED,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
formatted[i] = { ...msg, content: stripped };
|
|
1002
|
+
}
|
|
932
1003
|
|
|
933
1004
|
sentMessages = ensureToolPairing(
|
|
934
1005
|
repairOrphanedServerToolBlocks(formatted),
|
|
@@ -1274,6 +1345,19 @@ export class AnthropicProvider implements Provider {
|
|
|
1274
1345
|
let lastInputJsonEmitMs = 0;
|
|
1275
1346
|
let pendingInputJsonFlush: ReturnType<typeof setTimeout> | undefined;
|
|
1276
1347
|
|
|
1348
|
+
// Anthropic streams `server_tool_use` block input via `input_json_delta`
|
|
1349
|
+
// events (the block's own `input` field is `{}` at content_block_start).
|
|
1350
|
+
// We accumulate the JSON separately from regular `tool_use` blocks so
|
|
1351
|
+
// the daemon can read the resolved query when the paired
|
|
1352
|
+
// `web_search_tool_result` arrives — without this, downstream activity
|
|
1353
|
+
// metadata sees an empty query.
|
|
1354
|
+
let currentServerToolUseId: string | undefined;
|
|
1355
|
+
let accumulatedServerToolInputJson = "";
|
|
1356
|
+
const resolvedServerToolInputs = new Map<
|
|
1357
|
+
string,
|
|
1358
|
+
Record<string, unknown>
|
|
1359
|
+
>();
|
|
1360
|
+
|
|
1277
1361
|
stream.on("streamEvent", (event) => {
|
|
1278
1362
|
// Reset the text sentinel buffer at each content-block boundary.
|
|
1279
1363
|
// A new block starts fresh; at the end of a block, flush any
|
|
@@ -1300,6 +1384,8 @@ export class AnthropicProvider implements Provider {
|
|
|
1300
1384
|
event.type === "content_block_start" &&
|
|
1301
1385
|
event.content_block.type === "server_tool_use"
|
|
1302
1386
|
) {
|
|
1387
|
+
currentServerToolUseId = event.content_block.id;
|
|
1388
|
+
accumulatedServerToolInputJson = "";
|
|
1303
1389
|
onEvent?.({
|
|
1304
1390
|
type: "server_tool_start",
|
|
1305
1391
|
name: event.content_block.name,
|
|
@@ -1315,11 +1401,21 @@ export class AnthropicProvider implements Provider {
|
|
|
1315
1401
|
) {
|
|
1316
1402
|
const block = event.content_block as {
|
|
1317
1403
|
tool_use_id: string;
|
|
1318
|
-
content?:
|
|
1404
|
+
content?:
|
|
1405
|
+
| { type: "web_search_tool_result_error"; error_code?: string }
|
|
1406
|
+
| unknown[];
|
|
1319
1407
|
};
|
|
1320
1408
|
const isError =
|
|
1321
1409
|
!Array.isArray(block.content) &&
|
|
1322
1410
|
block.content?.type === "web_search_tool_result_error";
|
|
1411
|
+
const errorCode =
|
|
1412
|
+
isError && !Array.isArray(block.content)
|
|
1413
|
+
? block.content?.error_code
|
|
1414
|
+
: undefined;
|
|
1415
|
+
const resolvedInput = resolvedServerToolInputs.get(
|
|
1416
|
+
block.tool_use_id,
|
|
1417
|
+
);
|
|
1418
|
+
resolvedServerToolInputs.delete(block.tool_use_id);
|
|
1323
1419
|
onEvent?.({
|
|
1324
1420
|
type: "server_tool_complete",
|
|
1325
1421
|
toolUseId: block.tool_use_id,
|
|
@@ -1327,6 +1423,8 @@ export class AnthropicProvider implements Provider {
|
|
|
1327
1423
|
...(Array.isArray(block.content)
|
|
1328
1424
|
? { content: block.content }
|
|
1329
1425
|
: {}),
|
|
1426
|
+
...(resolvedInput ? { resolvedInput } : {}),
|
|
1427
|
+
...(errorCode ? { errorCode } : {}),
|
|
1330
1428
|
});
|
|
1331
1429
|
}
|
|
1332
1430
|
if (event.type === "content_block_stop") {
|
|
@@ -1345,6 +1443,25 @@ export class AnthropicProvider implements Provider {
|
|
|
1345
1443
|
currentStreamingToolName = undefined;
|
|
1346
1444
|
currentStreamingToolUseId = undefined;
|
|
1347
1445
|
accumulatedInputJson = "";
|
|
1446
|
+
// Finalize the resolved input for a `server_tool_use` block (e.g.
|
|
1447
|
+
// the actual web-search query) so the paired `web_search_tool_result`
|
|
1448
|
+
// emits `server_tool_complete` with `resolvedInput` populated.
|
|
1449
|
+
if (currentServerToolUseId && accumulatedServerToolInputJson) {
|
|
1450
|
+
try {
|
|
1451
|
+
const parsed = JSON.parse(accumulatedServerToolInputJson);
|
|
1452
|
+
if (parsed && typeof parsed === "object") {
|
|
1453
|
+
resolvedServerToolInputs.set(
|
|
1454
|
+
currentServerToolUseId,
|
|
1455
|
+
parsed as Record<string, unknown>,
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
// Malformed partial JSON — drop silently; downstream falls
|
|
1460
|
+
// back to whatever was captured at server_tool_start.
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
currentServerToolUseId = undefined;
|
|
1464
|
+
accumulatedServerToolInputJson = "";
|
|
1348
1465
|
// Flush residual text buffer unless it's exactly a sentinel.
|
|
1349
1466
|
if (textBuffer.length > 0 && !isCompleteSentinel(textBuffer)) {
|
|
1350
1467
|
onEvent?.({ type: "text_delta", text: textBuffer });
|
|
@@ -1354,6 +1471,13 @@ export class AnthropicProvider implements Provider {
|
|
|
1354
1471
|
});
|
|
1355
1472
|
|
|
1356
1473
|
stream.on("inputJson", (partialJson) => {
|
|
1474
|
+
if (currentServerToolUseId) {
|
|
1475
|
+
// Server-tool input (e.g. `web_search` query) — accumulate without
|
|
1476
|
+
// emitting `input_json_delta`; the daemon only consumes the
|
|
1477
|
+
// finalized value from `server_tool_complete.resolvedInput`.
|
|
1478
|
+
accumulatedServerToolInputJson += partialJson;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1357
1481
|
if (!currentStreamingToolName) return;
|
|
1358
1482
|
accumulatedInputJson += partialJson;
|
|
1359
1483
|
const now = Date.now();
|
|
@@ -1544,6 +1668,9 @@ export class AnthropicProvider implements Provider {
|
|
|
1544
1668
|
case "text":
|
|
1545
1669
|
return { type: "text", text: block.text };
|
|
1546
1670
|
case "thinking":
|
|
1671
|
+
if (!block.signature) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1547
1674
|
return {
|
|
1548
1675
|
type: "thinking",
|
|
1549
1676
|
thinking: block.thinking,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PROVIDER_CATALOG } from "../model-catalog.js";
|
|
1
2
|
import { OpenAIChatCompletionsProvider } from "../openai/chat-completions-provider.js";
|
|
2
3
|
|
|
3
4
|
export interface FireworksProviderOptions {
|
|
@@ -8,6 +9,15 @@ export interface FireworksProviderOptions {
|
|
|
8
9
|
|
|
9
10
|
const DEFAULT_FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1";
|
|
10
11
|
|
|
12
|
+
const FIREWORKS_MODEL_EFFORT_CEILINGS: ReadonlyMap<
|
|
13
|
+
string,
|
|
14
|
+
"high" | "xhigh" | "max"
|
|
15
|
+
> = new Map(
|
|
16
|
+
PROVIDER_CATALOG.find((p) => p.id === "fireworks")?.models.flatMap((m) =>
|
|
17
|
+
m.maxEffort ? ([[m.id, m.maxEffort]] as const) : [],
|
|
18
|
+
) ?? [],
|
|
19
|
+
);
|
|
20
|
+
|
|
11
21
|
export class FireworksProvider extends OpenAIChatCompletionsProvider {
|
|
12
22
|
constructor(
|
|
13
23
|
apiKey: string,
|
|
@@ -19,9 +29,17 @@ export class FireworksProvider extends OpenAIChatCompletionsProvider {
|
|
|
19
29
|
providerName: "fireworks",
|
|
20
30
|
providerLabel: "Fireworks",
|
|
21
31
|
streamTimeoutMs: options.streamTimeoutMs,
|
|
22
|
-
//
|
|
23
|
-
// low|medium|high
|
|
32
|
+
// Fallback for models not declared in the catalog. Most Fireworks
|
|
33
|
+
// chat-completions models only document `low|medium|high`; per-model
|
|
34
|
+
// overrides (e.g. DeepSeek V4 → "max") come from
|
|
35
|
+
// {@link resolveMaxReasoningEffort}.
|
|
24
36
|
maxReasoningEffort: "high",
|
|
25
37
|
});
|
|
26
38
|
}
|
|
39
|
+
|
|
40
|
+
protected override resolveMaxReasoningEffort(
|
|
41
|
+
model: string,
|
|
42
|
+
): "high" | "xhigh" | "max" {
|
|
43
|
+
return FIREWORKS_MODEL_EFFORT_CEILINGS.get(model) ?? "high";
|
|
44
|
+
}
|
|
27
45
|
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-level validation tests for base_url provider-type gate and SSRF
|
|
3
|
+
* protection on inference provider connections.
|
|
4
|
+
*
|
|
5
|
+
* These tests exercise the POST (create) and PATCH (update) route handlers
|
|
6
|
+
* with mocked DB, config, and DNS resolution.
|
|
7
|
+
*/
|
|
8
|
+
import { Database } from "bun:sqlite";
|
|
9
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
12
|
+
|
|
13
|
+
import { migrateCreateProviderConnections } from "../../../memory/migrations/243-provider-connections.js";
|
|
14
|
+
import { migrateProviderConnectionStatusLabel } from "../../../memory/migrations/244-provider-connection-status-label.js";
|
|
15
|
+
import { migrateProviderConnectionBaseUrlAndModels } from "../../../memory/migrations/250-provider-connection-base-url-and-models.js";
|
|
16
|
+
import * as schema from "../../../memory/schema.js";
|
|
17
|
+
|
|
18
|
+
function createTestDb() {
|
|
19
|
+
const sqlite = new Database(":memory:");
|
|
20
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
21
|
+
return drizzle(sqlite, { schema });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function bootDb() {
|
|
25
|
+
const db = createTestDb();
|
|
26
|
+
migrateCreateProviderConnections(db);
|
|
27
|
+
migrateProviderConnectionStatusLabel(db);
|
|
28
|
+
migrateProviderConnectionBaseUrlAndModels(db);
|
|
29
|
+
return db;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create and hold a test DB; the mock returns it from getDb().
|
|
33
|
+
const testDb = bootDb();
|
|
34
|
+
|
|
35
|
+
mock.module("../../../memory/db-connection.js", () => ({
|
|
36
|
+
getDb: () => testDb,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module("../../../config/assistant-feature-flags.js", () => ({
|
|
40
|
+
isAssistantFeatureFlagEnabled: (flag: string) =>
|
|
41
|
+
flag === "openai-compatible-endpoints",
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
mock.module("../../../config/loader.js", () => ({
|
|
45
|
+
getConfig: () => ({ llm: {} }),
|
|
46
|
+
getConfigReadOnly: () => ({ llm: {} }),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// Mock DNS resolution: all hostnames resolve to a public IP by default.
|
|
50
|
+
// Tests that need private-IP resolution override this per-test.
|
|
51
|
+
let mockResolvedAddresses: string[] = ["93.184.216.34"];
|
|
52
|
+
|
|
53
|
+
// Import the real url-safety module for use inside the mock.
|
|
54
|
+
const {
|
|
55
|
+
isPrivateOrLocalHost: realIsPrivateOrLocalHost,
|
|
56
|
+
isPrivateIPv4,
|
|
57
|
+
isPrivateIPv6,
|
|
58
|
+
isIPv4,
|
|
59
|
+
isIPv6,
|
|
60
|
+
parseUrl,
|
|
61
|
+
unwrapBracketedHostname,
|
|
62
|
+
extractEmbeddedIPv4FromIPv6,
|
|
63
|
+
looksLikeHostPortShorthand,
|
|
64
|
+
looksLikePathOnlyInput,
|
|
65
|
+
buildHostHeader,
|
|
66
|
+
stripUrlUserinfo,
|
|
67
|
+
sanitizeUrlForOutput,
|
|
68
|
+
sanitizeUrlStringForOutput,
|
|
69
|
+
} = await import("../../../tools/network/url-safety.js");
|
|
70
|
+
|
|
71
|
+
mock.module("../../../tools/network/url-safety.js", () => ({
|
|
72
|
+
isPrivateOrLocalHost: realIsPrivateOrLocalHost,
|
|
73
|
+
isPrivateIPv4,
|
|
74
|
+
isPrivateIPv6,
|
|
75
|
+
isIPv4,
|
|
76
|
+
isIPv6,
|
|
77
|
+
parseUrl,
|
|
78
|
+
unwrapBracketedHostname,
|
|
79
|
+
extractEmbeddedIPv4FromIPv6,
|
|
80
|
+
looksLikeHostPortShorthand,
|
|
81
|
+
looksLikePathOnlyInput,
|
|
82
|
+
buildHostHeader,
|
|
83
|
+
stripUrlUserinfo,
|
|
84
|
+
sanitizeUrlForOutput,
|
|
85
|
+
sanitizeUrlStringForOutput,
|
|
86
|
+
resolveHostAddresses: async () => mockResolvedAddresses,
|
|
87
|
+
resolveRequestAddress: async (
|
|
88
|
+
hostname: string,
|
|
89
|
+
_resolveHost: unknown,
|
|
90
|
+
allowPrivateNetwork: boolean,
|
|
91
|
+
) => {
|
|
92
|
+
if (!allowPrivateNetwork && realIsPrivateOrLocalHost(hostname)) {
|
|
93
|
+
return { addresses: [], blockedAddress: hostname };
|
|
94
|
+
}
|
|
95
|
+
const addresses = mockResolvedAddresses;
|
|
96
|
+
if (!allowPrivateNetwork) {
|
|
97
|
+
for (const addr of addresses) {
|
|
98
|
+
if (realIsPrivateOrLocalHost(addr)) {
|
|
99
|
+
return { addresses: [], blockedAddress: addr };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { addresses };
|
|
104
|
+
},
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const { ROUTES } = await import(
|
|
108
|
+
"../../../runtime/routes/inference-provider-connection-routes.js"
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const handleCreate = ROUTES.find(
|
|
112
|
+
(r) => r.operationId === "inference_provider_connections_create",
|
|
113
|
+
)!.handler;
|
|
114
|
+
|
|
115
|
+
const handleUpdate = ROUTES.find(
|
|
116
|
+
(r) => r.operationId === "inference_provider_connections_update",
|
|
117
|
+
)!.handler;
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Provider-type gate: base_url rejected for non-openai-compatible providers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
describe("base_url provider-type gate (create)", () => {
|
|
124
|
+
test("rejects base_url on anthropic provider", async () => {
|
|
125
|
+
await expect(
|
|
126
|
+
handleCreate({
|
|
127
|
+
body: {
|
|
128
|
+
name: "exfil-anthropic",
|
|
129
|
+
provider: "anthropic",
|
|
130
|
+
auth: { type: "api_key", credential: "cred-exfil" },
|
|
131
|
+
base_url: "https://evil.example.com/v1",
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
).rejects.toThrow(/base_url is only valid for openai-compatible/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("rejects base_url on openai provider", async () => {
|
|
138
|
+
await expect(
|
|
139
|
+
handleCreate({
|
|
140
|
+
body: {
|
|
141
|
+
name: "exfil-openai",
|
|
142
|
+
provider: "openai",
|
|
143
|
+
auth: { type: "api_key", credential: "cred-exfil" },
|
|
144
|
+
base_url: "https://evil.example.com/v1",
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toThrow(/base_url is only valid for openai-compatible/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("rejects base_url on gemini provider", async () => {
|
|
151
|
+
await expect(
|
|
152
|
+
handleCreate({
|
|
153
|
+
body: {
|
|
154
|
+
name: "exfil-gemini",
|
|
155
|
+
provider: "gemini",
|
|
156
|
+
auth: { type: "api_key", credential: "cred-exfil" },
|
|
157
|
+
base_url: "https://evil.example.com/v1",
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
).rejects.toThrow(/base_url is only valid for openai-compatible/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("accepts base_url on openai-compatible provider", async () => {
|
|
164
|
+
mockResolvedAddresses = ["93.184.216.34"];
|
|
165
|
+
const result = await handleCreate({
|
|
166
|
+
body: {
|
|
167
|
+
name: "valid-oai-compat",
|
|
168
|
+
provider: "openai-compatible",
|
|
169
|
+
auth: { type: "api_key", credential: "cred-vllm" },
|
|
170
|
+
base_url: "https://my-vllm.example.com/v1",
|
|
171
|
+
models: [{ id: "my-model" }],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
expect(result).toBeDefined();
|
|
175
|
+
expect((result as { baseUrl: string }).baseUrl).toBe(
|
|
176
|
+
"https://my-vllm.example.com/v1",
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("allows null base_url on non-openai-compatible provider", async () => {
|
|
181
|
+
// Setting base_url to null is always allowed (it's a no-op clear).
|
|
182
|
+
const result = await handleCreate({
|
|
183
|
+
body: {
|
|
184
|
+
name: "null-baseurl-anthropic",
|
|
185
|
+
provider: "anthropic",
|
|
186
|
+
auth: { type: "api_key", credential: "cred-test" },
|
|
187
|
+
base_url: null,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
expect(result).toBeDefined();
|
|
191
|
+
expect((result as { baseUrl: string | null }).baseUrl).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// SSRF protection: private/local network addresses blocked
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
describe("base_url SSRF protection (create)", () => {
|
|
200
|
+
test("rejects private IP (192.168.x.x)", async () => {
|
|
201
|
+
await expect(
|
|
202
|
+
handleCreate({
|
|
203
|
+
body: {
|
|
204
|
+
name: "ssrf-private-ip",
|
|
205
|
+
provider: "openai-compatible",
|
|
206
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
207
|
+
base_url: "http://192.168.1.1/v1",
|
|
208
|
+
models: [{ id: "m" }],
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
).rejects.toThrow(/private or local network/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("rejects localhost", async () => {
|
|
215
|
+
await expect(
|
|
216
|
+
handleCreate({
|
|
217
|
+
body: {
|
|
218
|
+
name: "ssrf-localhost",
|
|
219
|
+
provider: "openai-compatible",
|
|
220
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
221
|
+
base_url: "http://localhost:8080/v1",
|
|
222
|
+
models: [{ id: "m" }],
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
).rejects.toThrow(/private or local network/);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("rejects 0.0.0.0", async () => {
|
|
229
|
+
await expect(
|
|
230
|
+
handleCreate({
|
|
231
|
+
body: {
|
|
232
|
+
name: "ssrf-zero",
|
|
233
|
+
provider: "openai-compatible",
|
|
234
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
235
|
+
base_url: "http://0.0.0.0:8080/v1",
|
|
236
|
+
models: [{ id: "m" }],
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
).rejects.toThrow(/private or local network/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("rejects 10.x.x.x", async () => {
|
|
243
|
+
await expect(
|
|
244
|
+
handleCreate({
|
|
245
|
+
body: {
|
|
246
|
+
name: "ssrf-ten",
|
|
247
|
+
provider: "openai-compatible",
|
|
248
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
249
|
+
base_url: "http://10.0.0.1/v1",
|
|
250
|
+
models: [{ id: "m" }],
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
).rejects.toThrow(/private or local network/);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("rejects 127.0.0.1", async () => {
|
|
257
|
+
await expect(
|
|
258
|
+
handleCreate({
|
|
259
|
+
body: {
|
|
260
|
+
name: "ssrf-loopback",
|
|
261
|
+
provider: "openai-compatible",
|
|
262
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
263
|
+
base_url: "http://127.0.0.1:8080/v1",
|
|
264
|
+
models: [{ id: "m" }],
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
).rejects.toThrow(/private or local network/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("rejects metadata.google.internal", async () => {
|
|
271
|
+
await expect(
|
|
272
|
+
handleCreate({
|
|
273
|
+
body: {
|
|
274
|
+
name: "ssrf-metadata",
|
|
275
|
+
provider: "openai-compatible",
|
|
276
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
277
|
+
base_url: "http://metadata.google.internal/computeMetadata/v1/",
|
|
278
|
+
models: [{ id: "m" }],
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
).rejects.toThrow(/private or local network/);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("rejects hostname that resolves to a private IP", async () => {
|
|
285
|
+
mockResolvedAddresses = ["10.0.0.5"];
|
|
286
|
+
await expect(
|
|
287
|
+
handleCreate({
|
|
288
|
+
body: {
|
|
289
|
+
name: "ssrf-dns-rebind",
|
|
290
|
+
provider: "openai-compatible",
|
|
291
|
+
auth: { type: "api_key", credential: "cred-ssrf" },
|
|
292
|
+
base_url: "https://dns-rebind.example.com/v1",
|
|
293
|
+
models: [{ id: "m" }],
|
|
294
|
+
},
|
|
295
|
+
}),
|
|
296
|
+
).rejects.toThrow(/resolves to a private network/);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("accepts a valid public URL", async () => {
|
|
300
|
+
mockResolvedAddresses = ["93.184.216.34"];
|
|
301
|
+
const result = await handleCreate({
|
|
302
|
+
body: {
|
|
303
|
+
name: "valid-public-url",
|
|
304
|
+
provider: "openai-compatible",
|
|
305
|
+
auth: { type: "api_key", credential: "cred-valid" },
|
|
306
|
+
base_url: "https://api.example.com/v1",
|
|
307
|
+
models: [{ id: "my-model" }],
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
expect(result).toBeDefined();
|
|
311
|
+
expect((result as { baseUrl: string }).baseUrl).toBe(
|
|
312
|
+
"https://api.example.com/v1",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Update handler: base_url validation on existing connections
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
describe("base_url provider-type gate (update)", () => {
|
|
322
|
+
test("rejects adding base_url to an existing anthropic connection", async () => {
|
|
323
|
+
// Seed a connection first.
|
|
324
|
+
await handleCreate({
|
|
325
|
+
body: {
|
|
326
|
+
name: "update-test-anthropic",
|
|
327
|
+
provider: "anthropic",
|
|
328
|
+
auth: { type: "api_key", credential: "cred-update" },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
await expect(
|
|
333
|
+
handleUpdate({
|
|
334
|
+
pathParams: { name: "update-test-anthropic" },
|
|
335
|
+
body: {
|
|
336
|
+
auth: { type: "api_key", credential: "cred-update" },
|
|
337
|
+
base_url: "https://evil.example.com/v1",
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
).rejects.toThrow(/base_url is only valid for openai-compatible/);
|
|
341
|
+
});
|
|
342
|
+
});
|