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
34 changes: 28 additions & 6 deletions BitFun-Installer/src/data/modelProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const PROVIDER_DISPLAY_ORDER: string[] = [
'qwen',
'deepseek',
'volcengine',
'siliconflow',
'minimax',
'moonshot',
'anthropic',
Expand All @@ -36,7 +37,7 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
descriptionKey: 'model.providers.anthropic.description',
baseUrl: 'https://api.anthropic.com',
format: 'anthropic',
models: ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101', 'claude-haiku-4-5-20251001'],
models: ['claude-opus-4-6', 'claude-sonnet-4-6'],
helpUrl: 'https://console.anthropic.com/',
},
minimax: {
Expand All @@ -45,7 +46,7 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
descriptionKey: 'model.providers.minimax.description',
baseUrl: 'https://api.minimaxi.com/anthropic',
format: 'anthropic',
models: ['MiniMax-M2.5', 'MiniMax-M2.1', 'MiniMax-M2.1-lightning', 'MiniMax-M2'],
models: ['MiniMax-M2.5', 'MiniMax-M2.1'],
helpUrl: 'https://platform.minimax.io/',
baseUrlOptions: [
{
Expand Down Expand Up @@ -73,7 +74,7 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
id: 'deepseek',
nameKey: 'model.providers.deepseek.name',
descriptionKey: 'model.providers.deepseek.description',
baseUrl: 'https://api.deepseek.com',
baseUrl: 'https://api.deepseek.com/v1',
format: 'openai',
models: ['deepseek-chat', 'deepseek-reasoner'],
helpUrl: 'https://platform.deepseek.com/api_keys',
Expand All @@ -98,7 +99,7 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
noteKey: 'model.providers.zhipu.urlOptions.anthropic',
},
{
url: 'https://open.bigmodel.cn/api/coding/paas',
url: 'https://open.bigmodel.cn/api/coding/paas/v4',
format: 'openai',
noteKey: 'model.providers.zhipu.urlOptions.codingPlan',
},
Expand All @@ -110,7 +111,7 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
descriptionKey: 'model.providers.qwen.description',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
format: 'openai',
models: ['qwen3.5-plus', 'glm-5', 'kimi-k2.5', 'MiniMax-M2.5', 'qwen3-max', 'qwen3-coder-plus', 'qwen3-coder-flash'],
models: ['Qwen3.5-Plus', 'Qwen3.5-Flash'],
helpUrl: 'https://dashscope.console.aliyun.com/apiKey',
baseUrlOptions: [
{
Expand All @@ -136,9 +137,30 @@ export const PROVIDER_TEMPLATES: Record<string, ProviderTemplate> = {
descriptionKey: 'model.providers.volcengine.description',
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
format: 'openai',
models: ['glm-4-7-251222', 'doubao-seed-code-preview-251028'],
models: ['doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-pro-260215'],
helpUrl: 'https://console.volcengine.com/ark/',
},
siliconflow: {
id: 'siliconflow',
nameKey: 'model.providers.siliconflow.name',
descriptionKey: 'model.providers.siliconflow.description',
baseUrl: 'https://api.siliconflow.cn/v1',
format: 'openai',
models: [],
helpUrl: 'https://cloud.siliconflow.cn/account/ak',
baseUrlOptions: [
{
url: 'https://api.siliconflow.cn/v1',
format: 'openai',
noteKey: 'model.providers.siliconflow.urlOptions.default',
},
{
url: 'https://api.siliconflow.cn/v1/messages',
format: 'anthropic',
noteKey: 'model.providers.siliconflow.urlOptions.anthropic',
},
],
},
};

export function getOrderedProviders(): ProviderTemplate[] {
Expand Down
32 changes: 20 additions & 12 deletions BitFun-Installer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,33 @@
"advancedShow": "Show advanced settings",
"advancedHide": "Hide advanced settings",
"providers": {
"openbitfun": {
"name": "OpenBitFun",
"description": "OpenBitFun Model Platform"
},
"gemini": {
"name": "Google Gemini",
"description": "Google Gemini series models"
},
"anthropic": {
"name": "Anthropic Claude",
"description": "Anthropic Claude series models"
},
"minimax": {
"name": "MiniMax",
"description": "MiniMax M2 series large language models",
"description": "MiniMax series models",
"urlOptions": {
"default": "Anthropic Format - Default",
"openai": "OpenAI Compatible Format"
}
},
"moonshot": {
"name": "Moonshot AI",
"description": "Moonshot Kimi K2 series models"
"description": "Moonshot Kimi series models"
},
"deepseek": {
"name": "DeepSeek",
"description": "DeepSeek V3 and R1 reasoning models"
"description": "DeepSeek series models"
},
"zhipu": {
"name": "Zhipu AI",
Expand All @@ -90,7 +98,7 @@
},
"qwen": {
"name": "Qwen",
"description": "Alibaba Cloud Qwen3 series models",
"description": "Alibaba Cloud Bailian Model Platform",
"urlOptions": {
"default": "OpenAI Format - Default",
"codingPlan": "OpenAI Format - Coding Plan",
Expand All @@ -99,15 +107,15 @@
},
"volcengine": {
"name": "Volcano Engine",
"description": "ByteDance Volcano Engine Doubao large language models"
},
"openbitfun": {
"name": "OpenBitFun",
"description": "OpenBitFun Model Service"
"description": "ByteDance Volcano Engine Model Platform"
},
"gemini": {
"name": "Google Gemini",
"description": "Google Gemini 2.5 series multimodal models"
"siliconflow": {
"name": "SiliconFlow",
"description": "SiliconFlow Model Platform",
"urlOptions": {
"default": "OpenAI Format - Default",
"anthropic": "Anthropic Format"
}
}
},
"modelNameSelectPlaceholder": "Select a model...",
Expand Down
32 changes: 20 additions & 12 deletions BitFun-Installer/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,33 @@
"advancedShow": "Show advanced settings",
"advancedHide": "Hide advanced settings",
"providers": {
"openbitfun": {
"name": "OpenBitFun",
"description": "OpenBitFun 大模型平台"
},
"gemini": {
"name": "Google Gemini",
"description": "Google Gemini 系列模型"
},
"anthropic": {
"name": "Anthropic Claude",
"description": "Anthropic Claude 系列模型"
},
"minimax": {
"name": "MiniMax",
"description": "MiniMax M2 系列大语言模型",
"description": "MiniMax 系列模型",
"urlOptions": {
"default": "Anthropic格式-默认",
"openai": "OpenAI兼容格式"
}
},
"moonshot": {
"name": "月之暗面",
"description": "月之暗面 Kimi K2 系列模型"
"description": "月之暗面 Kimi 系列模型"
},
"deepseek": {
"name": "DeepSeek",
"description": "DeepSeek V3 和 R1 推理模型"
"description": "DeepSeek 系列模型"
},
"zhipu": {
"name": "智谱AI",
Expand All @@ -90,7 +98,7 @@
},
"qwen": {
"name": "通义千问",
"description": "阿里云通义千问 Qwen3 系列模型",
"description": "阿里云百炼大模型平台",
"urlOptions": {
"default": "OpenAI格式-默认",
"codingPlan": "OpenAI格式-Coding Plan",
Expand All @@ -99,15 +107,15 @@
},
"volcengine": {
"name": "火山引擎",
"description": "字节跳动火山引擎豆包大模型"
},
"openbitfun": {
"name": "OpenBitFun",
"description": "OpenBitFun 模型服务"
"description": "字节跳动火山引擎大模型平台"
},
"gemini": {
"name": "Google Gemini",
"description": "Google Gemini 2.5 系列多模态模型"
"siliconflow": {
"name": "硅基流动",
"description": "硅基流动大模型平台",
"urlOptions": {
"default": "OpenAI格式-默认",
"anthropic": "Anthropic格式"
}
}
},
"modelNameSelectPlaceholder": "选择模型...",
Expand Down
106 changes: 88 additions & 18 deletions src/crates/core/src/infrastructure/ai/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,30 @@ struct AnthropicModelEntry {
display_name: Option<String>,
}

#[derive(Debug, Deserialize)]
struct GeminiModelsResponse {
#[serde(default)]
models: Vec<GeminiModelEntry>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GeminiModelEntry {
name: String,
#[serde(default)]
display_name: Option<String>,
#[serde(default, deserialize_with = "deserialize_null_as_default")]
supported_generation_methods: Vec<String>,
}

fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: Default + serde::Deserialize<'de>,
{
Option::<T>::deserialize(deserializer).map(|v| v.unwrap_or_default())
}

impl AIClient {
const TEST_IMAGE_EXPECTED_CODE: &'static str = "BYGR";
const TEST_IMAGE_PNG_BASE64: &'static str =
Expand Down Expand Up @@ -266,6 +290,48 @@ impl AIClient {
))
}

fn resolve_gemini_models_url(&self) -> String {
let base = Self::normalize_base_url_for_discovery(&self.config.base_url);
let base = Self::gemini_base_url(&base);
format!("{}/v1beta/models", base)
}

async fn list_gemini_models(&self) -> Result<Vec<RemoteModelInfo>> {
let url = self.resolve_gemini_models_url();
debug!("Gemini models list URL: {}", url);

let response = self
.apply_gemini_headers(self.client.get(&url))
.send()
.await?
.error_for_status()?;

let payload: GeminiModelsResponse = response.json().await?;
Ok(Self::dedupe_remote_models(
payload
.models
.into_iter()
.filter(|m| {
m.supported_generation_methods.is_empty()
|| m.supported_generation_methods
.iter()
.any(|method| method == "generateContent")
})
.map(|model| {
let id = model
.name
.strip_prefix("models/")
.unwrap_or(&model.name)
.to_string();
RemoteModelInfo {
id,
display_name: model.display_name,
}
})
.collect(),
))
}

/// Create an AIClient without proxy (backward compatible)
pub fn new(config: AIConfig) -> Self {
let skip_ssl_verify = config.skip_ssl_verify;
Expand Down Expand Up @@ -565,7 +631,11 @@ impl AIClient {

builder = builder
.header("Content-Type", "application/json")
.header("x-goog-api-key", &self.config.api_key);
.header("x-goog-api-key", &self.config.api_key)
.header(
"Authorization",
format!("Bearer {}", self.config.api_key),
);

if has_custom_headers && is_merge_mode {
builder = self.apply_custom_headers(builder);
Expand Down Expand Up @@ -1107,26 +1177,25 @@ impl AIClient {
return String::new();
}

let mut url = trimmed
.replace(":generateContent", ":streamGenerateContent")
.replace(":streamGenerateContent?alt=sse", ":streamGenerateContent");
let base = Self::gemini_base_url(trimmed);
let encoded_model = urlencoding::encode(model_name.trim());
format!(
"{}/v1beta/models/{}:streamGenerateContent?alt=sse",
base, encoded_model
)
}

if !url.contains(":streamGenerateContent") {
if url.contains("/models/") {
url = format!("{}:streamGenerateContent", url);
} else {
let encoded_model = urlencoding::encode(model_name);
url = format!("{}/models/{}:streamGenerateContent", url, encoded_model);
}
/// Strip /v1beta, /models/... and similar suffixes from a gemini URL,
/// returning only the bare host root (e.g. https://generativelanguage.googleapis.com).
fn gemini_base_url(url: &str) -> &str {
let mut u = url;
if let Some(pos) = u.find("/v1beta") {
u = &u[..pos];
}

if url.contains("alt=sse") {
url
} else if url.contains('?') {
format!("{}&alt=sse", url)
} else {
format!("{}?alt=sse", url)
if let Some(pos) = u.find("/models/") {
u = &u[..pos];
}
u.trim_end_matches('/')
}

fn extract_openai_tool_name(tool: &serde_json::Value) -> String {
Expand Down Expand Up @@ -2008,6 +2077,7 @@ impl AIClient {
match self.get_api_format().to_ascii_lowercase().as_str() {
"openai" | "response" | "responses" => self.list_openai_models().await,
"anthropic" => self.list_anthropic_models().await,
format if Self::is_gemini_api_format(format) => self.list_gemini_models().await,
unsupported => Err(anyhow!(
"Listing models is not supported for API format: {}",
unsupported
Expand Down
Loading
Loading