@vellumai/assistant 0.12.2-dev.202609190043.ee5ba34 → 0.12.2-dev.202609191222.757d196
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/openapi.yaml +4 -0
- package/package.json +1 -1
- package/src/api/events/tool-result.ts +2 -0
- package/src/bundler/bundle-signer.ts +1 -3
- package/src/calls/media-stream-audio-transcode.ts +1 -3
- package/src/cli/lib/publish-plugin.ts +1 -2
- package/src/config/schemas/services.ts +5 -3
- package/src/daemon/message-types/web-activity.ts +7 -2
- package/src/messaging/providers/outlook/client.ts +16 -22
- package/src/oauth/connection-resolver.ts +1 -2
- package/src/persistence/conversation-queries.ts +3 -3
- package/src/plugins/defaults/memory/src/memory-eval-routes.ts +2 -3
- package/src/providers/__tests__/search-provider-catalog.test.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +11 -0
- package/src/providers/ratelimit.ts +1 -3
- package/src/providers/search-provider-catalog.ts +12 -0
- package/src/runtime/routes/credential-routes.ts +1 -2
- package/src/runtime/routes/email-routes.ts +3 -7
- package/src/runtime/routes/watcher-routes.ts +1 -3
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +1 -8
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +250 -0
- package/src/tools/network/__tests__/web-search.test.ts +92 -0
- package/src/tools/network/web-fetch.ts +272 -5
- package/src/tools/network/web-search.ts +228 -4
package/openapi.yaml
CHANGED
|
@@ -21217,6 +21217,7 @@ paths:
|
|
|
21217
21217
|
- firecrawl
|
|
21218
21218
|
- fastcrw
|
|
21219
21219
|
- searxng
|
|
21220
|
+
- tinyfish
|
|
21220
21221
|
resultCount:
|
|
21221
21222
|
type: number
|
|
21222
21223
|
durationMs:
|
|
@@ -21270,6 +21271,7 @@ paths:
|
|
|
21270
21271
|
- default
|
|
21271
21272
|
- firecrawl
|
|
21272
21273
|
- fastcrw
|
|
21274
|
+
- tinyfish
|
|
21273
21275
|
status:
|
|
21274
21276
|
type: number
|
|
21275
21277
|
contentType:
|
|
@@ -21735,6 +21737,7 @@ paths:
|
|
|
21735
21737
|
- firecrawl
|
|
21736
21738
|
- fastcrw
|
|
21737
21739
|
- searxng
|
|
21740
|
+
- tinyfish
|
|
21738
21741
|
resultCount:
|
|
21739
21742
|
type: number
|
|
21740
21743
|
durationMs:
|
|
@@ -21788,6 +21791,7 @@ paths:
|
|
|
21788
21791
|
- default
|
|
21789
21792
|
- firecrawl
|
|
21790
21793
|
- fastcrw
|
|
21794
|
+
- tinyfish
|
|
21791
21795
|
status:
|
|
21792
21796
|
type: number
|
|
21793
21797
|
contentType:
|
package/package.json
CHANGED
|
@@ -53,6 +53,7 @@ export const WebSearchProviderIdSchema = z.enum([
|
|
|
53
53
|
"firecrawl",
|
|
54
54
|
"fastcrw",
|
|
55
55
|
"searxng",
|
|
56
|
+
"tinyfish",
|
|
56
57
|
]);
|
|
57
58
|
|
|
58
59
|
export type WebSearchProviderId = z.infer<typeof WebSearchProviderIdSchema>;
|
|
@@ -61,6 +62,7 @@ export const WebFetchProviderIdSchema = z.enum([
|
|
|
61
62
|
"default",
|
|
62
63
|
"firecrawl",
|
|
63
64
|
"fastcrw",
|
|
65
|
+
"tinyfish",
|
|
64
66
|
]);
|
|
65
67
|
|
|
66
68
|
export type WebFetchProviderId = z.infer<typeof WebFetchProviderIdSchema>;
|
|
@@ -115,7 +115,7 @@ export async function signBundle(
|
|
|
115
115
|
const { signature, keyId } = await requestSignature(canonicalPayload);
|
|
116
116
|
|
|
117
117
|
// 5. Build SignatureJson
|
|
118
|
-
|
|
118
|
+
return {
|
|
119
119
|
algorithm: "ed25519",
|
|
120
120
|
signer: {
|
|
121
121
|
key_id: keyId,
|
|
@@ -124,6 +124,4 @@ export async function signBundle(
|
|
|
124
124
|
content_hashes: contentHashes,
|
|
125
125
|
signature,
|
|
126
126
|
};
|
|
127
|
-
|
|
128
|
-
return signatureJson;
|
|
129
127
|
}
|
|
@@ -68,9 +68,7 @@ function linearToMulaw(sample: number): number {
|
|
|
68
68
|
// Determine segment
|
|
69
69
|
const exponent = segmentSearch(sample);
|
|
70
70
|
const mantissa = (sample >> (exponent + 3)) & 0x0f;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return mulawByte;
|
|
71
|
+
return ~(sign | (exponent << 4) | mantissa) & 0xff;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
function segmentSearch(val: number): number {
|
|
@@ -83,8 +83,9 @@ const WebSearchServiceSchema = z.object({
|
|
|
83
83
|
.enum(VALID_WEB_SEARCH_PROVIDERS)
|
|
84
84
|
.default("inference-provider-native"),
|
|
85
85
|
// Origin for providers that support a custom API base (e.g. fastCRW,
|
|
86
|
-
// SearXNG). Empty / omitted uses the provider's cloud default
|
|
87
|
-
// exists. SearXNG has no cloud default and requires a user-supplied
|
|
86
|
+
// SearXNG, TinyFish). Empty / omitted uses the provider's cloud default
|
|
87
|
+
// when one exists. SearXNG has no cloud default and requires a user-supplied
|
|
88
|
+
// URL.
|
|
88
89
|
apiBase: z.string().optional(),
|
|
89
90
|
});
|
|
90
91
|
|
|
@@ -98,7 +99,8 @@ const WebFetchServiceSchema = z.object({
|
|
|
98
99
|
// `firecrawl`) scrape via their hosted API and reuse the same stored key as
|
|
99
100
|
// their web-search counterpart.
|
|
100
101
|
provider: z.enum(VALID_WEB_FETCH_PROVIDERS).default("default"),
|
|
101
|
-
// Origin for providers that support a custom API base (e.g. fastCRW
|
|
102
|
+
// Origin for providers that support a custom API base (e.g. fastCRW,
|
|
103
|
+
// TinyFish).
|
|
102
104
|
// Empty / omitted uses the provider's cloud default.
|
|
103
105
|
apiBase: z.string().optional(),
|
|
104
106
|
});
|
|
@@ -13,10 +13,15 @@ export type WebSearchProviderId =
|
|
|
13
13
|
| "keenable"
|
|
14
14
|
| "firecrawl"
|
|
15
15
|
| "fastcrw"
|
|
16
|
-
| "searxng"
|
|
16
|
+
| "searxng"
|
|
17
|
+
| "tinyfish";
|
|
17
18
|
|
|
18
19
|
/** Provider that backed a `web_fetch` call. `default` is the built-in fetcher. */
|
|
19
|
-
export type WebFetchProviderId =
|
|
20
|
+
export type WebFetchProviderId =
|
|
21
|
+
| "default"
|
|
22
|
+
| "firecrawl"
|
|
23
|
+
| "fastcrw"
|
|
24
|
+
| "tinyfish";
|
|
20
25
|
|
|
21
26
|
export interface WebSearchResultItem {
|
|
22
27
|
rank: number; // 1-indexed
|
|
@@ -69,28 +69,22 @@ async function request<T>(
|
|
|
69
69
|
const canRetry = isIdempotent(method);
|
|
70
70
|
|
|
71
71
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
body: options?.body ? JSON.parse(options.body as string) : undefined,
|
|
89
|
-
});
|
|
90
|
-
} catch (err) {
|
|
91
|
-
// Network-level errors from connection.request() are not retryable
|
|
92
|
-
throw err;
|
|
93
|
-
}
|
|
72
|
+
const extraHeaders =
|
|
73
|
+
options?.headers &&
|
|
74
|
+
typeof options.headers === "object" &&
|
|
75
|
+
!Array.isArray(options.headers)
|
|
76
|
+
? (options.headers as Record<string, string>)
|
|
77
|
+
: {};
|
|
78
|
+
const resp: OAuthConnectionResponse = await connection.request({
|
|
79
|
+
method,
|
|
80
|
+
path,
|
|
81
|
+
query,
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
...extraHeaders,
|
|
85
|
+
},
|
|
86
|
+
body: options?.body ? JSON.parse(options.body as string) : undefined,
|
|
87
|
+
});
|
|
94
88
|
|
|
95
89
|
if (resp.status < 200 || resp.status >= 300) {
|
|
96
90
|
if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
|
|
@@ -467,12 +467,11 @@ async function fetchPlatformConnections(options: {
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
const body = (await response.json()) as unknown;
|
|
470
|
-
|
|
470
|
+
return (
|
|
471
471
|
Array.isArray(body)
|
|
472
472
|
? body
|
|
473
473
|
: ((body as Record<string, unknown>).results ?? [])
|
|
474
474
|
) as PlatformConnectionEntry[];
|
|
475
|
-
return connections;
|
|
476
475
|
}
|
|
477
476
|
|
|
478
477
|
/**
|
|
@@ -1440,9 +1440,9 @@ function buildExcerptFromText(
|
|
|
1440
1440
|
text.length,
|
|
1441
1441
|
match.index + match.length + EXCERPT_WINDOW,
|
|
1442
1442
|
);
|
|
1443
|
-
|
|
1443
|
+
return (
|
|
1444
1444
|
(start > 0 ? "\u2026" : "") +
|
|
1445
1445
|
text.slice(start, end).replace(/\s+/g, " ").trim() +
|
|
1446
|
-
(end < text.length ? "\u2026" : "")
|
|
1447
|
-
|
|
1446
|
+
(end < text.length ? "\u2026" : "")
|
|
1447
|
+
);
|
|
1448
1448
|
}
|
|
@@ -23,7 +23,7 @@ import type { RouteDefinition } from "../../../../runtime/routes/types.js";
|
|
|
23
23
|
import { getLogger } from "../logging.js";
|
|
24
24
|
import { getWorkspaceDir } from "../paths.js";
|
|
25
25
|
import { runMemoryEval } from "../v3-eval/eval-packets.js";
|
|
26
|
-
import {
|
|
26
|
+
import { tallyVerdicts } from "../v3-eval/eval-tally.js";
|
|
27
27
|
|
|
28
28
|
const log = getLogger("memory-eval-routes");
|
|
29
29
|
|
|
@@ -138,12 +138,11 @@ export type MemoryEvalTallyResult = z.infer<typeof MemoryEvalTallyResultSchema>;
|
|
|
138
138
|
*/
|
|
139
139
|
export function handleMemoryEvalTally(body: unknown): MemoryEvalTallyResult {
|
|
140
140
|
const params = MemoryEvalTallyParamsSchema.parse(body ?? {});
|
|
141
|
-
|
|
141
|
+
return tallyVerdicts(
|
|
142
142
|
params.verdicts,
|
|
143
143
|
params.key,
|
|
144
144
|
params.alpha !== undefined ? { alpha: params.alpha } : {},
|
|
145
145
|
);
|
|
146
|
-
return result;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
const WRITE_POLICY: RoutePolicy = {
|
|
@@ -95,4 +95,13 @@ describe("search-provider-catalog", () => {
|
|
|
95
95
|
expect(searxng?.defaultApiBase).toBeUndefined();
|
|
96
96
|
expect(searxng?.envVar).toBe("SEARXNG_API_KEY");
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
test("TinyFish is a keyed BYOK provider with a cloud API base", () => {
|
|
100
|
+
const tinyfish = getSearchProvider("tinyfish");
|
|
101
|
+
expect(tinyfish?.kind).toBe("byok");
|
|
102
|
+
expect(tinyfish?.keyless).toBeUndefined();
|
|
103
|
+
expect(tinyfish?.supportsApiBase).toBe(true);
|
|
104
|
+
expect(tinyfish?.defaultApiBase).toBe("https://api.search.tinyfish.ai");
|
|
105
|
+
expect(tinyfish?.envVar).toBe("TINYFISH_API_KEY");
|
|
106
|
+
});
|
|
98
107
|
});
|
|
@@ -84,6 +84,17 @@ export const FETCH_PROVIDER_CATALOG: readonly FetchProviderCatalogEntry[] = [
|
|
|
84
84
|
supportsApiBase: true,
|
|
85
85
|
defaultApiBase: "https://api.fastcrw.com",
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
id: "tinyfish",
|
|
89
|
+
displayName: "TinyFish",
|
|
90
|
+
kind: "byok",
|
|
91
|
+
apiKeyPrefix: "TinyFish API key...",
|
|
92
|
+
envVar: "TINYFISH_API_KEY",
|
|
93
|
+
secretKey: "tinyfish",
|
|
94
|
+
privacyPolicyUrl: "https://www.tinyfish.ai/privacy-policy",
|
|
95
|
+
supportsApiBase: true,
|
|
96
|
+
defaultApiBase: "https://api.fetch.tinyfish.ai",
|
|
97
|
+
},
|
|
87
98
|
];
|
|
88
99
|
|
|
89
100
|
/** Provider ids accepted by the web-fetch config schema. */
|
|
@@ -62,9 +62,7 @@ export class RateLimitProvider implements Provider {
|
|
|
62
62
|
// calls from bypassing the rate limit during the async gap.
|
|
63
63
|
this.recordRequest();
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return response;
|
|
65
|
+
return this.inner.sendMessage(messages, options);
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
private enforceRequestRate(): void {
|
|
@@ -175,6 +175,18 @@ export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
|
|
|
175
175
|
privacyPolicyUrl: "https://docs.searxng.org/own-instance.html",
|
|
176
176
|
supportsApiBase: true,
|
|
177
177
|
},
|
|
178
|
+
{
|
|
179
|
+
id: "tinyfish",
|
|
180
|
+
displayName: "TinyFish",
|
|
181
|
+
kind: "byok",
|
|
182
|
+
apiKeyPrefix: "TinyFish API key...",
|
|
183
|
+
envVar: "TINYFISH_API_KEY",
|
|
184
|
+
secretKey: "tinyfish",
|
|
185
|
+
fallbackOrder: 8,
|
|
186
|
+
privacyPolicyUrl: "https://www.tinyfish.ai/privacy-policy",
|
|
187
|
+
supportsApiBase: true,
|
|
188
|
+
defaultApiBase: "https://api.search.tinyfish.ai",
|
|
189
|
+
},
|
|
178
190
|
];
|
|
179
191
|
|
|
180
192
|
/** Provider ids accepted by the web-search config schema. */
|
|
@@ -607,8 +607,7 @@ async function handleCredentialsDelete({ body }: RouteHandlerArgs) {
|
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
async function handleCredentialsStatus() {
|
|
610
|
-
|
|
611
|
-
return info;
|
|
610
|
+
return getActiveBackendInfoAsync();
|
|
612
611
|
}
|
|
613
612
|
|
|
614
613
|
// ---------------------------------------------------------------------------
|
|
@@ -169,7 +169,7 @@ async function handleEmailStatus(_args: RouteHandlerArgs) {
|
|
|
169
169
|
);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
return (await statusResponse.json()) as {
|
|
173
173
|
address: string;
|
|
174
174
|
status: string;
|
|
175
175
|
created_at: string;
|
|
@@ -181,8 +181,6 @@ async function handleEmailStatus(_args: RouteHandlerArgs) {
|
|
|
181
181
|
received_this_month: number;
|
|
182
182
|
};
|
|
183
183
|
};
|
|
184
|
-
|
|
185
|
-
return statusData;
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
async function handleEmailList({ queryParams = {} }: RouteHandlerArgs) {
|
|
@@ -239,8 +237,7 @@ async function handleEmailDownload({ queryParams = {} }: RouteHandlerArgs) {
|
|
|
239
237
|
throw new RouteError(String(detail), "DOWNLOAD_FAILED", response.status);
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
return msg;
|
|
240
|
+
return response.json();
|
|
244
241
|
}
|
|
245
242
|
|
|
246
243
|
async function handleEmailSend({ body = {} }: RouteHandlerArgs) {
|
|
@@ -323,11 +320,10 @@ async function handleEmailSend({ body = {} }: RouteHandlerArgs) {
|
|
|
323
320
|
throw new BadRequestError(String(detail));
|
|
324
321
|
}
|
|
325
322
|
|
|
326
|
-
|
|
323
|
+
return (await response.json()) as {
|
|
327
324
|
delivery_id: string;
|
|
328
325
|
status: string;
|
|
329
326
|
};
|
|
330
|
-
return data;
|
|
331
327
|
}
|
|
332
328
|
|
|
333
329
|
// ── Attachment handlers ───────────────────────────────────────────────
|
|
@@ -86,7 +86,7 @@ function handleWatcherCreate({ body = {} }: RouteHandlerArgs) {
|
|
|
86
86
|
const credentialService =
|
|
87
87
|
credentialServiceOverride ?? provider.requiredCredentialService;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
return createWatcher({
|
|
90
90
|
name,
|
|
91
91
|
providerId,
|
|
92
92
|
actionPrompt,
|
|
@@ -94,8 +94,6 @@ function handleWatcherCreate({ body = {} }: RouteHandlerArgs) {
|
|
|
94
94
|
pollIntervalMs,
|
|
95
95
|
configJson: config ? JSON.stringify(config) : null,
|
|
96
96
|
});
|
|
97
|
-
|
|
98
|
-
return watcher;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
function handleWatcherList({ body = {} }: RouteHandlerArgs) {
|
|
@@ -406,14 +406,7 @@ export class CdpInspectClient implements ScopedCdpClient {
|
|
|
406
406
|
httpError: unknown,
|
|
407
407
|
signal: AbortSignal,
|
|
408
408
|
): Promise<AttachedSession> {
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
wsUrl = this.helpers.buildBrowserWsUrl(host, port);
|
|
412
|
-
} catch (err) {
|
|
413
|
-
// buildBrowserWsUrl enforces loopback — if it throws, the host
|
|
414
|
-
// is non-loopback and we should not attempt any WS connection.
|
|
415
|
-
throw err;
|
|
416
|
-
}
|
|
409
|
+
const wsUrl = this.helpers.buildBrowserWsUrl(host, port);
|
|
417
410
|
|
|
418
411
|
let transport: CdpWsTransport;
|
|
419
412
|
try {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { setConfig } from "../../../__tests__/helpers/set-config.js";
|
|
4
|
+
|
|
5
|
+
let mockTinyfishSecureKey: string | undefined;
|
|
6
|
+
let mockResolveAddresses: string[] = [];
|
|
7
|
+
|
|
8
|
+
function seedWebFetch(provider: string, apiBase?: string): void {
|
|
9
|
+
const entry: Record<string, string> = { provider };
|
|
10
|
+
if (apiBase !== undefined) {
|
|
11
|
+
entry.apiBase = apiBase;
|
|
12
|
+
}
|
|
13
|
+
setConfig("services", { "web-fetch": entry });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
17
|
+
getProviderKeyAsync: async (provider: string) =>
|
|
18
|
+
provider === "tinyfish" ? mockTinyfishSecureKey : undefined,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const realLogger = await import("../../../util/logger.js");
|
|
22
|
+
mock.module("../../../util/logger.js", () => ({
|
|
23
|
+
...realLogger,
|
|
24
|
+
getLogger: () =>
|
|
25
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module("../../../permissions/types.js", () => ({
|
|
29
|
+
RiskLevel: { Low: "low", Medium: "medium", High: "high" },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const realUrlSafety = await import("../url-safety.js");
|
|
33
|
+
mock.module("../url-safety.js", () => ({
|
|
34
|
+
...realUrlSafety,
|
|
35
|
+
resolveHostAddresses: async () => mockResolveAddresses,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const { executeTinyfishFetch, webFetchTool } = await import("../web-fetch.js");
|
|
39
|
+
|
|
40
|
+
function fetchResponse(body: unknown, status = 200): Response {
|
|
41
|
+
return new Response(JSON.stringify(body), {
|
|
42
|
+
status,
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("executeTinyfishFetch", () => {
|
|
48
|
+
let originalFetch: typeof globalThis.fetch;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
originalFetch = globalThis.fetch;
|
|
52
|
+
seedWebFetch("tinyfish");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
globalThis.fetch = originalFetch;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("posts the URL and returns markdown with metadata", async () => {
|
|
60
|
+
let capturedUrl = "";
|
|
61
|
+
let capturedBody: Record<string, unknown> = {};
|
|
62
|
+
let capturedApiKey = "";
|
|
63
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
64
|
+
capturedUrl = url;
|
|
65
|
+
capturedBody = JSON.parse(String(init?.body));
|
|
66
|
+
capturedApiKey = new Headers(init?.headers).get("x-api-key") ?? "";
|
|
67
|
+
return fetchResponse({
|
|
68
|
+
results: [
|
|
69
|
+
{
|
|
70
|
+
url: "https://example.com/",
|
|
71
|
+
final_url: "https://www.example.com/",
|
|
72
|
+
title: "Example",
|
|
73
|
+
description: "An example page",
|
|
74
|
+
text: "# Example\n\nHello from TinyFish.",
|
|
75
|
+
format: "markdown",
|
|
76
|
+
latency_ms: 120,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
errors: [],
|
|
80
|
+
});
|
|
81
|
+
}) as typeof fetch;
|
|
82
|
+
|
|
83
|
+
const result = await executeTinyfishFetch(
|
|
84
|
+
{ url: "https://example.com", timeout_seconds: 45 },
|
|
85
|
+
{ apiKey: "tf_test" },
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result.isError).toBe(false);
|
|
89
|
+
expect(capturedUrl).toBe("https://api.fetch.tinyfish.ai/");
|
|
90
|
+
expect(capturedBody).toEqual({
|
|
91
|
+
urls: ["https://example.com/"],
|
|
92
|
+
format: "markdown",
|
|
93
|
+
per_url_timeout_ms: 45_000,
|
|
94
|
+
});
|
|
95
|
+
expect(capturedApiKey).toBe("tf_test");
|
|
96
|
+
expect(result.content).toContain("Hello from TinyFish.");
|
|
97
|
+
expect(result.content).toContain("Title: Example");
|
|
98
|
+
expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
|
|
99
|
+
expect(result.activityMetadata?.webFetch?.finalUrl).toBe(
|
|
100
|
+
"https://www.example.com/",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("uses a custom API base and applies character windows", async () => {
|
|
105
|
+
seedWebFetch("tinyfish", "https://fetch.example.com/api/");
|
|
106
|
+
let capturedUrl = "";
|
|
107
|
+
globalThis.fetch = (async (url: string) => {
|
|
108
|
+
capturedUrl = url;
|
|
109
|
+
return fetchResponse({
|
|
110
|
+
results: [
|
|
111
|
+
{
|
|
112
|
+
url: "https://example.com/",
|
|
113
|
+
final_url: "https://example.com/",
|
|
114
|
+
text: "0123456789",
|
|
115
|
+
format: "markdown",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
errors: [],
|
|
119
|
+
});
|
|
120
|
+
}) as typeof fetch;
|
|
121
|
+
|
|
122
|
+
const result = await executeTinyfishFetch(
|
|
123
|
+
{ url: "https://example.com", start_index: 2, max_chars: 4 },
|
|
124
|
+
{ apiKey: "tf_test" },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(capturedUrl).toBe("https://fetch.example.com/api/");
|
|
128
|
+
expect(result.content).toContain("Character Window: 2-6 of 10");
|
|
129
|
+
expect(result.content).toContain("2345");
|
|
130
|
+
expect(result.activityMetadata?.webFetch?.truncated).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("surfaces per-URL errors from a successful HTTP response", async () => {
|
|
134
|
+
globalThis.fetch = (async () =>
|
|
135
|
+
fetchResponse({
|
|
136
|
+
results: [],
|
|
137
|
+
errors: [
|
|
138
|
+
{
|
|
139
|
+
url: "https://example.com/",
|
|
140
|
+
error: "bot_blocked",
|
|
141
|
+
status: 403,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
})) as unknown as typeof fetch;
|
|
145
|
+
|
|
146
|
+
const result = await executeTinyfishFetch(
|
|
147
|
+
{ url: "https://example.com" },
|
|
148
|
+
{ apiKey: "tf_test" },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(result.isError).toBe(true);
|
|
152
|
+
expect(result.content).toContain("bot_blocked");
|
|
153
|
+
expect(result.content).toContain("HTTP 403");
|
|
154
|
+
expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
|
|
155
|
+
expect(result.activityMetadata?.webFetch?.status).toBe(403);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("rejects URL-embedded credentials before calling TinyFish", async () => {
|
|
159
|
+
let fetchCalls = 0;
|
|
160
|
+
globalThis.fetch = (async () => {
|
|
161
|
+
fetchCalls += 1;
|
|
162
|
+
return fetchResponse({ results: [], errors: [] });
|
|
163
|
+
}) as unknown as typeof fetch;
|
|
164
|
+
|
|
165
|
+
const result = await executeTinyfishFetch(
|
|
166
|
+
{ url: "https://user:pass@example.com/private" },
|
|
167
|
+
{ apiKey: "tf_test" },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.isError).toBe(true);
|
|
171
|
+
expect(result.content).toContain("embedded credentials");
|
|
172
|
+
expect(fetchCalls).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("webFetchTool TinyFish dispatch", () => {
|
|
177
|
+
let originalFetch: typeof globalThis.fetch;
|
|
178
|
+
|
|
179
|
+
const execute = (input: Record<string, unknown>, context: any = {}) =>
|
|
180
|
+
webFetchTool.execute(input, context);
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
originalFetch = globalThis.fetch;
|
|
184
|
+
seedWebFetch("tinyfish");
|
|
185
|
+
mockTinyfishSecureKey = "tf_test";
|
|
186
|
+
mockResolveAddresses = ["93.184.216.34"];
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
globalThis.fetch = originalFetch;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("routes public URLs to TinyFish when a key is stored", async () => {
|
|
194
|
+
let tinyfishHit = false;
|
|
195
|
+
globalThis.fetch = (async (url: string) => {
|
|
196
|
+
if (String(url).includes("api.fetch.tinyfish.ai")) {
|
|
197
|
+
tinyfishHit = true;
|
|
198
|
+
}
|
|
199
|
+
return fetchResponse({
|
|
200
|
+
results: [
|
|
201
|
+
{
|
|
202
|
+
url: "https://example.com/",
|
|
203
|
+
final_url: "https://example.com/",
|
|
204
|
+
text: "routed",
|
|
205
|
+
format: "markdown",
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
errors: [],
|
|
209
|
+
});
|
|
210
|
+
}) as typeof fetch;
|
|
211
|
+
|
|
212
|
+
const result = await execute({ url: "https://example.com" });
|
|
213
|
+
|
|
214
|
+
expect(tinyfishHit).toBe(true);
|
|
215
|
+
expect(result.activityMetadata?.webFetch?.provider).toBe("tinyfish");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("does not send private targets to TinyFish", async () => {
|
|
219
|
+
let tinyfishHit = false;
|
|
220
|
+
globalThis.fetch = (async (url: string) => {
|
|
221
|
+
if (String(url).includes("api.fetch.tinyfish.ai")) {
|
|
222
|
+
tinyfishHit = true;
|
|
223
|
+
}
|
|
224
|
+
return new Response("", { status: 200 });
|
|
225
|
+
}) as typeof fetch;
|
|
226
|
+
|
|
227
|
+
const result = await execute({ url: "http://localhost:8080/admin" });
|
|
228
|
+
|
|
229
|
+
expect(tinyfishHit).toBe(false);
|
|
230
|
+
expect(result.isError).toBe(true);
|
|
231
|
+
expect(result.activityMetadata?.webFetch?.provider).toBe("default");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("falls back to the built-in fetcher when no TinyFish key is stored", async () => {
|
|
235
|
+
mockTinyfishSecureKey = undefined;
|
|
236
|
+
mockResolveAddresses = [];
|
|
237
|
+
let tinyfishHit = false;
|
|
238
|
+
globalThis.fetch = (async (url: string) => {
|
|
239
|
+
if (String(url).includes("api.fetch.tinyfish.ai")) {
|
|
240
|
+
tinyfishHit = true;
|
|
241
|
+
}
|
|
242
|
+
return new Response("", { status: 200 });
|
|
243
|
+
}) as typeof fetch;
|
|
244
|
+
|
|
245
|
+
const result = await execute({ url: "https://example.com" });
|
|
246
|
+
|
|
247
|
+
expect(tinyfishHit).toBe(false);
|
|
248
|
+
expect(result.activityMetadata?.webFetch?.provider).toBe("default");
|
|
249
|
+
});
|
|
250
|
+
});
|