Skip to content

Using Polaris from OpenCode

OpenCode integrates with Polaris natively — no translation gateway required (unlike Claude Code). There are two ways to bring the Advisor in:

  1. As an MCP tool — the Advisor MCP endpoint. OpenCode keeps using its normal coding model and calls the Advisor for grounded, cited answers. Recommended — you keep OpenCode's full agentic/coding ability and add the brain as a tool.
  2. As the model backend — the OpenAI-compatible completions endpoint configured as a custom provider. Every OpenCode turn is answered by the Advisor.

The Advisor is a knowledge provider, not a coding model

The Advisor serves grounded, cite-then-verified answers from a published brain — it has no tool-use/function-calling and only answers from the brain's corpus. Using it as OpenCode's model turns OpenCode into a Q&A harness over your brain, not a coding agent. For real work, prefer option 1 (MCP).

Both paths authenticate with a Polaris API key (sk-polaris-…, advisor:read scope). Mint one from the SPA (Settings → API keys) or the CLI (see Managing keys).

OpenCode config lives in ~/.config/opencode/opencode.json (global) or opencode.json in the project root. Secrets stay out of the config file via interpolation, and OpenCode offers two forms:

Syntax Reads from Use when
{file:~/path/to/key} contents of a file Recommended. Persists across shells and reboots — nothing to re-export.
{env:POLARIS_API_KEY} the shell environment You already manage the key via your shell profile, direnv, or a secrets agent.

OpenCode does not read .env files

This is the single most common way this setup fails. OpenCode only interpolates {env:VAR} from the environment of the shell that launched it — putting POLARIS_API_KEY=… in a project .env does nothing. Worse, an unset variable is silently replaced with an empty string, so the request goes out as a bare Authorization: Bearer and Polaris rejects it with an opaque 401:

✗ polaris-advisor  failed
    SSE error: Non-200 status code (401)

If you see that 401, check echo $POLARIS_API_KEY in the shell you started OpenCode from before suspecting the key itself. Prefer {file:…} to sidestep the problem entirely.

Recommended setup — write the raw key to a file and reference it with {file:…}:

mkdir -p ~/.secrets && chmod 700 ~/.secrets
printf '%s' 'sk-polaris-xxxx' > ~/.secrets/polaris-key   # printf, not echo — see below
chmod 600 ~/.secrets/polaris-key

No trailing newline

{file:…} substitutes the file's contents verbatim, newline included. echo appends one, which lands inside the Authorization header value and breaks auth. Use printf '%s', or truncate -s -1 a file you already wrote.

Alternative — if you'd rather use {env:…}, put the export somewhere your shell actually loads (~/.bashrc, ~/.zshrc), or use direnv with a .envrc containing dotenv to pull in an existing .env. A one-off export in a terminal you later close will not survive:

export POLARIS_API_KEY=sk-polaris-xxxx

OpenCode config lives in ~/.config/opencode/opencode.json (global) or opencode.json in the project root; {env:VAR} interpolates environment variables so secrets stay out of the file.

<host> below is wherever the gateway is published — localhost:8088 locally, or the public origin of a deployed host (the reference deployment serves /v1/ through nginx behind a Tailscale Funnel; see Edge & Tailscale).

OpenCode supports remote MCP servers directly. Point it at the streamable-http Advisor endpoint and pass the key as a bearer header (oauth: false tells OpenCode to use the header instead of attempting an OAuth flow):

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "polaris-advisor": {
      "type": "remote",
      "url": "https://<host>/v1/mcp",
      "enabled": true,
      "oauth": false,
      "headers": {
        "Authorization": "Bearer {file:~/.secrets/polaris-key}"
      }
    }
  }
}

Swap in "Bearer {env:POLARIS_API_KEY}" if you're managing the key through your shell instead — but re-read the warning above about .env first.

Verify the connection before doing anything else:

opencode mcp list
#  ●  ✓ polaris-advisor  connected

The brain is selected by the key, so there's nothing else to configure. The Advisor's tools (answer, retrieve, read_wiki) then appear to the model; reference them by the server name in a prompt:

What does our brain say about CA lender licensing? use polaris-advisor

Local dev host

Against a local dev.sh stack the URL is http://localhost:8088/v1/mcp (plain http, port 8088). If you keep both a local and a deployed Polaris wired up, mint a separate key per host and store them in distinct files (e.g. ~/.secrets/polaris-local-key vs ~/.secrets/polaris-key) — a key minted on one gateway is not valid on the other, and a local DB reset invalidates local keys. Verify the right one with opencode mcp list.

Make citations survive

This is the one behavior that surprises people. The Advisor's answer/retrieve tools return the answer and a rendered ## Sources block — one entry per citation with the verbatim quote, [source](origin_url), and [wiki: entity](ref) deep-links (labels cleaned, duplicates collapsed) — plus the full structured citation model alongside it. But OpenCode hands the tool result to your coding model, and a summarizing model — especially a small local one — often rewrites the answer and drops the citations. That's a model behavior, not a Polaris omission: the sources are in the tool result. (Confirm any time with opencode mcp list + a raw call, or see consuming citations.)

