diff --git a/Cargo.lock b/Cargo.lock index d72d356e..354af577 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1301,6 +1301,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "command_handler" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49385f359e523e20d614c5f92195db2ba8e042e5ce8dadafb6174c2abd95adbb" +dependencies = [ + "anyhow", + "async-trait", + "log", + "tokio", + "tokio-util", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -3715,14 +3728,15 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "llama-cpp-bindings" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d1144611b04160ced5626c50470aa83b24b739174f06b57d17fe0acb2e20dd0" +checksum = "5cf9ab6d9bc5dcff5947b7623c874e474b02aa28b02b18546e8d13cb51c10c00" dependencies = [ "encoding_rs", "enumflags2", "llama-cpp-bindings-sys", "llama-cpp-bindings-types", + "llama-cpp-error-recorder", "llama-cpp-log-decoder", "llguidance", "log", @@ -3734,9 +3748,9 @@ dependencies = [ [[package]] name = "llama-cpp-bindings-build" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54fa3f1bf8856c5a080f76a9b595356de0199ff573c2605a6d47d0791401721" +checksum = "f797846ee27658c3b699a5efb370e294336b65dd4cb6588c52145c642edd9dd9" dependencies = [ "bindgen", "cc", @@ -3749,29 +3763,35 @@ dependencies = [ [[package]] name = "llama-cpp-bindings-sys" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d615ae977c1f81cb87cf8b75a7977d3e5d30cf20723050e939657a473dad6ac" +checksum = "8f8e35487cdf67f6c122f9d6caff463dd783564a250b637b4f338319726e5a4c" dependencies = [ "llama-cpp-bindings-build", ] [[package]] name = "llama-cpp-bindings-types" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafa1ae3e0c87d06d44d6a1476307e069bac06bd0efdf374942fcbcc41aa8bfd" +checksum = "0df5b3d79baa7ece61c5e6d490165d2294bdf8a15c3e991c8a350e4d9bc6ad7b" dependencies = [ "serde", "serde_json", "thiserror 2.0.18", ] +[[package]] +name = "llama-cpp-error-recorder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f4c103d2eb61be9dbeeaac990a6827e245fcb7706e9edd4b3452c5fe2ccfe5" + [[package]] name = "llama-cpp-log-decoder" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66433575f7419dd96c18928a4b56c1cd3a631f6b941515ff479c6896d713b8b3" +checksum = "de39c3889bc7503189d4e11a5f58a3a8612b22335aa9d5052d98ee6b7a62a588" [[package]] name = "llguidance" @@ -4870,7 +4890,6 @@ dependencies = [ "async-trait", "log", "nanoid", - "nix", "paddler_agent", "paddler_balancer", "paddler_messaging", @@ -4902,13 +4921,11 @@ dependencies = [ "anyhow", "async-trait", "clap", + "command_handler", "env_logger", "esbuild-metafile", - "log", - "nanoid", "paddler_balancer", "paddler_bootstrap", - "tokio", "tokio-util", "trzcina", ] @@ -4976,6 +4993,7 @@ dependencies = [ "actix-web", "anyhow", "clap", + "command_handler", "dashmap", "env_logger", "esbuild-metafile", diff --git a/Cargo.toml b/Cargo.toml index 7ee7bf75..42cdb4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ async-trait = "0.1" bytes = "1.11" cadence = "1.6" clap = { version = "4.5", features = ["derive"] } +command_handler = "=0.1.0" crossterm = { version = "=0.29.0", features = ["event-stream"] } dashmap = "6.1" encoding_rs = { version = "0.8", features = ["serde"] } @@ -55,9 +56,9 @@ http = "1" image = "0.25" indoc = "2" jsonschema = { version = "0.37", default-features = false } -llama-cpp-bindings = "=0.7.0" -llama-cpp-bindings-sys = "=0.7.0" -llama-cpp-bindings-types = "=0.7.0" +llama-cpp-bindings = "=0.9.0" +llama-cpp-bindings-sys = "=0.9.0" +llama-cpp-bindings-types = "=0.9.0" base64 = "0.22" log = "0.4" mime_guess = "2" @@ -118,26 +119,32 @@ pedantic = { level = "deny", priority = -1 } # Specific linter settings allow_attributes = "deny" +cast_possible_truncation = "allow" +cast_sign_loss = "allow" dbg_macro = "deny" -error_impl_error = "deny" +error_impl_error = "allow" expect_used = "deny" +future_not_send = "allow" infinite_loop = "deny" literal_string_with_formatting_args = "allow" missing_docs_in_private_items = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" +needless_pass_by_value = "allow" panic = "deny" print_stderr = "deny" print_stdout = "deny" rc_mutex = "deny" rest_pat_in_fully_bound_structs = "deny" self_named_module_files = "deny" +significant_drop_tightening = "allow" str_to_string = "deny" # integration tests live in tests/*.rs which is already compiled test-only by Cargo; # wrapping every file in `#[cfg(test)] mod tests { }` would be redundant. tests_outside_test_module = "allow" todo = "deny" +too_many_arguments = "allow" too_many_lines = "allow" unimplemented = "deny" unwrap_used = "deny" diff --git a/paddler_agent/src/chat_template_renderer/mod.rs b/paddler_agent/src/chat_template_renderer/mod.rs index 420b5512..bc5fb7a5 100644 --- a/paddler_agent/src/chat_template_renderer/mod.rs +++ b/paddler_agent/src/chat_template_renderer/mod.rs @@ -4,6 +4,7 @@ pub mod raise_exception; use anyhow::Context as _; use anyhow::Result; use minijinja::Environment; +use minijinja_contrib::add_to_environment; use minijinja_contrib::pycompat::unknown_method_callback; use paddler_messaging::chat_template::ChatTemplate; use serde::ser::Serialize; @@ -25,7 +26,7 @@ impl ChatTemplateRenderer { minijinja_env.add_template_owned(CHAT_TEMPLATE_NAME, content)?; minijinja_env.set_unknown_method_callback(unknown_method_callback); - minijinja_contrib::add_to_environment(&mut minijinja_env); + add_to_environment(&mut minijinja_env); minijinja_env.add_filter("tojson", pyjinja_tojson); Ok(Self { minijinja_env }) diff --git a/paddler_agent/src/chat_template_renderer/pyjinja_tojson.rs b/paddler_agent/src/chat_template_renderer/pyjinja_tojson.rs index 12c0f04f..a3b5c7d9 100644 --- a/paddler_agent/src/chat_template_renderer/pyjinja_tojson.rs +++ b/paddler_agent/src/chat_template_renderer/pyjinja_tojson.rs @@ -4,11 +4,6 @@ use minijinja::Value; use minijinja::filters::tojson; use minijinja::value::Kwargs; -#[expect( - clippy::needless_pass_by_value, - reason = "minijinja's Filter trait requires Kwargs by value; taking &Kwargs makes the \ - function unregisterable as a filter" -)] pub fn pyjinja_tojson(value: &Value, kwargs: Kwargs) -> Result { let indent: Option = kwargs.get("indent")?; diff --git a/paddler_agent/src/continuous_batch_scheduler/advance_generating_phase.rs b/paddler_agent/src/continuous_batch_scheduler/advance_generating_phase.rs index a67b9aa3..00874e81 100644 --- a/paddler_agent/src/continuous_batch_scheduler/advance_generating_phase.rs +++ b/paddler_agent/src/continuous_batch_scheduler/advance_generating_phase.rs @@ -79,7 +79,19 @@ impl AdvanceGeneratingPhase<'_> { } }; - let classified_outcomes = classify_token_phase::run(request, raw_token); + let classified_outcomes = match classify_token_phase::run(request, raw_token) { + Ok(outcomes) => outcomes, + Err(error) => { + error!( + "{:?}: sequence {} token classification failed: {error:#}", + self.scheduler_context.agent_name, request.state.sequence_id + ); + + return Some(AdvanceOutcome::Completed( + GeneratedTokenResult::DetokenizationFailed(error.to_string()), + )); + } + }; let completion_phase = CompletionCheckPhase { model: &self.scheduler_context.model, diff --git a/paddler_agent/src/continuous_batch_scheduler/classify_token_phase.rs b/paddler_agent/src/continuous_batch_scheduler/classify_token_phase.rs index 148b112b..d0d0870b 100644 --- a/paddler_agent/src/continuous_batch_scheduler/classify_token_phase.rs +++ b/paddler_agent/src/continuous_batch_scheduler/classify_token_phase.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use llama_cpp_bindings::SampledToken; use llama_cpp_bindings::sampled_token_classifier::IngestOutcome; use llama_cpp_bindings::sampled_token_classifier::SampledTokenSection; @@ -9,10 +10,11 @@ use crate::continuous_batch_scheduler::classified_token::ClassifiedToken; pub fn run( request: &mut ContinuousBatchActiveRequest, raw_token: LlamaToken, -) -> Vec { +) -> Result> { let section_before_ingest = request.token_classifier.current_section(); - let outcomes = request.token_classifier.ingest(raw_token); - classify_ingest_outcomes(outcomes, section_before_ingest) + let outcomes = request.token_classifier.ingest(raw_token)?; + + Ok(classify_ingest_outcomes(outcomes, section_before_ingest)) } fn classify_ingest_outcomes( diff --git a/paddler_agent/src/continuous_batch_scheduler/mod.rs b/paddler_agent/src/continuous_batch_scheduler/mod.rs index 9d89dcbe..882e9811 100644 --- a/paddler_agent/src/continuous_batch_scheduler/mod.rs +++ b/paddler_agent/src/continuous_batch_scheduler/mod.rs @@ -385,15 +385,19 @@ impl ContinuousBatchScheduler { )] fn build_token_classifier_for_active_request( &self, - ) -> llama_cpp_bindings::SampledTokenClassifier<'static> { - let classifier = self.scheduler_context.model.sampled_token_classifier(); + ) -> Result> { + let classifier = self + .scheduler_context + .model + .sampled_token_classifier() + .context("failed to build the sampled token classifier")?; - unsafe { + Ok(unsafe { std::mem::transmute::< llama_cpp_bindings::SampledTokenClassifier<'_>, llama_cpp_bindings::SampledTokenClassifier<'static>, >(classifier) - } + }) } fn build_tool_call_pipeline( @@ -431,10 +435,6 @@ impl ContinuousBatchScheduler { Ok(ToolCallPipelineBuildOutcome::Ready(pipeline)) } - #[expect( - clippy::too_many_arguments, - reason = "these are distinct concerns (the prompt, the generation config, the output channel, the stop signal, the slot guard) that do not form a cohesive value object; bundling them would violate single-responsibility grouping" - )] fn accept_text_prompt( &mut self, prompt: &str, @@ -527,7 +527,7 @@ impl ContinuousBatchScheduler { let chain = self.create_sampler_chain(); - let mut token_classifier = self.build_token_classifier_for_active_request(); + let mut token_classifier = self.build_token_classifier_for_active_request()?; token_classifier.record_prompt_tokens(prompt_tokens.len() as u64); token_classifier.ingest_prompt_tokens(&prompt_tokens); @@ -563,10 +563,6 @@ impl ContinuousBatchScheduler { Ok(()) } - #[expect( - clippy::too_many_arguments, - reason = "these are distinct concerns (the multimodal context, prompt, images, generation config, the output channel, the stop signal, the slot guard) that do not form a cohesive value object; bundling them would violate single-responsibility grouping" - )] fn accept_multimodal_request( &mut self, multimodal_context: &MtmdContext, @@ -685,7 +681,7 @@ impl ContinuousBatchScheduler { self.harvest_pending_samples_before_external_decode(); - let mut token_classifier = self.build_token_classifier_for_active_request(); + let mut token_classifier = self.build_token_classifier_for_active_request()?; let batch_size_i32 = i32::try_from(batch_size).context("batch_size does not fit in i32")?; @@ -810,7 +806,19 @@ impl ContinuousBatchScheduler { // batch via `pending_sampled_token`; their user-visible emission // happens in `advance_generating_phase` after the next decode, // not here. - let _ = active_request.token_classifier.ingest(raw_token); + if let Err(error) = active_request.token_classifier.ingest(raw_token) { + error!( + "{:?}: sequence {} pre-eval harvest detokenization error: {error:#}", + self.scheduler_context.agent_name, active_request.state.sequence_id + ); + active_request.complete_with_outcome( + self.scheduler_context.agent_name.as_deref(), + GeneratedTokenResult::DetokenizationFailed(error.to_string()), + ); + + continue; + } + active_request.state.pending_sampled_token = Some(llama_cpp_bindings::SampledToken::Content(raw_token)); active_request.state.i_batch = None; diff --git a/paddler_agent/src/decoded_image.rs b/paddler_agent/src/decoded_image.rs index 3122ada6..6d6c4e3d 100644 --- a/paddler_agent/src/decoded_image.rs +++ b/paddler_agent/src/decoded_image.rs @@ -1,20 +1,26 @@ use std::io::Cursor; +use std::str::from_utf8; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use image::DynamicImage; use image::ImageFormat; +use image::RgbaImage; +use image::guess_format; use image::imageops::FilterType; +use image::load_from_memory; use log::info; use paddler_messaging::image_url::ImageUrl; +use resvg::render; use resvg::tiny_skia::Pixmap; +use resvg::tiny_skia::Transform; use resvg::usvg::Options; use resvg::usvg::Tree as SvgTree; use crate::decoded_image_error::DecodedImageError; fn is_svg(data: &[u8]) -> bool { - let trimmed = match std::str::from_utf8(data) { + let trimmed = match from_utf8(data) { Ok(text) => text.trim_start(), Err(_) => return false, }; @@ -31,11 +37,6 @@ fn compute_target_dimension(svg_dim: f64, scale: f64) -> Result Vec { - std::fs::read(format!( + read(format!( "{}/../fixtures/{filename}", env!("CARGO_MANIFEST_DIR"), )) @@ -414,7 +412,7 @@ mod tests { let result = decoded_image.prepared_for_inference(1024).unwrap(); - let result_format = image::guess_format(&result.data).unwrap(); + let result_format = guess_format(&result.data).unwrap(); assert_eq!(result_format, ImageFormat::Png); } @@ -425,10 +423,10 @@ mod tests { let result = decoded_image.prepared_for_inference(1024).unwrap(); - let result_format = image::guess_format(&result.data).unwrap(); + let result_format = guess_format(&result.data).unwrap(); assert_eq!(result_format, ImageFormat::Png); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert_eq!(result_image.width(), 640); assert_eq!(result_image.height(), 427); } @@ -444,10 +442,10 @@ mod tests { let result = decoded_image.prepared_for_inference(1024).unwrap(); - let result_format = image::guess_format(&result.data).unwrap(); + let result_format = guess_format(&result.data).unwrap(); assert_eq!(result_format, ImageFormat::Png); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert_eq!(result_image.width(), 50); assert_eq!(result_image.height(), 50); } @@ -459,8 +457,8 @@ mod tests { let result = decoded_image.prepared_for_inference(320).unwrap(); - let result_format = image::guess_format(&result.data).unwrap(); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_format = guess_format(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert!(result_image.width() <= 320); assert!(result_image.height() <= 320); @@ -474,10 +472,10 @@ mod tests { let result = decoded_image.prepared_for_inference(1024).unwrap(); - let result_format = image::guess_format(&result.data).unwrap(); + let result_format = guess_format(&result.data).unwrap(); assert_eq!(result_format, ImageFormat::Jpeg); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert!(result_image.width() <= 1024); assert!(result_image.height() <= 1024); } @@ -489,7 +487,7 @@ mod tests { let result = decoded_image.prepared_for_inference(1000).unwrap(); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert_eq!(result_image.width(), 1000); assert_eq!(result_image.height(), 500); } @@ -498,14 +496,14 @@ mod tests { fn test_prepared_with_jpg_fixture_within_bound() { let fixture_data = load_fixture("llamas.jpg"); - let original_image = image::load_from_memory(&fixture_data).unwrap(); + let original_image = load_from_memory(&fixture_data).unwrap(); assert_eq!(original_image.width(), 640); assert_eq!(original_image.height(), 427); let decoded_image = DecodedImage { data: fixture_data }; let result = decoded_image.prepared_for_inference(320).unwrap(); - let result_image = image::load_from_memory(&result.data).unwrap(); + let result_image = load_from_memory(&result.data).unwrap(); assert_eq!(result_image.width(), 320); assert_eq!(result_image.height(), 214); } diff --git a/paddler_agent/src/model_source/local.rs b/paddler_agent/src/model_source/local.rs index cee294fd..20daccb6 100644 --- a/paddler_agent/src/model_source/local.rs +++ b/paddler_agent/src/model_source/local.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; use async_trait::async_trait; +use tokio::fs::try_exists; use crate::desired_model_resolution::DesiredModelResolution; use crate::resolves_model_source::ResolvesModelSource; @@ -27,7 +28,7 @@ impl ResolvesModelSource for LocalModelPath { ) -> Result { let local_path = PathBuf::from(&self.path); - if tokio::fs::try_exists(&local_path).await? { + if try_exists(&local_path).await? { Ok(DesiredModelResolution::Resolved(local_path)) } else { Ok(DesiredModelResolution::LocalFileMissing(local_path)) diff --git a/paddler_agent/src/model_source/url.rs b/paddler_agent/src/model_source/url.rs index 68d234fa..96a172d3 100644 --- a/paddler_agent/src/model_source/url.rs +++ b/paddler_agent/src/model_source/url.rs @@ -242,6 +242,9 @@ mod tests { use paddler_messaging::agent_issue::AgentIssue; use reqwest::StatusCode; use tempfile::TempDir; + use tokio::fs::create_dir; + use tokio::fs::read; + use tokio::fs::write; use tokio::io::AsyncBufReadExt as _; use tokio::io::AsyncWriteExt as _; use tokio::io::BufReader; @@ -290,7 +293,7 @@ mod tests { let url_string = "https://host.example/cached.gguf"; let cached = CachedDownloadedModel::new(&cache_dir, url_string).unwrap(); cached.ensure_cache_subdir_exists().await.unwrap(); - tokio::fs::write(&cached.cache_file_path, b"cached content") + write(&cached.cache_file_path, b"cached content") .await .unwrap(); @@ -394,7 +397,7 @@ mod tests { let url_string = "https://host.example/lock-as-directory.gguf"; let cached = CachedDownloadedModel::new(&cache_dir, url_string).unwrap(); cached.ensure_cache_subdir_exists().await.unwrap(); - tokio::fs::create_dir(&cached.lock_file_path).await.unwrap(); + create_dir(&cached.lock_file_path).await.unwrap(); let status = fresh_status(); let result = resolve_url_into_cache(url_string, &cache_dir, status.clone()).await; @@ -608,7 +611,7 @@ mod tests { #[tokio::test] async fn ensure_cache_subdir_failure_registers_model_cache_is_corrupted() { let directory = TempDir::new().unwrap(); - tokio::fs::write(directory.path().join("downloaded-models"), b"blocker") + write(directory.path().join("downloaded-models"), b"blocker") .await .unwrap(); let cache_dir = cache_dir_at(directory.path()); @@ -773,6 +776,6 @@ mod tests { resolution, DesiredModelResolution::Resolved(resolved_path) if resolved_path == expected_path )); - assert_eq!(tokio::fs::read(&expected_path).await.unwrap(), body); + assert_eq!(read(&expected_path).await.unwrap(), body); } } diff --git a/paddler_agent/src/prepare_conversation_history_request.rs b/paddler_agent/src/prepare_conversation_history_request.rs index 84e2e23e..2c80dd70 100644 --- a/paddler_agent/src/prepare_conversation_history_request.rs +++ b/paddler_agent/src/prepare_conversation_history_request.rs @@ -62,7 +62,7 @@ pub fn prepare_conversation_history_request( anyhow!(message) })?; - let media_marker = MediaMarker::new(mtmd_default_marker().to_owned()); + let media_marker = MediaMarker::new(mtmd_default_marker()?.to_owned()); let chat_template_messages = conversation_history.replace_images_with_marker(&media_marker); let raw_prompt = scheduler_context diff --git a/paddler_agent/src/sample_token_at_batch_index.rs b/paddler_agent/src/sample_token_at_batch_index.rs index 69e72dd1..fd897b3b 100644 --- a/paddler_agent/src/sample_token_at_batch_index.rs +++ b/paddler_agent/src/sample_token_at_batch_index.rs @@ -16,10 +16,14 @@ pub fn sample_token_at_batch_index( .context("failed to read token data array for sampling")?; if let Some(grammar) = grammar_sampler.as_ref() { - token_data_array.apply_sampler(grammar); + token_data_array + .apply_sampler(grammar) + .context("failed to apply grammar sampler to token data array")?; } - token_data_array.apply_sampler(chain); + token_data_array + .apply_sampler(chain) + .context("failed to apply sampler chain to token data array")?; let Some(llama_token) = token_data_array.selected_token() else { return Ok(SamplingOutcome::AllCandidatesEliminated); diff --git a/paddler_agent/src/tool_call_buffer.rs b/paddler_agent/src/tool_call_buffer.rs index cb214933..8974f363 100644 --- a/paddler_agent/src/tool_call_buffer.rs +++ b/paddler_agent/src/tool_call_buffer.rs @@ -1,3 +1,5 @@ +use std::mem::take; + #[derive(Debug, Default)] pub struct ToolCallBuffer { accumulated: String, @@ -26,7 +28,7 @@ impl ToolCallBuffer { #[must_use] pub fn take(&mut self) -> String { - std::mem::take(&mut self.accumulated) + take(&mut self.accumulated) } #[must_use] diff --git a/paddler_agent/src/tool_call_validator.rs b/paddler_agent/src/tool_call_validator.rs index e065ad94..26ad2082 100644 --- a/paddler_agent/src/tool_call_validator.rs +++ b/paddler_agent/src/tool_call_validator.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use jsonschema::Validator; +use jsonschema::validator_for; use llama_cpp_bindings::ParsedToolCall; use llama_cpp_bindings::ToolCallArguments; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::Tool; @@ -39,7 +40,7 @@ impl ToolCallValidator { message: err.to_string(), } })?; - let compiled = jsonschema::validator_for(&schema_value).map_err(|err| { + let compiled = validator_for(&schema_value).map_err(|err| { ValidatorBuildError::InvalidSchema { tool_name: function.name.clone(), message: err.to_string(), @@ -117,7 +118,7 @@ mod tests { let mut properties = Map::new(); properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "city"}), + json!({"type": "string", "description": "city"}), ); Tool::Function(FunctionCall { @@ -262,7 +263,7 @@ mod tests { fn tool_with_invalid_property_schema() -> Tool { let mut properties = Map::new(); - properties.insert("location".to_owned(), serde_json::json!({"type": 42})); + properties.insert("location".to_owned(), json!({"type": 42})); Tool::Function(FunctionCall { function: Function { diff --git a/paddler_balancer/src/agent_controller_pool.rs b/paddler_balancer/src/agent_controller_pool.rs index 795175ca..35922e6c 100644 --- a/paddler_balancer/src/agent_controller_pool.rs +++ b/paddler_balancer/src/agent_controller_pool.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use anyhow::Result; +use anyhow::anyhow; use async_trait::async_trait; use dashmap::DashMap; use paddler_messaging::agent_controller_pool_snapshot::AgentControllerPoolSnapshot; @@ -95,7 +96,7 @@ impl AgentControllerPool { Ok(()) } else { - Err(anyhow::anyhow!("AgentController already registered")) + Err(anyhow!("AgentController already registered")) } } diff --git a/paddler_balancer/src/compatibility/openai_service/arguments_to_tool_call_string.rs b/paddler_balancer/src/compatibility/openai_service/arguments_to_tool_call_string.rs index 0a831454..019659b4 100644 --- a/paddler_balancer/src/compatibility/openai_service/arguments_to_tool_call_string.rs +++ b/paddler_balancer/src/compatibility/openai_service/arguments_to_tool_call_string.rs @@ -14,16 +14,16 @@ pub fn arguments_to_tool_call_string(arguments: &ToolCallArguments) -> Result String { fn description_from_error_token(token: &GeneratedTokenResult) -> Option<&str> { match token { GeneratedTokenResult::ChatTemplateError(description) + | GeneratedTokenResult::DetokenizationFailed(description) | GeneratedTokenResult::GrammarIncompatibleWithThinking(description) | GeneratedTokenResult::GrammarRejectedModelOutput(description) | GeneratedTokenResult::GrammarInitializationFailed(description) @@ -115,6 +116,8 @@ impl OpenAIError { #[cfg(test)] mod tests { use llama_cpp_bindings_types::ToolCallArguments; + use serde_json::json; + use paddler_messaging::embedding_result::EmbeddingResult; use paddler_messaging::generation_summary::GenerationSummary; @@ -253,7 +256,7 @@ mod tests { let parsed = vec![llama_cpp_bindings_types::ParsedToolCall::new( "call_x".to_owned(), "get_weather".to_owned(), - ToolCallArguments::ValidJson(serde_json::json!({"location": "Paris"})), + ToolCallArguments::ValidJson(json!({"location": "Paris"})), )]; assert!( diff --git a/paddler_balancer/src/compatibility/openai_service/openai_message.rs b/paddler_balancer/src/compatibility/openai_service/openai_message.rs index a8cab097..36915735 100644 --- a/paddler_balancer/src/compatibility/openai_service/openai_message.rs +++ b/paddler_balancer/src/compatibility/openai_service/openai_message.rs @@ -20,11 +20,13 @@ impl OpenAIMessage { #[cfg(test)] mod tests { + use serde_json::json; + use super::OpenAIMessage; #[test] fn openai_message_converts_to_conversation_message() { - let input = serde_json::json!({ + let input = json!({ "role": "user", "content": [ {"type": "text", "text": "OCR this"}, diff --git a/paddler_balancer/src/compatibility/openai_service/responses_response_builder.rs b/paddler_balancer/src/compatibility/openai_service/responses_response_builder.rs index 1bbe52a4..519d7e58 100644 --- a/paddler_balancer/src/compatibility/openai_service/responses_response_builder.rs +++ b/paddler_balancer/src/compatibility/openai_service/responses_response_builder.rs @@ -78,6 +78,7 @@ impl ResponsesResponseBuilder { #[cfg(test)] mod tests { use llama_cpp_bindings_types::TokenUsage; + use serde_json::json; use super::ResponsesResponseBuilder; use crate::compatibility::openai_service::openai_error::OpenAIError; @@ -97,7 +98,7 @@ mod tests { assert_eq!(response["status"], "in_progress"); assert_eq!(response["object"], "response"); - assert_eq!(response["output"], serde_json::json!([])); + assert_eq!(response["output"], json!([])); assert!(response.get("usage").is_none()); } @@ -116,7 +117,7 @@ mod tests { #[test] fn completed_includes_usage_and_output() { let response = builder().completed( - vec![serde_json::json!({ "type": "message" })], + vec![json!({ "type": "message" })], &TokenUsage { prompt_tokens: 3, cached_prompt_tokens: 0, diff --git a/paddler_balancer/src/compatibility/openai_service/responses_streaming_response_transformer.rs b/paddler_balancer/src/compatibility/openai_service/responses_streaming_response_transformer.rs index f07b3453..13e28bb7 100644 --- a/paddler_balancer/src/compatibility/openai_service/responses_streaming_response_transformer.rs +++ b/paddler_balancer/src/compatibility/openai_service/responses_streaming_response_transformer.rs @@ -69,10 +69,6 @@ impl ResponsesStreamingResponseTransformer { impl TransformsOutgoingMessage for ResponsesStreamingResponseTransformer { type Output = ResponsesStreamEvent; - #[expect( - clippy::significant_drop_tightening, - reason = "one guard must span the whole per-message state transition; calls are serial so there is no contention" - )] async fn transform(&self, message: OutgoingMessage) -> Result> { let mut events: Vec = Vec::new(); let mut state = self.state.lock(); diff --git a/paddler_balancer/src/controls_websocket_endpoint.rs b/paddler_balancer/src/controls_websocket_endpoint.rs index d0ec7e96..3af5dec9 100644 --- a/paddler_balancer/src/controls_websocket_endpoint.rs +++ b/paddler_balancer/src/controls_websocket_endpoint.rs @@ -473,10 +473,6 @@ mod tests { .insert_header((header::SEC_WEBSOCKET_KEY, "dGhlIHNhbXBsZSBub25jZQ==")) } - #[expect( - clippy::future_not_send, - reason = "test-only helper; the future is awaited in place, never sent across threads" - )] async fn open_session() -> Session { let (request, mut raw_payload) = handshake_request().to_http_parts(); let payload = Payload::from_request(&request, &mut raw_payload) @@ -568,10 +564,6 @@ mod tests { assert!(connection_close.is_cancelled()); } - #[expect( - clippy::future_not_send, - reason = "test-only helper; the future is awaited in place, never sent across threads" - )] async fn drain_close_frame(endpoint: &impl ControlsWebSocketEndpoint) -> Bytes { let (request, mut raw_payload) = handshake_request().to_http_parts(); let payload = Payload::from_request(&request, &mut raw_payload) diff --git a/paddler_balancer/src/inference_service/http_route/api/post_generate_embedding_batch.rs b/paddler_balancer/src/inference_service/http_route/api/post_generate_embedding_batch.rs index 64576b88..90d51271 100644 --- a/paddler_balancer/src/inference_service/http_route/api/post_generate_embedding_batch.rs +++ b/paddler_balancer/src/inference_service/http_route/api/post_generate_embedding_batch.rs @@ -181,7 +181,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body; use actix_web::web; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; @@ -299,7 +302,7 @@ mod tests { #[actix_web::test] async fn responds_service_unavailable_when_balancer_state_is_not_set() { - let app = test::init_service( + let app = init_service( App::new() .app_data(web::Data::new(app_data( Arc::new(AgentControllerPool::default()), @@ -309,18 +312,18 @@ mod tests { ) .await; - let request = test::TestRequest::post() + let request = TestRequest::post() .uri("/api/v1/generate_embedding_batch") .set_json(single_document_params()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } #[actix_web::test] async fn responds_service_unavailable_when_no_agents_are_connected() { - let app = test::init_service( + let app = init_service( App::new() .app_data(web::Data::new(app_data( Arc::new(AgentControllerPool::default()), @@ -332,11 +335,11 @@ mod tests { ) .await; - let request = test::TestRequest::post() + let request = TestRequest::post() .uri("/api/v1/generate_embedding_batch") .set_json(single_document_params()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); } @@ -352,7 +355,7 @@ mod tests { ) .unwrap(); - let app = test::init_service( + let app = init_service( App::new() .app_data(web::Data::new(app_data( agent_controller_pool, @@ -364,11 +367,11 @@ mod tests { ) .await; - let request = test::TestRequest::post() + let request = TestRequest::post() .uri("/api/v1/generate_embedding_batch") .set_json(single_document_params()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -384,7 +387,7 @@ mod tests { ) .unwrap(); - let app = test::init_service( + let app = init_service( App::new() .app_data(web::Data::new(app_data( agent_controller_pool, @@ -396,15 +399,15 @@ mod tests { ) .await; - let request = test::TestRequest::post() + let request = TestRequest::post() .uri("/api/v1/generate_embedding_batch") .set_json(single_document_params()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let body = test::read_body(response).await; + let body = read_body(response).await; let body_text = String::from_utf8(body.to_vec()).unwrap(); assert!( diff --git a/paddler_balancer/src/inference_service/http_route/api/ws_inference_socket/mod.rs b/paddler_balancer/src/inference_service/http_route/api/ws_inference_socket/mod.rs index fec33e78..8c5bfb5d 100644 --- a/paddler_balancer/src/inference_service/http_route/api/ws_inference_socket/mod.rs +++ b/paddler_balancer/src/inference_service/http_route/api/ws_inference_socket/mod.rs @@ -120,10 +120,6 @@ impl ControlsWebSocketEndpoint for InferenceSocketController { } #[get("/api/v1/inference_socket")] -#[expect( - clippy::future_not_send, - reason = "actix-web handler futures are inherently !Send: each worker runs them on its own single-threaded runtime and never moves them across threads" -)] async fn respond( app_data: Data, payload: Payload, @@ -158,8 +154,9 @@ mod tests { use actix_web::FromRequest as _; use actix_web::http::StatusCode; use actix_web::http::header; - use actix_web::test; use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; use actix_web::web::Data; use actix_web::web::Payload; use tokio::sync::mpsc; @@ -253,10 +250,6 @@ mod tests { }) } - #[expect( - clippy::future_not_send, - reason = "actix_ws::Session is !Send and the future is awaited in place" - )] async fn open_session_controller() -> WebSocketSessionController { let (request, mut raw_payload) = TestRequest::get() .insert_header((header::CONNECTION, "upgrade")) @@ -329,16 +322,16 @@ mod tests { inference_service_configuration: inference_service_configuration(), shutdown: CancellationToken::new(), }); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; + let app = init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let request = TestRequest::get() .uri("/api/v1/inference_socket") .insert_header((header::UPGRADE, "websocket")) .insert_header((header::CONNECTION, "Upgrade")) .insert_header((header::SEC_WEBSOCKET_VERSION, "13")) .insert_header((header::SEC_WEBSOCKET_KEY, "dGhlIHNhbXBsZSBub25jZQ==")) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::SWITCHING_PROTOCOLS); } diff --git a/paddler_balancer/src/management_service/http_route/api/get_agents.rs b/paddler_balancer/src/management_service/http_route/api/get_agents.rs index 53d83e71..2f241465 100644 --- a/paddler_balancer/src/management_service/http_route/api/get_agents.rs +++ b/paddler_balancer/src/management_service/http_route/api/get_agents.rs @@ -33,7 +33,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body_json; use actix_web::web::Data; use tokio::sync::broadcast; use tokio::sync::mpsc; @@ -122,18 +125,18 @@ mod tests { ) .unwrap(); - let app = test::init_service( + let app = init_service( App::new() .app_data(app_data_with_pool(agent_controller_pool)) .configure(register), ) .await; - let request = test::TestRequest::get().uri("/api/v1/agents").to_request(); - let response = test::call_service(&app, request).await; + let request = TestRequest::get().uri("/api/v1/agents").to_request(); + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let snapshot: AgentControllerPoolSnapshot = test::read_body_json(response).await; + let snapshot: AgentControllerPoolSnapshot = read_body_json(response).await; assert_eq!(snapshot.agents.len(), 1); assert_eq!(snapshot.agents[0].id, "agent-test"); @@ -150,14 +153,14 @@ mod tests { ) .unwrap(); - let app = test::init_service( + let app = init_service( App::new() .app_data(app_data_with_pool(agent_controller_pool)) .configure(register), ) .await; - let request = test::TestRequest::get().uri("/api/v1/agents").to_request(); - let response = test::call_service(&app, request).await; + let request = TestRequest::get().uri("/api/v1/agents").to_request(); + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/paddler_balancer/src/management_service/http_route/api/get_balancer_applicable_state.rs b/paddler_balancer/src/management_service/http_route/api/get_balancer_applicable_state.rs index 6140eeb7..e64fefd0 100644 --- a/paddler_balancer/src/management_service/http_route/api/get_balancer_applicable_state.rs +++ b/paddler_balancer/src/management_service/http_route/api/get_balancer_applicable_state.rs @@ -26,7 +26,11 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body; + use actix_web::test::read_body_json; use actix_web::web::Data; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; @@ -92,15 +96,15 @@ mod tests { )); let app_data = build_app_data(balancer_applicable_state_holder); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get() .uri("/api/v1/balancer_applicable_state") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let agent_desired_state: AgentDesiredState = test::read_body_json(response).await; + let agent_desired_state: AgentDesiredState = read_body_json(response).await; assert_eq!( agent_desired_state.model, @@ -111,15 +115,15 @@ mod tests { #[actix_web::test] async fn responds_with_json_null_when_no_state_is_set() { let app_data = build_app_data(Arc::new(BalancerApplicableStateHolder::default())); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get() .uri("/api/v1/balancer_applicable_state") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let body = test::read_body(response).await; + let body = read_body(response).await; assert_eq!(body.as_ref(), b"null"); } diff --git a/paddler_balancer/src/management_service/http_route/api/get_balancer_desired_state.rs b/paddler_balancer/src/management_service/http_route/api/get_balancer_desired_state.rs index 13c3470e..282e0272 100644 --- a/paddler_balancer/src/management_service/http_route/api/get_balancer_desired_state.rs +++ b/paddler_balancer/src/management_service/http_route/api/get_balancer_desired_state.rs @@ -29,7 +29,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body_json; use actix_web::web::Data; use tempfile::TempDir; use tokio::sync::broadcast; @@ -88,15 +91,15 @@ mod tests { stored_state.clone(), )); let app_data = build_app_data(state_database); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get() .uri("/api/v1/balancer_desired_state") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let desired_state: BalancerDesiredState = test::read_body_json(response).await; + let desired_state: BalancerDesiredState = read_body_json(response).await; assert_eq!(desired_state, stored_state); } @@ -111,11 +114,11 @@ mod tests { temp_dir.path().to_path_buf(), )); let app_data = build_app_data(state_database); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get() .uri("/api/v1/balancer_desired_state") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/paddler_balancer/src/management_service/http_route/api/get_buffered_requests.rs b/paddler_balancer/src/management_service/http_route/api/get_buffered_requests.rs index 1fd3c55d..619edf49 100644 --- a/paddler_balancer/src/management_service/http_route/api/get_buffered_requests.rs +++ b/paddler_balancer/src/management_service/http_route/api/get_buffered_requests.rs @@ -28,7 +28,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body_json; use actix_web::web::Data; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; @@ -82,15 +85,15 @@ mod tests { statsd_prefix: "paddler".to_owned(), }); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get() .uri("/api/v1/buffered_requests") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let snapshot: BufferedRequestManagerSnapshot = test::read_body_json(response).await; + let snapshot: BufferedRequestManagerSnapshot = read_body_json(response).await; assert_eq!(snapshot.buffered_requests_current, 2); } diff --git a/paddler_balancer/src/management_service/http_route/api/put_balancer_desired_state.rs b/paddler_balancer/src/management_service/http_route/api/put_balancer_desired_state.rs index 0fcc429c..bc5b23dc 100644 --- a/paddler_balancer/src/management_service/http_route/api/put_balancer_desired_state.rs +++ b/paddler_balancer/src/management_service/http_route/api/put_balancer_desired_state.rs @@ -43,7 +43,9 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; use actix_web::web::Data; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; @@ -91,12 +93,12 @@ mod tests { BalancerDesiredState::default(), )); let app_data = build_app_data(state_database.clone()); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::put() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::put() .uri("/api/v1/balancer_desired_state") .set_json(BalancerDesiredState::default()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::NO_CONTENT); @@ -112,7 +114,7 @@ mod tests { BalancerDesiredState::default(), )); let app_data = build_app_data(state_database); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; + let app = init_service(App::new().app_data(app_data).configure(register)).await; let invalid_desired_state = BalancerDesiredState { inference_parameters: InferenceParameters { image_resize_to_fit: 0, @@ -120,11 +122,11 @@ mod tests { }, ..BalancerDesiredState::default() }; - let request = test::TestRequest::put() + let request = TestRequest::put() .uri("/api/v1/balancer_desired_state") .set_json(invalid_desired_state) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); @@ -143,12 +145,12 @@ mod tests { BalancerDesiredState::default(), )); let app_data = build_app_data(state_database); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::put() + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::put() .uri("/api/v1/balancer_desired_state") .set_json(BalancerDesiredState::default()) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/paddler_balancer/src/management_service/http_route/api/ws_agent_socket/mod.rs b/paddler_balancer/src/management_service/http_route/api/ws_agent_socket/mod.rs index cda3eb7a..050a30ac 100644 --- a/paddler_balancer/src/management_service/http_route/api/ws_agent_socket/mod.rs +++ b/paddler_balancer/src/management_service/http_route/api/ws_agent_socket/mod.rs @@ -312,10 +312,6 @@ struct PathParams { } #[get("/api/v1/agent_socket/{agent_id}")] -#[expect( - clippy::future_not_send, - reason = "actix-web handlers run on a single-threaded runtime" -)] async fn respond( app_data: Data, path_params: Path, diff --git a/paddler_balancer/src/management_service/mod.rs b/paddler_balancer/src/management_service/mod.rs index a89731db..a6324f64 100644 --- a/paddler_balancer/src/management_service/mod.rs +++ b/paddler_balancer/src/management_service/mod.rs @@ -142,6 +142,7 @@ mod tests { use std::sync::Arc; use std::time::Duration; + use anyhow::Result; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; use trzcina::Service as _; @@ -197,40 +198,40 @@ mod tests { } } - #[expect( - clippy::unwrap_used, - reason = "test fixture helper; allow-unwrap-in-tests is not applied to non-#[test] helpers inside cfg(all(test, feature)) modules" - )] - fn make_resolved_socket_addr(input_addr: &str) -> ResolvedSocketAddr { - ResolvedSocketAddr { + fn make_resolved_socket_addr(input_addr: &str) -> Result { + Ok(ResolvedSocketAddr { input_addr: input_addr.to_owned(), - socket_addr: input_addr.parse().unwrap(), - } + socket_addr: input_addr.parse()?, + }) } - fn make_web_admin_panel_configuration(addr: SocketAddr) -> WebAdminPanelServiceConfiguration { - WebAdminPanelServiceConfiguration { + fn make_web_admin_panel_configuration( + addr: SocketAddr, + ) -> Result { + Ok(WebAdminPanelServiceConfiguration { addr, template_data: TemplateData { buffered_request_timeout: Duration::from_secs(1), compat_openai_addr: None, - inference_addr: make_resolved_socket_addr("127.0.0.1:8081"), - management_addr: make_resolved_socket_addr("127.0.0.1:8082"), + inference_addr: make_resolved_socket_addr("127.0.0.1:8081")?, + management_addr: make_resolved_socket_addr("127.0.0.1:8082")?, max_buffered_requests: 1, statsd_addr: None, statsd_prefix: "paddler".to_owned(), statsd_reporting_interval: Duration::from_secs(1), }, - } + }) } #[test] - fn builds_http_origin_from_web_admin_panel_addr() { - let configuration = make_web_admin_panel_configuration("127.0.0.1:9000".parse().unwrap()); + fn builds_http_origin_from_web_admin_panel_addr() -> Result<()> { + let configuration = make_web_admin_panel_configuration("127.0.0.1:9000".parse()?)?; let allowed_hosts = collect_web_admin_panel_cors_allowed_hosts(Some(&configuration)); assert_eq!(allowed_hosts, vec!["http://127.0.0.1:9000".to_owned()]); + + Ok(()) } #[test] diff --git a/paddler_balancer/src/response/view_from_http_response_builder.rs b/paddler_balancer/src/response/view_from_http_response_builder.rs index 6b49a598..60019564 100644 --- a/paddler_balancer/src/response/view_from_http_response_builder.rs +++ b/paddler_balancer/src/response/view_from_http_response_builder.rs @@ -18,7 +18,7 @@ pub fn view_from_http_response_builder( #[cfg(test)] mod tests { use std::fmt; - use std::mem; + use std::mem::discriminant; use actix_web::HttpResponse; use actix_web::http::StatusCode; @@ -164,8 +164,8 @@ mod tests { .unwrap(); assert_eq!( - mem::discriminant(&write_error), - mem::discriminant(&AskamaError::ValueMissing), + discriminant(&write_error), + discriminant(&AskamaError::ValueMissing), ); } @@ -178,9 +178,6 @@ mod tests { .err() .unwrap(); - assert_eq!( - mem::discriminant(&write_error), - mem::discriminant(&AskamaError::Fmt), - ); + assert_eq!(discriminant(&write_error), discriminant(&AskamaError::Fmt),); } } diff --git a/paddler_balancer/src/state_database/file/mod.rs b/paddler_balancer/src/state_database/file/mod.rs index 1ffaaf75..4ad68eca 100644 --- a/paddler_balancer/src/state_database/file/mod.rs +++ b/paddler_balancer/src/state_database/file/mod.rs @@ -7,7 +7,7 @@ use anyhow::Result; use async_trait::async_trait; use log::warn; use paddler_messaging::balancer_desired_state::BalancerDesiredState; -use tokio::fs; +use tokio::fs::read_to_string; use tokio::io::AsyncWriteExt; use tokio::sync::RwLock; use tokio::sync::broadcast; @@ -35,7 +35,7 @@ impl File { } async fn read_schema_from_file(&self) -> Result { - match fs::read_to_string(&self.path).await { + match read_to_string(&self.path).await { Ok(content) => { if content.is_empty() { return self.store_default_schema().await; @@ -73,7 +73,7 @@ impl File { let serialized_schema = serde_json::to_string_pretty(schema) .context("Failed to serialize the state database schema")?; - let mut file = fs::File::create(&self.path).await?; + let mut file = tokio::fs::File::create(&self.path).await?; file.write_all(serialized_schema.as_bytes()).await?; file.sync_all().await?; @@ -127,7 +127,8 @@ mod tests { use log::LevelFilter; use tempfile::NamedTempFile; use tempfile::TempDir; - use tokio::fs; + use tokio::fs::metadata; + use tokio::fs::write; use tokio::sync::broadcast; use super::File; @@ -160,7 +161,7 @@ mod tests { let read_back = database.read_balancer_desired_state().await.unwrap(); assert_eq!(read_back.model, desired_state.model); - assert!(fs::metadata(&path).await.unwrap().is_file()); + assert!(metadata(&path).await.unwrap().is_file()); } #[tokio::test] @@ -174,7 +175,7 @@ mod tests { let read_state = database.read_balancer_desired_state().await.unwrap(); assert_eq!(read_state, BalancerDesiredState::default()); - assert!(fs::metadata(&path).await.unwrap().is_file()); + assert!(metadata(&path).await.unwrap().is_file()); } #[tokio::test] @@ -183,7 +184,7 @@ mod tests { broadcast::channel(8); let temp_file = NamedTempFile::new().unwrap(); let path = temp_file.path().to_path_buf(); - fs::write(&path, b"this is not valid json").await.unwrap(); + write(&path, b"this is not valid json").await.unwrap(); let database = File::new(balancer_desired_state_notify_tx, path); let read_result = database.read_balancer_desired_state().await; @@ -265,7 +266,7 @@ mod tests { let read_state = database.read_balancer_desired_state().await.unwrap(); assert_eq!(read_state, BalancerDesiredState::default()); - assert!(fs::metadata(&path).await.unwrap().is_file()); + assert!(metadata(&path).await.unwrap().is_file()); } #[tokio::test] diff --git a/paddler_balancer/src/statsd_service/mod.rs b/paddler_balancer/src/statsd_service/mod.rs index a683152e..7de8c06b 100644 --- a/paddler_balancer/src/statsd_service/mod.rs +++ b/paddler_balancer/src/statsd_service/mod.rs @@ -21,10 +21,6 @@ use crate::agent_controller_pool_total_slots::AgentControllerPoolTotalSlots; use crate::buffered_request_manager::BufferedRequestManager; use crate::statsd_service::configuration::Configuration as StatsdServiceConfiguration; -#[expect( - clippy::needless_pass_by_value, - reason = "cadence StatsdClient::with_error_handler requires an owned Fn(MetricError) handler" -)] fn log_statsd_error(error: MetricError) { error!("Statsd error: {error}"); } diff --git a/paddler_balancer/src/web_admin_panel_service/http_route/favicon.rs b/paddler_balancer/src/web_admin_panel_service/http_route/favicon.rs index 116fe59e..8d823e62 100644 --- a/paddler_balancer/src/web_admin_panel_service/http_route/favicon.rs +++ b/paddler_balancer/src/web_admin_panel_service/http_route/favicon.rs @@ -21,16 +21,19 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; use actix_web::http::header; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body; use super::FAVICON; use super::register; #[actix_web::test] async fn serves_embedded_favicon_as_svg() { - let app = test::init_service(App::new().configure(register)).await; - let request = test::TestRequest::get().uri("/favicon.ico").to_request(); - let response = test::call_service(&app, request).await; + let app = init_service(App::new().configure(register)).await; + let request = TestRequest::get().uri("/favicon.ico").to_request(); + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -43,7 +46,7 @@ mod tests { "image/svg+xml" ); - let body = test::read_body(response).await; + let body = read_body(response).await; assert_eq!(body.as_ref(), FAVICON); } diff --git a/paddler_balancer/src/web_admin_panel_service/http_route/home.rs b/paddler_balancer/src/web_admin_panel_service/http_route/home.rs index cd98ab99..15bd7d4a 100644 --- a/paddler_balancer/src/web_admin_panel_service/http_route/home.rs +++ b/paddler_balancer/src/web_admin_panel_service/http_route/home.rs @@ -60,7 +60,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body; use actix_web::web::Data; use parking_lot::Once; @@ -107,13 +110,13 @@ mod tests { statsd_reporting_interval: Duration::from_millis(500), }, }); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get().uri("/").to_request(); - let response = test::call_service(&app, request).await; + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get().uri("/").to_request(); + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let body = test::read_body(response).await; + let body = read_body(response).await; let body_text = std::str::from_utf8(body.as_ref()).unwrap(); assert!(body_text.contains("data-compat-openai-addr=\"127.0.0.1:8081\"")); @@ -148,13 +151,13 @@ mod tests { statsd_reporting_interval: Duration::from_millis(500), }, }); - let app = test::init_service(App::new().app_data(app_data).configure(register)).await; - let request = test::TestRequest::get().uri("/").to_request(); - let response = test::call_service(&app, request).await; + let app = init_service(App::new().app_data(app_data).configure(register)).await; + let request = TestRequest::get().uri("/").to_request(); + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); - let body = test::read_body(response).await; + let body = read_body(response).await; let body_text = std::str::from_utf8(body.as_ref()).unwrap(); assert!(body_text.contains("data-compat-openai-addr=\"\"")); diff --git a/paddler_balancer/src/web_admin_panel_service/http_route/static_files.rs b/paddler_balancer/src/web_admin_panel_service/http_route/static_files.rs index 554b8faa..50010845 100644 --- a/paddler_balancer/src/web_admin_panel_service/http_route/static_files.rs +++ b/paddler_balancer/src/web_admin_panel_service/http_route/static_files.rs @@ -27,7 +27,10 @@ mod tests { use actix_web::App; use actix_web::http::StatusCode; use actix_web::http::header::CONTENT_TYPE; - use actix_web::test; + use actix_web::test::TestRequest; + use actix_web::test::call_service; + use actix_web::test::init_service; + use actix_web::test::read_body; use mime_guess::from_path; use super::register; @@ -44,11 +47,11 @@ mod tests { async fn serves_embedded_file_with_guessed_content_type() { let existing_file_path = any_embedded_file_name(); - let app = test::init_service(App::new().configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().configure(register)).await; + let request = TestRequest::get() .uri(&format!("/static/{existing_file_path}")) .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::OK); @@ -61,22 +64,22 @@ mod tests { .unwrap() .data .into_owned(); - let body = test::read_body(response).await; + let body = read_body(response).await; assert_eq!(body.as_ref(), expected_body.as_slice()); } #[actix_web::test] async fn responds_with_not_found_for_missing_file() { - let app = test::init_service(App::new().configure(register)).await; - let request = test::TestRequest::get() + let app = init_service(App::new().configure(register)).await; + let request = TestRequest::get() .uri("/static/this_file_does_not_exist.txt") .to_request(); - let response = test::call_service(&app, request).await; + let response = call_service(&app, request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); - let body = test::read_body(response).await; + let body = read_body(response).await; assert_eq!(body.as_ref(), b"File not found"); } diff --git a/paddler_bootstrap/Cargo.toml b/paddler_bootstrap/Cargo.toml index 68666ac2..e4974139 100644 --- a/paddler_bootstrap/Cargo.toml +++ b/paddler_bootstrap/Cargo.toml @@ -28,9 +28,6 @@ trzcina = { workspace = true } reqwest = { workspace = true } tempfile = { workspace = true } -[target.'cfg(unix)'.dev-dependencies] -nix = { workspace = true } - [lints] workspace = true diff --git a/paddler_bootstrap/src/lib.rs b/paddler_bootstrap/src/lib.rs index 2bd68012..c49f7414 100644 --- a/paddler_bootstrap/src/lib.rs +++ b/paddler_bootstrap/src/lib.rs @@ -4,4 +4,3 @@ pub mod balancer_runner; pub mod balancer_service_bundle; pub mod run_service_manager; pub mod service_thread; -pub mod shutdown_signal; diff --git a/paddler_bootstrap/src/service_thread.rs b/paddler_bootstrap/src/service_thread.rs index 9abbe9dd..19a81bcd 100644 --- a/paddler_bootstrap/src/service_thread.rs +++ b/paddler_bootstrap/src/service_thread.rs @@ -1,5 +1,5 @@ use std::future::Future; -use std::thread; +use std::thread::JoinHandle; use anyhow::Result; use anyhow::anyhow; @@ -11,7 +11,7 @@ use tokio_util::sync::CancellationToken; pub struct ServiceThread { cancellation_token: CancellationToken, completion_rx: Option>>, - thread: Option>, + thread: Option>, } impl ServiceThread { @@ -23,7 +23,7 @@ impl ServiceThread { let task_token = cancellation_token.clone(); let (completion_tx, completion_rx) = oneshot::channel::>(); - let thread = thread::spawn(move || { + let thread = std::thread::spawn(move || { let result = actix_web::rt::System::new().block_on(run(task_token)); if let Err(unsent) = completion_tx.send(result) { match unsent { diff --git a/paddler_bootstrap/src/shutdown_signal/mod.rs b/paddler_bootstrap/src/shutdown_signal/mod.rs deleted file mode 100644 index 9453fb58..00000000 --- a/paddler_bootstrap/src/shutdown_signal/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[cfg(unix)] -mod unix; -#[cfg(windows)] -mod windows; - -#[cfg(unix)] -pub use unix::ShutdownSignals; -#[cfg(unix)] -pub use unix::register_shutdown_signals; -#[cfg(windows)] -pub use windows::ShutdownSignals; -#[cfg(windows)] -pub use windows::register_shutdown_signals; diff --git a/paddler_bootstrap/src/shutdown_signal/unix.rs b/paddler_bootstrap/src/shutdown_signal/unix.rs deleted file mode 100644 index 05a4a86b..00000000 --- a/paddler_bootstrap/src/shutdown_signal/unix.rs +++ /dev/null @@ -1,99 +0,0 @@ -use anyhow::Context as _; -use anyhow::Result; -use log::info; -use tokio::signal::unix::Signal; -use tokio::signal::unix::SignalKind; -use tokio::signal::unix::signal; - -pub struct ShutdownSignals { - sigterm: Signal, - sigint: Signal, - sighup: Signal, -} - -impl ShutdownSignals { - pub async fn wait(mut self) -> Result<()> { - tokio::select! { - _ = self.sigterm.recv() => info!("Received SIGTERM"), - _ = self.sigint.recv() => info!("Received SIGINT"), - _ = self.sighup.recv() => info!("Received SIGHUP"), - } - - Ok(()) - } -} - -fn listen_for_signal(kind: SignalKind, description: &str) -> Result { - signal(kind).with_context(|| format!("failed to listen for {description}")) -} - -fn register_signals( - terminate_kind: SignalKind, - interrupt_kind: SignalKind, - hangup_kind: SignalKind, -) -> Result { - Ok(ShutdownSignals { - sigterm: listen_for_signal(terminate_kind, "SIGTERM")?, - sigint: listen_for_signal(interrupt_kind, "SIGINT")?, - sighup: listen_for_signal(hangup_kind, "SIGHUP")?, - }) -} - -pub fn register_shutdown_signals() -> Result { - register_signals( - SignalKind::terminate(), - SignalKind::interrupt(), - SignalKind::hangup(), - ) -} - -#[cfg(test)] -mod tests { - use nix::sys::signal::Signal as UnixSignal; - use nix::sys::signal::raise; - use tokio::signal::unix::SignalKind; - - use super::register_shutdown_signals; - use super::register_signals; - - #[tokio::test] - async fn wait_returns_on_each_shutdown_signal() { - for shutdown_signal in [UnixSignal::SIGTERM, UnixSignal::SIGINT, UnixSignal::SIGHUP] { - let shutdown_signals = register_shutdown_signals().unwrap(); - - raise(shutdown_signal).unwrap(); - - shutdown_signals.wait().await.unwrap(); - } - } - - #[tokio::test] - async fn register_signals_errors_for_unregisterable_signal() { - let unregisterable = SignalKind::from_raw(UnixSignal::SIGKILL as i32); - - assert!( - register_signals( - unregisterable, - SignalKind::interrupt(), - SignalKind::hangup() - ) - .is_err() - ); - assert!( - register_signals( - SignalKind::terminate(), - unregisterable, - SignalKind::hangup() - ) - .is_err() - ); - assert!( - register_signals( - SignalKind::terminate(), - SignalKind::interrupt(), - unregisterable - ) - .is_err() - ); - } -} diff --git a/paddler_bootstrap/src/shutdown_signal/windows.rs b/paddler_bootstrap/src/shutdown_signal/windows.rs deleted file mode 100644 index 4d9df50d..00000000 --- a/paddler_bootstrap/src/shutdown_signal/windows.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Context as _; -use anyhow::Result; -use log::info; -use tokio::signal::windows::CtrlBreak; -use tokio::signal::windows::CtrlC; -use tokio::signal::windows::CtrlClose; -use tokio::signal::windows::CtrlShutdown; -use tokio::signal::windows::ctrl_break; -use tokio::signal::windows::ctrl_c; -use tokio::signal::windows::ctrl_close; -use tokio::signal::windows::ctrl_shutdown; - -#[expect( - clippy::struct_field_names, - reason = "field names mirror the Windows console control event types they hold; the shared `ctrl_` prefix is part of the Windows API vocabulary, and `break` is a reserved keyword" -)] -pub struct ShutdownSignals { - ctrl_c: CtrlC, - ctrl_break: CtrlBreak, - ctrl_close: CtrlClose, - ctrl_shutdown: CtrlShutdown, -} - -impl ShutdownSignals { - pub async fn wait(mut self) -> Result<()> { - tokio::select! { - _ = self.ctrl_c.recv() => info!("Received Ctrl+C"), - _ = self.ctrl_break.recv() => info!("Received Ctrl+Break"), - _ = self.ctrl_close.recv() => info!("Received console close"), - _ = self.ctrl_shutdown.recv() => info!("Received system shutdown"), - } - - Ok(()) - } -} - -pub fn register_shutdown_signals() -> Result { - let ctrl_c = ctrl_c().context("failed to listen for Ctrl+C")?; - let ctrl_break = ctrl_break().context("failed to listen for Ctrl+Break")?; - let ctrl_close = ctrl_close().context("failed to listen for console close")?; - let ctrl_shutdown = ctrl_shutdown().context("failed to listen for system shutdown")?; - - Ok(ShutdownSignals { - ctrl_c, - ctrl_break, - ctrl_close, - ctrl_shutdown, - }) -} diff --git a/paddler_cache_dir/src/cache_dir/mod.rs b/paddler_cache_dir/src/cache_dir/mod.rs deleted file mode 100644 index a2e566c3..00000000 --- a/paddler_cache_dir/src/cache_dir/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(unix)] -mod unix; -#[cfg(unix)] -pub use crate::cache_dir::unix::CacheDir; - -#[cfg(windows)] -mod windows; -#[cfg(windows)] -pub use crate::cache_dir::windows::CacheDir; diff --git a/paddler_cache_dir/src/cache_dir/unix.rs b/paddler_cache_dir/src/cache_dir/unix.rs index 7ac0d285..f2be9ed3 100644 --- a/paddler_cache_dir/src/cache_dir/unix.rs +++ b/paddler_cache_dir/src/cache_dir/unix.rs @@ -1,3 +1,4 @@ +use std::env::var; use std::path::PathBuf; use anyhow::Context as _; @@ -13,9 +14,9 @@ impl CacheDir { #[must_use] pub fn from_process_env() -> Self { Self { - explicit: std::env::var("PADDLER_CACHE_DIR").ok(), - home: std::env::var("HOME").ok(), - xdg: std::env::var("XDG_CACHE_HOME").ok(), + explicit: var("PADDLER_CACHE_DIR").ok(), + home: var("HOME").ok(), + xdg: var("XDG_CACHE_HOME").ok(), } } @@ -39,7 +40,7 @@ impl CacheDir { #[cfg(test)] mod tests { - use crate::cache_dir::unix::CacheDir; + use super::CacheDir; #[test] fn explicit_value_wins_over_xdg_and_home() { diff --git a/paddler_cache_dir/src/cache_dir/windows.rs b/paddler_cache_dir/src/cache_dir/windows.rs index 88606051..ad0a3fb4 100644 --- a/paddler_cache_dir/src/cache_dir/windows.rs +++ b/paddler_cache_dir/src/cache_dir/windows.rs @@ -1,3 +1,4 @@ +use std::env::var; use std::path::PathBuf; use anyhow::Context as _; @@ -13,9 +14,9 @@ impl CacheDir { #[must_use] pub fn from_process_env() -> Self { Self { - explicit: std::env::var("PADDLER_CACHE_DIR").ok(), - localappdata: std::env::var("LOCALAPPDATA").ok(), - userprofile: std::env::var("USERPROFILE").ok(), + explicit: var("PADDLER_CACHE_DIR").ok(), + localappdata: var("LOCALAPPDATA").ok(), + userprofile: var("USERPROFILE").ok(), } } @@ -42,7 +43,7 @@ impl CacheDir { #[cfg(test)] mod tests { - use crate::cache_dir::windows::CacheDir; + use super::CacheDir; #[test] fn explicit_value_wins_over_localappdata_and_userprofile() { diff --git a/paddler_cache_dir/src/cached_downloaded_model.rs b/paddler_cache_dir/src/cached_downloaded_model.rs index 2fbd586f..8b3c6c35 100644 --- a/paddler_cache_dir/src/cached_downloaded_model.rs +++ b/paddler_cache_dir/src/cached_downloaded_model.rs @@ -5,7 +5,8 @@ use anyhow::Result; use fslock::LockFile; use sha2::Digest; use sha2::Sha256; -use tokio::fs; +use tokio::fs::create_dir_all; +use tokio::fs::try_exists; use crate::cache_dir::CacheDir; use crate::cached_downloaded_model_lock::CachedDownloadedModelLock; @@ -45,11 +46,11 @@ impl CachedDownloadedModel { } pub async fn is_cached(&self) -> Result { - fs::try_exists(&self.cache_file_path).await + try_exists(&self.cache_file_path).await } pub async fn ensure_cache_subdir_exists(&self) -> Result<(), std::io::Error> { - fs::create_dir_all(&self.cache_subdir).await + create_dir_all(&self.cache_subdir).await } pub fn try_acquire_download_lock( diff --git a/paddler_cache_dir/src/lib.rs b/paddler_cache_dir/src/lib.rs index 45a09025..1875a95e 100644 --- a/paddler_cache_dir/src/lib.rs +++ b/paddler_cache_dir/src/lib.rs @@ -1,3 +1,8 @@ +#[cfg(unix)] +#[path = "cache_dir/unix.rs"] +pub mod cache_dir; +#[cfg(windows)] +#[path = "cache_dir/windows.rs"] pub mod cache_dir; pub mod cached_downloaded_model; pub mod cached_downloaded_model_lock; diff --git a/paddler_cli/Cargo.toml b/paddler_cli/Cargo.toml index 046f538a..c8639fb2 100644 --- a/paddler_cli/Cargo.toml +++ b/paddler_cli/Cargo.toml @@ -16,12 +16,10 @@ actix-web = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true } +command_handler = { workspace = true } env_logger = { workspace = true } -log = { workspace = true } -nanoid = { workspace = true } paddler_balancer = { workspace = true } paddler_bootstrap = { workspace = true } -tokio = { workspace = true } tokio-util = { workspace = true } trzcina = { workspace = true } diff --git a/paddler_cli/src/cmd/agent.rs b/paddler_cli/src/cmd/agent.rs index 8ae21863..79a9a989 100644 --- a/paddler_cli/src/cmd/agent.rs +++ b/paddler_cli/src/cmd/agent.rs @@ -1,13 +1,13 @@ use anyhow::Result; use async_trait::async_trait; use clap::Parser; +use command_handler::handler::Handler; use paddler_balancer::resolved_socket_addr::ResolvedSocketAddr; use paddler_bootstrap::agent_service_bundle::AgentServiceBundle; use tokio_util::sync::CancellationToken; use trzcina::ServiceManager; use trzcina::ServiceShutdownOptions; -use super::handler::Handler; use super::value_parser::parse_socket_addr::parse_socket_addr; #[derive(Parser)] @@ -25,9 +25,9 @@ pub struct Agent { slots: i32, } -#[async_trait] +#[async_trait(?Send)] impl Handler for Agent { - async fn handle(&self, shutdown: CancellationToken) -> Result<()> { + async fn handle(self, shutdown: CancellationToken) -> Result<()> { let bundle = AgentServiceBundle::new( self.name.clone(), &self.management_addr.socket_addr.to_string(), diff --git a/paddler_cli/src/cmd/balancer.rs b/paddler_cli/src/cmd/balancer.rs index b33128fe..873b9dda 100644 --- a/paddler_cli/src/cmd/balancer.rs +++ b/paddler_cli/src/cmd/balancer.rs @@ -3,6 +3,7 @@ use std::time::Duration; use anyhow::Result; use async_trait::async_trait; use clap::Parser; +use command_handler::handler::Handler; use paddler_balancer::compatibility::openai_service::configuration::Configuration as OpenAIServiceConfiguration; use paddler_balancer::inference_service::configuration::Configuration as InferenceServiceConfiguration; use paddler_balancer::management_service::configuration::Configuration as ManagementServiceConfiguration; @@ -19,7 +20,6 @@ use tokio_util::sync::CancellationToken; use trzcina::ServiceManager; use trzcina::ServiceShutdownOptions; -use super::handler::Handler; use super::value_parser::parse_duration::parse_duration; use super::value_parser::parse_socket_addr::parse_socket_addr; @@ -110,9 +110,9 @@ impl Balancer { } } -#[async_trait] +#[async_trait(?Send)] impl Handler for Balancer { - async fn handle(&self, shutdown: CancellationToken) -> Result<()> { + async fn handle(self, shutdown: CancellationToken) -> Result<()> { let shutdown_options = ServiceShutdownOptions::default(); let bundle = BalancerServiceBundle::new(BalancerBootstrapConfig { diff --git a/paddler_cli/src/cmd/handler.rs b/paddler_cli/src/cmd/handler.rs deleted file mode 100644 index 2840065c..00000000 --- a/paddler_cli/src/cmd/handler.rs +++ /dev/null @@ -1,8 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use tokio_util::sync::CancellationToken; - -#[async_trait] -pub trait Handler { - async fn handle(&self, shutdown: CancellationToken) -> Result<()>; -} diff --git a/paddler_cli/src/cmd/mod.rs b/paddler_cli/src/cmd/mod.rs index 8671e1ab..1ca02141 100644 --- a/paddler_cli/src/cmd/mod.rs +++ b/paddler_cli/src/cmd/mod.rs @@ -1,4 +1,3 @@ pub mod agent; pub mod balancer; -pub mod handler; pub mod value_parser; diff --git a/paddler_cli/src/cmd/value_parser/parse_socket_addr.rs b/paddler_cli/src/cmd/value_parser/parse_socket_addr.rs index 152ecf51..9276a9d8 100644 --- a/paddler_cli/src/cmd/value_parser/parse_socket_addr.rs +++ b/paddler_cli/src/cmd/value_parser/parse_socket_addr.rs @@ -1,55 +1,20 @@ -use std::net::SocketAddr; -use std::net::ToSocketAddrs; - use anyhow::Result; -use anyhow::anyhow; -use log::warn; +use command_handler::value_parser::parse_socket_addr::parse_socket_addr as resolve_socket_addr; use paddler_balancer::resolved_socket_addr::ResolvedSocketAddr; -fn resolve_socket_addr(input_addr: &str) -> Result { - let addrs: Vec = input_addr.to_socket_addrs()?.collect(); - - for addr in &addrs { - if addr.is_ipv4() { - return Ok(ResolvedSocketAddr { - input_addr: input_addr.to_owned(), - socket_addr: *addr, - }); - } - } - - for addr in addrs { - if addr.is_ipv6() { - return Ok(ResolvedSocketAddr { - input_addr: input_addr.to_owned(), - socket_addr: addr, - }); - } - } - - Err(anyhow!("Failed to resolve socket address")) -} - pub fn parse_socket_addr(input_addr: &str) -> Result { - match input_addr.parse() { - Ok(socket_addr) => Ok(ResolvedSocketAddr { - input_addr: input_addr.to_owned(), - socket_addr, - }), - Err(err) => { - warn!("Socket addr needs DNS resolution: {err:#?}"); - - Ok(resolve_socket_addr(input_addr)?) - } - } + Ok(ResolvedSocketAddr { + input_addr: input_addr.to_owned(), + socket_addr: resolve_socket_addr(input_addr)?, + }) } #[cfg(test)] mod tests { - use crate::cmd::value_parser::parse_socket_addr::parse_socket_addr; + use super::parse_socket_addr; #[test] - fn parses_ip_and_port_directly() { + fn wraps_resolved_address_with_original_input() { let result = parse_socket_addr("127.0.0.1:8080").unwrap(); assert_eq!(result.input_addr, "127.0.0.1:8080"); @@ -57,27 +22,10 @@ mod tests { assert!(result.socket_addr.is_ipv4()); } - #[test] - fn resolves_localhost_via_dns() { - let result = parse_socket_addr("localhost:9090").unwrap(); - - assert_eq!(result.input_addr, "localhost:9090"); - assert_eq!(result.socket_addr.port(), 9090); - } - #[test] fn rejects_invalid_address() { let result = parse_socket_addr("not-a-valid-host-that-does-not-exist.invalid:1234"); assert!(result.is_err()); } - - #[test] - fn parses_ipv6_address() { - let result = parse_socket_addr("[::1]:8080").unwrap(); - - assert_eq!(result.input_addr, "[::1]:8080"); - assert_eq!(result.socket_addr.port(), 8080); - assert!(result.socket_addr.is_ipv6()); - } } diff --git a/paddler_cli/src/lib.rs b/paddler_cli/src/lib.rs index c6bd3849..0e3fb470 100644 --- a/paddler_cli/src/lib.rs +++ b/paddler_cli/src/lib.rs @@ -5,10 +5,10 @@ use clap::Parser; use clap::Subcommand; use cmd::agent::Agent; use cmd::balancer::Balancer; -use cmd::handler::Handler as _; +use command_handler::handler::Handler as _; +use command_handler::shutdown_signal::register_shutdown_signals; #[cfg(feature = "web_admin_panel")] use esbuild_metafile::instance::initialize_instance; -use paddler_bootstrap::shutdown_signal::register_shutdown_signals; use tokio_util::sync::CancellationToken; #[cfg(feature = "web_admin_panel")] @@ -28,45 +28,29 @@ struct Cli { command: Option, } -#[expect( - clippy::large_enum_variant, - reason = "clap's #[derive(Subcommand)] requires unboxed `Args` payloads (Box is unsupported by the derive); the command is parsed once at startup, so the variant size difference is immaterial" -)] #[derive(Subcommand)] enum Commands { /// Generates tokens and embeddings; connects to the balancer Agent(Agent), /// Distributes incoming requests among agents - Balancer(Balancer), + Balancer(Box), } -async fn run_async() -> Result<()> { - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - - let shutdown_signals = register_shutdown_signals()?; - let shutdown = CancellationToken::new(); - let signal_shutdown = shutdown.clone(); +pub fn run() -> Result<()> { + actix_web::rt::System::new().block_on(async { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - tokio::spawn(async move { - if let Err(error) = shutdown_signals.wait().await { - log::error!("shutdown signal listener failed: {error}"); - return; - } - signal_shutdown.cancel(); - }); + let shutdown: CancellationToken = register_shutdown_signals()?.into(); - match Cli::parse().command { - Some(Commands::Agent(handler)) => handler.handle(shutdown).await, - Some(Commands::Balancer(handler)) => { - #[cfg(feature = "web_admin_panel")] - initialize_instance(ESBUILD_META_CONTENTS); + match Cli::parse().command { + Some(Commands::Agent(handler)) => handler.handle(shutdown).await, + Some(Commands::Balancer(handler)) => { + #[cfg(feature = "web_admin_panel")] + initialize_instance(ESBUILD_META_CONTENTS); - handler.handle(shutdown).await + (*handler).handle(shutdown).await + } + None => Ok(()), } - None => Ok(()), - } -} - -pub fn run() -> Result<()> { - actix_web::rt::System::new().block_on(run_async()) + }) } diff --git a/paddler_cli/src/main.rs b/paddler_cli/src/main.rs index ccd58c6a..e01e6112 100644 --- a/paddler_cli/src/main.rs +++ b/paddler_cli/src/main.rs @@ -1,3 +1,6 @@ -fn main() -> anyhow::Result<()> { - paddler_cli::run() +use anyhow::Result; +use paddler_cli::run; + +fn main() -> Result<()> { + run() } diff --git a/paddler_cli_tests/src/model_card/mod.rs b/paddler_cli_tests/src/model_card/mod.rs index cfba79c6..91d9b96c 100644 --- a/paddler_cli_tests/src/model_card/mod.rs +++ b/paddler_cli_tests/src/model_card/mod.rs @@ -4,6 +4,6 @@ pub mod qwen3_embedding_0_6b; use paddler_messaging::huggingface_model_reference::HuggingFaceModelReference; pub struct ModelCard { - pub gpu_layer_count: u32, + pub gpu_layer_count: i32, pub reference: HuggingFaceModelReference, } diff --git a/paddler_client/src/error.rs b/paddler_client/src/error.rs index de282c30..33e82305 100644 --- a/paddler_client/src/error.rs +++ b/paddler_client/src/error.rs @@ -1,8 +1,4 @@ #[derive(Debug, thiserror::Error)] -#[expect( - clippy::error_impl_error, - reason = "the crate's single public error type is idiomatically named `Error` and correctly implements std::error::Error; renaming would be smurf naming and break the module-named-after-its-item convention" -)] pub enum Error { #[error("HTTP request failed: {0}")] Http(#[from] reqwest::Error), diff --git a/paddler_client/src/inference_socket/spawn_read_task.rs b/paddler_client/src/inference_socket/spawn_read_task.rs index 3bf33be4..5967ad67 100644 --- a/paddler_client/src/inference_socket/spawn_read_task.rs +++ b/paddler_client/src/inference_socket/spawn_read_task.rs @@ -1,5 +1,6 @@ use futures_util::StreamExt; use futures_util::stream::SplitStream; +use log::debug; use log::error; use log::warn; use paddler_messaging::inference_client::message::Message as InferenceMessage; @@ -76,7 +77,7 @@ pub fn spawn_read_task(ws_read: WebSocketReadStream, pending: PendingRequests) - })) .is_err() { - log::debug!("Receiver already dropped for request: {}", entry.key()); + debug!("Receiver already dropped for request: {}", entry.key()); } } diff --git a/paddler_download_manager/src/partial_file.rs b/paddler_download_manager/src/partial_file.rs index c6a261a7..2c59a094 100644 --- a/paddler_download_manager/src/partial_file.rs +++ b/paddler_download_manager/src/partial_file.rs @@ -2,9 +2,12 @@ use std::io; use std::path::Path; use std::path::PathBuf; -use tokio::fs; use tokio::fs::File; use tokio::fs::OpenOptions; +use tokio::fs::create_dir_all; +use tokio::fs::metadata; +use tokio::fs::remove_file; +use tokio::fs::rename; const PARTIAL_EXTENSION: &str = "partial"; @@ -25,7 +28,7 @@ impl PartialFile { } pub async fn current_size(&self) -> Result { - match fs::metadata(&self.partial_path).await { + match metadata(&self.partial_path).await { Ok(metadata) => Ok(metadata.len()), Err(metadata_error) if metadata_error.kind() == io::ErrorKind::NotFound => Ok(0), Err(metadata_error) => Err(metadata_error), @@ -56,11 +59,11 @@ impl PartialFile { } pub async fn finalize(&self) -> Result<(), io::Error> { - fs::rename(&self.partial_path, &self.final_path).await + rename(&self.partial_path, &self.final_path).await } pub async fn remove(&self) -> Result<(), io::Error> { - match fs::remove_file(&self.partial_path).await { + match remove_file(&self.partial_path).await { Ok(()) => Ok(()), Err(remove_error) if remove_error.kind() == io::ErrorKind::NotFound => Ok(()), Err(remove_error) => Err(remove_error), @@ -70,7 +73,7 @@ impl PartialFile { async fn ensure_partial_parent_exists(&self) -> Result<(), io::Error> { let parent = self.partial_path.parent().unwrap_or_else(|| Path::new(".")); - fs::create_dir_all(parent).await + create_dir_all(parent).await } } @@ -80,6 +83,14 @@ mod tests { use std::path::PathBuf; use tempfile::TempDir; + use tokio::fs::create_dir; + use tokio::fs::create_dir_all; + use tokio::fs::metadata; + use tokio::fs::read; + use tokio::fs::remove_dir_all; + use tokio::fs::set_permissions; + use tokio::fs::try_exists; + use tokio::fs::write; use tokio::io::AsyncWriteExt; use crate::partial_file::PartialFile; @@ -98,9 +109,7 @@ mod tests { async fn current_size_returns_existing_size() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"twelve bytes") - .await - .unwrap(); + write(&partial.partial_path, b"twelve bytes").await.unwrap(); let size = partial.current_size().await.unwrap(); @@ -116,7 +125,7 @@ mod tests { file.write_all(b"hello").await.unwrap(); file.flush().await.unwrap(); - let bytes = tokio::fs::read(&partial.partial_path).await.unwrap(); + let bytes = read(&partial.partial_path).await.unwrap(); assert_eq!(bytes, b"hello"); } @@ -124,15 +133,13 @@ mod tests { async fn open_for_append_appends_to_existing() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"first") - .await - .unwrap(); + write(&partial.partial_path, b"first").await.unwrap(); let mut file = partial.open_for_append().await.unwrap(); file.write_all(b"-second").await.unwrap(); file.flush().await.unwrap(); - let bytes = tokio::fs::read(&partial.partial_path).await.unwrap(); + let bytes = read(&partial.partial_path).await.unwrap(); assert_eq!(bytes, b"first-second"); } @@ -140,9 +147,7 @@ mod tests { async fn truncate_resets_to_zero() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"keep me?") - .await - .unwrap(); + write(&partial.partial_path, b"keep me?").await.unwrap(); partial.truncate().await.unwrap(); @@ -154,16 +159,14 @@ mod tests { async fn finalize_renames_partial_to_final() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"complete") - .await - .unwrap(); + write(&partial.partial_path, b"complete").await.unwrap(); let final_path = partial.final_path.clone(); partial.finalize().await.unwrap(); - let exists = tokio::fs::try_exists(&final_path).await.unwrap(); + let exists = try_exists(&final_path).await.unwrap(); assert!(exists); - let bytes = tokio::fs::read(&final_path).await.unwrap(); + let bytes = read(&final_path).await.unwrap(); assert_eq!(bytes, b"complete"); } @@ -171,14 +174,12 @@ mod tests { async fn remove_deletes_partial() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"go away") - .await - .unwrap(); + write(&partial.partial_path, b"go away").await.unwrap(); let partial_path = partial.partial_path.clone(); partial.remove().await.unwrap(); - let exists = tokio::fs::try_exists(&partial_path).await.unwrap(); + let exists = try_exists(&partial_path).await.unwrap(); assert!(!exists); } @@ -195,9 +196,7 @@ mod tests { async fn current_size_propagates_non_notfound_error() { let directory = TempDir::new().unwrap(); let blocking_file = directory.path().join("blocker"); - tokio::fs::write(&blocking_file, b"a regular file") - .await - .unwrap(); + write(&blocking_file, b"a regular file").await.unwrap(); let partial = PartialFile::new(blocking_file.join("subdir").join("model.gguf")); let result = partial.current_size().await; @@ -210,7 +209,7 @@ mod tests { async fn truncate_returns_io_error_when_partial_is_a_directory() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::create_dir(&partial.partial_path).await.unwrap(); + create_dir(&partial.partial_path).await.unwrap(); let result = partial.truncate().await; @@ -222,7 +221,7 @@ mod tests { async fn open_for_append_returns_io_error_when_partial_is_a_directory() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::create_dir(&partial.partial_path).await.unwrap(); + create_dir(&partial.partial_path).await.unwrap(); let result = partial.open_for_append().await; @@ -244,11 +243,9 @@ mod tests { async fn finalize_returns_io_error_when_final_is_a_non_empty_directory() { let directory = TempDir::new().unwrap(); let partial = PartialFile::new(directory.path().join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"complete") - .await - .unwrap(); - tokio::fs::create_dir(&partial.final_path).await.unwrap(); - tokio::fs::write(partial.final_path.join("blocker"), b"x") + write(&partial.partial_path, b"complete").await.unwrap(); + create_dir(&partial.final_path).await.unwrap(); + write(partial.final_path.join("blocker"), b"x") .await .unwrap(); @@ -264,30 +261,18 @@ mod tests { let directory = TempDir::new().unwrap(); let locked_parent = directory.path().join("locked"); - tokio::fs::create_dir(&locked_parent).await.unwrap(); + create_dir(&locked_parent).await.unwrap(); let partial = PartialFile::new(locked_parent.join("model.gguf")); - tokio::fs::write(&partial.partial_path, b"go away") - .await - .unwrap(); - let mut perms = tokio::fs::metadata(&locked_parent) - .await - .unwrap() - .permissions(); + write(&partial.partial_path, b"go away").await.unwrap(); + let mut perms = metadata(&locked_parent).await.unwrap().permissions(); perms.set_mode(0o500); - tokio::fs::set_permissions(&locked_parent, perms) - .await - .unwrap(); + set_permissions(&locked_parent, perms).await.unwrap(); let result = partial.remove().await; - let mut restore = tokio::fs::metadata(&locked_parent) - .await - .unwrap() - .permissions(); + let mut restore = metadata(&locked_parent).await.unwrap().permissions(); restore.set_mode(0o700); - tokio::fs::set_permissions(&locked_parent, restore) - .await - .unwrap(); + set_permissions(&locked_parent, restore).await.unwrap(); assert!(result.is_err()); } @@ -297,7 +282,7 @@ mod tests { async fn open_for_append_fails_when_parent_blocked_by_file() { let directory = TempDir::new().unwrap(); let blocker = directory.path().join("blocker"); - tokio::fs::write(&blocker, b"i am a file").await.unwrap(); + write(&blocker, b"i am a file").await.unwrap(); let partial = PartialFile::new(blocker.join("subdir").join("model.gguf")); let result = partial.open_for_append().await; @@ -310,7 +295,7 @@ mod tests { async fn truncate_fails_when_parent_blocked_by_file() { let directory = TempDir::new().unwrap(); let blocker = directory.path().join("blocker"); - tokio::fs::write(&blocker, b"i am a file").await.unwrap(); + write(&blocker, b"i am a file").await.unwrap(); let partial = PartialFile::new(blocker.join("subdir").join("model.gguf")); let result = partial.truncate().await; @@ -326,13 +311,13 @@ mod tests { let dest = cache_subdir.join("model.gguf"); let partial = PartialFile::new(dest); - tokio::fs::create_dir_all(&cache_subdir).await.unwrap(); + create_dir_all(&cache_subdir).await.unwrap(); let mut file = partial.open_for_append().await.unwrap(); file.write_all(b"partial data").await.unwrap(); file.flush().await.unwrap(); drop(file); - tokio::fs::remove_dir_all(&cache_subdir).await.unwrap(); + remove_dir_all(&cache_subdir).await.unwrap(); let result = partial.finalize().await; diff --git a/paddler_download_manager/src/stream_to_partial_file.rs b/paddler_download_manager/src/stream_to_partial_file.rs index 349faa6f..8239838c 100644 --- a/paddler_download_manager/src/stream_to_partial_file.rs +++ b/paddler_download_manager/src/stream_to_partial_file.rs @@ -47,6 +47,9 @@ mod tests { use futures_util::stream; use tempfile::TempDir; use tokio::fs::OpenOptions; + use tokio::fs::read; + use tokio::fs::write; + use tokio::io::duplex; use crate::progress_sink::ProgressSink; use crate::stream_to_partial_file::stream_to_partial_file; @@ -106,7 +109,7 @@ mod tests { .await .unwrap(); - let bytes = tokio::fs::read(&path).await.unwrap(); + let bytes = read(&path).await.unwrap(); assert_eq!(bytes, b"firstsecond"); } @@ -139,7 +142,7 @@ mod tests { #[tokio::test] async fn write_to_closed_duplex_returns_error() { - let (reader_half, mut writer_half) = tokio::io::duplex(0); + let (reader_half, mut writer_half) = duplex(0); drop(reader_half); let chunks: Vec> = @@ -157,7 +160,7 @@ mod tests { async fn flush_to_read_only_file_returns_error() { let directory = TempDir::new().unwrap(); let path = directory.path().join("read_only.bin"); - tokio::fs::write(&path, b"existing").await.unwrap(); + write(&path, b"existing").await.unwrap(); let chunks: Vec> = vec![Ok(Bytes::from_static(b"more bytes"))]; let body_stream = stream::iter(chunks); diff --git a/paddler_download_manager/tests/download.rs b/paddler_download_manager/tests/download.rs index 7f854c65..f7fb3ada 100644 --- a/paddler_download_manager/tests/download.rs +++ b/paddler_download_manager/tests/download.rs @@ -4,12 +4,21 @@ use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; use anyhow::Result; +use anyhow::anyhow; use anyhow::bail; use paddler_download_manager::download_error::DownloadError; use paddler_download_manager::download_manager::DownloadManager; use paddler_download_manager::progress_sink::ProgressSink; use tempfile::TempDir; +use tokio::fs::create_dir; +use tokio::fs::metadata; +use tokio::fs::read; +use tokio::fs::remove_dir_all; +use tokio::fs::remove_file; +use tokio::fs::set_permissions; +use tokio::fs::try_exists; +use tokio::fs::write; use crate::local_http_fixture::FixtureResponse; use crate::local_http_fixture::LocalHttpFixture; @@ -77,7 +86,7 @@ async fn streams_200_response_to_disk_and_calls_progress_sink_per_chunk() -> Res .download(&fixture.url("/model.gguf"), &dest, progress_sink) .await?; - assert_eq!(tokio::fs::read(&dest).await?, body); + assert_eq!(read(&dest).await?, body); assert_eq!( sink.started_total.load(Ordering::Relaxed), body.len() as u64 @@ -95,7 +104,7 @@ async fn resumes_from_existing_partial_file_with_range_request() -> Result<()> { let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::write(&partial_path, b"first half ").await?; + write(&partial_path, b"first half ").await?; let body = b"second half".to_vec(); let total = 11_u64 + body.len() as u64; @@ -113,7 +122,7 @@ async fn resumes_from_existing_partial_file_with_range_request() -> Result<()> { .download(&fixture.url("/model.gguf"), &dest, progress_sink) .await?; - assert_eq!(tokio::fs::read(&dest).await?, b"first half second half"); + assert_eq!(read(&dest).await?, b"first half second half"); assert_eq!(sink.started_already.load(Ordering::Relaxed), 11); assert!( fixture @@ -130,7 +139,7 @@ async fn starts_over_when_server_returns_200_to_range_request() -> Result<()> { let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::write(&partial_path, b"stale partial bytes").await?; + write(&partial_path, b"stale partial bytes").await?; let body = b"fresh entire body".to_vec(); let fixture = @@ -142,7 +151,7 @@ async fn starts_over_when_server_returns_200_to_range_request() -> Result<()> { .download(&fixture.url("/model.gguf"), &dest, progress_sink) .await?; - assert_eq!(tokio::fs::read(&dest).await?, body); + assert_eq!(read(&dest).await?, body); Ok(()) } @@ -209,7 +218,7 @@ async fn returns_partial_file_stale_on_416_and_removes_partial() -> Result<()> { let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::write(&partial_path, b"stale").await?; + write(&partial_path, b"stale").await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::status(416))).await?; let sink: Arc = Arc::new(RecordingSink::new()); @@ -222,7 +231,7 @@ async fn returns_partial_file_stale_on_416_and_removes_partial() -> Result<()> { result, Err(DownloadError::PartialFileStale { .. }) )); - assert!(!tokio::fs::try_exists(&partial_path).await?); + assert!(!try_exists(&partial_path).await?); Ok(()) } @@ -244,7 +253,7 @@ async fn returns_partial_file_stale_on_416_even_when_no_partial_existed() -> Res result, Err(DownloadError::PartialFileStale { .. }) )); - assert!(!tokio::fs::try_exists(&partial_path).await?); + assert!(!try_exists(&partial_path).await?); Ok(()) } @@ -254,7 +263,7 @@ async fn mismatched_content_range_is_treated_as_partial_file_stale() -> Result<( let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::write(&partial_path, b"first half ").await?; + write(&partial_path, b"first half ").await?; let body = b"second half".to_vec(); let total = 11_u64 + body.len() as u64; @@ -275,7 +284,7 @@ async fn mismatched_content_range_is_treated_as_partial_file_stale() -> Result<( result, Err(DownloadError::PartialFileStale { .. }) )); - assert!(!tokio::fs::try_exists(&partial_path).await?); + assert!(!try_exists(&partial_path).await?); Ok(()) } @@ -359,7 +368,7 @@ async fn progress_sink_on_finished_fires_only_on_success() -> Result<()> { .await?; assert_eq!(sink_success.finished_count.load(Ordering::Relaxed), 1); - tokio::fs::remove_file(&dest).await?; + remove_file(&dest).await?; let fixture_404 = LocalHttpFixture::start(Scenario::always(FixtureResponse::status(404))).await?; @@ -475,7 +484,7 @@ async fn returns_io_error_when_destination_directory_does_not_exist_and_cannot_b -> Result<()> { let directory = TempDir::new()?; let blocker = directory.path().join("blocker"); - tokio::fs::write(&blocker, b"i am a file, not a directory").await?; + write(&blocker, b"i am a file, not a directory").await?; let dest = blocker.join("subdir").join("model.gguf"); let fixture = @@ -525,7 +534,7 @@ async fn read_timeout_fires_when_server_stalls_before_headers() -> Result<()> { .await; let result = outcome.map_err(|_elapsed| { - anyhow::anyhow!("download did not return within test guard; read_timeout never fired") + anyhow!("download did not return within test guard; read_timeout never fired") })?; assert!( @@ -559,7 +568,7 @@ async fn open_for_append_error_returns_io_when_partial_path_is_a_directory() -> let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::create_dir(&partial_path).await?; + create_dir(&partial_path).await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::ok(b"body".to_vec()))).await?; @@ -582,11 +591,11 @@ async fn download_returns_cache_permission_denied_when_dir_is_read_only() -> Res let directory = TempDir::new()?; let readonly_parent = directory.path().join("readonly"); - tokio::fs::create_dir(&readonly_parent).await?; + create_dir(&readonly_parent).await?; let dest = readonly_parent.join("model.gguf"); - let mut perms = tokio::fs::metadata(&readonly_parent).await?.permissions(); + let mut perms = metadata(&readonly_parent).await?.permissions(); perms.set_mode(0o500); - tokio::fs::set_permissions(&readonly_parent, perms).await?; + set_permissions(&readonly_parent, perms).await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::ok(b"body".to_vec()))).await?; @@ -596,9 +605,9 @@ async fn download_returns_cache_permission_denied_when_dir_is_read_only() -> Res .download(&fixture.url("/x"), &dest, sink) .await; - let mut restore = tokio::fs::metadata(&readonly_parent).await?.permissions(); + let mut restore = metadata(&readonly_parent).await?.permissions(); restore.set_mode(0o700); - tokio::fs::set_permissions(&readonly_parent, restore).await?; + set_permissions(&readonly_parent, restore).await?; let Err(DownloadError::CachePermissionDenied { source, .. }) = result else { bail!("expected CachePermissionDenied, got {result:?}"); @@ -613,8 +622,8 @@ async fn download_returns_cache_permission_denied_when_dir_is_read_only() -> Res async fn finalize_error_returns_io_when_destination_is_a_non_empty_directory() -> Result<()> { let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); - tokio::fs::create_dir(&dest).await?; - tokio::fs::write(dest.join("blocker"), b"x").await?; + create_dir(&dest).await?; + write(dest.join("blocker"), b"x").await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::ok(b"body".to_vec()))).await?; @@ -637,13 +646,13 @@ async fn partial_file_stale_with_unremovable_partial_returns_cache_permission_de let directory = TempDir::new()?; let locked_parent = directory.path().join("locked"); - tokio::fs::create_dir(&locked_parent).await?; + create_dir(&locked_parent).await?; let dest = locked_parent.join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::write(&partial_path, b"stale").await?; - let mut perms = tokio::fs::metadata(&locked_parent).await?.permissions(); + write(&partial_path, b"stale").await?; + let mut perms = metadata(&locked_parent).await?.permissions(); perms.set_mode(0o500); - tokio::fs::set_permissions(&locked_parent, perms).await?; + set_permissions(&locked_parent, perms).await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::status(416))).await?; let sink: Arc = Arc::new(RecordingSink::new()); @@ -652,9 +661,9 @@ async fn partial_file_stale_with_unremovable_partial_returns_cache_permission_de .download(&fixture.url("/x"), &dest, sink) .await; - let mut restore = tokio::fs::metadata(&locked_parent).await?.permissions(); + let mut restore = metadata(&locked_parent).await?.permissions(); restore.set_mode(0o700); - tokio::fs::set_permissions(&locked_parent, restore).await?; + set_permissions(&locked_parent, restore).await?; assert!(matches!( result, @@ -670,7 +679,7 @@ async fn truncate_error_during_ignore_range_returns_io() -> Result<()> { let directory = TempDir::new()?; let dest = directory.path().join("model.gguf"); let partial_path = dest.with_extension("partial"); - tokio::fs::create_dir(&partial_path).await?; + create_dir(&partial_path).await?; let fixture = LocalHttpFixture::start(Scenario::always(FixtureResponse::ok(b"body".to_vec()))).await?; @@ -729,9 +738,9 @@ async fn download_succeeds_after_cache_dir_was_deleted_between_calls() -> Result Arc::new(RecordingSink::new()) as Arc, ) .await?; - assert_eq!(tokio::fs::read(&dest).await?, body); + assert_eq!(read(&dest).await?, body); - tokio::fs::remove_dir_all(&cache_subdir).await?; + remove_dir_all(&cache_subdir).await?; DownloadManager::new()? .download( @@ -740,7 +749,7 @@ async fn download_succeeds_after_cache_dir_was_deleted_between_calls() -> Result Arc::new(RecordingSink::new()) as Arc, ) .await?; - assert_eq!(tokio::fs::read(&dest).await?, body); + assert_eq!(read(&dest).await?, body); Ok(()) } diff --git a/paddler_gui/Cargo.toml b/paddler_gui/Cargo.toml index 61218f74..042ee2a9 100644 --- a/paddler_gui/Cargo.toml +++ b/paddler_gui/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true actix-web = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } +command_handler = { workspace = true } env_logger = { workspace = true } esbuild-metafile = { workspace = true, optional = true } iced = { workspace = true } diff --git a/paddler_gui/src/app.rs b/paddler_gui/src/app.rs index fc71e24f..26b741ee 100644 --- a/paddler_gui/src/app.rs +++ b/paddler_gui/src/app.rs @@ -3,6 +3,7 @@ use std::net::SocketAddr; use std::sync::LazyLock; use std::time::Duration; +use command_handler::shutdown_signal::register_shutdown_signals; use iced::Bottom; use iced::Center; use iced::Element; @@ -32,7 +33,6 @@ use paddler_bootstrap::agent_runner::AgentRunner; use paddler_bootstrap::agent_runner::AgentRunnerParams; use paddler_bootstrap::balancer_runner::BalancerRunner; use paddler_bootstrap::balancer_runner::BalancerRunnerParams; -use paddler_bootstrap::shutdown_signal::register_shutdown_signals; use paddler_messaging::balancer_desired_state::BalancerDesiredState; use paddler_messaging::produces_snapshot::ProducesSnapshot; use paddler_messaging::subscribes_to_updates::SubscribesToUpdates as _; diff --git a/paddler_messaging/src/embedding_normalization_method.rs b/paddler_messaging/src/embedding_normalization_method.rs index 91995e53..baead1d4 100644 --- a/paddler_messaging/src/embedding_normalization_method.rs +++ b/paddler_messaging/src/embedding_normalization_method.rs @@ -1,4 +1,4 @@ -use std::mem; +use std::mem::discriminant; use serde::Deserialize; use serde::Serialize; @@ -23,7 +23,7 @@ impl EmbeddingNormalizationMethod { #[must_use] pub fn needs_transformation_to(&self, other: &Self) -> bool { - mem::discriminant(self) != mem::discriminant(other) + discriminant(self) != discriminant(other) } } diff --git a/paddler_messaging/src/generated_token_result.rs b/paddler_messaging/src/generated_token_result.rs index 63abed30..24357901 100644 --- a/paddler_messaging/src/generated_token_result.rs +++ b/paddler_messaging/src/generated_token_result.rs @@ -13,6 +13,7 @@ use crate::streamable_result::StreamableResult; pub enum GeneratedTokenResult { ChatTemplateError(String), ContentToken(String), + DetokenizationFailed(String), Done(GenerationSummary), GrammarIncompatibleWithThinking(String), GrammarInitializationFailed(String), @@ -74,6 +75,7 @@ impl StreamableResult for GeneratedTokenResult { matches!( self, Self::ChatTemplateError(_) + | Self::DetokenizationFailed(_) | Self::Done(_) | Self::GrammarIncompatibleWithThinking(_) | Self::GrammarInitializationFailed(_) @@ -102,6 +104,11 @@ mod tests { assert!(GeneratedTokenResult::ChatTemplateError("err".to_owned()).is_done()); } + #[test] + fn detokenization_failed_is_done() { + assert!(GeneratedTokenResult::DetokenizationFailed("err".to_owned()).is_done()); + } + #[test] fn grammar_incompatible_with_thinking_is_done() { assert!(GeneratedTokenResult::GrammarIncompatibleWithThinking("err".to_owned()).is_done()); diff --git a/paddler_messaging/src/inference_parameters.rs b/paddler_messaging/src/inference_parameters.rs index 9a535009..75e240a7 100644 --- a/paddler_messaging/src/inference_parameters.rs +++ b/paddler_messaging/src/inference_parameters.rs @@ -19,9 +19,9 @@ pub struct InferenceParameters { pub v_cache_dtype: KvCacheDtype, /// The minimum probability for a token to be considered, relative to the probability of the most likely token pub min_p: f32, - /// Number of model layers to offload to GPU. 0 = CPU-only. + /// Number of model layers to offload to GPU. 0 = CPU-only, -1 = offload all layers. /// Set to a value >= the model's transformer block count for full GPU offload. - pub n_gpu_layers: u32, + pub n_gpu_layers: i32, pub penalty_frequency: f32, /// How many tokens to scan for repetitions (-1 = context size, 0 = disabled) pub penalty_last_n: i32, diff --git a/paddler_openai_response_format_validator/src/openai_validator.rs b/paddler_openai_response_format_validator/src/openai_validator.rs index a7e19656..f7d6be85 100644 --- a/paddler_openai_response_format_validator/src/openai_validator.rs +++ b/paddler_openai_response_format_validator/src/openai_validator.rs @@ -1,6 +1,7 @@ use anyhow::Result; use anyhow::anyhow; use jsonschema::Validator; +use jsonschema::validator_for; use serde_json::Value; use crate::openai_spec::OPENAPI_YAML; @@ -77,7 +78,7 @@ fn compile_strict_schema( ) -> Result { let schema = strict_chat_completion_schema(components, root_name, strict_pointers)?; - jsonschema::validator_for(&schema) + validator_for(&schema) .map_err(|error| anyhow!("compiling the strict {root_name:?} schema: {error}")) } diff --git a/paddler_test_cluster_harness/src/buffered_requests_stream_watcher.rs b/paddler_test_cluster_harness/src/buffered_requests_stream_watcher.rs index 8759c536..cad82689 100644 --- a/paddler_test_cluster_harness/src/buffered_requests_stream_watcher.rs +++ b/paddler_test_cluster_harness/src/buffered_requests_stream_watcher.rs @@ -57,6 +57,7 @@ impl BufferedRequestsStreamWatcher { #[cfg(test)] mod tests { + use anyhow::anyhow; use paddler_messaging::buffered_request_manager_snapshot::BufferedRequestManagerSnapshot; use super::BufferedRequestsStreamWatcher; @@ -96,7 +97,7 @@ mod tests { #[tokio::test] async fn propagates_a_stream_error() { - let mut watcher = watcher(vec![Err(anyhow::anyhow!("socket closed"))]); + let mut watcher = watcher(vec![Err(anyhow!("socket closed"))]); let error = watcher.until(|_| true).await.err().unwrap(); diff --git a/paddler_test_cluster_harness/src/collect_embedding_results.rs b/paddler_test_cluster_harness/src/collect_embedding_results.rs index 91e2038a..86118c2d 100644 --- a/paddler_test_cluster_harness/src/collect_embedding_results.rs +++ b/paddler_test_cluster_harness/src/collect_embedding_results.rs @@ -95,6 +95,7 @@ pub async fn collect_embedding_results( #[cfg(test)] mod tests { + use anyhow::anyhow; use paddler_messaging::embedding::Embedding; use paddler_messaging::embedding_normalization_method::EmbeddingNormalizationMethod; use paddler_messaging::generated_token_result::GeneratedTokenResult; @@ -269,7 +270,7 @@ mod tests { #[tokio::test] async fn propagates_a_stream_error() { - let error = collect_embedding_results(stream(vec![Err(anyhow::anyhow!("socket closed"))])) + let error = collect_embedding_results(stream(vec![Err(anyhow!("socket closed"))])) .await .err() .unwrap(); diff --git a/paddler_test_cluster_harness/src/collect_generated_tokens.rs b/paddler_test_cluster_harness/src/collect_generated_tokens.rs index c921c092..785fbd62 100644 --- a/paddler_test_cluster_harness/src/collect_generated_tokens.rs +++ b/paddler_test_cluster_harness/src/collect_generated_tokens.rs @@ -71,6 +71,7 @@ pub async fn collect_generated_tokens( #[cfg(test)] mod tests { + use anyhow::anyhow; use paddler_messaging::embedding_result::EmbeddingResult; use paddler_messaging::generated_token_result::GeneratedTokenResult; use paddler_messaging::inference_client::message::Message as InferenceMessage; @@ -193,7 +194,7 @@ mod tests { #[tokio::test] async fn propagates_a_stream_error() { - let error = collect_generated_tokens(stream(vec![Err(anyhow::anyhow!("socket closed"))])) + let error = collect_generated_tokens(stream(vec![Err(anyhow!("socket closed"))])) .await .err() .unwrap(); diff --git a/paddler_test_cluster_harness/src/load_test_image_data_uri.rs b/paddler_test_cluster_harness/src/load_test_image_data_uri.rs index 22e94a1c..96fe4323 100644 --- a/paddler_test_cluster_harness/src/load_test_image_data_uri.rs +++ b/paddler_test_cluster_harness/src/load_test_image_data_uri.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::fs::read; use anyhow::Context as _; use anyhow::Result; @@ -7,8 +7,8 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; pub fn load_test_image_data_uri() -> Result { let image_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../fixtures/llamas.jpg"); - let image_bytes = fs::read(image_path) - .with_context(|| format!("failed to read test fixture {image_path}"))?; + let image_bytes = + read(image_path).with_context(|| format!("failed to read test fixture {image_path}"))?; let encoded = BASE64_STANDARD.encode(&image_bytes); diff --git a/paddler_test_cluster_harness/src/resource_snapshot.rs b/paddler_test_cluster_harness/src/resource_snapshot.rs index 43bdea65..d70d7580 100644 --- a/paddler_test_cluster_harness/src/resource_snapshot.rs +++ b/paddler_test_cluster_harness/src/resource_snapshot.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::fs::read_dir; use anyhow::Context as _; use anyhow::Result; @@ -13,7 +13,7 @@ impl ResourceSnapshot { pub fn try_from_self() -> Result { let directory_path = open_descriptors_directory_path(); - let entries = fs::read_dir(directory_path).with_context(|| { + let entries = read_dir(directory_path).with_context(|| { format!( "failed to read open-descriptors directory {directory_path:?} for the current process" ) diff --git a/paddler_test_cluster_harness/src/wait_until_healthy.rs b/paddler_test_cluster_harness/src/wait_until_healthy.rs index cb94a67a..cc5d16e0 100644 --- a/paddler_test_cluster_harness/src/wait_until_healthy.rs +++ b/paddler_test_cluster_harness/src/wait_until_healthy.rs @@ -2,6 +2,7 @@ use std::time::Duration; use anyhow::Context as _; use anyhow::Result; +use anyhow::anyhow; use reqwest::Client; use reqwest::StatusCode; use url::Url; @@ -22,7 +23,7 @@ pub async fn wait_until_healthy(base_url: &Url, endpoint: &str) -> Result<()> { tokio::time::sleep(HEALTHCHECK_PROBE_INTERVAL).await; } other => { - return Err(anyhow::anyhow!( + return Err(anyhow!( "unexpected status {other} while probing {health_url}" )); } diff --git a/paddler_tests/src/model_card/mod.rs b/paddler_tests/src/model_card/mod.rs index 6701da22..7fa0fa46 100644 --- a/paddler_tests/src/model_card/mod.rs +++ b/paddler_tests/src/model_card/mod.rs @@ -16,6 +16,6 @@ pub mod smolvlm2_256m_mmproj; use paddler_messaging::huggingface_model_reference::HuggingFaceModelReference; pub struct ModelCard { - pub gpu_layer_count: u32, + pub gpu_layer_count: i32, pub reference: HuggingFaceModelReference, } diff --git a/paddler_tests/tests/agent_conversation_with_function_tool_succeeds.rs b/paddler_tests/tests/agent_conversation_with_function_tool_succeeds.rs index 8041c3fe..5530d263 100644 --- a/paddler_tests/tests/agent_conversation_with_function_tool_succeeds.rs +++ b/paddler_tests/tests/agent_conversation_with_function_tool_succeeds.rs @@ -13,6 +13,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +24,7 @@ async fn agent_conversation_with_function_tool_succeeds() -> Result<()> { location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster diff --git a/paddler_tests/tests/agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_registered.rs b/paddler_tests/tests/agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_registered.rs index bb19828d..e95ccfdf 100644 --- a/paddler_tests/tests/agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_registered.rs +++ b/paddler_tests/tests/agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_registered.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use anyhow::Result; +use anyhow::anyhow; use anyhow::bail; use llama_cpp_bindings::ToolCallArguments; use llama_cpp_bindings::llama_backend::LlamaBackend; @@ -20,6 +21,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; const QWEN_XML_PAYLOAD: &str = "\n\ \n\ @@ -50,7 +52,7 @@ fn agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_re let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let tools = vec![Tool::Function(FunctionCall { function: Function { @@ -90,7 +92,7 @@ fn agent_pipeline_recognizes_duck_typed_tool_call_format_when_template_is_not_re let mapped = ToolCallEvent::Resolved(parsed_calls) .into_generated_token_result() - .ok_or_else(|| anyhow::anyhow!("Resolved must produce a GeneratedTokenResult variant"))?; + .ok_or_else(|| anyhow!("Resolved must produce a GeneratedTokenResult variant"))?; let GeneratedTokenResult::ToolCallParsed(wire_calls) = mapped else { bail!("expected GeneratedTokenResult::ToolCallParsed after mapping"); }; diff --git a/paddler_tests/tests/agent_rejects_structurally_invalid_tool_schema.rs b/paddler_tests/tests/agent_rejects_structurally_invalid_tool_schema.rs index d5499bf3..a7b33d6f 100644 --- a/paddler_tests/tests/agent_rejects_structurally_invalid_tool_schema.rs +++ b/paddler_tests/tests/agent_rejects_structurally_invalid_tool_schema.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::agent_desired_model::AgentDesiredModel; use paddler_messaging::balancer_desired_state::BalancerDesiredState; use paddler_messaging::conversation_history::ConversationHistory; @@ -20,6 +21,7 @@ use paddler_tests::model_card::ModelCard; use paddler_tests::model_card::qwen3_0_6b::qwen3_0_6b; use paddler_tests::start_cluster::start_cluster; use serde_json::Map; +use serde_json::json; #[tokio::test(flavor = "multi_thread")] async fn agent_rejects_structurally_invalid_tool_schema() -> Result<()> { @@ -55,7 +57,7 @@ async fn agent_rejects_structurally_invalid_tool_schema() -> Result<()> { // rejects it, so the agent's tool-call pipeline build reports the tool's schema // as invalid and the scheduler emits `ToolSchemaInvalid` before any generation. let mut invalid_properties = Map::new(); - invalid_properties.insert("location".to_owned(), serde_json::json!({ "type": 123 })); + invalid_properties.insert("location".to_owned(), json!({ "type": 123 })); let collected = cluster .continue_from_conversation_history(&ContinueFromConversationHistoryParams { @@ -93,7 +95,7 @@ async fn agent_rejects_structurally_invalid_tool_schema() -> Result<()> { _ => None, }) .ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "expected a ToolSchemaInvalid event when a tool's JSON Schema is invalid; got:\n{}", collected.text ) diff --git a/paddler_tests/tests/agent_rejects_tool_with_invalid_required_field_in_schema.rs b/paddler_tests/tests/agent_rejects_tool_with_invalid_required_field_in_schema.rs index 32663d9b..595c9b13 100644 --- a/paddler_tests/tests/agent_rejects_tool_with_invalid_required_field_in_schema.rs +++ b/paddler_tests/tests/agent_rejects_tool_with_invalid_required_field_in_schema.rs @@ -13,6 +13,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; #[tokio::test(flavor = "multi_thread")] async fn agent_rejects_tool_with_invalid_required_field_in_schema() -> Result<()> { @@ -20,7 +21,7 @@ async fn agent_rejects_tool_with_invalid_required_field_in_schema() -> Result<() let mut name_properties = Map::new(); - name_properties.insert("name".to_owned(), serde_json::json!({"type": "string"})); + name_properties.insert("name".to_owned(), json!({"type": "string"})); let outcome = cluster .continue_from_conversation_history(&ContinueFromConversationHistoryParams { diff --git a/paddler_tests/tests/agent_reports_grammar_initialization_failure_for_invalid_gbnf.rs b/paddler_tests/tests/agent_reports_grammar_initialization_failure_for_invalid_gbnf.rs index a3dc5aae..5382eee7 100644 --- a/paddler_tests/tests/agent_reports_grammar_initialization_failure_for_invalid_gbnf.rs +++ b/paddler_tests/tests/agent_reports_grammar_initialization_failure_for_invalid_gbnf.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::generated_token_result::GeneratedTokenResult; use paddler_messaging::grammar_constraint::GrammarConstraint; use paddler_messaging::request_params::continue_from_raw_prompt_params::ContinueFromRawPromptParams; @@ -37,7 +38,7 @@ async fn agent_reports_grammar_initialization_failure_for_invalid_gbnf() -> Resu _ => None, }) .ok_or_else(|| { - anyhow::anyhow!( + anyhow!( "expected a GrammarInitializationFailed event for malformed GBNF; got:\n{}", collected.text ) diff --git a/paddler_tests/tests/agent_reports_tool_call_validation_failure.rs b/paddler_tests/tests/agent_reports_tool_call_validation_failure.rs index 0081ed35..517d6930 100644 --- a/paddler_tests/tests/agent_reports_tool_call_validation_failure.rs +++ b/paddler_tests/tests/agent_reports_tool_call_validation_failure.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::agent_desired_model::AgentDesiredModel; use paddler_messaging::balancer_desired_state::BalancerDesiredState; use paddler_messaging::inference_parameters::InferenceParameters; @@ -20,6 +21,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -53,7 +55,7 @@ async fn agent_reports_tool_call_validation_failure() -> Result<()> { let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "integer", "description": "The city name"}), + json!({"type": "integer", "description": "The city name"}), ); let collected = cluster @@ -105,7 +107,7 @@ async fn agent_reports_tool_call_validation_failure() -> Result<()> { .iter() .flat_map(|messages| messages.iter()) .next() - .ok_or_else(|| anyhow::anyhow!("no validation-failure messages in any event"))?; + .ok_or_else(|| anyhow!("no validation-failure messages in any event"))?; assert!( first_failure.contains("get_weather"), diff --git a/paddler_tests/tests/continuous_batch_generates_tokens_with_partial_layer_offload.rs b/paddler_tests/tests/continuous_batch_generates_tokens_with_partial_layer_offload.rs index 19e4001f..988d8295 100644 --- a/paddler_tests/tests/continuous_batch_generates_tokens_with_partial_layer_offload.rs +++ b/paddler_tests/tests/continuous_batch_generates_tokens_with_partial_layer_offload.rs @@ -13,7 +13,7 @@ use paddler_tests::model_card::ModelCard; use paddler_tests::model_card::qwen3_0_6b::qwen3_0_6b; use paddler_tests::start_cluster::start_cluster; -const PARTIAL_GPU_LAYER_COUNT: u32 = 14; +const PARTIAL_GPU_LAYER_COUNT: i32 = 14; #[tokio::test(flavor = "multi_thread")] async fn continuous_batch_generates_tokens_with_partial_layer_offload() -> Result<()> { diff --git a/paddler_tests/tests/continuous_batch_rejects_embedding_during_active_generation.rs b/paddler_tests/tests/continuous_batch_rejects_embedding_during_active_generation.rs index 07409c9d..e9be0e90 100644 --- a/paddler_tests/tests/continuous_batch_rejects_embedding_during_active_generation.rs +++ b/paddler_tests/tests/continuous_batch_rejects_embedding_during_active_generation.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use futures_util::StreamExt as _; use paddler_messaging::embedding_input_document::EmbeddingInputDocument; use paddler_messaging::embedding_normalization_method::EmbeddingNormalizationMethod; @@ -25,7 +26,7 @@ async fn continuous_batch_rejects_embedding_during_active_generation() -> Result let _first_token = generation_stream .next() .await - .ok_or_else(|| anyhow::anyhow!("generation stream must yield at least one message"))?; + .ok_or_else(|| anyhow!("generation stream must yield at least one message"))?; let embedding_outcome = cluster .generate_embedding_batch(&GenerateEmbeddingBatchParams { diff --git a/paddler_tests/tests/continuous_batch_releases_slots_on_shutdown_with_active_request.rs b/paddler_tests/tests/continuous_batch_releases_slots_on_shutdown_with_active_request.rs index eca5e55b..c959c31f 100644 --- a/paddler_tests/tests/continuous_batch_releases_slots_on_shutdown_with_active_request.rs +++ b/paddler_tests/tests/continuous_batch_releases_slots_on_shutdown_with_active_request.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use futures_util::StreamExt as _; use paddler_messaging::request_params::continue_from_raw_prompt_params::ContinueFromRawPromptParams; use paddler_test_cluster_harness::agent_config::AgentConfig; @@ -21,7 +22,7 @@ async fn continuous_batch_releases_slots_on_shutdown_with_active_request() -> Re let _first_message = stream .next() .await - .ok_or_else(|| anyhow::anyhow!("inference stream must yield at least one message"))?; + .ok_or_else(|| anyhow!("inference stream must yield at least one message"))?; drop(stream); diff --git a/paddler_tests/tests/continuous_batch_stops_generation_when_stop_sender_dropped.rs b/paddler_tests/tests/continuous_batch_stops_generation_when_stop_sender_dropped.rs index a492c0b0..cad765f6 100644 --- a/paddler_tests/tests/continuous_batch_stops_generation_when_stop_sender_dropped.rs +++ b/paddler_tests/tests/continuous_batch_stops_generation_when_stop_sender_dropped.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use futures_util::StreamExt as _; use paddler_messaging::generated_token_result::GeneratedTokenResult; use paddler_messaging::request_params::continue_from_raw_prompt_params::ContinueFromRawPromptParams; @@ -23,7 +24,7 @@ async fn continuous_batch_stops_generation_when_stop_sender_dropped() -> Result< let _first_token = first_stream .next() .await - .ok_or_else(|| anyhow::anyhow!("first stream must yield at least one message"))?; + .ok_or_else(|| anyhow!("first stream must yield at least one message"))?; drop(first_stream); diff --git a/paddler_tests/tests/deepseek_r1_distill_llama_8b_internal_endpoint_emits_reasoning_tokens.rs b/paddler_tests/tests/deepseek_r1_distill_llama_8b_internal_endpoint_emits_reasoning_tokens.rs index 1441ede2..aa846374 100644 --- a/paddler_tests/tests/deepseek_r1_distill_llama_8b_internal_endpoint_emits_reasoning_tokens.rs +++ b/paddler_tests/tests/deepseek_r1_distill_llama_8b_internal_endpoint_emits_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -45,7 +46,7 @@ async fn deepseek_r1_distill_llama_8b_internal_endpoint_emits_reasoning_tokens() let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens.rs b/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens.rs index e66b2637..8a44c069 100644 --- a/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens.rs +++ b/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -44,7 +45,7 @@ async fn gemma4_internal_endpoint_emits_reasoning_tokens() -> Result<()> { let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens_for_image_request.rs b/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens_for_image_request.rs index 5c8ff69c..886b45cb 100644 --- a/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens_for_image_request.rs +++ b/paddler_tests/tests/gemma4_internal_endpoint_emits_reasoning_tokens_for_image_request.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -58,7 +59,7 @@ async fn gemma4_internal_endpoint_emits_reasoning_tokens_for_image_request() -> let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/gemma4_internal_endpoint_emits_tool_call_parsed_event.rs b/paddler_tests/tests/gemma4_internal_endpoint_emits_tool_call_parsed_event.rs index 778ec08b..4778fd3b 100644 --- a/paddler_tests/tests/gemma4_internal_endpoint_emits_tool_call_parsed_event.rs +++ b/paddler_tests/tests/gemma4_internal_endpoint_emits_tool_call_parsed_event.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_gemma_4::start_cluster_with_gemma_4; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +25,7 @@ async fn gemma4_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -74,7 +76,7 @@ async fn gemma4_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { .iter() .flat_map(|calls| calls.iter()) .next() - .ok_or_else(|| anyhow::anyhow!("no parsed tool calls in any event"))?; + .ok_or_else(|| anyhow!("no parsed tool calls in any event"))?; assert_eq!(first_call.name, "get_weather"); diff --git a/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens.rs b/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens.rs index 6f4cee84..26b88412 100644 --- a/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens.rs +++ b/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -44,7 +45,7 @@ async fn mistral3_internal_endpoint_emits_reasoning_tokens() -> Result<()> { let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens_for_image_request.rs b/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens_for_image_request.rs index f2961841..a1a4b9b2 100644 --- a/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens_for_image_request.rs +++ b/paddler_tests/tests/mistral3_internal_endpoint_emits_reasoning_tokens_for_image_request.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -58,7 +59,7 @@ async fn mistral3_internal_endpoint_emits_reasoning_tokens_for_image_request() - let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/mistral3_internal_endpoint_emits_tool_call_parsed_event.rs b/paddler_tests/tests/mistral3_internal_endpoint_emits_tool_call_parsed_event.rs index 83808082..1a21f764 100644 --- a/paddler_tests/tests/mistral3_internal_endpoint_emits_tool_call_parsed_event.rs +++ b/paddler_tests/tests/mistral3_internal_endpoint_emits_tool_call_parsed_event.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_tests::ministral_3_cluster_params::Ministral3ClusterParams; use paddler_tests::start_cluster_with_ministral_3::start_cluster_with_ministral_3; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -27,7 +29,7 @@ async fn mistral3_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -78,7 +80,7 @@ async fn mistral3_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> .iter() .flat_map(|calls| calls.iter()) .next() - .ok_or_else(|| anyhow::anyhow!("no parsed tool calls in any event"))?; + .ok_or_else(|| anyhow!("no parsed tool calls in any event"))?; assert_eq!(first_call.name, "get_weather"); diff --git a/paddler_tests/tests/qwen35_internal_endpoint_emits_reasoning_tokens_for_image_request.rs b/paddler_tests/tests/qwen35_internal_endpoint_emits_reasoning_tokens_for_image_request.rs index 170b98af..9dca8e5d 100644 --- a/paddler_tests/tests/qwen35_internal_endpoint_emits_reasoning_tokens_for_image_request.rs +++ b/paddler_tests/tests/qwen35_internal_endpoint_emits_reasoning_tokens_for_image_request.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -58,7 +59,7 @@ async fn qwen35_internal_endpoint_emits_reasoning_tokens_for_image_request() -> let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen35_internal_endpoint_emits_tool_call_parsed_event.rs b/paddler_tests/tests/qwen35_internal_endpoint_emits_tool_call_parsed_event.rs index 0f84c81f..cafaf81f 100644 --- a/paddler_tests/tests/qwen35_internal_endpoint_emits_tool_call_parsed_event.rs +++ b/paddler_tests/tests/qwen35_internal_endpoint_emits_tool_call_parsed_event.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3_5::start_cluster_with_qwen3_5; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +25,7 @@ async fn qwen35_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -74,7 +76,7 @@ async fn qwen35_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { .iter() .flat_map(|calls| calls.iter()) .next() - .ok_or_else(|| anyhow::anyhow!("no parsed tool calls in any event"))?; + .ok_or_else(|| anyhow!("no parsed tool calls in any event"))?; assert_eq!(first_call.name, "get_weather"); diff --git a/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_disabled_emits_only_content_tokens.rs b/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_disabled_emits_only_content_tokens.rs index 752f8fc0..98c9915b 100644 --- a/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_disabled_emits_only_content_tokens.rs +++ b/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_disabled_emits_only_content_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -70,7 +71,7 @@ async fn qwen35_internal_endpoint_with_thinking_disabled_emits_only_content_toke let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs b/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs index f863d196..94ee3da0 100644 --- a/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs +++ b/paddler_tests/tests/qwen35_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -44,7 +45,7 @@ async fn qwen35_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens() let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_concurrent_requests_independent_usage.rs b/paddler_tests/tests/qwen3_internal_endpoint_concurrent_requests_independent_usage.rs index c6232133..60e16071 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_concurrent_requests_independent_usage.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_concurrent_requests_independent_usage.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use futures_util::future; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; @@ -39,12 +40,12 @@ async fn qwen3_internal_endpoint_concurrent_requests_keep_independent_usage() -> let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; match &last.token_result { GeneratedTokenResult::Done(summary) => { Ok::(*summary) } - other => Err(anyhow::anyhow!("last result was not Done: {other:?}")), + other => Err(anyhow!("last result was not Done: {other:?}")), } } }); diff --git a/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_parsed_event.rs b/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_parsed_event.rs index 4cf79535..79295eda 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_parsed_event.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_parsed_event.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +25,7 @@ async fn qwen3_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -74,7 +76,7 @@ async fn qwen3_internal_endpoint_emits_tool_call_parsed_event() -> Result<()> { .iter() .flat_map(|calls| calls.iter()) .next() - .ok_or_else(|| anyhow::anyhow!("no parsed tool calls in any event"))?; + .ok_or_else(|| anyhow!("no parsed tool calls in any event"))?; assert_eq!(first_call.name, "get_weather"); diff --git a/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_tokens.rs b/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_tokens.rs index 43a41250..9034e2ec 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_tokens.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_emits_tool_call_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +25,7 @@ async fn qwen3_internal_endpoint_emits_tool_call_tokens() -> Result<()> { let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -69,7 +71,7 @@ async fn qwen3_internal_endpoint_emits_tool_call_tokens() -> Result<()> { let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_max_tokens_usage_matches.rs b/paddler_tests/tests/qwen3_internal_endpoint_max_tokens_usage_matches.rs index cb0cb642..3a9b5f35 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_max_tokens_usage_matches.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_max_tokens_usage_matches.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -39,7 +40,7 @@ async fn qwen3_internal_endpoint_max_tokens_usage_matches_streamed_count() -> Re let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_pure_content_usage.rs b/paddler_tests/tests/qwen3_internal_endpoint_pure_content_usage.rs index f0638f59..f31f8fec 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_pure_content_usage.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_pure_content_usage.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -31,7 +32,7 @@ async fn qwen3_internal_endpoint_pure_content_usage_breakdown() -> Result<()> { let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens.rs b/paddler_tests/tests/qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens.rs index 5c505933..5353a6cc 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use paddler_messaging::conversation_history::ConversationHistory; @@ -14,6 +15,7 @@ use paddler_messaging::request_params::continue_from_conversation_history_params use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters::Parameters; use paddler_messaging::request_params::continue_from_conversation_history_params::tool::tool_params::function_call::parameters_schema::validated_parameters_schema::ValidatedParametersSchema; use serde_json::Map; +use serde_json::json; use serde_json::Value; #[tokio::test(flavor = "multi_thread")] @@ -23,7 +25,7 @@ async fn qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens() let mut location_properties = Map::new(); location_properties.insert( "location".to_owned(), - serde_json::json!({"type": "string", "description": "The city name"}), + json!({"type": "string", "description": "The city name"}), ); let collected = cluster @@ -72,7 +74,7 @@ async fn qwen3_internal_endpoint_tools_without_parse_flag_emit_only_raw_tokens() let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(_) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_disabled_emits_no_reasoning_tokens.rs b/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_disabled_emits_no_reasoning_tokens.rs index cc75fba1..f36d86a1 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_disabled_emits_no_reasoning_tokens.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_disabled_emits_no_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -48,7 +49,7 @@ async fn qwen3_internal_endpoint_with_thinking_disabled_emits_no_reasoning_token let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs b/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs index af5ecd72..7710bcf1 100644 --- a/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs +++ b/paddler_tests/tests/qwen3_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_messaging::conversation_history::ConversationHistory; use paddler_messaging::conversation_message::ConversationMessage; use paddler_messaging::conversation_message_content::ConversationMessageContent; @@ -44,7 +45,7 @@ async fn qwen3_internal_endpoint_with_thinking_enabled_emits_reasoning_tokens() let last = collected .token_results .last() - .ok_or_else(|| anyhow::anyhow!("no token results received"))?; + .ok_or_else(|| anyhow!("no token results received"))?; let GeneratedTokenResult::Done(summary) = &last.token_result else { anyhow::bail!("last result was not Done: {last:?}"); }; diff --git a/paddler_tests/tests/qwen3_openai_non_streaming_emits_tool_calls_for_function_tool.rs b/paddler_tests/tests/qwen3_openai_non_streaming_emits_tool_calls_for_function_tool.rs index 2494c2d9..a4bba681 100644 --- a/paddler_tests/tests/qwen3_openai_non_streaming_emits_tool_calls_for_function_tool.rs +++ b/paddler_tests/tests/qwen3_openai_non_streaming_emits_tool_calls_for_function_tool.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -39,7 +40,7 @@ async fn qwen3_openai_non_streaming_emits_tool_calls_for_function_tool() -> Resu let tool_calls = response .pointer("/choices/0/message/tool_calls") .and_then(Value::as_array) - .ok_or_else(|| anyhow::anyhow!("response missing message.tool_calls: {response}"))?; + .ok_or_else(|| anyhow!("response missing message.tool_calls: {response}"))?; assert_eq!( tool_calls.len(), @@ -58,20 +59,20 @@ async fn qwen3_openai_non_streaming_emits_tool_calls_for_function_tool() -> Resu let id = first_call .pointer("/id") .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("tool call missing id"))?; + .ok_or_else(|| anyhow!("tool call missing id"))?; assert!(!id.is_empty(), "tool call id must not be empty"); let function_name = first_call .pointer("/function/name") .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("tool call missing function.name"))?; + .ok_or_else(|| anyhow!("tool call missing function.name"))?; assert_eq!(function_name, "get_weather"); let function_arguments = first_call .pointer("/function/arguments") .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("tool call missing function.arguments"))?; + .ok_or_else(|| anyhow!("tool call missing function.arguments"))?; let parsed_arguments: Value = serde_json::from_str(function_arguments)?; assert!( @@ -82,14 +83,14 @@ async fn qwen3_openai_non_streaming_emits_tool_calls_for_function_tool() -> Resu let finish_reason = response .pointer("/choices/0/finish_reason") .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("response missing finish_reason"))?; + .ok_or_else(|| anyhow!("response missing finish_reason"))?; assert_eq!(finish_reason, "tool_calls"); let completion_tokens = response .pointer("/usage/completion_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("response missing usage.completion_tokens"))?; + .ok_or_else(|| anyhow!("response missing usage.completion_tokens"))?; assert!(completion_tokens > 0); cluster.shutdown().await?; diff --git a/paddler_tests/tests/qwen3_openai_non_streaming_returns_usage.rs b/paddler_tests/tests/qwen3_openai_non_streaming_returns_usage.rs index 1cc97b9c..bddb9a88 100644 --- a/paddler_tests/tests/qwen3_openai_non_streaming_returns_usage.rs +++ b/paddler_tests/tests/qwen3_openai_non_streaming_returns_usage.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -20,20 +21,20 @@ async fn qwen3_openai_non_streaming_returns_usage() -> Result<()> { let usage = response .get("usage") - .ok_or_else(|| anyhow::anyhow!("non-streaming response missing usage: {response}"))?; + .ok_or_else(|| anyhow!("non-streaming response missing usage: {response}"))?; let prompt_tokens = usage .get("prompt_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.prompt_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.prompt_tokens missing"))?; let completion_tokens = usage .get("completion_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.completion_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.completion_tokens missing"))?; let total_tokens = usage .get("total_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.total_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.total_tokens missing"))?; assert!(prompt_tokens > 0); assert!(completion_tokens > 0); @@ -42,7 +43,7 @@ async fn qwen3_openai_non_streaming_returns_usage() -> Result<()> { let content = response .pointer("/choices/0/message/content") .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("non-streaming response missing message content"))?; + .ok_or_else(|| anyhow!("non-streaming response missing message content"))?; assert!( !content.is_empty(), diff --git a/paddler_tests/tests/qwen3_openai_non_streaming_usage_with_tool_calls.rs b/paddler_tests/tests/qwen3_openai_non_streaming_usage_with_tool_calls.rs index 99b72836..c5282e49 100644 --- a/paddler_tests/tests/qwen3_openai_non_streaming_usage_with_tool_calls.rs +++ b/paddler_tests/tests/qwen3_openai_non_streaming_usage_with_tool_calls.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -39,25 +40,25 @@ async fn qwen3_openai_non_streaming_usage_with_tool_calls() -> Result<()> { let tool_calls = response .pointer("/choices/0/message/tool_calls") .and_then(Value::as_array) - .ok_or_else(|| anyhow::anyhow!("response missing message.tool_calls: {response}"))?; + .ok_or_else(|| anyhow!("response missing message.tool_calls: {response}"))?; assert!(!tool_calls.is_empty()); let usage = response .get("usage") - .ok_or_else(|| anyhow::anyhow!("response missing usage: {response}"))?; + .ok_or_else(|| anyhow!("response missing usage: {response}"))?; let prompt_tokens = usage .get("prompt_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.prompt_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.prompt_tokens missing"))?; let completion_tokens = usage .get("completion_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.completion_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.completion_tokens missing"))?; let total_tokens = usage .get("total_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.total_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.total_tokens missing"))?; assert!(prompt_tokens > 0); // A request that produced a tool call must have spent tokens generating diff --git a/paddler_tests/tests/qwen3_openai_streaming_emits_tool_calls_for_function_tool.rs b/paddler_tests/tests/qwen3_openai_streaming_emits_tool_calls_for_function_tool.rs index 06317c55..e736de08 100644 --- a/paddler_tests/tests/qwen3_openai_streaming_emits_tool_calls_for_function_tool.rs +++ b/paddler_tests/tests/qwen3_openai_streaming_emits_tool_calls_for_function_tool.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -60,7 +61,7 @@ async fn qwen3_openai_streaming_emits_tool_calls_for_function_tool() -> Result<( .pointer("/choices/0/delta/tool_calls/0/function/name") .and_then(Value::as_str) .ok_or_else(|| { - anyhow::anyhow!("structured tool-call chunk missing function.name: {structured_chunk}") + anyhow!("structured tool-call chunk missing function.name: {structured_chunk}") })?; assert_eq!(function_name, "get_weather"); @@ -69,9 +70,7 @@ async fn qwen3_openai_streaming_emits_tool_calls_for_function_tool() -> Result<( .pointer("/choices/0/delta/tool_calls/0/function/arguments") .and_then(Value::as_str) .ok_or_else(|| { - anyhow::anyhow!( - "structured tool-call chunk missing function.arguments: {structured_chunk}" - ) + anyhow!("structured tool-call chunk missing function.arguments: {structured_chunk}") })?; let parsed_arguments: Value = serde_json::from_str(function_arguments)?; diff --git a/paddler_tests/tests/qwen3_openai_streaming_emits_usage_when_requested.rs b/paddler_tests/tests/qwen3_openai_streaming_emits_usage_when_requested.rs index f343d8d1..dd138048 100644 --- a/paddler_tests/tests/qwen3_openai_streaming_emits_usage_when_requested.rs +++ b/paddler_tests/tests/qwen3_openai_streaming_emits_usage_when_requested.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -22,24 +23,24 @@ async fn qwen3_openai_streaming_emits_usage_when_requested() -> Result<()> { let last_chunk = chunks .last() - .ok_or_else(|| anyhow::anyhow!("no chunks received from streaming endpoint"))?; + .ok_or_else(|| anyhow!("no chunks received from streaming endpoint"))?; let usage = last_chunk .get("usage") - .ok_or_else(|| anyhow::anyhow!("trailing chunk lacks usage field: {last_chunk}"))?; + .ok_or_else(|| anyhow!("trailing chunk lacks usage field: {last_chunk}"))?; let prompt_tokens = usage .get("prompt_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.prompt_tokens missing or not u64"))?; + .ok_or_else(|| anyhow!("usage.prompt_tokens missing or not u64"))?; let completion_tokens = usage .get("completion_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.completion_tokens missing or not u64"))?; + .ok_or_else(|| anyhow!("usage.completion_tokens missing or not u64"))?; let total_tokens = usage .get("total_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.total_tokens missing or not u64"))?; + .ok_or_else(|| anyhow!("usage.total_tokens missing or not u64"))?; assert!(prompt_tokens > 0); assert!(completion_tokens > 0); @@ -48,7 +49,7 @@ async fn qwen3_openai_streaming_emits_usage_when_requested() -> Result<()> { let trailing_choices = last_chunk .get("choices") .and_then(Value::as_array) - .ok_or_else(|| anyhow::anyhow!("trailing chunk lacks choices array"))?; + .ok_or_else(|| anyhow!("trailing chunk lacks choices array"))?; assert!( trailing_choices.is_empty(), diff --git a/paddler_tests/tests/qwen3_openai_streaming_usage_breakdown_with_thinking.rs b/paddler_tests/tests/qwen3_openai_streaming_usage_breakdown_with_thinking.rs index 63b66188..2f1a89d1 100644 --- a/paddler_tests/tests/qwen3_openai_streaming_usage_breakdown_with_thinking.rs +++ b/paddler_tests/tests/qwen3_openai_streaming_usage_breakdown_with_thinking.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -27,20 +28,20 @@ async fn qwen3_openai_streaming_usage_breakdown_with_thinking() -> Result<()> { .iter() .rev() .find(|chunk| chunk.get("usage").is_some_and(|usage| !usage.is_null())) - .ok_or_else(|| anyhow::anyhow!("no chunk contained usage information"))?; + .ok_or_else(|| anyhow!("no chunk contained usage information"))?; let prompt_tokens = usage_chunk .pointer("/usage/prompt_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage chunk missing prompt_tokens"))?; + .ok_or_else(|| anyhow!("usage chunk missing prompt_tokens"))?; let completion_tokens = usage_chunk .pointer("/usage/completion_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage chunk missing completion_tokens"))?; + .ok_or_else(|| anyhow!("usage chunk missing completion_tokens"))?; let total_tokens = usage_chunk .pointer("/usage/total_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage chunk missing total_tokens"))?; + .ok_or_else(|| anyhow!("usage chunk missing total_tokens"))?; assert!(prompt_tokens > 0); assert!(completion_tokens > 0); diff --git a/paddler_tests/tests/qwen3_responses_non_streaming_returns_text_and_usage.rs b/paddler_tests/tests/qwen3_responses_non_streaming_returns_text_and_usage.rs index 436a4f24..a3628446 100644 --- a/paddler_tests/tests/qwen3_responses_non_streaming_returns_text_and_usage.rs +++ b/paddler_tests/tests/qwen3_responses_non_streaming_returns_text_and_usage.rs @@ -1,6 +1,7 @@ #![cfg(feature = "tests_that_use_llms")] use anyhow::Result; +use anyhow::anyhow; use paddler_test_cluster_harness::agent_config::AgentConfig; use paddler_tests::start_cluster_with_qwen3::start_cluster_with_qwen3; use serde_json::Value; @@ -26,20 +27,20 @@ async fn qwen3_responses_non_streaming_returns_text_and_usage() -> Result<()> { let usage = response .get("usage") - .ok_or_else(|| anyhow::anyhow!("responses response missing usage: {response}"))?; + .ok_or_else(|| anyhow!("responses response missing usage: {response}"))?; let input_tokens = usage .get("input_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.input_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.input_tokens missing"))?; let output_tokens = usage .get("output_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.output_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.output_tokens missing"))?; let total_tokens = usage .get("total_tokens") .and_then(Value::as_u64) - .ok_or_else(|| anyhow::anyhow!("usage.total_tokens missing"))?; + .ok_or_else(|| anyhow!("usage.total_tokens missing"))?; assert!(input_tokens > 0); assert!(output_tokens > 0); @@ -48,12 +49,12 @@ async fn qwen3_responses_non_streaming_returns_text_and_usage() -> Result<()> { let message_text = response .get("output") .and_then(Value::as_array) - .ok_or_else(|| anyhow::anyhow!("responses response missing output array"))? + .ok_or_else(|| anyhow!("responses response missing output array"))? .iter() .find(|item| item.get("type").and_then(Value::as_str) == Some("message")) .and_then(|message| message.pointer("/content/0/text")) .and_then(Value::as_str) - .ok_or_else(|| anyhow::anyhow!("responses output has no message text: {response}"))?; + .ok_or_else(|| anyhow!("responses output has no message text: {response}"))?; assert!( !message_text.is_empty(),