Skip to content
dsh.fish
Bundle

web-search-opencode-responses

dsh WebSearchProvider over the OpenCode Zen Go Responses API server-side web_search tool

Source
bailynlove
stars
1 stars
License
MIT
Updated
Updated 13 days ago

Readme

# 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 <name>`), 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#<sha>
```

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.

Install

dsh plugin --profile web add github:bailynlove/web-search-opencode-responses

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source