Two fixes, most effective first:

1. Add a standing rule so the agent always keeps sources. OpenCode auto-loads a global ~/.config/opencode/AGENTS.md for every project — put the rule there:

## Polaris Advisor (polaris-advisor MCP)

When you call any `polaris-advisor` tool (`answer`, `retrieve`, `read_wiki`):
- Always preserve its citations. If the tool's text response contains a `## Sources` section,
  reproduce it **verbatim** at the end of your reply — never summarize it away or drop it.
- Keep the `[source](…)` URLs and `[wiki: …](…)` deep-links intact.
- If the Advisor returns no sources, say so rather than implying the answer is grounded.

AGENTS.md is read at session startrestart OpenCode after creating it. (A project-local AGENTS.md, or the instructions array in opencode.json, works too; the global file applies everywhere. Note instructions paths are resolved relative to the project, so a global config should prefer the auto-loaded AGENTS.md.)

2. Use a stronger model for advisory turns. A larger model (Claude, or a bigger local model) preserves tool output far more reliably than a small local model. Keep your coding model for code and switch when citations matter, or add a per-prompt nudge: "…use polaris-advisor and include its Sources section."

Integration Prompt

You will need to create an API key for the advisor to use the MCP server. Have your coding agent do the wiring:

I have opencode installed. Look at https://polaris-themis-lime.tail8cee6e.ts.net/docs/OPENCODE/ for
instructions on how to configure opencode for polaris. Assume polaris-themis-lime.tail8cee6e.ts.net
is the hostname. Let's configure the MCP server first. My API key is in .env — note that opencode
does NOT read .env, so follow the doc's recommended {file:...} setup rather than pointing the config
at {env:...}. Verify with `opencode mcp list` in a shell where POLARIS_API_KEY is unset.

2. Advisor as a model provider (completions)

OpenCode speaks OpenAI Chat Completions via the @ai-sdk/openai-compatible provider, so it can call the Polaris completions endpoint directly. Set baseURL to the brain-scoped path (the SDK appends /chat/completions) and apiKey to your sk-polaris-… key:

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "polaris": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Polaris Advisor",
      "options": {
        "baseURL": "https://<host>/v1/brains/<brain_id>",
        "apiKey": "{file:~/.secrets/polaris-key}"
      },
      "models": {
        "polaris-advisor": {
          "name": "Polaris Advisor",
          "limit": { "context": 200000, "output": 65536 }
        }
      }
    }
  }
}

Then pick it with the model switcher (/models) or pin a default:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "polaris/polaris-advisor"
}
  • apiKey is sent as Authorization: Bearer … — exactly what the endpoint expects.
  • The endpoint ignores OpenCode's extra request fields (tools, sampling params) and always serves the grounded answer; it streams, so OpenCode renders progressively.
  • The Advisor's structured citations ride along in the response's polaris object / via response_format: json_object — see Structured metadata — but whether OpenCode surfaces that extra field depends on the AI SDK.

Caveats

  • No tool use. As a model backend the Advisor can't call tools, so OpenCode's editing/agentic loop degrades to Q&A. Keep OpenCode on a real coding model and use option 1 (MCP) to add the brain.
  • One brain per key. The MCP endpoint derives the brain from the key; the provider baseURL pins it in the path. Use separate keys/providers for separate brains.
  • Published brain required. Grounded answers need a published version of the brain.

Troubleshooting

polaris-advisor failed — SSE error: Non-200 status code (401)

The key never reached the server. In order of likelihood:

  1. Config uses {env:POLARIS_API_KEY} but the variable isn't set in the launching shell. The key living in a project .env does not count — OpenCode doesn't read .env. Interpolation of an unset variable yields an empty string, so the header goes out as a bare Bearer. Check with echo $POLARIS_API_KEY; switch to {file:…} for a durable fix.
  2. The key file has a trailing newline. printf '%s' writes it clean; echo does not. wc -c < ~/.secrets/polaris-key should match the key length exactly.
  3. Wrong scope or a revoked key. The key needs advisor:read. Confirm it independently:
curl -sS -o /dev/null -w '%{http_code}\n' -L -X POST https://<host>/v1/mcp \
  -H "Authorization: Bearer $(cat ~/.secrets/polaris-key)" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

200 means the key and endpoint are fine and the problem is in how OpenCode is resolving it.

curl needs -L, OpenCode does not

/v1/mcp may answer with a 308 redirect depending on how the host is fronted. OpenCode's MCP client follows it transparently, so both /v1/mcp and /v1/mcp/ work in config. Hand-rolled curl checks need -L or they'll report a confusing 308 that looks like an endpoint error.

References