use std::path::{Path, PathBuf};
use serde_json::Value;
use super::ollama_tool_call_classifier::{
classify_streaming_tool_call, classify_tool_call_schema, classify_tool_name_allowlist,
StreamingToolCallOutcome, ToolCallSchemaOutcome, ToolNameAllowlistOutcome,
};
use crate::error::{CliError, Result};
pub(crate) fn run(
response_file: &Path,
request_file: Option<&Path>,
stream: bool,
json: bool,
) -> Result<()> {
if !response_file.exists() {
return Err(CliError::FileNotFound(PathBuf::from(response_file)));
}
let body = std::fs::read_to_string(response_file)?;
let declared: Vec<String> = match request_file {
Some(p) => load_declared_tool_names(p)?,
None => Vec::new(),
};
if stream {
run_stream(&body, response_file, json)
} else {
run_non_streaming(&body, response_file, &declared, json)
}
}
fn run_non_streaming(body: &str, path: &Path, declared: &[String], json: bool) -> Result<()> {
let response: Value = serde_json::from_str(body).map_err(|e| {
CliError::InvalidFormat(format!(
"apr ollama-tools-lint: failed to parse JSON from {}: {e}",
path.display()
))
})?;
let schema = classify_tool_call_schema(&response);
let declared_refs: Vec<&str> = declared.iter().map(String::as_str).collect();
let allowlist = classify_tool_name_allowlist(&response, &declared_refs);
print_non_streaming_report(path, &schema, &allowlist, declared, json);
match (&schema, &allowlist) {
(ToolCallSchemaOutcome::Ok, ToolNameAllowlistOutcome::Ok) => Ok(()),
(ToolCallSchemaOutcome::Ok, bad) => Err(CliError::ValidationFailed(format!(
"tool-name allowlist gate rejected response: {bad:?}"
))),
(bad, _) => Err(CliError::ValidationFailed(format!(
"tool-call schema gate rejected response: {bad:?}"
))),
}
}
fn run_stream(body: &str, path: &Path, json: bool) -> Result<()> {
let mut frames: Vec<Value> = Vec::new();
for (idx, line) in body.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let frame: Value = serde_json::from_str(trimmed).map_err(|e| {
CliError::InvalidFormat(format!(
"apr ollama-tools-lint: invalid NDJSON at line {} of {}: {e}",
idx + 1,
path.display()
))
})?;
frames.push(frame);
}
let outcome = classify_streaming_tool_call(&frames);
print_stream_report(path, frames.len(), &outcome, json);
match outcome {
StreamingToolCallOutcome::Ok => Ok(()),
bad => Err(CliError::ValidationFailed(format!(
"NDJSON tool-call stream gate rejected frames: {bad:?}"
))),
}
}
fn load_declared_tool_names(path: &Path) -> Result<Vec<String>> {
if !path.exists() {
return Err(CliError::FileNotFound(PathBuf::from(path)));
}
let body = std::fs::read_to_string(path)?;
let req: Value = serde_json::from_str(&body).map_err(|e| {
CliError::InvalidFormat(format!(
"apr ollama-tools-lint: failed to parse request JSON from {}: {e}",
path.display()
))
})?;
let names = req
.get("tools")
.and_then(Value::as_array)
.map(|tools| {
tools
.iter()
.filter_map(|t| {
t.get("function")
.and_then(|f| f.get("name"))
.and_then(Value::as_str)
.map(String::from)
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
Ok(names)
}
fn print_non_streaming_report(
path: &Path,
schema: &ToolCallSchemaOutcome,
allowlist: &ToolNameAllowlistOutcome,
declared: &[String],
json: bool,
) {
if json {
let v = serde_json::json!({
"mode": "non_streaming",
"response_path": path.display().to_string(),
"declared_tool_names": declared,
"schema_outcome": format!("{:?}", schema),
"allowlist_outcome": format!("{:?}", allowlist),
"schema_ok": matches!(schema, ToolCallSchemaOutcome::Ok),
"allowlist_ok": matches!(allowlist, ToolNameAllowlistOutcome::Ok),
});
println!(
"{}",
serde_json::to_string_pretty(&v).unwrap_or_else(|_| v.to_string())
);
} else {
println!("ollama-tools-lint (non-streaming) for {}", path.display());
println!(" declared_tools: {declared:?}");
println!(" schema_outcome: {schema:?}");
println!(" allowlist_outcome: {allowlist:?}");
}
}
fn print_stream_report(
path: &Path,
frame_count: usize,
outcome: &StreamingToolCallOutcome,
json: bool,
) {
if json {
let v = serde_json::json!({
"mode": "streaming_ndjson",
"response_path": path.display().to_string(),
"num_frames": frame_count,
"ndjson_outcome": format!("{:?}", outcome),
"ndjson_ok": matches!(outcome, StreamingToolCallOutcome::Ok),
});
println!(
"{}",
serde_json::to_string_pretty(&v).unwrap_or_else(|_| v.to_string())
);
} else {
println!(
"ollama-tools-lint (streaming NDJSON) for {}",
path.display()
);
println!(" num_frames: {frame_count}");
println!(" ndjson_outcome: {outcome:?}");
}
}