@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
|
@@ -28,6 +28,10 @@ import type {
|
|
|
28
28
|
ToolExecutionResult,
|
|
29
29
|
} from "../types.js";
|
|
30
30
|
import { extractDomain } from "./domain-normalize.js";
|
|
31
|
+
import {
|
|
32
|
+
FASTCRW_DEFAULT_API_BASE,
|
|
33
|
+
resolveProviderApiUrl,
|
|
34
|
+
} from "./firecrawl-compat.js";
|
|
31
35
|
import {
|
|
32
36
|
buildHostHeader,
|
|
33
37
|
isIPv4,
|
|
@@ -46,6 +50,7 @@ import {
|
|
|
46
50
|
const log = getLogger("web-fetch");
|
|
47
51
|
|
|
48
52
|
const FIRECRAWL_SCRAPE_API_URL = "https://api.firecrawl.dev/v2/scrape";
|
|
53
|
+
const FASTCRW_SCRAPE_PATH = "/v1/scrape";
|
|
49
54
|
|
|
50
55
|
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
51
56
|
const MAX_TIMEOUT_SECONDS = 60;
|
|
@@ -1114,25 +1119,29 @@ interface FirecrawlScrapeResponse {
|
|
|
1114
1119
|
|
|
1115
1120
|
function getWebFetchProvider(): WebFetchProviderId {
|
|
1116
1121
|
const configured = getConfig().services["web-fetch"]?.provider ?? "default";
|
|
1117
|
-
|
|
1122
|
+
if (configured === "firecrawl" || configured === "fastcrw") {
|
|
1123
|
+
return configured;
|
|
1124
|
+
}
|
|
1125
|
+
return "default";
|
|
1118
1126
|
}
|
|
1119
1127
|
|
|
1120
1128
|
/**
|
|
1121
|
-
* Decide whether a request may be routed to
|
|
1129
|
+
* Decide whether a request may be routed to a hosted scrape provider
|
|
1130
|
+
* (Firecrawl / fastCRW).
|
|
1122
1131
|
*
|
|
1123
|
-
* Posting a URL to
|
|
1124
|
-
* a third party, so we apply the SAME safety gate as the built-in
|
|
1125
|
-
* BEFORE dispatching — not just the lexical host check:
|
|
1126
|
-
* - only http(s) URLs (
|
|
1132
|
+
* Posting a URL to a hosted scraper sends its path + query (which can hold
|
|
1133
|
+
* secrets) to a third party, so we apply the SAME safety gate as the built-in
|
|
1134
|
+
* fetcher BEFORE dispatching — not just the lexical host check:
|
|
1135
|
+
* - only http(s) URLs (hosted scrapers can't do other schemes anyway),
|
|
1127
1136
|
* - never `allow_private_network` requests (those are intentionally local;
|
|
1128
|
-
*
|
|
1137
|
+
* hosted scrapers can't reach them and the built-in path owns that mode),
|
|
1129
1138
|
* - and a DNS resolution check so a public hostname that resolves to a
|
|
1130
1139
|
* private/blocked address (e.g. `internal.example` → 10.x.x.x) is NOT
|
|
1131
|
-
* leaked to
|
|
1140
|
+
* leaked to the hosted scraper.
|
|
1132
1141
|
* Anything that fails falls back to the built-in fetcher, which enforces its
|
|
1133
1142
|
* own SSRF rules and returns the appropriate error.
|
|
1134
1143
|
*/
|
|
1135
|
-
async function
|
|
1144
|
+
async function canRouteToHostedScraper(
|
|
1136
1145
|
input: Record<string, unknown>,
|
|
1137
1146
|
): Promise<boolean> {
|
|
1138
1147
|
if (input.allow_private_network === true) {
|
|
@@ -1163,10 +1172,11 @@ async function canRouteToFirecrawl(
|
|
|
1163
1172
|
return true;
|
|
1164
1173
|
}
|
|
1165
1174
|
|
|
1166
|
-
function
|
|
1175
|
+
function hostedScrapeErrorResult(
|
|
1167
1176
|
requestedUrl: string,
|
|
1168
1177
|
startedAt: number,
|
|
1169
1178
|
errorMessage: string,
|
|
1179
|
+
provider: "firecrawl" | "fastcrw",
|
|
1170
1180
|
status = 0,
|
|
1171
1181
|
): ToolExecutionResult {
|
|
1172
1182
|
const domain = extractDomain(requestedUrl);
|
|
@@ -1177,7 +1187,7 @@ function firecrawlErrorResult(
|
|
|
1177
1187
|
webFetch: {
|
|
1178
1188
|
url: requestedUrl,
|
|
1179
1189
|
finalUrl: requestedUrl,
|
|
1180
|
-
provider
|
|
1190
|
+
provider,
|
|
1181
1191
|
status,
|
|
1182
1192
|
byteCount: 0,
|
|
1183
1193
|
charCount: 0,
|
|
@@ -1193,7 +1203,7 @@ function firecrawlErrorResult(
|
|
|
1193
1203
|
}
|
|
1194
1204
|
|
|
1195
1205
|
/**
|
|
1196
|
-
* Fetch a page via Firecrawl
|
|
1206
|
+
* Fetch a page via a Firecrawl-compatible scrape endpoint, returning clean
|
|
1197
1207
|
* markdown. Mirrors the built-in fetcher's output shape (same
|
|
1198
1208
|
* {@link formatWebFetchOutput} and {@link WebFetchMetadata}) so the model and
|
|
1199
1209
|
* client UIs can't tell which backend served the page apart from
|
|
@@ -1202,9 +1212,15 @@ function firecrawlErrorResult(
|
|
|
1202
1212
|
* Exported for direct unit testing; the registered tool dispatches here via
|
|
1203
1213
|
* {@link getWebFetchProvider}.
|
|
1204
1214
|
*/
|
|
1205
|
-
export async function
|
|
1215
|
+
export async function executeFirecrawlCompatScrape(
|
|
1206
1216
|
input: Record<string, unknown>,
|
|
1207
|
-
options: {
|
|
1217
|
+
options: {
|
|
1218
|
+
apiKey: string;
|
|
1219
|
+
endpoint: string;
|
|
1220
|
+
provider: "firecrawl" | "fastcrw";
|
|
1221
|
+
displayName: string;
|
|
1222
|
+
signal?: AbortSignal;
|
|
1223
|
+
},
|
|
1208
1224
|
): Promise<ToolExecutionResult> {
|
|
1209
1225
|
const startedAt = Date.now();
|
|
1210
1226
|
const parsedUrl = parseUrl(input.url);
|
|
@@ -1215,22 +1231,24 @@ export async function executeFirecrawlScrape(
|
|
|
1215
1231
|
: sanitizeUrlStringForOutput(targetUrl);
|
|
1216
1232
|
|
|
1217
1233
|
if (!targetUrl) {
|
|
1218
|
-
return
|
|
1234
|
+
return hostedScrapeErrorResult(
|
|
1219
1235
|
safeRequestedUrl,
|
|
1220
1236
|
startedAt,
|
|
1221
1237
|
"url is required and must be a valid HTTP(S) URL",
|
|
1238
|
+
options.provider,
|
|
1222
1239
|
);
|
|
1223
1240
|
}
|
|
1224
1241
|
|
|
1225
|
-
// Never forward URL-embedded credentials (user:password@host) to
|
|
1226
|
-
//
|
|
1227
|
-
// the target;
|
|
1228
|
-
// party.
|
|
1242
|
+
// Never forward URL-embedded credentials (user:password@host) to a hosted
|
|
1243
|
+
// scrape API. The built-in fetcher turns them into a Basic-auth header to
|
|
1244
|
+
// the target; hosted scrapers can't, so reject rather than leak them to a
|
|
1245
|
+
// third party.
|
|
1229
1246
|
if (parsedUrl?.username || parsedUrl?.password) {
|
|
1230
|
-
return
|
|
1247
|
+
return hostedScrapeErrorResult(
|
|
1231
1248
|
safeRequestedUrl,
|
|
1232
1249
|
startedAt,
|
|
1233
|
-
|
|
1250
|
+
`URLs with embedded credentials are not supported by the ${options.displayName} provider. Remove the user:password@ portion of the URL.`,
|
|
1251
|
+
options.provider,
|
|
1234
1252
|
);
|
|
1235
1253
|
}
|
|
1236
1254
|
|
|
@@ -1255,16 +1273,21 @@ export async function executeFirecrawlScrape(
|
|
|
1255
1273
|
timeout: timeoutSeconds * 1000,
|
|
1256
1274
|
};
|
|
1257
1275
|
|
|
1276
|
+
const headers: Record<string, string> = {
|
|
1277
|
+
"Content-Type": "application/json",
|
|
1278
|
+
"X-Client-Source": "vellum-assistant",
|
|
1279
|
+
};
|
|
1280
|
+
const trimmedKey = options.apiKey.trim();
|
|
1281
|
+
if (trimmedKey.length > 0) {
|
|
1282
|
+
headers.Authorization = `Bearer ${trimmedKey}`;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1258
1285
|
for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
|
|
1259
1286
|
let response: Response;
|
|
1260
1287
|
try {
|
|
1261
|
-
response = await fetch(
|
|
1288
|
+
response = await fetch(options.endpoint, {
|
|
1262
1289
|
method: "POST",
|
|
1263
|
-
headers
|
|
1264
|
-
"Content-Type": "application/json",
|
|
1265
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
1266
|
-
"X-Client-Source": "vellum-assistant",
|
|
1267
|
-
},
|
|
1290
|
+
headers,
|
|
1268
1291
|
body: JSON.stringify(requestBody),
|
|
1269
1292
|
signal: options.signal,
|
|
1270
1293
|
});
|
|
@@ -1273,17 +1296,19 @@ export async function executeFirecrawlScrape(
|
|
|
1273
1296
|
options.signal?.aborted ||
|
|
1274
1297
|
(err instanceof Error && err.name === "AbortError")
|
|
1275
1298
|
) {
|
|
1276
|
-
return
|
|
1299
|
+
return hostedScrapeErrorResult(
|
|
1277
1300
|
safeRequestedUrl,
|
|
1278
1301
|
startedAt,
|
|
1279
1302
|
"web fetch was cancelled",
|
|
1303
|
+
options.provider,
|
|
1280
1304
|
);
|
|
1281
1305
|
}
|
|
1282
1306
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1283
|
-
return
|
|
1307
|
+
return hostedScrapeErrorResult(
|
|
1284
1308
|
safeRequestedUrl,
|
|
1285
1309
|
startedAt,
|
|
1286
|
-
|
|
1310
|
+
`${options.displayName} scrape failed: ${msg}`,
|
|
1311
|
+
options.provider,
|
|
1287
1312
|
);
|
|
1288
1313
|
}
|
|
1289
1314
|
|
|
@@ -1292,24 +1317,26 @@ export async function executeFirecrawlScrape(
|
|
|
1292
1317
|
try {
|
|
1293
1318
|
json = (await response.json()) as FirecrawlScrapeResponse;
|
|
1294
1319
|
} catch {
|
|
1295
|
-
return
|
|
1320
|
+
return hostedScrapeErrorResult(
|
|
1296
1321
|
safeRequestedUrl,
|
|
1297
1322
|
startedAt,
|
|
1298
|
-
|
|
1323
|
+
`${options.displayName} scrape returned an invalid JSON payload.`,
|
|
1324
|
+
options.provider,
|
|
1299
1325
|
response.status,
|
|
1300
1326
|
);
|
|
1301
1327
|
}
|
|
1302
1328
|
const data = json.data ?? {};
|
|
1303
|
-
const
|
|
1329
|
+
const scrapeMeta = data.metadata ?? {};
|
|
1304
1330
|
// A 200 can still carry a payload-level failure (success:false, a
|
|
1305
1331
|
// top-level error, or a per-page error in data.metadata). Surface it
|
|
1306
1332
|
// instead of treating an empty body as a successful "no content" scrape.
|
|
1307
|
-
const payloadError = json.error ??
|
|
1333
|
+
const payloadError = json.error ?? scrapeMeta.error;
|
|
1308
1334
|
if (json.success === false || payloadError) {
|
|
1309
|
-
return
|
|
1335
|
+
return hostedScrapeErrorResult(
|
|
1310
1336
|
safeRequestedUrl,
|
|
1311
1337
|
startedAt,
|
|
1312
|
-
payloadError ??
|
|
1338
|
+
payloadError ?? `${options.displayName} scrape failed.`,
|
|
1339
|
+
options.provider,
|
|
1313
1340
|
response.status,
|
|
1314
1341
|
);
|
|
1315
1342
|
}
|
|
@@ -1322,15 +1349,15 @@ export async function executeFirecrawlScrape(
|
|
|
1322
1349
|
const sliced = processed.slice(safeStart, safeEnd);
|
|
1323
1350
|
const bytesRead = Buffer.byteLength(processed, "utf8");
|
|
1324
1351
|
|
|
1325
|
-
const finalUrlRaw =
|
|
1352
|
+
const finalUrlRaw = scrapeMeta.url ?? scrapeMeta.sourceURL ?? targetUrl;
|
|
1326
1353
|
const finalUrl = sanitizeUrlStringForOutput(finalUrlRaw);
|
|
1327
|
-
const status =
|
|
1328
|
-
const contentType =
|
|
1354
|
+
const status = scrapeMeta.statusCode ?? 200;
|
|
1355
|
+
const contentType = scrapeMeta.contentType ?? "text/markdown";
|
|
1329
1356
|
|
|
1330
1357
|
const notices: string[] = [];
|
|
1331
1358
|
const warning = data.warning ?? json.warning;
|
|
1332
1359
|
if (warning) {
|
|
1333
|
-
notices.push(
|
|
1360
|
+
notices.push(`${options.displayName}: ${warning}`);
|
|
1334
1361
|
}
|
|
1335
1362
|
if (safeEnd < processed.length) {
|
|
1336
1363
|
notices.push(`Output truncated by max_chars=${maxChars}.`);
|
|
@@ -1352,8 +1379,8 @@ export async function executeFirecrawlScrape(
|
|
|
1352
1379
|
startIndex: safeStart,
|
|
1353
1380
|
endIndex: safeEnd,
|
|
1354
1381
|
content: sliced,
|
|
1355
|
-
title:
|
|
1356
|
-
description:
|
|
1382
|
+
title: scrapeMeta.title,
|
|
1383
|
+
description: scrapeMeta.description,
|
|
1357
1384
|
notices,
|
|
1358
1385
|
raw: false,
|
|
1359
1386
|
markdown: true,
|
|
@@ -1363,13 +1390,13 @@ export async function executeFirecrawlScrape(
|
|
|
1363
1390
|
const meta: WebFetchMetadata = {
|
|
1364
1391
|
url: safeRequestedUrl,
|
|
1365
1392
|
finalUrl,
|
|
1366
|
-
provider:
|
|
1393
|
+
provider: options.provider,
|
|
1367
1394
|
status,
|
|
1368
1395
|
contentType,
|
|
1369
1396
|
byteCount: bytesRead,
|
|
1370
1397
|
charCount: sliced.length,
|
|
1371
1398
|
truncated: safeEnd < processed.length,
|
|
1372
|
-
title:
|
|
1399
|
+
title: scrapeMeta.title,
|
|
1373
1400
|
domain: finalDomain,
|
|
1374
1401
|
faviconUrl: faviconUrlForDomain(finalDomain),
|
|
1375
1402
|
redirectCount: 0,
|
|
@@ -1387,10 +1414,11 @@ export async function executeFirecrawlScrape(
|
|
|
1387
1414
|
const bodyText = await response.text();
|
|
1388
1415
|
|
|
1389
1416
|
if (response.status === 401 || response.status === 403) {
|
|
1390
|
-
return
|
|
1417
|
+
return hostedScrapeErrorResult(
|
|
1391
1418
|
safeRequestedUrl,
|
|
1392
1419
|
startedAt,
|
|
1393
|
-
|
|
1420
|
+
`Invalid or expired ${options.displayName} API key`,
|
|
1421
|
+
options.provider,
|
|
1394
1422
|
response.status,
|
|
1395
1423
|
);
|
|
1396
1424
|
}
|
|
@@ -1402,39 +1430,77 @@ export async function executeFirecrawlScrape(
|
|
|
1402
1430
|
DEFAULT_BASE_DELAY_MS,
|
|
1403
1431
|
);
|
|
1404
1432
|
log.warn(
|
|
1405
|
-
{ attempt: attempt + 1, delayMs },
|
|
1406
|
-
|
|
1433
|
+
{ attempt: attempt + 1, delayMs, provider: options.provider },
|
|
1434
|
+
`${options.displayName} scrape rate limited, retrying`,
|
|
1407
1435
|
);
|
|
1408
1436
|
await sleep(delayMs);
|
|
1409
1437
|
continue;
|
|
1410
1438
|
}
|
|
1411
1439
|
|
|
1412
1440
|
log.warn(
|
|
1413
|
-
{
|
|
1414
|
-
|
|
1441
|
+
{
|
|
1442
|
+
status: response.status,
|
|
1443
|
+
body: safeStringSlice(bodyText, 0, 200),
|
|
1444
|
+
provider: options.provider,
|
|
1445
|
+
},
|
|
1446
|
+
`${options.displayName} scrape API error`,
|
|
1415
1447
|
);
|
|
1416
1448
|
const errorMessage =
|
|
1417
1449
|
response.status === 429
|
|
1418
|
-
?
|
|
1450
|
+
? `${options.displayName} scrape rate limit exceeded after retries. Try again shortly.`
|
|
1419
1451
|
: response.status === 402
|
|
1420
|
-
?
|
|
1421
|
-
:
|
|
1422
|
-
return
|
|
1452
|
+
? `${options.displayName} scrape failed: account balance/credits exhausted.`
|
|
1453
|
+
: `${options.displayName} scrape API returned status ${response.status}`;
|
|
1454
|
+
return hostedScrapeErrorResult(
|
|
1423
1455
|
safeRequestedUrl,
|
|
1424
1456
|
startedAt,
|
|
1425
1457
|
errorMessage,
|
|
1458
|
+
options.provider,
|
|
1426
1459
|
response.status,
|
|
1427
1460
|
);
|
|
1428
1461
|
}
|
|
1429
1462
|
|
|
1430
|
-
return
|
|
1463
|
+
return hostedScrapeErrorResult(
|
|
1431
1464
|
safeRequestedUrl,
|
|
1432
1465
|
startedAt,
|
|
1433
|
-
|
|
1466
|
+
`${options.displayName} scrape rate limit exceeded after retries. Try again shortly.`,
|
|
1467
|
+
options.provider,
|
|
1434
1468
|
429,
|
|
1435
1469
|
);
|
|
1436
1470
|
}
|
|
1437
1471
|
|
|
1472
|
+
export async function executeFirecrawlScrape(
|
|
1473
|
+
input: Record<string, unknown>,
|
|
1474
|
+
options: { apiKey: string; signal?: AbortSignal },
|
|
1475
|
+
): Promise<ToolExecutionResult> {
|
|
1476
|
+
return executeFirecrawlCompatScrape(input, {
|
|
1477
|
+
apiKey: options.apiKey,
|
|
1478
|
+
endpoint: FIRECRAWL_SCRAPE_API_URL,
|
|
1479
|
+
provider: "firecrawl",
|
|
1480
|
+
displayName: "Firecrawl",
|
|
1481
|
+
signal: options.signal,
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export async function executeFastcrwScrape(
|
|
1486
|
+
input: Record<string, unknown>,
|
|
1487
|
+
options: { apiKey: string; signal?: AbortSignal },
|
|
1488
|
+
): Promise<ToolExecutionResult> {
|
|
1489
|
+
const apiBase = getConfig().services["web-fetch"]?.apiBase;
|
|
1490
|
+
const endpoint = resolveProviderApiUrl(
|
|
1491
|
+
apiBase,
|
|
1492
|
+
FASTCRW_SCRAPE_PATH,
|
|
1493
|
+
FASTCRW_DEFAULT_API_BASE,
|
|
1494
|
+
);
|
|
1495
|
+
return executeFirecrawlCompatScrape(input, {
|
|
1496
|
+
apiKey: options.apiKey,
|
|
1497
|
+
endpoint,
|
|
1498
|
+
provider: "fastcrw",
|
|
1499
|
+
displayName: "fastCRW",
|
|
1500
|
+
signal: options.signal,
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1438
1504
|
export const webFetchTool = {
|
|
1439
1505
|
name: "web_fetch",
|
|
1440
1506
|
description:
|
|
@@ -1482,19 +1548,33 @@ export const webFetchTool = {
|
|
|
1482
1548
|
input: Record<string, unknown>,
|
|
1483
1549
|
context: ToolContext,
|
|
1484
1550
|
): Promise<ToolExecutionResult> {
|
|
1551
|
+
const fetchProvider = getWebFetchProvider();
|
|
1485
1552
|
if (
|
|
1486
|
-
|
|
1487
|
-
(await
|
|
1553
|
+
(fetchProvider === "firecrawl" || fetchProvider === "fastcrw") &&
|
|
1554
|
+
(await canRouteToHostedScraper(input))
|
|
1488
1555
|
) {
|
|
1489
|
-
const apiKey = await getProviderKeyAsync("
|
|
1490
|
-
|
|
1491
|
-
|
|
1556
|
+
const apiKey = (await getProviderKeyAsync(fetchProvider)) ?? "";
|
|
1557
|
+
const webFetchApiBase = getConfig().services["web-fetch"]?.apiBase;
|
|
1558
|
+
const allowsKeylessSelfHost =
|
|
1559
|
+
fetchProvider === "fastcrw" &&
|
|
1560
|
+
typeof webFetchApiBase === "string" &&
|
|
1561
|
+
webFetchApiBase.trim().length > 0;
|
|
1562
|
+
|
|
1563
|
+
if (apiKey || allowsKeylessSelfHost) {
|
|
1564
|
+
if (fetchProvider === "firecrawl") {
|
|
1565
|
+
return executeFirecrawlScrape(input, {
|
|
1566
|
+
apiKey,
|
|
1567
|
+
signal: context.signal,
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return executeFastcrwScrape(input, {
|
|
1492
1571
|
apiKey,
|
|
1493
1572
|
signal: context.signal,
|
|
1494
1573
|
});
|
|
1495
1574
|
}
|
|
1496
1575
|
log.info(
|
|
1497
|
-
|
|
1576
|
+
{ provider: fetchProvider },
|
|
1577
|
+
"web_fetch BYOK provider has no API key configured; falling back to the built-in fetcher",
|
|
1498
1578
|
);
|
|
1499
1579
|
// Fall through to the built-in fetcher.
|
|
1500
1580
|
}
|