Skip to content

fix(models): accept non-standard roles in AnthropicMessage so normalization can run - #249

Open
Navapon wants to merge 1 commit into
jwadow:mainfrom
Navapon:fix/anthropic-message-role-422
Open

fix(models): accept non-standard roles in AnthropicMessage so normalization can run#249
Navapon wants to merge 1 commit into
jwadow:mainfrom
Navapon:fix/anthropic-message-role-422

Conversation

@Navapon

@Navapon Navapon commented Jul 17, 2026

Copy link
Copy Markdown

What

Widens AnthropicMessage.role from Literal["user", "assistant"] to str in kiro/models_anthropic.py, so requests containing inline non-standard roles (e.g. role: "system" sent by newer Claude Code clients for SessionStart hook context) are accepted and handled by the gateway's existing normalization pipeline instead of being rejected with:

422 {"detail":[{"type":"literal_error","loc":["body","messages",N,"role"],"msg":"Input should be 'user' or 'assistant'", ...}]}

Fixes #190
Fixes #226

Why this approach

  • The handling logic already existsnormalize_message_roles() in converters_core.py (added for issue BUG: API error calling from codex App #64) coerces any non-user/assistant role to user before ensure_alternating_roles(). The strict Literal on the Anthropic model rejects the request before that logic can run. This PR removes the gate rather than adding a new mechanism, handling the entire class of unknown-role issues ("system", "developer", future roles), not just one value.
  • Consistency across both APIs (per CONTRIBUTING.md): the OpenAI path already does exactly this — ChatMessage.role is str (kiro/models_openai.py:78), and tests/unit/test_routes_openai.py::test_validates_invalid_role already asserts unknown roles pass validation. This brings the Anthropic path into parity. Streaming and non-streaming share the same request model, so both modes are covered by one change.
  • Field-tested: we've been running this exact patch locally in front of Claude Code (Sonnet 5 / Opus 4.8) since it started sending inline system messages; the 422s disappeared and conversations flow normally.

Behavior change (intentional)

tests/unit/test_routes_anthropic.py::TestMessagesValidation::test_validates_invalid_role previously asserted unknown role → 422. That assertion enforced the bug, so it is replaced by test_accepts_non_standard_role (mirroring the existing OpenAI-side test of the same name and expectation).

Test coverage

  • tests/unit/test_models_anthropic.py — new TestAnthropicMessageRoleAcceptance (5 tests): user/assistant regression guards, system role (primary repro), arbitrary unknown role, and a full AnthropicMessagesRequest with an inline system message mid-conversation (exact Claude Code shape).
  • tests/unit/test_converters_anthropic.py — new end-to-end test test_inline_system_role_message_is_normalized: converts a request with an inline system message via anthropic_to_kiro() and asserts the Kiro history contains only userInputMessage/assistantResponseMessage entries and the system content survives as a user message.
  • tests/unit/test_routes_anthropic.py — route-level tests: test_accepts_non_standard_role and test_accepts_inline_system_role_message (no 422 at /v1/messages).

pytest -q: 1698 passed (full unit + integration suite).

Related PRs

I'm aware several open PRs address the same 422 (#191, #195, #197, #199, #205, #206, #207, #210, #234...). This one aims to be the minimal, test-covered variant that reuses the gateway's existing normalization rather than introducing new hoisting/merging logic — and it updates the route test that previously locked in the strict behavior. The merge-into-system approach discussed in #190 (comment by @W0n9) is a reasonable semantic refinement that could layer on top of this change later; happy to close this in favor of whichever direction the maintainer prefers.

…zation can run

Newer Claude Code clients inline role="system" messages (e.g. SessionStart
hook context) in the messages array. AnthropicMessage.role was
Literal["user", "assistant"], so Pydantic rejected these requests with
422 before normalize_message_roles() in converters_core.py could coerce
them to "user".

Widen role to str, matching the OpenAI path (ChatMessage.role is already
str) so both APIs behave consistently: unknown roles pass validation and
are normalized downstream before reaching the Kiro API.

Fixes jwadow#190
Fixes jwadow#226
@cla-bot

cla-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

Thanks for the PR! 🎉

Before merge, we need a one-time CLA confirmation.
It confirms that you have the right to contribute this code and allow the project to use it.

Full CLA text:
https://github.com/jwadow/kiro-gateway/blob/main/CLA.md

Please reply once with:

I have read the CLA and I accept its terms

You need to write once, all further messages from me can be ignored.

@Navapon

Navapon commented Jul 17, 2026

Copy link
Copy Markdown
Author

I have read the CLA and I accept its terms

@Navapon
Navapon marked this pull request as ready for review July 17, 2026 03:13
@ankitcharolia

Copy link
Copy Markdown

@Navapon could you give a try to this gateway: https://github.com/ankitcharolia/kiro-gateway

It supports all necessary endpoints for OpenAI and Anthropic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants