From e992b57cede0911383f740ccaff23644346bebc9 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 23 Feb 2026 10:22:14 -0500 Subject: [PATCH] Add regression tests for Anthropic beta headers and Gemini thought_signature Covers two recent runtime failures that lacked test coverage: - Anthropic: assert deprecated beta header values are not sent - Gemini: test function call parsing/translation with and without thoughtSignature Co-Authored-By: Claude Opus 4.6 --- crates/llm/src/providers/anthropic.rs | 27 ++++++++++ crates/llm/src/providers/gemini.rs | 76 +++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/crates/llm/src/providers/anthropic.rs b/crates/llm/src/providers/anthropic.rs index 70b087c42..9434b2801 100644 --- a/crates/llm/src/providers/anthropic.rs +++ b/crates/llm/src/providers/anthropic.rs @@ -1881,6 +1881,33 @@ mod tests { assert_eq!(result["source"]["media_type"], "application/pdf"); } + /// Regression test: deprecated beta header values must not be sent. + /// The Anthropic API rejects requests containing these old headers. + #[test] + fn beta_header_rejects_deprecated_values() { + let deprecated = [ + "extended-thinking-2025-04-14", + "max-tokens-3-5-sonnet-2025-04-14", + ]; + + // No user headers — only cache header should appear + let header = build_beta_header(None, true).unwrap_or_default(); + for dep in &deprecated { + assert!(!header.contains(dep), "default header must not contain deprecated value {dep}"); + } + + // With a valid user header + let opts = serde_json::json!({ + "anthropic": { + "beta_headers": ["interleaved-thinking-2025-05-14"] + } + }); + let header = build_beta_header(Some(&opts), true).unwrap_or_default(); + for dep in &deprecated { + assert!(!header.contains(dep), "header with user values must not contain deprecated value {dep}"); + } + } + #[test] fn audio_produces_text_fallback() { let part = ContentPart::Audio(crate::types::AudioData { diff --git a/crates/llm/src/providers/gemini.rs b/crates/llm/src/providers/gemini.rs index a3fdcccd0..462310ab9 100644 --- a/crates/llm/src/providers/gemini.rs +++ b/crates/llm/src/providers/gemini.rs @@ -1259,6 +1259,82 @@ mod tests { } } + #[test] + fn parse_part_function_call() { + let part = serde_json::json!({ + "functionCall": { + "name": "get_weather", + "args": {"location": "NYC"} + } + }); + let result = parse_part(&part).expect("should parse function call"); + match result { + ContentPart::ToolCall(tc) => { + assert_eq!(tc.name, "get_weather"); + assert_eq!(tc.arguments, serde_json::json!({"location": "NYC"})); + assert!(tc.provider_metadata.is_none()); + } + other => panic!("expected ToolCall, got {other:?}"), + } + } + + #[test] + fn parse_part_function_call_with_thought_signature() { + let part = serde_json::json!({ + "functionCall": { + "name": "get_weather", + "args": {"location": "NYC"} + }, + "thoughtSignature": "abc123sig" + }); + let result = parse_part(&part).expect("should parse function call with thought signature"); + match result { + ContentPart::ToolCall(tc) => { + assert_eq!(tc.name, "get_weather"); + let meta = tc.provider_metadata.expect("provider_metadata should be set"); + assert_eq!(meta["thoughtSignature"], "abc123sig"); + } + other => panic!("expected ToolCall, got {other:?}"), + } + } + + #[test] + fn translate_messages_function_call_includes_thought_signature() { + let mut tc = ToolCall::new("call-1", "get_weather", serde_json::json!({"location": "NYC"})); + tc.provider_metadata = Some(serde_json::json!({"thoughtSignature": "sig456"})); + + let msg = Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); + assert_eq!(contents.len(), 1); + + let part = &contents[0].parts[0]; + assert!(part.get("functionCall").is_some()); + assert_eq!(part["thoughtSignature"], "sig456"); + } + + #[test] + fn translate_messages_function_call_without_thought_signature() { + let tc = ToolCall::new("call-1", "get_weather", serde_json::json!({"location": "NYC"})); + + let msg = Message { + role: Role::Assistant, + content: vec![ContentPart::ToolCall(tc)], + name: None, + tool_call_id: None, + }; + let contents = translate_messages(&[&msg]); + assert_eq!(contents.len(), 1); + + let part = &contents[0].parts[0]; + assert!(part.get("functionCall").is_some()); + assert!(part.get("thoughtSignature").is_none()); + } + #[test] fn parse_part_thought_false_is_regular_text() { let part = serde_json::json!({"text": "Regular text", "thought": false});