mirror of
https://github.com/whit3rabbit/anyllm-proxy.git
synced 2026-09-22 16:00:51 +00:00
- Admin UI: operator view with request volume, token usage, latency, cost charts, failure table, and request timeline - Backend: add error_kind() method and infer_error_kind() for stable error classification (rate_limit, timeout, backend_error, client_error) - Admin DB: error_kind column in request_log, observability aggregate queries (bucketed timeseries, failure breakdown, timeline) - Gemini: improved streaming translation, thinking block support, grounding metadata passthrough - Request timeout: configurable REQUEST_TIMEOUT_SECS with streaming watchdog - Model pricing: MODEL_PRICING_FILE for external pricing overrides - Degradation header: ANYLLM_DEGRADATION_WARNINGS env var control Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71 lines
2.3 KiB
Rust
71 lines
2.3 KiB
Rust
//! Streaming example: receive tokens incrementally as they arrive.
|
|
//!
|
|
//! ```bash
|
|
//! CHAT_COMPLETIONS_URL=https://api.openai.com/v1/chat/completions \
|
|
//! OPENAI_API_KEY=sk-... \
|
|
//! cargo run --example streaming -p anyllm_client
|
|
//! ```
|
|
|
|
use anyllm_client::{Client, ClientError};
|
|
use anyllm_translate::anthropic::{Delta, MessageCreateRequest, StreamEvent};
|
|
use futures::StreamExt;
|
|
use std::io::Write;
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
if let Err(e) = run().await {
|
|
eprintln!("Error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
async fn run() -> Result<(), ClientError> {
|
|
let url = std::env::var("CHAT_COMPLETIONS_URL")
|
|
.unwrap_or_else(|_| "https://api.openai.com/v1/chat/completions".to_string());
|
|
let api_key = std::env::var("OPENAI_API_KEY").unwrap_or_default();
|
|
|
|
let client = Client::builder().base_url(&url).api_key(&api_key).build()?;
|
|
|
|
let req: MessageCreateRequest = serde_json::from_str(
|
|
r#"{
|
|
"model": "claude-sonnet-4-6",
|
|
"max_tokens": 512,
|
|
"messages": [
|
|
{"role": "user", "content": "Write a haiku about Rust programming."}
|
|
]
|
|
}"#,
|
|
)
|
|
.expect("static request JSON is valid");
|
|
|
|
// messages_stream() returns (stream, rate_limit_headers).
|
|
// The stream yields StreamEvent items translated from the backend's SSE chunks.
|
|
let (mut stream, rate_limits) = client.messages_stream(&req).await?;
|
|
|
|
while let Some(event) = stream.next().await {
|
|
match event? {
|
|
StreamEvent::ContentBlockDelta { delta, .. } => {
|
|
// TextDelta carries incremental text. Print immediately without buffering.
|
|
if let Delta::TextDelta { text } = delta {
|
|
print!("{text}");
|
|
std::io::stdout().flush().ok();
|
|
}
|
|
}
|
|
StreamEvent::MessageDelta { usage, .. } => {
|
|
if let Some(u) = usage {
|
|
eprintln!("\n[output tokens: {}]", u.output_tokens);
|
|
}
|
|
}
|
|
StreamEvent::MessageStop {} => {
|
|
println!(); // ensure a trailing newline
|
|
}
|
|
_ => {} // MessageStart, ContentBlockStart/Stop, Ping are informational
|
|
}
|
|
}
|
|
|
|
if let Some(remaining) = &rate_limits.requests_remaining {
|
|
eprintln!("[requests remaining: {remaining}]");
|
|
}
|
|
|
|
Ok(())
|
|
}
|