Merge pull request #324 from H-Chris233/fix/issue-323-llm-providers
增加更多 LLM Provider 预设
This commit is contained in:
8 files changed
+158
-13
No files matched your search
@@ -234,7 +234,7 @@ pub fn get_credentials() -> CredentialsStatus {
|
||||
let active_llm_provider = CredentialsVault::get_active_llm();
|
||||
let volcengine_configured = volcengine_configured(&snap);
|
||||
let asr_configured = asr_configured_for_provider(&active_asr_provider, &snap);
|
||||
let llm_configured = llm_configured_for_snapshot(&snap);
|
||||
let llm_configured = llm_configured_for_provider(&active_llm_provider, &snap);
|
||||
CredentialsStatus {
|
||||
active_asr_provider,
|
||||
active_llm_provider,
|
||||
@@ -262,8 +262,43 @@ fn asr_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bo
|
||||
configured(&snap.asr_endpoint) && configured(&snap.asr_model)
|
||||
}
|
||||
|
||||
fn llm_configured_for_snapshot(snap: &CredentialsSnapshot) -> bool {
|
||||
configured(&snap.ark_endpoint) && configured(&snap.ark_model_id)
|
||||
fn llm_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bool {
|
||||
let endpoint = snap.ark_endpoint.as_deref().unwrap_or_default();
|
||||
let endpoint_and_model = configured(&snap.ark_endpoint) && configured(&snap.ark_model_id);
|
||||
if endpoint_and_model
|
||||
&& llm_provider_default_endpoint(provider)
|
||||
.map(|default| same_llm_endpoint(endpoint, default))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return configured(&snap.ark_api_key);
|
||||
}
|
||||
endpoint_and_model
|
||||
}
|
||||
|
||||
fn llm_provider_default_endpoint(provider: &str) -> Option<&'static str> {
|
||||
match provider {
|
||||
"ark" => Some("https://ark.cn-beijing.volces.com/api/v3"),
|
||||
"deepseek" => Some("https://api.deepseek.com/v1"),
|
||||
"siliconflow" => Some("https://api.siliconflow.cn/v1"),
|
||||
"openai" => Some("https://api.openai.com/v1"),
|
||||
"mimo" => Some("https://api.xiaomimimo.com/v1"),
|
||||
"cometapi" => Some("https://api.cometapi.com/v1"),
|
||||
"openrouterFree" => Some("https://openrouter.ai/api/v1"),
|
||||
"alibabaCoding" => Some("https://coding-intl.dashscope.aliyuncs.com/v1"),
|
||||
"codingPlanX" => Some("https://api.codingplanx.ai/v1"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn same_llm_endpoint(a: &str, b: &str) -> bool {
|
||||
fn normalize(value: &str) -> &str {
|
||||
value
|
||||
.trim()
|
||||
.trim_end_matches('/')
|
||||
.trim_end_matches("/chat/completions")
|
||||
.trim_end_matches('/')
|
||||
}
|
||||
normalize(a).eq_ignore_ascii_case(normalize(b))
|
||||
}
|
||||
|
||||
fn configured(field: &Option<String>) -> bool {
|
||||
@@ -1554,7 +1589,7 @@ mod tests {
|
||||
use super::{
|
||||
active_asr_is_keyless_for_validation, active_foundry_model_from_prefs,
|
||||
asr_configured_for_provider, asr_transcriptions_url, fetch_provider_models,
|
||||
llm_configured_for_snapshot, local_asr_release_plan_for_provider, models_url,
|
||||
llm_configured_for_provider, local_asr_release_plan_for_provider, models_url,
|
||||
normalize_foundry_language_hint, parse_model_ids, persist_settings,
|
||||
release_foundry_runtime_if_inactive, validate_foundry_model_alias, ProviderConfig,
|
||||
SettingsWriter,
|
||||
@@ -1719,26 +1754,43 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credentials_status_accepts_keyless_llm_with_endpoint_and_model() {
|
||||
fn credentials_status_accepts_keyless_custom_llm_only() {
|
||||
let keyless_ready = CredentialsSnapshot {
|
||||
ark_endpoint: Some("http://localhost:11434/v1".into()),
|
||||
ark_model_id: Some("qwen".into()),
|
||||
..snapshot()
|
||||
};
|
||||
assert!(llm_configured_for_snapshot(&keyless_ready));
|
||||
assert!(llm_configured_for_provider("custom", &keyless_ready));
|
||||
assert!(llm_configured_for_provider("self-hosted", &keyless_ready));
|
||||
assert!(llm_configured_for_provider("openrouterFree", &keyless_ready));
|
||||
|
||||
let hosted_keyless = CredentialsSnapshot {
|
||||
ark_endpoint: Some("https://openrouter.ai/api/v1".into()),
|
||||
ark_model_id: Some("qwen/qwen3-coder:free".into()),
|
||||
..snapshot()
|
||||
};
|
||||
assert!(!llm_configured_for_provider("openrouterFree", &hosted_keyless));
|
||||
|
||||
let hosted_ready = CredentialsSnapshot {
|
||||
ark_api_key: Some("key".into()),
|
||||
ark_endpoint: Some("https://openrouter.ai/api/v1/chat/completions".into()),
|
||||
ark_model_id: Some("qwen/qwen3-coder:free".into()),
|
||||
..snapshot()
|
||||
};
|
||||
assert!(llm_configured_for_provider("openrouterFree", &hosted_ready));
|
||||
|
||||
let key_without_endpoint = CredentialsSnapshot {
|
||||
ark_api_key: Some("key".into()),
|
||||
ark_model_id: Some("qwen".into()),
|
||||
..snapshot()
|
||||
};
|
||||
assert!(!llm_configured_for_snapshot(&key_without_endpoint));
|
||||
assert!(!llm_configured_for_provider("custom", &key_without_endpoint));
|
||||
|
||||
let endpoint_without_model = CredentialsSnapshot {
|
||||
ark_endpoint: Some("http://localhost:11434/v1".into()),
|
||||
..snapshot()
|
||||
};
|
||||
assert!(!llm_configured_for_snapshot(&endpoint_without_model));
|
||||
assert!(!llm_configured_for_provider("custom", &endpoint_without_model));
|
||||
}
|
||||
|
||||
impl SettingsWriter for FakeSettingsWriter {
|
||||
|
||||
@@ -317,6 +317,11 @@ export const en: typeof zhCN = {
|
||||
deepseek: 'DeepSeek',
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter (free models)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
custom: 'Custom',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrSiliconflow: 'SiliconFlow SenseVoice',
|
||||
|
||||
@@ -319,6 +319,11 @@ export const ja: typeof zhCN = {
|
||||
deepseek: 'DeepSeek',
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(無料モデル)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
custom: 'カスタム',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrSiliconflow: 'SiliconFlow SenseVoice',
|
||||
|
||||
@@ -319,6 +319,11 @@ export const ko: typeof zhCN = {
|
||||
deepseek: 'DeepSeek',
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(무료 모델)',
|
||||
alibabaCoding: 'Alibaba Cloud Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
custom: '사용자 정의',
|
||||
asrVolcengine: 'Volcengine bigasr',
|
||||
asrSiliconflow: 'SiliconFlow SenseVoice',
|
||||
|
||||
@@ -315,6 +315,11 @@ export const zhCN = {
|
||||
deepseek: 'DeepSeek',
|
||||
siliconflow: '硅基流动',
|
||||
openai: 'OpenAI',
|
||||
mimo: '小米 MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(免费模型)',
|
||||
alibabaCoding: '阿里云 Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
custom: '自定义',
|
||||
asrVolcengine: '火山引擎 bigasr',
|
||||
asrSiliconflow: '硅基流动 SenseVoice',
|
||||
|
||||
@@ -317,6 +317,11 @@ export const zhTW: typeof zhCN = {
|
||||
deepseek: 'DeepSeek',
|
||||
siliconflow: '硅基流動',
|
||||
openai: 'OpenAI',
|
||||
mimo: '小米 MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(免費模型)',
|
||||
alibabaCoding: '阿里雲 Coding Plan',
|
||||
codingPlanX: 'CodingPlanX',
|
||||
custom: '自定義',
|
||||
asrVolcengine: '火山引擎 bigasr',
|
||||
asrSiliconflow: '硅基流動 SenseVoice',
|
||||
|
||||
@@ -38,6 +38,11 @@ const LLM_NAME_KEY_BY_ID: Record<string, string> = {
|
||||
deepseek: 'deepseek',
|
||||
siliconflow: 'siliconflow',
|
||||
openai: 'openai',
|
||||
mimo: 'mimo',
|
||||
cometapi: 'cometapi',
|
||||
openrouterFree: 'openrouterFree',
|
||||
alibabaCoding: 'alibabaCoding',
|
||||
codingPlanX: 'codingPlanX',
|
||||
custom: 'custom',
|
||||
};
|
||||
|
||||
|
||||
@@ -1033,11 +1033,66 @@ function Toggle({ on, onToggle }: { on: boolean; onToggle?: (next: boolean) => v
|
||||
}
|
||||
|
||||
const LLM_PRESETS = [
|
||||
{ id: 'ark', nameKey: 'ark', baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', modelPlaceholder: 'deepseek-v3-2' },
|
||||
{ id: 'deepseek', nameKey: 'deepseek', baseUrl: 'https://api.deepseek.com/v1', modelPlaceholder: 'deepseek-v4-flash' },
|
||||
{ id: 'siliconflow', nameKey: 'siliconflow', baseUrl: 'https://api.siliconflow.cn/v1', modelPlaceholder: 'Qwen/Qwen2.5-7B-Instruct' },
|
||||
{ id: 'openai', nameKey: 'openai', baseUrl: 'https://api.openai.com/v1', modelPlaceholder: 'gpt-4o' },
|
||||
{ id: 'custom', nameKey: 'custom', baseUrl: '', modelPlaceholder: '' },
|
||||
{
|
||||
id: 'ark',
|
||||
nameKey: 'ark',
|
||||
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
modelPlaceholder: 'deepseek-v3-2',
|
||||
},
|
||||
{
|
||||
id: 'deepseek',
|
||||
nameKey: 'deepseek',
|
||||
baseUrl: 'https://api.deepseek.com/v1',
|
||||
modelPlaceholder: 'deepseek-v4-flash',
|
||||
},
|
||||
{
|
||||
id: 'siliconflow',
|
||||
nameKey: 'siliconflow',
|
||||
baseUrl: 'https://api.siliconflow.cn/v1',
|
||||
modelPlaceholder: 'Qwen/Qwen2.5-7B-Instruct',
|
||||
},
|
||||
{
|
||||
id: 'openai',
|
||||
nameKey: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
modelPlaceholder: 'gpt-4o',
|
||||
},
|
||||
{
|
||||
id: 'mimo',
|
||||
nameKey: 'mimo',
|
||||
baseUrl: 'https://api.xiaomimimo.com/v1',
|
||||
modelPlaceholder: 'xiaomi/mimo-v2-flash',
|
||||
},
|
||||
{
|
||||
id: 'cometapi',
|
||||
nameKey: 'cometapi',
|
||||
baseUrl: 'https://api.cometapi.com/v1',
|
||||
modelPlaceholder: 'gpt-4o',
|
||||
},
|
||||
{
|
||||
id: 'openrouterFree',
|
||||
nameKey: 'openrouterFree',
|
||||
baseUrl: 'https://openrouter.ai/api/v1',
|
||||
modelPlaceholder: 'qwen/qwen3-coder:free',
|
||||
},
|
||||
{
|
||||
id: 'alibabaCoding',
|
||||
nameKey: 'alibabaCoding',
|
||||
baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/v1',
|
||||
modelPlaceholder: 'qwen3-coder-plus',
|
||||
},
|
||||
{
|
||||
id: 'codingPlanX',
|
||||
nameKey: 'codingPlanX',
|
||||
baseUrl: 'https://api.codingplanx.ai/v1',
|
||||
modelPlaceholder: 'gpt-5-mini',
|
||||
},
|
||||
{
|
||||
id: 'custom',
|
||||
nameKey: 'custom',
|
||||
baseUrl: '',
|
||||
modelPlaceholder: '',
|
||||
},
|
||||
] as const;
|
||||
|
||||
type LlmPresetId = typeof LLM_PRESETS[number]['id'];
|
||||
@@ -1122,6 +1177,14 @@ function ProvidersSection() {
|
||||
if (seq !== llmSwitchSeqRef.current) return;
|
||||
}
|
||||
}
|
||||
if (preset?.modelPlaceholder) {
|
||||
const existing = await readCredential('ark.model_id');
|
||||
if (seq !== llmSwitchSeqRef.current) return;
|
||||
if (!existing) {
|
||||
await setCredential('ark.model_id', preset.modelPlaceholder);
|
||||
if (seq !== llmSwitchSeqRef.current) return;
|
||||
}
|
||||
}
|
||||
setCommittedLlmProvider(id);
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user