byok-opencode: no documented/headless way to configure BYOK provider — MCP rejects raw `byokProvider` keys, REST payload shape is undocumented
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_runtool: unconditionally rejects any call that includesbyokProvider(orapiKey, or a credential-shaped field insideinputs) 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, instartRun()— the check runs onObject.prototype.hasOwnProperty.call(args, 'byokProvider')etc.)REST
POST /api/runs: accepts the identicalbyokProviderobject directly in the request body and works (apps/daemon/src/routes/runs.tsforwardsrequestBody.byokProviderthrough to the run, andapps/daemon/src/runtimes/byok-opencode.tsbuilds 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'sagentCliEnvallowlist foropencode/byok-opencodeonly exposesOPENCODE_BIN(the binary path) — there is no allow-listed slot for a base URL, API key, or protocol for this runtime (seeAGENT_CLI_ENV_KEYSinapps/daemon/src/app-config.ts; compare to e.g.claude's entry, which does allowANTHROPIC_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_runpick 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, notprompt. - The project reference field is
projectId, notproject.
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
Call the MCP
start_runtool with abyokProviderobject 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()inapps/daemon/src/mcp.ts, guarded on the mere presence ofbyokProvider/apiKeyin the tool-call args.)Call
POST /api/runsdirectly against the daemon with what turns out to be the equivalent payload — but using the actual field names, discovered only by readingapps/daemon/src/routes/runs.ts(which forwardsbyokProviderstraight through) andapps/daemon/src/runtimes/byok-opencode.ts(which consumesprotocol/baseUrl/apiKey/apiVersionand 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.
Note the gap: there is no documented reference anywhere (README, API docs, MCP tool description) that states (a)
POST /api/runsis the sanctioned headless path for BYOK, (b) the exact shape ofbyokProvider, or (c) that the body usesmessage/projectIdrather than theprompt/projectnames accepted by the MCP tool. We only arrived at a working call by readingapps/daemon/src/routes/runs.ts,apps/daemon/src/runtimes/byok-opencode.ts, andpackages/contracts/src/api/chat.ts(ChatRequest,ByokChatProviderConfig) end to end.
Proposed fix / ask
One or both of the following would close the gap:
- Document the
POST /api/runsbyokProviderpayload shape officially for headless/API-driven usage with a custom OpenAI-compatible provider — field names (protocol,baseUrl,apiKey,apiVersion,model), acceptedprotocolvalues, and how this differs from the MCPstart_runargument names (messagevsprompt,projectIdvsproject). Right now this is only recoverable by source-readingapps/daemon/src/routes/runs.tsandapps/daemon/src/runtimes/byok-opencode.ts. - Provide a documented, persistent, headless-friendly BYOK configuration mechanism — e.g. an
agentCliEnvallowlist entry foropencode/byok-opencodethat accepts a base URL/protocol/API-key slot (analogous toANTHROPIC_BASE_URL/ANTHROPIC_API_KEYforclaude), or a config/credential-storage API endpoint — so a server deployment can configure "Local BYOK" once at startup without a browser, and MCPstart_runcalls 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_runtool, RESTPOST /api/runs - Related: #8227 (different specific gap in the same general BYOK/headless area — not duplicating its content here)
Source: nexu-io/open-design