Conversations
The client keeps nothing between calls. The request is the conversation, and the caller owns it: each turn adds the user's message, sends the request, and puts the model's answer back into it.
use svir::prelude::*;
async fn converse(client: &Client, lines: Vec<String>) -> Result<(), Error> {
let mut request = Request::new("qwen3-27b").system("You are a concise assistant.");
for line in lines {
request = request.user(line);
let done = client.complete(&request).await?;
println!("{}", done.text.trim());
// The answer goes back as it is: text, tool calls, reasoning.
request = request.assistant(done);
}
Ok(())
}
Put the completion itself back
request.assistant(done) takes the Completion by value and turns it into an
assistant message with its reasoning, text, and tool calls. Clone it first if
you also store it elsewhere. Message::from(done) does the same for code that
builds messages by hand.
Rebuilding the assistant turn from done.text with Message::assistant(..)
loses the tool calls and their IDs. That is fine for a stored conversation that
had no tools, and wrong for a live one.
History is yours
- The history grows with every turn. Trimming or summarizing it is the
application's policy; a request that no longer fits fails with
ErrorKind::ContextOverflow. - When a call fails, the user message already added is still in the request. Decide whether to keep it for the retry or rebuild the request.
- Build one
Clientper server and clone it freely. Many conversations can share it: it holds connections, not chats.
Sending reasoning back
Reasoning in earlier answers is kept in the history but not sent back unless
the request says .send_reasoning(true). When it is sent, it goes under the
field it arrived in (reasoning_content or reasoning), since a server
expects its own field back unchanged. Reasoning split out of inline <think>
tags is never sent back.
use svir::prelude::*;
fn with_reasoning(request: Request) -> Request {
// For servers that want the model's earlier reasoning in the history.
request.send_reasoning(true)
}
Storing and restoring
Request, Message, Part, Completion, and the rest of the public data
types implement Serialize and Deserialize. Their serde form is part of the
public API, pinned by the crate's tests, so a stored conversation is a format
you can rely on rather than an accident of the struct layout.
use svir::prelude::*;
fn roundtrip(request: &Request) -> Result<Request, serde_json::Error> {
let stored = serde_json::to_string(request)?;
serde_json::from_str(&stored)
}
An attachment given as a path is stored as the path, and one given as bytes is stored as base64. A stored path has to exist again when the request is sent.
To rebuild a conversation from your own storage instead, use the
constructors: Message::user(..), Message::assistant(..), and
Message::tool_result(..). Public data types are #[non_exhaustive], so they
cannot be built with struct literals.