use futures::StreamExt;
use rig::client::CompletionClient;
use rig::completion::{CompletionModel, CompletionRequestBuilder, ToolDefinition};
use rig::message::{AssistantContent, Message};
use rig::providers::openai;
use rig::streaming::{StreamFinal, StreamedAssistantContent};
#[test]
fn completions_stream_call_chain_compiles() {
let client = openai::Client::builder()
.api_key("test-key")
.base_url("http://127.0.0.1:9/v1")
.build()
.expect("client builds without network");
let model = client.completions_api().completion_model("some-model");
let tool = ToolDefinition {
name: "get_weather".to_string(),
description: "Look up the current weather for a city".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"]
}),
};
let request = CompletionRequestBuilder::new(
model.clone(),
"What is the weather in Tokyo? Use the get_weather tool.",
)
.preamble("You are a concise weather assistant.".to_string())
.message(Message::Assistant {
id: None,
content: vec![AssistantContent::text(
"Understood, I will answer weather questions.",
)],
})
.tool(tool)
.temperature(0.5)
.max_tokens(512)
.build();
assert!(request.preamble.is_none());
assert!(!request.chat_history.is_empty());
assert!(
matches!(request.chat_history.first(), Some(Message::System { .. })),
"preamble must land as the first System message, got {:?}",
request.chat_history.first()
);
assert_eq!(request.tools.len(), 1);
assert_eq!(request.tools[0].name, "get_weather");
let stream_future = model.stream(request);
drop(stream_future);
}
#[tokio::test]
#[ignore = "live test; run with WCODE_LIVE=1 WCODE_BASE_URL=... WCODE_API_KEY=... WCODE_MODEL=... cargo test -p wcode-harness --test rig_spike -- --ignored"]
async fn live_completions_stream() {
let (Ok(base_url), Ok(api_key), Ok(model_name)) = (
std::env::var("WCODE_BASE_URL"),
std::env::var("WCODE_API_KEY"),
std::env::var("WCODE_MODEL"),
) else {
eprintln!("skipping: WCODE_BASE_URL / WCODE_API_KEY / WCODE_MODEL not set");
return;
};
if std::env::var("WCODE_LIVE").ok().as_deref() != Some("1") {
eprintln!("skipping: WCODE_LIVE != 1");
return;
}
let client = openai::Client::builder()
.api_key(api_key)
.base_url(base_url)
.build()
.expect("client builds");
let model = client.completions_api().completion_model(model_name);
let request = CompletionRequestBuilder::new(model.clone(), "Reply with exactly the word: pong")
.preamble("You are a terse echo assistant.".to_string())
.build();
let mut stream = model
.stream(request)
.await
.expect("stream request accepted");
let mut variant_names: Vec<&'static str> = Vec::new();
let mut text = String::new();
let mut terminal: Option<StreamFinal> = None;
let mut errors: Vec<String> = Vec::new();
while let Some(item) = stream.next().await {
match item {
Ok(content) => {
variant_names.push(match &content {
StreamedAssistantContent::Text(t) => {
text.push_str(&t.text);
"Text"
}
StreamedAssistantContent::ToolCall { tool_call, .. } => {
eprintln!("tool call: {}", tool_call.function.name);
"ToolCall"
}
StreamedAssistantContent::ToolCallDelta { .. } => "ToolCallDelta",
StreamedAssistantContent::Reasoning { .. } => "Reasoning",
StreamedAssistantContent::ReasoningDelta { .. } => "ReasoningDelta",
StreamedAssistantContent::Final(final_record) => {
terminal = Some(final_record.clone());
"Final"
}
StreamedAssistantContent::Unknown(_) => "Unknown",
});
}
Err(err) => errors.push(err.to_string()),
}
}
println!("streamed variants: {variant_names:?}");
println!("aggregated text: {text:?}");
println!("aggregated choice: {:?}", stream.choice);
assert!(
variant_names.contains(&"Text") || variant_names.contains(&"ToolCall"),
"expected at least one content item, got {variant_names:?}"
);
assert!(errors.is_empty(), "stream surfaced errors: {errors:?}");
let final_record = terminal.or_else(|| stream.response.clone());
let final_record = final_record.expect("provider must emit a terminal record");
println!(
"terminal: usage={} finish_reason={:?} provider={} model={:?}",
final_record.usage.total_tokens,
final_record.finish_reason,
final_record.provider,
final_record.model,
);
assert!(
final_record.finish_reason.is_some(),
"finish_reason should be readable on the terminal record"
);
assert_eq!(
stream.usage().total_tokens,
final_record.usage.total_tokens,
"stream.usage() mirrors the terminal record"
);
}