diff --git a/CHANGELOG.md b/CHANGELOG.md index dc93e2d..9135b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ until 1.0, minor versions may contain breaking changes. ### Fixed +- OpenAI-compatible providers: tool results no longer carry a `name` field. Gateways that + translate to Anthropic (opencode) rejected the request with "anthropic-messages cannot + preserve named conversation participants". - Telegram: the token field is no longer filled in by the browser's password manager, the bot token is found in whatever is pasted (also the whole BotFather message), and a wrong format gets a clear message instead of "HTTP 404". diff --git a/core/src/provider/openai.rs b/core/src/provider/openai.rs index 10f0068..4b52ef5 100644 --- a/core/src/provider/openai.rs +++ b/core/src/provider/openai.rs @@ -47,10 +47,13 @@ pub fn to_wire_messages(system: &str, messages: &[Message]) -> Vec { Part::Image { image } => parts.push(json!({ "type": "image_url", "image_url": {"url": data_url(image)} })), - Part::ToolResult { call_id, name, content, images, is_error, .. } => { + Part::ToolResult { call_id, content, images, is_error, .. } => { let text = if *is_error { format!("ERROR: {content}") } else { content.clone() }; + // No "name": it is not part of the tool message in the Chat + // Completions API, and gateways that translate to Anthropic + // (opencode) reject named participants. out.push(json!({ - "role": "tool", "tool_call_id": call_id, "name": name, + "role": "tool", "tool_call_id": call_id, "content": if text.is_empty() { "(no output)".into() } else { text } })); tool_images.extend(images.iter().cloned()); @@ -328,6 +331,22 @@ mod tests { (text, reasoning) } + #[test] + fn tool_messages_have_no_name() { + use crate::types::{Message, Part, Role}; + let call = Part::ToolCall { id: "c1".into(), name: "bash".into(), input: serde_json::json!({"command": "ls"}), extra: None }; + let result = Part::ToolResult { call_id: "c1".into(), name: "bash".into(), content: "ok".into(), images: vec![], is_error: false, meta: None }; + let msgs = vec![ + Message::new(Role::User, vec![Part::Text { text: "hi".into() }]), + Message::new(Role::Assistant, vec![call]), + Message::new(Role::User, vec![result]), + ]; + let wire = super::to_wire_messages("sys", &msgs); + let tool = wire.iter().find(|m| m["role"] == "tool").expect("tool message"); + assert_eq!(tool["tool_call_id"], "c1"); + assert!(wire.iter().all(|m| m.get("name").is_none()), "no message may carry a name: {wire:?}"); + } + #[test] fn splits_think_tags_across_chunks() { let (text, reasoning) = run(&["plan ", "the fixHere is ", "the answer."]);