Summary
When the daemon is configured with LLM_API_PROTOCOL=chat_completions (e.g. for DeepSeek / vLLM / Ollama-style OpenAI-compatible upstreams that only implement /chat/completions), sandboxes created for the codex provider and for opencode with an openai/* model still get an LLM facade token issued for the responses wire protocol only. Guest-side calls to /chat/completions then fail with 403/405 (and retries surface as 502).
Observed behavior
- Daemon env:
LLM_API_PROTOCOL=chat_completions, LLM_API_ENDPOINT=<openai-compatible upstream>, LLM_MODEL=deepseek-v4-flash.
- Agent:
provider: opencode, model: openai/deepseek-v4-flash (also reproducible with provider: codex).
- Inside the sandbox:
echo $LLM_API_PROTOCOL prints responses (expected chat_completions).
- Daemon log:
POST /api/runtime/sandboxes/.../llm/openai/v1/responses 403, then retries fail; guest calls to /chat/completions return 405.
Root cause (current main)
The facade wire API is hardcoded to llms.APIProtocolResponses in three places:
pkg/llms/runtimefacade/config.go — ensureSessionCodexConfig: facadeWireAPI := llms.APIProtocolResponses
pkg/llms/runtimefacade/config.go — ensureOpenCodeOpenAIConfig: token + LLM_API_PROTOCOL env hardcoded to llms.APIProtocolResponses
pkg/runs/controller.go (~line 1048-1063) — prompt-attach path: token, WriteCodexRuntimeConfig and injected env all hardcoded to llms.APIProtocolResponses
Note that ensureOpenCodeCustomProviderConfig (used for openai-compatible/* models) already resolves the wire API from the provider config via llms.NormalizeWireAPI(target.WireAPI) — the three paths above should follow the same resolution (daemon LLM_API_PROTOCOL env / provider config) instead of hardcoding.
Workaround
Use a custom provider model id (e.g. openai-compatible/<model>) whose wire API resolves to chat_completions, so the facade token matches the protocol the agent actually speaks.
Environment
ghcr.io/chaitin/agent-compose:latest (2026-07), Linux x86_64, Docker bridge network
- Upstream: OpenAI-compatible gateway implementing only
/v1/chat/completions
Summary
When the daemon is configured with
LLM_API_PROTOCOL=chat_completions(e.g. for DeepSeek / vLLM / Ollama-style OpenAI-compatible upstreams that only implement/chat/completions), sandboxes created for the codex provider and for opencode with anopenai/*model still get an LLM facade token issued for theresponseswire protocol only. Guest-side calls to/chat/completionsthen fail with 403/405 (and retries surface as 502).Observed behavior
LLM_API_PROTOCOL=chat_completions,LLM_API_ENDPOINT=<openai-compatible upstream>,LLM_MODEL=deepseek-v4-flash.provider: opencode,model: openai/deepseek-v4-flash(also reproducible withprovider: codex).echo $LLM_API_PROTOCOLprintsresponses(expectedchat_completions).POST /api/runtime/sandboxes/.../llm/openai/v1/responses403, then retries fail; guest calls to/chat/completionsreturn 405.Root cause (current main)
The facade wire API is hardcoded to
llms.APIProtocolResponsesin three places:pkg/llms/runtimefacade/config.go—ensureSessionCodexConfig:facadeWireAPI := llms.APIProtocolResponsespkg/llms/runtimefacade/config.go—ensureOpenCodeOpenAIConfig: token +LLM_API_PROTOCOLenv hardcoded tollms.APIProtocolResponsespkg/runs/controller.go(~line 1048-1063) — prompt-attach path: token,WriteCodexRuntimeConfigand injected env all hardcoded tollms.APIProtocolResponsesNote that
ensureOpenCodeCustomProviderConfig(used foropenai-compatible/*models) already resolves the wire API from the provider config viallms.NormalizeWireAPI(target.WireAPI)— the three paths above should follow the same resolution (daemonLLM_API_PROTOCOLenv / provider config) instead of hardcoding.Workaround
Use a custom provider model id (e.g.
openai-compatible/<model>) whose wire API resolves tochat_completions, so the facade token matches the protocol the agent actually speaks.Environment
ghcr.io/chaitin/agent-compose:latest(2026-07), Linux x86_64, Docker bridge network/v1/chat/completions