Skip to content

role:system messages inside messages are mapped to assistant -> upstream 400 invalid_message_role #272

Description

@awcator

Summary

A /v1/messages request whose messages array ends with a role: "system" entry is translated to an OpenAI payload that ends on an assistant turn, which upstream (Copilot's Gemini backend) rejects with:

400 {"error":{"message":"The last message in a Gemini request must not be from the assistant. Ensure the conversation ends with a user or tool message.","code":"invalid_message_role"}}

Root cause

In src/routes/messages/non-stream-translation.ts, translateAnthropicMessagesToOpenAI only special-cases role === "user" and routes everything else through handleAssistantMessage:

const otherMessages = anthropicMessages.flatMap((message) =>
  message.role === "user" ?
    handleUserMessage(message)
  : handleAssistantMessage(message),
)

So a role: "system" message — which Anthropic clients such as Claude Code place in messages[] rather than the top-level system field — becomes an OpenAI assistant message. When that message is the last one, the forwarded request ends on an assistant turn and upstream returns invalid_message_role.

Repro

Works (single user message):

{"model":"gemini-3.6-flash","max_tokens":100,"messages":[{"role":"user","content":"hello"}]}

Fails (trailing role: "system" — what Claude Code actually sends):

{"model":"gemini-3.6-flash","max_tokens":100,"messages":[{"role":"user","content":"hello"},{"role":"system","content":"some instructions"}]}

role: "system" is not part of the AnthropicUserMessage | AnthropicAssistantMessage union in anthropic-types.ts, which is why the translator never handles it.

Suggested fix (minimal)

Fold any role: "system" message's text into the OpenAI system prompt instead of routing it to the assistant branch, so the last OpenAI message is never an assistant turn:

  • Treat role === "system" messages like the top-level system field (append text to the leading system message, or emit one if none exists).
  • Ensure a system-only/assistant-only tail can never reach upstream.

Related: #261 covers the adjacent "trailing assistant prefill" case but not inline role:"system" messages.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions