Skip to content

fix(openai): handle top-level code/param on Responses WS error frames#2024

Merged
davidzhao merged 2 commits into
mainfrom
bypast-fagots-glossing
Jul 11, 2026
Merged

fix(openai): handle top-level code/param on Responses WS error frames#2024
davidzhao merged 2 commits into
mainfrom
bypast-fagots-glossing

Conversation

@rosetta-livekit-bot

@rosetta-livekit-bot rosetta-livekit-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Omit nullish object fields when serializing OpenAI Responses WebSocket response.create requests, matching SDK behavior for unset optional fields.
  • Preserve top-level Responses WS error code and param fields so request-validation protocol errors surface cleanly.
  • Port the source regression coverage into the existing OpenAI Responses test file.

Source diff coverage

Classification
Source file Classification Target file Notes
livekit-plugins/livekit-plugins-openai/livekit/plugins/openai/responses/llm.py Adapted plugins/openai/src/ws/llm.ts, plugins/openai/src/ws/types.ts Python keeps Responses WS transport and event parsing in one file. JS splits WS transport into ws/llm.ts and schemas into ws/types.ts, so the serializer fix was adapted to JSON.stringify with a nullish-field replacer and the error-frame parsing fix was adapted by accepting top-level code/param and consulting top-level code in the handler.
tests/test_plugin_openai_responses.py Adapted plugins/openai/src/responses/llm.test.ts Ported the two regression tests into the existing Vitest Responses test file, using a minimal fake websocket to capture sent JSON and the existing Zod schema to assert top-level protocol errors parse without a sequence number.

Verification

  • pnpm build --filter @livekit/agents-plugin-openai... passed.
  • pnpm lint --filter @livekit/agents-plugin-openai... passed with existing warnings.
  • pnpm test -- plugins/openai/src/responses/llm.test.ts plugins/openai/src/ws/llm.test.ts passed.
  • pnpm test -- plugins/openai ran the full OpenAI plugin suite; 5 files passed and 2 existing suites failed outside this port.
  • Full OpenAI plugin suite failure: plugins/openai/src/stt.test.ts fails loading plugins/silero/dist/silero_vad.onnx with Protobuf parsing failed.
  • Full OpenAI plugin suite failure: plugins/openai/src/tts.test.ts fails because STT now requires a VAD for gpt-realtime-whisper.

Ported from livekit/agents#6385

Original PR description

Summary

The OpenAI Responses WebSocket transport (use_websocket=True, the default for responses.LLM) serializes request models by hand — json.dumps(msg, default=lambda o: o.model_dump(mode="json")) — instead of going through the openai SDK. The SDK omits unset fields; this hand-rolled path did not (exclude_none was missing), so every Optional field that defaults to None was emitted as an explicit null on the wire.

That is fine until the OpenAI schema grows an Optional enum field. openai-python 2.45.0 added Reasoning.mode (Literal["standard","pro"] | None, default None). The plugin auto-attaches Reasoning(effort="none") for gpt-5.x models, which now serializes as:

"reasoning": {"context": null, "effort": "none", "generate_summary": null, "mode": null, "summary": null}

and the Responses API rejects it with HTTP 400:

Invalid type for 'reasoning.mode': expected one of 'standard' or 'pro', but got null instead.

Worse, that 400 was invisible: the error-frame parser crashed while trying to report it (see fix #2), so users saw a misleading pydantic ValidationError about sequence_number instead of the real message.

This breaks any Responses-API user on the WS transport running openai ≥ 2.45.0 (gpt-5.x, where reasoning is auto-set). The HTTP transport (use_websocket=False) is unaffected because the SDK omits unset fields.

What changed

Two small fixes in responses/llm.py, plus a hermetic regression test.

  1. Serialization_ResponsesWebsocket.generate_response._default now uses model_dump(mode="json", exclude_none=True). This mirrors the SDK's on-the-wire shape and is forward-compatible with any future Optional field OpenAI adds to these models. (Matches the file's own existing convention — _handle_response_completed already uses exclude_none=True.)

  2. Error reporting_parse_ws_event defaults sequence_number when validating top-level error frames. Those frames (e.g. a request-validation 400) don't carry sequence_number, which ResponseErrorEvent marks required, so validating them raised a ValidationError that masked the real API message. With the default, genuine errors surface cleanly as an APIStatusError via _handle_error.

  3. Testtests/test_plugin_openai_responses.py, hermetic (no network/keys):

    • drives the real generate_response through a fake WS pool and asserts the serialized reasoning has no null-valued keys;
    • asserts a sequence_number-less error frame parses cleanly and preserves the message.

What this changes for users

Before: on openai ≥ 2.45.0, a gpt-5.x agent using the Responses API over WebSocket (the default) fails every turn with an opaque ValidationError about sequence_number; the underlying cause (a 400 on reasoning.mode) is hidden.

After: the request serializes the way the SDK would, the turn succeeds, and if any request is rejected the real API error message is surfaced instead of a parser crash.

Testing

  • uv run pytest tests/test_plugin_openai_responses.py --plugin openai → 2 passed.
  • Verified end-to-end against the live OpenAI Responses WebSocket on openai 2.45.0: a gpt-5.4 turn with Reasoning(effort="none") now returns response.completed (400 before the fix); a deliberately invalid request surfaces the real error message (pydantic crash before the fix).
  • Negative control: both the test and the live checks fail on the pre-patch code, confirming they guard the regression.
  • ruff format --check and ruff check pass on the changed file.

Notes

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from a team as a code owner July 11, 2026 16:16
@changeset-bot

changeset-bot Bot commented Jul 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: dff5d41

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 36 packages
Name Type
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-xai Patch
@livekit/agents Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugins-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@rosetta-livekit-bot rosetta-livekit-bot Bot requested a review from davidzhao July 11, 2026 16:16

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

The nullish-field replacer was preventive hardening with no live bug in
JS (unlike Python, unset optional fields are never serialized as null).
Keep only the error-frame fix: accept top-level code/param on Responses
WS error frames so error-code routing works for top-level codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@davidzhao davidzhao changed the title fix(openai): omit unset Responses WS fields fix(openai): handle top-level code/param on Responses WS error frames Jul 11, 2026
@davidzhao davidzhao merged commit 307ebdb into main Jul 11, 2026
8 checks passed
@davidzhao davidzhao deleted the bypast-fagots-glossing branch July 11, 2026 17:56
@github-actions github-actions Bot mentioned this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

list of as-yet-unfinished components and their dependencies

1 participant