Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/deploy-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ jobs:
STRIPE_PRO_YEARLY_PRICE_ID: ${{ secrets.STRIPE_PRO_YEARLY_PRICE_ID }}
SLACK_BILLING_WEBHOOK_URL: ${{ secrets.SLACK_BILLING_WEBHOOK_URL }}
SECRETS_ENCRYPTION_KEY: ${{ secrets.SECRETS_ENCRYPTION_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
run: |
vercel pull --yes --environment=preview --token=$VERCEL_TOKEN
vercel build --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -252,7 +253,8 @@ jobs:
--env STRIPE_PRO_MONTHLY_PRICE_ID=$STRIPE_PRO_MONTHLY_PRICE_ID \
--env STRIPE_PRO_YEARLY_PRICE_ID=$STRIPE_PRO_YEARLY_PRICE_ID \
--env SLACK_BILLING_WEBHOOK_URL=$SLACK_BILLING_WEBHOOK_URL \
--env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY)
--env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY \
--env TAVILY_API_KEY=$TAVILY_API_KEY)
vercel alias $VERCEL_URL ${{ env.API_ALIAS }} --scope=$VERCEL_ORG_ID --token=$VERCEL_TOKEN
echo "vercel_url=$VERCEL_URL" >> $GITHUB_OUTPUT

Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ jobs:
STRIPE_PRO_YEARLY_PRICE_ID: ${{ secrets.STRIPE_PRO_YEARLY_PRICE_ID }}
SLACK_BILLING_WEBHOOK_URL: ${{ secrets.SLACK_BILLING_WEBHOOK_URL }}
SECRETS_ENCRYPTION_KEY: ${{ secrets.SECRETS_ENCRYPTION_KEY }}
TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}
run: |
vercel pull --yes --environment=production --token=$VERCEL_TOKEN
vercel build --prod --token=$VERCEL_TOKEN
Expand Down Expand Up @@ -156,7 +157,8 @@ jobs:
--env STRIPE_PRO_MONTHLY_PRICE_ID=$STRIPE_PRO_MONTHLY_PRICE_ID \
--env STRIPE_PRO_YEARLY_PRICE_ID=$STRIPE_PRO_YEARLY_PRICE_ID \
--env SLACK_BILLING_WEBHOOK_URL=$SLACK_BILLING_WEBHOOK_URL \
--env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY
--env SECRETS_ENCRYPTION_KEY=$SECRETS_ENCRYPTION_KEY \
--env TAVILY_API_KEY=$TAVILY_API_KEY

deploy-web:
name: Deploy Web to Vercel
Expand Down
3 changes: 3 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@
"@superset/shared": "workspace:*",
"@superset/trpc": "workspace:*",
"@t3-oss/env-nextjs": "^0.13.8",
"@tavily/core": "^0.7.1",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@tavily/core npm package latest version

💡 Result:

The latest published version of the @tavily/core npm package is 0.5.11. [1]


Update @tavily/core to a valid published version — 0.7.1 does not exist.

