Skip to content

GET /session/:id/message returns 400 when messages contain format with inline JSON schema #26929

Closed as not planned

Description

@dhshah13

Description

What happened?

When sending a message via POST /session/:id/prompt_async with a format field:

{
  "format": {
    "type": "json_schema",
    "schema": {
      "type": "object",
      "properties": {
        "status": { "type": "string", "enum": ["created", "needs_info"] },
        "summary": { "type": "string" }
      },
      "required": ["status", "summary"]
    },
    "retryCount": 2
  }
}

The prompt is accepted (204), the LLM runs and completes successfully. But when calling GET /session/:id/message to retrieve messages, the server returns 400:

{"name":"BadRequest","data":{"message":"Expected OutputFormatJsonSchema, got {\"type\":\"json_schema\",\"schema\":{...}}"}}

The format field matches the OutputFormatJsonSchema definition in the OpenAPI spec at GET /doc exactly (type, schema, retryCount with additionalProperties: false). The message listing endpoint's deserialization rejects what the prompt endpoint accepted.

This appears to be a regression in the May 10-11 2026 releases that tightened HTTP API validation. The same workflow worked on versions before May 10.

Plugins

None (using opencode serve as HTTP API server with MCP servers configured via opencode.json)

OpenCode version

1.14.48

Steps to reproduce

