Merge pull request #401 from BBossss/beta
Merging Codex OAuth LLM provider integration from external contributor.
This commit is contained in:
15 files changed
+1098
-67
No files matched your search
@@ -29,7 +29,7 @@ serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-native-roots"] }
|
||||
futures-util = "0.3"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "native-tls", "stream"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "rustls-tls", "native-tls", "stream", "system-proxy"] }
|
||||
zip = "2"
|
||||
thiserror = "1"
|
||||
anyhow = "1"
|
||||
|
||||
@@ -508,7 +508,9 @@ pub fn partial_actual_size(partial: &Path) -> u64 {
|
||||
let mut seen: HashSet<usize> = HashSet::new();
|
||||
let mut total: u64 = 0;
|
||||
for line in content.lines() {
|
||||
let Ok(idx) = line.trim().parse::<usize>() else { continue };
|
||||
let Ok(idx) = line.trim().parse::<usize>() else {
|
||||
continue;
|
||||
};
|
||||
if !seen.insert(idx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub struct VolcengineCredentials {
|
||||
|
||||
impl VolcengineCredentials {
|
||||
pub fn default_resource_id() -> &'static str {
|
||||
"volc.seedasr.sauc.duration"
|
||||
"volc.bigasr.sauc.duration"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,7 +719,7 @@ mod tests {
|
||||
fn default_resource_id_is_sauc_duration() {
|
||||
assert_eq!(
|
||||
VolcengineCredentials::default_resource_id(),
|
||||
"volc.seedasr.sauc.duration"
|
||||
"volc.bigasr.sauc.duration"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,7 @@ pub struct WhisperBatchASR {
|
||||
}
|
||||
|
||||
impl WhisperBatchASR {
|
||||
pub fn new(
|
||||
api_key: String,
|
||||
base_url: String,
|
||||
model: String,
|
||||
prompt: Option<String>,
|
||||
) -> Self {
|
||||
pub fn new(api_key: String, base_url: String, model: String, prompt: Option<String>) -> Self {
|
||||
Self {
|
||||
api_key,
|
||||
base_url,
|
||||
@@ -205,7 +200,10 @@ mod tests {
|
||||
#[test]
|
||||
fn build_prompt_single_phrase() {
|
||||
let phrases = vec!["梁山泊".to_string()];
|
||||
assert_eq!(build_prompt_from_phrases(&phrases), Some("梁山泊.".to_string()));
|
||||
assert_eq!(
|
||||
build_prompt_from_phrases(&phrases),
|
||||
Some("梁山泊.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Tauri command surface — every IPC entry the React UI invokes lives here.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
@@ -16,7 +15,11 @@ use crate::asr::local::FoundryLocalRuntime;
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::permissions::{self, PermissionStatus};
|
||||
use crate::persistence::{CredentialAccount, CredentialsSnapshot, CredentialsVault};
|
||||
use crate::polish::{LLMError, OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
|
||||
use crate::polish::{
|
||||
http_client_builder, CodexOAuthConfig, CodexOAuthCredentials, CodexOAuthLLMProvider, LLMError,
|
||||
OpenAICompatibleConfig, OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL,
|
||||
CODEX_OAUTH_PROVIDER_ID,
|
||||
};
|
||||
use crate::recorder::{AudioConsumer, Recorder};
|
||||
use crate::types::{
|
||||
ChineseScriptPreference, ComboBinding, CorrectionRule, CredentialsStatus, DictationSession,
|
||||
@@ -392,6 +395,9 @@ fn asr_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bo
|
||||
}
|
||||
|
||||
fn llm_configured_for_provider(provider: &str, snap: &CredentialsSnapshot) -> bool {
|
||||
if provider == CODEX_OAUTH_PROVIDER_ID {
|
||||
return CodexOAuthCredentials::load_default().is_ok();
|
||||
}
|
||||
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
|
||||
@@ -554,6 +560,16 @@ pub async fn list_provider_models(kind: String) -> Result<ProviderModelsResult,
|
||||
models: vec![crate::asr::bailian::DEFAULT_MODEL.to_string()],
|
||||
});
|
||||
}
|
||||
if kind == "llm" && CredentialsVault::get_active_llm() == CODEX_OAUTH_PROVIDER_ID {
|
||||
return Ok(ProviderModelsResult {
|
||||
models: vec![
|
||||
CODEX_DEFAULT_MODEL.to_string(),
|
||||
"gpt-5.3-codex".to_string(),
|
||||
"gpt-5.4".to_string(),
|
||||
"gpt-5.5".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
let config = read_openai_provider_config(&kind)?;
|
||||
fetch_provider_models(&config)
|
||||
.await
|
||||
@@ -595,6 +611,33 @@ fn read_openai_provider_config(kind: &str) -> Result<ProviderConfig, String> {
|
||||
}
|
||||
|
||||
async fn validate_llm_provider() -> Result<(), String> {
|
||||
if CredentialsVault::get_active_llm() == CODEX_OAUTH_PROVIDER_ID {
|
||||
let model = CredentialsVault::get(CredentialAccount::ArkModelId)
|
||||
.map_err(|e| e.to_string())?
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| CODEX_DEFAULT_MODEL.to_string());
|
||||
let provider = CodexOAuthLLMProvider::new(CodexOAuthConfig::new(model));
|
||||
return provider
|
||||
.polish(
|
||||
"验证连接",
|
||||
PolishMode::Raw,
|
||||
&[],
|
||||
&[],
|
||||
ChineseScriptPreference::Auto,
|
||||
OutputLanguagePreference::Auto,
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| match e {
|
||||
LLMError::InvalidResponse { status, .. } => {
|
||||
format!("providerHttpStatus:{status}")
|
||||
}
|
||||
other => other.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let config = read_openai_provider_config("llm")?;
|
||||
let model = CredentialsVault::get(CredentialAccount::ArkModelId)
|
||||
.map_err(|e| e.to_string())?
|
||||
@@ -711,8 +754,7 @@ async fn validate_asr_transcription(config: &ProviderConfig, model: &str) -> Res
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("file", wav_part)
|
||||
.text("model", model.to_string());
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(20))
|
||||
let client = http_client_builder(&url, 20)
|
||||
.build()
|
||||
.map_err(|_| "providerClientInitFailed".to_string())?;
|
||||
let response = client
|
||||
@@ -812,8 +854,7 @@ async fn fetch_provider_models(config: &ProviderConfig) -> Result<Vec<String>, S
|
||||
let url = models_url(&config.base_url);
|
||||
let is_gemini = is_gemini_base_url(&config.base_url);
|
||||
log::info!("[provider-check] GET {url} (gemini={is_gemini})");
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(15))
|
||||
let client = http_client_builder(&config.base_url, 15)
|
||||
.build()
|
||||
.map_err(|e| format!("HTTP client 初始化失败: {e}"))?;
|
||||
let mut request = client.get(&url);
|
||||
@@ -903,7 +944,10 @@ fn parse_gemini_model_ids(body: &str) -> Result<Vec<String>, String> {
|
||||
let mut ids = models
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
match item.get("supportedGenerationMethods").and_then(|v| v.as_array()) {
|
||||
match item
|
||||
.get("supportedGenerationMethods")
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
Some(methods) => methods
|
||||
.iter()
|
||||
.any(|m| m.as_str() == Some("generateContent")),
|
||||
@@ -911,7 +955,12 @@ fn parse_gemini_model_ids(body: &str) -> Result<Vec<String>, String> {
|
||||
}
|
||||
})
|
||||
.filter_map(|item| item.get("name").and_then(|n| n.as_str()))
|
||||
.map(|name| name.strip_prefix("models/").unwrap_or(name).trim().to_string())
|
||||
.map(|name| {
|
||||
name.strip_prefix("models/")
|
||||
.unwrap_or(name)
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
.filter(|id| !id.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
ids.sort();
|
||||
@@ -2234,10 +2283,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn is_gemini_base_url_matches_official_domain() {
|
||||
assert!(is_gemini_base_url("https://generativelanguage.googleapis.com/v1beta"));
|
||||
assert!(is_gemini_base_url("https://generativelanguage.googleapis.com/v1beta/"));
|
||||
assert!(is_gemini_base_url(
|
||||
"https://generativelanguage.googleapis.com/v1beta"
|
||||
));
|
||||
assert!(is_gemini_base_url(
|
||||
"https://generativelanguage.googleapis.com/v1beta/"
|
||||
));
|
||||
assert!(!is_gemini_base_url("https://api.openai.com/v1"));
|
||||
assert!(!is_gemini_base_url("https://ark.cn-beijing.volces.com/api/v3"));
|
||||
assert!(!is_gemini_base_url(
|
||||
"https://ark.cn-beijing.volces.com/api/v3"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -37,7 +37,10 @@ use crate::persistence::{
|
||||
};
|
||||
|
||||
use crate::llm_gemini::{GeminiConfig, GeminiProvider};
|
||||
use crate::polish::{OpenAICompatibleConfig, OpenAICompatibleLLMProvider};
|
||||
use crate::polish::{
|
||||
ActiveLLMProvider, CodexOAuthConfig, CodexOAuthLLMProvider, OpenAICompatibleConfig,
|
||||
OpenAICompatibleLLMProvider, CODEX_DEFAULT_MODEL, CODEX_OAUTH_PROVIDER_ID,
|
||||
};
|
||||
use crate::qa_hotkey::{QaHotkeyError, QaHotkeyEvent, QaHotkeyMonitor};
|
||||
use crate::recorder::{Recorder, RecorderError};
|
||||
use crate::selection::capture_selection;
|
||||
@@ -2068,18 +2071,7 @@ async fn polish_text(
|
||||
.await?);
|
||||
}
|
||||
|
||||
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
|
||||
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "deepseek-v3-2".to_string());
|
||||
let endpoint = resolve_ark_endpoint(&api_key)?;
|
||||
let base_url = endpoint
|
||||
.trim_end_matches("/chat/completions")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
|
||||
let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
|
||||
let provider = OpenAICompatibleLLMProvider::new(config);
|
||||
let provider = build_active_llm_provider()?;
|
||||
Ok(provider
|
||||
.polish(
|
||||
raw,
|
||||
@@ -2146,18 +2138,7 @@ async fn translate_text(
|
||||
.await?);
|
||||
}
|
||||
|
||||
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
|
||||
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "deepseek-v3-2".to_string());
|
||||
let endpoint = resolve_ark_endpoint(&api_key)?;
|
||||
let base_url = endpoint
|
||||
.trim_end_matches("/chat/completions")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
|
||||
let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
|
||||
let provider = OpenAICompatibleLLMProvider::new(config);
|
||||
let provider = build_active_llm_provider()?;
|
||||
Ok(provider
|
||||
.translate_to(
|
||||
raw,
|
||||
@@ -2736,17 +2717,7 @@ where
|
||||
.await?);
|
||||
}
|
||||
|
||||
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
|
||||
let model = CredentialsVault::get(CredentialAccount::ArkModelId)?
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "deepseek-v3-2".to_string());
|
||||
let endpoint = resolve_ark_endpoint(&api_key)?;
|
||||
let base_url = endpoint
|
||||
.trim_end_matches("/chat/completions")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let config = OpenAICompatibleConfig::new("ark", "Doubao Ark", base_url, api_key, model);
|
||||
let provider = OpenAICompatibleLLMProvider::new(config);
|
||||
let provider = build_active_llm_provider()?;
|
||||
Ok(provider
|
||||
.answer_chat_streaming(
|
||||
messages,
|
||||
@@ -2784,6 +2755,29 @@ fn read_gemini_credentials() -> anyhow::Result<(String, String, String)> {
|
||||
Ok((api_key, model, base_url))
|
||||
}
|
||||
|
||||
fn build_active_llm_provider() -> anyhow::Result<ActiveLLMProvider> {
|
||||
let active = CredentialsVault::get_active_llm();
|
||||
let model =
|
||||
CredentialsVault::get(CredentialAccount::ArkModelId)?.filter(|s| !s.trim().is_empty());
|
||||
if active == CODEX_OAUTH_PROVIDER_ID {
|
||||
let config =
|
||||
CodexOAuthConfig::new(model.unwrap_or_else(|| CODEX_DEFAULT_MODEL.to_string()));
|
||||
return Ok(ActiveLLMProvider::Codex(CodexOAuthLLMProvider::new(config)));
|
||||
}
|
||||
|
||||
let api_key = CredentialsVault::get(CredentialAccount::ArkApiKey)?.unwrap_or_default();
|
||||
let model = model.unwrap_or_else(|| "deepseek-v3-2".to_string());
|
||||
let endpoint = resolve_ark_endpoint(&api_key)?;
|
||||
let base_url = endpoint
|
||||
.trim_end_matches("/chat/completions")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let config = OpenAICompatibleConfig::new(active, "OpenLess LLM", base_url, api_key, model);
|
||||
Ok(ActiveLLMProvider::OpenAI(OpenAICompatibleLLMProvider::new(
|
||||
config,
|
||||
)))
|
||||
}
|
||||
|
||||
fn resolve_ark_endpoint(api_key: &str) -> anyhow::Result<String> {
|
||||
let endpoint = CredentialsVault::get(CredentialAccount::ArkEndpoint)?.filter(|s| !s.is_empty());
|
||||
resolve_ark_endpoint_with_policy(api_key, endpoint)
|
||||
|
||||
@@ -796,8 +796,7 @@ impl HistoryStore {
|
||||
// Prepend so the newest session is at index 0, matching the Swift impl.
|
||||
sessions.insert(0, session);
|
||||
if retention_days > 0 {
|
||||
let cutoff =
|
||||
chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
|
||||
let cutoff = chrono::Utc::now() - chrono::Duration::days(i64::from(retention_days));
|
||||
sessions.retain(|s| {
|
||||
chrono::DateTime::parse_from_rfc3339(&s.created_at)
|
||||
.map(|t| t.with_timezone(&chrono::Utc) >= cutoff)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -355,6 +355,7 @@ export const en: typeof zhCN = {
|
||||
providerLabel: 'Provider',
|
||||
llmProviderDesc: 'Selecting a preset auto-fills the default Base URL.',
|
||||
credentialStorageNotice: 'Credentials are stored in the OS credential vault. Legacy local JSON credentials are migrated into the vault and removed after a successful write.',
|
||||
codexOAuthNotice: 'Codex OAuth uses the local Codex login state (~/.codex/auth.json). OpenLess does not store an API key or Base URL for this provider.',
|
||||
asrProviderDesc: 'Switching providers automatically loads the matching credentials.',
|
||||
asrTitle: 'ASR (transcription)',
|
||||
asrDesc: 'Used to turn speech into text in real time.',
|
||||
@@ -364,6 +365,7 @@ export const en: typeof zhCN = {
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Google Gemini',
|
||||
codexOAuth: 'Codex OAuth',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter (free models)',
|
||||
|
||||
@@ -357,6 +357,7 @@ export const ja: typeof zhCN = {
|
||||
providerLabel: 'サプライヤー',
|
||||
llmProviderDesc: '選択するとデフォルトの Base URL が自動入力されます。',
|
||||
credentialStorageNotice: '認証情報は OS の認証情報ストアに保存されます。旧バージョンのローカル JSON 認証情報はストアへ移行され、書き込み成功後に削除されます。',
|
||||
codexOAuthNotice: 'Codex OAuth はローカルの Codex ログイン状態(~/.codex/auth.json)を使用します。OpenLess は API Key や Base URL を保存しません。',
|
||||
asrProviderDesc: '切り替えると対応する認証情報が自動選択されます。',
|
||||
asrTitle: 'ASR 音声(転写)',
|
||||
asrDesc: '口述をリアルタイムでテキストに転写。',
|
||||
@@ -366,6 +367,7 @@ export const ja: typeof zhCN = {
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Google Gemini',
|
||||
codexOAuth: 'Codex OAuth',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(無料モデル)',
|
||||
|
||||
@@ -357,6 +357,7 @@ export const ko: typeof zhCN = {
|
||||
providerLabel: '공급자',
|
||||
llmProviderDesc: '선택 시 Base URL 기본값이 자동 입력됩니다.',
|
||||
credentialStorageNotice: '자격 증명은 OS 자격 증명 저장소에 저장됩니다. 이전 로컬 JSON 자격 증명은 저장소로 마이그레이션되고, 쓰기 성공 후 삭제됩니다.',
|
||||
codexOAuthNotice: 'Codex OAuth는 로컬 Codex 로그인 상태(~/.codex/auth.json)를 사용합니다. OpenLess는 API Key나 Base URL을 저장하지 않습니다.',
|
||||
asrProviderDesc: '전환 시 해당하는 자격 증명이 자동 선택됩니다.',
|
||||
asrTitle: 'ASR 음성(전사)',
|
||||
asrDesc: '구술을 실시간으로 텍스트로 전사합니다.',
|
||||
@@ -366,6 +367,7 @@ export const ko: typeof zhCN = {
|
||||
siliconflow: 'SiliconFlow',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Google Gemini',
|
||||
codexOAuth: 'Codex OAuth',
|
||||
mimo: 'Xiaomi MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(무료 모델)',
|
||||
|
||||
@@ -353,6 +353,7 @@ export const zhCN = {
|
||||
providerLabel: '供应商',
|
||||
llmProviderDesc: '选择后将自动填入 Base URL 默认值。',
|
||||
credentialStorageNotice: '凭据保存在系统凭据库中。旧版本地 JSON 凭据会迁移到系统凭据库,并在成功写入后删除。',
|
||||
codexOAuthNotice: 'Codex OAuth 使用本机 Codex 登录状态(~/.codex/auth.json),无需在 OpenLess 中保存 API Key 或 Base URL。',
|
||||
asrProviderDesc: '切换后将自动选用对应凭据。',
|
||||
asrTitle: 'ASR 语音(转写)',
|
||||
asrDesc: '用于将口述实时转写为文本。',
|
||||
@@ -362,6 +363,7 @@ export const zhCN = {
|
||||
siliconflow: '硅基流动',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Google Gemini',
|
||||
codexOAuth: 'Codex OAuth',
|
||||
mimo: '小米 MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(免费模型)',
|
||||
|
||||
@@ -355,6 +355,7 @@ export const zhTW: typeof zhCN = {
|
||||
providerLabel: '供應商',
|
||||
llmProviderDesc: '選擇後將自動填入 Base URL 默認值。',
|
||||
credentialStorageNotice: '憑據儲存在系統憑據庫中。舊版本機 JSON 憑據會遷移到系統憑據庫,並在成功寫入後刪除。',
|
||||
codexOAuthNotice: 'Codex OAuth 使用本機 Codex 登入狀態(~/.codex/auth.json),無需在 OpenLess 中保存 API Key 或 Base URL。',
|
||||
asrProviderDesc: '切換後將自動選用對應憑據。',
|
||||
asrTitle: 'ASR 語音(轉寫)',
|
||||
asrDesc: '用於將口述實時轉寫爲文本。',
|
||||
@@ -364,6 +365,7 @@ export const zhTW: typeof zhCN = {
|
||||
siliconflow: '硅基流動',
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Google Gemini',
|
||||
codexOAuth: 'Codex OAuth',
|
||||
mimo: '小米 MiMo',
|
||||
cometapi: 'CometAPI',
|
||||
openrouterFree: 'OpenRouter(免費模型)',
|
||||
|
||||
@@ -39,6 +39,7 @@ const LLM_NAME_KEY_BY_ID: Record<string, string> = {
|
||||
deepseek: 'deepseek',
|
||||
siliconflow: 'siliconflow',
|
||||
openai: 'openai',
|
||||
codex_oauth: 'codexOAuth',
|
||||
mimo: 'mimo',
|
||||
cometapi: 'cometapi',
|
||||
openrouterFree: 'openrouterFree',
|
||||
|
||||
@@ -1177,6 +1177,12 @@ const LLM_PRESETS = [
|
||||
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
||||
modelPlaceholder: 'gemini-2.5-flash',
|
||||
},
|
||||
{
|
||||
id: 'codex_oauth',
|
||||
nameKey: 'codexOAuth',
|
||||
baseUrl: '',
|
||||
modelPlaceholder: 'gpt-5.3-codex-spark',
|
||||
},
|
||||
{
|
||||
id: 'mimo',
|
||||
nameKey: 'mimo',
|
||||
@@ -1217,7 +1223,7 @@ const LLM_PRESETS = [
|
||||
|
||||
type LlmPresetId = typeof LLM_PRESETS[number]['id'];
|
||||
|
||||
const ASR_DEFAULT_RESOURCE_ID = 'volc.seedasr.sauc.duration';
|
||||
const ASR_DEFAULT_RESOURCE_ID = 'volc.bigasr.sauc.duration';
|
||||
|
||||
// `volcengine` / `bailian` 走自建流式客户端;其余走 OpenAI 兼容
|
||||
// `/audio/transcriptions`(`coordinator.rs::is_whisper_compatible_provider`)。
|
||||
@@ -1374,6 +1380,7 @@ function ProvidersSection() {
|
||||
// 否则受控 <select> 立刻切到新厂商,但凭据字段还在显示旧 entry,placeholder
|
||||
// 会先于实际数据切换、视觉上对不上。
|
||||
const preset = LLM_PRESETS.find(p => p.id === committedLlmProvider) ?? LLM_PRESETS[LLM_PRESETS.length - 1];
|
||||
const codexOAuthSelected = committedLlmProvider === 'codex_oauth';
|
||||
const asrPreset = visibleAsrPresets.find(p => p.id === committedAsrProvider);
|
||||
|
||||
return (
|
||||
@@ -1401,9 +1408,17 @@ function ProvidersSection() {
|
||||
))}
|
||||
</select>
|
||||
</SettingRow>
|
||||
<CredentialField key={`${committedLlmProvider}:api_key`} label={t('settings.providers.apiKeyLabel')} account="ark.api_key" mono mask />
|
||||
<CredentialField key={`${committedLlmProvider}:endpoint`} label={t('settings.providers.baseUrlLabel')} account="ark.endpoint"
|
||||
placeholder={preset.baseUrl || 'https://your-endpoint/v1'} />
|
||||
{codexOAuthSelected ? (
|
||||
<div style={{ fontSize: 11.5, color: 'var(--ol-ink-4)', lineHeight: 1.6, margin: '2px 0 10px' }}>
|
||||
{t('settings.providers.codexOAuthNotice')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CredentialField key={`${committedLlmProvider}:api_key`} label={t('settings.providers.apiKeyLabel')} account="ark.api_key" mono mask />
|
||||
<CredentialField key={`${committedLlmProvider}:endpoint`} label={t('settings.providers.baseUrlLabel')} account="ark.endpoint"
|
||||
placeholder={preset.baseUrl || 'https://your-endpoint/v1'} />
|
||||
</>
|
||||
)}
|
||||
<CredentialField key={`${committedLlmProvider}:model:${llmModelRevision}`} label={t('settings.providers.modelLabel')} account="ark.model_id"
|
||||
placeholder={preset.modelPlaceholder || 'model-name'} mono />
|
||||
<ProviderTools key={committedLlmProvider} kind="llm" modelAccount="ark.model_id" onModelSelected={() => setLlmModelRevision(v => v + 1)} />
|
||||
|
||||
Reference in new issue
Block a user