Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src-tauri/src/auto_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ mod tests {
provider: LlmProviderPreset::DeepSeek,
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
insecure: false,
});
}
config
Expand Down
56 changes: 36 additions & 20 deletions src-tauri/src/command/llm_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::command::base::CommandResult;
use crate::config::{LlmChainLink, LlmProviderPreset, PRIMARY_LLM_ENTRY_ID};
use crate::credential::{self, CredentialStatus, EffectiveCredentialSource, ResolvedCredential};
use crate::error::AppError;
use crate::llm::service::{normalize_provider_base_url, provider_requires_key, LlmService};
use crate::llm::service::{http_client_for, normalize_provider_base_url, provider_requires_key, LlmService};
use crate::llm::types::ConnectionReport;
use rig::client::ModelListingClient;
use rig::model::{Model, ModelList, ModelListingError};
Expand Down Expand Up @@ -177,17 +177,18 @@ pub async fn test_llm_entry_connection(
}
}

/// 界面上正在编辑的服务参数。两项齐备才算数:只传一半无从判断该配哪个客户端,
/// 与其猜一个,不如退回已落盘的配置。
/// 界面上正在编辑的服务参数。三项齐备才算数:只传一半无从判断该配哪个客户端,
/// 与其猜一个,不如退回已落盘的配置。insecure 缺省时视为关闭。
fn draft_override(
provider: Option<LlmProviderPreset>,
base_url: Option<String>,
) -> Option<(LlmProviderPreset, String)> {
insecure: Option<bool>,
) -> Option<(LlmProviderPreset, String, bool)> {
let base_url = base_url.map(|url| url.trim().to_string())?;
if base_url.is_empty() {
return None;
}
Some((provider?, base_url))
Some((provider?, base_url, insecure.unwrap_or(false)))
}

/// 列出降级链中某个服务可用的模型。
Expand All @@ -202,18 +203,19 @@ pub async fn list_llm_models_for(
entry_id: String,
provider: Option<LlmProviderPreset>,
base_url: Option<String>,
insecure: Option<bool>,
) -> CommandResult<Vec<String>> {
let (provider, base_url, credential) = match draft_override(provider, base_url) {
Some((provider, base_url)) => match credential::resolve_for_entry(&entry_id) {
Ok(credential) => (provider, base_url, credential),
let (provider, base_url, insecure, credential) = match draft_override(provider, base_url, insecure) {
Some((provider, base_url, insecure)) => match credential::resolve_for_entry(&entry_id) {
Ok(credential) => (provider, base_url, insecure, credential),
Err(error) => return CommandResult::err(error),
},
None => match resolve_chain_entry(app_handle, &entry_id) {
Ok((link, credential)) => (link.provider, link.base_url, credential),
Ok((link, credential)) => (link.provider, link.base_url, link.insecure, credential),
Err(error) => return CommandResult::err(error),
},
};
to_command_result(fetch_model_list_with_credential(provider, &base_url, &credential).await)
to_command_result(fetch_model_list_with_credential(provider, &base_url, &credential, insecure).await)
}

fn validate_llm_base_url(base_url: &str) -> Result<(), AppError> {
Expand All @@ -236,9 +238,10 @@ const MODEL_LIST_TIMEOUT_SECONDS: u64 = 30;
async fn fetch_model_list(
provider: LlmProviderPreset,
base_url: &str,
insecure: bool,
) -> Result<Vec<String>, AppError> {
let credential = credential::resolve_for_entry(PRIMARY_LLM_ENTRY_ID)?;
fetch_model_list_with_credential(provider, base_url, &credential).await
fetch_model_list_with_credential(provider, base_url, &credential, insecure).await
}

/// 用指定服务自己的密钥拉取模型列表。
Expand All @@ -247,6 +250,7 @@ async fn fetch_model_list_with_credential(
provider: LlmProviderPreset,
base_url: &str,
credential: &ResolvedCredential,
insecure: bool,
) -> Result<Vec<String>, AppError> {
validate_llm_base_url(base_url)?;
if provider_requires_key(&provider) && credential.secret().is_none() {
Expand All @@ -260,12 +264,14 @@ async fn fetch_model_list_with_credential(
"noop"
});
let base_url = normalize_provider_base_url(&provider, base_url);
let http_client = http_client_for(insecure)?;

let models = match provider {
LlmProviderPreset::Anthropic => {
let client = rig::providers::anthropic::Client::builder()
.api_key(api_key)
.base_url(&base_url)
.http_client(http_client.clone())
.build()
.map_err(|error| {
AppError::configuration("无法创建大模型客户端").with_detail(error.to_string())
Expand All @@ -278,11 +284,12 @@ async fn fetch_model_list_with_credential(
.map_err(|_| AppError::network("获取模型列表超时"))?
.map_err(map_model_listing_error)?
}
LlmProviderPreset::DeepSeek => fetch_openai_compatible_models(&base_url, api_key).await?,
LlmProviderPreset::DeepSeek => fetch_openai_compatible_models(&base_url, api_key, insecure).await?,
LlmProviderPreset::OpenAi | LlmProviderPreset::OpenAiResponses => {
let client = rig::providers::openai::Client::builder()
.api_key(api_key)
.base_url(&base_url)
.http_client(http_client.clone())
.build()
.map_err(|error| {
AppError::configuration("无法创建大模型客户端").with_detail(error.to_string())
Expand All @@ -295,7 +302,7 @@ async fn fetch_model_list_with_credential(
.map_err(|_| AppError::network("获取模型列表超时"))?
{
Ok(models) => models,
Err(rig_error) => fetch_openai_compatible_models(&base_url, api_key)
Err(rig_error) => fetch_openai_compatible_models(&base_url, api_key, insecure)
.await
.map_err(|fallback_error| {
let rig_error = map_model_listing_error(rig_error);
Expand All @@ -320,6 +327,7 @@ async fn fetch_model_list_with_credential(
let client = rig::providers::ollama::Client::builder()
.api_key(api_key)
.base_url(&base_url)
.http_client(http_client.clone())
.build()
.map_err(|error| {
AppError::configuration("无法创建大模型客户端").with_detail(error.to_string())
Expand All @@ -336,6 +344,7 @@ async fn fetch_model_list_with_credential(
let client = rig::providers::openrouter::Client::builder()
.api_key(api_key)
.base_url(&base_url)
.http_client(http_client.clone())
.build()
.map_err(|error| {
AppError::configuration("无法创建大模型客户端").with_detail(error.to_string())
Expand All @@ -352,6 +361,7 @@ async fn fetch_model_list_with_credential(
let client = rig::providers::xiaomimimo::Client::builder()
.api_key(api_key)
.base_url(&base_url)
.http_client(http_client.clone())
.build()
.map_err(|error| {
AppError::configuration("无法创建大模型客户端").with_detail(error.to_string())
Expand Down Expand Up @@ -383,11 +393,13 @@ fn openai_compatible_models_url(base_url: &str) -> String {
async fn fetch_openai_compatible_models(
base_url: &str,
api_key: &str,
insecure: bool,
) -> Result<ModelList, AppError> {
let url = openai_compatible_models_url(base_url);
let client = http_client_for(insecure)?;
let response = timeout(
Duration::from_secs(MODEL_LIST_TIMEOUT_SECONDS),
reqwest::Client::new()
client
.get(&url)
.bearer_auth(api_key)
.header(reqwest::header::ACCEPT, "application/json")
Expand Down Expand Up @@ -498,8 +510,9 @@ fn map_model_listing_error(error: ModelListingError) -> AppError {
pub async fn list_llm_models(
provider: LlmProviderPreset,
base_url: String,
insecure: Option<bool>,
) -> CommandResult<Vec<String>> {
match fetch_model_list(provider, &base_url).await {
match fetch_model_list(provider, &base_url, insecure.unwrap_or(false)).await {
Ok(models) => CommandResult::ok(models),
Err(error) => CommandResult::err(error),
}
Expand Down Expand Up @@ -534,6 +547,7 @@ mod tests {
provider: LlmProviderPreset::OpenAi,
base_url: "https://llm.example.test/v1".to_string(),
model: model.to_string(),
insecure: false,
}
}

Expand Down Expand Up @@ -592,30 +606,32 @@ mod tests {
let draft = draft_override(
Some(LlmProviderPreset::DeepSeek),
Some(" https://api.deepseek.com ".to_string()),
Some(true),
);

assert_eq!(
draft,
Some((
LlmProviderPreset::DeepSeek,
"https://api.deepseek.com".to_string()
"https://api.deepseek.com".to_string(),
true
))
);
}

#[test]
fn draft_override_falls_back_to_saved_config_when_incomplete() {
// 只传一半、空串、全不传,都退回已落盘的配置,不去猜另一半
assert_eq!(draft_override(Some(LlmProviderPreset::OpenAi), None), None);
assert_eq!(draft_override(Some(LlmProviderPreset::OpenAi), None, None), None);
assert_eq!(
draft_override(None, Some("https://api.openai.com/v1".to_string())),
draft_override(None, Some("https://api.openai.com/v1".to_string()), None),
None
);
assert_eq!(
draft_override(Some(LlmProviderPreset::OpenAi), Some(" ".to_string())),
draft_override(Some(LlmProviderPreset::OpenAi), Some(" ".to_string()), None),
None
);
assert_eq!(draft_override(None, None), None);
assert_eq!(draft_override(None, None, Some(true)), None);
}

#[test]
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@ struct RawLlmConfig {
base_url: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
insecure: Option<bool>,
}

fn parse_llm_config(
Expand Down Expand Up @@ -369,6 +371,7 @@ fn parse_llm_config(
provider,
base_url,
model,
insecure: raw.insecure.unwrap_or(false),
};
normalize_llm_config(&mut config);
Ok(Some(config))
Expand Down Expand Up @@ -840,6 +843,10 @@ pub struct LlmConfig {
pub provider: LlmProviderPreset,
pub base_url: String,
pub model: String,
/// 是否跳过 TLS 证书校验。仅用于自签证书的内网/自建服务,默认关闭;
/// 开启后中间人可窃取传输内容(含 API Key),切勿对公网服务开启。
#[serde(default)]
pub insecure: bool,
}

/// 一个大模型服务是否填写完整、可以真正发起调用。
Expand Down Expand Up @@ -877,6 +884,11 @@ pub struct LlmProviderEntry {

pub model: String,

/// 是否跳过 TLS 证书校验。仅用于自签证书的内网/自建服务,默认关闭;
/// 开启后中间人可窃取传输内容(含 API Key),切勿对公网服务开启。
#[serde(default)]
pub insecure: bool,

/// 是否参与降级链。关闭后保留配置但不再被调用
#[serde(default = "default_true")]
pub enabled: bool,
Expand Down Expand Up @@ -948,6 +960,8 @@ pub struct LlmChainLink {
pub provider: LlmProviderPreset,
pub base_url: String,
pub model: String,
/// 是否跳过 TLS 证书校验(来自主用配置或备用条目的 insecure 字段)
pub insecure: bool,
}

impl LlmChainLink {
Expand Down Expand Up @@ -1018,6 +1032,7 @@ impl AppRuntimeConfig {
provider: primary.provider.clone(),
base_url: primary.base_url.clone(),
model: primary.model.clone(),
insecure: primary.insecure,
});

chain.extend(
Expand All @@ -1031,6 +1046,7 @@ impl AppRuntimeConfig {
provider: entry.provider.clone(),
base_url: entry.base_url.clone(),
model: entry.model.clone(),
insecure: entry.insecure,
}),
);

Expand Down Expand Up @@ -2055,6 +2071,7 @@ mod tests {
provider: LlmProviderPreset::Ollama,
base_url: " http://127.0.0.1:11434/v1/// ".to_string(),
model: " qwen3 ".to_string(),
insecure: false,
}
}

Expand Down Expand Up @@ -2094,6 +2111,7 @@ mod tests {
provider: LlmProviderPreset::OpenAi,
base_url: base_url.to_string(),
model: model.to_string(),
insecure: false,
});

let yaml = serde_yaml::to_string(&config).unwrap();
Expand Down Expand Up @@ -3010,6 +3028,7 @@ job_profiles: []
provider: LlmProviderPreset::OpenAi,
base_url: "https://llm.example.test/v1".to_string(),
model: model.to_string(),
insecure: false,
enabled: true,
}
}
Expand All @@ -3031,6 +3050,7 @@ job_profiles: []
provider: LlmProviderPreset::DeepSeek,
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
insecure: false,
});
let mut disabled = fallback_entry("backup-b", "gpt-4o-mini");
disabled.enabled = false;
Expand Down Expand Up @@ -3060,6 +3080,7 @@ job_profiles: []
provider: LlmProviderPreset::DeepSeek,
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
insecure: false,
});
config.llm_enabled = Some(false);

Expand All @@ -3074,6 +3095,7 @@ job_profiles: []
provider: LlmProviderPreset::OpenAi,
base_url: "https://llm.example.test/v1".to_string(),
model: "primary-model".to_string(),
insecure: false,
});
let mut labeled = fallback_entry("backup-a", "qwen-max");
labeled.label = Some("阿里备用".to_string());
Expand Down Expand Up @@ -3145,6 +3167,7 @@ job_profiles: []
provider: LlmProviderPreset::OpenAi,
base_url: "https://api.openai.com/v1".to_string(),
model: "gpt-4o".to_string(),
insecure: false,
});
let mut blank = fallback_entry("backup-blank", "");
blank.base_url = " ".to_string();
Expand Down
Loading
Loading