byok-opencode: no documented/headless way to configure BYOK provider — MCP rejects raw `byokProvider` keys, REST payload shape is undocumented

Author: estebanmunchjones2019Created Sep 17, 2026Updated Sep 17, 2026

Description

We're integrating the opencode / byok-opencode agent runtime against a self-hosted OpenAI-compatible LLM gateway (an internal LiteLLM-style proxy that speaks the OpenAI chat-completions protocol). This is a fully headless deployment — CI/Docker, no browser, no access to the Open Design desktop UI.

To start a run against a custom provider, the runtime needs a byokProvider object (protocol / baseUrl / apiKey / optional apiVersion, model) supplied on the request. In practice we found two very different behaviors depending on entry point:

  • MCP start_run tool: unconditionally rejects any call that includes byokProvider (or apiKey, or a credential-shaped field inside inputs) with a hard error, before even looking at the payload:

    raw API keys are not accepted by Open Design MCP. Configure Local BYOK in the Open Design UI and start that run from the local product instead.

    (apps/daemon/src/mcp.ts, in startRun() — the check runs on Object.prototype.hasOwnProperty.call(args, 'byokProvider') etc.)

  • REST POST /api/runs: accepts the identical byokProvider object directly in the request body and works (apps/daemon/src/routes/runs.ts forwards requestBody.byokProvider through to the run, and apps/daemon/src/runtimes/byok-opencode.ts builds the OpenCode provider/env config from it).

So the only working path for a headless/API-only integration is the raw REST endpoint — but that path is not documented anywhere as the sanctioned way to use a custom OpenAI-compatible provider. The MCP error message points to "Configure Local BYOK in the Open Design UI," but:

  • app-config's agentCliEnv allowlist for opencode / byok-opencode only exposes OPENCODE_BIN (the binary path) — there is no allow-listed slot for a base URL, API key, or protocol for this runtime (see AGENT_CLI_ENV_KEYS in apps/daemon/src/app-config.ts; compare to e.g. claude's entry, which does allow ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY).
  • There is no persistent, per-deployment BYOK credential storage/config endpoint we could find that a headless server could call once at startup and have every subsequent start_run pick up automatically — "Local BYOK" appears to mean UI-entered, browser-local state with no headless equivalent.

The practical result: every single caller (script, CI job, agent, etc.) has to construct and re-supply the exact byokProvider shape on every single run, and the only way to learn that shape today is reading daemon source, because it isn't in any API reference. Two shape details in particular are easy to get wrong and cost us real debugging time:

  • The chat/run payload field is message, not prompt.
  • The project reference field is projectId, not project.

Both were only discoverable by reading apps/daemon/src/routes/runs.ts and the ChatRequest / ByokChatProviderConfig types in packages/contracts/src/api/chat.ts.

Steps to reproduce

  1. Call the MCP start_run tool with a byokProvider object pointing at a custom OpenAI-compatible endpoint, e.g.:

    {
      "agentId": "byok-opencode",
      "project": "some-project-id",
      "prompt": "hello",
      "byokProvider": {
        "protocol": "openai",
        "baseUrl": "https://internal-llm-gateway.example.internal/v1",
        "apiKey": "***"
      }
    }
    

    Result: immediate rejection, before the run is even attempted:

    raw API keys are not accepted by Open Design MCP. Configure Local BYOK in the Open Design UI and start that run from the local product instead.
    

    (Thrown from startRun() in apps/daemon/src/mcp.ts, guarded on the mere presence of byokProvider/apiKey in the tool-call args.)

  2. Call POST /api/runs directly against the daemon with what turns out to be the equivalent payload — but using the actual field names, discovered only by reading apps/daemon/src/routes/runs.ts (which forwards byokProvider straight through) and apps/daemon/src/runtimes/byok-opencode.ts (which consumes protocol / baseUrl / apiKey / apiVersion and builds the OpenCode provider config + env):

    {
      "agentId": "byok-opencode",
      "projectId": "some-project-id",
      "message": "hello",
      "byokProvider": {
        "protocol": "openai",
        "baseUrl": "https://internal-llm-gateway.example.internal/v1",
        "apiKey": "***"
      }
    }
    

    Result: the run starts successfully and the internal LLM gateway is used as the provider.

  3. Note the gap: there is no documented reference anywhere (README, API docs, MCP tool description) that states (a) POST /api/runs is the sanctioned headless path for BYOK, (b) the exact shape of byokProvider, or (c) that the body uses message/projectId rather than the prompt/project names accepted by the MCP tool. We only arrived at a working call by reading apps/daemon/src/routes/runs.ts, apps/daemon/src/runtimes/byok-opencode.ts, and packages/contracts/src/api/chat.ts (ChatRequest, ByokChatProviderConfig) end to end.

Proposed fix / ask

One or both of the following would close the gap:

  1. Document the POST /api/runs byokProvider payload shape officially for headless/API-driven usage with a custom OpenAI-compatible provider — field names (protocol, baseUrl, apiKey, apiVersion, model), accepted protocol values, and how this differs from the MCP start_run argument names (message vs prompt, projectId vs project). Right now this is only recoverable by source-reading apps/daemon/src/routes/runs.ts and apps/daemon/src/runtimes/byok-opencode.ts.
  2. Provide a documented, persistent, headless-friendly BYOK configuration mechanism — e.g. an agentCliEnv allowlist entry for opencode/byok-opencode that accepts a base URL/protocol/API-key slot (analogous to ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY for claude), or a config/credential-storage API endpoint — so a server deployment can configure "Local BYOK" once at startup without a browser, and MCP start_run calls can then succeed without embedding a raw API key in every tool call.

Either change would let MCP's own error message ("Configure Local BYOK in the Open Design UI...") actually be actionable for deployments that have no UI to open.

Environment

  • Deployment: headless server / CI / Docker, no browser available
  • Agent runtime: opencode / byok-opencode
  • Provider: self-hosted OpenAI-compatible LLM gateway (internal proxy, OpenAI chat-completions protocol)
  • Entry points tested: MCP start_run tool, REST POST /api/runs
  • Related: #8227 (different specific gap in the same general BYOK/headless area — not duplicating its content here)