The latest published version of @tavily/core is 0.5.11. Specifying ^0.7.1 will cause npm install to fail due to an unresolvable dependency. Update the version constraint to ^0.5.11 or lower.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/package.json` at line 33, The package.json dependency for
"@tavily/core" specifies a non-existent version "^0.7.1"; update the dependency
entry for "@tavily/core" in package.json to a published version (e.g.,
"^0.5.11") so npm install can resolve it, then run npm install and verify the
lockfile updates and CI passes.

"@trpc/server": "^11.7.1",
"@upstash/qstash": "^2.8.4",
"@upstash/ratelimit": "^2.0.4",
"@upstash/redis": "^1.34.3",
"@vercel/blob": "^2.0.0",
"better-auth": "1.4.18",
"date-fns": "^4.1.0",
Expand Down
75 changes: 75 additions & 0 deletions apps/api/src/app/api/chat/tools/web-search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { auth } from "@superset/auth/server";
import { tavily } from "@tavily/core";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { env } from "@/env";

const ratelimit = new Ratelimit({
redis: new Redis({
url: env.KV_REST_API_URL,
token: env.KV_REST_API_TOKEN,
}),
limiter: Ratelimit.slidingWindow(1000, "1 d"),
prefix: "ratelimit:web-search",
});

export async function POST(request: Request): Promise<Response> {
const session = await auth.api.getSession({
headers: request.headers,
});

if (!session?.user) {
return new Response("Unauthorized", { status: 401 });
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const { success } = await ratelimit.limit(session.user.id);
if (!success) {
return Response.json(
{ error: "Rate limit exceeded. Try again later." },
{ status: 429 },
);
}

let body: { query?: string; maxResults?: number };
try {
body = await request.json();
} catch {
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
}

if (!body.query || typeof body.query !== "string") {
return Response.json(
{ error: "Missing or invalid 'query' field" },
{ status: 400 },
);
}

const rawMax = body.maxResults;
const maxResults =
typeof rawMax === "number" && Number.isFinite(rawMax)
? Math.min(Math.max(rawMax, 1), 10)
: 5;
Comment on lines +47 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

@tavily/core search() maxResults parameter type integer number typescript

💡 Result:

I couldn’t find any public docs/source for @tavily/core’s search() signature via web search right now (all queries returned no results), so I can’t verify the exact exported TypeScript type for maxResults.

That said, in TypeScript there is no distinct integer type—integers are represented as number. So SDKs virtually always type an “integer” option like this as:

  • maxResults?: number

and rely on runtime validation (or the API) to enforce “must be an integer”.

Practical guidance:

await tavily.search("query", { maxResults: 10 }); // pass an integer number

If you want to enforce integer-ness yourself:

const maxResults = 10;
if (!Number.isInteger(maxResults)) throw new Error("maxResults must be an integer");

🏁 Script executed:

# Check for package.json files
fd -t f 'package.json' | head -5

# Look for `@tavily/core` in lock files
rg '@tavily/core' --type json -A 3 2>/dev/null | head -30

Repository: superset-sh/superset

Length of output: 395


🏁 Script executed:

# Search for uses of tavily client.search
rg 'client\.search' --type ts --type tsx -B 2 -A 3

# Look for TypeScript type definitions related to tavily
fd -e 'd.ts' | xargs rg 'maxResults|Tavily' 2>/dev/null | head -20

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Search for uses of tavily client.search properly
rg 'client\.search' -A 3 -B 2

# Also check for type definitions in node_modules
find . -path ./node_modules -prune -o -name '*tavily*' -type f -print | head -20

Repository: superset-sh/superset

Length of output: 576


🌐 Web query:

Tavily API maxResults parameter specification documentation

💡 Result:

In the official Tavily Search API, the parameter is named max_results (snake_case), not maxResults.

  • Parameter: max_results
  • Type: integer
  • Default: 5
  • Meaning: “The maximum number of search results to return.”
  • Allowed range: 0 <= x <= 20  [1]
  • Tavily’s docs also note max_results is one of the parameters that must be set manually when using auto_parameters, because it affects response size. [1]

If you’re seeing maxResults (camelCase), that’s typically from a third-party wrapper/integration, not Tavily’s native REST API (example: Wanaku’s Tavily tool exposes maxResults:int). [2]

Sources:
[1] Tavily Docs – Search endpoint API reference (parameter max_results, default/range, auto_parameters note)
[2] Wanaku MCP Router docs – Tavily tool parameter maxResults


Number.isFinite accepts floats — maxResults may forward a non-integer to Tavily.

Number.isFinite(3.7) returns true, so a float value like 3.7 passes validation and is sent to client.search(). The Tavily API expects max_results to be an integer.

🛠️ Proposed fix
 const rawMax = body.maxResults;
 const maxResults =
-	typeof rawMax === "number" && Number.isFinite(rawMax)
+	Number.isInteger(rawMax)
 		? Math.min(Math.max(rawMax, 1), 10)
 		: 5;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rawMax = body.maxResults;
const maxResults =
typeof rawMax === "number" && Number.isFinite(rawMax)
? Math.min(Math.max(rawMax, 1), 10)
: 5;
const rawMax = body.maxResults;
const maxResults =
Number.isInteger(rawMax)
? Math.min(Math.max(rawMax, 1), 10)
: 5;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/api/chat/tools/web-search/route.ts` around lines 28 - 32,
The code uses Number.isFinite on rawMax which allows floats (e.g., 3.7) to pass,
so change the validation/coercion for maxResults to ensure an integer between 1
and 10: either use Number.isInteger(rawMax) in the conditional or explicitly
coerce with Math.trunc/Math.floor/Math.round (e.g., const sanitized =
Math.trunc(Number(rawMax)); const maxResults = Number.isFinite(sanitized) ?
Math.min(Math.max(sanitized, 1), 10) : 5), and make sure the integer maxResults
is what you pass to client.search().


