Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
dist
.DS_Store
node_modules
target
Cargo.lock
data/tokenspeed-monitor.sqlite
data/tokenspeed-monitor.sqlite-shm
data/tokenspeed-monitor.sqlite-wal
72 changes: 72 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
[workspace]
members = ["crates/*"]
resolver = "2"

[workspace.package]
edition = "2024"
version = "0.1.0"
rust-version = "1.87.0"
publish = false

[workspace.dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0.18"
toml = "1.0.4"

[workspace.lints.rust]
keyword_idents_2024 = "forbid"
missing_unsafe_on_extern = "forbid"
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "forbid"
unused_results = "forbid"

[workspace.lints.clippy]
absolute_paths = "deny"
as_conversions = "deny"
as_pointer_underscore = "deny"
as_underscore = "deny"
cast_possible_truncation = "deny"
cast_possible_wrap = "deny"
cast_precision_loss = "deny"
cast_sign_loss = "deny"
clone_on_ref_ptr = "deny"
dbg_macro = "deny"
empty_drop = "deny"
exhaustive_enums = "allow"
exhaustive_structs = "allow"
filetype_is_file = "deny"
fn_to_numeric_cast_any = "deny"
get_unwrap = "deny"
infinite_loop = "deny"
let_underscore_must_use = "deny"
mem_forget = "deny"
missing_asserts_for_indexing = "deny"
module_name_repetitions = "allow"
multiple_crate_versions = "allow"
multiple_unsafe_ops_per_block = "deny"
mutex_atomic = "deny"
panic = "deny"
rc_buffer = "deny"
rc_mutex = "deny"
str_to_string = "deny"
string_slice = "deny"
struct_field_names = "allow"
tests_outside_test_module = "deny"
todo = "deny"
try_err = "deny"
unimplemented = "deny"
unnecessary_safety_comment = "deny"
unnecessary_safety_doc = "deny"
unreachable = "deny"
unwrap_in_result = "deny"
wildcard_imports = "allow"

cargo = { level = "deny", priority = -1 }
complexity = { level = "deny", priority = -1 }
correctness = { level = "deny", priority = -1 }
nursery = { level = "deny", priority = -1 }
pedantic = { level = "deny", priority = -1 }
perf = { level = "deny", priority = -1 }
style = { level = "deny", priority = -1 }
suspicious = { level = "deny", priority = -1 }
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.PHONY: check lint fmt

check:
cargo check && cargo check --tests

lint:
cargo clippy && cargo clippy --tests

fmt:
cargo fmt --all
19 changes: 19 additions & 0 deletions crates/modelsdev/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "modelsdev"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
publish.workspace = true
build = "build.rs"

[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

[build-dependencies]
serde_json = { workspace = true }
toml = { workspace = true }

[lints]
workspace = true
112 changes: 112 additions & 0 deletions crates/modelsdev/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let repo_root = manifest_dir
.parent()
.and_then(Path::parent)
.ok_or_else(|| io::Error::other("failed to resolve repo root"))?;
let providers_dir = repo_root.join("providers");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let catalog_path = out_dir.join("catalog.json");

println!("cargo:rerun-if-changed={}", providers_dir.display());

let catalog = serde_json::json!({
"providers": build_catalog(&providers_dir)?,
});
let json = serde_json::to_string(&catalog)?;
fs::write(catalog_path, json)?;
Ok(())
}

fn build_catalog(providers_dir: &Path) -> Result<BTreeMap<String, serde_json::Value>, io::Error> {
let mut providers = BTreeMap::new();
for entry in fs::read_dir(providers_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let provider_id = entry.file_name().to_string_lossy().to_string();
let provider_dir = entry.path();
let provider_toml = provider_dir.join("provider.toml");
if !provider_toml.is_file() {
continue;
}

let mut provider = read_toml_value(&provider_toml)?;
let Some(provider_object) = provider.as_object_mut() else {
return Err(io::Error::other(format!(
"provider TOML must be an object: {}",
provider_toml.display()
)));
};
let _ = provider_object.insert(
"id".to_owned(),
serde_json::Value::String(provider_id.clone()),
);
let _ = provider_object.insert(
"models".to_owned(),
serde_json::Value::Object(serde_json::Map::new()),
);

let models_dir = provider_dir.join("models");
if models_dir.is_dir() {
let mut models = BTreeMap::new();
collect_models(&models_dir, &models_dir, &mut models)?;
let models_object = models.into_iter().collect::<serde_json::Map<String, _>>();
let _ = provider_object.insert(
"models".to_owned(),
serde_json::Value::Object(models_object),
);
}

let _ = providers.insert(provider_id, provider);
}
Ok(providers)
}

fn collect_models(
models_dir: &Path,
current_dir: &Path,
models: &mut BTreeMap<String, serde_json::Value>,
) -> Result<(), io::Error> {
for entry in fs::read_dir(current_dir)? {
let entry = entry?;
let file_type = entry.file_type()?;
let path = entry.path();
if file_type.is_dir() {
collect_models(models_dir, &path, models)?;
continue;
}
if path.extension().and_then(|ext| ext.to_str()) != Some("toml") || !path.is_file() {
continue;
}
let relative = path.strip_prefix(models_dir).map_err(io::Error::other)?;
let model_id = relative
.to_string_lossy()
.strip_suffix(".toml")
.unwrap_or_default()
.replace('\\', "/");
let mut model = read_toml_value(&path)?;
let Some(model_object) = model.as_object_mut() else {
return Err(io::Error::other(format!(
"model TOML must be an object: {}",
path.display()
)));
};
let _ = model_object.insert("id".to_owned(), serde_json::Value::String(model_id.clone()));
let _ = models.insert(model_id, model);
}
Ok(())
}

fn read_toml_value(path: &Path) -> Result<serde_json::Value, io::Error> {
let text = fs::read_to_string(path)?;
let toml_value = toml::from_str::<toml::Value>(&text).map_err(io::Error::other)?;
serde_json::to_value(toml_value).map_err(io::Error::other)
}
Loading