Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
75 KiB
JSON
1 line
75 KiB
JSON
{"repo": "ailib-official/ai-lib-rust", "files": {"crates/ai-lib-rust/examples/tavily_tool_calling.rs": {"022cb30c227f111b8e044e5e37b9706339ffcf57": {"t": 1785745838.325615, "n": 10600, "h": "6b539bf785958088", "c": "//! Tavily Search Tool Calling Example\n//!\n//! This example demonstrates how to use tool calling with the Tavily search API.\n//! It shows a complete workflow:\n//! 1. Define the Tavily search tool\n//! 2. Send a request with the tool to a capable model\n//! 3. Parse the tool calls from the model response\n//! 4. Execute the Tavily search\n//! 5. Send the results back to the model for final response\n//!\n//! Prerequisites:\n//! Set one of the following environment variables:\n//! - DEEPSEEK_API_KEY (recommended for tool calling)\n//! - OPENAI_API_KEY\n//! - ANTHROPIC_API_KEY\n//! - GROQ_API_KEY\n//! - TAVILY_API_KEY (for actual Tavily searches) - optional, can use mock\n//!\n//! Usage:\n//! # Use DeepSeek (recommended)\n//! $env:DEEPSEEK_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling\n//!\n//! # Or use OpenAI\n//! $env:OPENAI_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling -- --provider openai\n\nuse ai_lib_rust::types::message::{ContentBlock, MessageContent, MessageRole};\nuse ai_lib_rust::types::tool::{FunctionDefinition, ToolCall, ToolDefinition, ToolResult};\nuse ai_lib_rust::{AiClient, Message};\nuse serde_json::{json, Value};\nuse std::env;\n\n/// Define the Tavily search tool\nfn tavily_search_tool() -> ToolDefinition {\n ToolDefinition {\n tool_type: \"function\".to_string(),\n function: FunctionDefinition {\n name: \"tavily_search\".to_string(),\n description: Some(\n \"Search the web using Tavily API for real-time information\".to_string(),\n ),\n parameters: Some(json!({\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"The search query to find information\"\n },\n \"search_depth\": {\n \"type\": \"string\",\n \"enum\": [\"basic\", \"advanced\"],\n \"default\": \"basic\",\n \"description\": \"Search depth (basic for quick results, advanced for comprehensive)\"\n },\n \"max_results\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10,\n \"default\": 5,\n \"description\": \"Maximum number of search results\"\n }\n },\n \"required\": [\"query\"]\n })),\n },\n }\n}\n\n/// Mock Tavily search implementation\n/// In production, this would call the actual Tavily API\nasync fn mock_tavily_search(\n query: &str,\n _depth: &str,\n _max_results: i32,\n) -> Result<Value, Box<dyn std::error::Error>> {\n println!(\"\\n🔍 Executing Tavily search for: {}\", query);\n\n // Simulated search results\n let results = json!({\n \"results\": [\n {\n \"title\": \"Latest News about Rust Programming\",\n \"url\": \"https://example.com/rust-news\",\n \"content\": \"Rust 1.82 released with improved performance and new features...\",\n \"published_date\": \"2025-02-08\"\n },\n {\n \"title\": \"Rust Web Framework Benchmarks 2025\",\n \"url\": \"https://example.com/rust-benchmarks\",\n \"content\": \"Comparison of Actix, Axum, and Rocket frameworks...\",\n \"published_date\": \"2025-02-05\"\n },\n {\n \"title\": \"Rust AI Libraries Overview\",\n \"url\": \"https://example.com/rust-ai\",\n \"content\": \"A comprehensive guide to AI/ML libraries in Rust ecosystem...\",\n \"published_date\": \"2025-02-01\"\n }\n ],\n \"response_time\": \"0.423s\",\n \"query\": query\n });\n\n Ok(results)\n}\n\n/// Process tool calls from model response\nasync fn process_tool_calls(\n tool_calls: &[ToolCall],\n) -> Result<Vec<ToolResult>, Box<dyn std::error::Error>> {\n let mut results = Vec::new();\n\n for tool_call in tool_calls {\n println!(\"\\n📌 Tool Call: {}\", tool_call.name);\n println!(\" ID: {}\", tool_call.id);\n println!(\n \" Arguments: {}\",\n serde_json::to_string_pretty(&tool_call.arguments)?\n );\n\n let result = if tool_call.name == \"tavily_search\" {\n let query = tool_call\n .arguments\n .get(\"query\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown\");\n let depth = tool_call\n .arguments\n .get(\"search_depth\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"basic\");\n let max_results = tool_call\n .arguments\n .get(\"max_results\")\n .and_then(|v| v.as_i64())\n .unwrap_or(5) as i32;\n\n match mock_tavily_search(query, depth, max_results).await {\n Ok(search_results) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: search_results,\n is_error: false,\n },\n Err(e) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": e.to_string() }),\n is_error: true,\n },\n }\n } else {\n ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": format!(\"Unknown tool: {}\", tool_call.name) }),\n is_error: true,\n }\n };\n\n results.push(result);\n }\n\n Ok(results)\n}\n\n/// Get provider from arguments or environment\nfn get_provider() -> String {\n let args: Vec<String> = env::args().collect();\n\n // Check for --provider argument\n if let Some(idx) = args.iter().position(|arg| arg == \"--provider\") {\n if let Some(provider) = args.get(idx + 1) {\n return format!(\"{}/auto\", provider);\n }\n }\n\n // Auto-detect from available environment variables\n if env::var(\"DEEPSEEK_API_KEY\").is_ok() {\n \"deepseek/auto\".to_string()\n } else if env::var(\"OPENAI_API_KEY\").is_ok() {\n \"openai/auto\".to_string()\n } else if env::var(\"ANTHROPIC_API_KEY\").is_ok() {\n \"anthropic/auto\".to_string()\n } else if env::var(\"GROQ_API_KEY\").is_ok() {\n \"groq/auto\".to_string()\n } else {\n eprintln!(\"❌ Error: No API key found in environment variables.\");\n eprintln!(\"Please set one of:\");\n eprintln!(\" - DEEPSEEK_API_KEY\");\n eprintln!(\" - OPENAI_API_KEY\");\n eprintln!(\" - ANTHROPIC_API_KEY\");\n eprintln!(\" - GROQ_API_KEY\");\n std::process::exit(1);\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n // Initialize tracing\n tracing_subscriber::fmt()\n .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())\n .init();\n\n println!(\"🚀 Tavily Search Tool Calling Example\\n\");\n\n let provider = get_provider();\n println!(\"📦 Using provider: {}\\n\", provider);\n\n // Create client\n let client = AiClient::new(&provider).await?;\n\n // First request: Ask the model to search for information\n let messages = vec![\n Message::system(\n \"You are a helpful research assistant. When asked about topics, use the tavily_search tool to find current information.\"\n ),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Force tool usage\n let tool_choice = json!({\n \"type\": \"function\",\n \"function\": { \"name\": \"tavily_search\" }\n });\n\n println!(\"📤 Sending initial request with tool definition...\\n\");\n\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.7)\n .max_tokens(1024)\n .tools(vec![tavily_search_tool()])\n .tool_choice(tool_choice)\n .execute()\n .await?;\n\n println!(\"✅ Initial response received\");\n println!(\" Content: {}\", resp.content);\n\n // Check if model called tools\n if resp.tool_calls.is_empty() {\n println!(\"\\n⚠️ No tool calls in response. Some models may not support tool calling.\");\n println!(\" Response: {}\", resp.content);\n return Ok(());\n }\n\n // Process tool calls\n println!(\"\\n🔄 Processing {} tool call(s)...\", resp.tool_calls.len());\n let tool_results = process_tool_calls(&resp.tool_calls).await?;\n\n // Second request: Send tool results back to model for final response\n println!(\"\\n📤 Sending tool results back to model...\\n\");\n\n let mut follow_up_messages = vec![\n Message::system(\"You are a helpful research assistant.\"),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Add assistant's tool call as a content block\n let mut assistant_blocks = vec![ContentBlock::text(&resp.content)];\n for tool_call in &resp.tool_calls {\n assistant_blocks.push(ContentBlock::ToolUse {\n id: tool_call.id.clone(),\n name: tool_call.name.clone(),\n input: tool_call.arguments.clone(),\n });\n }\n follow_up_messages.push(Message::with_content(\n MessageRole::Assistant,\n MessageContent::blocks(assistant_blocks),\n ));\n\n // Add tool results\n for result in tool_results {\n follow_up_messages.push(Message::with_content(\n MessageRole::User,\n MessageContent::blocks(vec![ContentBlock::ToolResult {\n tool_use_id: result.tool_use_id,\n content: result.content,\n }]),\n ));\n }\n\n // Get final response\n let final_resp = client\n .chat()\n .messages(follow_up_messages)\n .temperature(0.7)\n .max_tokens(2048)\n .execute()\n .await?;\n\n println!(\"✅ Final response received\\n\");\n println!(\"📝 Assistant Response:\\n{}\", final_resp.content);\n\n if let Some(usage) = final_resp.usage {\n println!(\"\\n📊 Token Usage:\");\n if let Some(prompt_tokens) = usage.get(\"prompt_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Prompt tokens: {}\", prompt_tokens);\n }\n if let Some(completion_tokens) = usage.get(\"completion_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Completion tokens: {}\", completion_tokens);\n }\n }\n\n println!(\"\\n✨ Example completed successfully!\");\n\n Ok(())\n}\n"}, "d32d4bc6a5c18cddebc9050cc5746a3496d8f58d": {"t": 1785757088.238675, "n": 10600, "h": "6b539bf785958088", "c": "//! Tavily Search Tool Calling Example\n//!\n//! This example demonstrates how to use tool calling with the Tavily search API.\n//! It shows a complete workflow:\n//! 1. Define the Tavily search tool\n//! 2. Send a request with the tool to a capable model\n//! 3. Parse the tool calls from the model response\n//! 4. Execute the Tavily search\n//! 5. Send the results back to the model for final response\n//!\n//! Prerequisites:\n//! Set one of the following environment variables:\n//! - DEEPSEEK_API_KEY (recommended for tool calling)\n//! - OPENAI_API_KEY\n//! - ANTHROPIC_API_KEY\n//! - GROQ_API_KEY\n//! - TAVILY_API_KEY (for actual Tavily searches) - optional, can use mock\n//!\n//! Usage:\n//! # Use DeepSeek (recommended)\n//! $env:DEEPSEEK_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling\n//!\n//! # Or use OpenAI\n//! $env:OPENAI_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling -- --provider openai\n\nuse ai_lib_rust::types::message::{ContentBlock, MessageContent, MessageRole};\nuse ai_lib_rust::types::tool::{FunctionDefinition, ToolCall, ToolDefinition, ToolResult};\nuse ai_lib_rust::{AiClient, Message};\nuse serde_json::{json, Value};\nuse std::env;\n\n/// Define the Tavily search tool\nfn tavily_search_tool() -> ToolDefinition {\n ToolDefinition {\n tool_type: \"function\".to_string(),\n function: FunctionDefinition {\n name: \"tavily_search\".to_string(),\n description: Some(\n \"Search the web using Tavily API for real-time information\".to_string(),\n ),\n parameters: Some(json!({\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"The search query to find information\"\n },\n \"search_depth\": {\n \"type\": \"string\",\n \"enum\": [\"basic\", \"advanced\"],\n \"default\": \"basic\",\n \"description\": \"Search depth (basic for quick results, advanced for comprehensive)\"\n },\n \"max_results\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10,\n \"default\": 5,\n \"description\": \"Maximum number of search results\"\n }\n },\n \"required\": [\"query\"]\n })),\n },\n }\n}\n\n/// Mock Tavily search implementation\n/// In production, this would call the actual Tavily API\nasync fn mock_tavily_search(\n query: &str,\n _depth: &str,\n _max_results: i32,\n) -> Result<Value, Box<dyn std::error::Error>> {\n println!(\"\\n🔍 Executing Tavily search for: {}\", query);\n\n // Simulated search results\n let results = json!({\n \"results\": [\n {\n \"title\": \"Latest News about Rust Programming\",\n \"url\": \"https://example.com/rust-news\",\n \"content\": \"Rust 1.82 released with improved performance and new features...\",\n \"published_date\": \"2025-02-08\"\n },\n {\n \"title\": \"Rust Web Framework Benchmarks 2025\",\n \"url\": \"https://example.com/rust-benchmarks\",\n \"content\": \"Comparison of Actix, Axum, and Rocket frameworks...\",\n \"published_date\": \"2025-02-05\"\n },\n {\n \"title\": \"Rust AI Libraries Overview\",\n \"url\": \"https://example.com/rust-ai\",\n \"content\": \"A comprehensive guide to AI/ML libraries in Rust ecosystem...\",\n \"published_date\": \"2025-02-01\"\n }\n ],\n \"response_time\": \"0.423s\",\n \"query\": query\n });\n\n Ok(results)\n}\n\n/// Process tool calls from model response\nasync fn process_tool_calls(\n tool_calls: &[ToolCall],\n) -> Result<Vec<ToolResult>, Box<dyn std::error::Error>> {\n let mut results = Vec::new();\n\n for tool_call in tool_calls {\n println!(\"\\n📌 Tool Call: {}\", tool_call.name);\n println!(\" ID: {}\", tool_call.id);\n println!(\n \" Arguments: {}\",\n serde_json::to_string_pretty(&tool_call.arguments)?\n );\n\n let result = if tool_call.name == \"tavily_search\" {\n let query = tool_call\n .arguments\n .get(\"query\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown\");\n let depth = tool_call\n .arguments\n .get(\"search_depth\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"basic\");\n let max_results = tool_call\n .arguments\n .get(\"max_results\")\n .and_then(|v| v.as_i64())\n .unwrap_or(5) as i32;\n\n match mock_tavily_search(query, depth, max_results).await {\n Ok(search_results) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: search_results,\n is_error: false,\n },\n Err(e) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": e.to_string() }),\n is_error: true,\n },\n }\n } else {\n ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": format!(\"Unknown tool: {}\", tool_call.name) }),\n is_error: true,\n }\n };\n\n results.push(result);\n }\n\n Ok(results)\n}\n\n/// Get provider from arguments or environment\nfn get_provider() -> String {\n let args: Vec<String> = env::args().collect();\n\n // Check for --provider argument\n if let Some(idx) = args.iter().position(|arg| arg == \"--provider\") {\n if let Some(provider) = args.get(idx + 1) {\n return format!(\"{}/auto\", provider);\n }\n }\n\n // Auto-detect from available environment variables\n if env::var(\"DEEPSEEK_API_KEY\").is_ok() {\n \"deepseek/auto\".to_string()\n } else if env::var(\"OPENAI_API_KEY\").is_ok() {\n \"openai/auto\".to_string()\n } else if env::var(\"ANTHROPIC_API_KEY\").is_ok() {\n \"anthropic/auto\".to_string()\n } else if env::var(\"GROQ_API_KEY\").is_ok() {\n \"groq/auto\".to_string()\n } else {\n eprintln!(\"❌ Error: No API key found in environment variables.\");\n eprintln!(\"Please set one of:\");\n eprintln!(\" - DEEPSEEK_API_KEY\");\n eprintln!(\" - OPENAI_API_KEY\");\n eprintln!(\" - ANTHROPIC_API_KEY\");\n eprintln!(\" - GROQ_API_KEY\");\n std::process::exit(1);\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n // Initialize tracing\n tracing_subscriber::fmt()\n .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())\n .init();\n\n println!(\"🚀 Tavily Search Tool Calling Example\\n\");\n\n let provider = get_provider();\n println!(\"📦 Using provider: {}\\n\", provider);\n\n // Create client\n let client = AiClient::new(&provider).await?;\n\n // First request: Ask the model to search for information\n let messages = vec![\n Message::system(\n \"You are a helpful research assistant. When asked about topics, use the tavily_search tool to find current information.\"\n ),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Force tool usage\n let tool_choice = json!({\n \"type\": \"function\",\n \"function\": { \"name\": \"tavily_search\" }\n });\n\n println!(\"📤 Sending initial request with tool definition...\\n\");\n\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.7)\n .max_tokens(1024)\n .tools(vec![tavily_search_tool()])\n .tool_choice(tool_choice)\n .execute()\n .await?;\n\n println!(\"✅ Initial response received\");\n println!(\" Content: {}\", resp.content);\n\n // Check if model called tools\n if resp.tool_calls.is_empty() {\n println!(\"\\n⚠️ No tool calls in response. Some models may not support tool calling.\");\n println!(\" Response: {}\", resp.content);\n return Ok(());\n }\n\n // Process tool calls\n println!(\"\\n🔄 Processing {} tool call(s)...\", resp.tool_calls.len());\n let tool_results = process_tool_calls(&resp.tool_calls).await?;\n\n // Second request: Send tool results back to model for final response\n println!(\"\\n📤 Sending tool results back to model...\\n\");\n\n let mut follow_up_messages = vec![\n Message::system(\"You are a helpful research assistant.\"),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Add assistant's tool call as a content block\n let mut assistant_blocks = vec![ContentBlock::text(&resp.content)];\n for tool_call in &resp.tool_calls {\n assistant_blocks.push(ContentBlock::ToolUse {\n id: tool_call.id.clone(),\n name: tool_call.name.clone(),\n input: tool_call.arguments.clone(),\n });\n }\n follow_up_messages.push(Message::with_content(\n MessageRole::Assistant,\n MessageContent::blocks(assistant_blocks),\n ));\n\n // Add tool results\n for result in tool_results {\n follow_up_messages.push(Message::with_content(\n MessageRole::User,\n MessageContent::blocks(vec![ContentBlock::ToolResult {\n tool_use_id: result.tool_use_id,\n content: result.content,\n }]),\n ));\n }\n\n // Get final response\n let final_resp = client\n .chat()\n .messages(follow_up_messages)\n .temperature(0.7)\n .max_tokens(2048)\n .execute()\n .await?;\n\n println!(\"✅ Final response received\\n\");\n println!(\"📝 Assistant Response:\\n{}\", final_resp.content);\n\n if let Some(usage) = final_resp.usage {\n println!(\"\\n📊 Token Usage:\");\n if let Some(prompt_tokens) = usage.get(\"prompt_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Prompt tokens: {}\", prompt_tokens);\n }\n if let Some(completion_tokens) = usage.get(\"completion_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Completion tokens: {}\", completion_tokens);\n }\n }\n\n println!(\"\\n✨ Example completed successfully!\");\n\n Ok(())\n}\n"}, "3bc56f0edaab54ac146898dba17ef31b659f3bf8": {"t": 1786068327.883242, "n": 10600, "h": "6b539bf785958088", "c": "//! Tavily Search Tool Calling Example\n//!\n//! This example demonstrates how to use tool calling with the Tavily search API.\n//! It shows a complete workflow:\n//! 1. Define the Tavily search tool\n//! 2. Send a request with the tool to a capable model\n//! 3. Parse the tool calls from the model response\n//! 4. Execute the Tavily search\n//! 5. Send the results back to the model for final response\n//!\n//! Prerequisites:\n//! Set one of the following environment variables:\n//! - DEEPSEEK_API_KEY (recommended for tool calling)\n//! - OPENAI_API_KEY\n//! - ANTHROPIC_API_KEY\n//! - GROQ_API_KEY\n//! - TAVILY_API_KEY (for actual Tavily searches) - optional, can use mock\n//!\n//! Usage:\n//! # Use DeepSeek (recommended)\n//! $env:DEEPSEEK_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling\n//!\n//! # Or use OpenAI\n//! $env:OPENAI_API_KEY=\"your_key\"; cargo run --example tavily_tool_calling -- --provider openai\n\nuse ai_lib_rust::types::message::{ContentBlock, MessageContent, MessageRole};\nuse ai_lib_rust::types::tool::{FunctionDefinition, ToolCall, ToolDefinition, ToolResult};\nuse ai_lib_rust::{AiClient, Message};\nuse serde_json::{json, Value};\nuse std::env;\n\n/// Define the Tavily search tool\nfn tavily_search_tool() -> ToolDefinition {\n ToolDefinition {\n tool_type: \"function\".to_string(),\n function: FunctionDefinition {\n name: \"tavily_search\".to_string(),\n description: Some(\n \"Search the web using Tavily API for real-time information\".to_string(),\n ),\n parameters: Some(json!({\n \"type\": \"object\",\n \"properties\": {\n \"query\": {\n \"type\": \"string\",\n \"description\": \"The search query to find information\"\n },\n \"search_depth\": {\n \"type\": \"string\",\n \"enum\": [\"basic\", \"advanced\"],\n \"default\": \"basic\",\n \"description\": \"Search depth (basic for quick results, advanced for comprehensive)\"\n },\n \"max_results\": {\n \"type\": \"integer\",\n \"minimum\": 1,\n \"maximum\": 10,\n \"default\": 5,\n \"description\": \"Maximum number of search results\"\n }\n },\n \"required\": [\"query\"]\n })),\n },\n }\n}\n\n/// Mock Tavily search implementation\n/// In production, this would call the actual Tavily API\nasync fn mock_tavily_search(\n query: &str,\n _depth: &str,\n _max_results: i32,\n) -> Result<Value, Box<dyn std::error::Error>> {\n println!(\"\\n🔍 Executing Tavily search for: {}\", query);\n\n // Simulated search results\n let results = json!({\n \"results\": [\n {\n \"title\": \"Latest News about Rust Programming\",\n \"url\": \"https://example.com/rust-news\",\n \"content\": \"Rust 1.82 released with improved performance and new features...\",\n \"published_date\": \"2025-02-08\"\n },\n {\n \"title\": \"Rust Web Framework Benchmarks 2025\",\n \"url\": \"https://example.com/rust-benchmarks\",\n \"content\": \"Comparison of Actix, Axum, and Rocket frameworks...\",\n \"published_date\": \"2025-02-05\"\n },\n {\n \"title\": \"Rust AI Libraries Overview\",\n \"url\": \"https://example.com/rust-ai\",\n \"content\": \"A comprehensive guide to AI/ML libraries in Rust ecosystem...\",\n \"published_date\": \"2025-02-01\"\n }\n ],\n \"response_time\": \"0.423s\",\n \"query\": query\n });\n\n Ok(results)\n}\n\n/// Process tool calls from model response\nasync fn process_tool_calls(\n tool_calls: &[ToolCall],\n) -> Result<Vec<ToolResult>, Box<dyn std::error::Error>> {\n let mut results = Vec::new();\n\n for tool_call in tool_calls {\n println!(\"\\n📌 Tool Call: {}\", tool_call.name);\n println!(\" ID: {}\", tool_call.id);\n println!(\n \" Arguments: {}\",\n serde_json::to_string_pretty(&tool_call.arguments)?\n );\n\n let result = if tool_call.name == \"tavily_search\" {\n let query = tool_call\n .arguments\n .get(\"query\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown\");\n let depth = tool_call\n .arguments\n .get(\"search_depth\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"basic\");\n let max_results = tool_call\n .arguments\n .get(\"max_results\")\n .and_then(|v| v.as_i64())\n .unwrap_or(5) as i32;\n\n match mock_tavily_search(query, depth, max_results).await {\n Ok(search_results) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: search_results,\n is_error: false,\n },\n Err(e) => ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": e.to_string() }),\n is_error: true,\n },\n }\n } else {\n ToolResult {\n tool_use_id: tool_call.id.clone(),\n content: json!({ \"error\": format!(\"Unknown tool: {}\", tool_call.name) }),\n is_error: true,\n }\n };\n\n results.push(result);\n }\n\n Ok(results)\n}\n\n/// Get provider from arguments or environment\nfn get_provider() -> String {\n let args: Vec<String> = env::args().collect();\n\n // Check for --provider argument\n if let Some(idx) = args.iter().position(|arg| arg == \"--provider\") {\n if let Some(provider) = args.get(idx + 1) {\n return format!(\"{}/auto\", provider);\n }\n }\n\n // Auto-detect from available environment variables\n if env::var(\"DEEPSEEK_API_KEY\").is_ok() {\n \"deepseek/auto\".to_string()\n } else if env::var(\"OPENAI_API_KEY\").is_ok() {\n \"openai/auto\".to_string()\n } else if env::var(\"ANTHROPIC_API_KEY\").is_ok() {\n \"anthropic/auto\".to_string()\n } else if env::var(\"GROQ_API_KEY\").is_ok() {\n \"groq/auto\".to_string()\n } else {\n eprintln!(\"❌ Error: No API key found in environment variables.\");\n eprintln!(\"Please set one of:\");\n eprintln!(\" - DEEPSEEK_API_KEY\");\n eprintln!(\" - OPENAI_API_KEY\");\n eprintln!(\" - ANTHROPIC_API_KEY\");\n eprintln!(\" - GROQ_API_KEY\");\n std::process::exit(1);\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n // Initialize tracing\n tracing_subscriber::fmt()\n .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())\n .init();\n\n println!(\"🚀 Tavily Search Tool Calling Example\\n\");\n\n let provider = get_provider();\n println!(\"📦 Using provider: {}\\n\", provider);\n\n // Create client\n let client = AiClient::new(&provider).await?;\n\n // First request: Ask the model to search for information\n let messages = vec![\n Message::system(\n \"You are a helpful research assistant. When asked about topics, use the tavily_search tool to find current information.\"\n ),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Force tool usage\n let tool_choice = json!({\n \"type\": \"function\",\n \"function\": { \"name\": \"tavily_search\" }\n });\n\n println!(\"📤 Sending initial request with tool definition...\\n\");\n\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.7)\n .max_tokens(1024)\n .tools(vec![tavily_search_tool()])\n .tool_choice(tool_choice)\n .execute()\n .await?;\n\n println!(\"✅ Initial response received\");\n println!(\" Content: {}\", resp.content);\n\n // Check if model called tools\n if resp.tool_calls.is_empty() {\n println!(\"\\n⚠️ No tool calls in response. Some models may not support tool calling.\");\n println!(\" Response: {}\", resp.content);\n return Ok(());\n }\n\n // Process tool calls\n println!(\"\\n🔄 Processing {} tool call(s)...\", resp.tool_calls.len());\n let tool_results = process_tool_calls(&resp.tool_calls).await?;\n\n // Second request: Send tool results back to model for final response\n println!(\"\\n📤 Sending tool results back to model...\\n\");\n\n let mut follow_up_messages = vec![\n Message::system(\"You are a helpful research assistant.\"),\n Message::user(\n \"What are the latest developments in Rust programming? Please search for recent news and updates.\"\n ),\n ];\n\n // Add assistant's tool call as a content block\n let mut assistant_blocks = vec![ContentBlock::text(&resp.content)];\n for tool_call in &resp.tool_calls {\n assistant_blocks.push(ContentBlock::ToolUse {\n id: tool_call.id.clone(),\n name: tool_call.name.clone(),\n input: tool_call.arguments.clone(),\n });\n }\n follow_up_messages.push(Message::with_content(\n MessageRole::Assistant,\n MessageContent::blocks(assistant_blocks),\n ));\n\n // Add tool results\n for result in tool_results {\n follow_up_messages.push(Message::with_content(\n MessageRole::User,\n MessageContent::blocks(vec![ContentBlock::ToolResult {\n tool_use_id: result.tool_use_id,\n content: result.content,\n }]),\n ));\n }\n\n // Get final response\n let final_resp = client\n .chat()\n .messages(follow_up_messages)\n .temperature(0.7)\n .max_tokens(2048)\n .execute()\n .await?;\n\n println!(\"✅ Final response received\\n\");\n println!(\"📝 Assistant Response:\\n{}\", final_resp.content);\n\n if let Some(usage) = final_resp.usage {\n println!(\"\\n📊 Token Usage:\");\n if let Some(prompt_tokens) = usage.get(\"prompt_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Prompt tokens: {}\", prompt_tokens);\n }\n if let Some(completion_tokens) = usage.get(\"completion_tokens\").and_then(|v| v.as_u64()) {\n println!(\" Completion tokens: {}\", completion_tokens);\n }\n }\n\n println!(\"\\n✨ Example completed successfully!\");\n\n Ok(())\n}\n"}}, "crates/ai-lib-rust/examples/deepseek_chat_stream.rs": {"022cb30c227f111b8e044e5e37b9706339ffcf57": {"t": 1785745854.9920666, "n": 1402, "h": "7c731174e4469464", "c": "//! Real DeepSeek streaming example (no mock).\n//!\n//! Prerequisites:\n//! - Set `DEEPSEEK_API_KEY`\n//! - Ensure `ai-protocol` provider manifests are available (see README paths)\n//!\n//! Run:\n//! DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\n\nuse ai_lib_rust::{AiClient, Message};\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n tracing_subscriber::fmt::init();\n\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n eprintln!(\"Error: DEEPSEEK_API_KEY environment variable is not set.\");\n eprintln!(\"Run with: DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\");\n std::process::exit(1);\n }\n\n let messages = vec![\n Message::system(\"You are a helpful assistant.\"),\n Message::user(\"Say hello in one short sentence, then list two numbers.\"),\n ];\n\n // Create client directly using provider/model string (protocol-driven)\n let client = AiClient::new(\"deepseek/deepseek-chat\").await?;\n\n // Stream then collect into UnifiedResponse\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.2)\n .max_tokens(128)\n .stream()\n .execute()\n .await?;\n\n println!(\"\\n\\n--- Content ---\\n{}\", resp.content);\n println!(\"\\n--- Tool calls ---\\n{:#?}\", resp.tool_calls);\n println!(\"\\n--- Usage ---\\n{:#?}\", resp.usage);\n\n Ok(())\n}\n"}, "d32d4bc6a5c18cddebc9050cc5746a3496d8f58d": {"t": 1785757086.6150508, "n": 1402, "h": "7c731174e4469464", "c": "//! Real DeepSeek streaming example (no mock).\n//!\n//! Prerequisites:\n//! - Set `DEEPSEEK_API_KEY`\n//! - Ensure `ai-protocol` provider manifests are available (see README paths)\n//!\n//! Run:\n//! DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\n\nuse ai_lib_rust::{AiClient, Message};\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n tracing_subscriber::fmt::init();\n\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n eprintln!(\"Error: DEEPSEEK_API_KEY environment variable is not set.\");\n eprintln!(\"Run with: DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\");\n std::process::exit(1);\n }\n\n let messages = vec![\n Message::system(\"You are a helpful assistant.\"),\n Message::user(\"Say hello in one short sentence, then list two numbers.\"),\n ];\n\n // Create client directly using provider/model string (protocol-driven)\n let client = AiClient::new(\"deepseek/deepseek-chat\").await?;\n\n // Stream then collect into UnifiedResponse\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.2)\n .max_tokens(128)\n .stream()\n .execute()\n .await?;\n\n println!(\"\\n\\n--- Content ---\\n{}\", resp.content);\n println!(\"\\n--- Tool calls ---\\n{:#?}\", resp.tool_calls);\n println!(\"\\n--- Usage ---\\n{:#?}\", resp.usage);\n\n Ok(())\n}\n"}, "3bc56f0edaab54ac146898dba17ef31b659f3bf8": {"t": 1786068328.431178, "n": 1402, "h": "7c731174e4469464", "c": "//! Real DeepSeek streaming example (no mock).\n//!\n//! Prerequisites:\n//! - Set `DEEPSEEK_API_KEY`\n//! - Ensure `ai-protocol` provider manifests are available (see README paths)\n//!\n//! Run:\n//! DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\n\nuse ai_lib_rust::{AiClient, Message};\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n tracing_subscriber::fmt::init();\n\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n eprintln!(\"Error: DEEPSEEK_API_KEY environment variable is not set.\");\n eprintln!(\"Run with: DEEPSEEK_API_KEY=your_key cargo run --example deepseek_chat_stream\");\n std::process::exit(1);\n }\n\n let messages = vec![\n Message::system(\"You are a helpful assistant.\"),\n Message::user(\"Say hello in one short sentence, then list two numbers.\"),\n ];\n\n // Create client directly using provider/model string (protocol-driven)\n let client = AiClient::new(\"deepseek/deepseek-chat\").await?;\n\n // Stream then collect into UnifiedResponse\n let resp = client\n .chat()\n .messages(messages)\n .temperature(0.2)\n .max_tokens(128)\n .stream()\n .execute()\n .await?;\n\n println!(\"\\n\\n--- Content ---\\n{}\", resp.content);\n println!(\"\\n--- Tool calls ---\\n{:#?}\", resp.tool_calls);\n println!(\"\\n--- Usage ---\\n{:#?}\", resp.usage);\n\n Ok(())\n}\n"}}, "crates/ai-lib-rust/examples/connectivity_report.rs": {"022cb30c227f111b8e044e5e37b9706339ffcf57": {"t": 1785745871.9222388, "n": 11348, "h": "019dc9a61f1f043e", "c": "//! AI-Protocol connectivity report\n//!\n//! For each provider that has a local API key (env var `{PROVIDER_ID}_API_KEY`),\n//! runs a basic_usage-style chat request and records success or error.\n//! Outputs a report and error analysis with links to provider API references.\n//!\n//! Usage:\n//! AI_PROTOCOL_DIR=/path/to/ai-protocol cargo run --example connectivity_report\n//! Or with keys set:\n//! DEEPSEEK_API_KEY=\"...\" GROQ_API_KEY=\"...\" cargo run --example connectivity_report\n\nuse ai_lib_rust::{AiClient, Message};\nuse std::env;\nuse std::time::Instant;\n\n/// One (provider_id, model_id) to test. model_id format: \"provider/model-name\"\n/// (model-name is the key in ai-protocol v1/models/{provider}.yaml or cross-provider key).\n/// Aligned with ai-protocol v1/providers and v1/models.\nconst PROVIDER_MODELS: &[(&str, &str)] = &[\n (\"deepseek\", \"deepseek/deepseek-chat\"),\n (\"groq\", \"groq/llama-3.1-8b-instant\"),\n (\"nvidia\", \"nvidia/meta/llama3-70b\"),\n (\"minimax\", \"minimax/abab6.5s-chat\"),\n (\"zhipu\", \"zhipu/glm-4-plus\"),\n (\"openai\", \"openai/gpt-4o-mini\"),\n (\"anthropic\", \"anthropic/claude-3-5-sonnet\"),\n (\n \"together\",\n \"together/together/meta-llama/Llama-3-70b-chat-hf\",\n ),\n (\"qwen\", \"qwen/qwen-turbo\"),\n (\"moonshot\", \"moonshot/moonshot-v1-8k\"),\n (\"mistral\", \"mistral/mistral-small-latest\"),\n (\"cohere\", \"cohere/command-r-plus-08-2024\"),\n (\n \"fireworks\",\n \"fireworks/accounts/fireworks/models/llama-v3p1-70b-instruct\",\n ),\n (\n \"deepinfra\",\n \"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct\",\n ),\n (\"lepton\", \"lepton/meta-llama/Meta-Llama-3.1-70B-Instruct\"),\n (\"sensenova\", \"sensenova/sensenova-v1\"),\n (\"spark\", \"spark/general\"),\n (\"yi\", \"yi/yi-large\"),\n (\"baichuan\", \"baichuan/Baichuan2-Turbo\"),\n (\"doubao\", \"doubao/doubao-1-5-pro-32k\"),\n (\"hunyuan\", \"hunyuan/hunyuan-lite\"),\n (\"tiangong\", \"tiangong/sky-pro\"),\n (\"siliconflow\", \"siliconflow/SF-Llama3.1-8B-Instruct\"),\n (\"perplexity\", \"perplexity/llama-3.1-sonar-small-128k-online\"),\n (\"replicate\", \"replicate/meta/meta-llama-3-70b-instruct\"),\n (\"cerebras\", \"cerebras/llama3.1-8b\"),\n (\"ai21\", \"ai21/j3-mini\"),\n (\"baidu\", \"baidu/ernie-bot-4\"),\n];\n\nfn env_key_for_provider(provider_id: &str) -> String {\n format!(\"{}_API_KEY\", provider_id.to_uppercase())\n}\n\nfn has_api_key(provider_id: &str) -> bool {\n env::var(env_key_for_provider(provider_id)).is_ok()\n}\n\n#[derive(Debug)]\nstruct Row {\n provider_id: String,\n model_id: String,\n status: String,\n duration_ms: u64,\n error_message: Option<String>,\n}\n\n/// Provider API reference URLs for error analysis.\nfn api_reference_url(provider_id: &str) -> &'static str {\n match provider_id {\n \"deepseek\" => \"https://platform.deepseek.com/docs\",\n \"groq\" => \"https://console.groq.com/docs\",\n \"nvidia\" => \"https://docs.api.nvidia.com/nim/reference/llm-apis\",\n \"minimax\" => \"https://platform.minimaxi.com/document/ChatCompletion%20v2\",\n \"zhipu\" => \"https://open.bigmodel.cn/dev/api\",\n \"openai\" => \"https://platform.openai.com/docs/api-reference\",\n \"anthropic\" => \"https://docs.anthropic.com/en/api\",\n \"openrouter\" => \"https://openrouter.ai/docs\",\n \"together\" => \"https://docs.together.ai/reference/chat-completions\",\n \"qwen\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/api-details\",\n \"moonshot\" => \"https://platform.moonshot.cn/docs\",\n \"mistral\" => \"https://docs.mistral.ai/api/\",\n \"cohere\" => \"https://docs.cohere.com/reference/chat\",\n \"fireworks\" => \"https://docs.fireworks.ai/api-reference\",\n \"deepinfra\" => \"https://deepinfra.com/docs\",\n \"lepton\" => \"https://www.lepton.ai/docs\",\n \"sensenova\" => \"https://platform.sensenova.cn/doc\",\n \"spark\" => \"https://www.xfyun.cn/doc/spark/Web.html\",\n \"yi\" => \"https://platform.lingyiwanwu.com/docs\",\n \"baichuan\" => \"https://platform.baichuan-ai.com/docs\",\n \"doubao\" => \"https://www.volcengine.com/docs/82379\",\n \"hunyuan\" => \"https://cloud.tencent.com/document/product/1729\",\n \"tiangong\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/tongyi-qianwen\",\n \"siliconflow\" => \"https://docs.siliconflow.cn\",\n \"perplexity\" => \"https://docs.perplexity.ai/api-docs\",\n \"replicate\" => \"https://replicate.com/docs/reference/http\",\n \"cerebras\" => \"https://docs.cerebras.ai/inference-api\",\n \"ai21\" => \"https://docs.ai21.com/reference/complete\",\n \"baidu\" => \"https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html\",\n _ => \"https://github.com/ailib-official/ai-protocol\",\n }\n}\n\nfn classify_error(err: &str) -> (&'static str, &'static str) {\n let e = err.to_lowercase();\n // Check 429 / quota before 401 so \"429 + insufficient_quota\" is not misclassified as auth\n if e.contains(\"429\")\n || e.contains(\"rate_limited\")\n || e.contains(\"insufficient_quota\")\n || e.contains(\"quota\") && (e.contains(\"exceeded\") || e.contains(\"billing\"))\n {\n (\n \"429 / 限流或配额\",\n \"请求过于频繁或配额用尽;可重试或检查账单/升级配额。\",\n )\n } else if e.contains(\"401\")\n || e.contains(\"authentication\")\n || e.contains(\"invalid api key\")\n || e.contains(\"authorized_error\")\n {\n (\"401 / 认证失败\", \"检查 API Key 是否正确、是否过期;确认环境变量名与 manifest 中 token_env 一致(如 DEEPSEEK_API_KEY)。\")\n } else if e.contains(\"404\") || e.contains(\"not found\") || e.contains(\"not_found\") {\n (\n \"404 / 资源不存在\",\n \"确认 manifest 中 endpoint path 与厂商文档一致;模型 ID 是否在厂商当前可用列表中。\",\n )\n } else if e.contains(\"500\")\n || e.contains(\"503\")\n || e.contains(\"server_error\")\n || e.contains(\"overloaded\")\n {\n (\"5xx / 服务端错误\", \"厂商服务暂时不可用;稍后重试。\")\n } else if e.contains(\"timeout\") || e.contains(\"timed out\") {\n (\"超时\", \"增大 AI_HTTP_TIMEOUT_SECS 或检查网络。\")\n } else if e.contains(\"protocol\") || (e.contains(\"not found\") && e.contains(\"provider\")) {\n (\n \"协议/Manifest 未找到\",\n \"设置 AI_PROTOCOL_DIR 指向 ai-protocol 目录;或确认该 provider 在 ai-protocol 中存在。\",\n )\n } else {\n (\"其他\", \"对照厂商 API 文档核对请求格式与参数。\")\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let _ = tracing_subscriber::fmt()\n .with_env_filter(\n tracing_subscriber::EnvFilter::from_default_env().add_directive(\"info\".parse()?),\n )\n .with_target(false)\n .try_init();\n\n // Auto-detect ai-protocol in common relative paths (cross-platform)\n if env::var(\"AI_PROTOCOL_DIR\").is_err() && env::var(\"AI_PROTOCOL_PATH\").is_err() {\n let candidates = [\"ai-protocol\", \"../ai-protocol\", \"../../ai-protocol\"];\n for p in &candidates {\n if std::path::Path::new(p).join(\"v1/providers\").exists() {\n env::set_var(\"AI_PROTOCOL_DIR\", *p);\n println!(\"Using AI_PROTOCOL_DIR={}\\n\", p);\n break;\n }\n }\n }\n\n let to_test: Vec<(&str, &str)> = PROVIDER_MODELS\n .iter()\n .copied()\n .filter(|(pid, _)| has_api_key(pid))\n .collect();\n\n if to_test.is_empty() {\n println!(\"No API keys found. Set env vars such as:\");\n println!(\" DEEPSEEK_API_KEY, GROQ_API_KEY, NVIDIA_API_KEY, MINIMAX_API_KEY, ZHIPU_API_KEY, OPENAI_API_KEY, ...\");\n println!(\"(Convention: {{PROVIDER_ID}}_API_KEY, e.g. deepseek -> DEEPSEEK_API_KEY)\\n\");\n println!(\"Skipped providers (no key):\");\n for (pid, mid) in PROVIDER_MODELS {\n println!(\" {} ({})\", pid, mid);\n }\n return Ok(());\n }\n\n println!(\"=== AI-Protocol connectivity test ===\\n\");\n println!(\"Testing {} provider(s) with API key set.\\n\", to_test.len());\n\n let mut rows: Vec<Row> = Vec::new();\n for (provider_id, model_id) in &to_test {\n let start = Instant::now();\n let (status, error_message) = match run_one(model_id).await {\n Ok(()) => (\"OK\".to_string(), None),\n Err(e) => {\n let msg = e.to_string();\n (\"FAIL\".to_string(), Some(msg))\n }\n };\n let duration_ms = start.elapsed().as_millis() as u64;\n rows.push(Row {\n provider_id: (*provider_id).to_string(),\n model_id: (*model_id).to_string(),\n status,\n duration_ms,\n error_message,\n });\n }\n\n // Print table\n println!(\"| Provider | Model ID (abbr) | Status | Duration(ms) |\");\n println!(\"|------------|----------------------------|--------|--------------|\");\n for r in &rows {\n let mid_short = if r.model_id.len() > 26 {\n format!(\"{}...\", &r.model_id[..23])\n } else {\n r.model_id.clone()\n };\n let err_short = r\n .error_message\n .as_ref()\n .map(|s| {\n if s.len() > 60 {\n format!(\"{}...\", &s[..57])\n } else {\n s.clone()\n }\n })\n .unwrap_or_default();\n println!(\n \"| {:<10} | {:<26} | {:<6} | {:<12} |\",\n r.provider_id, mid_short, r.status, r.duration_ms\n );\n if !err_short.is_empty() {\n println!(\"| | {} |\", err_short);\n }\n }\n\n // Error analysis\n let failures: Vec<_> = rows.iter().filter(|r| r.status == \"FAIL\").collect();\n if !failures.is_empty() {\n println!(\"\\n=== Error analysis (fact-check against provider API reference) ===\\n\");\n for r in &failures {\n let err = r.error_message.as_deref().unwrap_or(\"\");\n let (kind, suggestion) = classify_error(err);\n let url = api_reference_url(&r.provider_id);\n println!(\"[{}] {} — {}\", r.provider_id, kind, suggestion);\n println!(\" API reference: {}\", url);\n println!(\" Raw error: {}\\n\", err);\n }\n }\n\n let ok_count = rows.iter().filter(|r| r.status == \"OK\").count();\n println!(\n \"=== Summary: {} OK, {} FAIL ===\\n\",\n ok_count,\n failures.len()\n );\n\n if let Ok(path) = env::var(\"CONNECTIVITY_REPORT_OUT\") {\n let mut out = String::new();\n out.push_str(\"=== AI-Protocol connectivity report ===\\n\\n\");\n for r in &rows {\n out.push_str(&format!(\n \"{} | {} | {} | {} ms\\n\",\n r.provider_id, r.model_id, r.status, r.duration_ms\n ));\n if let Some(e) = &r.error_message {\n out.push_str(&format!(\" Error: {}\\n\", e));\n }\n }\n out.push_str(&format!(\n \"\\nSummary: {} OK, {} FAIL\\n\",\n ok_count,\n failures.len()\n ));\n std::fs::write(&path, out).ok();\n println!(\"Report written to {}\", path);\n }\n Ok(())\n}\n\nasync fn run_one(model_id: &str) -> Result<(), Box<dyn std::error::Error>> {\n let client = AiClient::new(model_id).await?;\n let messages = vec![\n Message::system(\"You are a helpful assistant. Reply in one short sentence.\"),\n Message::user(\"Say hello.\"),\n ];\n let _ = client\n .chat()\n .messages(messages)\n .temperature(0.1)\n .max_tokens(100)\n .execute()\n .await?;\n Ok(())\n}\n"}, "d32d4bc6a5c18cddebc9050cc5746a3496d8f58d": {"t": 1785757086.5315628, "n": 11348, "h": "019dc9a61f1f043e", "c": "//! AI-Protocol connectivity report\n//!\n//! For each provider that has a local API key (env var `{PROVIDER_ID}_API_KEY`),\n//! runs a basic_usage-style chat request and records success or error.\n//! Outputs a report and error analysis with links to provider API references.\n//!\n//! Usage:\n//! AI_PROTOCOL_DIR=/path/to/ai-protocol cargo run --example connectivity_report\n//! Or with keys set:\n//! DEEPSEEK_API_KEY=\"...\" GROQ_API_KEY=\"...\" cargo run --example connectivity_report\n\nuse ai_lib_rust::{AiClient, Message};\nuse std::env;\nuse std::time::Instant;\n\n/// One (provider_id, model_id) to test. model_id format: \"provider/model-name\"\n/// (model-name is the key in ai-protocol v1/models/{provider}.yaml or cross-provider key).\n/// Aligned with ai-protocol v1/providers and v1/models.\nconst PROVIDER_MODELS: &[(&str, &str)] = &[\n (\"deepseek\", \"deepseek/deepseek-chat\"),\n (\"groq\", \"groq/llama-3.1-8b-instant\"),\n (\"nvidia\", \"nvidia/meta/llama3-70b\"),\n (\"minimax\", \"minimax/abab6.5s-chat\"),\n (\"zhipu\", \"zhipu/glm-4-plus\"),\n (\"openai\", \"openai/gpt-4o-mini\"),\n (\"anthropic\", \"anthropic/claude-3-5-sonnet\"),\n (\n \"together\",\n \"together/together/meta-llama/Llama-3-70b-chat-hf\",\n ),\n (\"qwen\", \"qwen/qwen-turbo\"),\n (\"moonshot\", \"moonshot/moonshot-v1-8k\"),\n (\"mistral\", \"mistral/mistral-small-latest\"),\n (\"cohere\", \"cohere/command-r-plus-08-2024\"),\n (\n \"fireworks\",\n \"fireworks/accounts/fireworks/models/llama-v3p1-70b-instruct\",\n ),\n (\n \"deepinfra\",\n \"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct\",\n ),\n (\"lepton\", \"lepton/meta-llama/Meta-Llama-3.1-70B-Instruct\"),\n (\"sensenova\", \"sensenova/sensenova-v1\"),\n (\"spark\", \"spark/general\"),\n (\"yi\", \"yi/yi-large\"),\n (\"baichuan\", \"baichuan/Baichuan2-Turbo\"),\n (\"doubao\", \"doubao/doubao-1-5-pro-32k\"),\n (\"hunyuan\", \"hunyuan/hunyuan-lite\"),\n (\"tiangong\", \"tiangong/sky-pro\"),\n (\"siliconflow\", \"siliconflow/SF-Llama3.1-8B-Instruct\"),\n (\"perplexity\", \"perplexity/llama-3.1-sonar-small-128k-online\"),\n (\"replicate\", \"replicate/meta/meta-llama-3-70b-instruct\"),\n (\"cerebras\", \"cerebras/llama3.1-8b\"),\n (\"ai21\", \"ai21/j3-mini\"),\n (\"baidu\", \"baidu/ernie-bot-4\"),\n];\n\nfn env_key_for_provider(provider_id: &str) -> String {\n format!(\"{}_API_KEY\", provider_id.to_uppercase())\n}\n\nfn has_api_key(provider_id: &str) -> bool {\n env::var(env_key_for_provider(provider_id)).is_ok()\n}\n\n#[derive(Debug)]\nstruct Row {\n provider_id: String,\n model_id: String,\n status: String,\n duration_ms: u64,\n error_message: Option<String>,\n}\n\n/// Provider API reference URLs for error analysis.\nfn api_reference_url(provider_id: &str) -> &'static str {\n match provider_id {\n \"deepseek\" => \"https://platform.deepseek.com/docs\",\n \"groq\" => \"https://console.groq.com/docs\",\n \"nvidia\" => \"https://docs.api.nvidia.com/nim/reference/llm-apis\",\n \"minimax\" => \"https://platform.minimaxi.com/document/ChatCompletion%20v2\",\n \"zhipu\" => \"https://open.bigmodel.cn/dev/api\",\n \"openai\" => \"https://platform.openai.com/docs/api-reference\",\n \"anthropic\" => \"https://docs.anthropic.com/en/api\",\n \"openrouter\" => \"https://openrouter.ai/docs\",\n \"together\" => \"https://docs.together.ai/reference/chat-completions\",\n \"qwen\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/api-details\",\n \"moonshot\" => \"https://platform.moonshot.cn/docs\",\n \"mistral\" => \"https://docs.mistral.ai/api/\",\n \"cohere\" => \"https://docs.cohere.com/reference/chat\",\n \"fireworks\" => \"https://docs.fireworks.ai/api-reference\",\n \"deepinfra\" => \"https://deepinfra.com/docs\",\n \"lepton\" => \"https://www.lepton.ai/docs\",\n \"sensenova\" => \"https://platform.sensenova.cn/doc\",\n \"spark\" => \"https://www.xfyun.cn/doc/spark/Web.html\",\n \"yi\" => \"https://platform.lingyiwanwu.com/docs\",\n \"baichuan\" => \"https://platform.baichuan-ai.com/docs\",\n \"doubao\" => \"https://www.volcengine.com/docs/82379\",\n \"hunyuan\" => \"https://cloud.tencent.com/document/product/1729\",\n \"tiangong\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/tongyi-qianwen\",\n \"siliconflow\" => \"https://docs.siliconflow.cn\",\n \"perplexity\" => \"https://docs.perplexity.ai/api-docs\",\n \"replicate\" => \"https://replicate.com/docs/reference/http\",\n \"cerebras\" => \"https://docs.cerebras.ai/inference-api\",\n \"ai21\" => \"https://docs.ai21.com/reference/complete\",\n \"baidu\" => \"https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html\",\n _ => \"https://github.com/ailib-official/ai-protocol\",\n }\n}\n\nfn classify_error(err: &str) -> (&'static str, &'static str) {\n let e = err.to_lowercase();\n // Check 429 / quota before 401 so \"429 + insufficient_quota\" is not misclassified as auth\n if e.contains(\"429\")\n || e.contains(\"rate_limited\")\n || e.contains(\"insufficient_quota\")\n || e.contains(\"quota\") && (e.contains(\"exceeded\") || e.contains(\"billing\"))\n {\n (\n \"429 / 限流或配额\",\n \"请求过于频繁或配额用尽;可重试或检查账单/升级配额。\",\n )\n } else if e.contains(\"401\")\n || e.contains(\"authentication\")\n || e.contains(\"invalid api key\")\n || e.contains(\"authorized_error\")\n {\n (\"401 / 认证失败\", \"检查 API Key 是否正确、是否过期;确认环境变量名与 manifest 中 token_env 一致(如 DEEPSEEK_API_KEY)。\")\n } else if e.contains(\"404\") || e.contains(\"not found\") || e.contains(\"not_found\") {\n (\n \"404 / 资源不存在\",\n \"确认 manifest 中 endpoint path 与厂商文档一致;模型 ID 是否在厂商当前可用列表中。\",\n )\n } else if e.contains(\"500\")\n || e.contains(\"503\")\n || e.contains(\"server_error\")\n || e.contains(\"overloaded\")\n {\n (\"5xx / 服务端错误\", \"厂商服务暂时不可用;稍后重试。\")\n } else if e.contains(\"timeout\") || e.contains(\"timed out\") {\n (\"超时\", \"增大 AI_HTTP_TIMEOUT_SECS 或检查网络。\")\n } else if e.contains(\"protocol\") || (e.contains(\"not found\") && e.contains(\"provider\")) {\n (\n \"协议/Manifest 未找到\",\n \"设置 AI_PROTOCOL_DIR 指向 ai-protocol 目录;或确认该 provider 在 ai-protocol 中存在。\",\n )\n } else {\n (\"其他\", \"对照厂商 API 文档核对请求格式与参数。\")\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let _ = tracing_subscriber::fmt()\n .with_env_filter(\n tracing_subscriber::EnvFilter::from_default_env().add_directive(\"info\".parse()?),\n )\n .with_target(false)\n .try_init();\n\n // Auto-detect ai-protocol in common relative paths (cross-platform)\n if env::var(\"AI_PROTOCOL_DIR\").is_err() && env::var(\"AI_PROTOCOL_PATH\").is_err() {\n let candidates = [\"ai-protocol\", \"../ai-protocol\", \"../../ai-protocol\"];\n for p in &candidates {\n if std::path::Path::new(p).join(\"v1/providers\").exists() {\n env::set_var(\"AI_PROTOCOL_DIR\", *p);\n println!(\"Using AI_PROTOCOL_DIR={}\\n\", p);\n break;\n }\n }\n }\n\n let to_test: Vec<(&str, &str)> = PROVIDER_MODELS\n .iter()\n .copied()\n .filter(|(pid, _)| has_api_key(pid))\n .collect();\n\n if to_test.is_empty() {\n println!(\"No API keys found. Set env vars such as:\");\n println!(\" DEEPSEEK_API_KEY, GROQ_API_KEY, NVIDIA_API_KEY, MINIMAX_API_KEY, ZHIPU_API_KEY, OPENAI_API_KEY, ...\");\n println!(\"(Convention: {{PROVIDER_ID}}_API_KEY, e.g. deepseek -> DEEPSEEK_API_KEY)\\n\");\n println!(\"Skipped providers (no key):\");\n for (pid, mid) in PROVIDER_MODELS {\n println!(\" {} ({})\", pid, mid);\n }\n return Ok(());\n }\n\n println!(\"=== AI-Protocol connectivity test ===\\n\");\n println!(\"Testing {} provider(s) with API key set.\\n\", to_test.len());\n\n let mut rows: Vec<Row> = Vec::new();\n for (provider_id, model_id) in &to_test {\n let start = Instant::now();\n let (status, error_message) = match run_one(model_id).await {\n Ok(()) => (\"OK\".to_string(), None),\n Err(e) => {\n let msg = e.to_string();\n (\"FAIL\".to_string(), Some(msg))\n }\n };\n let duration_ms = start.elapsed().as_millis() as u64;\n rows.push(Row {\n provider_id: (*provider_id).to_string(),\n model_id: (*model_id).to_string(),\n status,\n duration_ms,\n error_message,\n });\n }\n\n // Print table\n println!(\"| Provider | Model ID (abbr) | Status | Duration(ms) |\");\n println!(\"|------------|----------------------------|--------|--------------|\");\n for r in &rows {\n let mid_short = if r.model_id.len() > 26 {\n format!(\"{}...\", &r.model_id[..23])\n } else {\n r.model_id.clone()\n };\n let err_short = r\n .error_message\n .as_ref()\n .map(|s| {\n if s.len() > 60 {\n format!(\"{}...\", &s[..57])\n } else {\n s.clone()\n }\n })\n .unwrap_or_default();\n println!(\n \"| {:<10} | {:<26} | {:<6} | {:<12} |\",\n r.provider_id, mid_short, r.status, r.duration_ms\n );\n if !err_short.is_empty() {\n println!(\"| | {} |\", err_short);\n }\n }\n\n // Error analysis\n let failures: Vec<_> = rows.iter().filter(|r| r.status == \"FAIL\").collect();\n if !failures.is_empty() {\n println!(\"\\n=== Error analysis (fact-check against provider API reference) ===\\n\");\n for r in &failures {\n let err = r.error_message.as_deref().unwrap_or(\"\");\n let (kind, suggestion) = classify_error(err);\n let url = api_reference_url(&r.provider_id);\n println!(\"[{}] {} — {}\", r.provider_id, kind, suggestion);\n println!(\" API reference: {}\", url);\n println!(\" Raw error: {}\\n\", err);\n }\n }\n\n let ok_count = rows.iter().filter(|r| r.status == \"OK\").count();\n println!(\n \"=== Summary: {} OK, {} FAIL ===\\n\",\n ok_count,\n failures.len()\n );\n\n if let Ok(path) = env::var(\"CONNECTIVITY_REPORT_OUT\") {\n let mut out = String::new();\n out.push_str(\"=== AI-Protocol connectivity report ===\\n\\n\");\n for r in &rows {\n out.push_str(&format!(\n \"{} | {} | {} | {} ms\\n\",\n r.provider_id, r.model_id, r.status, r.duration_ms\n ));\n if let Some(e) = &r.error_message {\n out.push_str(&format!(\" Error: {}\\n\", e));\n }\n }\n out.push_str(&format!(\n \"\\nSummary: {} OK, {} FAIL\\n\",\n ok_count,\n failures.len()\n ));\n std::fs::write(&path, out).ok();\n println!(\"Report written to {}\", path);\n }\n Ok(())\n}\n\nasync fn run_one(model_id: &str) -> Result<(), Box<dyn std::error::Error>> {\n let client = AiClient::new(model_id).await?;\n let messages = vec![\n Message::system(\"You are a helpful assistant. Reply in one short sentence.\"),\n Message::user(\"Say hello.\"),\n ];\n let _ = client\n .chat()\n .messages(messages)\n .temperature(0.1)\n .max_tokens(100)\n .execute()\n .await?;\n Ok(())\n}\n"}, "3bc56f0edaab54ac146898dba17ef31b659f3bf8": {"t": 1786068332.5205314, "n": 11348, "h": "019dc9a61f1f043e", "c": "//! AI-Protocol connectivity report\n//!\n//! For each provider that has a local API key (env var `{PROVIDER_ID}_API_KEY`),\n//! runs a basic_usage-style chat request and records success or error.\n//! Outputs a report and error analysis with links to provider API references.\n//!\n//! Usage:\n//! AI_PROTOCOL_DIR=/path/to/ai-protocol cargo run --example connectivity_report\n//! Or with keys set:\n//! DEEPSEEK_API_KEY=\"...\" GROQ_API_KEY=\"...\" cargo run --example connectivity_report\n\nuse ai_lib_rust::{AiClient, Message};\nuse std::env;\nuse std::time::Instant;\n\n/// One (provider_id, model_id) to test. model_id format: \"provider/model-name\"\n/// (model-name is the key in ai-protocol v1/models/{provider}.yaml or cross-provider key).\n/// Aligned with ai-protocol v1/providers and v1/models.\nconst PROVIDER_MODELS: &[(&str, &str)] = &[\n (\"deepseek\", \"deepseek/deepseek-chat\"),\n (\"groq\", \"groq/llama-3.1-8b-instant\"),\n (\"nvidia\", \"nvidia/meta/llama3-70b\"),\n (\"minimax\", \"minimax/abab6.5s-chat\"),\n (\"zhipu\", \"zhipu/glm-4-plus\"),\n (\"openai\", \"openai/gpt-4o-mini\"),\n (\"anthropic\", \"anthropic/claude-3-5-sonnet\"),\n (\n \"together\",\n \"together/together/meta-llama/Llama-3-70b-chat-hf\",\n ),\n (\"qwen\", \"qwen/qwen-turbo\"),\n (\"moonshot\", \"moonshot/moonshot-v1-8k\"),\n (\"mistral\", \"mistral/mistral-small-latest\"),\n (\"cohere\", \"cohere/command-r-plus-08-2024\"),\n (\n \"fireworks\",\n \"fireworks/accounts/fireworks/models/llama-v3p1-70b-instruct\",\n ),\n (\n \"deepinfra\",\n \"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct\",\n ),\n (\"lepton\", \"lepton/meta-llama/Meta-Llama-3.1-70B-Instruct\"),\n (\"sensenova\", \"sensenova/sensenova-v1\"),\n (\"spark\", \"spark/general\"),\n (\"yi\", \"yi/yi-large\"),\n (\"baichuan\", \"baichuan/Baichuan2-Turbo\"),\n (\"doubao\", \"doubao/doubao-1-5-pro-32k\"),\n (\"hunyuan\", \"hunyuan/hunyuan-lite\"),\n (\"tiangong\", \"tiangong/sky-pro\"),\n (\"siliconflow\", \"siliconflow/SF-Llama3.1-8B-Instruct\"),\n (\"perplexity\", \"perplexity/llama-3.1-sonar-small-128k-online\"),\n (\"replicate\", \"replicate/meta/meta-llama-3-70b-instruct\"),\n (\"cerebras\", \"cerebras/llama3.1-8b\"),\n (\"ai21\", \"ai21/j3-mini\"),\n (\"baidu\", \"baidu/ernie-bot-4\"),\n];\n\nfn env_key_for_provider(provider_id: &str) -> String {\n format!(\"{}_API_KEY\", provider_id.to_uppercase())\n}\n\nfn has_api_key(provider_id: &str) -> bool {\n env::var(env_key_for_provider(provider_id)).is_ok()\n}\n\n#[derive(Debug)]\nstruct Row {\n provider_id: String,\n model_id: String,\n status: String,\n duration_ms: u64,\n error_message: Option<String>,\n}\n\n/// Provider API reference URLs for error analysis.\nfn api_reference_url(provider_id: &str) -> &'static str {\n match provider_id {\n \"deepseek\" => \"https://platform.deepseek.com/docs\",\n \"groq\" => \"https://console.groq.com/docs\",\n \"nvidia\" => \"https://docs.api.nvidia.com/nim/reference/llm-apis\",\n \"minimax\" => \"https://platform.minimaxi.com/document/ChatCompletion%20v2\",\n \"zhipu\" => \"https://open.bigmodel.cn/dev/api\",\n \"openai\" => \"https://platform.openai.com/docs/api-reference\",\n \"anthropic\" => \"https://docs.anthropic.com/en/api\",\n \"openrouter\" => \"https://openrouter.ai/docs\",\n \"together\" => \"https://docs.together.ai/reference/chat-completions\",\n \"qwen\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/api-details\",\n \"moonshot\" => \"https://platform.moonshot.cn/docs\",\n \"mistral\" => \"https://docs.mistral.ai/api/\",\n \"cohere\" => \"https://docs.cohere.com/reference/chat\",\n \"fireworks\" => \"https://docs.fireworks.ai/api-reference\",\n \"deepinfra\" => \"https://deepinfra.com/docs\",\n \"lepton\" => \"https://www.lepton.ai/docs\",\n \"sensenova\" => \"https://platform.sensenova.cn/doc\",\n \"spark\" => \"https://www.xfyun.cn/doc/spark/Web.html\",\n \"yi\" => \"https://platform.lingyiwanwu.com/docs\",\n \"baichuan\" => \"https://platform.baichuan-ai.com/docs\",\n \"doubao\" => \"https://www.volcengine.com/docs/82379\",\n \"hunyuan\" => \"https://cloud.tencent.com/document/product/1729\",\n \"tiangong\" => \"https://help.aliyun.com/zh/model-studio/developer-reference/tongyi-qianwen\",\n \"siliconflow\" => \"https://docs.siliconflow.cn\",\n \"perplexity\" => \"https://docs.perplexity.ai/api-docs\",\n \"replicate\" => \"https://replicate.com/docs/reference/http\",\n \"cerebras\" => \"https://docs.cerebras.ai/inference-api\",\n \"ai21\" => \"https://docs.ai21.com/reference/complete\",\n \"baidu\" => \"https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html\",\n _ => \"https://github.com/ailib-official/ai-protocol\",\n }\n}\n\nfn classify_error(err: &str) -> (&'static str, &'static str) {\n let e = err.to_lowercase();\n // Check 429 / quota before 401 so \"429 + insufficient_quota\" is not misclassified as auth\n if e.contains(\"429\")\n || e.contains(\"rate_limited\")\n || e.contains(\"insufficient_quota\")\n || e.contains(\"quota\") && (e.contains(\"exceeded\") || e.contains(\"billing\"))\n {\n (\n \"429 / 限流或配额\",\n \"请求过于频繁或配额用尽;可重试或检查账单/升级配额。\",\n )\n } else if e.contains(\"401\")\n || e.contains(\"authentication\")\n || e.contains(\"invalid api key\")\n || e.contains(\"authorized_error\")\n {\n (\"401 / 认证失败\", \"检查 API Key 是否正确、是否过期;确认环境变量名与 manifest 中 token_env 一致(如 DEEPSEEK_API_KEY)。\")\n } else if e.contains(\"404\") || e.contains(\"not found\") || e.contains(\"not_found\") {\n (\n \"404 / 资源不存在\",\n \"确认 manifest 中 endpoint path 与厂商文档一致;模型 ID 是否在厂商当前可用列表中。\",\n )\n } else if e.contains(\"500\")\n || e.contains(\"503\")\n || e.contains(\"server_error\")\n || e.contains(\"overloaded\")\n {\n (\"5xx / 服务端错误\", \"厂商服务暂时不可用;稍后重试。\")\n } else if e.contains(\"timeout\") || e.contains(\"timed out\") {\n (\"超时\", \"增大 AI_HTTP_TIMEOUT_SECS 或检查网络。\")\n } else if e.contains(\"protocol\") || (e.contains(\"not found\") && e.contains(\"provider\")) {\n (\n \"协议/Manifest 未找到\",\n \"设置 AI_PROTOCOL_DIR 指向 ai-protocol 目录;或确认该 provider 在 ai-protocol 中存在。\",\n )\n } else {\n (\"其他\", \"对照厂商 API 文档核对请求格式与参数。\")\n }\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n let _ = tracing_subscriber::fmt()\n .with_env_filter(\n tracing_subscriber::EnvFilter::from_default_env().add_directive(\"info\".parse()?),\n )\n .with_target(false)\n .try_init();\n\n // Auto-detect ai-protocol in common relative paths (cross-platform)\n if env::var(\"AI_PROTOCOL_DIR\").is_err() && env::var(\"AI_PROTOCOL_PATH\").is_err() {\n let candidates = [\"ai-protocol\", \"../ai-protocol\", \"../../ai-protocol\"];\n for p in &candidates {\n if std::path::Path::new(p).join(\"v1/providers\").exists() {\n env::set_var(\"AI_PROTOCOL_DIR\", *p);\n println!(\"Using AI_PROTOCOL_DIR={}\\n\", p);\n break;\n }\n }\n }\n\n let to_test: Vec<(&str, &str)> = PROVIDER_MODELS\n .iter()\n .copied()\n .filter(|(pid, _)| has_api_key(pid))\n .collect();\n\n if to_test.is_empty() {\n println!(\"No API keys found. Set env vars such as:\");\n println!(\" DEEPSEEK_API_KEY, GROQ_API_KEY, NVIDIA_API_KEY, MINIMAX_API_KEY, ZHIPU_API_KEY, OPENAI_API_KEY, ...\");\n println!(\"(Convention: {{PROVIDER_ID}}_API_KEY, e.g. deepseek -> DEEPSEEK_API_KEY)\\n\");\n println!(\"Skipped providers (no key):\");\n for (pid, mid) in PROVIDER_MODELS {\n println!(\" {} ({})\", pid, mid);\n }\n return Ok(());\n }\n\n println!(\"=== AI-Protocol connectivity test ===\\n\");\n println!(\"Testing {} provider(s) with API key set.\\n\", to_test.len());\n\n let mut rows: Vec<Row> = Vec::new();\n for (provider_id, model_id) in &to_test {\n let start = Instant::now();\n let (status, error_message) = match run_one(model_id).await {\n Ok(()) => (\"OK\".to_string(), None),\n Err(e) => {\n let msg = e.to_string();\n (\"FAIL\".to_string(), Some(msg))\n }\n };\n let duration_ms = start.elapsed().as_millis() as u64;\n rows.push(Row {\n provider_id: (*provider_id).to_string(),\n model_id: (*model_id).to_string(),\n status,\n duration_ms,\n error_message,\n });\n }\n\n // Print table\n println!(\"| Provider | Model ID (abbr) | Status | Duration(ms) |\");\n println!(\"|------------|----------------------------|--------|--------------|\");\n for r in &rows {\n let mid_short = if r.model_id.len() > 26 {\n format!(\"{}...\", &r.model_id[..23])\n } else {\n r.model_id.clone()\n };\n let err_short = r\n .error_message\n .as_ref()\n .map(|s| {\n if s.len() > 60 {\n format!(\"{}...\", &s[..57])\n } else {\n s.clone()\n }\n })\n .unwrap_or_default();\n println!(\n \"| {:<10} | {:<26} | {:<6} | {:<12} |\",\n r.provider_id, mid_short, r.status, r.duration_ms\n );\n if !err_short.is_empty() {\n println!(\"| | {} |\", err_short);\n }\n }\n\n // Error analysis\n let failures: Vec<_> = rows.iter().filter(|r| r.status == \"FAIL\").collect();\n if !failures.is_empty() {\n println!(\"\\n=== Error analysis (fact-check against provider API reference) ===\\n\");\n for r in &failures {\n let err = r.error_message.as_deref().unwrap_or(\"\");\n let (kind, suggestion) = classify_error(err);\n let url = api_reference_url(&r.provider_id);\n println!(\"[{}] {} — {}\", r.provider_id, kind, suggestion);\n println!(\" API reference: {}\", url);\n println!(\" Raw error: {}\\n\", err);\n }\n }\n\n let ok_count = rows.iter().filter(|r| r.status == \"OK\").count();\n println!(\n \"=== Summary: {} OK, {} FAIL ===\\n\",\n ok_count,\n failures.len()\n );\n\n if let Ok(path) = env::var(\"CONNECTIVITY_REPORT_OUT\") {\n let mut out = String::new();\n out.push_str(\"=== AI-Protocol connectivity report ===\\n\\n\");\n for r in &rows {\n out.push_str(&format!(\n \"{} | {} | {} | {} ms\\n\",\n r.provider_id, r.model_id, r.status, r.duration_ms\n ));\n if let Some(e) = &r.error_message {\n out.push_str(&format!(\" Error: {}\\n\", e));\n }\n }\n out.push_str(&format!(\n \"\\nSummary: {} OK, {} FAIL\\n\",\n ok_count,\n failures.len()\n ));\n std::fs::write(&path, out).ok();\n println!(\"Report written to {}\", path);\n }\n Ok(())\n}\n\nasync fn run_one(model_id: &str) -> Result<(), Box<dyn std::error::Error>> {\n let client = AiClient::new(model_id).await?;\n let messages = vec![\n Message::system(\"You are a helpful assistant. Reply in one short sentence.\"),\n Message::user(\"Say hello.\"),\n ];\n let _ = client\n .chat()\n .messages(messages)\n .temperature(0.1)\n .max_tokens(100)\n .execute()\n .await?;\n Ok(())\n}\n"}}}} |