feat(proxy): add isFullUrl toggle for full API endpoint mode

- provider.rs/types.ts: add is_full_url field to ProviderMeta
- forwarder.rs: when isFullUrl is set, use base_url directly instead of
  appending endpoint path; also handle query passthrough and strip
  beta=true when transforming to /v1/chat/completions
- EndpointField.tsx: add Link2 icon toggle button for full URL mode
- ClaudeFormFields.tsx: pass through isFullUrl/onFullUrlChange props
- ProviderForm.tsx: manage localIsFullUrl state, persist to meta on save
- useProviderActions.ts: block switching to isFullUrl or openai_chat
  providers when proxy is not running, show warning toast
- App.tsx: pass isProxyRunning to useProviderActions
- i18n: add fullUrlEnabled/fullUrlDisabled/fullUrlHint and
  proxyRequiredForSwitch translations for zh/en/ja
This commit is contained in:
YoVinchen
2026-02-25 22:11:52 +08:00
parent 87b08ce242
commit 6427ab2128
11 changed files with 142 additions and 14 deletions
+3
View File
@@ -240,6 +240,9 @@ pub struct ProviderMeta {
/// - "ANTHROPIC_API_KEY": 少数供应商需要原生 API Key
#[serde(rename = "apiKeyField", skip_serializing_if = "Option::is_none")]
pub api_key_field: Option<String>,
/// 是否将 base_url 视为完整 API 端点(不拼接 endpoint 路径)
#[serde(rename = "isFullUrl", skip_serializing_if = "Option::is_none")]
pub is_full_url: Option<bool>,
}
impl ProviderManager {
+46 -6
View File
@@ -749,15 +749,55 @@ impl RequestForwarder {
// 检查是否需要格式转换
let needs_transform = adapter.needs_transform(provider);
let effective_endpoint =
if needs_transform && adapter.name() == "Claude" && endpoint == "/v1/messages" {
"/v1/chat/completions"
// 检查 isFullUrl 模式:直接使用 base_url 作为完整 API 端点
let is_full_url = provider
.meta
.as_ref()
.and_then(|m| m.is_full_url)
.unwrap_or(false);
let url = if is_full_url {
// 全链接模式:直接使用 base_url,将客户端 query 追加
let query = endpoint.split_once('?').map(|(_, q)| q);
match query {
Some(q) if !q.is_empty() => {
if base_url.contains('?') {
format!("{base_url}&{q}")
} else {
format!("{base_url}?{q}")
}
}
_ => base_url.clone(),
}
} else {
// 正常模式:endpoint 可能带 query string,需拆分路径和参数
let effective_endpoint: String = if needs_transform && adapter.name() == "Claude" {
let (path, query) = match endpoint.split_once('?') {
Some((p, q)) => (p, Some(q)),
None => (endpoint, None),
};
if path == "/v1/messages" || path == "/claude/v1/messages" {
// 转换到 chat/completions 时剥离 beta=trueAnthropic 专有参数)
let filtered = query.map(|q| {
q.split('&')
.filter(|p| !p.starts_with("beta="))
.collect::<Vec<_>>()
.join("&")
});
match filtered.as_deref() {
Some(q) if !q.is_empty() => format!("/v1/chat/completions?{q}"),
_ => "/v1/chat/completions".to_string(),
}
} else {
endpoint.to_string()
}
} else {
endpoint
endpoint.to_string()
};
// 使用适配器构建 URL
let url = adapter.build_url(&base_url, effective_endpoint);
// 使用适配器构建 URL
adapter.build_url(&base_url, &effective_endpoint)
};
// 应用模型映射(独立于格式转换)
let (mapped_body, _original_model, _mapped_model) =