[GH-ISSUE #60] BUG: Improperly formed request when messages don't alternate or start with assistant #40

Closed
opened 2026-02-27 07:17:39 +03:00 by kerem · 3 comments
Owner

Originally created by @bhaskoro-muthohar on GitHub (Feb 1, 2026).
Original GitHub issue: https://github.com/jwadow/kiro-gateway/issues/60

Kiro Gateway Version

latest (as of 2026-02-01)

What happened?

When using kiro-gateway with OpenClaw (formerly ClawdBot) as the client, requests fail with HTTP 400 "Improperly formed request" when the conversation history contains:

  1. Multiple consecutive messages with the same role (e.g., several user messages in a row)
  2. Conversations that start with an assistant message instead of a user message

This happens because the Claude/Anthropic API requires:

  • Strict alternation between user and assistant roles
  • First message must be from user role

OpenClaw builds conversation history from chat platforms (WhatsApp, Telegram, etc.) where multiple messages from the same user can arrive before the bot responds, resulting in consecutive user messages.

Root Cause

LiteLLM handles this automatically in anthropic_messages_pt() by:

  1. Merging consecutive messages with the same role into a single message
  2. Prepending {"role": "user", "content": [{"type": "text", "text": "."}]} if the first message isn't from user

Kiro-gateway doesn't have this preprocessing, so malformed requests pass through to the Kiro API.

Suggested Fix

Add a merge_consecutive_messages() function that runs before anthropic_to_kiro():

from kiro.models_anthropic import AnthropicMessage

def merge_consecutive_messages(messages):
    """
    Merge consecutive messages with the same role and ensure first message is from user.
    Matches LiteLLM behavior for Anthropic API compatibility.
    """
    if not messages:
        return messages
    
    merged = []
    for msg in messages:
        if not merged or merged[-1].role != msg.role:
            merged.append(msg)
        else:
            prev_msg = merged[-1]
            
            def get_content_list(content):
                if isinstance(content, str):
                    return [{"type": "text", "text": content}]
                elif isinstance(content, list):
                    return list(content)
                return []
            
            prev_content = get_content_list(prev_msg.content)
            curr_content = get_content_list(msg.content)
            merged_content = prev_content + curr_content
            merged[-1] = AnthropicMessage(role=msg.role, content=merged_content)
    
    # Ensure first message is from user (LiteLLM uses "." as minimal content)
    if merged and merged[0].role != "user":
        synthetic_user = AnthropicMessage(
            role="user",
            content=[{"type": "text", "text": "."}]
        )
        merged.insert(0, synthetic_user)
    
    return merged

Debug Logs

Example failing request (before fix):

{
  "messages": [
    {"role": "assistant", "content": [...]},  // First message is assistant - INVALID
    {"role": "user", "content": [...]},
    {"role": "user", "content": [...]},       // Consecutive user messages - INVALID  
    {"role": "user", "content": [...]},
    {"role": "assistant", "content": [...]}
  ]
}

After fix, messages are properly merged (35 → 30 messages) and first message is user.

Originally created by @bhaskoro-muthohar on GitHub (Feb 1, 2026). Original GitHub issue: https://github.com/jwadow/kiro-gateway/issues/60 ## Kiro Gateway Version latest (as of 2026-02-01) ## What happened? When using kiro-gateway with **OpenClaw** (formerly ClawdBot) as the client, requests fail with HTTP 400 `"Improperly formed request"` when the conversation history contains: 1. Multiple consecutive messages with the same role (e.g., several user messages in a row) 2. Conversations that start with an assistant message instead of a user message This happens because the Claude/Anthropic API requires: - Strict alternation between `user` and `assistant` roles - First message must be from `user` role OpenClaw builds conversation history from chat platforms (WhatsApp, Telegram, etc.) where multiple messages from the same user can arrive before the bot responds, resulting in consecutive user messages. ## Root Cause LiteLLM handles this automatically in `anthropic_messages_pt()` by: 1. Merging consecutive messages with the same role into a single message 2. Prepending `{"role": "user", "content": [{"type": "text", "text": "."}]}` if the first message isn't from user Kiro-gateway doesn't have this preprocessing, so malformed requests pass through to the Kiro API. ## Suggested Fix Add a `merge_consecutive_messages()` function that runs before `anthropic_to_kiro()`: ```python from kiro.models_anthropic import AnthropicMessage def merge_consecutive_messages(messages): """ Merge consecutive messages with the same role and ensure first message is from user. Matches LiteLLM behavior for Anthropic API compatibility. """ if not messages: return messages merged = [] for msg in messages: if not merged or merged[-1].role != msg.role: merged.append(msg) else: prev_msg = merged[-1] def get_content_list(content): if isinstance(content, str): return [{"type": "text", "text": content}] elif isinstance(content, list): return list(content) return [] prev_content = get_content_list(prev_msg.content) curr_content = get_content_list(msg.content) merged_content = prev_content + curr_content merged[-1] = AnthropicMessage(role=msg.role, content=merged_content) # Ensure first message is from user (LiteLLM uses "." as minimal content) if merged and merged[0].role != "user": synthetic_user = AnthropicMessage( role="user", content=[{"type": "text", "text": "."}] ) merged.insert(0, synthetic_user) return merged ``` ## Debug Logs Example failing request (before fix): ```json { "messages": [ {"role": "assistant", "content": [...]}, // First message is assistant - INVALID {"role": "user", "content": [...]}, {"role": "user", "content": [...]}, // Consecutive user messages - INVALID {"role": "user", "content": [...]}, {"role": "assistant", "content": [...]} ] } ``` After fix, messages are properly merged (35 → 30 messages) and first message is user.
kerem 2026-02-27 07:17:39 +03:00
  • closed this issue
  • added the
    bug
    fixed
    labels
Author
Owner

@bhaskoro-muthohar commented on GitHub (Feb 1, 2026):

@jwadow I also noticed another issue that might affect users: CONTENT_LENGTH_EXCEEDS_THRESHOLD errors when conversations exceed the model's context window.

Probably the solution would be to add proactive context trimming before sending requests - similar to how LiteLLM handles this with their trim_messages() function. It could leverage the existing tokenizer.py infrastructure and trim oldest messages when approaching the limit.

Should I open a separate issue for this?

<!-- gh-comment-id:3831702050 --> @bhaskoro-muthohar commented on GitHub (Feb 1, 2026): @jwadow I also noticed another issue that might affect users: `CONTENT_LENGTH_EXCEEDS_THRESHOLD` errors when conversations exceed the model's context window. Probably the solution would be to add proactive context trimming before sending requests - similar to how LiteLLM handles this with their `trim_messages()` function. It could leverage the existing `tokenizer.py` infrastructure and trim oldest messages when approaching the limit. Should I open a separate issue for this?
Author
Owner

@jwadow commented on GitHub (Feb 2, 2026):

@jwadow I also noticed another issue that might affect users: CONTENT_LENGTH_EXCEEDS_THRESHOLD errors when conversations exceed the model's context window.

Probably the solution would be to add proactive context trimming before sending requests - similar to how LiteLLM handles this with their trim_messages() function. It could leverage the existing tokenizer.py infrastructure and trim oldest messages when approaching the limit.

Should I open a separate issue for this?

Hi, our gateway is transparent, we make minimal changes to the user's original request (with the exception of Kiro’s bugs and shortcomings).

If the user nevertheless encounters such a context blocking error, then we do not have the right to change the request itself. Because if a user made the exact same request to the official Anthropic API, they would encounter exactly the same problem. But Anthropic does not fix this problem in any way too.

This problem should be fixed by the client running our gateway, be it OpenCode, Claude Code or something else. That is, this is their problem. Therefore, I will not modify the request in any way, much less delete the context or unnecessary messages. Because user needs are absolutely diverse.

<!-- gh-comment-id:3833936904 --> @jwadow commented on GitHub (Feb 2, 2026): > [@jwadow](https://github.com/jwadow) I also noticed another issue that might affect users: `CONTENT_LENGTH_EXCEEDS_THRESHOLD` errors when conversations exceed the model's context window. > > Probably the solution would be to add proactive context trimming before sending requests - similar to how LiteLLM handles this with their `trim_messages()` function. It could leverage the existing `tokenizer.py` infrastructure and trim oldest messages when approaching the limit. > > Should I open a separate issue for this? Hi, our gateway is transparent, we make minimal changes to the user's original request (with the exception of Kiro’s bugs and shortcomings). If the user nevertheless encounters such a context blocking error, then we do not have the right to change the request itself. Because if a user made the exact same request to the official Anthropic API, they would encounter exactly the same problem. But Anthropic does not fix this problem in any way too. This problem should be fixed by the client running our gateway, be it OpenCode, Claude Code or something else. That is, this is their problem. Therefore, I will not modify the request in any way, much less delete the context or unnecessary messages. Because user needs are absolutely diverse.
Author
Owner

@jwadow commented on GitHub (Feb 2, 2026):

Good catch. Reproduced the issue - Kiro API rejects conversations that don't start with user role.

Fixed by prepending a minimal synthetic user message when needed (just a dot, like LiteLLM does). Tested with your examples, all pass now.

Updated CONTRIBUTORS.md with your contribution. Thanks for the detailed report.

<!-- gh-comment-id:3834041943 --> @jwadow commented on GitHub (Feb 2, 2026): Good catch. Reproduced the issue - Kiro API rejects conversations that don't start with user role. Fixed by prepending a minimal synthetic user message when needed (just a dot, like LiteLLM does). Tested with your examples, all pass now. Updated CONTRIBUTORS.md with your contribution. Thanks for the detailed report.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/kiro-gateway-jwadow#40
No description provided.