# web-search-opencode-responses A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) `WebSearchProvider` plugin that runs web search **server-side through the OpenAI Responses API** on the OpenCode Zen Go gateway — so `web_search` bills to your OpenCode plan instead of consuming DeepSeek API quota. The official `web-search-deepseek` provider is Anthropic-bound: it speaks DeepSeek's Anthropic-compatible Messages API (`POST /messages` on `https://api.deepseek.com/anthropic/v1`), a wire that only ever reaches DeepSeek's own endpoint — it **cannot be pointed at OpenCode**. This plugin exists to bypass that limitation: it swaps in the **OpenAI Responses API** (`POST /responses` with the native `web_search` server tool) against `https://opencode.ai/zen/go/v1`, so the same `WebSearchProvider` seam runs search on the OpenCode plan. The only thing carried over from the official provider is the working principle — one search is one model call whose inference backend executes the search; the wire, endpoint, and billing are all different. ## How sources are derived The gateway does not populate `url_citation` annotations, so the provider maps: 1. `web_search_call` actions of type `open_page` — pages the server actually opened (highest trust; the `#ws_call_id=…` tracking fragment is stripped); 2. `url_citation` annotations, when a gateway provides them (they override the hostname fallback title and can add snippets); 3. as a fallback, absolute URLs in the final answer text — what the model reports having found after searching. If none of these yield a source, the search **fails loud** (`WEB_PROVIDER_ERROR`) instead of returning an unsourced answer. ## Install (into a `web` profile) The package ships as a dsh **bundle**: one install command both adds the plugin dependency and applies the patch layer that points the web seam at this provider. From a profile directory (or with `--profile `), install from GitHub: ```sh dsh plugin --profile web add github:bailynlove/web-search-opencode-responses ``` That links the package, appends it to `dsh.profile.bundles`, and applies its `cordis.patch.yml` — inserting the `web-search-opencode-responses` row and setting `web.searchProvider: opencode-responses`. Verify the layer, then boot: ```sh dsh --profile web --dump-config # shows a "# == web-search-opencode-responses" layer dsh --profile web ``` To pin a specific release instead of the moving default branch, use a commit: ```sh dsh plugin --profile web add github:bailynlove/web-search-opencode-responses# ``` For a local checkout during development, keep the older `file:` wiring: declare `"web-search-opencode-responses": "file:./plugins/web-search-opencode-responses"` in the profile `package.json` and add the same two patch rows (insert the plugin, set `web.searchProvider: opencode-responses`) to the profile's `cordis.patch.yml`. ## Configuration The plugin registers a settings section (`web-search-opencode-responses`) on the Host. **Note:** the dsh web *Plugin configuration* tab only shows cards for the provider namespaces the web client ships (DeepSeek, Shell, Agent loop) — a third-party namespace like this one has no client card yet, so configure it through the settings document instead: - **key** — visual: dsh web **Settings → Models**, write the `OPENCODE_GO_API_KEY` credential (stored in `~/.dsh/.credentials.yaml`). - **endpoint / model / key reference** — edit the settings document `~/.dsh/settings.yaml`, opened in the web UI by the **Open configuration file** button: ```yaml web-search-opencode-responses: baseURL: https://opencode.ai/zen/go/v1 # optional; /responses is appended model: deepseek-v4-flash # optional; server-side search model apiKeyEnv: OPENCODE_GO_API_KEY # optional; credential reference apiKey: "" # optional; literal key over the ref ``` The file hot-reloads, so a change applies on the **next search**; the provider reads the section per search, no restart needed. Equivalent static values can be authored in the plugin's `cordis.patch.yml` entry (the composition base); the settings document overrides it. See the Configuration field table below: | field | default | meaning | | ----------- | -------------------------------- | ----------------------------------------- | | `apiKey` | – | literal key; prefer the credential store | | `apiKeyEnv` | `OPENCODE_GO_API_KEY` | credential reference resolved per search | | `baseURL` | `https://opencode.ai/zen/go/v1` | gateway endpoint; `/responses` appended | | `model` | `deepseek-v4-flash` | Responses model with server-side search | The key resolves through dsh's credentials service first (the web Models page writes `~/.dsh/.credentials.yaml`), then the launching environment; a stored secret never leaves the machine — the wire surface redacts it. ## Tests ```sh node --test test/mapping.test.mjs test/provider.test.mjs # unit, offline node test/integration.mjs # real API, needs the key ``` ## Notes - **Undocumented routing.** OpenCode's [go docs](https://opencode.ai/docs/go) list `deepseek-v4-flash` only under chat/completions and list Grok/GPT/Muse under `/v1/responses`. Empirically the gateway also routes deepseek models on `/v1/responses` and executes server-side `web_search` there (repeatedly verified with fresh, real results), matching DeepSeek's own Responses API, where `web_search` is documented as server-side. If OpenCode ever restricts routing to the documented matrix, searches start failing with `WEB_PROVIDER_ERROR` — loud, never fake. Note the officially listed responses models are not a fallback: grok-4.5 accepted the tool but executed no search and answered from memory. - Each search consumes one model round; the server injects search results into the model context (≈25k input tokens observed) — billed to OpenCode. - Search quality is the model's: it rewrites queries and opens pages autonomously across several rounds. - The answer-text URL fallback engages only when the output contains at least one executed `web_search_call` action — URLs from a model that never searched are memory, not sources.