Hmbown/CodeWhale · error

only POST /v1/chat/completions is proxied

Error message

only POST /v1/chat/completions is proxied

What it means

The chat2responses proxy only forwards POST requests whose URL contains /chat/completions (plus GET /health). Anything else gets a 404 JSON error with this message. The proxy intentionally exposes a minimal surface.

Solutions

  1. Use POST with a URL containing /v1/chat/completions
  2. If the client speaks the Responses API, switch it to chat-completions mode or use a different proxy
  3. Use GET /health for health checks
  4. Check the client's base_url so the full path is /v1/chat/completions

Example fix

// before
POST /v1/responses
// after
POST /v1/chat/completions
Defensive patterns

Strategy: try-catch

Validate before calling

function assertProxyRequest(method, url) {
  if (method === "GET" && url === "/health") return;
  if (method !== "POST" || !url.includes("/chat/completions")) {
    throw new Error("proxy only accepts POST /v1/chat/completions or GET /health");
  }
}

Try / catch

const res = await fetch(proxyUrl, opts);
if (res.status === 404) {
  const body = await res.json();
  if (body.error === "only POST /v1/chat/completions is proxied") {
    throw new Error("Configure the client for chat-completions, not responses API");
  }
}

Prevention

When it happens

Trigger: Requesting /v1/responses or /models, using GET/PUT/DELETE on a chat-completions path, or hitting any other path with any verb other than the two allowed combos.

Common situations: Pointing an OpenAI SDK configured for the Responses API (POST /v1/responses) at this proxy, a client probing /health with POST, or a misconfigured base URL causing requests to /v1/chat/completions/foo with the wrong verb.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/a492ccdbe867113d. Report an issue: GitHub.

Appendix: source

Thrown at scripts/opencode-chat2responses-proxy.mjs:135

        out += `data: ${JSON.stringify({ id: evt.response?.id ?? "chatcmpl-proxy", object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: item.call_id, type: "function", function: { name: item.name, arguments: "" } }] }, finish_reason: null }] })}\n\n`;
      } else if (type === "response.function_call_arguments.delta") {
        out += `data: ${JSON.stringify({ id: evt.response?.id ?? "chatcmpl-proxy", object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta: { tool_calls: [{ index: 0, function: { arguments: evt.delta ?? "" } }] }, finish_reason: null }] })}\n\n`;
      } else if (type === "response.completed" || type === "response.incomplete") {
        out += `data: ${JSON.stringify({ id: evt.response?.id ?? "chatcmpl-proxy", object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`;
      }
    } catch {}
  }
  return out;
}

const server = http.createServer(async (req, res) => {
  if (req.method === "GET" && req.url === "/health") {
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ ok: true, upstream: UPSTREAM_BASE }));
    return;
  }
  if (req.method !== "POST" || !req.url?.includes("/chat/completions")) {
    res.writeHead(404, { "content-type": "application/json" });
    res.end(JSON.stringify({ error: "only POST /v1/chat/completions is proxied" }));
    return;
  }
  let body = "";
  req.on("data", (chunk) => (body += chunk));
  req.on("end", async () => {
    try {
      const chatBody = JSON.parse(body || "{}");
      const model = chatBody.model ?? "muse-spark-1.2-contributor-free";
      const isStream = chatBody.stream === true;
      const apiKey = req.headers.authorization?.replace(/^Bearer\s+/i, "") ?? process.env.OPENCODE_ZEN_API_KEY ?? "";
      const responsesBody = chatToResponses(chatBody);
      const upstreamUrl = `${UPSTREAM_BASE}${UPSTREAM_PATH}`;
      const headers = {
        "content-type": "application/json",
        accept: isStream ? "text/event-stream" : "application/json",
      };
      if (apiKey) headers.authorization = `Bearer ${apiKey}`;

View on GitHub (pinned to 433685b202)