Fix web_fetch summarizer routing to wrong LLM provider

The WebFetchSummarizer was sending requests without specifying a
provider, so they always routed to the default (Anthropic). When using
the OpenAI or Gemini profile, the summarizer model (e.g. gpt-4o-mini)
was rejected by Anthropic with a 404.

Add a `provider` field to WebFetchSummarizer so the summarization
request routes to the correct provider. Also improve the error message
to include the model name, and relax the parity test assertion to
accept summarized content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-28 01:14:04 -05:00
parent 13877b4e81
commit 3263d0c21e
3 changed files with 73 additions and 8 deletions

View file

@ -150,9 +150,15 @@ fn build_summarizer(provider: &str, llm_client: Option<Client>) -> Option<crate:
// anthropic and unknown providers
_ => "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,
})
}

View file

@ -14,6 +14,7 @@ const MAX_WEB_FETCH_BYTES: usize = 100 * 1024;
pub struct WebFetchSummarizer {
pub client: Client,
pub model: String,
pub provider: Option<String>,
}
/// Returns true if the input looks like it contains HTML markup.
@ -535,7 +536,7 @@ pub(crate) fn make_web_fetch_tool(summarizer: Option<WebFetchSummarizer>) -> 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<WebFetchSummarizer>) -> 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<dyn ProviderAdapter> = 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<dyn ProviderAdapter> = 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<dyn ExecutionEnvironment> = Arc::new(MockExecutionEnvironment {
exec_result: ExecResult {
stdout: "<html><body><p>Page content</p></body></html>".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("<h1>Hello</h1><p>World</p>");

View file

@ -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)]
);