mirror of
https://github.com/AlexsJones/llmfit.git
synced 2026-10-19 15:33:24 +00:00
fix(bench): identify Ferrum and vLLM by endpoint owner (#994)
* fix(bench): identify Ferrum and vLLM by endpoint owner * fix(bench): normalize equivalent loopback endpoints * fix(bench): reject malformed endpoint ports * fix(bench): consume complete fixture HTTP requests Read headers and the Content-Length body before sending the fixture response, avoiding Windows connection aborts when a POST arrives in multiple TCP reads. Cover short reads, bodies larger than the old buffer, and truncated requests while retaining the Ferrum provider attribution assertions.
This commit is contained in:
parent
856db58855
commit
b3e09fd2d8
9 changed files with 662 additions and 126 deletions
|
|
@ -363,6 +363,9 @@ llmfit bench --provider ollama --url http://my-server:11434 llama3.2
|
|||
# vLLM エンドポイントを上書き
|
||||
llmfit bench --provider vllm --url http://localhost:8000
|
||||
|
||||
# Ferrum をベンチマーク(FERRUM_HOST または http://localhost:8000 を使用)
|
||||
llmfit bench --provider ferrum
|
||||
|
||||
# JSON として出力(スクリプト用)
|
||||
llmfit bench --json
|
||||
|
||||
|
|
@ -379,6 +382,7 @@ llmfit bench --quality --routing
|
|||
|---|---|---|
|
||||
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama API のベース URL |
|
||||
| `VLLM_PORT` | `8000` | vLLM サーバーのポート(`http://localhost:$VLLM_PORT` として使用) |
|
||||
| `FERRUM_HOST` | `http://localhost:8000` | Ferrum API のベース URL |
|
||||
|
||||
### テーマ
|
||||
|
||||
|
|
|
|||
14
docs/cli.md
14
docs/cli.md
|
|
@ -98,12 +98,14 @@ python3 scripts/test_api.py --base-url http://127.0.0.1:8787
|
|||
### Contributing benchmarks (`bench --share`)
|
||||
|
||||
`llmfit bench` measures inference performance against a running provider
|
||||
(Ollama, vLLM, MLX, or llama-server). llama-server is auto-detected on port
|
||||
8080 via its `/props` endpoint (override with `LLAMA_SERVER_HOST` for a full
|
||||
URL, or `LLAMA_SERVER_PORT`), or select it explicitly with
|
||||
`--provider llamacpp`. Add `--share` to contribute your results back to the
|
||||
project as a pull request — **no `gh` CLI and no account on a third-party
|
||||
service required**:
|
||||
(Ollama, vLLM, Ferrum, MLX, or llama-server). vLLM and Ferrum are distinguished
|
||||
by the `owned_by` identity in `/v1/models`; set `FERRUM_HOST` to override
|
||||
Ferrum's default `http://localhost:8000` endpoint. llama-server is
|
||||
auto-detected on port 8080 via its `/props` endpoint (override with
|
||||
`LLAMA_SERVER_HOST` for a full URL, or `LLAMA_SERVER_PORT`), or select it
|
||||
explicitly with `--provider llamacpp`. Add `--share` to contribute your results
|
||||
back to the project as a pull request — **no `gh` CLI and no account on a
|
||||
third-party service required**:
|
||||
|
||||
```sh
|
||||
# Benchmark every discovered model and open a PR with the results
|
||||
|
|
|
|||
|
|
@ -266,6 +266,9 @@ llmfit bench --provider ollama --url http://my-server:11434 llama3.2
|
|||
# Override vLLM endpoint
|
||||
llmfit bench --provider vllm --url http://localhost:8000
|
||||
|
||||
# Benchmark Ferrum (uses FERRUM_HOST or http://localhost:8000)
|
||||
llmfit bench --provider ferrum
|
||||
|
||||
# Output as JSON (for scripting)
|
||||
llmfit bench --json
|
||||
|
||||
|
|
@ -282,6 +285,7 @@ llmfit bench --quality --routing
|
|||
|---|---|---|
|
||||
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama API base URL |
|
||||
| `VLLM_PORT` | `8000` | vLLM server port (used as `http://localhost:$VLLM_PORT`) |
|
||||
| `FERRUM_HOST` | `http://localhost:8000` | Ferrum API base URL |
|
||||
|
||||
### Themes
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"required": ["model", "provider", "numRuns", "avgTps", "minTps", "maxTps", "avgTotalMs", "avgOutputTokens"],
|
||||
"properties": {
|
||||
"model": { "type": "string", "minLength": 1 },
|
||||
"provider": { "type": "string", "enum": ["ollama", "vllm", "mlx", "llamacpp"] },
|
||||
"provider": { "type": "string", "enum": ["ollama", "vllm", "ferrum", "mlx", "llamacpp"] },
|
||||
"numRuns": { "type": "integer", "minimum": 1 },
|
||||
"avgTps": { "type": "number", "minimum": 0, "maximum": 100000 },
|
||||
"minTps": { "type": "number", "minimum": 0, "maximum": 100000 },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
//! LLM inference benchmarking against Ollama, vLLM, and MLX endpoints.
|
||||
//! LLM inference benchmarking against Ollama and OpenAI-compatible endpoints.
|
||||
//!
|
||||
//! Measures time-to-first-token (TTFT), tokens per second (TPS),
|
||||
//! and total latency using real inference requests.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::providers::{OpenAiEndpointIdentity, fetch_openai_model_list, openai_model_ids};
|
||||
|
||||
/// Results from a single benchmark run.
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct BenchRun {
|
||||
|
|
@ -256,7 +258,7 @@ pub(crate) struct ChatUsage {
|
|||
pub(crate) completion_tokens: u32,
|
||||
}
|
||||
|
||||
/// Benchmark a model via OpenAI-compatible /v1/chat/completions (vLLM, MLX).
|
||||
/// Benchmark a model via OpenAI-compatible /v1/chat/completions.
|
||||
pub fn bench_openai_compat(
|
||||
base_url: &str,
|
||||
model: &str,
|
||||
|
|
@ -347,14 +349,48 @@ fn openai_chat(url: &str, model: &str, prompt: &str, max_tokens: u32) -> Result<
|
|||
// ── Auto-detect and benchmark ──────────────────────────────────────
|
||||
|
||||
/// Which provider to benchmark against.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BenchTarget {
|
||||
Ollama { url: String, model: String },
|
||||
VLlm { url: String, model: String },
|
||||
Ferrum { url: String, model: String },
|
||||
Mlx { url: String, model: String },
|
||||
LlamaCpp { url: String, model: String },
|
||||
}
|
||||
|
||||
/// Benchmark a discovered target while preserving its provider attribution.
|
||||
pub fn benchmark_target(
|
||||
target: &BenchTarget,
|
||||
num_runs: usize,
|
||||
on_progress: &dyn Fn(usize, usize),
|
||||
) -> Result<BenchResult, String> {
|
||||
match target {
|
||||
BenchTarget::Ollama { url, model } => bench_ollama(url, model, num_runs, on_progress),
|
||||
BenchTarget::VLlm { url, model } => {
|
||||
bench_openai_compat(url, model, "vllm", num_runs, on_progress)
|
||||
}
|
||||
BenchTarget::Ferrum { url, model } => {
|
||||
bench_openai_compat(url, model, "ferrum", num_runs, on_progress)
|
||||
}
|
||||
BenchTarget::Mlx { url, model } => {
|
||||
bench_openai_compat(url, model, "mlx", num_runs, on_progress)
|
||||
}
|
||||
BenchTarget::LlamaCpp { url, model } => {
|
||||
bench_openai_compat(url, model, "llamacpp", num_runs, on_progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base URL for a running Ferrum server.
|
||||
/// `FERRUM_HOST` is a full URL and defaults to Ferrum's default listen port.
|
||||
pub fn ferrum_url() -> String {
|
||||
std::env::var("FERRUM_HOST")
|
||||
.ok()
|
||||
.filter(|host| !host.trim().is_empty())
|
||||
.map(|host| host.trim().trim_end_matches('/').to_string())
|
||||
.unwrap_or_else(|| "http://localhost:8000".to_string())
|
||||
}
|
||||
|
||||
/// Base URL for a running llama-server instance.
|
||||
/// `LLAMA_SERVER_HOST` (full URL) wins; otherwise localhost with
|
||||
/// `LLAMA_SERVER_PORT` (default 8080, llama-server's own default).
|
||||
|
|
@ -380,16 +416,165 @@ pub fn probe_llamacpp(base_url: &str) -> bool {
|
|||
.is_ok()
|
||||
}
|
||||
|
||||
fn vllm_url() -> String {
|
||||
let port = std::env::var("VLLM_PORT").unwrap_or_else(|_| "8000".to_string());
|
||||
format!("http://localhost:{}", port)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct EndpointKey {
|
||||
scheme: String,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
path: String,
|
||||
query: Option<String>,
|
||||
}
|
||||
|
||||
/// Build a comparison-only endpoint identity without resolving DNS. Loopback
|
||||
/// spellings are equivalent; schemes, non-default ports, paths, and queries
|
||||
/// remain significant.
|
||||
fn endpoint_key(raw_url: &str) -> Option<EndpointKey> {
|
||||
let uri = raw_url.trim().parse::<http::Uri>().ok()?;
|
||||
let scheme = uri.scheme_str()?.to_ascii_lowercase();
|
||||
let authority = uri.authority()?;
|
||||
// `http::Uri` accepts userinfo and syntactically present ports that do not
|
||||
// fit in a u16. Keep those URLs distinct rather than canonicalizing away
|
||||
// authentication or treating an invalid port as the scheme default.
|
||||
let authority_text = authority.as_str();
|
||||
if authority_text.contains('@') {
|
||||
return None;
|
||||
}
|
||||
let raw_host = authority.host();
|
||||
let port = match authority_text.strip_prefix(raw_host)? {
|
||||
"" => match scheme.as_str() {
|
||||
"http" => Some(80),
|
||||
"https" => Some(443),
|
||||
_ => None,
|
||||
},
|
||||
suffix => Some(suffix.strip_prefix(':')?.parse::<u16>().ok()?),
|
||||
};
|
||||
let host = raw_host.trim_matches(['[', ']']);
|
||||
let is_loopback = host.eq_ignore_ascii_case("localhost")
|
||||
|| host
|
||||
.parse::<std::net::IpAddr>()
|
||||
.ok()
|
||||
.is_some_and(|address| address.is_loopback());
|
||||
let host = if is_loopback {
|
||||
"loopback".to_string()
|
||||
} else {
|
||||
host.to_ascii_lowercase()
|
||||
};
|
||||
let path = uri.path().trim_end_matches('/');
|
||||
let path = if path.is_empty() { "/" } else { path }.to_string();
|
||||
|
||||
Some(EndpointKey {
|
||||
scheme,
|
||||
host,
|
||||
port,
|
||||
path,
|
||||
query: uri.query().map(str::to_owned),
|
||||
})
|
||||
}
|
||||
|
||||
fn endpoint_urls_match(left: &str, right: &str) -> bool {
|
||||
match (endpoint_key(left), endpoint_key(right)) {
|
||||
(Some(left), Some(right)) => left == right,
|
||||
_ => left.trim().trim_end_matches('/') == right.trim().trim_end_matches('/'),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_endpoint_url(urls: &mut Vec<String>, candidate: String) {
|
||||
if !urls
|
||||
.iter()
|
||||
.any(|existing| endpoint_urls_match(existing, &candidate))
|
||||
{
|
||||
urls.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_bench_urls() -> Vec<String> {
|
||||
let mut urls = vec![vllm_url()];
|
||||
push_unique_endpoint_url(&mut urls, ferrum_url());
|
||||
urls
|
||||
}
|
||||
|
||||
fn identified_openai_models(
|
||||
base_url: &str,
|
||||
timeout: Duration,
|
||||
) -> Result<(OpenAiEndpointIdentity, Vec<String>), String> {
|
||||
let (list, identity) = fetch_openai_model_list(base_url, timeout)
|
||||
.ok_or_else(|| format!("Cannot read {}/v1/models", base_url.trim_end_matches('/')))?;
|
||||
let models = openai_model_ids(&list).map(str::to_owned).collect();
|
||||
Ok((identity, models))
|
||||
}
|
||||
|
||||
fn choose_model(models: &[String], hint: Option<&str>) -> Result<String, String> {
|
||||
if models.is_empty() {
|
||||
return Err("No models loaded".to_string());
|
||||
}
|
||||
|
||||
if let Some(hint) = hint {
|
||||
let hint_lower = hint.to_lowercase();
|
||||
if let Some(model) = models
|
||||
.iter()
|
||||
.find(|model| model.to_lowercase().contains(&hint_lower))
|
||||
{
|
||||
return Ok(model.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(models[0].clone())
|
||||
}
|
||||
|
||||
fn target_for_identity(
|
||||
identity: OpenAiEndpointIdentity,
|
||||
url: String,
|
||||
model: String,
|
||||
) -> Option<BenchTarget> {
|
||||
match identity {
|
||||
OpenAiEndpointIdentity::Vllm => Some(BenchTarget::VLlm { url, model }),
|
||||
OpenAiEndpointIdentity::Ferrum => Some(BenchTarget::Ferrum { url, model }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_detect_openai_target(urls: &[String], hint: Option<&str>) -> Option<BenchTarget> {
|
||||
urls.iter().find_map(|url| {
|
||||
let (identity, models) = identified_openai_models(url, Duration::from_secs(5)).ok()?;
|
||||
let model = choose_model(&models, hint).ok()?;
|
||||
target_for_identity(identity, url.clone(), model)
|
||||
})
|
||||
}
|
||||
|
||||
fn discover_openai_targets(urls: &[String]) -> Vec<BenchTarget> {
|
||||
let mut targets = Vec::new();
|
||||
let mut probed_urls: Vec<&str> = Vec::new();
|
||||
for url in urls {
|
||||
if probed_urls
|
||||
.iter()
|
||||
.any(|probed| endpoint_urls_match(probed, url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
probed_urls.push(url);
|
||||
let Ok((identity, models)) = identified_openai_models(url, Duration::from_secs(3)) else {
|
||||
continue;
|
||||
};
|
||||
for model in models {
|
||||
if let Some(target) = target_for_identity(identity, url.clone(), model) {
|
||||
targets.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
/// Auto-detect available providers and pick the best one to benchmark.
|
||||
pub fn auto_detect_target(model_hint: Option<&str>) -> Result<BenchTarget, String> {
|
||||
// Check vLLM via VLLM_PORT env var (defaults to 8000)
|
||||
let vllm_port = std::env::var("VLLM_PORT").unwrap_or_else(|_| "8000".to_string());
|
||||
let vllm_url = format!("http://localhost:{}", vllm_port);
|
||||
if let Ok(model_name) = detect_vllm_model(&vllm_url, model_hint) {
|
||||
return Ok(BenchTarget::VLlm {
|
||||
url: vllm_url,
|
||||
model: model_name,
|
||||
});
|
||||
// vLLM and Ferrum share port 8000 by default, so classify the endpoint
|
||||
// from positive `owned_by` evidence instead of assigning it by port.
|
||||
if let Some(target) = auto_detect_openai_target(&openai_bench_urls(), model_hint) {
|
||||
return Ok(target);
|
||||
}
|
||||
|
||||
// Check Ollama
|
||||
|
|
@ -438,45 +623,58 @@ pub fn auto_detect_target(model_hint: Option<&str>) -> Result<BenchTarget, Strin
|
|||
});
|
||||
}
|
||||
|
||||
Err("No inference provider found. Start Ollama, vLLM, MLX, or llama-server first.".to_string())
|
||||
Err(
|
||||
"No inference provider found. Start Ollama, vLLM, Ferrum, MLX, or llama-server first."
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Discover all available models across all providers.
|
||||
pub fn discover_all_targets() -> Vec<BenchTarget> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
// Check vLLM via VLLM_PORT env var (defaults to 8000)
|
||||
let vllm_port = std::env::var("VLLM_PORT").unwrap_or_else(|_| "8000".to_string());
|
||||
let vllm_url = format!("http://localhost:{}", vllm_port);
|
||||
if let Ok(models) = list_openai_models(&vllm_url) {
|
||||
for model in models {
|
||||
targets.push(BenchTarget::VLlm {
|
||||
url: vllm_url.clone(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check Ollama
|
||||
let ollama_url =
|
||||
std::env::var("OLLAMA_HOST").unwrap_or_else(|_| "http://localhost:11434".to_string());
|
||||
if let Ok(models) = list_ollama_models(&ollama_url) {
|
||||
let llama_url = llamacpp_url();
|
||||
let mlx_url =
|
||||
std::env::var("MLX_LM_HOST").unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||
discover_all_targets_at(&openai_bench_urls(), &ollama_url, &llama_url, &mlx_url)
|
||||
}
|
||||
|
||||
fn identified_openai_claims_url(targets: &[BenchTarget], candidate_url: &str) -> bool {
|
||||
targets.iter().any(|target| match target {
|
||||
BenchTarget::VLlm { url, .. } | BenchTarget::Ferrum { url, .. } => {
|
||||
endpoint_urls_match(url, candidate_url)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn discover_all_targets_at(
|
||||
openai_urls: &[String],
|
||||
ollama_url: &str,
|
||||
llama_url: &str,
|
||||
mlx_url: &str,
|
||||
) -> Vec<BenchTarget> {
|
||||
let mut targets = discover_openai_targets(openai_urls);
|
||||
|
||||
// Check Ollama
|
||||
if let Ok(models) = list_ollama_models(ollama_url) {
|
||||
for model in models {
|
||||
targets.push(BenchTarget::Ollama {
|
||||
url: ollama_url.clone(),
|
||||
url: ollama_url.to_string(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check llama-server before MLX: both default to port 8080, but only
|
||||
// llama.cpp answers /props, so it can be identified positively.
|
||||
let llama_url = llamacpp_url();
|
||||
let llamacpp_found = probe_llamacpp(&llama_url);
|
||||
if llamacpp_found && let Ok(models) = list_llamacpp_models(&llama_url) {
|
||||
// llama.cpp answers /props, so it can be identified positively. Do not
|
||||
// probe a URL already claimed by vLLM or Ferrum.
|
||||
let llamacpp_found =
|
||||
!identified_openai_claims_url(&targets, llama_url) && probe_llamacpp(llama_url);
|
||||
if llamacpp_found && let Ok(models) = list_llamacpp_models(llama_url) {
|
||||
for model in models {
|
||||
targets.push(BenchTarget::LlamaCpp {
|
||||
url: llama_url.clone(),
|
||||
url: llama_url.to_string(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
|
@ -484,14 +682,13 @@ pub fn discover_all_targets() -> Vec<BenchTarget> {
|
|||
|
||||
// Check MLX (skip if the llama-server probe already claimed this URL,
|
||||
// e.g. both on the default port 8080)
|
||||
let mlx_url =
|
||||
std::env::var("MLX_LM_HOST").unwrap_or_else(|_| "http://localhost:8080".to_string());
|
||||
if !(llamacpp_found && mlx_url.trim_end_matches('/') == llama_url)
|
||||
&& let Ok(models) = list_openai_models(&mlx_url)
|
||||
if !identified_openai_claims_url(&targets, mlx_url)
|
||||
&& !(llamacpp_found && endpoint_urls_match(mlx_url, llama_url))
|
||||
&& let Ok(models) = list_openai_models(mlx_url)
|
||||
{
|
||||
for model in models {
|
||||
targets.push(BenchTarget::Mlx {
|
||||
url: mlx_url.clone(),
|
||||
url: mlx_url.to_string(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
|
@ -562,8 +759,31 @@ pub fn detect_model_from_url(base_url: &str, hint: Option<&str>) -> Result<Strin
|
|||
detect_openai_model(base_url, hint)
|
||||
}
|
||||
|
||||
fn detect_vllm_model(base_url: &str, hint: Option<&str>) -> Result<String, String> {
|
||||
detect_openai_model(base_url, hint)
|
||||
/// Detect a model only after the endpoint positively identifies as vLLM.
|
||||
pub fn detect_vllm_model(base_url: &str, hint: Option<&str>) -> Result<String, String> {
|
||||
detect_identified_openai_model(base_url, hint, OpenAiEndpointIdentity::Vllm, "vLLM")
|
||||
}
|
||||
|
||||
/// Detect a model only after the endpoint positively identifies as Ferrum.
|
||||
pub fn detect_ferrum_model(base_url: &str, hint: Option<&str>) -> Result<String, String> {
|
||||
detect_identified_openai_model(base_url, hint, OpenAiEndpointIdentity::Ferrum, "Ferrum")
|
||||
}
|
||||
|
||||
fn detect_identified_openai_model(
|
||||
base_url: &str,
|
||||
hint: Option<&str>,
|
||||
expected: OpenAiEndpointIdentity,
|
||||
provider: &str,
|
||||
) -> Result<String, String> {
|
||||
let (identity, models) = identified_openai_models(base_url, Duration::from_secs(5))?;
|
||||
if identity != expected {
|
||||
return Err(format!(
|
||||
"Endpoint at {} did not identify as {}",
|
||||
base_url.trim_end_matches('/'),
|
||||
provider
|
||||
));
|
||||
}
|
||||
choose_model(&models, hint)
|
||||
}
|
||||
|
||||
fn detect_llamacpp_model(base_url: &str, hint: Option<&str>) -> Result<String, String> {
|
||||
|
|
@ -727,6 +947,299 @@ impl BenchResult {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const FERRUM_MODELS_FIXTURE: &str = r#"{"data":[{"id":"ferrum","owned_by":"ferrum"}]}"#;
|
||||
const VLLM_MODELS_FIXTURE: &str = r#"{"object":"list","data":[{"id":"facebook/opt-125m","object":"model","created":1788290125,"owned_by":"vllm","root":"facebook/opt-125m","parent":null,"max_model_len":512}]}"#;
|
||||
const UNKNOWN_MODELS_FIXTURE: &str = r#"{"data":[{"id":"foreign-model"}]}"#;
|
||||
const CHAT_COMPLETION_FIXTURE: &str = r#"{"choices":[{"message":{"content":"hello"}}],"usage":{"prompt_tokens":3,"completion_tokens":2}}"#;
|
||||
|
||||
fn read_fixture_request(reader: impl std::io::Read) -> std::io::Result<Vec<u8>> {
|
||||
use std::io::{BufRead, BufReader, Error, ErrorKind, Read};
|
||||
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut request = Vec::new();
|
||||
let mut content_length = 0;
|
||||
loop {
|
||||
let start = request.len();
|
||||
if reader.read_until(b'\n', &mut request)? == 0 {
|
||||
return Err(Error::from(ErrorKind::UnexpectedEof));
|
||||
}
|
||||
let line = std::str::from_utf8(&request[start..])
|
||||
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
|
||||
if line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
if let Some((name, value)) = line.split_once(':') {
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
content_length = value
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
|
||||
} else if name.eq_ignore_ascii_case("transfer-encoding") {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"fixture expects Content-Length, as sent by ureq::send_json",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// A TCP read can end within either the headers or the body. Closing
|
||||
// with unread POST bytes can abort the client's connection on Windows.
|
||||
let body_start = request.len();
|
||||
let request_length = body_start
|
||||
.checked_add(content_length)
|
||||
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
|
||||
request.resize(request_length, 0);
|
||||
reader.read_exact(&mut request[body_start..])?;
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_reads_complete_request_across_short_reads() {
|
||||
use std::io::Read;
|
||||
|
||||
struct ShortReads<'a>(&'a [u8]);
|
||||
impl Read for ShortReads<'_> {
|
||||
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
|
||||
let count = buffer.len().min(3);
|
||||
self.0.read(&mut buffer[..count])
|
||||
}
|
||||
}
|
||||
|
||||
let body = serde_json::json!({ "content": "hello".repeat(512) }).to_string();
|
||||
let requests = [
|
||||
"GET /v1/models HTTP/1.1\r\nHost: localhost\r\n\r\n".to_string(),
|
||||
format!(
|
||||
"POST /v1/chat/completions HTTP/1.1\r\nHost: localhost\r\ncontent-length: {}\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
),
|
||||
];
|
||||
for request in requests {
|
||||
let actual = read_fixture_request(ShortReads(request.as_bytes()))
|
||||
.expect("read the complete fixture request");
|
||||
assert_eq!(actual, request.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixture_rejects_incomplete_request_headers_and_body() {
|
||||
for request in [
|
||||
"GET /v1/models HTTP/1.1\r\nHost: localhost\r\n",
|
||||
"POST /v1/chat/completions HTTP/1.1\r\nContent-Length: 5\r\n\r\n{}",
|
||||
] {
|
||||
let error = read_fixture_request(request.as_bytes())
|
||||
.expect_err("do not respond before consuming the complete request");
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::UnexpectedEof);
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve JSON responses on an ephemeral loopback port.
|
||||
fn serve_fixture(body: &'static str) -> String {
|
||||
use std::io::Write;
|
||||
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind test listener");
|
||||
let addr = listener.local_addr().expect("test listener addr");
|
||||
std::thread::spawn(move || {
|
||||
while let Ok((mut stream, _)) = listener.accept() {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.expect("set fixture read timeout");
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.expect("set fixture write timeout");
|
||||
read_fixture_request(&mut stream).expect("read fixture request");
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("write fixture response");
|
||||
}
|
||||
});
|
||||
format!("http://{}", addr)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_detects_ferrum_from_positive_identity() {
|
||||
let url = serve_fixture(FERRUM_MODELS_FIXTURE);
|
||||
let target = auto_detect_openai_target(std::slice::from_ref(&url), None)
|
||||
.expect("Ferrum endpoint should be detected");
|
||||
assert_eq!(
|
||||
target,
|
||||
BenchTarget::Ferrum {
|
||||
url,
|
||||
model: "ferrum".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_identity_normalizes_loopback_defaults_and_trailing_slashes() {
|
||||
let port_8000 = endpoint_key("http://localhost:8000/").expect("valid endpoint");
|
||||
assert_eq!(
|
||||
port_8000,
|
||||
endpoint_key("http://127.0.0.1:8000").expect("valid IPv4 endpoint")
|
||||
);
|
||||
assert_eq!(
|
||||
port_8000,
|
||||
endpoint_key("http://[::1]:8000///").expect("valid IPv6 endpoint")
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_key("http://localhost").expect("valid HTTP endpoint"),
|
||||
endpoint_key("http://127.0.0.1:80/").expect("valid explicit HTTP endpoint")
|
||||
);
|
||||
assert_eq!(
|
||||
endpoint_key("https://localhost/").expect("valid HTTPS endpoint"),
|
||||
endpoint_key("https://[::1]:443").expect("valid explicit HTTPS endpoint")
|
||||
);
|
||||
assert_ne!(
|
||||
port_8000,
|
||||
endpoint_key("http://127.0.0.1:8001").expect("valid distinct port")
|
||||
);
|
||||
assert_ne!(
|
||||
endpoint_key("http://localhost:8000/api").expect("valid path endpoint"),
|
||||
endpoint_key("http://127.0.0.1:8000/other").expect("valid distinct path")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_identity_rejects_malformed_explicit_ports_and_userinfo() {
|
||||
let default_http = "http://127.0.0.1:80";
|
||||
for malformed in [
|
||||
"http://localhost:abc",
|
||||
"http://localhost:",
|
||||
"http://localhost:65536",
|
||||
"http://[::1]:abc",
|
||||
"http://[::1]:",
|
||||
"http://[::1]:65536",
|
||||
"http://user@localhost",
|
||||
"http://user:password@localhost:80",
|
||||
] {
|
||||
assert!(
|
||||
endpoint_key(malformed).is_none(),
|
||||
"{malformed} must not have a canonical endpoint identity"
|
||||
);
|
||||
assert!(
|
||||
!endpoint_urls_match(malformed, default_http),
|
||||
"{malformed} must not match the default HTTP endpoint"
|
||||
);
|
||||
|
||||
let mut urls = vec![malformed.to_string()];
|
||||
push_unique_endpoint_url(&mut urls, default_http.to_string());
|
||||
assert_eq!(
|
||||
urls.len(),
|
||||
2,
|
||||
"{malformed} must not swallow a valid default-port endpoint"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_candidate_urls_deduplicate_loopback_aliases() {
|
||||
let mut urls = Vec::new();
|
||||
for url in [
|
||||
"http://localhost:8000/",
|
||||
"http://127.0.0.1:8000",
|
||||
"http://[::1]:8000///",
|
||||
] {
|
||||
push_unique_endpoint_url(&mut urls, url.to_string());
|
||||
}
|
||||
assert_eq!(urls, vec!["http://localhost:8000/".to_string()]);
|
||||
|
||||
push_unique_endpoint_url(&mut urls, "http://localhost".to_string());
|
||||
push_unique_endpoint_url(&mut urls, "http://127.0.0.1:80/".to_string());
|
||||
assert_eq!(urls.len(), 2, "default HTTP port should deduplicate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_includes_only_identified_vllm_and_ferrum_targets() {
|
||||
let ferrum_url = serve_fixture(FERRUM_MODELS_FIXTURE);
|
||||
let vllm_url = serve_fixture(VLLM_MODELS_FIXTURE);
|
||||
let unknown_url = serve_fixture(UNKNOWN_MODELS_FIXTURE);
|
||||
|
||||
let targets =
|
||||
discover_openai_targets(&[ferrum_url.clone(), vllm_url.clone(), unknown_url.clone()]);
|
||||
|
||||
assert_eq!(
|
||||
targets,
|
||||
vec![
|
||||
BenchTarget::Ferrum {
|
||||
url: ferrum_url,
|
||||
model: "ferrum".to_string(),
|
||||
},
|
||||
BenchTarget::VLlm {
|
||||
url: vllm_url,
|
||||
model: "facebook/opt-125m".to_string(),
|
||||
},
|
||||
]
|
||||
);
|
||||
assert!(auto_detect_openai_target(&[unknown_url], None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_all_does_not_reclassify_claimed_loopback_aliases() {
|
||||
let ferrum_url = serve_fixture(FERRUM_MODELS_FIXTURE);
|
||||
let port = ferrum_url
|
||||
.rsplit(':')
|
||||
.next()
|
||||
.expect("fixture URL has a port");
|
||||
let localhost_url = format!("http://localhost:{port}/");
|
||||
let ipv6_url = format!("http://[::1]:{port}");
|
||||
let openai_urls = vec![ferrum_url.clone(), localhost_url.clone(), ipv6_url.clone()];
|
||||
let expected = vec![BenchTarget::Ferrum {
|
||||
url: ferrum_url.clone(),
|
||||
model: "ferrum".to_string(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
discover_all_targets_at(&openai_urls, &ferrum_url, &localhost_url, &ipv6_url),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
discover_all_targets_at(&openai_urls, &ferrum_url, &ipv6_url, &localhost_url),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_vllm_and_ferrum_detection_rejects_foreign_endpoints() {
|
||||
let vllm_url = serve_fixture(VLLM_MODELS_FIXTURE);
|
||||
let ferrum_url = serve_fixture(FERRUM_MODELS_FIXTURE);
|
||||
let unknown_url = serve_fixture(UNKNOWN_MODELS_FIXTURE);
|
||||
|
||||
assert_eq!(
|
||||
detect_vllm_model(&vllm_url, None).expect("vLLM identity should match"),
|
||||
"facebook/opt-125m"
|
||||
);
|
||||
assert_eq!(
|
||||
detect_ferrum_model(&ferrum_url, None).expect("Ferrum identity should match"),
|
||||
"ferrum"
|
||||
);
|
||||
assert!(detect_vllm_model(&ferrum_url, Some("ferrum")).is_err());
|
||||
assert!(detect_ferrum_model(&vllm_url, Some("opt-125m")).is_err());
|
||||
assert!(detect_vllm_model(&unknown_url, Some("foreign-model")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ferrum_target_keeps_json_provider_attribution() {
|
||||
let url = serve_fixture(CHAT_COMPLETION_FIXTURE);
|
||||
let result = benchmark_target(
|
||||
&BenchTarget::Ferrum {
|
||||
url,
|
||||
model: "ferrum".to_string(),
|
||||
},
|
||||
1,
|
||||
&|_, _| {},
|
||||
)
|
||||
.expect("Ferrum OpenAI-compatible benchmark should succeed");
|
||||
|
||||
assert_eq!(result.provider, "ferrum");
|
||||
let json = serde_json::json!({ "result": result });
|
||||
assert_eq!(json["result"]["provider"], "ferrum");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_llamacpp_gguf_paths_to_filenames() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -456,7 +456,7 @@ impl ModelProvider for OllamaProvider {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct OpenAiModelList {
|
||||
pub(crate) struct OpenAiModelList {
|
||||
data: Vec<OpenAiModel>,
|
||||
}
|
||||
|
||||
|
|
@ -482,16 +482,19 @@ fn openai_models_url(base_url: &str) -> String {
|
|||
/// `owned_by: "llamacpp"`; llama-swap lists models with
|
||||
/// `owned_by: "llama-swap"`; mlx_lm.server (0.31.3) sends a Python
|
||||
/// `BaseHTTP` Server header and no `owned_by` field at all. vLLM and Docker
|
||||
/// Model Runner were measured 2026-09-01 (see the variants below); LM Studio is
|
||||
/// identified out-of-band via its native /api/v0 API (`endpoint_is_lmstudio`).
|
||||
/// Model Runner were measured 2026-09-01 (see the variants below); Ferrum was
|
||||
/// captured 2026-09-02 in #992. LM Studio is identified out-of-band via its
|
||||
/// native /api/v0 API (`endpoint_is_lmstudio`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum OpenAiEndpointIdentity {
|
||||
pub(crate) enum OpenAiEndpointIdentity {
|
||||
/// llama.cpp serving directly.
|
||||
LlamaCpp,
|
||||
/// A llama-swap proxy fronting llama.cpp instances.
|
||||
LlamaSwap,
|
||||
/// vLLM's OpenAI server (measured 2026-09-01: owned_by "vllm").
|
||||
Vllm,
|
||||
/// Ferrum's OpenAI-compatible server (owned_by "ferrum").
|
||||
Ferrum,
|
||||
/// Docker Model Runner (measured 2026-09-01: owned_by "docker").
|
||||
DockerModelRunner,
|
||||
/// No foreign marker recognized.
|
||||
|
|
@ -515,6 +518,9 @@ fn classify_openai_endpoint(
|
|||
if owned_by("vllm") {
|
||||
return OpenAiEndpointIdentity::Vllm;
|
||||
}
|
||||
if owned_by("ferrum") {
|
||||
return OpenAiEndpointIdentity::Ferrum;
|
||||
}
|
||||
// Docker Model Runner stamps owned_by "docker" (plus a per-model `dmr`
|
||||
// object) and sends no Server header. Measured 2026-09-01.
|
||||
if owned_by("docker") {
|
||||
|
|
@ -529,7 +535,7 @@ fn classify_openai_endpoint(
|
|||
OpenAiEndpointIdentity::Unrecognized
|
||||
}
|
||||
|
||||
fn fetch_openai_model_list(
|
||||
pub(crate) fn fetch_openai_model_list(
|
||||
base_url: &str,
|
||||
timeout: std::time::Duration,
|
||||
) -> Option<(OpenAiModelList, OpenAiEndpointIdentity)> {
|
||||
|
|
@ -558,7 +564,7 @@ fn openai_model_list_is_omlx(list: &OpenAiModelList) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
fn openai_model_ids(list: &OpenAiModelList) -> impl Iterator<Item = &str> {
|
||||
pub(crate) fn openai_model_ids(list: &OpenAiModelList) -> impl Iterator<Item = &str> {
|
||||
list.data.iter().map(|model| model.id.as_str())
|
||||
}
|
||||
|
||||
|
|
@ -6839,6 +6845,7 @@ mod tests {
|
|||
|
||||
/// Verbatim `/v1/models` body captured from vLLM 0.28.0 on 2026-09-01.
|
||||
const VLLM_MODELS_FIXTURE: &str = r#"{"object":"list","data":[{"id":"facebook/opt-125m","object":"model","created":1788290125,"owned_by":"vllm","root":"facebook/opt-125m","parent":null,"max_model_len":512}]}"#;
|
||||
const FERRUM_MODELS_FIXTURE: &str = r#"{"data":[{"id":"ferrum","owned_by":"ferrum"}]}"#;
|
||||
|
||||
/// Verbatim `/v1/models` body captured from Docker Model Runner on
|
||||
/// 2026-09-01: owned_by "docker" plus a per-model `dmr` object.
|
||||
|
|
@ -6897,6 +6904,13 @@ mod tests {
|
|||
OpenAiEndpointIdentity::Vllm
|
||||
);
|
||||
|
||||
let ferrum: OpenAiModelList =
|
||||
serde_json::from_str(FERRUM_MODELS_FIXTURE).expect("fixture should parse");
|
||||
assert_eq!(
|
||||
classify_openai_endpoint(None, &ferrum),
|
||||
OpenAiEndpointIdentity::Ferrum
|
||||
);
|
||||
|
||||
let docker: OpenAiModelList =
|
||||
serde_json::from_str(DOCKER_MR_MODELS_FIXTURE).expect("fixture should parse");
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -1318,6 +1318,24 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn sample_ferrum_result() -> BenchResult {
|
||||
use crate::bench::BenchSummary;
|
||||
BenchResult {
|
||||
model: "ferrum".to_string(),
|
||||
provider: "ferrum".to_string(),
|
||||
runs: vec![],
|
||||
summary: BenchSummary {
|
||||
num_runs: 3,
|
||||
avg_ttft_ms: None,
|
||||
avg_tps: 96.7,
|
||||
min_tps: 94.0,
|
||||
max_tps: 99.0,
|
||||
avg_total_ms: 3100.0,
|
||||
avg_output_tokens: 300.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slug_is_filename_safe() {
|
||||
let submission = build_submission(&[sample_result()], &specs_with_gpu("NVIDIA RTX 4090!!"));
|
||||
|
|
@ -1397,7 +1415,7 @@ mod tests {
|
|||
unsafe { std::env::set_var("LLMFIT_BENCH_STORE", &dir) };
|
||||
|
||||
let specs = specs_with_gpu("NVIDIA GeForce RTX 4090");
|
||||
let path = store_local(&[sample_result()], &specs).unwrap();
|
||||
let path = store_local(&[sample_result(), sample_ferrum_result()], &specs).unwrap();
|
||||
assert!(path.starts_with(dir.join("pending")));
|
||||
|
||||
let pending = pending_benchmarks();
|
||||
|
|
@ -1405,8 +1423,12 @@ mod tests {
|
|||
assert_eq!(pending[0].payload["schemaVersion"], 1);
|
||||
assert_eq!(
|
||||
pending[0].result_lines(),
|
||||
vec!["llama3.1:8b via ollama — 128.4 tok/s".to_string()]
|
||||
vec![
|
||||
"llama3.1:8b via ollama — 128.4 tok/s".to_string(),
|
||||
"ferrum via ferrum — 96.7 tok/s".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(pending[0].payload["results"][1]["provider"], "ferrum");
|
||||
assert!(shared_benchmarks().is_empty());
|
||||
|
||||
// #819: a payload written by an older binary can still carry an
|
||||
|
|
@ -1499,9 +1521,10 @@ mod tests {
|
|||
avg_output_tokens: 100.0,
|
||||
},
|
||||
};
|
||||
let ferrum_result = sample_ferrum_result();
|
||||
|
||||
let submission = build_submission(
|
||||
&[result, llamacpp_result],
|
||||
&[result, llamacpp_result, ferrum_result],
|
||||
&specs_with_gpu("NVIDIA GeForce RTX 4090"),
|
||||
);
|
||||
let value = serde_json::to_value(&submission).unwrap();
|
||||
|
|
@ -1528,6 +1551,7 @@ mod tests {
|
|||
assert_eq!(value["hardware"]["hwClass"], "DISCRETE_GPU");
|
||||
assert_eq!(value["hardware"]["memTierGb"], 24);
|
||||
assert_eq!(value["results"][0]["avgTps"], 128.44);
|
||||
assert_eq!(value["results"][2]["provider"], "ferrum");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -851,7 +851,7 @@ AGENT USAGE:
|
|||
/// Model name to benchmark (auto-detects provider if omitted)
|
||||
model: Option<String>,
|
||||
|
||||
/// Provider to benchmark (auto, ollama, vllm, mlx, llamacpp)
|
||||
/// Provider to benchmark (auto, ollama, vllm, ferrum, mlx, llamacpp)
|
||||
#[arg(long, default_value = "auto")]
|
||||
provider: String,
|
||||
|
||||
|
|
@ -2627,6 +2627,7 @@ fn target_info(target: &bench::BenchTarget) -> (&str, &str, &str) {
|
|||
match target {
|
||||
bench::BenchTarget::Ollama { url, model } => ("Ollama", url.as_str(), model.as_str()),
|
||||
bench::BenchTarget::VLlm { url, model } => ("vLLM", url.as_str(), model.as_str()),
|
||||
bench::BenchTarget::Ferrum { url, model } => ("Ferrum", url.as_str(), model.as_str()),
|
||||
bench::BenchTarget::Mlx { url, model } => ("MLX", url.as_str(), model.as_str()),
|
||||
bench::BenchTarget::LlamaCpp { url, model } => ("llama.cpp", url.as_str(), model.as_str()),
|
||||
}
|
||||
|
|
@ -2675,7 +2676,7 @@ fn run_bench(
|
|||
let targets = bench::discover_all_targets();
|
||||
if targets.is_empty() {
|
||||
eprintln!(
|
||||
"No providers or models found. Start Ollama, vLLM, MLX, or llama-server first."
|
||||
"No providers or models found. Start Ollama, vLLM, Ferrum, MLX, or llama-server first."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
@ -2706,20 +2707,7 @@ fn run_bench(
|
|||
}
|
||||
};
|
||||
|
||||
let result = match target {
|
||||
bench::BenchTarget::Ollama { url, model } => {
|
||||
bench::bench_ollama(url, model, runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::VLlm { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "vllm", runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::Mlx { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "mlx", runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::LlamaCpp { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "llamacpp", runs, &progress)
|
||||
}
|
||||
};
|
||||
let result = bench::benchmark_target(target, runs, &progress);
|
||||
|
||||
if !json {
|
||||
eprintln!();
|
||||
|
|
@ -2774,23 +2762,27 @@ fn run_bench(
|
|||
let port = std::env::var("VLLM_PORT").unwrap_or_else(|_| "8000".to_string());
|
||||
format!("http://localhost:{}", port)
|
||||
});
|
||||
match bench::detect_model_from_url(&url, model.as_deref()) {
|
||||
match bench::detect_vllm_model(&url, model.as_deref()) {
|
||||
Ok(model_name) => bench::BenchTarget::VLlm {
|
||||
url,
|
||||
model: model_name,
|
||||
},
|
||||
Err(_) => {
|
||||
let model_name = model.unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"Error: could not detect model from vLLM at {}. Use --model",
|
||||
url
|
||||
);
|
||||
std::process::exit(1);
|
||||
});
|
||||
bench::BenchTarget::VLlm {
|
||||
url,
|
||||
model: model_name,
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"ferrum" => {
|
||||
let url = url_override.clone().unwrap_or_else(bench::ferrum_url);
|
||||
match bench::detect_ferrum_model(&url, model.as_deref()) {
|
||||
Ok(model_name) => bench::BenchTarget::Ferrum {
|
||||
url,
|
||||
model: model_name,
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2852,20 +2844,7 @@ fn run_bench(
|
|||
}
|
||||
};
|
||||
|
||||
let result = match &target {
|
||||
bench::BenchTarget::Ollama { url, model } => {
|
||||
bench::bench_ollama(url, model, runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::VLlm { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "vllm", runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::Mlx { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "mlx", runs, &progress)
|
||||
}
|
||||
bench::BenchTarget::LlamaCpp { url, model } => {
|
||||
bench::bench_openai_compat(url, model, "llamacpp", runs, &progress)
|
||||
}
|
||||
};
|
||||
let result = bench::benchmark_target(&target, runs, &progress);
|
||||
|
||||
if !json {
|
||||
eprintln!();
|
||||
|
|
@ -3018,7 +2997,7 @@ fn run_quality_bench(
|
|||
let all_targets = bench::discover_all_targets();
|
||||
if all_targets.is_empty() {
|
||||
eprintln!(
|
||||
"No providers or models found. Start Ollama, vLLM, MLX, or llama-server first."
|
||||
"No providers or models found. Start Ollama, vLLM, Ferrum, MLX, or llama-server first."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
@ -3055,23 +3034,27 @@ fn run_quality_bench(
|
|||
let port = std::env::var("VLLM_PORT").unwrap_or_else(|_| "8000".to_string());
|
||||
format!("http://localhost:{}", port)
|
||||
});
|
||||
match bench::detect_model_from_url(&url, model.as_deref()) {
|
||||
match bench::detect_vllm_model(&url, model.as_deref()) {
|
||||
Ok(model_name) => bench::BenchTarget::VLlm {
|
||||
url,
|
||||
model: model_name,
|
||||
},
|
||||
Err(_) => {
|
||||
let model_name = model.unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"Error: could not detect model from vLLM at {}. Use --model",
|
||||
url
|
||||
);
|
||||
std::process::exit(1);
|
||||
});
|
||||
bench::BenchTarget::VLlm {
|
||||
url,
|
||||
model: model_name,
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
"ferrum" => {
|
||||
let url = url_override.clone().unwrap_or_else(bench::ferrum_url);
|
||||
match bench::detect_ferrum_model(&url, model.as_deref()) {
|
||||
Ok(model_name) => bench::BenchTarget::Ferrum {
|
||||
url,
|
||||
model: model_name,
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3150,6 +3133,7 @@ fn run_quality_bench(
|
|||
quality::bench_quality_ollama(url, model, &config, rf)
|
||||
}
|
||||
bench::BenchTarget::VLlm { url, model }
|
||||
| bench::BenchTarget::Ferrum { url, model }
|
||||
| bench::BenchTarget::Mlx { url, model }
|
||||
| bench::BenchTarget::LlamaCpp { url, model } => {
|
||||
quality::bench_quality_openai_compat(url, model, provider_name, &config, rf)
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ fn bench_offer_worker(
|
|||
let model = match t {
|
||||
BenchTarget::Ollama { model, .. }
|
||||
| BenchTarget::VLlm { model, .. }
|
||||
| BenchTarget::Ferrum { model, .. }
|
||||
| BenchTarget::Mlx { model, .. }
|
||||
| BenchTarget::LlamaCpp { model, .. } => model,
|
||||
};
|
||||
|
|
@ -408,6 +409,7 @@ fn bench_offer_worker(
|
|||
let (provider, url, tag) = match &target {
|
||||
BenchTarget::Ollama { url, model } => ("ollama", url, model),
|
||||
BenchTarget::VLlm { url, model } => ("vllm", url, model),
|
||||
BenchTarget::Ferrum { url, model } => ("ferrum", url, model),
|
||||
BenchTarget::Mlx { url, model } => ("mlx", url, model),
|
||||
BenchTarget::LlamaCpp { url, model } => ("llamacpp", url, model),
|
||||
};
|
||||
|
|
@ -451,18 +453,7 @@ fn bench_offer_worker(
|
|||
let _ = progress_tx.send(BenchOfferMsg::Progress(msg));
|
||||
};
|
||||
|
||||
let result = match &target {
|
||||
BenchTarget::Ollama { url: u, model } => bench::bench_ollama(u, model, RUNS, &on_progress),
|
||||
BenchTarget::VLlm { url: u, model } => {
|
||||
bench::bench_openai_compat(u, model, "vllm", RUNS, &on_progress)
|
||||
}
|
||||
BenchTarget::Mlx { url: u, model } => {
|
||||
bench::bench_openai_compat(u, model, "mlx", RUNS, &on_progress)
|
||||
}
|
||||
BenchTarget::LlamaCpp { url: u, model } => {
|
||||
bench::bench_openai_compat(u, model, "llamacpp", RUNS, &on_progress)
|
||||
}
|
||||
};
|
||||
let result = bench::benchmark_target(&target, RUNS, &on_progress);
|
||||
|
||||
let result = match result {
|
||||
Ok(r) => r,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue