mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
refactor(server): simplify model availability probes
Use a lightweight basic probe target for preflight instead of fabricating catalog models, run configured model probes with bounded concurrency, and keep expensive model choices opt-in for defaults and live tests.
This commit is contained in:
parent
2ef34a228e
commit
38b51c4c29
8 changed files with 165 additions and 109 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use fabro_model::Model;
|
|
||||||
pub use fabro_model::ModelTestMode;
|
pub use fabro_model::ModelTestMode;
|
||||||
|
use fabro_model::{Model, Provider};
|
||||||
use strum::IntoStaticStr;
|
use strum::IntoStaticStr;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
|
|
@ -54,8 +54,18 @@ pub async fn run_model_test(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_basic_test(info: &Model, client: Arc<Client>) -> ModelTestOutcome {
|
async fn run_basic_test(info: &Model, client: Arc<Client>) -> ModelTestOutcome {
|
||||||
let params = GenerateParams::new(&info.id, client)
|
run_basic_model_probe(&info.id, info.provider, client).await
|
||||||
.provider(<&'static str>::from(info.provider))
|
}
|
||||||
|
|
||||||
|
/// Run the cheap single-prompt model availability probe without requiring a
|
||||||
|
/// catalog-backed [`Model`].
|
||||||
|
pub async fn run_basic_model_probe(
|
||||||
|
model_id: &str,
|
||||||
|
provider: Provider,
|
||||||
|
client: Arc<Client>,
|
||||||
|
) -> ModelTestOutcome {
|
||||||
|
let params = GenerateParams::new(model_id, client)
|
||||||
|
.provider(<&'static str>::from(provider))
|
||||||
.prompt("Say OK")
|
.prompt("Say OK")
|
||||||
.max_tokens(16);
|
.max_tokens(16);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -210,10 +210,18 @@ fn provider_error_from_openai_error_json(error: &serde_json::Value) -> Error {
|
||||||
.map_or_else(|| "OpenAI stream error".to_string(), str::to_string);
|
.map_or_else(|| "OpenAI stream error".to_string(), str::to_string);
|
||||||
|
|
||||||
let kind = match classifier {
|
let kind = match classifier {
|
||||||
Some("insufficient_quota") => ProviderErrorKind::QuotaExceeded,
|
Some("insufficient_quota" | "billing_hard_limit_reached") => {
|
||||||
Some("rate_limit_exceeded") => ProviderErrorKind::RateLimit,
|
ProviderErrorKind::QuotaExceeded
|
||||||
Some("invalid_api_key" | "invalid_authentication") => ProviderErrorKind::Authentication,
|
}
|
||||||
Some("account_deactivated" | "permission_denied") => ProviderErrorKind::AccessDenied,
|
Some("rate_limit_error" | "rate_limit_exceeded" | "too_many_requests") => {
|
||||||
|
ProviderErrorKind::RateLimit
|
||||||
|
}
|
||||||
|
Some("authentication_error" | "invalid_api_key" | "invalid_authentication") => {
|
||||||
|
ProviderErrorKind::Authentication
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
"access_denied" | "account_deactivated" | "permission_denied" | "permission_error",
|
||||||
|
) => ProviderErrorKind::AccessDenied,
|
||||||
Some("content_filter" | "content_policy_violation") => ProviderErrorKind::ContentFilter,
|
Some("content_filter" | "content_policy_violation") => ProviderErrorKind::ContentFilter,
|
||||||
Some("context_length_exceeded") => ProviderErrorKind::ContextLength,
|
Some("context_length_exceeded") => ProviderErrorKind::ContextLength,
|
||||||
Some("server_error" | "internal_error" | "service_unavailable" | "engine_overloaded") => {
|
Some("server_error" | "internal_error" | "service_unavailable" | "engine_overloaded") => {
|
||||||
|
|
@ -1850,6 +1858,29 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_event_with_rate_limit_error_returns_rate_limit() {
|
||||||
|
let mut state = empty_sse_state();
|
||||||
|
let data = r#"{
|
||||||
|
"type": "error",
|
||||||
|
"error": {
|
||||||
|
"type": "rate_limit_error",
|
||||||
|
"message": "Too many requests."
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let err = process_sse_event(&mut state, Some("error"), data)
|
||||||
|
.expect_err("error event should fail the stream");
|
||||||
|
|
||||||
|
match err {
|
||||||
|
Error::Provider { kind, detail } => {
|
||||||
|
assert_eq!(kind, ProviderErrorKind::RateLimit);
|
||||||
|
assert_eq!(detail.error_code.as_deref(), Some("rate_limit_error"));
|
||||||
|
}
|
||||||
|
other => panic!("expected provider error, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_event_with_unknown_invalid_prefix_returns_invalid_request() {
|
fn error_event_with_unknown_invalid_prefix_returns_invalid_request() {
|
||||||
let mut state = empty_sse_state();
|
let mut state = empty_sse_state();
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,10 @@ async fn openai_gpt_5_5_complete() {
|
||||||
assert_eq!(response.provider, "openai");
|
assert_eq!(response.provider, "openai");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[fabro_macros::e2e_test(live("OPENAI_API_KEY"))]
|
#[fabro_macros::e2e_test(live("OPENAI_GPT_5_5_PRO_API_KEY"))]
|
||||||
async fn openai_gpt_5_5_pro_complete() {
|
async fn openai_gpt_5_5_pro_complete() {
|
||||||
let api_key = std::env::var(EnvVars::OPENAI_API_KEY).expect("OPENAI_API_KEY must be set");
|
let api_key = std::env::var("OPENAI_GPT_5_5_PRO_API_KEY")
|
||||||
|
.expect("OPENAI_GPT_5_5_PRO_API_KEY must be set");
|
||||||
let adapter = OpenAiAdapter::new(api_key);
|
let adapter = OpenAiAdapter::new(api_key);
|
||||||
let request = Request {
|
let request = Request {
|
||||||
temperature: None,
|
temperature: None,
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@
|
||||||
"cache_input_cost_per_mtok": 0.50
|
"cache_input_cost_per_mtok": 0.50
|
||||||
},
|
},
|
||||||
"estimated_output_tps": 25,
|
"estimated_output_tps": 25,
|
||||||
"aliases": ["opus", "claude-opus"],
|
"aliases": ["opus", "claude-opus"]
|
||||||
"default": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "claude-opus-4-6",
|
"id": "claude-opus-4-6",
|
||||||
|
|
@ -66,7 +65,8 @@
|
||||||
"cache_input_cost_per_mtok": 0.30
|
"cache_input_cost_per_mtok": 0.30
|
||||||
},
|
},
|
||||||
"estimated_output_tps": 50,
|
"estimated_output_tps": 50,
|
||||||
"aliases": ["sonnet", "claude-sonnet"]
|
"aliases": ["sonnet", "claude-sonnet"],
|
||||||
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "claude-haiku-4-5",
|
"id": "claude-haiku-4-5",
|
||||||
|
|
@ -185,7 +185,8 @@
|
||||||
"cache_input_cost_per_mtok": 0.25
|
"cache_input_cost_per_mtok": 0.25
|
||||||
},
|
},
|
||||||
"estimated_output_tps": 70,
|
"estimated_output_tps": 70,
|
||||||
"aliases": ["gpt54", "gpt-54"]
|
"aliases": ["gpt54", "gpt-54"],
|
||||||
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gpt-5.5",
|
"id": "gpt-5.5",
|
||||||
|
|
@ -202,8 +203,7 @@
|
||||||
"cache_input_cost_per_mtok": 0.50
|
"cache_input_cost_per_mtok": 0.50
|
||||||
},
|
},
|
||||||
"estimated_output_tps": 70,
|
"estimated_output_tps": 70,
|
||||||
"aliases": ["gpt55", "gpt-55"],
|
"aliases": ["gpt55", "gpt-55"]
|
||||||
"default": true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gpt-5.5-pro",
|
"id": "gpt-5.5-pro",
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,7 @@ impl Catalog {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn probe_for_provider(&self, p: Provider) -> Option<&Model> {
|
pub fn probe_for_provider(&self, p: Provider) -> Option<&Model> {
|
||||||
let override_id: Option<&str> = match p {
|
let override_id: Option<&str> = match p {
|
||||||
|
Provider::Anthropic => Some("claude-haiku-4-5"),
|
||||||
Provider::OpenAi => Some("gpt-5.4-mini"),
|
Provider::OpenAi => Some("gpt-5.4-mini"),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
@ -226,13 +227,13 @@ mod tests {
|
||||||
let m = Catalog::builtin()
|
let m = Catalog::builtin()
|
||||||
.default_for_provider(Provider::Anthropic)
|
.default_for_provider(Provider::Anthropic)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(m.id, "claude-opus-4-7");
|
assert_eq!(m.id, "claude-sonnet-4-6");
|
||||||
assert!(m.default);
|
assert!(m.default);
|
||||||
|
|
||||||
let m = Catalog::builtin()
|
let m = Catalog::builtin()
|
||||||
.default_for_provider(Provider::OpenAi)
|
.default_for_provider(Provider::OpenAi)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(m.id, "gpt-5.5");
|
assert_eq!(m.id, "gpt-5.4");
|
||||||
|
|
||||||
let m = Catalog::builtin()
|
let m = Catalog::builtin()
|
||||||
.default_for_provider(Provider::Gemini)
|
.default_for_provider(Provider::Gemini)
|
||||||
|
|
@ -249,11 +250,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_probe_anthropic_returns_default() {
|
fn builtin_probe_anthropic_returns_override() {
|
||||||
let m = Catalog::builtin()
|
let m = Catalog::builtin()
|
||||||
.probe_for_provider(Provider::Anthropic)
|
.probe_for_provider(Provider::Anthropic)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(m.id, "claude-opus-4-7");
|
assert_eq!(m.id, "claude-haiku-4-5");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -730,7 +731,7 @@ mod tests {
|
||||||
"gpt54",
|
"gpt54",
|
||||||
"gpt-54",
|
"gpt-54",
|
||||||
],
|
],
|
||||||
default: false,
|
default: true,
|
||||||
configured: false,
|
configured: false,
|
||||||
}
|
}
|
||||||
"#);
|
"#);
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ mod tests {
|
||||||
assert_eq!(info.cache_input_cost_per_mtok(), Some(0.5));
|
assert_eq!(info.cache_input_cost_per_mtok(), Some(0.5));
|
||||||
assert_eq!(info.estimated_output_tps(), Some(25.0));
|
assert_eq!(info.estimated_output_tps(), Some(25.0));
|
||||||
assert!(!info.aliases().is_empty());
|
assert!(!info.aliases().is_empty());
|
||||||
assert!(info.is_default());
|
assert!(!info.is_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult {
|
||||||
let message = auth_issue_message(*provider, issue);
|
let message = auth_issue_message(*provider, issue);
|
||||||
failures.push(ProviderFailure {
|
failures.push(ProviderFailure {
|
||||||
provider: *provider,
|
provider: *provider,
|
||||||
short: short_error_line(&message),
|
summary_line: short_error_line(&message),
|
||||||
});
|
});
|
||||||
details.push(CheckDetail::new(message));
|
details.push(CheckDetail::new(message));
|
||||||
}
|
}
|
||||||
|
|
@ -135,14 +135,14 @@ async fn check_llm_providers(state: &AppState) -> CheckResult {
|
||||||
let rendered = collect_chain(&err).join(": ");
|
let rendered = collect_chain(&err).join(": ");
|
||||||
failures.push(ProviderFailure {
|
failures.push(ProviderFailure {
|
||||||
provider,
|
provider,
|
||||||
short: short_error_line(&rendered),
|
summary_line: short_error_line(&rendered),
|
||||||
});
|
});
|
||||||
details.push(CheckDetail::new(format!("{provider}: {rendered}")));
|
details.push(CheckDetail::new(format!("{provider}: {rendered}")));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
failures.push(ProviderFailure {
|
failures.push(ProviderFailure {
|
||||||
provider,
|
provider,
|
||||||
short: "timeout (30s)".to_string(),
|
summary_line: "timeout (30s)".to_string(),
|
||||||
});
|
});
|
||||||
details.push(CheckDetail::new(format!("{provider}: timeout (30s)")));
|
details.push(CheckDetail::new(format!("{provider}: timeout (30s)")));
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +166,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult {
|
||||||
};
|
};
|
||||||
let remediation = failures
|
let remediation = failures
|
||||||
.iter()
|
.iter()
|
||||||
.map(|f| format!("{}: {}", f.provider, f.short))
|
.map(|f| format!("{}: {}", f.provider, f.summary_line))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
|
|
@ -181,7 +181,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult {
|
||||||
|
|
||||||
struct ProviderFailure {
|
struct ProviderFailure {
|
||||||
provider: Provider,
|
provider: Provider,
|
||||||
short: String,
|
summary_line: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_SHORT_LEN: usize = 120;
|
const MAX_SHORT_LEN: usize = 120;
|
||||||
|
|
@ -194,7 +194,7 @@ fn short_error_line(rendered: &str) -> String {
|
||||||
.unwrap_or("error");
|
.unwrap_or("error");
|
||||||
if first.chars().count() > MAX_SHORT_LEN {
|
if first.chars().count() > MAX_SHORT_LEN {
|
||||||
let cutoff: String = first.chars().take(MAX_SHORT_LEN).collect();
|
let cutoff: String = first.chars().take(MAX_SHORT_LEN).collect();
|
||||||
format!("{cutoff}…")
|
format!("{cutoff}...")
|
||||||
} else {
|
} else {
|
||||||
first.to_string()
|
first.to_string()
|
||||||
}
|
}
|
||||||
|
|
@ -667,10 +667,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn short_error_line_truncates_long_input_with_ellipsis() {
|
fn short_error_line_truncates_long_input_with_ascii_ellipsis() {
|
||||||
let input = "a".repeat(MAX_SHORT_LEN + 50);
|
let input = "a".repeat(MAX_SHORT_LEN + 50);
|
||||||
let result = short_error_line(&input);
|
let result = short_error_line(&input);
|
||||||
let expected = format!("{}…", "a".repeat(MAX_SHORT_LEN));
|
let expected = format!("{}...", "a".repeat(MAX_SHORT_LEN));
|
||||||
assert_eq!(result, expected);
|
assert_eq!(result, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ use fabro_config::{
|
||||||
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
use fabro_graphviz::graph::{Graph, is_llm_handler_type};
|
||||||
use fabro_graphviz::render::apply_direction;
|
use fabro_graphviz::render::apply_direction;
|
||||||
use fabro_llm::Provider;
|
use fabro_llm::Provider;
|
||||||
use fabro_llm::model_test::{ModelTestMode, ModelTestStatus, run_model_test};
|
use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe};
|
||||||
use fabro_model::{Catalog, Model, ModelCosts, ModelFeatures, ModelLimits};
|
use fabro_model::Catalog;
|
||||||
use fabro_sandbox::config::{
|
use fabro_sandbox::config::{
|
||||||
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
|
DaytonaNetwork, DaytonaSnapshotSettings, DockerfileSource as SandboxDockerfileSource,
|
||||||
};
|
};
|
||||||
|
|
@ -39,6 +39,7 @@ use fabro_workflow::pipeline::Validated;
|
||||||
use fabro_workflow::run_materialization::materialize_run;
|
use fabro_workflow::run_materialization::materialize_run;
|
||||||
use fabro_workflow::workflow_bundle::{BundledWorkflow, ParsedWorkflowConfig, WorkflowBundle};
|
use fabro_workflow::workflow_bundle::{BundledWorkflow, ParsedWorkflowConfig, WorkflowBundle};
|
||||||
use fabro_workflow::{Error as WorkflowError, ManifestPath};
|
use fabro_workflow::{Error as WorkflowError, ManifestPath};
|
||||||
|
use futures_util::stream::{self, StreamExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
|
|
@ -912,6 +913,15 @@ async fn run_sandbox_check(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MODEL_PREFLIGHT_PROBE_CONCURRENCY: usize = 4;
|
||||||
|
|
||||||
|
struct PendingModelProbe {
|
||||||
|
index: usize,
|
||||||
|
model_id: String,
|
||||||
|
provider_name: String,
|
||||||
|
provider: Provider,
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_llm_check(
|
async fn run_llm_check(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
checks: &mut Vec<CheckResult>,
|
checks: &mut Vec<CheckResult>,
|
||||||
|
|
@ -971,55 +981,50 @@ async fn run_llm_check(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut all_ok = true;
|
let mut all_ok = true;
|
||||||
for (model_id, provider_name) in &model_providers {
|
let mut completed_checks: Vec<(usize, CheckResult)> = Vec::new();
|
||||||
|
let mut pending_probes = Vec::new();
|
||||||
|
for (index, (model_id, provider_name)) in model_providers.iter().enumerate() {
|
||||||
match provider_name.parse::<Provider>() {
|
match provider_name.parse::<Provider>() {
|
||||||
Ok(provider) => {
|
Ok(provider) => {
|
||||||
let mut status = CheckStatus::Pass;
|
if let Some((_, issue)) = auth_issues
|
||||||
let remediation = if let Some((_, issue)) = auth_issues
|
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(candidate, _)| *candidate == provider)
|
.find(|(candidate, _)| *candidate == provider)
|
||||||
{
|
{
|
||||||
status = CheckStatus::Warning;
|
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
Some(auth_issue_message(provider, issue))
|
completed_checks.push((index, CheckResult {
|
||||||
} else if !configured.iter().any(|name| name == provider_name) {
|
|
||||||
status = CheckStatus::Warning;
|
|
||||||
all_ok = false;
|
|
||||||
Some(format!("Provider \"{provider_name}\" is not configured"))
|
|
||||||
} else {
|
|
||||||
let probe_model = preflight_probe_model(model_id, provider);
|
|
||||||
let outcome = run_model_test(
|
|
||||||
&probe_model,
|
|
||||||
ModelTestMode::Basic,
|
|
||||||
Arc::clone(&client),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if outcome.status == ModelTestStatus::Ok {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
status = CheckStatus::Error;
|
|
||||||
all_ok = false;
|
|
||||||
Some(format!(
|
|
||||||
"Model availability probe failed: {}",
|
|
||||||
outcome
|
|
||||||
.error_message
|
|
||||||
.unwrap_or_else(|| "unknown error".to_string())
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
checks.push(CheckResult {
|
|
||||||
name: "LLM".into(),
|
name: "LLM".into(),
|
||||||
status,
|
status: CheckStatus::Warning,
|
||||||
summary: model_id.clone(),
|
summary: model_id.clone(),
|
||||||
details: vec![
|
details: vec![CheckDetail::new(format!(
|
||||||
CheckDetail::new(format!("Provider: {provider_name}")),
|
"Provider: {provider_name}"
|
||||||
CheckDetail::new("Probe: basic generation".to_string()),
|
))],
|
||||||
],
|
remediation: Some(auth_issue_message(provider, issue)),
|
||||||
remediation,
|
}));
|
||||||
|
} else if !configured.iter().any(|name| name == provider_name) {
|
||||||
|
all_ok = false;
|
||||||
|
completed_checks.push((index, CheckResult {
|
||||||
|
name: "LLM".into(),
|
||||||
|
status: CheckStatus::Warning,
|
||||||
|
summary: model_id.clone(),
|
||||||
|
details: vec![CheckDetail::new(format!(
|
||||||
|
"Provider: {provider_name}"
|
||||||
|
))],
|
||||||
|
remediation: Some(format!(
|
||||||
|
"Provider \"{provider_name}\" is not configured"
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
pending_probes.push(PendingModelProbe {
|
||||||
|
index,
|
||||||
|
model_id: model_id.clone(),
|
||||||
|
provider_name: provider_name.clone(),
|
||||||
|
provider,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
checks.push(CheckResult {
|
all_ok = false;
|
||||||
|
completed_checks.push((index, CheckResult {
|
||||||
name: "LLM".into(),
|
name: "LLM".into(),
|
||||||
status: CheckStatus::Error,
|
status: CheckStatus::Error,
|
||||||
summary: model_id.clone(),
|
summary: model_id.clone(),
|
||||||
|
|
@ -1029,11 +1034,55 @@ async fn run_llm_check(
|
||||||
remediation: Some(format!(
|
remediation: Some(format!(
|
||||||
"Invalid provider \"{provider_name}\": {err}"
|
"Invalid provider \"{provider_name}\": {err}"
|
||||||
)),
|
)),
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut probe_checks = stream::iter(pending_probes)
|
||||||
|
.map(|probe| {
|
||||||
|
let client = Arc::clone(&client);
|
||||||
|
async move {
|
||||||
|
let outcome =
|
||||||
|
run_basic_model_probe(&probe.model_id, probe.provider, client).await;
|
||||||
|
let (status, remediation) = if outcome.status == ModelTestStatus::Ok {
|
||||||
|
(CheckStatus::Pass, None)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
CheckStatus::Error,
|
||||||
|
Some(format!(
|
||||||
|
"Model availability probe failed: {}",
|
||||||
|
outcome
|
||||||
|
.error_message
|
||||||
|
.unwrap_or_else(|| "unknown error".to_string())
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
(probe.index, CheckResult {
|
||||||
|
name: "LLM".into(),
|
||||||
|
status,
|
||||||
|
summary: probe.model_id,
|
||||||
|
details: vec![
|
||||||
|
CheckDetail::new(format!("Provider: {}", probe.provider_name)),
|
||||||
|
CheckDetail::new("Probe: basic generation".to_string()),
|
||||||
|
],
|
||||||
|
remediation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.buffer_unordered(MODEL_PREFLIGHT_PROBE_CONCURRENCY)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if probe_checks
|
||||||
|
.iter()
|
||||||
|
.any(|(_, check)| check.status != CheckStatus::Pass)
|
||||||
|
{
|
||||||
all_ok = false;
|
all_ok = false;
|
||||||
}
|
}
|
||||||
}
|
completed_checks.append(&mut probe_checks);
|
||||||
}
|
completed_checks.sort_by_key(|(index, _)| *index);
|
||||||
|
checks.extend(completed_checks.into_iter().map(|(_, check)| check));
|
||||||
all_ok
|
all_ok
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -1049,42 +1098,6 @@ async fn run_llm_check(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preflight_probe_model(model_id: &str, provider: Provider) -> Model {
|
|
||||||
if let Some(info) = Catalog::builtin().get(model_id) {
|
|
||||||
let mut model = info.clone();
|
|
||||||
model.provider = provider;
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
Model {
|
|
||||||
id: model_id.to_string(),
|
|
||||||
provider,
|
|
||||||
family: "custom".to_string(),
|
|
||||||
display_name: model_id.to_string(),
|
|
||||||
limits: ModelLimits {
|
|
||||||
context_window: 0,
|
|
||||||
max_output: None,
|
|
||||||
},
|
|
||||||
training: None,
|
|
||||||
knowledge_cutoff: None,
|
|
||||||
features: ModelFeatures {
|
|
||||||
tools: false,
|
|
||||||
vision: false,
|
|
||||||
reasoning: false,
|
|
||||||
effort: false,
|
|
||||||
},
|
|
||||||
costs: ModelCosts {
|
|
||||||
input_cost_per_mtok: None,
|
|
||||||
output_cost_per_mtok: None,
|
|
||||||
cache_input_cost_per_mtok: None,
|
|
||||||
},
|
|
||||||
estimated_output_tps: None,
|
|
||||||
aliases: Vec::new(),
|
|
||||||
default: false,
|
|
||||||
configured: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_model_provider(
|
fn resolve_model_provider(
|
||||||
settings: &RunNamespace,
|
settings: &RunNamespace,
|
||||||
_graph: &Graph,
|
_graph: &Graph,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue