1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use reqwest::Client;
use serde_json::Value;
use std::collections::HashSet;
use std::time::Duration;
use crate::error::AppError;
use super::ProviderService;
impl ProviderService {
/// 尝试从远端拉取模型列表
pub async fn fetch_provider_models(
base_url: &str,
api_key: Option<&str>,
) -> Result<Vec<String>, AppError> {
let base_url = base_url.trim().trim_end_matches('/');
if base_url.is_empty() {
return Err(AppError::localized(
"fetch.invalid_url",
"URL 不能为空",
"URL cannot be empty",
));
}
let mut candidate_urls = Vec::new();
// 如果用户直接填了 /v1/models 或者 /models,我们就直接用
if base_url.ends_with("/models") {
candidate_urls.push(base_url.to_string());
} else {
// 智能适配:如果没带 /models,尝试追加
candidate_urls.push(format!("{}/models", base_url));
if !base_url.ends_with("/v1") && !base_url.ends_with("/v1beta") {
candidate_urls.push(format!("{}/v1/models", base_url));
}
}
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()
.map_err(|e| AppError::Message(e.to_string()))?;
let mut last_err_zh = None;
let mut last_err_en = None;
for url in candidate_urls {
let mut req = client.get(&url);
if let Some(key) = api_key {
let key = key.trim();
// 同时添加 OpenAI 的 Bearer 和 Anthropic 的 x-api-key 格式,代理服务通常会接受其中之一
req = req
.header("Authorization", format!("Bearer {}", key))
.header("x-api-key", key);
}
match req.send().await {
Ok(resp) => {
if resp.status().is_success() {
if let Ok(json) = resp.json::<Value>().await {
let mut models = Vec::new();
// 测试格式 1: OpenAI 兼容格式 {"data": [{"id": "gpt-4o"}]}
if let Some(data) = json.get("data").and_then(|d| d.as_array()) {
for item in data {
if let Some(id) = item.get("id").and_then(|i| i.as_str()) {
models.push(id.to_string());
}
}
}
// 测试格式 2: Gemini 格式 {"models": [{"name": "models/gemini-pro"}]}
if models.is_empty() {
if let Some(data) = json.get("models").and_then(|d| d.as_array()) {
for item in data {
if let Some(name) =
item.get("name").and_then(|i| i.as_str())
{
let id = name.strip_prefix("models/").unwrap_or(name);
models.push(id.to_string());
}
}
}
}
// 测试格式 3: 直接的数组格式 [{"id": "llama-3"}]
if models.is_empty() {
if let Some(arr) = json.as_array() {
for item in arr {
if let Some(id) = item.get("id").and_then(|i| i.as_str()) {
models.push(id.to_string());
}
}
}
}
if !models.is_empty() {
// 保序去重,避免非相邻重复项残留。
let mut seen = HashSet::new();
models.retain(|model| seen.insert(model.clone()));
return Ok(models);
} else {
last_err_zh =
Some(format!("未能在响应中找到模型列表 (URL: {})", url));
last_err_en =
Some(format!("No model list found in response (URL: {})", url));
}
} else {
last_err_zh = Some(format!("无法解析 JSON 响应 (URL: {})", url));
last_err_en =
Some(format!("Failed to parse JSON response (URL: {})", url));
}
} else {
let err = format!("HTTP {} (URL: {})", resp.status(), url);
last_err_zh = Some(err.clone());
last_err_en = Some(err);
}
}
Err(e) => {
let err = e.to_string();
last_err_zh = Some(err.clone());
last_err_en = Some(err);
}
}
}
let err_zh = last_err_zh.unwrap_or_else(|| "未知错误".to_string());
let err_en = last_err_en.unwrap_or_else(|| "Unknown error".to_string());
Err(AppError::localized(
"fetch.failed",
format!("拉取失败: {}", err_zh),
format!("Fetch failed: {}", err_en),
))
}
}