fix(anthropic): accept inline system messages in messages array - #264
fix(anthropic): accept inline system messages in messages array#264willgosnold wants to merge 1 commit into
Conversation
|
Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Will Gosnold.
|
…ow#190, jwadow#219, jwadow#226, jwadow#255) Claude Code 2.1.x and its IDE extensions deliver runtime context (such as <system-reminder> blocks and agent-type listings) as messages with role="system" inside the messages array, rather than only in the top-level system field. AnthropicMessage.role was Literal["user", "assistant"], so these requests failed Pydantic validation with HTTP 422 before reaching the converter. The client could not complete a single turn. Relax role to str and add extract_inline_system_messages(), which peels system-role entries out of the conversation and merges their text into the system prompt (top-level content first, inline content after, so the client's ordering is preserved). This mirrors convert_openai_messages_to_unified(), which already extracts system messages the same way, keeping both API surfaces consistent. Hoisting rather than normalizing to "user" keeps the content as instructions instead of folding it into the user turn, and avoids ensure_alternating_roles() fabricating a synthetic assistant reply between a user turn and a following system message. A messages array containing only system entries now falls back to a placeholder user turn instead of raising "No messages to send".
619b416 to
188912d
Compare
|
Thanks for the PR! 🎉 Before merge, we need a one-time CLA confirmation. Full CLA text: Please reply once with: You need to write once, all further messages from me can be ignored. |
|
I have read the CLA and I accept its terms |
|
@willgosnold could you give a try to this gateway: https://github.com/ankitcharolia/kiro-gateway It works quite well with All AI harness and actively being developed. The most important thing is that it is ACP compliant |
Summary
Claude Code 2.1.x and its IDE extensions deliver runtime context —
<system-reminder>blocks,currentDate, agent-type listings — as messages withrole: "system"inside themessagesarray, not only in the top-levelsystemfield.AnthropicMessage.rolewasLiteral["user", "assistant"], so Pydantic rejected these with HTTP 422 before the converter ever ran, and the client could not complete a single turn.AnthropicMessage.roletostrextract_inline_system_messages()to peelrole="system"entries out of the conversation and merge their text into the system promptmessagesarray contains nothing but system entriesFixes #190
Fixes #219
Fixes #226
Fixes #255
Root cause
{"type": "literal_error", "loc": ["body", "messages", 1, "role"], "msg": "Input should be 'user' or 'assistant'", "input": "system"}Minimal repro against current
main:normalize_message_roles()already knows how to handle unrecognised roles (added fordeveloperin #64), but validation rejects the request before it can run.Why hoist instead of normalising to "user"
Two fixes were proposed across the linked issues: widen the
Literaland letnormalize_message_roles()fold the message into"user", or hoist the content into the system prompt. This PR hoists, for three reasons:convert_openai_messages_to_unified()(kiro/converters_openai.py:161) already extracts inline system messages and merges them into the system prompt. Normalising to"user"on the Anthropic path would leave the two surfaces behaving differently on identical input."user"leaves two adjacent user messages.normalize_message_roles()runs aftermerge_adjacent_messages()inbuild_kiro_payload(), so they never merge, andensure_alternating_roles()then inserts a synthetic(empty placeholder)assistant message between them — inventing an assistant reply that never happened, mid-conversation. Hoisting removes the messages before any of that logic sees them, so the core pipeline needs no reordering.Changes
kiro/models_anthropic.py—AnthropicMessage.role:Literal["user", "assistant"]→str. Chosen overLiteral[..., "system"]for forward compatibility, since clients have already shippeddeveloper(BUG: API error calling from codex App #64) and may ship more; unrecognised roles are still handled downstream.kiro/converters_anthropic.py— newextract_inline_system_messages(), wired intoanthropic_to_kiro(). Inline text is appended after the top-levelsystemcontent so the client's ordering is preserved. A system-onlymessagesarray falls back to an(empty placeholder)user turn instead of raisingValueError("No messages to send")./v1/messages/count_tokens(raised in #255) needed no separate change — it counts message text irrespective of role, so content is still counted once validation accepts it. Verified below.Tests
9 new tests in
tests/unit/test_converters_anthropic.py.TestExtractInlineSystemMessages— single extraction; list-of-blocks content withcache_control; multiple system messages joined in send order; no-system-messages passthrough; empty system message produces no stray separator.TestInlineSystemMessagesEndToEnd—role="system"accepted by the model; inline content merged into the system prompt with base-before-inline ordering asserted and no synthetic assistant turn in history; system-only array gets a placeholder turn; normal multi-turn conversation still builds correct history.tests/unit/test_routes_anthropic.py::test_validates_invalid_roleasserted the old 422 behaviour, so it is replaced bytest_accepts_unknown_role, which asserts the request passes validation.Test plan
/v1/messages/count_tokenswith an inline system message → 200messagesarray → 200test_auth_manager.py/test_config.pyare environment-dependent — local.env/credentials.jsonleaking into those tests — and are identical before and after this change)