mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
Add ullm models sync command to download OpenRouter model metadata
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 6908e4dfbe5e
This commit is contained in:
parent
52d3536a1d
commit
b74d837b95
3 changed files with 408 additions and 17 deletions
259
Cargo.lock
generated
259
Cargo.lock
generated
|
|
@ -123,6 +123,16 @@ version = "1.0.101"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert_cmd"
|
||||
version = "2.1.2"
|
||||
|
|
@ -138,6 +148,27 @@ dependencies = [
|
|||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-object-pool"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1ac0219111eb7bb7cb76d4cf2cb50c598e7ae549091d3616f9e95442c18486f"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"event-listener",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
|
|
@ -293,6 +324,15 @@ version = "2.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bollard"
|
||||
version = "0.18.1"
|
||||
|
|
@ -371,6 +411,9 @@ name = "bytes"
|
|||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
|
|
@ -481,6 +524,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.2"
|
||||
|
|
@ -510,6 +562,31 @@ version = "0.8.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
|
|
@ -544,6 +621,16 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
|
|
@ -625,6 +712,27 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.17.0"
|
||||
|
|
@ -793,6 +901,12 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
|
|
@ -810,6 +924,16 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
|
|
@ -901,6 +1025,30 @@ dependencies = [
|
|||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
"http",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"sha1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "headers-core"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
|
||||
dependencies = [
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
|
|
@ -958,6 +1106,40 @@ version = "1.0.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "httpmock"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf4888a4d02d8e1f92ffb6b4965cf5ff56dda36ef41975f41c6fa0f6bde78c4e"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"async-object-pool",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"crossbeam-utils",
|
||||
"form_urlencoded",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"path-tree",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_regex",
|
||||
"similar",
|
||||
"stringmetrics",
|
||||
"tabwriter",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
|
|
@ -1359,11 +1541,13 @@ dependencies = [
|
|||
"dotenvy",
|
||||
"futures",
|
||||
"http",
|
||||
"httpmock",
|
||||
"predicates",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
|
@ -1558,6 +1742,12 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
|
|
@ -1587,6 +1777,15 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "path-tree"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a97453bc21a968f722df730bfe11bd08745cb50d1300b0df2bda131dece136"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -2232,6 +2431,16 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_regex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
|
|
@ -2273,6 +2482,17 @@ dependencies = [
|
|||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
|
|
@ -2295,6 +2515,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
|
|
@ -2323,6 +2549,12 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stringmetrics"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b3c8667cd96245cbb600b8dec5680a7319edd719c5aa2b5d23c6bff94f39765"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -2366,6 +2598,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabwriter"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.44"
|
||||
|
|
@ -2613,9 +2854,21 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
|
|
@ -2631,6 +2884,12 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
|
|
|
|||
|
|
@ -39,3 +39,5 @@ http = "1"
|
|||
tokio = { workspace = true, features = ["test-util", "macros"] }
|
||||
assert_cmd = "2"
|
||||
predicates = "3"
|
||||
httpmock = "0.8"
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -64,6 +64,17 @@ enum ModelsCommand {
|
|||
#[arg(short, long)]
|
||||
query: Option<String>,
|
||||
},
|
||||
|
||||
/// Download model metadata from OpenRouter
|
||||
Sync {
|
||||
/// URL to fetch models from
|
||||
#[arg(long, default_value = "https://openrouter.ai/api/v1/models")]
|
||||
url: String,
|
||||
|
||||
/// Output file path
|
||||
#[arg(short, long, default_value = "openrouter_models.json")]
|
||||
output: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_option(s: &str) -> Result<(String, String), String> {
|
||||
|
|
@ -219,6 +230,27 @@ async fn run_prompt(args: PromptArgs) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_models(url: &str, output: &str) -> Result<()> {
|
||||
let body = reqwest::get(url)
|
||||
.await
|
||||
.context("failed to connect to models endpoint")?
|
||||
.error_for_status()
|
||||
.context("models endpoint returned an error")?
|
||||
.text()
|
||||
.await
|
||||
.context("failed to read response body")?;
|
||||
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(&body).context("response is not valid JSON")?;
|
||||
let pretty =
|
||||
serde_json::to_string_pretty(&json).context("failed to format JSON")?;
|
||||
|
||||
std::fs::write(output, &pretty).with_context(|| format!("failed to write {output}"))?;
|
||||
|
||||
eprintln!("Saved models to {output}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
|
@ -246,26 +278,32 @@ async fn main() -> Result<()> {
|
|||
.await?;
|
||||
}
|
||||
Command::Models { command } => {
|
||||
let ModelsCommand::List { provider, query } =
|
||||
command.unwrap_or(ModelsCommand::List {
|
||||
provider: None,
|
||||
query: None,
|
||||
});
|
||||
let command = command.unwrap_or(ModelsCommand::List {
|
||||
provider: None,
|
||||
query: None,
|
||||
});
|
||||
|
||||
let mut models = catalog::list_models(provider.as_deref());
|
||||
match command {
|
||||
ModelsCommand::List { provider, query } => {
|
||||
let mut models = catalog::list_models(provider.as_deref());
|
||||
|
||||
if let Some(q) = &query {
|
||||
let q_lower = q.to_lowercase();
|
||||
models.retain(|m| {
|
||||
m.id.to_lowercase().contains(&q_lower)
|
||||
|| m.display_name.to_lowercase().contains(&q_lower)
|
||||
|| m.aliases
|
||||
.iter()
|
||||
.any(|a| a.to_lowercase().contains(&q_lower))
|
||||
});
|
||||
if let Some(q) = &query {
|
||||
let q_lower = q.to_lowercase();
|
||||
models.retain(|m| {
|
||||
m.id.to_lowercase().contains(&q_lower)
|
||||
|| m.display_name.to_lowercase().contains(&q_lower)
|
||||
|| m.aliases
|
||||
.iter()
|
||||
.any(|a| a.to_lowercase().contains(&q_lower))
|
||||
});
|
||||
}
|
||||
|
||||
print_models_table(&models);
|
||||
}
|
||||
ModelsCommand::Sync { url, output } => {
|
||||
sync_models(&url, &output).await?;
|
||||
}
|
||||
}
|
||||
|
||||
print_models_table(&models);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +393,98 @@ mod tests {
|
|||
.stdout(predicate::str::contains("gemini-3.1-pro-preview"));
|
||||
}
|
||||
|
||||
// models sync downloads and saves pretty-printed JSON
|
||||
#[test]
|
||||
fn models_sync_downloads_and_saves() {
|
||||
let server = httpmock::MockServer::start();
|
||||
let mock_response = serde_json::json!({
|
||||
"data": [{"id": "test-model", "name": "Test Model"}]
|
||||
});
|
||||
server.mock(|when, then| {
|
||||
when.method("GET").path("/api/v1/models");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.body(serde_json::to_string(&mock_response).unwrap());
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_path = dir.path().join("models.json");
|
||||
|
||||
ullm()
|
||||
.args([
|
||||
"models",
|
||||
"sync",
|
||||
"--url",
|
||||
&server.url("/api/v1/models"),
|
||||
"--output",
|
||||
output_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(predicate::str::contains("Saved models to"));
|
||||
|
||||
let contents = std::fs::read_to_string(&output_path).unwrap();
|
||||
let expected = serde_json::to_string_pretty(&mock_response).unwrap();
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
// models sync reports HTTP errors
|
||||
#[test]
|
||||
fn models_sync_reports_http_errors() {
|
||||
let server = httpmock::MockServer::start();
|
||||
server.mock(|when, then| {
|
||||
when.method("GET").path("/api/v1/models");
|
||||
then.status(500);
|
||||
});
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_path = dir.path().join("models.json");
|
||||
|
||||
ullm()
|
||||
.args([
|
||||
"models",
|
||||
"sync",
|
||||
"--url",
|
||||
&server.url("/api/v1/models"),
|
||||
"--output",
|
||||
output_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("error").or(predicate::str::contains("Error")));
|
||||
}
|
||||
|
||||
// models sync with real OpenRouter (requires network)
|
||||
#[test]
|
||||
#[ignore = "requires network"]
|
||||
fn models_sync_integration_smoke_test() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output_path = dir.path().join("models.json");
|
||||
|
||||
ullm()
|
||||
.args([
|
||||
"models",
|
||||
"sync",
|
||||
"--output",
|
||||
output_path.to_str().unwrap(),
|
||||
])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let contents = std::fs::read_to_string(&output_path).unwrap();
|
||||
assert!(contents.contains("\"data\""));
|
||||
}
|
||||
|
||||
// models sync --help succeeds and mentions openrouter
|
||||
#[test]
|
||||
fn models_sync_help_mentions_openrouter() {
|
||||
ullm()
|
||||
.args(["models", "sync", "--help"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("openrouter").or(predicate::str::contains("OpenRouter")));
|
||||
}
|
||||
|
||||
// Step 5: prompt requires prompt text (errors when no prompt and stdin is tty)
|
||||
#[test]
|
||||
fn prompt_errors_without_prompt_text() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue