use async_openai::{
types::chat::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs},
Client,
};
use runcycles::models::*;
use runcycles::{with_cycles, CyclesClient, WithCyclesConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cycles = CyclesClient::builder("my-api-key", "http://localhost:7878")
.tenant("acme")
.build();
let openai = Client::new();
let prompt = "Summarize the runcycles crate in one sentence.";
let reply = with_cycles(
&cycles,
WithCyclesConfig::new(Amount::tokens(1_500))
.action("llm.completion", "gpt-4o-mini")
.subject(Subject {
tenant: Some("acme".into()),
..Default::default()
}),
|ctx| async move {
let mut max_tokens: u32 = 800;
if let Some(caps) = &ctx.caps {
if let Some(cap) = caps.max_tokens {
let cap_u32 = u32::try_from(cap)
.map_err(|_| "caps.max_tokens is negative — refusing to call OpenAI")?;
if cap_u32 == 0 {
return Err("caps.max_tokens is 0 — refusing to call OpenAI".into());
}
max_tokens = cap_u32.min(max_tokens);
}
}
let request = CreateChatCompletionRequestArgs::default()
.model("gpt-4o-mini")
.max_completion_tokens(max_tokens)
.messages([ChatCompletionRequestUserMessageArgs::default()
.content(prompt)
.build()?
.into()])
.build()?;
let response = openai.chat().create(request).await?;
let text = response
.choices
.first()
.and_then(|c| c.message.content.clone())
.ok_or("OpenAI response had no message content")?;
let usage = response
.usage
.ok_or("OpenAI response omitted usage — refusing to commit a guessed amount")?;
let actual_tokens = i64::from(usage.total_tokens);
Ok((text, Amount::tokens(actual_tokens)))
},
)
.await?;
println!("Reply: {reply}");
Ok(())
}