mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: 修复智谱 API 路径问题并支持自定义模型列表
修复: 1. 修复 OpenAI Custom Provider 的 URL 路径拼接问题 - 支持检测 /v1, /v2, /v3, /v4 等版本号路径 - 避免重复拼接导致 /v4/v1/chat/completions 错误 新功能: 1. API Key Provider 支持自定义模型列表 - 数据库添加 custom_models 字段 - 前端 Provider 配置表单添加自定义模型输入 - 用于不支持 /models 接口的 Provider(如智谱) 2. API Server 测试页面使用自定义模型 - 自动使用当前 Provider 的自定义模型进行测试 - 为多个自定义模型生成独立测试端点
This commit is contained in:
Generated
+66
@@ -71,6 +71,9 @@ importers:
|
||||
'@tauri-apps/plugin-shell':
|
||||
specifier: ^2.0.0
|
||||
version: 2.3.3
|
||||
'@tonejs/midi':
|
||||
specifier: ^2.0.28
|
||||
version: 2.0.28
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
@@ -164,6 +167,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
tone:
|
||||
specifier: ^15.1.22
|
||||
version: 15.1.22
|
||||
devDependencies:
|
||||
'@babel/plugin-transform-react-jsx-source':
|
||||
specifier: ^7.27.1
|
||||
@@ -1320,56 +1326,67 @@ packages:
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
@@ -1493,30 +1510,35 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.9.6':
|
||||
resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
|
||||
resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.9.6':
|
||||
resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.9.6':
|
||||
resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.9.6':
|
||||
resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==}
|
||||
@@ -1553,6 +1575,9 @@ packages:
|
||||
'@tauri-apps/plugin-shell@2.3.3':
|
||||
resolution: {integrity: sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ==}
|
||||
|
||||
'@tonejs/midi@2.0.28':
|
||||
resolution: {integrity: sha512-RII6YpInPsOZ5t3Si/20QKpNqB1lZ2OCFJSOzJxz38YdY/3zqDr3uaml4JuCWkdixuPqP1/TBnXzhQ39csyoVg==}
|
||||
|
||||
'@tootallnate/once@2.0.0':
|
||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -1833,6 +1858,9 @@ packages:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
array-flatten@3.0.0:
|
||||
resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1840,6 +1868,10 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
automation-events@7.1.14:
|
||||
resolution: {integrity: sha512-33doW0iTYXR2gSNBEKosQfcUZw1j3PCo3le41Wp3LRKStYXjWejTSjkd38Tm6b5AF0k+0IHmgDO0hfVROyDoUQ==}
|
||||
engines: {node: '>=18.2.0'}
|
||||
|
||||
autoprefixer@10.4.23:
|
||||
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -2993,6 +3025,9 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
midi-file@1.2.4:
|
||||
resolution: {integrity: sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3499,6 +3534,9 @@ packages:
|
||||
stackback@0.0.2:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
standardized-audio-context@25.3.77:
|
||||
resolution: {integrity: sha512-Ki9zNz6pKcC5Pi+QPjPyVsD9GwJIJWgryji0XL9cAJXMGyn+dPOf6Qik1AHei0+UNVcc4BOCa0hWLBzlwqsW/A==}
|
||||
|
||||
std-env@3.10.0:
|
||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||
|
||||
@@ -3603,6 +3641,9 @@ packages:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
tone@15.1.22:
|
||||
resolution: {integrity: sha512-TCScAGD4sLsama5DjvTUXlLDXSqPealhL64nsdV1hhr6frPWve0DeSo63AKnSJwgfg55fhvxj0iPPRwPN5o0ag==}
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5111,6 +5152,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.9.1
|
||||
|
||||
'@tonejs/midi@2.0.28':
|
||||
dependencies:
|
||||
array-flatten: 3.0.0
|
||||
midi-file: 1.2.4
|
||||
|
||||
'@tootallnate/once@2.0.0':
|
||||
optional: true
|
||||
|
||||
@@ -5439,11 +5485,18 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
array-flatten@3.0.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
asynckit@0.4.0:
|
||||
optional: true
|
||||
|
||||
automation-events@7.1.14:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
tslib: 2.8.1
|
||||
|
||||
autoprefixer@10.4.23(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -7006,6 +7059,8 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
midi-file@1.2.4: {}
|
||||
|
||||
mime-db@1.52.0:
|
||||
optional: true
|
||||
|
||||
@@ -7543,6 +7598,12 @@ snapshots:
|
||||
|
||||
stackback@0.0.2: {}
|
||||
|
||||
standardized-audio-context@25.3.77:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
automation-events: 7.1.14
|
||||
tslib: 2.8.1
|
||||
|
||||
std-env@3.10.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
@@ -7684,6 +7745,11 @@ snapshots:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
tone@15.1.22:
|
||||
dependencies:
|
||||
standardized-audio-context: 25.3.77
|
||||
tslib: 2.8.1
|
||||
|
||||
tough-cookie@4.1.4:
|
||||
dependencies:
|
||||
psl: 1.15.0
|
||||
|
||||
@@ -45,6 +45,8 @@ pub struct UpdateProviderRequest {
|
||||
pub project: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub region: Option<String>,
|
||||
/// 自定义模型列表
|
||||
pub custom_models: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 添加 API Key 请求
|
||||
@@ -71,6 +73,8 @@ pub struct ProviderDisplay {
|
||||
pub project: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub region: Option<String>,
|
||||
/// 自定义模型列表
|
||||
pub custom_models: Vec<String>,
|
||||
pub api_key_count: usize,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
@@ -130,6 +134,7 @@ fn provider_to_display(provider: &ApiKeyProvider, api_key_count: usize) -> Provi
|
||||
project: provider.project.clone(),
|
||||
location: provider.location.clone(),
|
||||
region: provider.region.clone(),
|
||||
custom_models: provider.custom_models.clone(),
|
||||
api_key_count,
|
||||
created_at: provider.created_at.to_rfc3339(),
|
||||
updated_at: provider.updated_at.to_rfc3339(),
|
||||
@@ -247,6 +252,7 @@ pub fn update_api_key_provider(
|
||||
request.project,
|
||||
request.location,
|
||||
request.region,
|
||||
request.custom_models,
|
||||
)?;
|
||||
|
||||
// 获取 API Key 数量
|
||||
|
||||
@@ -126,6 +126,10 @@ pub struct ApiKeyProvider {
|
||||
pub project: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub region: Option<String>,
|
||||
/// 自定义模型列表(JSON 数组格式存储)
|
||||
/// 用于不支持 /models 接口的 Provider(如智谱)
|
||||
#[serde(default)]
|
||||
pub custom_models: Vec<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -166,7 +170,7 @@ impl ApiKeyProviderDao {
|
||||
pub fn get_all_providers(conn: &Connection) -> Result<Vec<ApiKeyProvider>, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, type, api_host, is_system, group_name, enabled, sort_order,
|
||||
api_version, project, location, region, created_at, updated_at
|
||||
api_version, project, location, region, custom_models, created_at, updated_at
|
||||
FROM api_key_providers
|
||||
ORDER BY sort_order ASC, created_at ASC",
|
||||
)?;
|
||||
@@ -186,7 +190,7 @@ impl ApiKeyProviderDao {
|
||||
) -> Result<Option<ApiKeyProvider>, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, type, api_host, is_system, group_name, enabled, sort_order,
|
||||
api_version, project, location, region, created_at, updated_at
|
||||
api_version, project, location, region, custom_models, created_at, updated_at
|
||||
FROM api_key_providers
|
||||
WHERE id = ?1",
|
||||
)?;
|
||||
@@ -206,7 +210,7 @@ impl ApiKeyProviderDao {
|
||||
) -> Result<Vec<ApiKeyProvider>, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, type, api_host, is_system, group_name, enabled, sort_order,
|
||||
api_version, project, location, region, created_at, updated_at
|
||||
api_version, project, location, region, custom_models, created_at, updated_at
|
||||
FROM api_key_providers
|
||||
WHERE group_name = ?1
|
||||
ORDER BY sort_order ASC, created_at ASC",
|
||||
@@ -225,11 +229,17 @@ impl ApiKeyProviderDao {
|
||||
conn: &Connection,
|
||||
provider: &ApiKeyProvider,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
let custom_models_json = if provider.custom_models.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::to_string(&provider.custom_models).unwrap_or_default())
|
||||
};
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO api_key_providers
|
||||
(id, name, type, api_host, is_system, group_name, enabled, sort_order,
|
||||
api_version, project, location, region, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||||
api_version, project, location, region, custom_models, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
provider.id,
|
||||
provider.name,
|
||||
@@ -243,6 +253,7 @@ impl ApiKeyProviderDao {
|
||||
provider.project,
|
||||
provider.location,
|
||||
provider.region,
|
||||
custom_models_json,
|
||||
provider.created_at.to_rfc3339(),
|
||||
provider.updated_at.to_rfc3339(),
|
||||
],
|
||||
@@ -255,11 +266,17 @@ impl ApiKeyProviderDao {
|
||||
conn: &Connection,
|
||||
provider: &ApiKeyProvider,
|
||||
) -> Result<(), rusqlite::Error> {
|
||||
let custom_models_json = if provider.custom_models.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::to_string(&provider.custom_models).unwrap_or_default())
|
||||
};
|
||||
|
||||
conn.execute(
|
||||
"UPDATE api_key_providers SET
|
||||
name = ?2, type = ?3, api_host = ?4, is_system = ?5, group_name = ?6,
|
||||
enabled = ?7, sort_order = ?8, api_version = ?9, project = ?10,
|
||||
location = ?11, region = ?12, updated_at = ?13
|
||||
location = ?11, region = ?12, custom_models = ?13, updated_at = ?14
|
||||
WHERE id = ?1",
|
||||
params![
|
||||
provider.id,
|
||||
@@ -274,6 +291,7 @@ impl ApiKeyProviderDao {
|
||||
provider.project,
|
||||
provider.location,
|
||||
provider.region,
|
||||
custom_models_json,
|
||||
provider.updated_at.to_rfc3339(),
|
||||
],
|
||||
)?;
|
||||
@@ -311,8 +329,9 @@ impl ApiKeyProviderDao {
|
||||
let project: Option<String> = row.get(9)?;
|
||||
let location: Option<String> = row.get(10)?;
|
||||
let region: Option<String> = row.get(11)?;
|
||||
let created_at_str: String = row.get(12)?;
|
||||
let updated_at_str: String = row.get(13)?;
|
||||
let custom_models_json: Option<String> = row.get(12)?;
|
||||
let created_at_str: String = row.get(13)?;
|
||||
let updated_at_str: String = row.get(14)?;
|
||||
|
||||
let provider_type: ApiProviderType = type_str.parse().unwrap_or(ApiProviderType::Openai);
|
||||
let group: ProviderGroup = group_str.parse().unwrap_or(ProviderGroup::Custom);
|
||||
@@ -324,6 +343,11 @@ impl ApiKeyProviderDao {
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now());
|
||||
|
||||
// 解析自定义模型列表
|
||||
let custom_models: Vec<String> = custom_models_json
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(ApiKeyProvider {
|
||||
id,
|
||||
name,
|
||||
@@ -337,6 +361,7 @@ impl ApiKeyProviderDao {
|
||||
project,
|
||||
location,
|
||||
region,
|
||||
custom_models,
|
||||
created_at,
|
||||
updated_at,
|
||||
})
|
||||
@@ -398,7 +423,7 @@ impl ApiKeyProviderDao {
|
||||
k.usage_count, k.error_count, k.last_used_at, k.created_at,
|
||||
p.id, p.name, p.type, p.api_host, p.is_system, p.group_name, p.enabled,
|
||||
p.sort_order, p.api_version, p.project, p.location, p.region,
|
||||
p.created_at, p.updated_at
|
||||
p.custom_models, p.created_at, p.updated_at
|
||||
FROM api_keys k
|
||||
JOIN api_key_providers p ON k.provider_id = p.id
|
||||
WHERE p.type = ?1 AND k.enabled = 1 AND p.enabled = 1
|
||||
@@ -431,8 +456,9 @@ impl ApiKeyProviderDao {
|
||||
};
|
||||
|
||||
// 解析 Provider
|
||||
let provider_created_at_str: String = row.get(21)?;
|
||||
let provider_updated_at_str: String = row.get(22)?;
|
||||
let custom_models_json: Option<String> = row.get(21)?;
|
||||
let provider_created_at_str: String = row.get(22)?;
|
||||
let provider_updated_at_str: String = row.get(23)?;
|
||||
let provider_created_at = DateTime::parse_from_rfc3339(&provider_created_at_str)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now());
|
||||
@@ -440,6 +466,11 @@ impl ApiKeyProviderDao {
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now());
|
||||
|
||||
// 解析自定义模型列表
|
||||
let custom_models: Vec<String> = custom_models_json
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let provider = ApiKeyProvider {
|
||||
id: row.get(9)?,
|
||||
name: row.get(10)?,
|
||||
@@ -459,6 +490,7 @@ impl ApiKeyProviderDao {
|
||||
project: row.get(18)?,
|
||||
location: row.get(19)?,
|
||||
region: row.get(20)?,
|
||||
custom_models,
|
||||
created_at: provider_created_at,
|
||||
updated_at: provider_updated_at,
|
||||
};
|
||||
@@ -666,7 +698,7 @@ impl ApiKeyProviderDao {
|
||||
) -> Result<Vec<ProviderWithKeys>, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, type, api_host, is_system, group_name, enabled, sort_order,
|
||||
api_version, project, location, region, created_at, updated_at
|
||||
api_version, project, location, region, custom_models, created_at, updated_at
|
||||
FROM api_key_providers
|
||||
WHERE enabled = 1
|
||||
ORDER BY sort_order ASC, created_at ASC",
|
||||
|
||||
@@ -17,12 +17,19 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> {
|
||||
project TEXT,
|
||||
location TEXT,
|
||||
region TEXT,
|
||||
custom_models TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// Migration: 添加 custom_models 列(如果不存在)
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE api_key_providers ADD COLUMN custom_models TEXT",
|
||||
[],
|
||||
);
|
||||
|
||||
// 创建 api_key_providers 索引
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_api_key_providers_group ON api_key_providers(group_name)",
|
||||
|
||||
@@ -626,6 +626,7 @@ pub fn to_api_key_provider(def: &SystemProviderDef) -> ApiKeyProvider {
|
||||
project: None,
|
||||
location: None,
|
||||
region: None,
|
||||
custom_models: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
<!--
|
||||
* @Author: Chiron 598621670@qq.com
|
||||
* @Date: 2026-01-06 17:34:03
|
||||
* @LastEditors: Chiron 598621670@qq.com
|
||||
* @LastEditTime: 2026-01-11 01:09:49
|
||||
* @FilePath: /proxycast/src-tauri/src/providers/README.md
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
# providers
|
||||
|
||||
<!-- 一旦我所属的文件夹有所变化,请更新我 -->
|
||||
|
||||
@@ -54,16 +54,34 @@ impl OpenAICustomProvider {
|
||||
}
|
||||
|
||||
/// 构建完整的 API URL
|
||||
/// 智能处理用户输入的 base_url,无论是否带 /v1 都能正确工作
|
||||
/// 智能处理用户输入的 base_url,支持多种 API 版本格式
|
||||
///
|
||||
/// 支持的格式:
|
||||
/// - `https://api.openai.com` -> `https://api.openai.com/v1/chat/completions`
|
||||
/// - `https://api.openai.com/v1` -> `https://api.openai.com/v1/chat/completions`
|
||||
/// - `https://open.bigmodel.cn/api/paas/v4` -> `https://open.bigmodel.cn/api/paas/v4/chat/completions`
|
||||
/// - `https://api.deepseek.com/v1` -> `https://api.deepseek.com/v1/chat/completions`
|
||||
fn build_url(&self, endpoint: &str) -> String {
|
||||
let base = self.get_base_url();
|
||||
let base = base.trim_end_matches('/');
|
||||
|
||||
// 如果用户输入了带 /v1 的 URL,直接拼接 endpoint
|
||||
// 否则拼接 /v1/endpoint
|
||||
if base.ends_with("/v1") {
|
||||
// 检查是否已经包含版本号路径(/v1, /v2, /v3, /v4 等)
|
||||
// 使用正则匹配 /v 后跟数字的模式
|
||||
let has_version = base
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.map(|last_segment| {
|
||||
last_segment.starts_with('v')
|
||||
&& last_segment.len() >= 2
|
||||
&& last_segment[1..].chars().all(|c| c.is_ascii_digit())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_version {
|
||||
// 已有版本号,直接拼接 endpoint
|
||||
format!("{}/{}", base, endpoint)
|
||||
} else {
|
||||
// 没有版本号,添加 /v1
|
||||
format!("{}/v1/{}", base, endpoint)
|
||||
}
|
||||
}
|
||||
@@ -104,6 +122,9 @@ impl OpenAICustomProvider {
|
||||
.ok_or("OpenAI API key not configured")?;
|
||||
|
||||
let url = self.build_url("chat/completions");
|
||||
|
||||
eprintln!("[OPENAI_CUSTOM] chat_completions URL: {}", url);
|
||||
eprintln!("[OPENAI_CUSTOM] chat_completions base_url: {}", self.get_base_url());
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
@@ -125,6 +146,8 @@ impl OpenAICustomProvider {
|
||||
.ok_or("OpenAI API key not configured")?;
|
||||
|
||||
let url = self.build_url("models");
|
||||
|
||||
eprintln!("[OPENAI_CUSTOM] list_models URL: {}", url);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
@@ -136,6 +159,7 @@ impl OpenAICustomProvider {
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
eprintln!("[OPENAI_CUSTOM] list_models 失败: {} - {}", status, body);
|
||||
return Err(format!("Failed to list models: {status} - {body}").into());
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@ impl ApiKeyProviderService {
|
||||
project,
|
||||
location,
|
||||
region,
|
||||
custom_models: Vec::new(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -265,6 +266,7 @@ impl ApiKeyProviderService {
|
||||
project: Option<String>,
|
||||
location: Option<String>,
|
||||
region: Option<String>,
|
||||
custom_models: Option<Vec<String>>,
|
||||
) -> Result<ApiKeyProvider, String> {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
let mut provider = ApiKeyProviderDao::get_provider_by_id(&conn, id)
|
||||
@@ -296,6 +298,9 @@ impl ApiKeyProviderService {
|
||||
if let Some(r) = region {
|
||||
provider.region = if r.is_empty() { None } else { Some(r) };
|
||||
}
|
||||
if let Some(models) = custom_models {
|
||||
provider.custom_models = models;
|
||||
}
|
||||
provider.updated_at = Utc::now();
|
||||
|
||||
ApiKeyProviderDao::update_provider(&conn, &provider).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -527,8 +527,27 @@ export function ApiServerPage() {
|
||||
const serverUrl = getTestUrl(currentHost, currentPort);
|
||||
const apiKey = config?.server.api_key ?? "";
|
||||
|
||||
// 获取当前选中 Provider 的自定义模型列表
|
||||
const getCurrentProviderCustomModels = (): string[] => {
|
||||
// 先从 API Key Provider 中查找
|
||||
const apiKeyProvider = apiKeyProviders.find(
|
||||
(p) => p.id === defaultProvider && p.enabled
|
||||
);
|
||||
if (apiKeyProvider?.custom_models && apiKeyProvider.custom_models.length > 0) {
|
||||
return apiKeyProvider.custom_models;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 根据 Provider 类型获取测试模型
|
||||
const getTestModel = (provider: string): string => {
|
||||
// 优先使用自定义模型列表中的第一个模型
|
||||
const customModels = getCurrentProviderCustomModels();
|
||||
if (customModels.length > 0) {
|
||||
return customModels[0];
|
||||
}
|
||||
|
||||
// 否则使用默认模型
|
||||
switch (provider) {
|
||||
case "antigravity":
|
||||
return "gemini-3-pro-preview";
|
||||
@@ -542,6 +561,8 @@ export function ApiServerPage() {
|
||||
return "claude-sonnet-4-20250514";
|
||||
case "deepseek":
|
||||
return "deepseek-chat";
|
||||
case "zhipu":
|
||||
return "glm-4";
|
||||
case "kiro":
|
||||
default:
|
||||
return "claude-opus-4-5-20251101";
|
||||
@@ -549,6 +570,7 @@ export function ApiServerPage() {
|
||||
};
|
||||
|
||||
const testModel = getTestModel(defaultProvider);
|
||||
const customModels = getCurrentProviderCustomModels();
|
||||
|
||||
// 根据 Provider 类型获取 Gemini 测试模型列表
|
||||
const getGeminiTestModels = (provider: string): string[] => {
|
||||
@@ -593,7 +615,7 @@ export function ApiServerPage() {
|
||||
},
|
||||
{
|
||||
id: "chat",
|
||||
name: "OpenAI Chat",
|
||||
name: `OpenAI Chat (${testModel})`,
|
||||
method: "POST",
|
||||
path: "/v1/chat/completions",
|
||||
needsAuth: true,
|
||||
@@ -602,9 +624,23 @@ export function ApiServerPage() {
|
||||
messages: [{ role: "user", content: "Say hi in one word" }],
|
||||
}),
|
||||
},
|
||||
// 为自定义模型列表中的其他模型生成测试端点
|
||||
...(customModels.length > 1
|
||||
? customModels.slice(1).map((model, index) => ({
|
||||
id: `custom-model-${index}`,
|
||||
name: `OpenAI Chat (${model})`,
|
||||
method: "POST",
|
||||
path: "/v1/chat/completions",
|
||||
needsAuth: true,
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [{ role: "user", content: "Say hi in one word" }],
|
||||
}),
|
||||
}))
|
||||
: []),
|
||||
{
|
||||
id: "anthropic",
|
||||
name: "Anthropic Messages",
|
||||
name: `Anthropic Messages (${testModel})`,
|
||||
method: "POST",
|
||||
path: "/v1/messages",
|
||||
needsAuth: true,
|
||||
|
||||
@@ -86,6 +86,7 @@ interface FormState {
|
||||
project: string;
|
||||
location: string;
|
||||
region: string;
|
||||
customModels: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -125,6 +126,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
|
||||
project: provider.project || "",
|
||||
location: provider.location || "",
|
||||
region: provider.region || "",
|
||||
customModels: (provider.custom_models || []).join(", "),
|
||||
});
|
||||
|
||||
// 保存状态
|
||||
@@ -143,6 +145,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
|
||||
project: provider.project || "",
|
||||
location: provider.location || "",
|
||||
region: provider.region || "",
|
||||
customModels: (provider.custom_models || []).join(", "),
|
||||
});
|
||||
setSaveError(null);
|
||||
}, [
|
||||
@@ -152,6 +155,7 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
|
||||
provider.project,
|
||||
provider.location,
|
||||
provider.region,
|
||||
provider.custom_models,
|
||||
]);
|
||||
|
||||
// 保存配置
|
||||
@@ -163,12 +167,19 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
|
||||
setSaveError(null);
|
||||
|
||||
try {
|
||||
// 解析自定义模型列表(逗号分隔)
|
||||
const customModels = state.customModels
|
||||
.split(",")
|
||||
.map((m) => m.trim())
|
||||
.filter((m) => m.length > 0);
|
||||
|
||||
const request: UpdateProviderRequest = {
|
||||
api_host: state.apiHost || undefined,
|
||||
api_version: state.apiVersion || undefined,
|
||||
project: state.project || undefined,
|
||||
location: state.location || undefined,
|
||||
region: state.region || undefined,
|
||||
custom_models: customModels.length > 0 ? customModels : undefined,
|
||||
};
|
||||
|
||||
await onUpdate(provider.id, request);
|
||||
@@ -330,6 +341,25 @@ export const ProviderConfigForm: React.FC<ProviderConfigFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义模型列表 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="custom-models" className="text-sm font-medium">
|
||||
自定义模型
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-models"
|
||||
type="text"
|
||||
value={formState.customModels}
|
||||
onChange={(e) => handleFieldChange("customModels", e.target.value)}
|
||||
placeholder="glm-4, glm-4-flash, glm-4.7"
|
||||
disabled={loading || isSaving}
|
||||
data-testid="custom-models-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
该 Provider 支持的模型列表,用逗号分隔。用于不支持 /models 接口的 Provider(如智谱)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 保存状态指示 */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
{isSaving ? (
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface UpdateProviderRequest {
|
||||
project?: string;
|
||||
location?: string;
|
||||
region?: string;
|
||||
/** 自定义模型列表 */
|
||||
custom_models?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +71,8 @@ export interface ProviderDisplay {
|
||||
project?: string;
|
||||
location?: string;
|
||||
region?: string;
|
||||
/** 自定义模型列表 */
|
||||
custom_models?: string[];
|
||||
api_key_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
Reference in New Issue
Block a user