Steps to reproduce:

  1. Start opencode serve --port 4096
  2. Create a session: POST /session with {"title": "test"}
  3. Send a prompt with format: POST /session/:id/prompt_async with body containing "format": {"type": "json_schema", "schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}, "retryCount": 2}
  4. Wait for the LLM to complete (poll GET /session/status until not busy)
  5. List messages: GET /session/:id/message
  6. Observe 400 Bad Request with "Expected OutputFormatJsonSchema" error

Screenshot and/or share link

No response

Operating System

Linux (OpenShift container), also reproducible on macOS 25.4.0

Terminal

N/A (HTTP API usage via Python httpx client)

Activity

  1. JoAMD commented on May 23, 2026

    @JoAMD

    similar error for me
    Happened when i opened a session created in an older opencode version, upgraded opencode to latest, and then opened the same session again. Using web ui (TUI shows no previous messages)

    Something went wrong
    An error occurred while loading the application.

    Error Details

    Error: Unknown error
        at xf (.../assets/index-DiwOaO2o.js:2:10361)
        at .../assets/index-DiwOaO2o.js:2:3660
    
    
    
    ────────────────────────────────────────
    Caused by:
    BadRequest
    Expected OutputFormatJsonSchema, got {"type":"json_schema","schema": ......
    

    1.14.41 is the last working version for me

  2. ririnto commented on Jun 4, 2026

    @ririnto

    On current dev, this OutputFormatJsonSchema codec area appears to have regressed further — from a 400 on read to a hang on write.

    While adding a regression test that sends a structured prompt (format: { type: "json_schema", schema: { ... }, retryCount: 0 }), I found that persisting the user message never completes. The hang happens at message creation, before the model is ever called:

    SessionPrompt.prompt
      -> createUserMessage
        -> sessions.updateMessage(info)
          -> EventV2 publish of `message.updated` (durable)
            -> commitSyncEvent
              -> Schema.encodeUnknownSync(<message.updated data schema>)(event.data)   // does not terminate
    

    Schema.encodeUnknownSync of the durable message.updated event data does not terminate when info.format is an OutputFormatJsonSchema. Non-structured messages (no format) encode and persist normally on the exact same path; only format: json_schema triggers it. It reproduces deterministically (observed via a 3s test timeout in packages/opencode test/session/prompt.test.ts — the encode call never returns), with [email protected].

    This looks like the same root cause as the read-side 400 reported here (the OutputFormatJsonSchema / inline-JSON-schema codec), now also surfacing on the encode/persist side as a non-terminating encode rather than a validation error.

  3. akoukoullis commented on Jun 17, 2026

    @akoukoullis

    I just wanted to add another reproduction case to this thread.

    I'm using the Node.js SDK and calling client.session.promptAsync() with a format key containing a JSON schema to force structured JSON output. When I do this:

    • client.session.messages contains no text messages after the call completes
    • No SSE events fire that carry text message parts — the stream appears to go silent on the text side

    Removing the format key entirely restores normal behaviour — messages populate and SSE events fire as expected — but the final response is text. So it's definitely isolated to the format: json_schema code path, consistent with what's described here.

    It's still happening as of v1.17.7. Any update on a fix or workaround would be much appreciated.

  4. sud0why commented on Jul 2, 2026

    @sud0why

    I can reproduce this with matching versions:

    • opencode: 1.17.13
    • @opencode-ai/sdk: 1.17.13

    Using the SDK:

    await client.session.promptAsync({
      sessionID,
      agent: "build",
      parts: [{ type: "text", text: "Return {\"ok\": true}" }],
      format: {
        type: "json_schema",
        schema: {
          type: "object",
          properties: { ok: { type: "boolean" } },
          required: ["ok"],
          additionalProperties: false,
        },
        retryCount: 2,
      },
    });
    
    const result = await client.session.messages({ sessionID });

    promptAsync succeeds with HTTP 204, but session.messages() returns HTTP 400:

    Expected OutputFormatJsonSchema, got {"type":"json_schema","schema":...,"retryCount":2}
    

    Using the same format with synchronous session.prompt() works and returns info.structured.

    So it looks like the async prompt accepts the format, but the message listing endpoint later fails on it. This blocks SDK clients that use promptAsync + session.messages() with native structured output.

    Hope this can be fixed soon. Thanks!

  5. evilhikari commented on Aug 9, 2026

    @evilhikari

    Still reproduces on 1.18.15 (Desktop app on Windows) - months after this was reported.

    Additional data points from our debugging:

    PR #37541 already contains the fix (wire schemas changed from Schema.Class to Schema.Struct) with the regression test passing and a verified Windows build - but it has sat unreviewed/unmerged since Jul 17. People are actively hitting this. Can we get it reviewed and merged?

  6. Futuri-Risk commented on Aug 12, 2026

    @Futuri-Risk

    Confirming in 1.18.x — and the blast radius is much larger than "a 400 on message GET"

    Reproducing this on opencode 1.18.x (server) today, but via the most common real‑world trigger: the opencode-claude-memory plugin, whose recall-selector agent sends exactly this format on every session.prompt:

    {
      "type": "json_schema",
      "schema": {
        "type": "object",
        "properties": { "selected_memories": { "type": "array", "items": { "type": "string" } } },
        "required": ["selected_memories"],
        "additionalProperties": false
      },
      "retryCount": 2
    }

    The prompt is accepted, the LLM completes, but the server then rejects the persisted message on read‑back and retries the whole structured‑output call in a tight loop — indefinitely:

    level=WARN message="schema rejection" kind=Body
    reason="Expected OutputFormatJsonSchema, got {\"type\":\"json_schema\",\"schema\":{...},\"retryCount\":2}\n  at [0][\"info\"][\"format\"]"
    

    Why this matters beyond a 400

    1. It's a runaway retry loop, not a one‑off rejection. Each rejection retries every ~5 s, 24/7, for the life of the session. I measured >1,600 schema rejection lines/hour sustained over days from a single recall sub‑session — i.e. thousands of wasted LLM calls + a session that never goes idle. The original report stops at "GET returns 400"; the actual user‑visible behaviour is an infinite retry storm.
    2. It breaks the most popular memory plugin for everyone. Any opencode install with opencode-claude-memory enabled hits this on every turn — recall is non‑fatal to the main conversation, but it silently burns quota and stalls the session. The plugin conforms to the SDK's OutputFormatJsonSchema exactly; the server's validator is the one out of sync.
    3. It cascades into downstream integrations. I'm seeing this via kimaki (a Discord bridge over opencode): each stuck recall sub‑session gets mirrored into Discord as a Sync: thread, producing hundreds of spam threads across every channel + a bot stuck "typing…" forever. That downstream damage is what surfaced the bug for us — but it's a symptom of this same root cause.

    Expected vs actual

    • Expected: a format that the prompt endpoint accepts (and that matches GET /doc's OutputFormatJsonSchema) round‑trips through GET /session/:id/message unchanged.
    • Actual: the message‑listing deserialiser rejects its own persisted shape (Expected OutputFormatJsonSchema, got {"type":"json_schema",...}) and the caller retries forever.

    Environment

    • opencode 1.18.x (server, via kimaki → opencode SDK), Windows + the in‑process plugin path.
    • Triggering client: @opencode-ai/sdk session.prompt with format: { type: "json_schema", ... } (the recall‑selector pattern).

    Fix

    #37541 addresses exactly this — it changes the OutputFormat* schemas in packages/schema/src/v1/session.ts so persisted formats are encoded with a stable identifier and round‑trip cleanly, and adds a returns persisted output formats test covering the json_schema case. From reading the diff it should close both the HTTP‑API 400 and the in‑process retry loop reported here (same root cause: the persisted format losing its schema identifier on read‑back). Could this get some review eyes? The retry‑loop + quota‑burn angle makes it higher priority than the issue title suggests — every opencode-claude-memory user is affected.

  7. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions