@vellumai/assistant 0.11.5 → 0.11.6-staging.1
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 +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -21,6 +21,13 @@ import type {
|
|
|
21
21
|
ToolExecutionResult,
|
|
22
22
|
} from "../types.js";
|
|
23
23
|
import { extractDomain } from "./domain-normalize.js";
|
|
24
|
+
import {
|
|
25
|
+
extractFirecrawlCompatSearchResults,
|
|
26
|
+
FASTCRW_DEFAULT_API_BASE,
|
|
27
|
+
type FirecrawlCompatSearchResult,
|
|
28
|
+
resolveProviderApiUrl,
|
|
29
|
+
searchResultSnippet,
|
|
30
|
+
} from "./firecrawl-compat.js";
|
|
24
31
|
import type { ManagedSearchProxyResult } from "./managed-search-proxy.js";
|
|
25
32
|
import {
|
|
26
33
|
classifyWebSearchFailure,
|
|
@@ -35,6 +42,7 @@ const BRAVE_API_URL = `https://api.search.brave.com${BRAVE_SEARCH_PATH}`;
|
|
|
35
42
|
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
|
36
43
|
const TAVILY_API_URL = "https://api.tavily.com/search";
|
|
37
44
|
const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v2/search";
|
|
45
|
+
const FASTCRW_SEARCH_PATH = "/v1/search";
|
|
38
46
|
// Keenable is keyless by default: the public path needs no key (rate-limited);
|
|
39
47
|
// a key switches to the authenticated path and lifts the cap.
|
|
40
48
|
const KEENABLE_API_BASE_URL = "https://api.keenable.ai";
|
|
@@ -46,7 +54,8 @@ type WebSearchProvider =
|
|
|
46
54
|
| "brave"
|
|
47
55
|
| "tavily"
|
|
48
56
|
| "firecrawl"
|
|
49
|
-
| "keenable"
|
|
57
|
+
| "keenable"
|
|
58
|
+
| "fastcrw";
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* Arguments passed to every {@link WebSearchAdapter}. The full superset is
|
|
@@ -123,25 +132,21 @@ interface TavilySearchResponse {
|
|
|
123
132
|
results?: TavilySearchResult[];
|
|
124
133
|
}
|
|
125
134
|
|
|
126
|
-
interface FirecrawlSearchResult {
|
|
127
|
-
title?: string;
|
|
128
|
-
description?: string;
|
|
129
|
-
url?: string;
|
|
130
|
-
category?: string;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
135
|
/**
|
|
134
|
-
* Firecrawl `/v2/search`
|
|
135
|
-
*
|
|
136
|
-
* `
|
|
136
|
+
* Firecrawl `/v2/search` and fastCRW `/v1/search` payloads. Result rows are
|
|
137
|
+
* read via {@link extractFirecrawlCompatSearchResults} (grouped `data.web`,
|
|
138
|
+
* self-hosted `data.results.web`, flat `data.results`, or array `data`).
|
|
137
139
|
*/
|
|
138
|
-
interface
|
|
140
|
+
interface FirecrawlCompatSearchResponse {
|
|
139
141
|
success?: boolean;
|
|
140
|
-
data?:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
data?:
|
|
143
|
+
| FirecrawlCompatSearchResult[]
|
|
144
|
+
| {
|
|
145
|
+
web?: FirecrawlCompatSearchResult[];
|
|
146
|
+
results?: FirecrawlCompatSearchResult[];
|
|
147
|
+
news?: FirecrawlCompatSearchResult[];
|
|
148
|
+
images?: unknown[];
|
|
149
|
+
};
|
|
145
150
|
warning?: string | null;
|
|
146
151
|
}
|
|
147
152
|
|
|
@@ -278,11 +283,11 @@ function formatTavilyResults(
|
|
|
278
283
|
return lines.join("\n");
|
|
279
284
|
}
|
|
280
285
|
|
|
281
|
-
function
|
|
282
|
-
data:
|
|
286
|
+
function formatFirecrawlCompatResults(
|
|
287
|
+
data: FirecrawlCompatSearchResponse,
|
|
283
288
|
query: string,
|
|
284
289
|
): string {
|
|
285
|
-
const results = data
|
|
290
|
+
const results = extractFirecrawlCompatSearchResults(data);
|
|
286
291
|
|
|
287
292
|
if (results.length === 0) {
|
|
288
293
|
return `No results found for "${query}".`;
|
|
@@ -297,8 +302,9 @@ function formatFirecrawlResults(
|
|
|
297
302
|
if (r.url) {
|
|
298
303
|
lines.push(` URL: ${r.url}`);
|
|
299
304
|
}
|
|
300
|
-
|
|
301
|
-
|
|
305
|
+
const snippet = searchResultSnippet(r);
|
|
306
|
+
if (snippet) {
|
|
307
|
+
lines.push(` ${snippet}`);
|
|
302
308
|
}
|
|
303
309
|
lines.push("");
|
|
304
310
|
}
|
|
@@ -442,12 +448,13 @@ function tavilyTimeRangeForFreshness(
|
|
|
442
448
|
}
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
function
|
|
446
|
-
data:
|
|
451
|
+
function buildFirecrawlCompatMetadata(
|
|
452
|
+
data: FirecrawlCompatSearchResponse,
|
|
447
453
|
query: string,
|
|
448
454
|
durationMs: number,
|
|
455
|
+
provider: "firecrawl" | "fastcrw",
|
|
449
456
|
): WebSearchMetadata {
|
|
450
|
-
const results = data
|
|
457
|
+
const results = extractFirecrawlCompatSearchResults(data);
|
|
451
458
|
const items: WebSearchResultItem[] = results.map((r, i) => {
|
|
452
459
|
const url = r.url ?? "";
|
|
453
460
|
const domain = extractDomain(url);
|
|
@@ -457,12 +464,12 @@ function buildFirecrawlMetadata(
|
|
|
457
464
|
url,
|
|
458
465
|
domain,
|
|
459
466
|
faviconUrl: faviconUrlForDomain(domain),
|
|
460
|
-
snippet: r
|
|
467
|
+
snippet: searchResultSnippet(r),
|
|
461
468
|
};
|
|
462
469
|
});
|
|
463
470
|
return {
|
|
464
471
|
query,
|
|
465
|
-
provider
|
|
472
|
+
provider,
|
|
466
473
|
resultCount: items.length,
|
|
467
474
|
durationMs,
|
|
468
475
|
results: items,
|
|
@@ -1080,12 +1087,17 @@ async function executeTavilySearch(
|
|
|
1080
1087
|
);
|
|
1081
1088
|
}
|
|
1082
1089
|
|
|
1083
|
-
async function
|
|
1090
|
+
async function executeFirecrawlCompatSearch(
|
|
1084
1091
|
query: string,
|
|
1085
1092
|
count: number,
|
|
1086
1093
|
freshness: string | undefined,
|
|
1087
1094
|
apiKey: string,
|
|
1088
|
-
|
|
1095
|
+
options: {
|
|
1096
|
+
endpoint: string;
|
|
1097
|
+
provider: "firecrawl" | "fastcrw";
|
|
1098
|
+
displayName: string;
|
|
1099
|
+
signal?: AbortSignal;
|
|
1100
|
+
},
|
|
1089
1101
|
): Promise<ToolExecutionResult> {
|
|
1090
1102
|
const tbs = firecrawlTbsForFreshness(freshness);
|
|
1091
1103
|
const body: Record<string, unknown> = {
|
|
@@ -1098,36 +1110,51 @@ async function executeFirecrawlSearch(
|
|
|
1098
1110
|
}
|
|
1099
1111
|
|
|
1100
1112
|
const startedAt = Date.now();
|
|
1113
|
+
const headers: Record<string, string> = {
|
|
1114
|
+
"Content-Type": "application/json",
|
|
1115
|
+
"X-Client-Source": "vellum-assistant",
|
|
1116
|
+
};
|
|
1117
|
+
const trimmedKey = apiKey.trim();
|
|
1118
|
+
if (trimmedKey.length > 0) {
|
|
1119
|
+
headers.Authorization = `Bearer ${trimmedKey}`;
|
|
1120
|
+
}
|
|
1101
1121
|
|
|
1102
1122
|
for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
|
|
1103
1123
|
let response: Response;
|
|
1104
1124
|
try {
|
|
1105
|
-
response = await fetch(
|
|
1125
|
+
response = await fetch(options.endpoint, {
|
|
1106
1126
|
method: "POST",
|
|
1107
|
-
headers
|
|
1108
|
-
"Content-Type": "application/json",
|
|
1109
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1110
|
-
"X-Client-Source": "vellum-assistant",
|
|
1111
|
-
},
|
|
1127
|
+
headers,
|
|
1112
1128
|
body: JSON.stringify(body),
|
|
1113
|
-
signal,
|
|
1129
|
+
signal: options.signal,
|
|
1114
1130
|
});
|
|
1115
1131
|
} catch (err) {
|
|
1116
|
-
return networkFailureResult(
|
|
1132
|
+
return networkFailureResult(
|
|
1133
|
+
query,
|
|
1134
|
+
options.provider,
|
|
1135
|
+
startedAt,
|
|
1136
|
+
err,
|
|
1137
|
+
options.signal,
|
|
1138
|
+
);
|
|
1117
1139
|
}
|
|
1118
1140
|
|
|
1119
1141
|
if (response.ok) {
|
|
1120
|
-
const data = (await response.json()) as
|
|
1142
|
+
const data = (await response.json()) as FirecrawlCompatSearchResponse;
|
|
1121
1143
|
const durationMs = Date.now() - startedAt;
|
|
1122
1144
|
return {
|
|
1123
1145
|
content:
|
|
1124
|
-
wrapUntrustedContent(
|
|
1146
|
+
wrapUntrustedContent(formatFirecrawlCompatResults(data, query), {
|
|
1125
1147
|
source: "search",
|
|
1126
|
-
sourceDetail:
|
|
1148
|
+
sourceDetail: options.provider,
|
|
1127
1149
|
}) + CITATION_INSTRUCTION,
|
|
1128
1150
|
isError: false,
|
|
1129
1151
|
activityMetadata: {
|
|
1130
|
-
webSearch:
|
|
1152
|
+
webSearch: buildFirecrawlCompatMetadata(
|
|
1153
|
+
data,
|
|
1154
|
+
query,
|
|
1155
|
+
durationMs,
|
|
1156
|
+
options.provider,
|
|
1157
|
+
),
|
|
1131
1158
|
},
|
|
1132
1159
|
};
|
|
1133
1160
|
}
|
|
@@ -1137,9 +1164,9 @@ async function executeFirecrawlSearch(
|
|
|
1137
1164
|
if (response.status === 401 || response.status === 403) {
|
|
1138
1165
|
return errorResult(
|
|
1139
1166
|
query,
|
|
1140
|
-
|
|
1167
|
+
options.provider,
|
|
1141
1168
|
startedAt,
|
|
1142
|
-
|
|
1169
|
+
`Invalid or expired ${options.displayName} API key`,
|
|
1143
1170
|
);
|
|
1144
1171
|
}
|
|
1145
1172
|
|
|
@@ -1150,34 +1177,73 @@ async function executeFirecrawlSearch(
|
|
|
1150
1177
|
DEFAULT_BASE_DELAY_MS,
|
|
1151
1178
|
);
|
|
1152
1179
|
log.warn(
|
|
1153
|
-
{ attempt: attempt + 1, delayMs },
|
|
1154
|
-
|
|
1180
|
+
{ attempt: attempt + 1, delayMs, provider: options.provider },
|
|
1181
|
+
`${options.displayName} Search rate limited, retrying`,
|
|
1155
1182
|
);
|
|
1156
1183
|
await sleep(delayMs);
|
|
1157
1184
|
continue;
|
|
1158
1185
|
}
|
|
1159
1186
|
|
|
1160
|
-
log.warn(
|
|
1187
|
+
log.warn(
|
|
1188
|
+
{ status: response.status, provider: options.provider },
|
|
1189
|
+
`${options.displayName} Search API error`,
|
|
1190
|
+
);
|
|
1161
1191
|
return backendFailureResult(
|
|
1162
1192
|
query,
|
|
1163
|
-
|
|
1193
|
+
options.provider,
|
|
1164
1194
|
startedAt,
|
|
1165
1195
|
{ statusCode: response.status, error: rawBodyDetail(bodyText) },
|
|
1166
1196
|
response.status === 429
|
|
1167
|
-
?
|
|
1168
|
-
:
|
|
1197
|
+
? `${options.displayName} Search rate limit exceeded after retries. Try again shortly.`
|
|
1198
|
+
: `${options.displayName} Search API returned status ${response.status}`,
|
|
1169
1199
|
);
|
|
1170
1200
|
}
|
|
1171
1201
|
|
|
1172
1202
|
return backendFailureResult(
|
|
1173
1203
|
query,
|
|
1174
|
-
|
|
1204
|
+
options.provider,
|
|
1175
1205
|
startedAt,
|
|
1176
1206
|
{ statusCode: 429 },
|
|
1177
|
-
|
|
1207
|
+
`${options.displayName} Search rate limit exceeded after retries. Try again shortly.`,
|
|
1178
1208
|
);
|
|
1179
1209
|
}
|
|
1180
1210
|
|
|
1211
|
+
function executeFirecrawlSearch(
|
|
1212
|
+
query: string,
|
|
1213
|
+
count: number,
|
|
1214
|
+
freshness: string | undefined,
|
|
1215
|
+
apiKey: string,
|
|
1216
|
+
signal?: AbortSignal,
|
|
1217
|
+
): Promise<ToolExecutionResult> {
|
|
1218
|
+
return executeFirecrawlCompatSearch(query, count, freshness, apiKey, {
|
|
1219
|
+
endpoint: FIRECRAWL_API_URL,
|
|
1220
|
+
provider: "firecrawl",
|
|
1221
|
+
displayName: "Firecrawl",
|
|
1222
|
+
signal,
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function executeFastcrwSearch(
|
|
1227
|
+
query: string,
|
|
1228
|
+
count: number,
|
|
1229
|
+
freshness: string | undefined,
|
|
1230
|
+
apiKey: string,
|
|
1231
|
+
signal?: AbortSignal,
|
|
1232
|
+
): Promise<ToolExecutionResult> {
|
|
1233
|
+
const apiBase = getConfig().services["web-search"]?.apiBase;
|
|
1234
|
+
const endpoint = resolveProviderApiUrl(
|
|
1235
|
+
apiBase,
|
|
1236
|
+
FASTCRW_SEARCH_PATH,
|
|
1237
|
+
FASTCRW_DEFAULT_API_BASE,
|
|
1238
|
+
);
|
|
1239
|
+
return executeFirecrawlCompatSearch(query, count, freshness, apiKey, {
|
|
1240
|
+
endpoint,
|
|
1241
|
+
provider: "fastcrw",
|
|
1242
|
+
displayName: "fastCRW",
|
|
1243
|
+
signal,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1181
1247
|
async function executeKeenableSearch(
|
|
1182
1248
|
query: string,
|
|
1183
1249
|
count: number,
|
|
@@ -1345,6 +1411,14 @@ const keenableSearchAdapter: WebSearchAdapter = {
|
|
|
1345
1411
|
executeKeenableSearch(query, count, freshness, apiKey, signal),
|
|
1346
1412
|
};
|
|
1347
1413
|
|
|
1414
|
+
const fastcrwSearchAdapter: WebSearchAdapter = {
|
|
1415
|
+
id: "fastcrw",
|
|
1416
|
+
providerKeyName: "fastcrw",
|
|
1417
|
+
fallbackOrder: 6,
|
|
1418
|
+
execute: ({ query, count, freshness, apiKey, signal }) =>
|
|
1419
|
+
executeFastcrwSearch(query, count, freshness, apiKey, signal),
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1348
1422
|
/**
|
|
1349
1423
|
* All built-in web-search adapters keyed by provider id. The
|
|
1350
1424
|
* `Record<WebSearchProvider, ...>` shape forces TypeScript to flag any
|
|
@@ -1356,6 +1430,7 @@ const WEB_SEARCH_ADAPTERS: Record<WebSearchProvider, WebSearchAdapter> = {
|
|
|
1356
1430
|
tavily: tavilySearchAdapter,
|
|
1357
1431
|
firecrawl: firecrawlSearchAdapter,
|
|
1358
1432
|
keenable: keenableSearchAdapter,
|
|
1433
|
+
fastcrw: fastcrwSearchAdapter,
|
|
1359
1434
|
};
|
|
1360
1435
|
|
|
1361
1436
|
/**
|
|
@@ -1387,7 +1462,7 @@ export const webSearchTool = {
|
|
|
1387
1462
|
count: {
|
|
1388
1463
|
type: "number",
|
|
1389
1464
|
description:
|
|
1390
|
-
"Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, and
|
|
1465
|
+
"Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, Keenable, and fastCRW providers.",
|
|
1391
1466
|
},
|
|
1392
1467
|
offset: {
|
|
1393
1468
|
type: "number",
|
|
@@ -1397,7 +1472,7 @@ export const webSearchTool = {
|
|
|
1397
1472
|
freshness: {
|
|
1398
1473
|
type: "string",
|
|
1399
1474
|
description:
|
|
1400
|
-
'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, and
|
|
1475
|
+
'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, Keenable, and fastCRW providers.',
|
|
1401
1476
|
},
|
|
1402
1477
|
},
|
|
1403
1478
|
required: ["query"],
|
|
@@ -1462,13 +1537,24 @@ export const webSearchTool = {
|
|
|
1462
1537
|
let provider = getWebSearchProvider();
|
|
1463
1538
|
let apiKey = await getApiKey(provider);
|
|
1464
1539
|
|
|
1540
|
+
const webSearchApiBase = getConfig().services["web-search"]?.apiBase;
|
|
1541
|
+
const fastcrwAllowsKeylessSelfHost =
|
|
1542
|
+
provider === "fastcrw" &&
|
|
1543
|
+
typeof webSearchApiBase === "string" &&
|
|
1544
|
+
webSearchApiBase.trim().length > 0;
|
|
1545
|
+
|
|
1465
1546
|
// A keyless provider (e.g. Keenable) runs without a stored key, so skip the
|
|
1466
1547
|
// missing-key fallback/error entirely and execute it as configured.
|
|
1548
|
+
// fastCRW with a custom API Base may also run without a key (self-hosted).
|
|
1467
1549
|
// Fallback: if a key-based provider has no key, try other BYOK search
|
|
1468
1550
|
// providers in a stable order. This preserves existing installs that only
|
|
1469
1551
|
// configured one search-provider key while still allowing new providers to
|
|
1470
1552
|
// be selected explicitly.
|
|
1471
|
-
if (
|
|
1553
|
+
if (
|
|
1554
|
+
!apiKey &&
|
|
1555
|
+
!WEB_SEARCH_ADAPTERS[provider].keyless &&
|
|
1556
|
+
!fastcrwAllowsKeylessSelfHost
|
|
1557
|
+
) {
|
|
1472
1558
|
for (const fallback of fallbackProvidersFor(provider)) {
|
|
1473
1559
|
const fallbackKey = await getApiKey(fallback);
|
|
1474
1560
|
if (!fallbackKey) {
|
|
@@ -1502,7 +1588,7 @@ export const webSearchTool = {
|
|
|
1502
1588
|
query,
|
|
1503
1589
|
provider,
|
|
1504
1590
|
startedAt,
|
|
1505
|
-
"No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, or `keys set
|
|
1591
|
+
"No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, `keys set firecrawl <key>`, or `keys set fastcrw <key>`, or configure it from the Settings page under API Keys. Or switch the web-search provider to Keenable, which works without a key.",
|
|
1506
1592
|
);
|
|
1507
1593
|
}
|
|
1508
1594
|
}
|
package/src/tools/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ChannelConversationType,
|
|
2
3
|
RiskAllowlistOption,
|
|
3
4
|
RiskDirectoryScopeOption,
|
|
4
5
|
} from "@vellumai/gateway-client";
|
|
@@ -377,7 +378,7 @@ export interface ToolContext {
|
|
|
377
378
|
* resolution; undefined when the chat type is unknown or ambiguous.
|
|
378
379
|
* @legacy
|
|
379
380
|
*/
|
|
380
|
-
channelConversationType?:
|
|
381
|
+
channelConversationType?: ChannelConversationType;
|
|
381
382
|
/**
|
|
382
383
|
* External channel/conversation ID of the current chat (the binding's
|
|
383
384
|
* external chat id — Slack channel, Telegram chat, …). Keys the channel
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findContentTypeHeader,
|
|
5
|
+
isJsonContentType,
|
|
6
|
+
parseRequestBodyData,
|
|
7
|
+
} from "./oauth-request-body.js";
|
|
8
|
+
|
|
9
|
+
describe("isJsonContentType", () => {
|
|
10
|
+
test("recognizes application/json with and without parameters", () => {
|
|
11
|
+
expect(isJsonContentType("application/json")).toBe(true);
|
|
12
|
+
expect(isJsonContentType("application/json; charset=UTF-8")).toBe(true);
|
|
13
|
+
expect(isJsonContentType("Application/JSON")).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("recognizes structured JSON suffixes", () => {
|
|
17
|
+
expect(isJsonContentType("application/vnd.api+json")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("rejects non-JSON media types and empty values", () => {
|
|
21
|
+
expect(isJsonContentType("multipart/related; boundary=b")).toBe(false);
|
|
22
|
+
expect(isJsonContentType("text/plain")).toBe(false);
|
|
23
|
+
expect(isJsonContentType(undefined)).toBe(false);
|
|
24
|
+
expect(isJsonContentType("")).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("findContentTypeHeader", () => {
|
|
29
|
+
test("matches the header in any casing", () => {
|
|
30
|
+
expect(findContentTypeHeader({ "content-type": "text/csv" })).toBe(
|
|
31
|
+
"text/csv",
|
|
32
|
+
);
|
|
33
|
+
expect(findContentTypeHeader({ "Content-Type": "text/csv" })).toBe(
|
|
34
|
+
"text/csv",
|
|
35
|
+
);
|
|
36
|
+
expect(findContentTypeHeader({ "CONTENT-TYPE": "text/csv" })).toBe(
|
|
37
|
+
"text/csv",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns undefined when absent", () => {
|
|
42
|
+
expect(findContentTypeHeader(undefined)).toBeUndefined();
|
|
43
|
+
expect(
|
|
44
|
+
findContentTypeHeader({ Accept: "application/json" }),
|
|
45
|
+
).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("parseRequestBodyData", () => {
|
|
50
|
+
test("keeps the raw string under a non-JSON Content-Type", () => {
|
|
51
|
+
expect(parseRequestBodyData('{"a":1}', "multipart/related")).toBe(
|
|
52
|
+
'{"a":1}',
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("parses under a JSON Content-Type", () => {
|
|
57
|
+
expect(parseRequestBodyData('{"a":1}', "application/json")).toEqual({
|
|
58
|
+
a: 1,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("parses when no Content-Type is given", () => {
|
|
63
|
+
expect(parseRequestBodyData('{"a":1}', undefined)).toEqual({ a: 1 });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("keeps unparseable text raw when no Content-Type is given", () => {
|
|
67
|
+
expect(parseRequestBodyData("a=1&b=2", undefined)).toBe("a=1&b=2");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("keeps a JSON string scalar in its quoted wire form", () => {
|
|
71
|
+
expect(parseRequestBodyData('"hello"', "application/json")).toBe('"hello"');
|
|
72
|
+
expect(parseRequestBodyData('"hello"', undefined)).toBe('"hello"');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for deciding how an outbound OAuth request body is encoded.
|
|
3
|
+
*
|
|
4
|
+
* A body is either structured (an object or array, serialized as JSON) or
|
|
5
|
+
* raw (a string forwarded to the provider byte-for-byte). The Content-Type
|
|
6
|
+
* the caller supplied decides which one a string body is: multipart, XML,
|
|
7
|
+
* form-encoded, and CSV payloads must survive intact, while JSON text is
|
|
8
|
+
* already in its wire form and must not be re-encoded.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** True when a Content-Type header names a JSON media type. */
|
|
12
|
+
export function isJsonContentType(
|
|
13
|
+
contentType: string | undefined | null,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (!contentType) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const mediaType = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
19
|
+
return mediaType === "application/json" || mediaType.endsWith("+json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Case-insensitive Content-Type lookup over a plain header map. */
|
|
23
|
+
export function findContentTypeHeader(
|
|
24
|
+
headers: Record<string, string> | undefined,
|
|
25
|
+
): string | undefined {
|
|
26
|
+
if (!headers) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
30
|
+
if (key.toLowerCase() === "content-type") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Decide how a `-d` payload reaches the provider.
|
|
39
|
+
*
|
|
40
|
+
* With a non-JSON Content-Type the raw string is kept verbatim. Otherwise the
|
|
41
|
+
* string is JSON-parsed so structured bodies keep flowing through the pipeline
|
|
42
|
+
* as objects; text that does not parse stays a string.
|
|
43
|
+
*/
|
|
44
|
+
export function parseRequestBodyData(
|
|
45
|
+
raw: string,
|
|
46
|
+
contentType: string | undefined,
|
|
47
|
+
): unknown {
|
|
48
|
+
if (contentType !== undefined && !isJsonContentType(contentType)) {
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
let parsed: unknown;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
} catch {
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
// A JSON string scalar ("hello") would be indistinguishable from a raw
|
|
58
|
+
// body once unquoted, so the original quoted text is kept as the wire form.
|
|
59
|
+
return typeof parsed === "string" ? raw : parsed;
|
|
60
|
+
}
|