Files

1 line
53 KiB
JSON
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "apts-1547/gcop-rs", "files": {"src/llm/provider/utils.rs": {"1b7d1085f02154bf170c4e8d36d713df9e92d606": {"t": 1785746222.602999, "n": 7969, "h": "d47eeca572f6248c", "c": "//! Provider utility functions\n//!\n//! Contains common functions such as URL processing and endpoint completion\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// OpenAI Responses API endpoint suffix\npub const OPENAI_RESPONSES_API_SUFFIX: &str = \"/v1/responses\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude default base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI default base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama default base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// Gemini default base URL\npub const DEFAULT_GEMINI_BASE: &str = \"https://generativelanguage.googleapis.com\";\n\n/// Smart completion API endpoint\n///\n/// # Behavior\n/// 1. Remove trailing slashes\n/// 2. Check whether the URL contains the full path\n/// 3. If incomplete, automatically complete suffix\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. Clean URLs: Remove trailing slashes\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. If the expected suffix is ​​already included, return directly\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. Check whether the URL contains the partial prefix of suffix\n // For example: url is \"https://api.com/v1\", suffix is ​​\"v1/chat/completions\"\n // Then we should only complete \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // Check from back to front to see if the URL already contains the suffix prefix\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // The URL already contains part of the suffix, only the remaining part is completed.\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. Check whether it is a customized complete API path\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. Complete the complete suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// Check if the URL is already a full API path\n///\n/// Heuristic rules:\n/// - Path depth >= 2 is considered a complete path (such as /v1/chat, /api/generate)\n/// - This allows users to use fully customized endpoints\nfn is_complete_api_path(url: &str) -> bool {\n // Extract the path part (remove the protocol and domain name)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // Count non-empty path segments\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // Path depth >= 2 is considered a user-defined complete path\n segment_count >= 2\n}\n\n/// Mask API key to prevent log leaks\n///\n/// # rule\n/// - length > 8: display first 4 characters + `...` + last 4 characters\n/// - length <= 8: display `****`\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::mask_api_key;\n///\n/// assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n/// assert_eq!(mask_api_key(\"short\"), \"****\");\n/// assert_eq!(mask_api_key(\"\"), \"****\");\n/// ```\npub fn mask_api_key(key: &str) -> String {\n if key.len() > 8 {\n format!(\"{}...{}\", &key[..4], &key[key.len() - 4..])\n } else {\n \"****\".to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_mask_api_key() {\n // Long key: first 4 + ... + last 4\n assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n assert_eq!(mask_api_key(\"AIzaSyD-1234567890abcdef\"), \"AIza...cdef\");\n\n // short key\n assert_eq!(mask_api_key(\"12345678\"), \"****\");\n assert_eq!(mask_api_key(\"short\"), \"****\");\n\n // null\n assert_eq!(mask_api_key(\"\"), \"****\");\n\n // Exactly 9 characters\n assert_eq!(mask_api_key(\"123456789\"), \"1234...6789\");\n }\n\n #[test]\n fn test_complete_endpoint_basic() {\n // Basic completion\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // with trailing slash\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // Already complete\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // Only the version number needs to be completed\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // Customize the full path and keep it as is\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // full path\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // Incomplete path\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama local address\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude Agent\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix with leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix without leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "a053707a1a5dfd9231583f8f6b2f8f5dcff7c4f3": {"t": 1785760744.7968214, "n": 7969, "h": "d47eeca572f6248c", "c": "//! Provider utility functions\n//!\n//! Contains common functions such as URL processing and endpoint completion\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// OpenAI Responses API endpoint suffix\npub const OPENAI_RESPONSES_API_SUFFIX: &str = \"/v1/responses\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude default base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI default base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama default base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// Gemini default base URL\npub const DEFAULT_GEMINI_BASE: &str = \"https://generativelanguage.googleapis.com\";\n\n/// Smart completion API endpoint\n///\n/// # Behavior\n/// 1. Remove trailing slashes\n/// 2. Check whether the URL contains the full path\n/// 3. If incomplete, automatically complete suffix\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. Clean URLs: Remove trailing slashes\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. If the expected suffix is ​​already included, return directly\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. Check whether the URL contains the partial prefix of suffix\n // For example: url is \"https://api.com/v1\", suffix is ​​\"v1/chat/completions\"\n // Then we should only complete \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // Check from back to front to see if the URL already contains the suffix prefix\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // The URL already contains part of the suffix, only the remaining part is completed.\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. Check whether it is a customized complete API path\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. Complete the complete suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// Check if the URL is already a full API path\n///\n/// Heuristic rules:\n/// - Path depth >= 2 is considered a complete path (such as /v1/chat, /api/generate)\n/// - This allows users to use fully customized endpoints\nfn is_complete_api_path(url: &str) -> bool {\n // Extract the path part (remove the protocol and domain name)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // Count non-empty path segments\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // Path depth >= 2 is considered a user-defined complete path\n segment_count >= 2\n}\n\n/// Mask API key to prevent log leaks\n///\n/// # rule\n/// - length > 8: display first 4 characters + `...` + last 4 characters\n/// - length <= 8: display `****`\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::mask_api_key;\n///\n/// assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n/// assert_eq!(mask_api_key(\"short\"), \"****\");\n/// assert_eq!(mask_api_key(\"\"), \"****\");\n/// ```\npub fn mask_api_key(key: &str) -> String {\n if key.len() > 8 {\n format!(\"{}...{}\", &key[..4], &key[key.len() - 4..])\n } else {\n \"****\".to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_mask_api_key() {\n // Long key: first 4 + ... + last 4\n assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n assert_eq!(mask_api_key(\"AIzaSyD-1234567890abcdef\"), \"AIza...cdef\");\n\n // short key\n assert_eq!(mask_api_key(\"12345678\"), \"****\");\n assert_eq!(mask_api_key(\"short\"), \"****\");\n\n // null\n assert_eq!(mask_api_key(\"\"), \"****\");\n\n // Exactly 9 characters\n assert_eq!(mask_api_key(\"123456789\"), \"1234...6789\");\n }\n\n #[test]\n fn test_complete_endpoint_basic() {\n // Basic completion\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // with trailing slash\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // Already complete\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // Only the version number needs to be completed\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // Customize the full path and keep it as is\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // full path\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // Incomplete path\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama local address\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude Agent\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix with leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix without leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "0014608ec4caad037bd987487e55929b1217a715": {"t": 1785760746.0359905, "n": 7864, "h": "a6ec09106b0e0a9a", "c": "//! Provider utility functions\n//!\n//! Contains common functions such as URL processing and endpoint completion\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude default base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI default base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama default base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// Gemini default base URL\npub const DEFAULT_GEMINI_BASE: &str = \"https://generativelanguage.googleapis.com\";\n\n/// Smart completion API endpoint\n///\n/// # Behavior\n/// 1. Remove trailing slashes\n/// 2. Check whether the URL contains the full path\n/// 3. If incomplete, automatically complete suffix\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. Clean URLs: Remove trailing slashes\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. If the expected suffix is ​​already included, return directly\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. Check whether the URL contains the partial prefix of suffix\n // For example: url is \"https://api.com/v1\", suffix is ​​\"v1/chat/completions\"\n // Then we should only complete \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // Check from back to front to see if the URL already contains the suffix prefix\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // The URL already contains part of the suffix, only the remaining part is completed.\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. Check whether it is a customized complete API path\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. Complete the complete suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// Check if the URL is already a full API path\n///\n/// Heuristic rules:\n/// - Path depth >= 2 is considered a complete path (such as /v1/chat, /api/generate)\n/// - This allows users to use fully customized endpoints\nfn is_complete_api_path(url: &str) -> bool {\n // Extract the path part (remove the protocol and domain name)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // Count non-empty path segments\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // Path depth >= 2 is considered a user-defined complete path\n segment_count >= 2\n}\n\n/// Mask API key to prevent log leaks\n///\n/// # rule\n/// - length > 8: display first 4 characters + `...` + last 4 characters\n/// - length <= 8: display `****`\n///\n/// # Example\n/// ```\n/// use gcop_rs::llm::provider::utils::mask_api_key;\n///\n/// assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n/// assert_eq!(mask_api_key(\"short\"), \"****\");\n/// assert_eq!(mask_api_key(\"\"), \"****\");\n/// ```\npub fn mask_api_key(key: &str) -> String {\n if key.len() > 8 {\n format!(\"{}...{}\", &key[..4], &key[key.len() - 4..])\n } else {\n \"****\".to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_mask_api_key() {\n // Long key: first 4 + ... + last 4\n assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n assert_eq!(mask_api_key(\"AIzaSyD-1234567890abcdef\"), \"AIza...cdef\");\n\n // short key\n assert_eq!(mask_api_key(\"12345678\"), \"****\");\n assert_eq!(mask_api_key(\"short\"), \"****\");\n\n // null\n assert_eq!(mask_api_key(\"\"), \"****\");\n\n // Exactly 9 characters\n assert_eq!(mask_api_key(\"123456789\"), \"1234...6789\");\n }\n\n #[test]\n fn test_complete_endpoint_basic() {\n // Basic completion\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // with trailing slash\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // Already complete\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // Only the version number needs to be completed\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // Customize the full path and keep it as is\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // full path\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // Incomplete path\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama local address\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude Agent\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix with leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix without leading slash\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "53a08c4cf7c2cf60245183ed43664da44a261245": {"t": 1785760748.4164808, "n": 6972, "h": "7420017de3f554cb", "c": "//! Provider 工具函数\n//!\n//! 包含 URL 处理、endpoint 补全等通用功能\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude 默认 base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI 默认 base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama 默认 base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// Gemini 默认 base URL\npub const DEFAULT_GEMINI_BASE: &str = \"https://generativelanguage.googleapis.com\";\n\n/// 智能补全 API endpoint\n///\n/// # 行为\n/// 1. 移除 trailing slashes\n/// 2. 检测 URL 是否已包含完整路径\n/// 3. 如果不完整,自动补全 suffix\n///\n/// # 示例\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. 清理 URL: 移除尾部斜杠\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. 如果已经包含期望的 suffix,直接返回\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. 检测 URL 是否包含 suffix 的部分前缀\n // 例如: url 是 \"https://api.com/v1\", suffix 是 \"v1/chat/completions\"\n // 那么我们应该只补全 \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // 从后往前检查,看 URL 是否已经包含 suffix 的前缀\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // URL 已经包含了部分 suffix,只补全剩余部分\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. 检测是否是自定义的完整 API 路径\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. 补全完整 suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// 检测 URL 是否已经是完整的 API 路径\n///\n/// 启发式规则:\n/// - 路径深度 >= 2 认为是完整路径 (如 /v1/chat, /api/generate)\n/// - 这允许用户使用完全自定义的 endpoint\nfn is_complete_api_path(url: &str) -> bool {\n // 提取路径部分 (去掉协议和域名)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // 统计非空路径段\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // 路径深度 >= 2 认为是用户自定义的完整路径\n segment_count >= 2\n}\n\n/// 掩码 API key,防止日志泄露\n///\n/// # 规则\n/// - 长度 > 8:显示前 4 字符 + `...` + 后 4 字符\n/// - 长度 <= 8:显示 `****`\n///\n/// # 示例\n/// ```\n/// use gcop_rs::llm::provider::utils::mask_api_key;\n///\n/// assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n/// assert_eq!(mask_api_key(\"short\"), \"****\");\n/// assert_eq!(mask_api_key(\"\"), \"****\");\n/// ```\npub fn mask_api_key(key: &str) -> String {\n if key.len() > 8 {\n format!(\"{}...{}\", &key[..4], &key[key.len() - 4..])\n } else {\n \"****\".to_string()\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_mask_api_key() {\n // 长 key:前4 + ... + 后4\n assert_eq!(mask_api_key(\"sk-ant-api03-abcdefgh\"), \"sk-a...efgh\");\n assert_eq!(mask_api_key(\"AIzaSyD-1234567890abcdef\"), \"AIza...cdef\");\n\n // 短 key\n assert_eq!(mask_api_key(\"12345678\"), \"****\");\n assert_eq!(mask_api_key(\"short\"), \"****\");\n\n // 空\n assert_eq!(mask_api_key(\"\"), \"****\");\n\n // 刚好 9 字符\n assert_eq!(mask_api_key(\"123456789\"), \"1234...6789\");\n }\n\n #[test]\n fn test_complete_endpoint_basic() {\n // 基本补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // 带尾部斜杠\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // 已经完整\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // 只有版本号,需要补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // 自定义完整路径,保持原样\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // 完整路径\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // 不完整路径\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama 本地地址\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude 代理\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix 带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix 不带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "dd5254b686e455f068d817f11e208a57e94704a8": {"t": 1785760749.8167741, "n": 5958, "h": "888065d1398b4afe", "c": "//! Provider 工具函数\n//!\n//! 包含 URL 处理、endpoint 补全等通用功能\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude 默认 base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI 默认 base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama 默认 base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// Gemini 默认 base URL\npub const DEFAULT_GEMINI_BASE: &str = \"https://generativelanguage.googleapis.com\";\n\n/// 智能补全 API endpoint\n///\n/// # 行为\n/// 1. 移除 trailing slashes\n/// 2. 检测 URL 是否已包含完整路径\n/// 3. 如果不完整,自动补全 suffix\n///\n/// # 示例\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. 清理 URL: 移除尾部斜杠\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. 如果已经包含期望的 suffix,直接返回\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. 检测 URL 是否包含 suffix 的部分前缀\n // 例如: url 是 \"https://api.com/v1\", suffix 是 \"v1/chat/completions\"\n // 那么我们应该只补全 \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // 从后往前检查,看 URL 是否已经包含 suffix 的前缀\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // URL 已经包含了部分 suffix,只补全剩余部分\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. 检测是否是自定义的完整 API 路径\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. 补全完整 suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// 检测 URL 是否已经是完整的 API 路径\n///\n/// 启发式规则:\n/// - 路径深度 >= 2 认为是完整路径 (如 /v1/chat, /api/generate)\n/// - 这允许用户使用完全自定义的 endpoint\nfn is_complete_api_path(url: &str) -> bool {\n // 提取路径部分 (去掉协议和域名)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // 统计非空路径段\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // 路径深度 >= 2 认为是用户自定义的完整路径\n segment_count >= 2\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_complete_endpoint_basic() {\n // 基本补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // 带尾部斜杠\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // 已经完整\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // 只有版本号,需要补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // 自定义完整路径,保持原样\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // 完整路径\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // 不完整路径\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama 本地地址\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude 代理\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix 带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix 不带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "2838a6a189d3be21e58c33bb1adfa82e49b09117": {"t": 1785760751.0537198, "n": 5851, "h": "ed06a3a40a68495b", "c": "//! Provider 工具函数\n//!\n//! 包含 URL 处理、endpoint 补全等通用功能\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude 默认 base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI 默认 base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama 默认 base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// 智能补全 API endpoint\n///\n/// # 行为\n/// 1. 移除 trailing slashes\n/// 2. 检测 URL 是否已包含完整路径\n/// 3. 如果不完整,自动补全 suffix\n///\n/// # 示例\n/// ```\n/// use gcop_rs::llm::provider::utils::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. 清理 URL: 移除尾部斜杠\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. 如果已经包含期望的 suffix,直接返回\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. 检测 URL 是否包含 suffix 的部分前缀\n // 例如: url 是 \"https://api.com/v1\", suffix 是 \"v1/chat/completions\"\n // 那么我们应该只补全 \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // 从后往前检查,看 URL 是否已经包含 suffix 的前缀\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // URL 已经包含了部分 suffix,只补全剩余部分\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. 检测是否是自定义的完整 API 路径\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. 补全完整 suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// 检测 URL 是否已经是完整的 API 路径\n///\n/// 启发式规则:\n/// - 路径深度 >= 2 认为是完整路径 (如 /v1/chat, /api/generate)\n/// - 这允许用户使用完全自定义的 endpoint\nfn is_complete_api_path(url: &str) -> bool {\n // 提取路径部分 (去掉协议和域名)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // 统计非空路径段\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // 路径深度 >= 2 认为是用户自定义的完整路径\n segment_count >= 2\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_complete_endpoint_basic() {\n // 基本补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // 带尾部斜杠\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // 已经完整\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // 只有版本号,需要补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // 自定义完整路径,保持原样\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // 完整路径\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // 不完整路径\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama 本地地址\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude 代理\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix 带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix 不带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}, "22e958e272a9eeedaad96bbb6fe75d40990ade24": {"t": 1785760752.4464233, "n": 5844, "h": "4efbf2854b3e6789", "c": "//! Provider 工具函数\n//!\n//! 包含 URL 处理、endpoint 补全等通用功能\n\n/// Claude API endpoint suffix\npub const CLAUDE_API_SUFFIX: &str = \"/v1/messages\";\n\n/// OpenAI API endpoint suffix\npub const OPENAI_API_SUFFIX: &str = \"/v1/chat/completions\";\n\n/// Ollama API endpoint suffix\npub const OLLAMA_API_SUFFIX: &str = \"/api/generate\";\n\n/// Claude 默认 base URL\npub const DEFAULT_CLAUDE_BASE: &str = \"https://api.anthropic.com\";\n\n/// OpenAI 默认 base URL\npub const DEFAULT_OPENAI_BASE: &str = \"https://api.openai.com\";\n\n/// Ollama 默认 base URL\npub const DEFAULT_OLLAMA_BASE: &str = \"http://localhost:11434\";\n\n/// 智能补全 API endpoint\n///\n/// # 行为\n/// 1. 移除 trailing slashes\n/// 2. 检测 URL 是否已包含完整路径\n/// 3. 如果不完整,自动补全 suffix\n///\n/// # 示例\n/// ```\n/// use gcop_rs::llm::provider::complete_endpoint;\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/v1/chat/completions\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n///\n/// assert_eq!(\n/// complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n/// \"https://api.deepseek.com/v1/chat/completions\"\n/// );\n/// ```\npub fn complete_endpoint(base_url: &str, expected_suffix: &str) -> String {\n // 1. 清理 URL: 移除尾部斜杠\n let url = base_url.trim_end_matches('/');\n let suffix = expected_suffix.trim_start_matches('/');\n\n // 2. 如果已经包含期望的 suffix,直接返回\n if url.ends_with(suffix) {\n return url.to_string();\n }\n\n // 3. 检测 URL 是否包含 suffix 的部分前缀\n // 例如: url 是 \"https://api.com/v1\", suffix 是 \"v1/chat/completions\"\n // 那么我们应该只补全 \"/chat/completions\"\n let suffix_parts: Vec<&str> = suffix.split('/').collect();\n\n // 从后往前检查,看 URL 是否已经包含 suffix 的前缀\n for i in 0..suffix_parts.len() {\n let partial_suffix = suffix_parts[..=i].join(\"/\");\n if url.ends_with(&partial_suffix) {\n // URL 已经包含了部分 suffix,只补全剩余部分\n let remaining_suffix = &suffix_parts[i + 1..].join(\"/\");\n if remaining_suffix.is_empty() {\n return url.to_string();\n }\n return format!(\"{}/{}\", url, remaining_suffix);\n }\n }\n\n // 4. 检测是否是自定义的完整 API 路径\n if is_complete_api_path(url) {\n return url.to_string();\n }\n\n // 5. 补全完整 suffix\n format!(\"{}/{}\", url, suffix)\n}\n\n/// 检测 URL 是否已经是完整的 API 路径\n///\n/// 启发式规则:\n/// - 路径深度 >= 2 认为是完整路径 (如 /v1/chat, /api/generate)\n/// - 这允许用户使用完全自定义的 endpoint\nfn is_complete_api_path(url: &str) -> bool {\n // 提取路径部分 (去掉协议和域名)\n let path = url\n .strip_prefix(\"http://\")\n .or_else(|| url.strip_prefix(\"https://\"))\n .and_then(|rest| rest.split_once('/'))\n .map(|(_, path)| path)\n .unwrap_or(\"\");\n\n if path.is_empty() {\n return false;\n }\n\n // 统计非空路径段\n let segment_count = path.split('/').filter(|s| !s.is_empty()).count();\n\n // 路径深度 >= 2 认为是用户自定义的完整路径\n segment_count >= 2\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_complete_endpoint_basic() {\n // 基本补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_trailing_slash() {\n // 带尾部斜杠\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_already_complete() {\n // 已经完整\n assert_eq!(\n complete_endpoint(\n \"https://api.deepseek.com/v1/chat/completions\",\n \"/v1/chat/completions\"\n ),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_with_version_only() {\n // 只有版本号,需要补全\n assert_eq!(\n complete_endpoint(\"https://api.deepseek.com/v1\", \"/v1/chat/completions\"),\n \"https://api.deepseek.com/v1/chat/completions\"\n );\n }\n\n #[test]\n fn test_complete_endpoint_custom_path() {\n // 自定义完整路径,保持原样\n assert_eq!(\n complete_endpoint(\"https://custom.com/my/custom/path\", \"/v1/chat/completions\"),\n \"https://custom.com/my/custom/path\"\n );\n }\n\n #[test]\n fn test_is_complete_api_path() {\n // 完整路径\n assert!(is_complete_api_path(\"https://api.com/v1/chat\"));\n assert!(is_complete_api_path(\"http://localhost:11434/api/generate\"));\n\n // 不完整路径\n assert!(!is_complete_api_path(\"https://api.com\"));\n assert!(!is_complete_api_path(\"https://api.com/\"));\n assert!(!is_complete_api_path(\"https://api.com/v1\"));\n }\n\n #[test]\n fn test_ollama_localhost() {\n // Ollama 本地地址\n assert_eq!(\n complete_endpoint(\"http://localhost:11434\", \"/api/generate\"),\n \"http://localhost:11434/api/generate\"\n );\n }\n\n #[test]\n fn test_claude_endpoint() {\n // Claude API\n assert_eq!(\n complete_endpoint(\"https://api.anthropic.com\", \"/v1/messages\"),\n \"https://api.anthropic.com/v1/messages\"\n );\n\n // Claude 代理\n assert_eq!(\n complete_endpoint(\"https://cc.autobits.cc\", \"/v1/messages\"),\n \"https://cc.autobits.cc/v1/messages\"\n );\n }\n\n #[test]\n fn test_suffix_variations() {\n // suffix 带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"/v1/test\"),\n \"https://api.com/v1/test\"\n );\n\n // suffix 不带前导斜杠\n assert_eq!(\n complete_endpoint(\"https://api.com\", \"v1/test\"),\n \"https://api.com/v1/test\"\n );\n }\n}\n"}}}}