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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Install ALSA (cpal dev-dependency, used by the voice example)
run: sudo apt-get update && sudo apt-get install -y libasound2-dev pkg-config
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
Expand All @@ -30,6 +32,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install ALSA (cpal dev-dependency, used by the voice example)
run: sudo apt-get update && sudo apt-get install -y libasound2-dev pkg-config
- name: Build
run: cargo build --verbose
- name: Run tests
Expand Down
7 changes: 3 additions & 4 deletions core/src/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,9 @@ impl<P, Output> Chat<P, Output> {
tool_ids.push(tool.id.clone());
}
Some(PauseReason::Scheduled { .. }) => {
let prev = std::mem::replace(
&mut pass.pause,
Some(PauseReason::AwaitingApproval { tool_ids: vec![] }),
);
let prev = pass
.pause
.replace(PauseReason::AwaitingApproval { tool_ids: vec![] });
if let Some(PauseReason::Scheduled {
tool_ids: sch_ids,
earliest,
Expand Down
4 changes: 4 additions & 0 deletions core/src/chat/stream/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ use crate::{
/// handle is `Clone + Send + 'static` precisely so that loop can live in a
/// task. If the library ever owns the pumping, that's a differently-named
/// method; `send` means "send a thing", not "attach a firehose".
// `Cancel` is unit while `Content`/`Item` carry payloads, but `Input` is a
// short-lived channel message (one per `send`), so the size gap is irrelevant;
// boxing would only churn the public variant for no real gain.
#[allow(clippy::large_enum_variant)]
pub enum Input {
/// A single part. Text/file/structured parts become user content
/// (coalescing into the trailing user turn); a `Tool` part resolves a
Expand Down
25 changes: 20 additions & 5 deletions core/src/chat/stream/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ impl<CP: StreamProvider> Chat<CP, InputStreamed> {

Ok(ChatStream {
input: InputStream { tx },
output: OutputStream { inner: Box::pin(output) },
output: OutputStream {
inner: Box::pin(output),
},
})
}
}
Expand Down Expand Up @@ -170,7 +172,10 @@ mod tests {

impl Session {
fn ready(events: Vec<Result<StreamEvent, ChatError>>) -> Self {
Session { events, pend: false }
Session {
events,
pend: false,
}
}
fn pending(events: Vec<Result<StreamEvent, ChatError>>) -> Self {
Session { events, pend: true }
Expand Down Expand Up @@ -279,7 +284,11 @@ mod tests {
}
drop(stream);

assert_eq!(*invocations.lock().unwrap(), 2, "provider restarted on input");
assert_eq!(
*invocations.lock().unwrap(),
2,
"provider restarted on input"
);
assert!(
messages.0.iter().any(|c| c.role == RoleEnum::User
&& c.parts
Expand All @@ -306,7 +315,10 @@ mod tests {
#[test]
fn apply_text_input_pushes_user_content() {
let mut messages = Messages::default();
apply_input_to_messages(&mut messages, Input::Item(PartEnum::from("hello".to_string())));
apply_input_to_messages(
&mut messages,
Input::Item(PartEnum::from("hello".to_string())),
);
assert_eq!(messages.0.len(), 1);
assert_eq!(messages.0[0].role, RoleEnum::User);
assert!(matches!(&messages.0[0].parts.0[0], PartEnum::Text(t) if t.0 == "hello"));
Expand All @@ -315,7 +327,10 @@ mod tests {
#[test]
fn consecutive_text_inputs_coalesce_into_one_turn() {
let mut messages = Messages::default();
apply_input_to_messages(&mut messages, Input::Item(PartEnum::from("audio-ish".to_string())));
apply_input_to_messages(
&mut messages,
Input::Item(PartEnum::from("audio-ish".to_string())),
);
apply_input_to_messages(
&mut messages,
Input::Item(PartEnum::from("actually, that".to_string())),
Expand Down
12 changes: 7 additions & 5 deletions core/src/chat/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,11 @@ fn collect_tool_results(pass: &ToolCallPass, messages: &Messages) -> Vec<Functio
return Vec::new();
}
match messages.0.last() {
Some(last) => last.parts.tools().filter_map(|t| t.response().cloned()).collect(),
Some(last) => last
.parts
.tools()
.filter_map(|t| t.response().cloned())
.collect(),
None => Vec::new(),
}
}
Expand Down Expand Up @@ -336,10 +340,8 @@ mod tests {
_messages: &mut Messages,
_tool_declarations: Option<&dyn ToolDeclarations>,
_options: Option<&ChatOptions>,
) -> Result<
futures::stream::BoxStream<'static, Result<StreamEvent, ChatError>>,
ChatError,
> {
) -> Result<futures::stream::BoxStream<'static, Result<StreamEvent, ChatError>>, ChatError>
{
let events = std::mem::take(&mut self.events);
Ok(Box::pin(futures::stream::iter(events)))
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/types/messages/parts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ impl PartEnum {
/// Used by deserialization shims and tests. Runtime flows should instead
/// construct a Pending tool and call [`Tool::complete`] after execution.
pub fn from_function_response(fr: FunctionResponse) -> PartEnum {
let id = fr.id.clone().unwrap_or_else(CallId::new);
let id = fr.id.clone().unwrap_or_default();
let call = FunctionCall {
id: Some(id.clone()),
name: fr.name.clone(),
Expand Down
2 changes: 1 addition & 1 deletion core/src/types/messages/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl Tool {
/// Create a new Tool in `Pending` state from a model-emitted call.
/// Uses the call's existing id, or mints a fresh one.
pub fn new(call: FunctionCall) -> Self {
let id = call.id.clone().unwrap_or_else(CallId::new);
let id = call.id.clone().unwrap_or_default();
Self {
id,
call,
Expand Down
7 changes: 0 additions & 7 deletions core/src/types/metadata/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ mod tests {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
..Default::default()
};
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 50);
Expand All @@ -48,8 +47,6 @@ mod tests {
input_tokens: 100,
output_tokens: 50,
total_tokens: 170,
//reasoning_tokens: 20,
..Default::default()
};
//assert_eq!(usage.reasoning_tokens, 20);
assert_eq!(usage.total_tokens, 170);
Expand Down Expand Up @@ -101,7 +98,6 @@ mod tests {
input_tokens: 75,
output_tokens: 25,
total_tokens: 100,
..Default::default()
};

let cloned = usage.clone();
Expand All @@ -116,21 +112,18 @@ mod tests {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
..Default::default()
};

let usage2 = Usage {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
..Default::default()
};

let usage3 = Usage {
input_tokens: 200,
output_tokens: 100,
total_tokens: 300,
..Default::default()
};

assert_eq!(usage1, usage2);
Expand Down
10 changes: 1 addition & 9 deletions core/src/types/provider_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,12 @@ use std::any::Any;
use std::collections::HashMap;
use std::fmt;

#[derive(Default)]
pub struct ProviderMeta {
pub description: Option<String>,
pub data: HashMap<String, Box<dyn Any + Send + Sync>>,
Comment thread
EggerMarc marked this conversation as resolved.
}

impl Default for ProviderMeta {
fn default() -> Self {
Self {
description: None,
data: HashMap::new(),
}
}
}

impl fmt::Debug for ProviderMeta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProviderMeta")
Expand Down
18 changes: 8 additions & 10 deletions examples/claude/hitl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,14 @@ fn ask_user(tool_name: &str, args: &serde_json::Value) -> Decision {
println!("│ args: {args}");
println!("└───────────────────────────────────");

loop {
match prompt(" approve? [y/n/reason]: ")
.unwrap_or_default()
.as_str()
{
"y" | "Y" | "yes" => return Decision::Approve,
"n" | "N" | "no" => return Decision::Reject("denied by user".into()),
"" => return Decision::Reject("denied by user".into()),
other => return Decision::Reject(other.into()),
}
let input = prompt(" approve? [y/n/reason]: ").unwrap_or_default();
// Normalize for matching: trims whitespace (incl. a Windows `\r`) and folds
// case, so "Y", "yes", "y\r" all approve. The reason keeps its original case.
let answer = input.trim();
match answer.to_lowercase().as_str() {
"y" | "yes" => Decision::Approve,
"n" | "no" | "" => Decision::Reject("denied by user".into()),
_ => Decision::Reject(answer.to_string()),
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/gemini/google_maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use chat_rs::{
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = gemini::GeminiBuilder::new()
.with_model("gemini-2.5-flash".to_string())
.with_google_maps(Some((34.050_481, -118.248_526)), false)
.with_google_maps(Some((34.050_48, -118.248_53)), false)
.with_google_search()
.build();

Expand Down
7 changes: 5 additions & 2 deletions examples/mistralrs/voice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
//! - On first launch macOS will ask for microphone permission. Grant it in
//! System Settings → Privacy & Security → Microphone.
//! - Run with the Metal backend for sane speed:
//! cargo run --release --example mistralrs-voice \
//! --features "mistralrs stream chat-mistralrs/metal"
//!
//! ```bash
//! cargo run --release --example mistralrs-voice \
//! --features "mistralrs stream chat-mistralrs/metal"
//! ```
//!
//! Expect a few minutes on first run while ~6 GB of weights download into
//! `~/.cache/huggingface/`. With `IsqType::Q4K` the loaded model lives in
Expand Down
18 changes: 8 additions & 10 deletions examples/openai/hitl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,14 @@ fn ask_user(tool_name: &str, args: &serde_json::Value) -> Decision {
println!("│ args: {args}");
println!("└───────────────────────────────────");

loop {
match prompt(" approve? [y/n/reason]: ")
.unwrap_or_default()
.as_str()
{
"y" | "Y" | "yes" => return Decision::Approve,
"n" | "N" | "no" => return Decision::Reject("denied by user".into()),
"" => return Decision::Reject("denied by user".into()),
other => return Decision::Reject(other.into()),
}
let input = prompt(" approve? [y/n/reason]: ").unwrap_or_default();
// Normalize for matching: trims whitespace (incl. a Windows `\r`) and folds
// case, so "Y", "yes", "y\r" all approve. The reason keeps its original case.
let answer = input.trim();
match answer.to_lowercase().as_str() {
"y" | "yes" => Decision::Approve,
"n" | "no" | "" => Decision::Reject("denied by user".into()),
_ => Decision::Reject(answer.to_string()),
}
}

Expand Down
2 changes: 1 addition & 1 deletion examples/router/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl RoutingStrategy for KeywordRouter {
})
.collect();

scored.sort_by(|a, b| b.1.cmp(&a.1));
scored.sort_by_key(|b| std::cmp::Reverse(b.1));

Ok(scored.into_iter().map(|(idx, _)| idx).collect())
}
Expand Down
2 changes: 1 addition & 1 deletion examples/router/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl RoutingStrategy for KeywordRouter {
})
.collect();

scored.sort_by(|a, b| b.1.cmp(&a.1));
scored.sort_by_key(|b| std::cmp::Reverse(b.1));
Ok(scored.into_iter().map(|(idx, _)| idx).collect())
}
}
Expand Down
15 changes: 6 additions & 9 deletions providers/claude/src/api/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,14 @@ fn parse_claude_event_stream(
{
match event_type.as_str() {
"message_start" => {
if let Ok(data) = serde_json::from_str::<Value>(&json_str) {
if let Some(msg) = data.get("message") {
if let Ok(data) = serde_json::from_str::<Value>(&json_str)
&& let Some(msg) = data.get("message") {
message_id = msg.get("id").and_then(|v| v.as_str()).map(str::to_string);
model = msg.get("model").and_then(|v| v.as_str()).map(str::to_string);
if let Some(u) = msg.get("usage") {
input_tokens = u.get("input_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
}
}
}
}
"content_block_start" => {
// Check if it's a tool_use block to start accumulating input
Expand All @@ -192,11 +191,10 @@ fn parse_claude_event_stream(
tool_input_buffer.clear();
}
}
if let Ok(Some(part)) = sse_event_to_part("content_block_start", &json_str) {
if let Some(event) = final_parts.merge_chunk(part) {
if let Ok(Some(part)) = sse_event_to_part("content_block_start", &json_str)
&& let Some(event) = final_parts.merge_chunk(part) {
yield event;
}
}
}
"content_block_delta" => {
// Handle tool input accumulation separately
Expand All @@ -215,11 +213,10 @@ fn parse_claude_event_stream(
}
}

if let Ok(Some(part)) = sse_event_to_part("content_block_delta", &json_str) {
if let Some(event) = final_parts.merge_chunk(part) {
if let Ok(Some(part)) = sse_event_to_part("content_block_delta", &json_str)
&& let Some(event) = final_parts.merge_chunk(part) {
yield event;
}
}
}
"content_block_stop" => {
// If we were accumulating tool input, finalize the Tool part's
Expand Down
3 changes: 3 additions & 0 deletions providers/claude/src/api/types/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub struct ClaudeErrorDetail {
pub message: String,
}

// `ChatFailure` is the engine-wide error type (carries metadata); its size is
// fixed by the trait surface, so boxing here would just diverge from it.
#[allow(clippy::result_large_err)]
pub fn handle_claude_error(res: Response) -> Result<Response, ChatFailure> {
let status = res.status;

Expand Down
15 changes: 1 addition & 14 deletions providers/completions/src/api/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ struct ToolCallState {
announced: bool,
}

#[derive(Default)]
struct StreamState {
text_buf: String,
reasoning_buf: String,
Expand All @@ -145,20 +146,6 @@ struct StreamState {
usage: Option<Usage>,
}

impl Default for StreamState {
fn default() -> Self {
Self {
text_buf: String::new(),
reasoning_buf: String::new(),
tool_calls: BTreeMap::new(),
finish_reason: None,
id: None,
model: None,
usage: None,
}
}
}

impl StreamState {
fn handle_chunk(&mut self, chunk: StreamChunk) -> Vec<StreamEvent> {
let mut events = Vec::new();
Expand Down
Loading
Loading