From 17a010aecc1ffa7303551c03319018712eab780c Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Thu, 7 May 2026 04:26:38 -0500 Subject: [PATCH] fix(doctor): explain strict tool mode endpoints (#1008) --- crates/tui/src/main.rs | 182 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 566e120b..d55731eb 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1685,6 +1685,19 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt println!(" · provider: {}", api_target.provider); println!(" · base_url: {}", api_target.base_url); println!(" · model: {}", api_target.model); + let strict_tool_mode = doctor_strict_tool_mode_status(config); + let strict_icon = match strict_tool_mode.status { + "ready" => "✓".truecolor(aqua_r, aqua_g, aqua_b), + "fallback_non_beta" | "custom_endpoint" => "!".truecolor(sky_r, sky_g, sky_b), + _ => "·".dimmed(), + }; + println!( + " {} strict_tool_mode: {}", + strict_icon, strict_tool_mode.message + ); + if let Some(recommended) = strict_tool_mode.recommended_base_url.as_ref() { + println!(" Use `base_url = \"{recommended}\"` for DeepSeek strict schemas."); + } let capability = crate::config::provider_capability(config.api_provider(), &api_target.model); if let Some(alias) = capability.alias_deprecation.as_ref() { println!( @@ -2208,6 +2221,7 @@ fn run_doctor_json( "file_present": memory_path.exists(), }); let api_target = doctor_api_target(config); + let strict_tool_mode = doctor_strict_tool_mode_status(config); let report = json!({ "version": env!("CARGO_PKG_VERSION"), @@ -2219,6 +2233,13 @@ fn run_doctor_json( }, "base_url": api_target.base_url, "default_text_model": api_target.model, + "strict_tool_mode": { + "enabled": strict_tool_mode.enabled, + "status": strict_tool_mode.status, + "function_strict_sent": strict_tool_mode.function_strict_sent, + "message": strict_tool_mode.message, + "recommended_base_url": strict_tool_mode.recommended_base_url, + }, "memory": memory_summary, "mcp": mcp_summary, "skills": { @@ -2333,6 +2354,15 @@ struct DoctorApiTarget { model: String, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct DoctorStrictToolModeStatus { + enabled: bool, + status: &'static str, + function_strict_sent: bool, + message: String, + recommended_base_url: Option, +} + fn doctor_api_target(config: &Config) -> DoctorApiTarget { let provider = config.api_provider(); DoctorApiTarget { @@ -2342,6 +2372,79 @@ fn doctor_api_target(config: &Config) -> DoctorApiTarget { } } +fn doctor_strict_tool_mode_status(config: &Config) -> DoctorStrictToolModeStatus { + if !config.strict_tool_mode.unwrap_or(false) { + return DoctorStrictToolModeStatus { + enabled: false, + status: "disabled", + function_strict_sent: false, + message: "disabled".to_string(), + recommended_base_url: None, + }; + } + + let target = doctor_api_target(config); + match known_deepseek_base_url_kind(&target.base_url) { + Some(DeepSeekBaseUrlKind::Beta) => DoctorStrictToolModeStatus { + enabled: true, + status: "ready", + function_strict_sent: true, + message: "enabled; DeepSeek strict schemas use the beta endpoint".to_string(), + recommended_base_url: None, + }, + Some(DeepSeekBaseUrlKind::NonBeta) => { + let recommended = recommended_strict_base_url(config, &target.base_url); + DoctorStrictToolModeStatus { + enabled: true, + status: "fallback_non_beta", + function_strict_sent: false, + message: + "enabled, but function.strict is stripped for this non-beta DeepSeek endpoint" + .to_string(), + recommended_base_url: Some(recommended.to_string()), + } + } + None => DoctorStrictToolModeStatus { + enabled: true, + status: "custom_endpoint", + function_strict_sent: true, + message: "enabled; function.strict will be sent to this custom endpoint".to_string(), + recommended_base_url: None, + }, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DeepSeekBaseUrlKind { + Beta, + NonBeta, +} + +fn known_deepseek_base_url_kind(base_url: &str) -> Option { + match base_url.trim_end_matches('/').to_ascii_lowercase().as_str() { + "https://api.deepseek.com/beta" | "https://api.deepseeki.com/beta" => { + Some(DeepSeekBaseUrlKind::Beta) + } + "https://api.deepseek.com" + | "https://api.deepseek.com/v1" + | "https://api.deepseeki.com" + | "https://api.deepseeki.com/v1" => Some(DeepSeekBaseUrlKind::NonBeta), + _ => None, + } +} + +fn recommended_strict_base_url(config: &Config, base_url: &str) -> &'static str { + if matches!( + config.api_provider(), + crate::config::ApiProvider::DeepseekCN + ) || base_url.to_ascii_lowercase().contains("api.deepseeki.com") + { + "https://api.deepseeki.com/beta" + } else { + crate::config::DEFAULT_DEEPSEEK_BASE_URL + } +} + fn doctor_timeout_recovery_lines(config: &Config) -> Vec { let target = doctor_api_target(config); let mut lines = vec![format!( @@ -4288,6 +4391,85 @@ mod doctor_endpoint_tests { assert_eq!(target.model, crate::config::DEFAULT_TEXT_MODEL); } + #[test] + fn strict_tool_mode_doctor_reports_disabled_by_default() { + let config = Config::default(); + + let status = doctor_strict_tool_mode_status(&config); + + assert!(!status.enabled); + assert_eq!(status.status, "disabled"); + assert!(!status.function_strict_sent); + assert!(status.recommended_base_url.is_none()); + } + + #[test] + fn strict_tool_mode_doctor_accepts_default_beta_endpoint() { + let config = Config { + strict_tool_mode: Some(true), + ..Default::default() + }; + + let status = doctor_strict_tool_mode_status(&config); + + assert!(status.enabled); + assert_eq!(status.status, "ready"); + assert!(status.function_strict_sent); + assert!(status.message.contains("beta endpoint")); + assert!(status.recommended_base_url.is_none()); + } + + #[test] + fn strict_tool_mode_doctor_warns_for_non_beta_deepseek_endpoint() { + let config = Config { + strict_tool_mode: Some(true), + base_url: Some("https://api.deepseek.com".to_string()), + ..Default::default() + }; + + let status = doctor_strict_tool_mode_status(&config); + + assert_eq!(status.status, "fallback_non_beta"); + assert!(!status.function_strict_sent); + assert_eq!( + status.recommended_base_url.as_deref(), + Some(crate::config::DEFAULT_DEEPSEEK_BASE_URL) + ); + } + + #[test] + fn strict_tool_mode_doctor_warns_for_deepseek_cn_default_endpoint() { + let config = Config { + provider: Some("deepseek-cn".to_string()), + strict_tool_mode: Some(true), + ..Default::default() + }; + + let status = doctor_strict_tool_mode_status(&config); + + assert_eq!(status.status, "fallback_non_beta"); + assert!(!status.function_strict_sent); + assert_eq!( + status.recommended_base_url.as_deref(), + Some("https://api.deepseeki.com/beta") + ); + } + + #[test] + fn strict_tool_mode_doctor_marks_custom_endpoint_as_forwarded() { + let config = Config { + provider: Some("vllm".to_string()), + strict_tool_mode: Some(true), + ..Default::default() + }; + + let status = doctor_strict_tool_mode_status(&config); + + assert_eq!(status.status, "custom_endpoint"); + assert!(status.function_strict_sent); + assert!(status.message.contains("custom endpoint")); + } + #[test] fn provider_capability_report_exposes_alias_deprecation_for_deepseek_chat() { let config = Config {