use agentic_tools_core::error::ToolError;
use anthropic_async::types::ContentBlock;
use anthropic_async::types::MessageParam;
use anthropic_async::types::MessageRole;
use anthropic_async::types::MessagesCreateRequest;
use tracing::debug;
use crate::WebTools;
pub async fn summarize_markdown(tools: &WebTools, markdown: &str) -> Result<String, ToolError> {
const MAX_MARKDOWN_CHARS: usize = 100_000;
let markdown: String = markdown.chars().take(MAX_MARKDOWN_CHARS).collect();
let base_url = tools.anthropic_cfg.base_url.clone();
let client = tools
.anthropic
.get_or_try_init(|| async { init_anthropic_client(&base_url).await })
.await
.map_err(|e| ToolError::external(format!("Failed to initialize Anthropic client: {e}")))?;
let req = MessagesCreateRequest {
model: tools.cfg.summarizer.model.clone(),
max_tokens: tools.cfg.summarizer.max_tokens,
messages: vec![MessageParam {
role: MessageRole::User,
content: format!(
"Summarize the following web page content in 5-8 concise bullet points. \
Focus on the key facts and takeaways.\n\n{markdown}"
)
.into(),
}],
#[expect(clippy::cast_possible_truncation)]
temperature: Some(tools.cfg.summarizer.temperature as f32),
..Default::default()
};
let resp = client
.messages()
.create(req)
.await
.map_err(|e| ToolError::external(format!("Haiku API call failed: {e}")))?;
let text = resp
.content
.iter()
.filter_map(|block| {
if let ContentBlock::Text { text, .. } = block {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("\n");
Ok(text)
}
fn normalize_key(s: &str) -> Option<String> {
let t = s.trim();
if t.is_empty() {
None
} else {
Some(t.to_string())
}
}
async fn init_anthropic_client(
base_url: &str,
) -> Result<anthropic_async::Client<anthropic_async::AnthropicConfig>, ToolError> {
if let Ok(key) = std::env::var("ANTHROPIC_API_KEY")
&& let Some(key) = normalize_key(&key)
{
debug!("Using ANTHROPIC_API_KEY from environment");
let config = anthropic_async::AnthropicConfig::new()
.with_api_base(base_url)
.with_api_key(key);
return Ok(anthropic_async::Client::with_config(config));
}
match get_anthropic_key_from_opencode().await {
Ok(key) => {
debug!("Using Anthropic key from OpenCode provider");
let config = anthropic_async::AnthropicConfig::new()
.with_api_base(base_url)
.with_api_key(key);
Ok(anthropic_async::Client::with_config(config))
}
Err(e) => Err(ToolError::external(format!(
"No Anthropic credentials available. Set ANTHROPIC_API_KEY or ensure OpenCode is running. Error: {e}"
))),
}
}
async fn get_anthropic_key_from_opencode() -> Result<String, String> {
let client = opencode_rs::Client::builder()
.build()
.map_err(|e| format!("Failed to build OpenCode client: {e}"))?;
let providers = client
.providers()
.list()
.await
.map_err(|e| format!("Failed to list OpenCode providers: {e}"))?;
for provider in providers.all {
if provider.id == "anthropic"
&& let Some(key) = provider.key
&& !key.is_empty()
{
return Ok(key);
}
}
Err("No Anthropic provider found in OpenCode".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_key_returns_none_for_empty() {
assert_eq!(normalize_key(""), None);
}
#[test]
fn normalize_key_returns_none_for_whitespace_only() {
assert_eq!(normalize_key(" "), None);
assert_eq!(normalize_key("\t\n"), None);
assert_eq!(normalize_key(" \n "), None);
}
#[test]
fn normalize_key_trims_and_returns_valid_key() {
assert_eq!(normalize_key("key"), Some("key".to_string()));
assert_eq!(normalize_key(" key "), Some("key".to_string()));
assert_eq!(normalize_key("\nkey\n"), Some("key".to_string()));
assert_eq!(
normalize_key(" my-api-key-123 "),
Some("my-api-key-123".to_string())
);
}
}