{"record":{"id":"8692848dc1002860","repo":"sigoden/aichat","slug":"invalid-messages","errorCode":null,"errorMessage":"Invalid messages","messagePattern":"Invalid messages","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/serve.rs","lineNumber":910,"sourceCode":"                        output.push(Message::new(\n                            MessageRole::Assistant,\n                            MessageContent::ToolCalls(MessageContentToolCalls::new(list, text)),\n                        ));\n                        tool_results = None;\n                    } else {\n                        tool_results = Some((text, tool_calls, tool_values));\n                    }\n                }\n                None => return Err(err()),\n            },\n            _ => {\n                return Err(err());\n            }\n        }\n    }\n\n    if tool_results.is_some() {\n        bail!(\"Invalid messages\");\n    }\n\n    Ok(output)\n}\n\nfn parse_tools(tools: Option<Vec<Value>>) -> Result<Option<Vec<FunctionDeclaration>>> {\n    let tools = match tools {\n        Some(v) => v,\n        None => return Ok(None),\n    };\n    let mut functions = vec![];\n    for (i, tool) in tools.into_iter().enumerate() {\n        if let (Some(\"function\"), Some(function)) = (\n            tool[\"type\"].as_str(),\n            tool[\"function\"]\n                .as_object()\n                .and_then(|v| serde_json::from_value(json!(v)).ok()),\n        ) {","sourceCodeStart":892,"sourceCodeEnd":928,"githubUrl":"https://github.com/sigoden/aichat/blob/82976d349ad97ac9aae0655ad631dace5e2a6385/src/serve.rs#L892-L928","documentation":"parse_messages rejects the request when 'tool_results' were set (i.e. messages contained tool/role content) but the message sequence is not a valid tool-call continuation. The OpenAI-compatible endpoint requires tool result messages to follow a corresponding assistant tool_calls message.","triggerScenarios":"POST /v1/chat/completions with a messages array where a tool message appears without a preceding assistant message containing tool_calls, or tool results are present in a malformed conversation shape.","commonSituations":"Clients replaying tool outputs after truncating the assistant tool_calls message; hand-built request bodies missing the assistant turn; agent loops that drop intermediate messages.","solutions":["Ensure every tool message is immediately preceded by an assistant message with tool_calls","Include the full conversation history from the assistant tool_calls turn onward","Match each tool message's tool_call_id to one of the assistant's tool_calls","If sending results without a tool call, send them as user/assistant content instead"],"exampleFix":"// before: tool result without the assistant tool_calls turn\n{\"messages\":[{\"role\":\"tool\",\"tool_call_id\":\"x\",\"content\":\"42\"}]}\n// after: include the assistant turn first\n{\"messages\":[{\"role\":\"assistant\",\"tool_calls\":[{\"id\":\"x\",...}]},{\"role\":\"tool\",\"tool_call_id\":\"x\",\"content\":\"42\"}]}","handlingStrategy":"validation","validationCode":"function validateToolFlow(messages) {\n  for (let i = 0; i < messages.length; i++) {\n    const m = messages[i];\n    if (m.role === 'tool') {\n      const prev = messages[i - 1];\n      if (!prev || prev.role !== 'assistant' || !prev.tool_calls) {\n        throw new Error(`tool message at ${i} has no preceding assistant tool_calls`);\n      }\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always send the complete tool-call conversation loop","Match tool_call_id to assistant tool_calls","Use an SDK that builds message arrays instead of hand-writing them"],"tags":["openai-api","messages","validation","tool-calls"],"backgroundTag":"schema-validation-failed","analyzedSha":"82976d349ad97ac9aae0655ad631dace5e2a6385","analyzedAt":"2026-09-09T18:33:06.139Z","contentChangedAt":"2026-09-09T18:33:06.139Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}