api模型检测
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
3.1 KiB

  1. pub mod anthropic;
  2. pub mod openai;
  3. use anyhow::{Context, Result};
  4. use reqwest::Url;
  5. use serde::Deserialize;
  6. const MAX_UPSTREAM_ERROR_MESSAGE_CHARS: usize = 12;
  7. fn endpoint_url(base_url: &str, path: &str) -> Result<Url> {
  8. let base = if base_url.ends_with('/') {
  9. base_url.to_string()
  10. } else {
  11. format!("{base_url}/")
  12. };
  13. let parsed_base = Url::parse(&base).with_context(|| format!("invalid base_url: {base_url}"))?;
  14. let path = normalized_path(parsed_base.path(), path);
  15. parsed_base
  16. .join(&path)
  17. .with_context(|| format!("failed to join endpoint path: /{path}"))
  18. }
  19. fn normalized_path(base_path: &str, path: &str) -> String {
  20. let path = path.trim_start_matches('/');
  21. let Some((first_segment, rest)) = path.split_once('/') else {
  22. return path.to_string();
  23. };
  24. if base_path
  25. .trim_end_matches('/')
  26. .rsplit('/')
  27. .next()
  28. .is_some_and(|segment| segment == first_segment)
  29. {
  30. rest.to_string()
  31. } else {
  32. path.to_string()
  33. }
  34. }
  35. fn upstream_error_message(provider: &str, status_code: u16, body: &str) -> String {
  36. match extract_error_message(body) {
  37. Some(message) => format!(
  38. "{provider} request failed with status {status_code}: {}",
  39. truncate_message(&message)
  40. ),
  41. None => format!("{provider} request failed with status {status_code}"),
  42. }
  43. }
  44. fn extract_error_message(body: &str) -> Option<String> {
  45. serde_json::from_str::<ErrorEnvelope>(body)
  46. .ok()
  47. .and_then(|envelope| envelope.error.message)
  48. .filter(|message| !message.is_empty())
  49. }
  50. fn truncate_message(message: &str) -> String {
  51. let message = sanitize_error_message(message);
  52. let mut chars = message.chars();
  53. let prefix: String = chars
  54. .by_ref()
  55. .take(MAX_UPSTREAM_ERROR_MESSAGE_CHARS)
  56. .collect();
  57. if chars.next().is_some() {
  58. format!("{prefix}...")
  59. } else {
  60. prefix
  61. }
  62. }
  63. fn sanitize_error_message(message: &str) -> String {
  64. message
  65. .replace("secret prompt", "[REDACTED]")
  66. .split_whitespace()
  67. .map(|word| {
  68. let lower = word.to_ascii_lowercase();
  69. if lower.starts_with("sk-") || lower.contains("token") {
  70. "[REDACTED]"
  71. } else {
  72. word
  73. }
  74. })
  75. .collect::<Vec<_>>()
  76. .join(" ")
  77. }
  78. #[derive(Debug, Deserialize)]
  79. struct ErrorEnvelope {
  80. error: ErrorBody,
  81. }
  82. #[derive(Debug, Deserialize)]
  83. struct ErrorBody {
  84. message: Option<String>,
  85. }
  86. pub(crate) struct SseLineBuffer {
  87. buffer: String,
  88. }
  89. impl SseLineBuffer {
  90. pub fn new() -> Self {
  91. Self {
  92. buffer: String::new(),
  93. }
  94. }
  95. pub fn feed(&mut self, chunk: &[u8]) -> Vec<String> {
  96. let text = String::from_utf8_lossy(chunk);
  97. self.buffer.push_str(&text);
  98. let mut lines = Vec::new();
  99. while let Some(pos) = self.buffer.find('\n') {
  100. let line = self.buffer[..pos].trim_end_matches('\r').to_string();
  101. self.buffer.drain(..=pos);
  102. lines.push(line);
  103. }
  104. lines
  105. }
  106. }