DEV Community

lakshit gupta
lakshit gupta

Posted on

I Wanted to Try Pi Instead of OpenCode. Then I Had to Debug a 500.

I wanted to try Pi, the unopinionated coding harness, instead of OpenCode.

The idea was simple: keep my existing model setup, put Pi in front of it, and see how a less opinionated coding harness felt.

I already had OpenCodex (OCX) running as my local model proxy, so I expected this to be mostly configuration.

Instead, Muse Spark 1.2 Contributor Free started returning HTTP 500 errors.

What followed was a surprisingly deep rabbit hole involving OpenAI-compatible APIs, mitmproxy, OCX's adapter routing, and eventually a model-specific protocol mismatch.

And the final fix was only one configuration entry.

The setup

My intended architecture was:

Pi

OpenCodex (OCX)

Model providers

OCX exposes an OpenAI-compatible local API, so Pi could use it as its model backend.

Muse Spark 1.2 Contributor Free was already available through the OpenCode provider:

opencode-free/muse-spark-1.2-contributor-free

It was visible in live model discovery and the provider itself was reachable.

But requests through OCX returned:

500 Internal Server Error
First suspicion: authentication

The first thing I checked was authentication.

OCX's provider logs showed requests like:

[ocx:openai-chat:request]
{
"host": "opencode.ai",
"model": "muse-spark-1.2-contributor-free",
"stream": false,
"messageCount": 1,
"toolCount": 0,
"hasCredential": false
}

The hasCredential: false looked suspicious.

I tested the provider directly and OCX reported:

Connected — 64 models available.

So the provider itself was reachable.

I also tested with and without an authorization header.

The result was still a 500 for the free Muse model.

Authentication wasn't the real problem.

OpenCode was the clue

There was one useful fact:

OpenCode could use the exact same model successfully.

That gave me something much better than guessing.

Instead of asking what should happen, I could see what a working client was actually doing.

So I put OpenCode's traffic through mitmproxy.

That's where I found the important difference.

OpenCode was sending Muse Spark through:

POST /zen/v1/responses

not Chat Completions.

The request looked roughly like:

{
"model": "muse-spark-1.2-contributor-free",
"input": [...],
"max_output_tokens": 32000,
"reasoning": {
"effort": "xhigh",
"summary": "auto"
},
"stream": true
}

And the upstream response was:

HTTP/1.1 200 OK
Content-Type: text/event-stream

The stream contained the Responses API events, including reasoning and output text events, and completed successfully.

So the upstream model was working.

Now I had a much more interesting question:

Why was OCX getting a 500 when OpenCode wasn't?

Looking at the adapter

I started digging through the OCX source.

OCX has separate adapters for different OpenAI-compatible protocols, including:

openai-chat
openai-responses

My opencode-free provider was configured with:

{
"adapter": "openai-chat",
"baseUrl": "https://opencode.ai/zen/v1"
}

And the logs confirmed that Muse was being routed through the openai-chat adapter.

That gave me the mismatch:

OpenCode

Muse Spark

/responses

200 OK

while OCX was effectively doing:

OCX

Muse Spark

openai-chat

Chat Completions

500

The provider was OpenAI-compatible.

But that didn't mean every model necessarily wanted the same OpenAI-compatible wire.

The nice part: per-model routing

I didn't want to change the entire provider to openai-responses.

Other models were already working with the existing configuration.

While looking through OCX's configuration and adapter handling, I found support for per-model adapter selection.

That meant I could keep the provider itself on openai-chat and tell OCX that Muse Spark should use the Responses adapter.

I added:

{
"modelAdapters": {
"muse-spark-1.2-contributor-free": "openai-responses"
}
}

After applying the configuration, OCX reported the model-specific adapter:

{
"muse-spark-1.2-contributor-free": "openai-responses"
}
And then it worked

I sent the same request again.

Before:

500 Internal Server Error

After:

200 OK

The response was:

OCX Muse works

The final routing became:

Pi

OCX

opencode-free

Muse Spark

openai-responses

/zen/v1/responses

200 OK
The funny part

The entire investigation started because I wanted to try Pi.

I wasn't trying to reverse-engineer Muse Spark.

I wasn't trying to investigate OpenCode's HTTP implementation.

I wasn't even trying to debug OCX.

I just wanted to see what happened if I switched from OpenCode to a more unopinionated coding harness while keeping my existing model infrastructure.

That turned into:

Pi

500 error

Check credentials

Check provider

Compare with OpenCode

mitmproxy

Observe /responses

Inspect OCX source

Find model-specific adapter routing

openai-responses

200 OK

And honestly, this was one of those debugging sessions where the final configuration change is tiny, but getting to it is the interesting part.

The takeaway

The biggest lesson for me was that "OpenAI-compatible" is not necessarily one single wire protocol.

A provider can expose multiple models while different models—or different clients—may expect different API surfaces.

In this case, the model worked through the Responses API, while my proxy was initially sending it through the Chat Completions adapter.

The most useful debugging step wasn't staring at the 500.

It was capturing the request from the client that already worked and asking:

"What is this client actually sending?"

That turned a vague 500 into a concrete protocol mismatch.

And all of this because I wanted to try another coding harness. 😄

Top comments (0)