use std::process;
use harn_vm::llm::readiness::{probe_provider_readiness, ProviderReadiness};
use harn_vm::llm_config;
use serde::Serialize;
use crate::cli::{
ProviderCacheProbeArgs, ProviderProbeArgs, ProviderToolProbeArgs, ProviderToolProbeModeArg,
ProviderToolScorecardArgs,
};
use crate::commands::local::runtime::{fetch_ollama_ps, LoadedModel, LOCAL_PROVIDERS};
use crate::commands::provider_report::{dispatch_provider_report, ProviderReportDispatch};
const PROBE_PAYLOAD_ENV: &str = "HARN_PROVIDER_PROBE_PAYLOAD_JSON";
const PROBE_PAYLOAD_PRETTY_ENV: &str = "HARN_PROVIDER_PROBE_PAYLOAD_PRETTY";
const TOOL_PROBE_PAYLOAD_ENV: &str = "HARN_PROVIDER_TOOL_PROBE_PAYLOAD_JSON";
const TOOL_PROBE_PAYLOAD_PRETTY_ENV: &str = "HARN_PROVIDER_TOOL_PROBE_PAYLOAD_PRETTY";
const TOOL_SCORECARD_PAYLOAD_ENV: &str = "HARN_PROVIDER_TOOL_SCORECARD_PAYLOAD_JSON";
const TOOL_SCORECARD_PAYLOAD_PRETTY_ENV: &str = "HARN_PROVIDER_TOOL_SCORECARD_PAYLOAD_PRETTY";
const CACHE_PROBE_PAYLOAD_ENV: &str = "HARN_PROVIDER_CACHE_PROBE_PAYLOAD_JSON";
const CACHE_PROBE_PAYLOAD_PRETTY_ENV: &str = "HARN_PROVIDER_CACHE_PROBE_PAYLOAD_PRETTY";
#[derive(Debug, Serialize)]
struct ProviderProbe {
provider: String,
base_url: Option<String>,
readiness: ProviderReadiness,
#[serde(skip_serializing_if = "Option::is_none")]
runtime_profile: Option<harn_vm::llm::local_profiles::LocalRuntimeProfileReport>,
#[serde(skip_serializing_if = "Vec::is_empty")]
loaded_models: Vec<LoadedModel>,
}
const PROBE_REPORT_DISPATCH: ProviderReportDispatch = ProviderReportDispatch {
script_name: "providers/probe",
payload_name: "provider probe",
payload_env: PROBE_PAYLOAD_ENV,
pretty_env: PROBE_PAYLOAD_PRETTY_ENV,
};
const TOOL_PROBE_REPORT_DISPATCH: ProviderReportDispatch = ProviderReportDispatch {
script_name: "providers/tool_probe",
payload_name: "tool-probe",
payload_env: TOOL_PROBE_PAYLOAD_ENV,
pretty_env: TOOL_PROBE_PAYLOAD_PRETTY_ENV,
};
const TOOL_SCORECARD_REPORT_DISPATCH: ProviderReportDispatch = ProviderReportDispatch {
script_name: "providers/tool_scorecard",
payload_name: "tool-scorecard",
payload_env: TOOL_SCORECARD_PAYLOAD_ENV,
pretty_env: TOOL_SCORECARD_PAYLOAD_PRETTY_ENV,
};
const CACHE_PROBE_REPORT_DISPATCH: ProviderReportDispatch = ProviderReportDispatch {
script_name: "providers/cache_probe",
payload_name: "cache-probe",
payload_env: CACHE_PROBE_PAYLOAD_ENV,
pretty_env: CACHE_PROBE_PAYLOAD_PRETTY_ENV,
};
pub(crate) async fn run_provider_probe(args: ProviderProbeArgs) {
let exit_code = dispatch_provider_probe(args).await;
if exit_code != 0 {
process::exit(exit_code);
}
}
async fn dispatch_provider_probe(args: ProviderProbeArgs) -> i32 {
let probe = aggregate_provider_probe(&args).await;
let exit_code = i32::from(!probe.readiness.ok);
let render_exit = dispatch_provider_report(PROBE_REPORT_DISPATCH, args.json, &probe).await;
if render_exit != 0 {
render_exit
} else {
exit_code
}
}
async fn aggregate_provider_probe(args: &ProviderProbeArgs) -> ProviderProbe {
let readiness = probe_provider_readiness(
&args.provider,
args.model.as_deref(),
args.base_url.as_deref(),
)
.await;
let base_url = readiness.base_url.clone().or_else(|| {
llm_config::provider_config(&args.provider).map(|def| llm_config::resolve_base_url(&def))
});
let loaded_models = if args.provider == "ollama" {
let base = base_url
.clone()
.unwrap_or_else(|| "http://localhost:11434".to_string());
match fetch_ollama_ps(&base).await {
Ok(entries) => entries,
Err(error) => {
eprintln!("warning: /api/ps unavailable: {error}");
Vec::new()
}
}
} else {
Vec::new()
};
ProviderProbe {
provider: args.provider.clone(),
base_url,
readiness,
runtime_profile: if LOCAL_PROVIDERS.contains(&args.provider.as_str()) {
args.model.as_deref().map(|model| {
harn_vm::llm::local_profiles::local_runtime_profile_report(
model,
Some(&args.provider),
)
})
} else {
None
},
loaded_models,
}
}
pub(crate) async fn run_provider_tool_probe(args: ProviderToolProbeArgs) {
let exit_code = dispatch_provider_tool_probe(args).await;
if exit_code != 0 {
process::exit(exit_code);
}
}
async fn dispatch_provider_tool_probe(args: ProviderToolProbeArgs) -> i32 {
let report = match aggregate_tool_conformance_report(&args).await {
Ok(report) => report,
Err(error) => {
eprintln!("{error}");
return 1;
}
};
let fallback_disabled = report.tool_calling.fallback_mode
== harn_vm::llm::tool_conformance::ToolProbeFallbackMode::Disabled;
let render_exit =
dispatch_provider_report(TOOL_PROBE_REPORT_DISPATCH, args.json, &report).await;
if render_exit != 0 {
return render_exit;
}
i32::from(fallback_disabled)
}
async fn aggregate_tool_conformance_report(
args: &ProviderToolProbeArgs,
) -> Result<harn_vm::llm::tool_conformance::ToolConformanceReport, String> {
if let Some(path) = args.response_fixture.as_ref() {
if args.repeat != 1 {
return Err(
"error: --repeat is only supported for live provider tool probes; \
--response-fixture classifies one saved response"
.to_string(),
);
}
let raw = std::fs::read_to_string(path)
.map_err(|error| format!("error: failed to read {}: {error}", path.display()))?;
Ok(
harn_vm::llm::tool_conformance::classify_tool_conformance_fixture(
args.provider.clone(),
args.model.clone(),
modes_for_arg(args.mode)
.into_iter()
.next()
.unwrap_or(harn_vm::llm::tool_conformance::ToolProbeMode::NonStreaming),
args.marker.clone(),
&raw,
),
)
} else {
let mut options = harn_vm::llm::tool_conformance::ToolConformanceProbeOptions::new(
args.provider.clone(),
args.model.clone(),
);
options.base_url = args.base_url.clone();
options.modes = modes_for_arg(args.mode);
options.marker = args.marker.clone();
options.repeat = usize::from(args.repeat);
options.timeout_secs = args.timeout_secs;
Ok(harn_vm::llm::tool_conformance::run_tool_conformance_probe(options).await)
}
}
pub(crate) async fn run_provider_tool_scorecard(args: ProviderToolScorecardArgs) {
let exit_code = dispatch_provider_tool_scorecard(args).await;
if exit_code != 0 {
process::exit(exit_code);
}
}
async fn dispatch_provider_tool_scorecard(args: ProviderToolScorecardArgs) -> i32 {
let report = match aggregate_tool_scorecard(&args) {
Ok(report) => report,
Err(error) => {
eprintln!("{error}");
return 1;
}
};
dispatch_provider_report(TOOL_SCORECARD_REPORT_DISPATCH, args.json, &report).await
}
fn aggregate_tool_scorecard(
args: &ProviderToolScorecardArgs,
) -> Result<harn_vm::llm::tool_scorecard::ToolScorecardReport, String> {
let mut reports = Vec::new();
for path in &args.tool_probe_reports {
let raw = std::fs::read_to_string(path)
.map_err(|error| format!("error: failed to read {}: {error}", path.display()))?;
let report =
serde_json::from_str::<harn_vm::llm::tool_conformance::ToolConformanceReport>(&raw)
.map_err(|error| {
format!(
"error: failed to parse tool-probe report {}: {error}",
path.display()
)
})?;
if report.schema_version != harn_vm::llm::tool_conformance::TOOL_CONFORMANCE_SCHEMA_VERSION
{
return Err(format!(
"error: unsupported tool-probe report schema_version {} in {}; expected {}",
report.schema_version,
path.display(),
harn_vm::llm::tool_conformance::TOOL_CONFORMANCE_SCHEMA_VERSION
));
}
reports.push(report);
}
Ok(harn_vm::llm::tool_scorecard::scorecard_from_tool_reports(
reports,
))
}
pub(crate) async fn run_provider_cache_probe(args: ProviderCacheProbeArgs) {
let exit_code = dispatch_provider_cache_probe(args).await;
if exit_code != 0 {
process::exit(exit_code);
}
}
async fn dispatch_provider_cache_probe(args: ProviderCacheProbeArgs) -> i32 {
let Some(fixture_path) = args.usage_fixture.as_ref() else {
eprintln!(
"error: live prompt-cache probing is not yet wired; pass --usage-fixture <path> \
with a saved repeat-run usage fixture"
);
return 2;
};
let raw = match std::fs::read_to_string(fixture_path) {
Ok(raw) => raw,
Err(error) => {
eprintln!("error: failed to read {}: {error}", fixture_path.display());
return 1;
}
};
let report = match harn_vm::llm::cache_conformance::classify_cache_conformance_fixture(
args.provider.clone(),
args.model.clone(),
&raw,
) {
Ok(report) => report,
Err(error) => {
eprintln!("error: {error}");
return 1;
}
};
let dogfood_failure = report.dogfood_failure;
let render_exit =
dispatch_provider_report(CACHE_PROBE_REPORT_DISPATCH, args.json, &report).await;
if render_exit != 0 {
return render_exit;
}
i32::from(dogfood_failure)
}
fn modes_for_arg(
mode: ProviderToolProbeModeArg,
) -> Vec<harn_vm::llm::tool_conformance::ToolProbeMode> {
use harn_vm::llm::tool_conformance::ToolProbeMode;
match mode {
ProviderToolProbeModeArg::Both => {
vec![ToolProbeMode::NonStreaming, ToolProbeMode::Streaming]
}
ProviderToolProbeModeArg::NonStreaming => vec![ToolProbeMode::NonStreaming],
ProviderToolProbeModeArg::Streaming => vec![ToolProbeMode::Streaming],
}
}