|
|
@@ -1,8 +1,11 @@
|
|
|
import { Effect, Schema } from "effect"
|
|
|
import { HttpClient } from "effect/unstable/http"
|
|
|
import * as Tool from "./tool"
|
|
|
-import * as McpExa from "./mcp-exa"
|
|
|
+import * as McpWebSearch from "./mcp-websearch"
|
|
|
import DESCRIPTION from "./websearch.txt"
|
|
|
+import { Flag } from "@opencode-ai/core/flag/flag"
|
|
|
+import { checksum } from "@opencode-ai/core/util/encode"
|
|
|
+import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|
|
|
|
|
export const Parameters = Schema.Struct({
|
|
|
query: Schema.String.annotate({ description: "Websearch query" }),
|
|
|
@@ -21,6 +24,81 @@ export const Parameters = Schema.Struct({
|
|
|
}),
|
|
|
})
|
|
|
|
|
|
+const WebSearchProviderSchema = Schema.Literals(["exa", "parallel"])
|
|
|
+export type WebSearchProvider = Schema.Schema.Type<typeof WebSearchProviderSchema>
|
|
|
+
|
|
|
+export function selectWebSearchProvider(
|
|
|
+ sessionID: string,
|
|
|
+ flags = { exa: Flag.OPENCODE_ENABLE_EXA, parallel: Flag.OPENCODE_ENABLE_PARALLEL },
|
|
|
+): WebSearchProvider {
|
|
|
+ const override = process.env.OPENCODE_WEBSEARCH_PROVIDER
|
|
|
+ if (override === "exa" || override === "parallel") return override
|
|
|
+ if (flags.parallel) return "parallel"
|
|
|
+ if (flags.exa) return "exa"
|
|
|
+
|
|
|
+ return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel"
|
|
|
+}
|
|
|
+
|
|
|
+export function webSearchProviderLabel(provider: unknown) {
|
|
|
+ if (provider === "parallel") return "Parallel Web Search"
|
|
|
+ if (provider === "exa") return "Exa Web Search"
|
|
|
+ return "Web Search"
|
|
|
+}
|
|
|
+
|
|
|
+export function webSearchModelName(extra: Tool.Context["extra"]) {
|
|
|
+ const model = extra?.model
|
|
|
+ if (!model || typeof model !== "object") return undefined
|
|
|
+ const api = "api" in model && model.api && typeof model.api === "object" ? model.api : undefined
|
|
|
+ const apiID = api && "id" in api && typeof api.id === "string" ? api.id : undefined
|
|
|
+ const id = "id" in model && typeof model.id === "string" ? model.id : undefined
|
|
|
+ return (apiID ?? id)?.slice(0, 100)
|
|
|
+}
|
|
|
+
|
|
|
+function parallelAuthHeaders() {
|
|
|
+ const headers = { "User-Agent": `opencode/${InstallationVersion}` }
|
|
|
+ if (!process.env.PARALLEL_API_KEY) return headers
|
|
|
+ return { ...headers, Authorization: `Bearer ${process.env.PARALLEL_API_KEY}` }
|
|
|
+}
|
|
|
+
|
|
|
+function callProvider(
|
|
|
+ http: HttpClient.HttpClient,
|
|
|
+ provider: WebSearchProvider,
|
|
|
+ params: Schema.Schema.Type<typeof Parameters>,
|
|
|
+ ctx: Tool.Context,
|
|
|
+) {
|
|
|
+ if (provider === "parallel") {
|
|
|
+ return McpWebSearch.call(
|
|
|
+ http,
|
|
|
+ McpWebSearch.PARALLEL_URL,
|
|
|
+ "web_search",
|
|
|
+ McpWebSearch.ParallelSearchArgs,
|
|
|
+ {
|
|
|
+ objective: params.query,
|
|
|
+ search_queries: [params.query],
|
|
|
+ session_id: ctx.sessionID,
|
|
|
+ model_name: webSearchModelName(ctx.extra),
|
|
|
+ },
|
|
|
+ "25 seconds",
|
|
|
+ parallelAuthHeaders(),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return McpWebSearch.call(
|
|
|
+ http,
|
|
|
+ McpWebSearch.EXA_URL,
|
|
|
+ "web_search_exa",
|
|
|
+ McpWebSearch.SearchArgs,
|
|
|
+ {
|
|
|
+ query: params.query,
|
|
|
+ type: params.type || "auto",
|
|
|
+ numResults: params.numResults || 8,
|
|
|
+ livecrawl: params.livecrawl || "fallback",
|
|
|
+ contextMaxCharacters: params.contextMaxCharacters,
|
|
|
+ },
|
|
|
+ "25 seconds",
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
export const WebSearchTool = Tool.define(
|
|
|
"websearch",
|
|
|
Effect.gen(function* () {
|
|
|
@@ -33,6 +111,10 @@ export const WebSearchTool = Tool.define(
|
|
|
parameters: Parameters,
|
|
|
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
|
|
Effect.gen(function* () {
|
|
|
+ const provider = selectWebSearchProvider(ctx.sessionID)
|
|
|
+ const title = webSearchProviderLabel(provider)
|
|
|
+ yield* ctx.metadata({ title: `${title} "${params.query}"`, metadata: { provider } })
|
|
|
+
|
|
|
yield* ctx.ask({
|
|
|
permission: "websearch",
|
|
|
patterns: [params.query],
|
|
|
@@ -43,27 +125,16 @@ export const WebSearchTool = Tool.define(
|
|
|
livecrawl: params.livecrawl,
|
|
|
type: params.type,
|
|
|
contextMaxCharacters: params.contextMaxCharacters,
|
|
|
+ provider,
|
|
|
},
|
|
|
})
|
|
|
|
|
|
- const result = yield* McpExa.call(
|
|
|
- http,
|
|
|
- "web_search_exa",
|
|
|
- McpExa.SearchArgs,
|
|
|
- {
|
|
|
- query: params.query,
|
|
|
- type: params.type || "auto",
|
|
|
- numResults: params.numResults || 8,
|
|
|
- livecrawl: params.livecrawl || "fallback",
|
|
|
- contextMaxCharacters: params.contextMaxCharacters,
|
|
|
- },
|
|
|
- "25 seconds",
|
|
|
- )
|
|
|
+ const result = yield* callProvider(http, provider, params, ctx)
|
|
|
|
|
|
return {
|
|
|
output: result ?? "No search results found. Please try a different query.",
|
|
|
- title: `Web search: ${params.query}`,
|
|
|
- metadata: {},
|
|
|
+ title: `${title}: ${params.query}`,
|
|
|
+ metadata: { provider },
|
|
|
}
|
|
|
}).pipe(Effect.orDie),
|
|
|
}
|