Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/core/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export const Flag = {
OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT:
copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
OPENCODE_ENABLE_EXA: truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA"),
OPENCODE_ENABLE_PERPLEXITY:
!falsy("OPENCODE_DISABLE_PERPLEXITY") &&
!truthy("OPENCODE_DISABLE_PERPLEXITY") &&
(truthy("OPENCODE_ENABLE_PERPLEXITY") ||
!!process.env.PERPLEXITY_API_KEY ||
!!process.env.PPLX_API_KEY),
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS"),
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX"),
OPENCODE_EXPERIMENTAL_OXFMT: OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT"),
Expand Down
76 changes: 76 additions & 0 deletions packages/opencode/src/tool/mcp-perplexity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Duration, Effect, Schema } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
import { InstallationVersion } from "@opencode-ai/core/installation/version"

const PERPLEXITY_URL = "https://api.perplexity.ai/search"

export const SearchArgs = Schema.Struct({
query: Schema.String,
numResults: Schema.optional(Schema.Number),
contextMaxCharacters: Schema.optional(Schema.Number),
})

const PerplexityResultItem = Schema.Struct({
title: Schema.String,
url: Schema.String,
snippet: Schema.optional(Schema.String),
date: Schema.optional(Schema.String),
})

const PerplexityResult = Schema.Struct({
results: Schema.Array(PerplexityResultItem),
})

const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(PerplexityResult))

const PerplexityRequest = Schema.Struct({
query: Schema.String,
max_results: Schema.optional(Schema.Number),
})

export const call = (
http: HttpClient.HttpClient,
args: typeof SearchArgs.Type,
timeout: Duration.Input,
) =>
Effect.gen(function* () {
const apiKey = process.env.PERPLEXITY_API_KEY ?? process.env.PPLX_API_KEY
if (!apiKey) return yield* Effect.die(new Error("PERPLEXITY_API_KEY is not set"))

const request = yield* HttpClientRequest.post(PERPLEXITY_URL).pipe(
HttpClientRequest.bearerToken(apiKey),
HttpClientRequest.setHeader("X-Pplx-Integration", `opencode/${InstallationVersion}`),
HttpClientRequest.acceptJson,
HttpClientRequest.schemaBodyJson(PerplexityRequest)({
query: args.query,
max_results: args.numResults ?? 8,
}),
)

const response = yield* HttpClient.filterStatusOk(http)
.execute(request)
.pipe(
Effect.timeoutOrElse({
duration: timeout,
orElse: () => Effect.die(new Error("perplexity websearch request timed out")),
}),
)

const body = yield* response.text
const parsed = yield* decode(body)

let output = parsed.results
.map((r, i) => {
const parts = [`${i + 1}. ${r.title}`, ` ${r.url}`]
if (r.snippet) parts.push(` ${r.snippet}`)
if (r.date) parts.push(` (${r.date})`)
return parts.join("\n")
})
.join("\n\n")

if (args.contextMaxCharacters && output.length > args.contextMaxCharacters) {
output = output.slice(0, args.contextMaxCharacters)
}

return output || undefined
})
6 changes: 5 additions & 1 deletion packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,11 @@ export const layer: Layer.Layer<
const tools: Interface["tools"] = Effect.fn("ToolRegistry.tools")(function* (input) {
const filtered = (yield* all()).filter((tool) => {
if (tool.id === WebSearchTool.id) {
return input.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
return (
input.providerID === ProviderID.opencode ||
Flag.OPENCODE_ENABLE_EXA ||
Flag.OPENCODE_ENABLE_PERPLEXITY
)
}

const usePatch =
Expand Down
52 changes: 37 additions & 15 deletions packages/opencode/src/tool/websearch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Effect, Schema } from "effect"
import { HttpClient } from "effect/unstable/http"
import { Flag } from "@opencode-ai/core/flag/flag"
import * as Tool from "./tool"
import * as McpExa from "./mcp-exa"
import * as McpPerplexity from "./mcp-perplexity"
import DESCRIPTION from "./websearch.txt"

export const Parameters = Schema.Struct({
Expand All @@ -11,16 +13,26 @@ export const Parameters = Schema.Struct({
}),
livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
description:
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
"Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback'). Exa-only; ignored when Perplexity backend is in use.",
}),
type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
description:
"Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search. Exa-only; ignored when Perplexity backend is in use.",
}),
contextMaxCharacters: Schema.optional(Schema.Number).annotate({
description: "Maximum characters for context string optimized for LLMs (default: 10000)",
}),
})

// Backend selection precedence:
// 1. Perplexity (default) — when PERPLEXITY_API_KEY (or PPLX_API_KEY) is set
// and OPENCODE_DISABLE_PERPLEXITY is not set.
// 2. Exa — when OPENCODE_ENABLE_EXA is set or the hosted opencode provider is in use.
// 3. Otherwise the websearch tool is not registered (see registry.ts).
function usePerplexityBackend() {
return Flag.OPENCODE_ENABLE_PERPLEXITY && !!(process.env.PERPLEXITY_API_KEY ?? process.env.PPLX_API_KEY)
}

export const WebSearchTool = Tool.define(
"websearch",
Effect.gen(function* () {
Expand All @@ -46,19 +58,29 @@ export const WebSearchTool = Tool.define(
},
})

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 = usePerplexityBackend()
? yield* McpPerplexity.call(
http,
{
query: params.query,
numResults: params.numResults || 8,
contextMaxCharacters: params.contextMaxCharacters,
},
"25 seconds",
)
: 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",
)

return {
output: result ?? "No search results found. Please try a different query.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ exports[`tool parameters JSON Schema (wire shape) websearch 1`] = `
"type": "number",
},
"livecrawl": {
"description": "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
"description": "Live crawl mode - 'fallback': use live crawling as backup if cached content unavailable, 'preferred': prioritize live crawling (default: 'fallback'). Exa-only; ignored when Perplexity backend is in use.",
"enum": [
"fallback",
"preferred",
Expand All @@ -443,7 +443,7 @@ exports[`tool parameters JSON Schema (wire shape) websearch 1`] = `
"type": "string",
},
"type": {
"description": "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
"description": "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search. Exa-only; ignored when Perplexity backend is in use.",
"enum": [
"auto",
"fast",
Expand Down
91 changes: 91 additions & 0 deletions packages/web/src/content/docs/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1642,6 +1642,97 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te

---

### Perplexity

Perplexity exposes its [Agent API](https://docs.perplexity.ai/docs/agent-api/quickstart) (OpenAI Responses-compatible) and [Search API](https://docs.perplexity.ai/docs/search/quickstart) for grounded, citation-backed answers.

1. Create an API key in the [Perplexity API key dashboard](https://www.perplexity.ai/account/api/keys).

2. Run the `/connect` command and select **Other** (Perplexity is not yet in the built-in directory).

```txt
/connect
```

3. Enter `perplexity` as the provider ID and paste your API key.

```txt
┌ API key
└ enter
```

4. Add the following to your `opencode.json`. Because the Agent API is [OpenAI Responses-compatible](https://docs.perplexity.ai/docs/agent-api/openai-compatibility), use the `@ai-sdk/openai` package. The Agent API routes to many [third-party models](https://docs.perplexity.ai/docs/agent-api/models), so list the ones you want under `models`.

```json title="opencode.json" {5-9}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"perplexity": {
"npm": "@ai-sdk/openai",
"name": "Perplexity",
"options": {
"baseURL": "https://api.perplexity.ai/v1",
"apiKey": "{env:PERPLEXITY_API_KEY}"
},
"models": {
"openai/gpt-5.4": {
"name": "GPT-5.4 (via Perplexity)"
},
"anthropic/claude-sonnet-4-6": {
"name": "Claude Sonnet 4.6 (via Perplexity)"
}
}
}
}
}
```

5. Run `/models` and pick a model. See the [Agent API models](https://docs.perplexity.ai/docs/agent-api/models) page for the full list. [Presets](https://docs.perplexity.ai/docs/agent-api/presets) (for example `pro-search`) and the [Agent API reference](https://docs.perplexity.ai/api-reference/agent-post) cover advanced options like fallback model lists.

```txt
/models
```

#### Search API

For raw, ranked web search results (separate from chat completions), Perplexity also offers the [Search API](https://docs.perplexity.ai/docs/search/quickstart). Two ways to use it from opencode:

**Direct HTTP** for scripts and one-off calls:

```bash
curl -X POST https://api.perplexity.ai/search \
-H "Authorization: Bearer $PERPLEXITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "latest AI policy updates",
"max_results": 5,
"search_recency_filter": "month"
}'
```

**Via MCP** so the agent can call search as a tool. Perplexity ships an [official MCP server](https://github.com/perplexityai/modelcontextprotocol) on npm (`@perplexity-ai/mcp-server`); run it locally over stdio:

```json title="opencode.json" {4-13}
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"perplexity": {
"type": "local",
"command": ["npx", "-y", "@perplexity-ai/mcp-server"],
"environment": {
"PERPLEXITY_API_KEY": "{env:PERPLEXITY_API_KEY}"
}
}
}
}
```

The server exposes a `perplexity_search` tool (backed by the Search API) plus optional research and reasoning tools. See the [MCP server README](https://github.com/perplexityai/modelcontextprotocol#available-tools) for the tool list and the [Search API reference](https://docs.perplexity.ai/api-reference/search-post) for available filters and the response shape.

---

### LLM Gateway

1. Head over to the [LLM Gateway dashboard](https://llmgateway.io/dashboard), click **Create API Key**, and copy the key.
Expand Down
15 changes: 12 additions & 3 deletions packages/web/src/content/docs/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,21 @@ Allows the LLM to fetch and read web pages. Useful for looking up documentation

Search the web for information.

The `websearch` tool supports two backends:

- **Perplexity Search API** (default when `PERPLEXITY_API_KEY` is set). Calls Perplexity's `/search` REST endpoint directly.
- **Exa**. Used when `OPENCODE_ENABLE_EXA=1` is set or when the hosted `opencode` provider is in use.

Set `OPENCODE_DISABLE_PERPLEXITY=1` to force Exa even when a Perplexity key is present.

:::note
This tool is only available when using the OpenCode provider or when the `OPENCODE_ENABLE_EXA` environment variable is set to any truthy value (e.g., `true` or `1`).
This tool is only available when using the OpenCode provider, when `PERPLEXITY_API_KEY` is set, or when the `OPENCODE_ENABLE_EXA` environment variable is set to any truthy value (e.g., `true` or `1`).

To enable when launching OpenCode:

```bash
PERPLEXITY_API_KEY=pplx-... opencode
# or
OPENCODE_ENABLE_EXA=1 opencode
```

Expand All @@ -276,9 +285,9 @@ OPENCODE_ENABLE_EXA=1 opencode
}
```

Performs web searches using Exa AI to find relevant information online. Useful for researching topics, finding current events, or gathering information beyond the training data cutoff.
Performs web searches to find relevant information online. Useful for researching topics, finding current events, or gathering information beyond the training data cutoff.

No API key is required — the tool connects directly to Exa AI's hosted MCP service without authentication.
When the Perplexity backend is in use, the tool calls `https://api.perplexity.ai/search` with your `PERPLEXITY_API_KEY`. When the Exa backend is in use, the tool connects directly to Exa AI's hosted MCP service without authentication.

:::tip
Use `websearch` when you need to find information (discovery), and `webfetch` when you need to retrieve content from a specific URL (retrieval).
Expand Down
Loading