Add probe_model_for_provider to fix false doctor warning on ChatGPT/Codex backend

doctor and provider_auth used cheapest_model (gpt-5-mini) for connectivity
probes, but gpt-5-mini is rejected by the ChatGPT/Codex backend. Adds
probe_model_for_provider() which returns gpt-5.4-mini for OpenAI and falls
back to the default model for other providers.

Fixes #96

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-19 13:31:32 -04:00
parent 60e10cb78e
commit 9c84c8738e
No known key found for this signature in database
3 changed files with 43 additions and 12 deletions

View file

@ -855,16 +855,9 @@ async fn probe_daytona() -> Option<Result<(), String>> {
)
}
pub(crate) fn cheapest_model(provider: Provider) -> String {
let models = fabro_llm::catalog::list_models(Some(provider.as_str()));
models
.iter()
.min_by(|a, b| {
let cost_a = a.costs.input_cost_per_mtok.unwrap_or(f64::MAX);
let cost_b = b.costs.input_cost_per_mtok.unwrap_or(f64::MAX);
cost_a.total_cmp(&cost_b)
})
.map(|m| m.id.clone())
pub(crate) fn probe_model(provider: Provider) -> String {
fabro_llm::catalog::probe_model_for_provider(provider.as_str())
.map(|m| m.id)
.unwrap_or_else(|| format!("unknown-{}", provider.as_str()))
}
@ -873,7 +866,7 @@ async fn probe_llm_provider(
provider: Provider,
) -> (Provider, Result<(), String>) {
let request = fabro_llm::types::Request {
model: cheapest_model(provider),
model: probe_model(provider),
messages: vec![fabro_llm::types::Message::user("hi")],
provider: Some(provider.as_str().to_string()),
tools: None,

View file

@ -162,7 +162,7 @@ pub(crate) async fn validate_api_key(provider: Provider, api_key: &str) -> Resul
.await
.map_err(|e| e.to_string())?;
let params = fabro_llm::generate::GenerateParams::new(doctor::cheapest_model(provider))
let params = fabro_llm::generate::GenerateParams::new(doctor::probe_model(provider))
.provider(provider.as_str())
.prompt("Say OK")
.max_tokens(16)

View file

@ -56,6 +56,26 @@ pub fn default_model() -> ModelInfo {
.expect("catalog.json must contain at least one default model")
}
/// Get the probe model for a provider — the cheapest model known to work for
/// connectivity checks. Falls back to the default model when no explicit
/// override is configured.
#[must_use]
pub fn probe_model_for_provider(provider: &str) -> Option<ModelInfo> {
let provider = canonical_provider(provider);
// Provider-specific overrides where the cheapest model is unsuitable
// (e.g. gpt-5-mini is rejected by the ChatGPT/Codex backend).
let override_id: Option<&str> = match provider {
"openai" => Some("gpt-5.4-mini"),
_ => None,
};
if let Some(id) = override_id {
if let Some(info) = get_model_info(id) {
return Some(info);
}
}
default_model_for_provider(provider)
}
/// List all known models, optionally filtered by provider (Section 2.9).
#[must_use]
pub fn list_models(provider: Option<&str>) -> Vec<ModelInfo> {
@ -608,6 +628,24 @@ mod tests {
);
}
#[test]
fn probe_model_openai_returns_override() {
let m = probe_model_for_provider("openai").unwrap();
assert_eq!(m.id, "gpt-5.4-mini");
}
#[test]
fn probe_model_anthropic_returns_default() {
let m = probe_model_for_provider("anthropic").unwrap();
assert_eq!(m.id, "claude-opus-4-6");
}
#[test]
fn probe_model_gemini_returns_default() {
let m = probe_model_for_provider("gemini").unwrap();
assert_eq!(m.id, "gemini-3.1-pro-preview");
}
#[test]
fn closest_model_opus_to_gemini() {
let opus = get_model_info("claude-opus-4-6").unwrap();