Repository navigation
bug: opencode agent profiles never work — adapter requests OpenAPI operationIds as URL paths #212
Description
Summary
Every prompt sent to an opencode agent profile fails immediately with:
Expecting value: line 1 column 1 (char 0)
The root cause is in cptr/utils/agents/opencode.py: the adapter requests opencode's OpenAPI operationId values as if they were URL paths. Any route whose real path contains the {sessionID} parameter is therefore unreachable.
The affected route lists are byte-identical in 0.9.3, 0.9.7, 0.9.12, 0.9.16, 0.9.19 and 0.9.20, so this is not a regression — opencode profiles appear never to have worked in a released version.
Environment
- cptr 0.9.19 and 0.9.20 (both reproduce)
- opencode 1.18.5
- macOS, Python 3.11, host install via
uvx "cptr[agents]@latest"
Symptom
The assistant message contains only the error above. It appears ~90 ms after send — no model call is ever made. Server log:
File "cptr/utils/chat_task.py", line 1726, in _run_agent_target
raise RuntimeError(event.message)
AgentError(message='Expecting value: line 1 column 1 (char 0)')
RuntimeError: Expecting value: line 1 column 1 (char 0)
Root cause
_request() tries a list of candidate paths and returns the first that parses. Those candidates are opencode's operationIds, not routes. From opencode 1.18.5's own /doc:
| adapter requests | what the string actually is | real route |
|---|---|---|
session.create |
operationId | POST /session |
event.subscribe |
operationId | GET /event |
session.promptAsync |
operationId | POST /session/{sessionID}/prompt_async |
session.abort |
operationId | POST /session/{sessionID}/abort |
session.prompt |
operationId | POST /session/{sessionID}/message |
Why it half-works. For session.create and event.subscribe the real paths are the bare collection nouns /session and /event, and the adapter's third fallback happens to be exactly that — so session creation and the event stream succeed by coincidence. _start_opencode_prompt and the abort call map to routes containing {sessionID}, which no static candidate string can produce, so they always fail.
Why the error is a JSON error and not a 404. opencode serves its web UI for unmatched routes with HTTP 200, so response.raise_for_status() passes and response.json() then fails on <!doctype html> — which is exactly Expecting value: line 1 column 1 (char 0).
Reproduction
Configure any opencode agent profile with any model and send a message. Or directly against a running opencode serve:
# adapter's three prompt candidates -- all return the web UI with HTTP 200
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' -X POST \
"http://127.0.0.1:$PORT/session/prompt" \
-H 'Content-Type: application/json' -d '{"sessionID":"...","parts":[]}'
# 200 2884 <- HTML, not JSON
# the real route
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' -X POST \
"http://127.0.0.1:$PORT/session/$SID/prompt_async" \
-H 'Content-Type: application/json' \
-d '{"sessionID":"'"$SID"'","model":{"providerID":"...","modelID":"..."},"parts":[{"type":"text","text":"hi"}]}'
# 204 0 <- acceptedSecond defect, only observable once the routes are fixed
With the prompt route corrected, the reply is garbled two ways:
- Assistant text is emitted twice. opencode streams incremental
message.part.deltaevents and then a full-textmessage.part.updatedsnapshot. The delta branch of_text_from_eventnever records intoemitted, so the snapshot seesprevious == ""and re-emits the whole text. - The user's own prompt is echoed into the reply. opencode publishes the user's message as an ordinary text part, and
message.part.updatedis emitted for it without any role check.
Combined result for a one-line prompt: the prompt text followed by the answer repeated.
Suggested fix
In cptr/utils/agents/opencode.py:
- Prompt — try
f"session/{session_id}/prompt_async"first, keeping the existing names as fallbacks. - Abort — likewise
f"session/{session_id}/abort". _request— return{}whenresponse.contentis empty. The corrected prompt route answers204 No Content, which otherwise trips the identical JSON error. Empty body only: an HTML body must keep raising, or thesession.createprobe stops falling through to/session._text_from_event— build a role ledger frommessage.updated(it carriesproperties.sessionID, so it survives the session filter, plusproperties.info.{id,role}), skip parts whosemessageIDbelongs to a user message, and let the delta branch advanceemittedso the trailing snapshot is a no-op.
I've been running these four changes against opencode 1.18.5 for a day: clean single-copy replies, no prompt echo, and working tool calls.
Possibly related
#170 reports being unable to connect Computer to opencode. Different surface (Docker networking) and no error text quoted, so I filed separately, but it may share this root cause.
Activity
owui-terminator commentedon Aug 1, 2026
🔍 Related Issues Found
I found some existing issues that might be related. Please check if any of these are duplicates or contain helpful solutions:
This is the only clearly related issue found: it also concerns the opencode integration in Computer/Open WebUI and reports failure to connect to opencode. While the described cause differs (Docker networking), it is plausibly the same integration surface and may share adapter/protocol issues.
by insistence-essenn
💡 If your issue is a duplicate, please close it and add any additional details to the existing issue instead.
This comment was generated automatically. React with 👍 if helpful, 👎 if not.