Skip to content

fix: accept OpenAI-compatible chat completions request shape - #23

Merged
aviweit merged 2 commits into
skillberry-ai:mainfrom
aviweit:fix/openai-compat-chat-completions
Jul 23, 2026
Merged

aviweit merged 2 commits into
skillberry-ai:mainfrom
aviweit:fix/openai-compat-chat-completions

Conversation

@aviweit

@aviweit aviweit commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

fix: accept OpenAI-compatible chat completions request shape

Problem

POST /chat/completions returned 422 Unprocessable Entity for any
client following the full OpenAI chat completions spec, including
OpenHands. Two distinct validation failures were occurring:

  1. content as a list of content blocks — OpenAI spec allows
    content to be [{"type": "text", "text": "..."}] (multimodal
    format). ChatMessage.content: str rejected this with
    Input should be a valid string.

  2. content: null on assistant tool-call messages — When an
    assistant message contains tool_calls, OpenAI sets content: null.
    Same rejection as above.

  3. Unknown fields — OpenAI clients send stream, top_p, stop,
    frequency_penalty, presence_penalty, etc. Pydantic's default
    strict mode rejected these with 422.

Changes (fast_api/api_server.py)

What Before After
ChatMessage.content type str Optional[Union[str, List[ContentBlock]]]
content: null 422 accepted, treated as ""
content: [{"type":"text","text":"..."}] 422 accepted, text parts joined
Unknown fields on ChatMessage 422 silently ignored
Unknown fields on ChatRequest 422 silently ignored

New ContentBlock model parses {type, text} blocks. New
_content_as_str() method flattens any content shape to a plain string
before passing to LangChain.

Backward compatibility

Fully backward compatible. Existing callers sending content as a plain
string hit the same str branch of the Union as before — no behaviour
change. The fix only widens what is accepted.

Testing

Verified with curl against the running server:

  • plain string content → 200 ✅
  • content: null → 200 ✅
  • content: [{"type":"text","text":"..."}] → 200 ✅
  • extra fields (stream, top_p, …) → 200 ✅
  • end-to-end OpenHands bench run via BenchFlow → agent reaches LLM, no more 422s ✅

- ChatMessage.content: str -> Optional[Union[str, List[ContentBlock]]]
  Clients like OpenHands send content as a list of typed blocks
  ({type, text}) for system/user messages and null for assistant
  tool-call messages. Both were rejected with 422 before this fix.

- Add ContentBlock model to parse {type, text} blocks; _content_as_str()
  flattens list content to a plain string for LangChain consumption.

- Add model_config extra='ignore' to ChatMessage and ChatRequest so
  unknown OpenAI fields (stream, top_p, stop, frequency_penalty, etc.)
  are silently dropped instead of causing a 422.

Fully backward compatible: existing callers sending plain strings are
unaffected (str branch of the Union is tried first by Pydantic).
@aviweit aviweit self-assigned this Jul 8, 2026
@aviweit
aviweit merged commit 482eacc into skillberry-ai:main Jul 23, 2026
1 check failed
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.

1 participant