From 465cc93ff32c2e0eead34512ac93273f175b0514 Mon Sep 17 00:00:00 2001 From: Bhargav Donga Date: Fri, 27 Feb 2026 23:33:20 -0800 Subject: [PATCH] fix: handle OpenClaw message format to resolve 422 validation errors OpenClaw sends message formats that don't match the strict OpenAI spec the wrapper previously enforced, causing 422 Unprocessable Content errors. - Accept role 'tool' in Message (tool call result messages) - Allow null/None content (assistant messages mid-tool-call) - Loosen ContentPart to accept any content block type beyond just 'text' - Extract text from tool_result content blocks during normalization - Add extra='ignore' to drop unknown fields (e.g. store, service_tier) Co-Authored-By: Claude Sonnet 4.6 --- src/models.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/models.py b/src/models.py index 82e85f4..990fc30 100644 --- a/src/models.py +++ b/src/models.py @@ -18,26 +18,44 @@ def get_default_model(): class ContentPart(BaseModel): """Content part for multimodal messages (OpenAI format).""" - type: Literal["text"] - text: str + type: str + text: Optional[str] = None + + model_config = {"extra": "ignore"} class Message(BaseModel): - role: Literal["system", "user", "assistant"] - content: Union[str, List[ContentPart]] + role: Literal["system", "user", "assistant", "tool"] + content: Optional[Union[str, List[Any]]] = None name: Optional[str] = None + model_config = {"extra": "ignore"} + @model_validator(mode="after") def normalize_content(self): """Convert array content to string for Claude Code compatibility.""" + if self.content is None: + self.content = "" + return self + if isinstance(self.content, list): # Extract text from content parts and concatenate text_parts = [] for part in self.content: - if isinstance(part, ContentPart) and part.type == "text": + if isinstance(part, ContentPart) and part.text: text_parts.append(part.text) - elif isinstance(part, dict) and part.get("type") == "text": - text_parts.append(part.get("text", "")) + elif isinstance(part, dict): + if part.get("type") == "text" and part.get("text"): + text_parts.append(part["text"]) + elif part.get("type") == "tool_result": + # Extract text from tool result content + inner = part.get("content", "") + if isinstance(inner, str): + text_parts.append(inner) + elif isinstance(inner, list): + for block in inner: + if isinstance(block, dict) and block.get("text"): + text_parts.append(block["text"]) # Join all text parts with newlines self.content = "\n".join(text_parts) if text_parts else ""