Conversation
`POST /responses` required every input item to name a string `type`, so a
plain OpenAI Responses `EasyInputMessage` — `{ "role": "user", "content":
"hello" }` — was answered with a 400 that neither reference server gives.
Both resolve an absent discriminator to `message` before dispatching: .NET's
item validator supplies the default from its custom discriminator resolver,
Python's `_request_validators.py` reads `value.get("type", "message")`.
The schema pass now dispatches the same way. An item whose `type` property is
absent is judged by the message rules — `role` and `content` are required, and
each is reported at its own path (`$.input[i].role`, `$.input[i].content`) —
and `parseCreateRequest` writes the resolved `message` onto a copy of the item
before returning. Normalizing in the parser rather than in the server is what
puts the resolved type in front of every later reader at once: the handler
receives the parsed request, and the same `input` array feeds the id minting
and the stored transcript, where an untyped item has no known id prefix and
would silently drop out of persistence.
The default applies only to an absent property. `type: null` and any other
non-string `type` stay a 400 at `$.input[i]`, an explicit discriminator is
preserved untouched, and `{ "id": "x" }` is still refused — referencing a
stored item needs the explicit `item_reference` discriminator, which continues
to be accepted. Neither the caller's request object nor its items are written
to: only the items that need the default are rebuilt, and a request where none
does is handed back as-is. The error envelope and HTTP status of every
malformed input are unchanged.
Fixes #100
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
Pull request overview
This pull request updates the agentserver’s POST /responses request validation to match reference server behavior by accepting OpenAI Responses-style input items that omit the type discriminator (e.g. { role, content }). It normalizes such items to type: "message" in parseCreateRequest so downstream paths (handler dispatch, id minting, and transcript persistence) consistently observe the resolved discriminator.
Changes:
- Extend schema validation to treat an input item with an absent
typeas a message shape and report field-level errors at$.input[i].role/$.input[i].content. - Normalize type-less input items to
{ type: "message", ...item }without mutating caller-owned objects. - Add targeted unit/integration tests and document the behavior change in the changelog.
File summaries
| File | Description |
|---|---|
| packages/agentserver/src/validation.ts | Defaults absent input item type to "message" via normalization while preserving error envelope and avoiding caller-object mutation. |
| packages/agentserver/src/validation.test.ts | Adds schema/normalization tests covering defaulting, non-mutation, invalid discriminators, and field-level error paths. |
| packages/agentserver/src/server.test.ts | Adds an end-to-end route test ensuring the handler and persistence see type: "message" and that ids are minted/persisted. |
| CHANGELOG.md | Records the wire-format/validation change for the agentserver package in Unreleased. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
# Conflicts: # CHANGELOG.md
Defaulting an absent `type` to `message` gave the two spellings of one shape
two different validators: a type-less item answered for `role` and `content`,
while an item that named `message` returned early and answered for nothing. A
caller could skip the checks by writing out the discriminator they belong to,
and `{ "type": "message", "id": "x" }` was accepted while the equivalent
type-less object was refused.
Both reference servers dispatch the two spellings to one validator. Python's
`_validate_OpenAI_Item` reads `value.get("type", "message")` and hands every
`message` — defaulted or written — to `_validate_OpenAI_ItemMessage`, which
requires `role` and `content`. The explicit spelling now takes the same path
here, reported at the same `$.input[i].role` / `$.input[i].content` paths.
Items of every other type keep returning to their own shape.
The terse `{ type: 'message', id }` fixtures in the server tests stood for
input a caller sends, so they carry a role and content now; the stored output
item in the handler fixture is untouched.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
CHANGELOG.md:51
- The PR description states "Breaking change: no", but this changelog entry is marked [BREAKING] and describes a behavior change (
{ "type": "message", "id": "x" }was accepted before and is now a 400). Please reconcile these two so release notes and PR metadata agree on whether this is considered a breaking change.
- **[BREAKING] `@polymind-inc/agent-framework-agentserver`** — an input item that names
`type: "message"` is held to the message rules, the same ones an item that omits its `type`
answers for. Both reference servers dispatch the two spellings to one validator — Python's
`_validate_OpenAI_ItemMessage` requires `role` and `content` whether the discriminator was
written out or defaulted — so writing `"type": "message"` no longer buys a laxer check than
leaving it off. `{ "type": "message", "id": "x" }` was accepted before and is now a 400 naming
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
| if (!isPlainObject(item)) { | ||
| wrong(path, "an object with a string 'type'"); | ||
| return; | ||
| } |
What this changes
Fixes #100.
POST /responsesrequired every input item to name a stringtype, so a plain OpenAIResponses
EasyInputMessage—{ "role": "user", "content": "hello" }— was answered with a 400that neither reference server gives. An item whose
typeproperty is absent is now judged by themessage rules and normalized to
type: "message"inparseCreateRequest, which is what puts theresolved discriminator in front of every later reader at once: the handler receives the parsed
request, and the same
inputarray feeds the id minting and the stored transcript, where anuntyped item had no known id prefix and dropped silently out of persistence.
The default applies only to an absent property —
type: nulland any other non-stringtypestaya 400 — and neither the caller's request object nor its items are written to.
An item that names
type: "message"is held to the same message rules. Defaulting the absentdiscriminator would otherwise have given one shape two validators, letting a caller skip the
role/contentchecks by writing out the discriminator those checks belong to.Parity
ItemValidatorCustom.ResolveDefaultDiscriminator, which supplies"message", and Python_request_validators.py, whose_validate_OpenAI_Itemreadsvalue.get("type", "message")and hands both the defaulted and the written-outmessageto thesingle
_validate_OpenAI_ItemMessage, which requiresroleandcontent. Both refuse an id-onlyobject, which needs the explicit
item_referencediscriminator; that stays refused here.Deliberate divergence: .NET also defaults a non-string
type, and this follows Python in treatingone as a broken discriminator instead.
type. A malformed message item isreported at
$.input[i].roleand$.input[i].contentrather than only at$.input[i]; the HTTPstatus and error envelope are unchanged.
What breaks:
{ "type": "message", "id": "x" }was accepted and is now a 400. Point at a stored item with{ "type": "item_reference", "id": "x" }, or give the message aroleandcontent. Items of every other type are unaffected.Checklist
pnpm checkpasses (lint, typecheck, build, test)