Skip to content

Commit 9b134f4

Browse files
committed
refactor: split doctor run helpers
Break the doctor command into endpoint discovery, recommendation handoff, and isolated test helpers so the runtime flow stays small and readable. Made-with: Cursor
1 parent b882be9 commit 9b134f4

6 files changed

Lines changed: 130 additions & 97 deletions

File tree

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
- [x] `src/commands/doctor/endpoint/inference.rs`: split request building, HTTP execution/error handling, and response parsing.
7070
- [x] `src/commands/feedback_eval/report/build/stats.rs`: split threshold confusion-matrix scoring from bucket primitives.
7171
- [x] `src/commands/doctor/command/display.rs`: separate header/config output, endpoint listing, and inference result rendering.
72-
- [ ] `src/commands/doctor/command/run.rs`: separate endpoint discovery, recommendation flow, and test helpers.
72+
- [x] `src/commands/doctor/command/run.rs`: separate endpoint discovery, recommendation flow, and test helpers.
7373
- [ ] `src/commands/eval/runner/matching.rs`: split required-match search, unexpected-match detection, and rule metric assembly.
7474
- [ ] `src/commands/eval/runner/execute/loading.rs`: separate diff resolution from repo-path resolution if it grows again.
7575
- [ ] `src/commands/feedback_eval/report/examples.rs`: split ranking helpers from example builders.

src/commands/doctor/command/run.rs

Lines changed: 9 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,11 @@
1-
use anyhow::Result;
2-
use reqwest::Client;
3-
use std::time::Duration;
4-
5-
use crate::config::Config;
6-
7-
use super::super::system::check_system_resources;
8-
use super::display::{print_configuration, print_endpoint_models, print_header, print_unreachable};
9-
use super::probe::probe_endpoint;
10-
use super::recommend::inspect_recommended_model;
11-
12-
pub async fn doctor_command(config: Config) -> Result<()> {
13-
print_header();
14-
check_system_resources();
15-
print_configuration(&config);
16-
17-
let base_url = config
18-
.base_url
19-
.clone()
20-
.unwrap_or_else(|| "http://localhost:11434".to_string());
21-
22-
print!("Checking endpoint {}... ", base_url);
23-
let client = Client::builder().timeout(Duration::from_secs(5)).build()?;
24-
let Some(endpoint) = probe_endpoint(&client, &base_url).await? else {
25-
return print_unreachable(&base_url);
26-
};
27-
28-
println!("reachable ({})", endpoint.reachable_label);
29-
print_endpoint_models(endpoint.endpoint_type, &endpoint.models);
30-
inspect_recommended_model(&config, &base_url, &endpoint).await?;
31-
Ok(())
32-
}
33-
1+
#[path = "run/command.rs"]
2+
mod command;
3+
#[path = "run/endpoint.rs"]
4+
mod endpoint;
5+
#[path = "run/recommendation.rs"]
6+
mod recommendation;
347
#[cfg(test)]
35-
mod tests {
36-
use super::*;
37-
use serde_json::Value;
38-
39-
#[test]
40-
fn doctor_config_defaults() {
41-
let config = Config::default();
42-
assert!(config.adapter.is_none());
43-
assert!(config.context_window.is_none());
44-
}
45-
46-
#[test]
47-
fn test_detect_context_window_from_parameters() {
48-
let json =
49-
r#"{"parameters":"stop [INST]\nstop [/INST]\nnum_ctx 4096\nrepeat_penalty 1.1"}"#;
50-
let value: Value = serde_json::from_str(json).unwrap();
51-
assert_eq!(parse_context_window(&value), Some(4096));
52-
}
53-
54-
#[test]
55-
fn test_detect_context_window_from_model_info() {
56-
let json = r#"{"model_info":{"llama.context_length":8192}}"#;
57-
let value: Value = serde_json::from_str(json).unwrap();
58-
assert_eq!(parse_context_window(&value), Some(8192));
59-
}
60-
61-
#[test]
62-
fn test_detect_context_window_no_data() {
63-
let json = r#"{"license":"MIT","modelfile":"..."}"#;
64-
let value: Value = serde_json::from_str(json).unwrap();
65-
assert_eq!(parse_context_window(&value), None);
66-
}
67-
68-
fn parse_context_window(value: &Value) -> Option<usize> {
69-
if let Some(params) = value
70-
.get("parameters")
71-
.and_then(|parameters| parameters.as_str())
72-
{
73-
for line in params.lines() {
74-
let trimmed = line.trim();
75-
if trimmed.starts_with("num_ctx") {
76-
if let Some(raw_value) = trimmed.split_whitespace().nth(1) {
77-
if let Ok(parsed) = raw_value.parse() {
78-
return Some(parsed);
79-
}
80-
}
81-
}
82-
}
83-
}
84-
85-
let info = value.get("model_info")?;
86-
for key in &[
87-
"context_length",
88-
"llama.context_length",
89-
"general.context_length",
90-
] {
91-
if let Some(ctx) = info.get(*key).and_then(|value| value.as_u64()) {
92-
return Some(ctx as usize);
93-
}
94-
}
8+
#[path = "run/tests.rs"]
9+
mod tests;
9510

96-
None
97-
}
98-
}
11+
pub use command::doctor_command;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use anyhow::Result;
2+
3+
use crate::config::Config;
4+
5+
use super::super::super::system::check_system_resources;
6+
use super::super::display::{print_configuration, print_header, print_unreachable};
7+
use super::endpoint::{configured_base_url, discover_endpoint};
8+
use super::recommendation::run_recommendation_flow;
9+
10+
pub async fn doctor_command(config: Config) -> Result<()> {
11+
print_header();
12+
check_system_resources();
13+
print_configuration(&config);
14+
15+
let base_url = configured_base_url(&config);
16+
17+
print!("Checking endpoint {}... ", base_url);
18+
let Some(endpoint) = discover_endpoint(&base_url).await? else {
19+
return print_unreachable(&base_url);
20+
};
21+
22+
run_recommendation_flow(&config, &base_url, &endpoint).await
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use anyhow::Result;
2+
use reqwest::Client;
3+
use std::time::Duration;
4+
5+
use crate::config::Config;
6+
7+
use super::super::probe::{probe_endpoint, EndpointProbe};
8+
9+
pub(in super::super) fn configured_base_url(config: &Config) -> String {
10+
config
11+
.base_url
12+
.clone()
13+
.unwrap_or_else(|| "http://localhost:11434".to_string())
14+
}
15+
16+
pub(in super::super) async fn discover_endpoint(base_url: &str) -> Result<Option<EndpointProbe>> {
17+
let client = Client::builder().timeout(Duration::from_secs(5)).build()?;
18+
probe_endpoint(&client, base_url).await
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use anyhow::Result;
2+
3+
use crate::config::Config;
4+
5+
use super::super::display::print_endpoint_models;
6+
use super::super::probe::EndpointProbe;
7+
use super::super::recommend::inspect_recommended_model;
8+
9+
pub(in super::super) async fn run_recommendation_flow(
10+
config: &Config,
11+
base_url: &str,
12+
endpoint: &EndpointProbe,
13+
) -> Result<()> {
14+
println!("reachable ({})", endpoint.reachable_label);
15+
print_endpoint_models(endpoint.endpoint_type, &endpoint.models);
16+
inspect_recommended_model(config, base_url, endpoint).await
17+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::config::Config;
2+
use serde_json::Value;
3+
4+
#[test]
5+
fn doctor_config_defaults() {
6+
let config = Config::default();
7+
assert!(config.adapter.is_none());
8+
assert!(config.context_window.is_none());
9+
}
10+
11+
#[test]
12+
fn test_detect_context_window_from_parameters() {
13+
let json = r#"{"parameters":"stop [INST]\nstop [/INST]\nnum_ctx 4096\nrepeat_penalty 1.1"}"#;
14+
let value: Value = serde_json::from_str(json).unwrap();
15+
assert_eq!(parse_context_window(&value), Some(4096));
16+
}
17+
18+
#[test]
19+
fn test_detect_context_window_from_model_info() {
20+
let json = r#"{"model_info":{"llama.context_length":8192}}"#;
21+
let value: Value = serde_json::from_str(json).unwrap();
22+
assert_eq!(parse_context_window(&value), Some(8192));
23+
}
24+
25+
#[test]
26+
fn test_detect_context_window_no_data() {
27+
let json = r#"{"license":"MIT","modelfile":"..."}"#;
28+
let value: Value = serde_json::from_str(json).unwrap();
29+
assert_eq!(parse_context_window(&value), None);
30+
}
31+
32+
fn parse_context_window(value: &Value) -> Option<usize> {
33+
if let Some(params) = value
34+
.get("parameters")
35+
.and_then(|parameters| parameters.as_str())
36+
{
37+
for line in params.lines() {
38+
let trimmed = line.trim();
39+
if trimmed.starts_with("num_ctx") {
40+
if let Some(raw_value) = trimmed.split_whitespace().nth(1) {
41+
if let Ok(parsed) = raw_value.parse() {
42+
return Some(parsed);
43+
}
44+
}
45+
}
46+
}
47+
}
48+
49+
let info = value.get("model_info")?;
50+
for key in &[
51+
"context_length",
52+
"llama.context_length",
53+
"general.context_length",
54+
] {
55+
if let Some(ctx) = info.get(*key).and_then(|value| value.as_u64()) {
56+
return Some(ctx as usize);
57+
}
58+
}
59+
60+
None
61+
}

0 commit comments

Comments
 (0)