if (!env.TAVILY_API_KEY) {
return Response.json(
{ error: "Web search is not configured" },
{ status: 503 },
);
}

try {
const client = tavily({ apiKey: env.TAVILY_API_KEY });
const response = await client.search(body.query, { maxResults });

return Response.json({
results: response.results.map((r) => ({
title: r.title,
url: r.url,
content: r.content,
})),
});
} catch (error) {
const message = error instanceof Error ? error.message : "Search failed";
return Response.json({ error: message }, { status: 502 });
}
Comment on lines +60 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tavily client instantiated on every request; raw Tavily error text forwarded to caller.

Two separate concerns here:

  1. tavily({ apiKey: ... }) creates a new configured axios client on every request. Promote it to module scope as a lazy singleton.

  2. Line 47 returns error.message verbatim in the 502 body. Tavily error messages can include quota-exhaustion details or API-key validation text, which need not be exposed to the agent. Return a generic message and log the detail server-side.

♻️ Proposed refactor
+// Module-level singleton — initialised once per cold start
+let _client: ReturnType<typeof tavily> | null = null;
+function getClient() {
+	if (!_client) _client = tavily({ apiKey: env.TAVILY_API_KEY! });
+	return _client;
+}
+
 export async function POST(request: Request): Promise<Response> {
   // ... auth / body parsing ...
   try {
-    const client = tavily({ apiKey: env.TAVILY_API_KEY });
-    const response = await client.search(body.query, { maxResults });
+    const response = await getClient().search(body.query, { maxResults });
     return Response.json({ results: response.results.map(...) });
   } catch (error) {
-    const message = error instanceof Error ? error.message : "Search failed";
-    return Response.json({ error: message }, { status: 502 });
+    console.error("[web-search] Tavily error:", error);
+    return Response.json({ error: "Search failed" }, { status: 502 });
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/api/tools/web-search/route.ts` around lines 34 - 48, The
code instantiates tavily({ apiKey: env.TAVILY_API_KEY }) inside the request
handler and returns raw error.message to clients; move the Tavily axios client
to module scope as a lazy singleton (create a module-level variable and
initialize it on first use, referenced by the handler that calls client.search)
to avoid recreating the client per request, and change the catch block so it
logs the full error detail server-side (e.g., console.error or your request
logger with the caught error object from the catch) while returning a generic
Response.json({ error: "Search service unavailable" }, { status: 502 }) to the
caller instead of error.message; keep references to tavily, client.search,
env.TAVILY_API_KEY and Response.json when applying the changes.

Comment on lines +71 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tavily error messages forwarded verbatim to the client may leak internal details.

error.message from @tavily/core can include API-key status hints, rate-limit details, or other implementation information. Returning it in the 502 response exposes these to the caller.

🛡️ Proposed fix
 } catch (error) {
-	const message = error instanceof Error ? error.message : "Search failed";
-	return Response.json({ error: message }, { status: 502 });
+	console.error("[web-search] Tavily error:", error);
+	return Response.json({ error: "Search failed" }, { status: 502 });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
const message = error instanceof Error ? error.message : "Search failed";
return Response.json({ error: message }, { status: 502 });
}
} catch (error) {
console.error("[web-search] Tavily error:", error);
return Response.json({ error: "Search failed" }, { status: 502 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/api/chat/tools/web-search/route.ts` around lines 45 - 48,
The current catch block in apps/api/src/app/api/chat/tools/web-search/route.ts
returns error.message (from `@tavily/core`) to clients which may leak internal
details; update the catch in the async handler (the try/catch that returns
Response.json) to always send a generic error body (e.g., { error: "Search
failed" } or similar) and a 502 status, while logging the full error (the caught
error or error.message and stack) to the server logger or console for debugging;
keep the error instanceof Error check only for logging purposes and do not
forward the raw error.message to the Response.json payload.

}
1 change: 1 addition & 0 deletions apps/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const env = createEnv({
SENTRY_AUTH_TOKEN: z.string().optional(),
DURABLE_STREAMS_URL: z.string().url().optional(),
DURABLE_STREAMS_SECRET: z.string().min(1).optional(),
TAVILY_API_KEY: z.string().optional(),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/lib/agent-manager/utils/run-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { promisify } from "node:util";
import { RequestContext, superagent, toAISdkStream } from "@superset/agent";
import type { SessionHost } from "@superset/durable-session/host";
import type { UIMessageChunk } from "ai";
import { env } from "main/env.main";

// ---------------------------------------------------------------------------
// Shared session state
Expand Down Expand Up @@ -34,6 +35,7 @@ export interface RunAgentOptions {
cwd: string;
permissionMode?: string;
thinkingEnabled?: boolean;
authToken?: string;
}

export async function runAgent(options: RunAgentOptions): Promise<void> {
Expand All @@ -45,6 +47,7 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
cwd,
permissionMode,
thinkingEnabled,
authToken,
} = options;

// Abort any existing agent for this session
Expand All @@ -57,6 +60,8 @@ export async function runAgent(options: RunAgentOptions): Promise<void> {
const requestEntries: [string, string][] = [
["modelId", modelId],
["cwd", cwd],
["apiUrl", env.NEXT_PUBLIC_API_URL],
...(authToken ? ([["authToken", authToken]] as [string, string][]) : []),
...(thinkingEnabled
? ([["thinkingEnabled", "true"]] as [string, string][])
: []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class StreamWatcher {
cwd: this.host.config.cwd ?? process.env.HOME ?? "/",
permissionMode: this.host.config.permissionMode,
thinkingEnabled: this.host.config.thinkingEnabled,
authToken: options.authToken,
});
});

Expand Down
4 changes: 3 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"@mastra/core": "^1.3.0",
"@mastra/memory": "^1.2.0",
"@superset/durable-session": "workspace:*",
"@tavily/core": "^0.7.1",
"cheerio": "^1.2.0",
"zod": "^4.3.5"
},
Expand Down
58 changes: 34 additions & 24 deletions packages/agent/src/tools/web-search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { createTool } from "@mastra/core/tools";
import { tavily } from "@tavily/core";
import { z } from "zod";

const resultSchema = z.object({
results: z.array(
z.object({
title: z.string(),
url: z.string(),
content: z.string(),
}),
),
});

export const webSearchTool = createTool({
id: "web_search",
description:
Expand All @@ -16,34 +25,35 @@ export const webSearchTool = createTool({
.default(5)
.describe("Maximum number of results to return (1-10)"),
}),
outputSchema: z.object({
results: z.array(
z.object({
title: z.string(),
url: z.string(),
content: z.string(),
}),
),
}),
execute: async (input) => {
const apiKey = process.env.TAVILY_API_KEY;
if (!apiKey) {
outputSchema: resultSchema,
execute: async (input, context) => {
const apiUrl = context?.requestContext?.get("apiUrl");
const authToken = context?.requestContext?.get("authToken");
Comment on lines +29 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for RequestContext type definition in the repository
rg -n "RequestContext" --type ts -A 5 -B 1 | head -100

Repository: superset-sh/superset

Length of output: 2938


🏁 Script executed:

#!/bin/bash
# Also look specifically for interface or type definitions of RequestContext
rg -n "interface RequestContext|type RequestContext" --type ts -A 10

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check the web-search.ts file to see the actual usage on lines 29-43
cat -n packages/agent/src/tools/web-search.ts | sed -n '25,45p'

Repository: superset-sh/superset

Length of output: 827


🌐 Web query:

Mastra RequestContext type definition Map string unknown documentation

💡 Result:

In Mastra, RequestContext is a class (not a type alias) that wraps an internal registry Map and exposes set/get/has/delete/clear/keys/values/entries/forEach, plus toJSON() and an all getter.

From @mastra/core’s published type defs (dist/request-context/index.d.ts), you can see the Map<string, unknown> surface type in forEach (the callback’s 3rd arg is the backing map):

  • forEach(..., map: Map<string, unknown>) => void [1]

The same file defines the class and its constructor shape (iterable of [string, unknown] when untyped, or tuples derived from Values when typed) and also includes the reserved keys MASTRA_RESOURCE_ID_KEY / MASTRA_THREAD_ID_KEY [1].

Also, RequestContext.all is documented as a way to access the whole object (useful for destructuring) [2].

Sources: UNPKG type definition [1], Mastra release notes mentioning RequestContext.all [2].


Add type narrowing or assertions for apiUrl and authToken to comply with type-safety guidelines.

RequestContext.get() returns unknown type (Map<string, unknown>). The falsy check on line 33 (if (!apiUrl || !authToken)) does not narrow unknown to string, leaving both variables typed as unknown when used in the template literal (line 39) and Authorization header (line 43). Add explicit type narrowing via type assertions (as string) or type guards to satisfy the coding guideline requiring type-safe TypeScript without any/unknown without proper narrowing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/agent/src/tools/web-search.ts` around lines 29 - 31, The variables
apiUrl and authToken (from RequestContext.get) are typed as unknown; add
explicit narrowing before using them in execute: use type guards (e.g., check
typeof apiUrl === "string" and typeof authToken === "string") or assert them
(e.g., const apiUrlStr = apiUrl as string) and throw or return an error if they
are not strings, then use the narrowed apiUrlStr/authTokenStr in the template
literal and Authorization header; reference the execute function and the
RequestContext.get("apiUrl") / RequestContext.get("authToken") calls when making
the change.


if (!apiUrl || !authToken) {
throw new Error(
"TAVILY_API_KEY environment variable is not set. Web search is unavailable.",
"Web search requires apiUrl and authToken in request context.",
);
}
Comment on lines +29 to 37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing local-dev fallback — breaks stated test plan requirement.

The test plan specifies: "Local dev without the proxy (TAVILY_API_KEY in env) should continue to work as a fallback." But the code throws unconditionally when apiUrl or authToken are absent, and @tavily/core was removed from packages/agent/package.json, so there is no remaining code path for a direct Tavily call.

Any invocation of web_search without the proxy context (e.g., CLI, server-side agent, unit tests) will hard-fail.

Either restore the fallback path (and re-add @tavily/core to the agent package), or update the test plan to reflect that TAVILY_API_KEY alone no longer suffices.

🛠️ Proposed fix — proxy-first with direct Tavily fallback

Re-add "@tavily/core": "^0.7.1" to packages/agent/package.json, then:

+import { tavily } from "@tavily/core";
 import { createTool } from "@mastra/core/tools";
 import { z } from "zod";
 execute: async (input, context) => {
 	const apiUrl = context?.requestContext?.get("apiUrl");
 	const authToken = context?.requestContext?.get("authToken");

-	if (!apiUrl || !authToken) {
-		throw new Error(
-			"Web search requires apiUrl and authToken in request context.",
-		);
-	}
+	if (apiUrl && authToken) {
+		const response = await fetch(`${apiUrl}/api/chat/tools/web-search`, {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+				Authorization: `Bearer ${authToken}`,
+			},
+			body: JSON.stringify({
+				query: input.query,
+				maxResults: input.maxResults,
+			}),
+		});
+
+		if (!response.ok) {
+			throw new Error(
+				`Web search proxy returned ${response.status}: ${await response.text()}`,
+			);
+		}
+
+		return resultSchema.parse(await response.json());
+	}
+
+	// Fallback: direct Tavily call for local dev / server-side use
+	const tavilyApiKey = process.env.TAVILY_API_KEY;
+	if (!tavilyApiKey) {
+		throw new Error(
+			"Web search requires either (apiUrl + authToken) or TAVILY_API_KEY in the environment.",
+		);
+	}
+	const client = tavily({ apiKey: tavilyApiKey });
+	const tvResponse = await client.search(input.query, {
+		maxResults: input.maxResults,
+	});
+	return {
+		results: tvResponse.results.map((r) => ({
+			title: r.title,
+			url: r.url,
+			content: r.content,
+		})),
+	};
-
-	const response = await fetch(`${apiUrl}/api/chat/tools/web-search`, {
-		...
-	});
-
-	if (!response.ok) { ... }
-
-	return resultSchema.parse(await response.json());
 },


const client = tavily({ apiKey });
const response = await client.search(input.query, {
maxResults: input.maxResults,
const response = await fetch(`${apiUrl}/api/chat/tools/web-search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
query: input.query,
maxResults: input.maxResults,
}),
});

return {
results: response.results.map((r) => ({
title: r.title,
url: r.url,
content: r.content,
})),
};
if (!response.ok) {
throw new Error(
`Web search proxy returned ${response.status}: ${await response.text()}`,
);
}

return resultSchema.parse(await response.json());
},
});
Loading