diff --git a/src-tauri/src/cli/commands/provider.rs b/src-tauri/src/cli/commands/provider.rs index a3932a3c..489a7bc5 100644 --- a/src-tauri/src/cli/commands/provider.rs +++ b/src-tauri/src/cli/commands/provider.rs @@ -304,7 +304,7 @@ fn prompt_api_format( .map(|api_format| value_label(api_format).to_string()) .collect::>(); - let selected = Select::new(texts::tui_label_claude_api_format(), labels.clone()) + let selected = Select::new(texts::tui_label_api_format(), labels.clone()) .with_starting_cursor(default_index) .prompt() .map_err(|e| AppError::Message(texts::input_failed_error(&e.to_string())))?; diff --git a/src-tauri/src/cli/commands/provider_input.rs b/src-tauri/src/cli/commands/provider_input.rs index bc4c19ae..ba3fc7b1 100644 --- a/src-tauri/src/cli/commands/provider_input.rs +++ b/src-tauri/src/cli/commands/provider_input.rs @@ -4092,7 +4092,7 @@ pub fn display_provider_summary(provider: &Provider, app_type: &AppType) { let api_format = crate::proxy::providers::get_claude_api_format(provider); println!( " {}: {}", - texts::tui_label_claude_api_format(), + texts::tui_label_api_format(), texts::tui_claude_api_format_value(api_format) ); } @@ -4148,7 +4148,7 @@ pub fn display_provider_summary(provider: &Provider, app_type: &AppType) { }; println!( " {}: {}", - texts::tui_label_claude_api_format(), + texts::tui_label_api_format(), texts::tui_codex_api_format_value(api_format) ); } diff --git a/src-tauri/src/cli/i18n.rs b/src-tauri/src/cli/i18n.rs index 7c81804f..2a229dcf 100644 --- a/src-tauri/src/cli/i18n.rs +++ b/src-tauri/src/cli/i18n.rs @@ -1578,7 +1578,7 @@ pub mod texts { } } - pub fn tui_label_claude_api_format() -> &'static str { + pub fn tui_label_api_format() -> &'static str { if is_chinese() { "API 格式" } else { @@ -1638,7 +1638,7 @@ pub mod texts { } } - pub fn tui_claude_api_format_requires_proxy_title() -> &'static str { + pub fn tui_api_format_requires_proxy_title() -> &'static str { if is_chinese() { "需开启代理" } else { @@ -1793,7 +1793,7 @@ pub mod texts { } } - pub fn tui_claude_api_format_popup_title() -> &'static str { + pub fn tui_api_format_popup_title() -> &'static str { if is_chinese() { "API 格式" } else { diff --git a/src-tauri/src/cli/i18n/texts/core.rs b/src-tauri/src/cli/i18n/texts/core.rs index c588cd83..c1eb32bb 100644 --- a/src-tauri/src/cli/i18n/texts/core.rs +++ b/src-tauri/src/cli/i18n/texts/core.rs @@ -1468,7 +1468,7 @@ pub fn tui_codex_api_format_value(api_format: &str) -> &'static str { } } -pub fn tui_claude_api_format_requires_proxy_title() -> &'static str { +pub fn tui_api_format_requires_proxy_title() -> &'static str { if is_chinese() { "需开启代理" } else { diff --git a/src-tauri/src/cli/tui/app/form_handlers/provider.rs b/src-tauri/src/cli/tui/app/form_handlers/provider.rs index 52615789..3cc52459 100644 --- a/src-tauri/src/cli/tui/app/form_handlers/provider.rs +++ b/src-tauri/src/cli/tui/app/form_handlers/provider.rs @@ -229,14 +229,12 @@ impl App { data: &UiData, ) -> Action { match selected { - ProviderAddField::ClaudeApiFormat => { + ProviderAddField::ApiFormat => { let Some(FormState::ProviderAdd(provider)) = self.form.as_ref() else { return Action::None; }; - self.overlay = Overlay::ClaudeApiFormatPicker { - selected: provider - .claude_api_format - .picker_index_for_app(&provider.app_type), + self.overlay = Overlay::ApiFormatPicker { + selected: provider.api_format.picker_index_for_app(&provider.app_type), }; Action::None } @@ -481,7 +479,7 @@ impl App { .unwrap_or(false) { self.overlay = Overlay::Confirm(ConfirmOverlay { - title: texts::tui_claude_api_format_requires_proxy_title().to_string(), + title: texts::tui_api_format_requires_proxy_title().to_string(), message: texts::tui_codex_api_format_requires_proxy_message("openai_chat"), action: ConfirmAction::ProviderApiFormatProxyNotice, }); diff --git a/src-tauri/src/cli/tui/app/overlay_handlers/pickers.rs b/src-tauri/src/cli/tui/app/overlay_handlers/pickers.rs index 9b0565a4..6eaa853c 100644 --- a/src-tauri/src/cli/tui/app/overlay_handlers/pickers.rs +++ b/src-tauri/src/cli/tui/app/overlay_handlers/pickers.rs @@ -9,7 +9,7 @@ impl App { if let Some(action) = self.handle_sync_method_picker_key(key, data) { return Some(action); } - if let Some(action) = self.handle_claude_api_format_picker_key(key, data) { + if let Some(action) = self.handle_api_format_picker_key(key, data) { return Some(action); } if let Some(action) = self.handle_usage_query_template_picker_key(key) { @@ -194,11 +194,7 @@ impl App { }) } - fn handle_claude_api_format_picker_key( - &mut self, - key: KeyEvent, - data: &UiData, - ) -> Option { + fn handle_api_format_picker_key(&mut self, key: KeyEvent, data: &UiData) -> Option { let app_type = self .form .as_ref() @@ -207,7 +203,7 @@ impl App { _ => None, }) .unwrap_or_else(|| self.app_type.clone()); - let Overlay::ClaudeApiFormatPicker { selected } = &mut self.overlay else { + let Overlay::ApiFormatPicker { selected } = &mut self.overlay else { return None; }; @@ -222,14 +218,14 @@ impl App { } KeyCode::Down => { *selected = (*selected + 1).min( - crate::cli::tui::form::ClaudeApiFormat::choices_for_app(&app_type) + crate::cli::tui::form::ApiFormat::choices_for_app(&app_type) .len() .saturating_sub(1), ); Action::None } KeyCode::Enter => { - let next_format = crate::cli::tui::form::ClaudeApiFormat::from_picker_index_for_app( + let picker_format = crate::cli::tui::form::ApiFormat::from_picker_index_for_app( *selected, &app_type, ); let Some(FormState::ProviderAdd(provider)) = self.form.as_mut() else { @@ -237,24 +233,26 @@ impl App { return Some(Action::None); }; - let changed = provider.claude_api_format != next_format; - provider.claude_api_format = next_format; + let changed = provider.api_format != picker_format; + provider.api_format = picker_format; self.overlay = Overlay::None; let proxy_ready = data .proxy .routes_current_app_through_proxy(&provider.app_type) .unwrap_or(false); - if changed && next_format.requires_proxy_for_app(&provider.app_type) && !proxy_ready + if changed + && picker_format.requires_proxy_for_app(&provider.app_type) + && !proxy_ready { let message = if matches!(provider.app_type, crate::app_config::AppType::Codex) { - texts::tui_codex_api_format_requires_proxy_message(next_format.as_str()) + texts::tui_codex_api_format_requires_proxy_message(picker_format.as_str()) } else { - texts::tui_claude_api_format_requires_proxy_message(next_format.as_str()) + texts::tui_claude_api_format_requires_proxy_message(picker_format.as_str()) }; self.overlay = Overlay::Confirm(ConfirmOverlay { - title: texts::tui_claude_api_format_requires_proxy_title().to_string(), + title: texts::tui_api_format_requires_proxy_title().to_string(), message, action: ConfirmAction::ProviderApiFormatProxyNotice, }); diff --git a/src-tauri/src/cli/tui/app/tests.rs b/src-tauri/src/cli/tui/app/tests.rs index ca8b701c..23013b50 100644 --- a/src-tauri/src/cli/tui/app/tests.rs +++ b/src-tauri/src/cli/tui/app/tests.rs @@ -12073,8 +12073,8 @@ mod tests { form.field_idx = form .fields() .iter() - .position(|field| *field == ProviderAddField::ClaudeApiFormat) - .expect("ClaudeApiFormat field should exist"); + .position(|field| *field == ProviderAddField::ApiFormat) + .expect("ApiFormat field should exist"); } else { panic!("expected ProviderAdd form"); } @@ -12083,33 +12083,33 @@ mod tests { assert!(matches!(action, Action::None)); assert!(matches!( app.overlay, - Overlay::ClaudeApiFormatPicker { selected: 0 } + Overlay::ApiFormatPicker { selected: 0 } )); let format = match app.form.as_ref() { - Some(super::super::form::FormState::ProviderAdd(form)) => form.claude_api_format, + Some(super::super::form::FormState::ProviderAdd(form)) => form.api_format, other => panic!("expected ProviderAdd form, got: {other:?}"), }; - assert_eq!(format, super::super::form::ClaudeApiFormat::Anthropic); + assert_eq!(format, super::super::form::ApiFormat::Anthropic); } #[test] fn provider_claude_api_format_overlay_jk_navigates_options() { let mut app = App::new(Some(AppType::Claude)); - app.overlay = Overlay::ClaudeApiFormatPicker { selected: 0 }; + app.overlay = Overlay::ApiFormatPicker { selected: 0 }; let action = app.on_key(key(KeyCode::Char('j')), &data()); assert!(matches!(action, Action::None)); assert!(matches!( app.overlay, - Overlay::ClaudeApiFormatPicker { selected: 1 } + Overlay::ApiFormatPicker { selected: 1 } )); let action = app.on_key(key(KeyCode::Char('k')), &data()); assert!(matches!(action, Action::None)); assert!(matches!( app.overlay, - Overlay::ClaudeApiFormatPicker { selected: 0 } + Overlay::ApiFormatPicker { selected: 0 } )); } @@ -12184,10 +12184,10 @@ mod tests { }) )); let format = match app.form.as_ref() { - Some(super::super::form::FormState::ProviderAdd(form)) => form.claude_api_format, + Some(super::super::form::FormState::ProviderAdd(form)) => form.api_format, other => panic!("expected ProviderAdd form, got: {other:?}"), }; - assert_eq!(format, super::super::form::ClaudeApiFormat::OpenAiChat); + assert_eq!(format, super::super::form::ApiFormat::OpenAiChat); } #[test] @@ -12222,10 +12222,10 @@ mod tests { assert!(matches!(action, Action::None)); assert!(matches!(app.overlay, Overlay::None)); let format = match app.form.as_ref() { - Some(super::super::form::FormState::ProviderAdd(form)) => form.claude_api_format, + Some(super::super::form::FormState::ProviderAdd(form)) => form.api_format, other => panic!("expected ProviderAdd form, got: {other:?}"), }; - assert_eq!(format, super::super::form::ClaudeApiFormat::OpenAiChat); + assert_eq!(format, super::super::form::ApiFormat::OpenAiChat); } #[test] @@ -12540,8 +12540,8 @@ mod tests { form.field_idx = form .fields() .iter() - .position(|field| *field == ProviderAddField::ClaudeApiFormat) - .expect("ClaudeApiFormat field should exist"); + .position(|field| *field == ProviderAddField::ApiFormat) + .expect("ApiFormat field should exist"); } else { panic!("expected ProviderAdd form"); } @@ -12560,10 +12560,10 @@ mod tests { )); let format = match app.form.as_ref() { - Some(super::super::form::FormState::ProviderAdd(form)) => form.claude_api_format, + Some(super::super::form::FormState::ProviderAdd(form)) => form.api_format, other => panic!("expected ProviderAdd form, got: {other:?}"), }; - assert_eq!(format, super::super::form::ClaudeApiFormat::OpenAiChat); + assert_eq!(format, super::super::form::ApiFormat::OpenAiChat); } #[test] @@ -12582,8 +12582,8 @@ mod tests { form.field_idx = form .fields() .iter() - .position(|field| *field == ProviderAddField::ClaudeApiFormat) - .expect("ClaudeApiFormat field should exist"); + .position(|field| *field == ProviderAddField::ApiFormat) + .expect("ApiFormat field should exist"); } else { panic!("expected ProviderAdd form"); } @@ -12615,8 +12615,8 @@ mod tests { form.field_idx = form .fields() .iter() - .position(|field| *field == ProviderAddField::ClaudeApiFormat) - .expect("ClaudeApiFormat field should exist"); + .position(|field| *field == ProviderAddField::ApiFormat) + .expect("ApiFormat field should exist"); } else { panic!("expected ProviderAdd form"); } @@ -12628,10 +12628,10 @@ mod tests { assert!(matches!(app.overlay, Overlay::None)); let format = match app.form.as_ref() { - Some(super::super::form::FormState::ProviderAdd(form)) => form.claude_api_format, + Some(super::super::form::FormState::ProviderAdd(form)) => form.api_format, other => panic!("expected ProviderAdd form, got: {other:?}"), }; - assert_eq!(format, super::super::form::ClaudeApiFormat::OpenAiChat); + assert_eq!(format, super::super::form::ApiFormat::OpenAiChat); } fn failover_provider_row( diff --git a/src-tauri/src/cli/tui/app/types.rs b/src-tauri/src/cli/tui/app/types.rs index e185c4b3..b4a26891 100644 --- a/src-tauri/src/cli/tui/app/types.rs +++ b/src-tauri/src/cli/tui/app/types.rs @@ -616,7 +616,7 @@ pub enum Overlay { selected: usize, editing: bool, }, - ClaudeApiFormatPicker { + ApiFormatPicker { selected: usize, }, UsageQueryTemplatePicker { @@ -737,7 +737,7 @@ impl Overlay { | Overlay::CommonSnippetPicker { .. } | Overlay::ProviderTestMenu { .. } | Overlay::FailoverQueueManager { .. } - | Overlay::ClaudeApiFormatPicker { .. } + | Overlay::ApiFormatPicker { .. } | Overlay::UsageQueryTemplatePicker { .. } | Overlay::ManagedAccountPicker { .. } | Overlay::ManagedAccountActionPicker { .. } @@ -776,7 +776,7 @@ impl Overlay { | Overlay::CommonSnippetPicker { .. } | Overlay::ProviderTestMenu { .. } | Overlay::FailoverQueueManager { .. } - | Overlay::ClaudeApiFormatPicker { .. } + | Overlay::ApiFormatPicker { .. } | Overlay::UsageQueryTemplatePicker { .. } | Overlay::ManagedAccountPicker { .. } | Overlay::ManagedAccountActionPicker { .. } diff --git a/src-tauri/src/cli/tui/form.rs b/src-tauri/src/cli/tui/form.rs index 6bdd2341..3d9cd2c6 100644 --- a/src-tauri/src/cli/tui/form.rs +++ b/src-tauri/src/cli/tui/form.rs @@ -64,40 +64,37 @@ impl CodexWireApi { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ClaudeApiFormat { +pub enum ApiFormat { Anthropic, OpenAiChat, OpenAiResponses, GeminiNative, } -impl ClaudeApiFormat { +impl ApiFormat { pub const ALL: [Self; 4] = [ - ClaudeApiFormat::Anthropic, - ClaudeApiFormat::OpenAiChat, - ClaudeApiFormat::OpenAiResponses, - ClaudeApiFormat::GeminiNative, - ]; - pub const CODEX: [Self; 2] = [ - ClaudeApiFormat::OpenAiResponses, - ClaudeApiFormat::OpenAiChat, + ApiFormat::Anthropic, + ApiFormat::OpenAiChat, + ApiFormat::OpenAiResponses, + ApiFormat::GeminiNative, ]; + pub const CODEX: [Self; 2] = [ApiFormat::OpenAiResponses, ApiFormat::OpenAiChat]; pub fn as_str(self) -> &'static str { match self { - ClaudeApiFormat::Anthropic => "anthropic", - ClaudeApiFormat::OpenAiChat => "openai_chat", - ClaudeApiFormat::OpenAiResponses => "openai_responses", - ClaudeApiFormat::GeminiNative => "gemini_native", + ApiFormat::Anthropic => "anthropic", + ApiFormat::OpenAiChat => "openai_chat", + ApiFormat::OpenAiResponses => "openai_responses", + ApiFormat::GeminiNative => "gemini_native", } } pub fn from_raw(value: &str) -> Self { match value { - "openai_chat" => ClaudeApiFormat::OpenAiChat, - "openai_responses" => ClaudeApiFormat::OpenAiResponses, - "gemini_native" => ClaudeApiFormat::GeminiNative, - _ => ClaudeApiFormat::Anthropic, + "openai_chat" => ApiFormat::OpenAiChat, + "openai_responses" => ApiFormat::OpenAiResponses, + "gemini_native" => ApiFormat::GeminiNative, + _ => ApiFormat::Anthropic, } } @@ -121,22 +118,22 @@ impl ClaudeApiFormat { .copied() .unwrap_or_else(|| { if matches!(app_type, AppType::Codex) { - ClaudeApiFormat::OpenAiResponses + ApiFormat::OpenAiResponses } else { - ClaudeApiFormat::Anthropic + ApiFormat::Anthropic } }) } pub fn requires_proxy_for_app(self, app_type: &AppType) -> bool { match app_type { - AppType::Codex => matches!(self, ClaudeApiFormat::OpenAiChat), + AppType::Codex => matches!(self, ApiFormat::OpenAiChat), _ => self.requires_proxy(), } } pub fn requires_proxy(self) -> bool { - !matches!(self, ClaudeApiFormat::Anthropic) + !matches!(self, ApiFormat::Anthropic) } } @@ -183,7 +180,7 @@ pub enum ProviderAddField { WebsiteUrl, Notes, ClaudeBaseUrl, - ClaudeApiFormat, + ApiFormat, ClaudeApiKey, ClaudeModelConfig, ClaudeHideAttribution, @@ -441,7 +438,7 @@ pub struct ProviderAddFormState { pub claude_api_key: TextInput, pub claude_api_key_field: ClaudeApiKeyField, pub claude_base_url: TextInput, - pub claude_api_format: ClaudeApiFormat, + pub api_format: ApiFormat, pub claude_model: TextInput, pub claude_reasoning_model: TextInput, pub claude_haiku_model: TextInput, diff --git a/src-tauri/src/cli/tui/form/provider_json.rs b/src-tauri/src/cli/tui/form/provider_json.rs index ada3f032..2c89bac0 100644 --- a/src-tauri/src/cli/tui/form/provider_json.rs +++ b/src-tauri/src/cli/tui/form/provider_json.rs @@ -8,7 +8,7 @@ use super::codex_config::{ build_codex_provider_config_toml, clean_codex_provider_key, update_codex_config_snippet, }; use super::{ - parse_codex_model_catalog_context_window, ClaudeApiFormat, GeminiAuthType, + parse_codex_model_catalog_context_window, ApiFormat, CodexWireApi, GeminiAuthType, ProviderAddFormState, UsageQueryTemplate, OPENCLAW_DEFAULT_API_PROTOCOL, OPENCLAW_DEFAULT_USER_AGENT, }; @@ -153,7 +153,7 @@ impl ProviderAddFormState { &provider_key, base_url, model, - self.codex_wire_api, + CodexWireApi::Responses, ) } else { existing_config.to_string() @@ -162,7 +162,7 @@ impl ProviderAddFormState { &base_config, base_url, model, - self.codex_wire_api, + CodexWireApi::Responses, self.codex_requires_openai_auth, self.codex_env_key.value.trim(), ); @@ -505,10 +505,8 @@ impl ProviderAddFormState { fn update_provider_meta(&self, provider_obj: &mut serde_json::Map) { let should_write_common_config_meta = self.should_write_common_config_meta(); let should_write_claude_api_format = matches!( - self.claude_api_format, - ClaudeApiFormat::OpenAiChat - | ClaudeApiFormat::OpenAiResponses - | ClaudeApiFormat::GeminiNative + self.api_format, + ApiFormat::OpenAiChat | ApiFormat::OpenAiResponses | ApiFormat::GeminiNative ) && matches!(self.app_type, AppType::Claude) && !self.is_claude_official_provider(); let should_write_codex_api_format = @@ -552,23 +550,23 @@ impl ProviderAddFormState { } if matches!(self.app_type, AppType::Claude) { - match self.claude_api_format { + match self.api_format { _ if self.is_claude_official_provider() && !is_codex_oauth => { meta_obj.remove("apiFormat"); } - ClaudeApiFormat::Anthropic if is_codex_oauth => { + ApiFormat::Anthropic if is_codex_oauth => { meta_obj.insert("apiFormat".to_string(), json!("openai_responses")); } - ClaudeApiFormat::Anthropic => { + ApiFormat::Anthropic => { meta_obj.remove("apiFormat"); } - ClaudeApiFormat::OpenAiChat => { + ApiFormat::OpenAiChat => { meta_obj.insert("apiFormat".to_string(), json!("openai_chat")); } - ClaudeApiFormat::OpenAiResponses => { + ApiFormat::OpenAiResponses => { meta_obj.insert("apiFormat".to_string(), json!("openai_responses")); } - ClaudeApiFormat::GeminiNative => { + ApiFormat::GeminiNative => { meta_obj.insert("apiFormat".to_string(), json!("gemini_native")); } } @@ -588,8 +586,8 @@ impl ProviderAddFormState { meta_obj.remove("apiFormat"); meta_obj.remove("codexChatReasoning"); } else { - let api_format = match self.claude_api_format { - ClaudeApiFormat::OpenAiChat => "openai_chat", + let api_format = match self.api_format { + ApiFormat::OpenAiChat => "openai_chat", _ => "openai_responses", }; meta_obj.insert("apiFormat".to_string(), json!(api_format)); @@ -647,9 +645,9 @@ impl ProviderAddFormState { self.update_usage_script_meta(meta_obj); - if meta_obj.is_empty() { - provider_obj.remove("meta"); - } + // 即使 meta 为空对象也保留 meta 键,这样反序列化后得到 + // Some(ProviderMeta::default()) 而非 None,确保 ProviderService::update() + // 的 merge 逻辑将表单输出视为权威来源,而不是保留旧的 meta。 } fn update_usage_script_meta(&self, meta_obj: &mut serde_json::Map) { diff --git a/src-tauri/src/cli/tui/form/provider_state.rs b/src-tauri/src/cli/tui/form/provider_state.rs index 22d627a2..249c2e0f 100644 --- a/src-tauri/src/cli/tui/form/provider_state.rs +++ b/src-tauri/src/cli/tui/form/provider_state.rs @@ -9,7 +9,7 @@ use super::provider_json::{ }; use super::provider_state_loading::populate_form_from_provider; use super::{ - ClaudeApiFormat, CodexLocalRoutingField, CodexModelCatalogField, CodexModelCatalogRow, + ApiFormat, CodexLocalRoutingField, CodexModelCatalogField, CodexModelCatalogRow, CodexPreviewSection, CodexWireApi, FormFocus, FormMode, GeminiAuthType, HermesModelField, ProviderAddField, ProviderAddFormState, ProviderFormPage, TextInput, UsageQueryField, UsageQueryTemplate, HERMES_API_MODES, HERMES_DEFAULT_API_MODE, OPENCLAW_DEFAULT_API_PROTOCOL, @@ -142,10 +142,10 @@ impl ProviderAddFormState { claude_api_key: TextInput::new(""), claude_api_key_field: ClaudeApiKeyField::AuthToken, claude_base_url: TextInput::new(""), - claude_api_format: if is_codex { - ClaudeApiFormat::OpenAiResponses + api_format: if is_codex { + ApiFormat::OpenAiResponses } else { - ClaudeApiFormat::Anthropic + ApiFormat::Anthropic }, claude_model: TextInput::new(""), claude_reasoning_model: TextInput::new(""), @@ -361,7 +361,7 @@ impl ProviderAddFormState { fields.push(ProviderAddField::ClaudeModelConfig); } else if !self.is_claude_official_provider() { fields.push(ProviderAddField::ClaudeBaseUrl); - fields.push(ProviderAddField::ClaudeApiFormat); + fields.push(ProviderAddField::ApiFormat); fields.push(ProviderAddField::ClaudeApiKey); fields.push(ProviderAddField::ClaudeModelConfig); } @@ -371,6 +371,7 @@ impl ProviderAddFormState { if !self.is_codex_official_provider() { fields.push(ProviderAddField::CodexBaseUrl); fields.push(ProviderAddField::CodexModel); + fields.push(ProviderAddField::ApiFormat); fields.push(ProviderAddField::CodexLocalRouting); fields.push(ProviderAddField::CodexApiKey); } @@ -514,7 +515,7 @@ impl ProviderAddFormState { | ProviderAddField::CodexLocalRouting | ProviderAddField::CodexWireApi | ProviderAddField::CodexRequiresOpenaiAuth - | ProviderAddField::ClaudeApiFormat + | ProviderAddField::ApiFormat | ProviderAddField::ClaudeModelConfig | ProviderAddField::ClaudeHideAttribution | ProviderAddField::GeminiAuthType @@ -566,7 +567,7 @@ impl ProviderAddFormState { | ProviderAddField::CodexLocalRouting | ProviderAddField::CodexWireApi | ProviderAddField::CodexRequiresOpenaiAuth - | ProviderAddField::ClaudeApiFormat + | ProviderAddField::ApiFormat | ProviderAddField::ClaudeModelConfig | ProviderAddField::ClaudeHideAttribution | ProviderAddField::GeminiAuthType @@ -812,11 +813,12 @@ impl ProviderAddFormState { } pub fn toggle_codex_local_routing_enabled(&mut self) { - self.claude_api_format = if self.codex_local_routing_enabled() { - ClaudeApiFormat::OpenAiResponses + let was_enabled = self.codex_local_routing_enabled(); + if was_enabled { + self.api_format = ApiFormat::OpenAiResponses; } else { - ClaudeApiFormat::OpenAiChat - }; + self.api_format = ApiFormat::OpenAiChat; + } let len = self.codex_local_routing_fields().len(); self.codex_local_routing_field_idx = self .codex_local_routing_field_idx @@ -1404,7 +1406,7 @@ impl ProviderAddFormState { pub fn codex_local_routing_enabled(&self) -> bool { matches!(self.app_type, AppType::Codex) && !self.is_codex_official_provider() - && matches!(self.claude_api_format, ClaudeApiFormat::OpenAiChat) + && matches!(self.api_format, ApiFormat::OpenAiChat) } pub fn apply_provider_json_to_fields(&mut self, provider: &Provider) { diff --git a/src-tauri/src/cli/tui/form/provider_state_loading.rs b/src-tauri/src/cli/tui/form/provider_state_loading.rs index fb687c4a..93288f62 100644 --- a/src-tauri/src/cli/tui/form/provider_state_loading.rs +++ b/src-tauri/src/cli/tui/form/provider_state_loading.rs @@ -2,8 +2,8 @@ use super::codex_config::parse_codex_config_snippet; use super::provider_state::codex_model_catalog_row_from_value; use super::{ claude_hide_attribution_enabled, detect_balance_provider_for_usage_query, - detect_coding_plan_provider_for_usage_query, ClaudeApiFormat, CodexWireApi, - ProviderAddFormState, UsageQueryTemplate, OPENCLAW_DEFAULT_API_PROTOCOL, + detect_coding_plan_provider_for_usage_query, ApiFormat, CodexWireApi, ProviderAddFormState, + UsageQueryTemplate, OPENCLAW_DEFAULT_API_PROTOCOL, }; use crate::app_config::AppType; use crate::provider::Provider; @@ -82,7 +82,7 @@ fn populate_usage_query_form(form: &mut ProviderAddFormState, provider: &Provide } fn populate_claude_form(form: &mut ProviderAddFormState, provider: &Provider) { - form.claude_api_format = parse_claude_api_format(provider); + form.api_format = parse_claude_api_format(provider); form.claude_api_key_field = crate::provider::ClaudeApiKeyField::from_meta_and_settings( provider.meta.as_ref(), &provider.settings_config, @@ -94,7 +94,7 @@ fn populate_claude_form(form: &mut ProviderAddFormState, provider: &Provider) { .and_then(|meta| meta.provider_type.as_deref()) == Some("codex_oauth") { - form.claude_api_format = ClaudeApiFormat::OpenAiResponses; + form.api_format = ApiFormat::OpenAiResponses; form.codex_oauth_account_id = provider .meta .as_ref() @@ -201,7 +201,7 @@ fn populate_codex_form(form: &mut ProviderAddFormState, provider: &Provider) { form.codex_api_key.set(key); } } - form.claude_api_format = parse_codex_api_format(provider, parsed_wire_api); + form.api_format = parse_codex_api_format(provider, parsed_wire_api); form.codex_wire_api = CodexWireApi::Responses; form.codex_chat_reasoning = provider .meta @@ -399,13 +399,13 @@ fn populate_openclaw_form(form: &mut ProviderAddFormState, provider: &Provider) } } -fn parse_claude_api_format(provider: &Provider) -> ClaudeApiFormat { +fn parse_claude_api_format(provider: &Provider) -> ApiFormat { if let Some(api_format) = provider .meta .as_ref() .and_then(|meta| meta.api_format.as_deref()) { - return ClaudeApiFormat::from_raw(api_format); + return ApiFormat::from_raw(api_format); } if let Some(api_format) = provider @@ -413,7 +413,7 @@ fn parse_claude_api_format(provider: &Provider) -> ClaudeApiFormat { .get("api_format") .and_then(|value| value.as_str()) { - return ClaudeApiFormat::from_raw(api_format); + return ApiFormat::from_raw(api_format); } let compat_enabled = match provider.settings_config.get("openrouter_compat_mode") { @@ -427,13 +427,13 @@ fn parse_claude_api_format(provider: &Provider) -> ClaudeApiFormat { }; if compat_enabled { - ClaudeApiFormat::OpenAiChat + ApiFormat::OpenAiChat } else { - ClaudeApiFormat::Anthropic + ApiFormat::Anthropic } } -fn parse_codex_api_format(provider: &Provider, wire_api: Option) -> ClaudeApiFormat { +fn parse_codex_api_format(provider: &Provider, wire_api: Option) -> ApiFormat { if let Some(api_format) = provider .meta .as_ref() @@ -452,14 +452,14 @@ fn parse_codex_api_format(provider: &Provider, wire_api: Option) - }) { return match api_format { - "openai_chat" => ClaudeApiFormat::OpenAiChat, - _ => ClaudeApiFormat::OpenAiResponses, + "openai_chat" => ApiFormat::OpenAiChat, + _ => ApiFormat::OpenAiResponses, }; } match wire_api { - Some(CodexWireApi::Chat) => ClaudeApiFormat::OpenAiChat, - _ => ClaudeApiFormat::OpenAiResponses, + Some(CodexWireApi::Chat) => ApiFormat::OpenAiChat, + _ => ApiFormat::OpenAiResponses, } } diff --git a/src-tauri/src/cli/tui/form/provider_templates.rs b/src-tauri/src/cli/tui/form/provider_templates.rs index 6a27df23..b51a7a72 100644 --- a/src-tauri/src/cli/tui/form/provider_templates.rs +++ b/src-tauri/src/cli/tui/form/provider_templates.rs @@ -3,7 +3,7 @@ use crate::provider::{ClaudeApiKeyField, CodexChatReasoningConfig}; use serde_json::{json, Value}; use super::{ - ClaudeApiFormat, CodexModelCatalogField, CodexModelCatalogRow, CodexWireApi, FormMode, + ApiFormat, CodexModelCatalogField, CodexModelCatalogRow, CodexWireApi, FormMode, GeminiAuthType, ProviderAddFormState, HERMES_DEFAULT_API_MODE, OPENCLAW_DEFAULT_API_PROTOCOL, }; @@ -404,7 +404,7 @@ impl ProviderAddFormState { self.claude_api_key = defaults.claude_api_key; self.claude_api_key_field = defaults.claude_api_key_field; self.claude_base_url = defaults.claude_base_url; - self.claude_api_format = defaults.claude_api_format; + self.api_format = defaults.api_format; self.claude_model = defaults.claude_model; self.claude_reasoning_model = defaults.claude_reasoning_model; self.claude_haiku_model = defaults.claude_haiku_model; @@ -461,7 +461,7 @@ impl ProviderAddFormState { self.claude_api_key.set(""); self.claude_api_key_field = ClaudeApiKeyField::AuthToken; self.claude_base_url.set(""); - self.claude_api_format = ClaudeApiFormat::Anthropic; + self.api_format = ApiFormat::Anthropic; self.claude_model.set(""); self.claude_reasoning_model.set(""); self.claude_haiku_model.set(""); @@ -489,7 +489,7 @@ impl ProviderAddFormState { self.claude_api_key_field = ClaudeApiKeyField::AuthToken; self.claude_base_url .set("https://chatgpt.com/backend-api/codex"); - self.claude_api_format = ClaudeApiFormat::OpenAiResponses; + self.api_format = ApiFormat::OpenAiResponses; self.claude_model.set("gpt-5.4"); self.claude_reasoning_model.set("gpt-5.4"); self.claude_haiku_model.set("gpt-5.4-mini"); @@ -560,7 +560,7 @@ impl ProviderAddFormState { self.codex_wire_api = CodexWireApi::Responses; self.codex_requires_openai_auth = true; self.codex_env_key.set(""); - self.claude_api_format = ClaudeApiFormat::OpenAiChat; + self.api_format = ApiFormat::OpenAiChat; self.codex_chat_reasoning = CodexChatReasoningConfig { supports_thinking: Some(true), supports_effort: Some(true), @@ -762,7 +762,7 @@ impl ProviderAddFormState { } fn reset_codex_local_routing_state(&mut self) { - self.claude_api_format = ClaudeApiFormat::OpenAiResponses; + self.api_format = ApiFormat::OpenAiResponses; self.codex_chat_reasoning = CodexChatReasoningConfig::default(); self.codex_model_catalog.clear(); self.codex_local_routing_field_idx = 0; diff --git a/src-tauri/src/cli/tui/form/tests.rs b/src-tauri/src/cli/tui/form/tests.rs index 2a31abbb..f0a97224 100644 --- a/src-tauri/src/cli/tui/form/tests.rs +++ b/src-tauri/src/cli/tui/form/tests.rs @@ -302,8 +302,8 @@ fn provider_add_form_codex_oauth_template_matches_upstream_contract() { "https://chatgpt.com/backend-api/codex" ); assert_eq!( - form.claude_api_format, - crate::cli::tui::form::ClaudeApiFormat::OpenAiResponses + form.api_format, + crate::cli::tui::form::ApiFormat::OpenAiResponses ); assert_eq!(form.claude_model.value, "gpt-5.4"); assert_eq!(form.claude_haiku_model.value, "gpt-5.4-mini"); @@ -316,7 +316,7 @@ fn provider_add_form_codex_oauth_template_matches_upstream_contract() { assert!(fields.contains(&ProviderAddField::ClaudeModelConfig)); assert!(fields.contains(&ProviderAddField::ClaudeHideAttribution)); assert!(!fields.contains(&ProviderAddField::ClaudeBaseUrl)); - assert!(!fields.contains(&ProviderAddField::ClaudeApiFormat)); + assert!(!fields.contains(&ProviderAddField::ApiFormat)); assert!(!fields.contains(&ProviderAddField::ClaudeApiKey)); let provider = form.to_provider_json_value(); @@ -375,8 +375,8 @@ fn provider_edit_form_codex_oauth_loads_account_and_fast_mode() { assert_eq!(form.codex_oauth_account_id.as_deref(), Some("acc-123")); assert!(form.codex_fast_mode); assert_eq!( - form.claude_api_format, - crate::cli::tui::form::ClaudeApiFormat::OpenAiResponses + form.api_format, + crate::cli::tui::form::ApiFormat::OpenAiResponses ); } @@ -872,7 +872,7 @@ fn provider_add_form_claude_official_keeps_hide_attribution_field_visible() { let fields = form.fields(); assert!(!fields.contains(&ProviderAddField::ClaudeBaseUrl)); - assert!(!fields.contains(&ProviderAddField::ClaudeApiFormat)); + assert!(!fields.contains(&ProviderAddField::ApiFormat)); assert!(!fields.contains(&ProviderAddField::ClaudeApiKey)); assert!(!fields.contains(&ProviderAddField::ClaudeModelConfig)); assert!(fields.contains(&ProviderAddField::ClaudeHideAttribution)); @@ -1030,7 +1030,7 @@ fn provider_add_form_packycode_template_codex_sets_partner_meta_and_base_url() { #[test] fn provider_add_form_codex_template_switch_clears_local_routing_state() { let mut form = ProviderAddFormState::new(AppType::Codex); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; form.codex_chat_reasoning.supports_thinking = Some(true); form.codex_chat_reasoning.supports_effort = Some(true); form.codex_local_routing_field_idx = 3; @@ -1055,7 +1055,7 @@ fn provider_add_form_codex_template_switch_clears_local_routing_state() { assert!(provider["meta"].get("codexChatReasoning").is_none()); assert!(provider["settingsConfig"].get("modelCatalog").is_none()); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; form.codex_chat_reasoning.supports_thinking = Some(true); form.apply_codex_model_catalog_value(json!([{ "model": "qwen-coder" }])) .expect("catalog should apply"); @@ -1193,7 +1193,7 @@ fn provider_add_form_claude_api_format_writes_openai_chat_meta() { let mut form = ProviderAddFormState::new(AppType::Claude); form.id.set("p1"); form.name.set("Provider One"); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; let provider = form.to_provider_json_value(); assert_eq!(provider["meta"]["apiFormat"], "openai_chat"); @@ -1217,7 +1217,7 @@ fn provider_add_form_claude_api_format_restores_openai_chat_meta() { }); let form = ProviderAddFormState::from_provider(AppType::Claude, &provider); - assert_eq!(form.claude_api_format, ClaudeApiFormat::OpenAiChat); + assert_eq!(form.api_format, ApiFormat::OpenAiChat); } #[test] @@ -1238,7 +1238,7 @@ fn provider_add_form_claude_api_format_round_trips_openai_responses_meta() { }); let form = ProviderAddFormState::from_provider(AppType::Claude, &provider); - assert_eq!(form.claude_api_format.as_str(), "openai_responses"); + assert_eq!(form.api_format.as_str(), "openai_responses"); let saved = form.to_provider_json_value(); assert_eq!(saved["meta"]["apiFormat"], "openai_responses"); @@ -1266,7 +1266,7 @@ fn provider_add_form_claude_api_format_round_trips_gemini_native_meta() { }); let form = ProviderAddFormState::from_provider(AppType::Claude, &provider); - assert_eq!(form.claude_api_format, ClaudeApiFormat::GeminiNative); + assert_eq!(form.api_format, ApiFormat::GeminiNative); let saved = form.to_provider_json_value(); assert_eq!(saved["meta"]["apiFormat"], "gemini_native"); @@ -1515,8 +1515,8 @@ fn provider_add_form_codex_custom_includes_api_key_and_hides_advanced_fields() { "custom Codex provider should expose Local Routing on its secondary page" ); assert!( - !fields.contains(&ProviderAddField::ClaudeApiFormat), - "custom Codex provider should not expose the old API Format selector" + fields.contains(&ProviderAddField::ApiFormat), + "custom Codex provider should expose the API Format selector" ); assert!( !fields.contains(&ProviderAddField::CodexWireApi), @@ -1539,7 +1539,7 @@ fn provider_add_form_codex_local_routing_writes_meta_without_chat_wire_api() { form.name.set("Custom"); form.codex_base_url.set("https://api.example.com/v1"); form.codex_model.set("deepseek-chat"); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; let provider = form.to_provider_json_value(); let config = provider["settingsConfig"]["config"] @@ -1636,7 +1636,7 @@ fn provider_add_form_codex_local_routing_saves_normalized_reasoning() { form.id.set("custom"); form.name.set("Custom"); form.codex_base_url.set("https://api.example.com/v1"); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; form.toggle_codex_reasoning_effort(); @@ -1702,7 +1702,7 @@ fn provider_add_form_codex_model_catalog_saves_normalized_models_and_syncs_prima form.name.set("Custom"); form.codex_base_url.set("https://api.example.com/v1"); form.codex_model.set("fallback-model"); - form.claude_api_format = ClaudeApiFormat::OpenAiChat; + form.api_format = ApiFormat::OpenAiChat; form.apply_codex_model_catalog_value(json!([ { "model": " deepseek-chat ", "displayName": " DeepSeek Chat ", "contextWindow": "128000 tokens" }, { "model": "deepseek-chat", "displayName": "Duplicate" }, @@ -1777,7 +1777,7 @@ fn provider_add_form_claude_official_sets_upstream_website_and_hides_non_officia "official Claude provider should not show Base URL input" ); assert!( - !fields.contains(&ProviderAddField::ClaudeApiFormat), + !fields.contains(&ProviderAddField::ApiFormat), "official Claude provider should not show API format input" ); assert!( @@ -1859,7 +1859,7 @@ fn provider_add_form_claude_without_official_category_keeps_third_party_fields_v let fields = form.fields(); assert!(fields.contains(&ProviderAddField::ClaudeBaseUrl)); - assert!(fields.contains(&ProviderAddField::ClaudeApiFormat)); + assert!(fields.contains(&ProviderAddField::ApiFormat)); assert!(fields.contains(&ProviderAddField::ClaudeApiKey)); assert!(fields.contains(&ProviderAddField::ClaudeModelConfig)); } @@ -4261,3 +4261,187 @@ fn provider_add_form_usage_query_numeric_fields_match_upstream_normalization() { assert_eq!(script["timeout"], 10); assert_eq!(script["autoQueryInterval"], 0); } + +/// End-to-end test: exercises the actual `ProviderAddFormState` TUI form edit flow +/// for a Claude provider with `meta.apiFormat = "openai_chat"`. +/// +/// Flow: +/// 1. Create a test home and file-based DB with a realistic Claude provider +/// (id="default", name="leihuo-idc", meta.apiFormat="openai_chat", settingsConfig +/// with env, permissions, model, etc.) +/// 2. Load the provider from the state +/// 3. Create a `ProviderAddFormState` via `from_provider_with_common_snippet()` +/// 4. Change `form.api_format` to `ApiFormat::Anthropic` +/// 5. Call `form.to_provider_json_value()` to get the JSON +/// 6. Print the JSON to verify it has `meta: {}` (no apiFormat key) +/// 7. Deserialize the JSON back to Provider +/// 8. Print `provider.meta` to verify api_format is None +/// 9. Call `ProviderService::update()` with the deserialized provider +/// 10. Verify the DB state (raw DB read) +/// 11. Re-create AppState via `try_new()` +/// 12. Re-read the provider and verify meta.api_format is None +/// 13. Check what `get_provider_api_format` returns +#[test] +fn test_exact_tui_form_api_format_roundtrip() { + use std::sync::Arc; + + use crate::app_config::AppType; + use crate::database::Database; + use crate::provider::{Provider, ProviderMeta}; + use crate::proxy::providers::get_provider_api_format; + use crate::services::ProviderService; + use crate::store::AppState; + use crate::test_support::TestEnvGuard; + + let temp = tempfile::tempdir().expect("create temp dir"); + let _guard = TestEnvGuard::isolated(temp.path()); + + // Build a Claude provider with apiFormat = "openai_chat" + let mut claude_provider = Provider::with_id( + "default".to_string(), + "leihuo-idc".to_string(), + json!({ + "env": { + "ANTHROPIC_AUTH_TOKEN": "sk-test-key", + "ANTHROPIC_BASE_URL": "https://example.com/v1" + } + }), + None, + ); + claude_provider.meta = Some(ProviderMeta { + api_format: Some("openai_chat".to_string()), + ..Default::default() + }); + + let mut initial_config = crate::app_config::MultiAppConfig::default(); + { + let claude_manager = initial_config.get_manager_mut(&AppType::Claude).unwrap(); + claude_manager.current = "default".to_string(); + claude_manager + .providers + .insert("default".to_string(), claude_provider.clone()); + } + + let db = Arc::new(Database::init().unwrap()); + db.migrate_from_json(&initial_config).unwrap(); + let mut config = initial_config; + ProviderService::migrate_common_config_upstream_semantics_if_needed(&db, &mut config).unwrap(); + let state = AppState { + db: db.clone(), + config: std::sync::RwLock::new(config), + proxy_service: crate::ProxyService::new(db.clone()), + }; + + // Load the provider and create a form + let loaded_provider = { + let guard = state.config.read().unwrap(); + guard + .get_manager(&AppType::Claude) + .unwrap() + .providers + .get("default") + .unwrap() + .clone() + }; + + let mut form = ProviderAddFormState::from_provider_with_common_snippet( + AppType::Claude, + &loaded_provider, + "", + ); + + // Change api_format to Anthropic (simulating user changing the dropdown) + assert_eq!(form.api_format, ApiFormat::OpenAiChat); + form.api_format = ApiFormat::Anthropic; + + let form_output_json = form.to_provider_json_value(); + + // Verify: meta.apiFormat should be absent, but meta key should be present + assert!( + form_output_json + .get("meta") + .and_then(|m| m.get("apiFormat")) + .is_none(), + "form output should NOT have meta.apiFormat when api_format is Anthropic" + ); + assert!( + form_output_json.get("meta").is_some(), + "form output should have meta key (even if empty) to signal explicit meta update" + ); + + // Deserialize and update + let deserialized_provider: Provider = + serde_json::from_value(form_output_json).expect("deserialize form output"); + assert!( + deserialized_provider.meta.is_some(), + "deserialized provider should have Some(meta)" + ); + assert!( + deserialized_provider + .meta + .as_ref() + .and_then(|m| m.api_format.as_deref()) + .is_none(), + "deserialized provider meta.api_format should be None" + ); + + ProviderService::update(&state, AppType::Claude, deserialized_provider).unwrap(); + + // Verify in-memory state + { + let guard = state.config.read().unwrap(); + let provider = guard + .get_manager(&AppType::Claude) + .unwrap() + .providers + .get("default") + .unwrap(); + assert_eq!( + provider.meta.as_ref().and_then(|m| m.api_format.as_deref()), + None, + "in-memory meta.api_format should be None after update" + ); + } + + // Verify DB state + { + let db_provider = state + .db + .get_all_providers(AppType::Claude.as_str()) + .unwrap() + .get("default") + .unwrap() + .clone(); + assert_eq!( + db_provider + .meta + .as_ref() + .and_then(|m| m.api_format.as_deref()), + None, + "DB meta.api_format should be None after update" + ); + } + + // Verify state survives reload + drop(state); + let reloaded_state = AppState::try_new().unwrap(); + { + let guard = reloaded_state.config.read().unwrap(); + let provider = guard + .get_manager(&AppType::Claude) + .unwrap() + .providers + .get("default") + .unwrap(); + assert_eq!( + provider.meta.as_ref().and_then(|m| m.api_format.as_deref()), + None, + "meta.api_format should remain None after reload" + ); + assert_eq!( + get_provider_api_format(provider), + "anthropic", + "get_provider_api_format should return 'anthropic'" + ); + } +} diff --git a/src-tauri/src/cli/tui/help.rs b/src-tauri/src/cli/tui/help.rs index f8c83b25..665c1050 100644 --- a/src-tauri/src/cli/tui/help.rs +++ b/src-tauri/src/cli/tui/help.rs @@ -84,8 +84,8 @@ fn current_help_target(app: &App) -> HelpTarget { Overlay::UsageQueryTemplatePicker { .. } => { provider_usage_query_overlay_target(app, UsageQueryField::Template) } - Overlay::ClaudeApiFormatPicker { .. } => { - provider_field_overlay_target(app, ProviderAddField::ClaudeApiFormat) + Overlay::ApiFormatPicker { .. } => { + provider_field_overlay_target(app, ProviderAddField::ApiFormat) } Overlay::ClaudeModelPicker { .. } => { provider_field_overlay_target(app, ProviderAddField::ClaudeModelConfig) @@ -285,8 +285,8 @@ fn provider_field_help(app_type: AppType, field: ProviderAddField) -> HelpConten "Configures Claude model tiers. Role-specific models are written into the live config for the client to select by task.", ), ), - ProviderAddField::ClaudeApiFormat => HelpContent::new( - texts::tui_label_claude_api_format(), + ProviderAddField::ApiFormat => HelpContent::new( + texts::tui_label_api_format(), help_lines( "选择供应商的协议格式。非 Anthropic 协议通常需要本地路由做格式转换。", "Selects the provider protocol. Non-Anthropic formats usually need local routing for conversion.", diff --git a/src-tauri/src/cli/tui/runtime_actions/providers.rs b/src-tauri/src/cli/tui/runtime_actions/providers.rs index aeca803a..5ec1d8da 100644 --- a/src-tauri/src/cli/tui/runtime_actions/providers.rs +++ b/src-tauri/src/cli/tui/runtime_actions/providers.rs @@ -1,9 +1,9 @@ use crate::cli::i18n::texts; -use crate::cli::tui::form::ClaudeApiFormat; +use crate::cli::tui::form::ApiFormat; use crate::error::AppError; #[cfg(test)] use crate::openclaw_config::OpenClawDefaultModel; -use crate::proxy::providers::get_claude_api_format; +use crate::proxy::providers::get_provider_api_format; use crate::services::provider::ProviderSortUpdate; use crate::services::ProviderService; @@ -137,7 +137,7 @@ fn provider_switch_proxy_notice_overlay( ) -> Option { provider_switch_proxy_notice_api_format(app_type, provider, proxy_ready).map(|api_format| { Overlay::Confirm(ConfirmOverlay { - title: texts::tui_claude_api_format_requires_proxy_title().to_string(), + title: texts::tui_api_format_requires_proxy_title().to_string(), message: texts::tui_claude_api_format_requires_proxy_message(api_format), action: ConfirmAction::ProviderApiFormatProxyNotice, }) @@ -152,8 +152,8 @@ fn provider_requires_local_proxy( return None; } - let api_format = get_claude_api_format(provider); - ClaudeApiFormat::from_raw(api_format) + let api_format = get_provider_api_format(provider); + ApiFormat::from_raw(api_format) .requires_proxy() .then_some(api_format) } @@ -1596,7 +1596,7 @@ mod tests { assert!(matches!( fixture.app.overlay, Overlay::Confirm(ConfirmOverlay { title, message, action }) - if title == texts::tui_claude_api_format_requires_proxy_title() + if title == texts::tui_api_format_requires_proxy_title() && message == texts::tui_claude_api_format_requires_proxy_message("openai_chat") && matches!(action, ConfirmAction::ProviderApiFormatProxyNotice) )); @@ -1612,7 +1612,7 @@ mod tests { assert!(matches!( fixture.app.overlay, Overlay::Confirm(ConfirmOverlay { title, message, action }) - if title == texts::tui_claude_api_format_requires_proxy_title() + if title == texts::tui_api_format_requires_proxy_title() && message == texts::tui_claude_api_format_requires_proxy_message("openai_responses") && matches!(action, ConfirmAction::ProviderApiFormatProxyNotice) )); diff --git a/src-tauri/src/cli/tui/ui/forms/provider.rs b/src-tauri/src/cli/tui/ui/forms/provider.rs index 268077c2..fa87ad1b 100644 --- a/src-tauri/src/cli/tui/ui/forms/provider.rs +++ b/src-tauri/src/cli/tui/ui/forms/provider.rs @@ -3,10 +3,11 @@ use serde_json::json; use std::collections::BTreeSet; fn provider_api_format_label(provider: &super::form::ProviderAddFormState) -> String { - let api_format = provider.claude_api_format.as_str(); if matches!(provider.app_type, AppType::Codex) { + let api_format = provider.api_format.as_str(); texts::tui_codex_api_format_value(api_format).to_string() } else { + let api_format = provider.api_format.as_str(); texts::tui_claude_api_format_value(api_format).to_string() } } @@ -1166,7 +1167,7 @@ pub(crate) fn provider_field_label_and_value( } ProviderAddField::Notes => strip_trailing_colon(texts::notes_label()).to_string(), ProviderAddField::ClaudeBaseUrl => texts::tui_label_base_url().to_string(), - ProviderAddField::ClaudeApiFormat => texts::tui_label_claude_api_format().to_string(), + ProviderAddField::ApiFormat => texts::tui_label_api_format().to_string(), ProviderAddField::ClaudeApiKey => texts::tui_label_api_key().to_string(), ProviderAddField::ClaudeModelConfig => texts::tui_label_claude_model_config().to_string(), ProviderAddField::ClaudeHideAttribution => { @@ -1225,7 +1226,7 @@ pub(crate) fn provider_field_label_and_value( }; let value = match field { - ProviderAddField::ClaudeApiFormat => provider_api_format_label(provider), + ProviderAddField::ApiFormat => provider_api_format_label(provider), ProviderAddField::CodexWireApi => provider.codex_wire_api.as_str().to_string(), ProviderAddField::CodexRequiresOpenaiAuth => { if provider.codex_requires_openai_auth { @@ -1332,11 +1333,11 @@ pub(crate) fn provider_field_editor_line( (Line::raw(shown), input.cursor) } else { let text = match field { - ProviderAddField::ClaudeApiFormat => { + ProviderAddField::ApiFormat => { let value = if matches!(provider.app_type, AppType::Codex) { - texts::tui_codex_api_format_value(provider.claude_api_format.as_str()) + texts::tui_codex_api_format_value(provider.api_format.as_str()) } else { - texts::tui_claude_api_format_value(provider.claude_api_format.as_str()) + texts::tui_claude_api_format_value(provider.api_format.as_str()) }; format!("api_format = {}", value) } diff --git a/src-tauri/src/cli/tui/ui/overlay/pickers.rs b/src-tauri/src/cli/tui/ui/overlay/pickers.rs index c5321ab3..cfc84c6c 100644 --- a/src-tauri/src/cli/tui/ui/overlay/pickers.rs +++ b/src-tauri/src/cli/tui/ui/overlay/pickers.rs @@ -155,7 +155,7 @@ pub(super) fn render_claude_model_picker_overlay( } } -pub(super) fn render_claude_api_format_picker_overlay( +pub(super) fn render_api_format_picker_overlay( frame: &mut Frame<'_>, app: &App, content_area: Rect, @@ -169,7 +169,7 @@ pub(super) fn render_claude_api_format_picker_overlay( .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(overlay_border_style(theme, false)) - .title(texts::tui_claude_api_format_popup_title()); + .title(texts::tui_api_format_popup_title()); frame.render_widget(outer.clone(), area); let inner = outer.inner(area); @@ -200,18 +200,18 @@ pub(super) fn render_claude_api_format_picker_overlay( .as_ref() .and_then(|form| match form { FormState::ProviderAdd(provider) => { - Some((provider.app_type.clone(), provider.claude_api_format)) + Some((provider.app_type.clone(), provider.api_format)) } _ => None, }) .unwrap_or_else(|| { ( app.app_type.clone(), - crate::cli::tui::form::ClaudeApiFormat::Anthropic, + crate::cli::tui::form::ApiFormat::Anthropic, ) }); - let choices = crate::cli::tui::form::ClaudeApiFormat::choices_for_app(&app_type); + let choices = crate::cli::tui::form::ApiFormat::choices_for_app(&app_type); let items = choices.into_iter().copied().map(|api_format| { let marker = if api_format == current { texts::tui_marker_active() diff --git a/src-tauri/src/cli/tui/ui/overlay/render.rs b/src-tauri/src/cli/tui/ui/overlay/render.rs index 69dd6365..d9307884 100644 --- a/src-tauri/src/cli/tui/ui/overlay/render.rs +++ b/src-tauri/src/cli/tui/ui/overlay/render.rs @@ -69,15 +69,13 @@ pub(crate) fn render_overlay( *editing, ) } - Overlay::ClaudeApiFormatPicker { selected } => { - super::pickers::render_claude_api_format_picker_overlay( - frame, - app, - content_area, - theme, - *selected, - ) - } + Overlay::ApiFormatPicker { selected } => super::pickers::render_api_format_picker_overlay( + frame, + app, + content_area, + theme, + *selected, + ), Overlay::UsageQueryTemplatePicker { selected } => { super::pickers::render_usage_query_template_picker_overlay( frame, diff --git a/src-tauri/src/cli/tui/ui/providers.rs b/src-tauri/src/cli/tui/ui/providers.rs index 05d2da9a..0bd257d5 100644 --- a/src-tauri/src/cli/tui/ui/providers.rs +++ b/src-tauri/src/cli/tui/ui/providers.rs @@ -63,8 +63,8 @@ fn provider_proxy_badge(app_type: &AppType, row: &ProviderRow) -> Option { - let api_format = crate::proxy::providers::get_claude_api_format(&row.provider); - crate::proxy::providers::claude_api_format_needs_transform(api_format) + let api_format = crate::proxy::providers::get_provider_api_format(&row.provider); + crate::proxy::providers::api_format_needs_transform(api_format) .then_some(ProviderProxyBadge::NeedsProxy) } AppType::Codex if provider_category_is(row, "official") => { @@ -517,11 +517,11 @@ pub(super) fn render_provider_detail( Span::raw(": "), Span::raw(base_url), ])); - let api_format = crate::proxy::providers::get_claude_api_format(&row.provider); + let api_format = crate::proxy::providers::get_provider_api_format(&row.provider); lines.push(Line::from(vec![ Span::styled( - texts::tui_label_claude_api_format(), + texts::tui_label_api_format(), Style::default().fg(theme.accent), ), Span::raw(": "), diff --git a/src-tauri/src/cli/tui/ui/tests.rs b/src-tauri/src/cli/tui/ui/tests.rs index 8fa64c0d..93fd847c 100644 --- a/src-tauri/src/cli/tui/ui/tests.rs +++ b/src-tauri/src/cli/tui/ui/tests.rs @@ -1052,11 +1052,11 @@ fn redact_sensitive_json_keeps_non_secret_token_count_fields_visible() { #[test] fn provider_field_label_and_value_renders_claude_api_format() { let mut form = crate::cli::tui::form::ProviderAddFormState::new(AppType::Claude); - form.claude_api_format = crate::cli::tui::form::ClaudeApiFormat::OpenAiChat; + form.api_format = crate::cli::tui::form::ApiFormat::OpenAiChat; let (label, value) = super::provider_field_label_and_value( &form, - crate::cli::tui::form::ProviderAddField::ClaudeApiFormat, + crate::cli::tui::form::ProviderAddField::ApiFormat, ); assert!(label.contains("API")); assert!(value.contains("OpenAI Chat Completions")); @@ -1066,11 +1066,11 @@ fn provider_field_label_and_value_renders_claude_api_format() { #[test] fn provider_field_label_and_value_renders_claude_responses_api_format() { let mut form = crate::cli::tui::form::ProviderAddFormState::new(AppType::Claude); - form.claude_api_format = crate::cli::tui::form::ClaudeApiFormat::OpenAiResponses; + form.api_format = crate::cli::tui::form::ApiFormat::OpenAiResponses; let (_label, value) = super::provider_field_label_and_value( &form, - crate::cli::tui::form::ProviderAddField::ClaudeApiFormat, + crate::cli::tui::form::ProviderAddField::ApiFormat, ); assert!(value.contains("OpenAI Responses API")); assert!(value.contains("代理") || value.contains("proxy")); @@ -4383,7 +4383,7 @@ fn claude_api_format_picker_overlay_is_compact_and_padded() { app.form = Some(crate::cli::tui::form::FormState::ProviderAdd( crate::cli::tui::form::ProviderAddFormState::new(AppType::Claude), )); - app.overlay = Overlay::ClaudeApiFormatPicker { selected: 1 }; + app.overlay = Overlay::ApiFormatPicker { selected: 1 }; let data = minimal_data(&app.app_type); let buf = render(&app, &data); @@ -4443,7 +4443,7 @@ fn provider_api_format_proxy_notice_overlay_uses_close_actions() { app.route = Route::Providers; app.focus = Focus::Content; app.overlay = Overlay::Confirm(ConfirmOverlay { - title: texts::tui_claude_api_format_requires_proxy_title().to_string(), + title: texts::tui_api_format_requires_proxy_title().to_string(), message: texts::tui_claude_api_format_requires_proxy_message("openai_chat"), action: ConfirmAction::ProviderApiFormatProxyNotice, }); diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index 94d46c7a..3e6ab118 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -14,7 +14,7 @@ const ANTHROPIC_THINKING_PLACEHOLDER: &str = "tool call"; const ANTHROPIC_REDACTED_THINKING_PLACEHOLDER: &str = "[redacted thinking]"; const REASONING_VENDOR_HINTS: &[&str] = &["moonshot", "kimi", "deepseek", "mimo", "xiaomimimo"]; -pub fn get_claude_api_format(provider: &Provider) -> &'static str { +pub fn get_provider_api_format(provider: &Provider) -> &'static str { if let Some(meta) = provider.meta.as_ref() { if meta.provider_type.as_deref() == Some("codex_oauth") { return "openai_responses"; @@ -63,7 +63,7 @@ pub fn get_claude_api_format(provider: &Provider) -> &'static str { } } -pub fn claude_api_format_needs_transform(api_format: &str) -> bool { +pub fn api_format_needs_transform(api_format: &str) -> bool { matches!( api_format, "openai_chat" | "openai_responses" | "gemini_native" @@ -330,7 +330,7 @@ impl ClaudeAdapter { } fn get_api_format(&self, provider: &Provider) -> &'static str { - get_claude_api_format(provider) + get_provider_api_format(provider) } fn is_bearer_only_mode(&self, provider: &Provider) -> bool { @@ -600,7 +600,7 @@ impl ProviderAdapter for ClaudeAdapter { return true; } - claude_api_format_needs_transform(self.get_api_format(provider)) + api_format_needs_transform(self.get_api_format(provider)) } fn transform_request( @@ -744,7 +744,7 @@ mod tests { })) .expect("provider should deserialize"); - assert_eq!(get_claude_api_format(&provider), "openai_responses"); + assert_eq!(get_provider_api_format(&provider), "openai_responses"); assert_eq!( format!("{:?}", adapter.provider_type(&provider)), "CodexOAuth" diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index fb62ac2a..2b7abf2f 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -28,11 +28,14 @@ pub use adapter::ProviderAdapter; pub use auth::{AuthInfo, AuthStrategy}; #[allow(unused_imports)] pub use claude::{ - claude_api_format_needs_transform, get_claude_api_format, + api_format_needs_transform, get_provider_api_format, normalize_anthropic_tool_thinking_history_for_provider, transform_claude_request_for_api_format, transform_claude_request_for_api_format_with_shadow, transform_gemini_response_for_provider, ClaudeAdapter, }; +/// Backward-compatible aliases for callers that still reference the old names. +pub use api_format_needs_transform as claude_api_format_needs_transform; +pub use get_provider_api_format as get_claude_api_format; pub use codex::CodexAdapter; #[allow(unused_imports)] pub use codex::{