fix(neuron): accept bare {role,content} input on /v1/responses (agent-zero) #80

Merged
grenade merged 1 commits from fix/responses-easy-message-input into main 2026-06-26 16:53:09 +00:00
Owner

agent-zero (v2.0+, via litellm) drives the OpenAI Responses API and sends input items in the "easy input message" form — bare {role, content} with no type field. Our internally-tagged ResponsesInputItem rejected them, so axum's Json extractor returned 422 and every a0 request failed (both Main→beast and Utility→benjy). Confirmed by on-wire capture of 15 live requests: 36/36 items were bare easy-messages.

Round 1 makes input parsing match OpenAI's real tolerance (mirrors the top-level extra escape hatch): new ResponsesInputElement accepts typed items + bare easy-messages (content optional for null) + an Other(Value) catch-all so no unmodeled item can 422 the whole request again; ResponsesContentPart gets a #[serde(other)] Unknown arm; FunctionCallOutput.output becomes Value (string or array). Validated against all 15 captured bodies.

Tools forwarding + native function_call projection on the Responses path is a deliberate follow-up (Round 2), gated on observing a0's post-fix behaviour.

🤖 Generated with Claude Code

agent-zero (v2.0+, via litellm) drives the OpenAI Responses API and sends `input` items in the "easy input message" form — bare `{role, content}` with no `type` field. Our internally-tagged `ResponsesInputItem` rejected them, so axum's `Json` extractor returned **422** and every a0 request failed (both Main→beast and Utility→benjy). Confirmed by on-wire capture of 15 live requests: 36/36 items were bare easy-messages. Round 1 makes `input` parsing match OpenAI's real tolerance (mirrors the top-level `extra` escape hatch): new `ResponsesInputElement` accepts typed items + bare easy-messages (`content` optional for `null`) + an `Other(Value)` catch-all so no unmodeled item can 422 the whole request again; `ResponsesContentPart` gets a `#[serde(other)] Unknown` arm; `FunctionCallOutput.output` becomes `Value` (string or array). Validated against all 15 captured bodies. Tools forwarding + native `function_call` projection on the Responses path is a deliberate follow-up (Round 2), gated on observing a0's post-fix behaviour. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
grenade added 1 commit 2026-06-26 16:53:03 +00:00
fix(neuron): accept bare {role,content} input on /v1/responses (agent-zero)
All checks were successful
CI / Format (push) Successful in 41s
CI / CUDA type-check (push) Successful in 1m40s
CI / Clippy (push) Successful in 2m45s
CI / Test (push) Successful in 6m14s
CI / Build cortex SRPM (push) Has been skipped
CI / Build neuron SRPM (push) Has been skipped
CI / Publish cortex to COPR (push) Has been skipped
CI / Publish neuron to COPR (push) Has been skipped
CI / Bump version in source (push) Has been skipped
CI / Format (pull_request) Successful in 39s
CI / CUDA type-check (pull_request) Successful in 1m36s
CI / Clippy (pull_request) Successful in 3m18s
CI / Test (pull_request) Successful in 7m6s
CI / Build cortex SRPM (pull_request) Has been skipped
CI / Publish cortex to COPR (pull_request) Has been skipped
CI / Build neuron SRPM (pull_request) Has been skipped
CI / Publish neuron to COPR (pull_request) Has been skipped
CI / Bump version in source (pull_request) Has been skipped
6731adca51
agent-zero (via litellm) drives the OpenAI Responses API and sends
`input` items in the "easy input message" form — bare `{role, content}`
objects with NO `type` field. Our `ResponsesInputItem` is internally
tagged (`#[serde(tag="type")]`), so every such item failed the untagged
`ResponsesInput` deserialize and axum's `Json` extractor returned 422:

    OpenAIException - Failed to deserialize the JSON body into the target
    type: data did not match any variant of untagged enum ResponsesInput

This was a *total* failure for agent-zero (both the main model on beast
and the utility model on benjy), confirmed by on-wire capture of 15 live
requests: 36/36 input items were bare easy-messages. Other clients
(/v1/chat/completions, /v1/messages) were unaffected — only the
Responses path was exercised this strictly, for the first time.

Make `input`-item parsing match OpenAI's real tolerance, mirroring the
forward-compat `extra: Value` already at the top level of the request:

- New `ResponsesInputElement` wraps the existing typed item enum with
  two more shapes: `EasyMessage { role, content }` (bare, no type;
  `content` optional so an assistant turn with `content: null` parses)
  and `Other(Value)` — a catch-all so a single unmodeled item can never
  again 422 the whole request. The typed enum is unchanged.
- `ResponsesContentPart` gains a `#[serde(other)] Unknown` arm (e.g.
  `refusal`, audio) — dropped in translation, not rejected.
- `FunctionCallOutput.output` is now `Value` (string OR array of content
  parts, per OpenAI) so a structured tool result isn't lost.
- Translator handles all three element shapes; easy-messages translate
  exactly like typed messages, `Other` and unknown parts are dropped.

Tests cover the bare-message, null-content, unknown-item, unknown-part,
and array-tool-output shapes, validated against the 15 captured bodies.

Tools forwarding + native function_call projection on the Responses path
is deliberately a follow-up (Round 2), gated on observing how agent-zero
consumes responses once unblocked (in-band JSON vs native tool calls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RLyKaJVFDAYnAGiLvVrK8K
grenade merged commit 319b01e0b2 into main 2026-06-26 16:53:09 +00:00
grenade deleted branch fix/responses-easy-message-input 2026-06-26 16:53:09 +00:00
Sign in to join this conversation.