diff --git a/crates/agent/src/cli.rs b/crates/agent/src/cli.rs index 324bab607..2acab319a 100644 --- a/crates/agent/src/cli.rs +++ b/crates/agent/src/cli.rs @@ -150,9 +150,15 @@ fn build_summarizer(provider: &str, llm_client: Option) -> Option "claude-haiku-4-5-20251001", }; + let provider_name = match provider { + "openai" => Some("openai".to_string()), + "gemini" => Some("gemini".to_string()), + _ => None, + }; Some(crate::tools::WebFetchSummarizer { client, model: model.into(), + provider: provider_name, }) } diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index 10ba7570d..2886ea9ea 100644 --- a/crates/agent/src/tools.rs +++ b/crates/agent/src/tools.rs @@ -14,6 +14,7 @@ const MAX_WEB_FETCH_BYTES: usize = 100 * 1024; pub struct WebFetchSummarizer { pub client: Client, pub model: String, + pub provider: Option, } /// Returns true if the input looks like it contains HTML markup. @@ -535,7 +536,7 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option) -> Reg let request = Request { model: s.model.clone(), messages: vec![Message::user(summarization_prompt)], - provider: None, + provider: s.provider.clone(), tools: None, tool_choice: None, response_format: None, @@ -547,7 +548,9 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option) -> Reg metadata: None, provider_options: None, }; - let response = s.client.complete(&request).await.map_err(|e| format!("Summarization failed: {e}"))?; + let response = s.client.complete(&request).await.map_err(|e| { + format!("web_fetch summarization (model={}) failed: {e}", s.model) + })?; Ok(response.text()) } (Some(_), None) => { @@ -982,6 +985,7 @@ mod tests { let summarizer = WebFetchSummarizer { client, model: "mock-model".into(), + provider: None, }; let tool = make_web_fetch_tool(Some(summarizer)); @@ -1027,6 +1031,59 @@ mod tests { assert!(output.contains("Rust is a systems programming language"), "should contain page content, got: {output}"); } + #[tokio::test] + async fn web_fetch_summarizer_routes_to_specified_provider() { + use crate::test_support::{MockErrorProvider, MockLlmProvider, text_response}; + use llm::error::{ProviderErrorDetail, ProviderErrorKind, SdkError}; + use llm::provider::ProviderAdapter; + + // "other_provider" is the default — it rejects all requests. + let default_provider: Arc = Arc::new( + MockErrorProvider { + error: SdkError::Provider { + kind: ProviderErrorKind::NotFound, + detail: Box::new(ProviderErrorDetail::new( + "model not found", "other_provider", + )), + }, + }, + ); + // "target_provider" has the model we actually want. + let target_provider: Arc = Arc::new( + MockLlmProvider::new(vec![text_response("summarized content")]), + ); + + let mut providers = HashMap::new(); + providers.insert("other_provider".to_string(), default_provider); + providers.insert(target_provider.name().to_string(), target_provider); + let client = Client::new(providers, Some("other_provider".into()), vec![]); + + let summarizer = WebFetchSummarizer { + client, + model: "target-model".into(), + provider: Some("mock".into()), + }; + + let tool = make_web_fetch_tool(Some(summarizer)); + let env: Arc = Arc::new(MockExecutionEnvironment { + exec_result: ExecResult { + stdout: "

Page content

".into(), + stderr: String::new(), + exit_code: 0, + timed_out: false, + duration_ms: 100, + }, + ..Default::default() + }); + let result = (tool.executor)( + serde_json::json!({"url": "https://example.com", "prompt": "Summarize this"}), + ToolContext { env, cancel: CancellationToken::new() }, + ) + .await; + let output = result.expect("summarization should succeed when provider is correctly routed"); + assert_eq!(output, "summarized content"); + } + #[test] fn html_to_markdown_converts_basic_html() { let result = html_to_markdown("

Hello

World

"); diff --git a/crates/agent/tests/parity_matrix.rs b/crates/agent/tests/parity_matrix.rs index f2d897a57..b0dbf640f 100644 --- a/crates/agent/tests/parity_matrix.rs +++ b/crates/agent/tests/parity_matrix.rs @@ -8,14 +8,15 @@ use agent::{ use llm::client::Client; fn build_summarizer(provider: &str, client: &Client) -> WebFetchSummarizer { - let summarizer_model = match provider { - "openai" => "gpt-4o-mini", - "gemini" => "gemini-2.0-flash", - _ => "claude-haiku-4-5-20251001", + let (summarizer_model, provider_name) = match provider { + "openai" => ("gpt-4o-mini", Some("openai".to_string())), + "gemini" => ("gemini-2.0-flash", Some("gemini".to_string())), + _ => ("claude-haiku-4-5-20251001", None), }; WebFetchSummarizer { client: client.clone(), model: summarizer_model.into(), + provider: provider_name, } } @@ -420,9 +421,10 @@ async fn scenario_web_fetch(session: &mut Session, dir: &Path) { let path = dir.join("fetched.txt"); assert!(path.exists(), "fetched.txt should have been created"); let content = std::fs::read_to_string(&path).expect("failed to read fetched.txt"); + let lower = content.to_lowercase(); assert!( - content.contains("Example Domain"), - "Expected 'Example Domain' in fetched content, got first 200 chars: {}", + lower.contains("example domain") || lower.contains("example.com"), + "Expected 'Example Domain' or 'example.com' in fetched content, got first 200 chars: {}", &content[..content.len().min(200)] );