mod advisory;
mod answer;
mod backend;
mod calibrate;
mod error;
mod gate;
mod guard;
mod logprobs;
mod mock;
mod openai;
mod question;
mod systemone;
pub use advisory::Advisory;
pub use answer::{
distribution_confidence, Answer, ChoiceAnswer, DecisionUsage, Evaluation, NoulAnswer,
ScoreAnswer,
};
pub use backend::{Capabilities, DecisionBackend};
pub use calibrate::{
calibrate, calibrate_with, CalibrationError, CalibrationExample, CalibrationOptions,
CalibrationReport, Expected, ReliabilityBin, ThresholdPoint,
};
pub use error::{DecisionError, FallbackAttempt};
pub use gate::ToolGate;
pub use guard::InputGuard;
pub use logprobs::LogprobBackend;
pub use mock::MockBackend;
pub use openai::OpenAiDecisionBackend;
pub use question::{Question, QuestionKind, Request};
pub use systemone::{parse_systemone_response, SystemOneBackend};
use crate::provider::CostConfig;
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
pub(crate) const TYPESAFE_PRICE_PROVIDER: &str = "typesafe";
pub(crate) const CLOUDFLARE_PRICE_PROVIDER: &str = "cloudflare";
pub(crate) const OPENAI_DECISIONS_PRICE_PROVIDER: &str = "openai-decisions";
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
enum Slot {
SystemOne(SystemOneBackend),
Logprobs(LogprobBackend),
OpenAi(OpenAiDecisionBackend),
Custom(Arc<dyn DecisionBackend>),
}
impl Slot {
fn get(&self) -> &dyn DecisionBackend {
match self {
Slot::SystemOne(b) => b,
Slot::Logprobs(b) => b,
Slot::OpenAi(b) => b,
Slot::Custom(b) => b.as_ref(),
}
}
async fn evaluate_billed(
&self,
request: &Request,
billed: &std::sync::Mutex<Option<Evaluation>>,
) -> Result<Evaluation, DecisionError> {
match self {
Slot::SystemOne(b) => b.evaluate_billed(request, billed).await,
Slot::Logprobs(b) => b.evaluate_billed(request, billed).await,
Slot::OpenAi(b) => b.evaluate_billed(request, billed).await,
Slot::Custom(b) => b.evaluate(request).await,
}
}
}
#[derive(Clone, Debug, PartialEq)]
enum Pricing {
TypeSafeList,
CloudflareList,
OpenAiDecisionsList,
Fixed(CostConfig),
Unpriced,
}
#[derive(Clone)]
pub struct DecisionModel {
backend: Slot,
model: String,
timeout: Duration,
pricing: Pricing,
attempt_timeout: Option<Duration>,
fallbacks: Vec<DecisionModel>,
}
impl std::fmt::Debug for DecisionModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut d = f.debug_struct("DecisionModel");
d.field("model", &self.model)
.field("timeout", &self.timeout)
.field("pricing", &self.pricing);
if let Some(t) = self.attempt_timeout {
d.field("attempt_timeout", &t);
}
match &self.backend {
Slot::SystemOne(b) => d.field("backend", b),
Slot::Logprobs(b) => d.field("backend", b),
Slot::OpenAi(b) => d.field("backend", b),
Slot::Custom(_) => d.field("backend", &"custom"),
};
if !self.fallbacks.is_empty() {
d.field("fallbacks", &self.fallbacks);
}
d.finish()
}
}
impl DecisionModel {
pub fn jev() -> Self {
Self {
backend: Slot::SystemOne(SystemOneBackend::typesafe()),
model: "jev-latest".into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::TypeSafeList,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn jev_opencode() -> Self {
Self {
backend: Slot::SystemOne(SystemOneBackend::opencode_zen()),
model: "jev-1.13".into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::Unpriced,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn jev_opencode_free() -> Self {
Self {
model: "jev-1.13-free".into(),
..Self::jev_opencode()
}
}
pub fn clef(account_id: impl AsRef<str>) -> Self {
Self::workers_ai(account_id, "clef")
}
pub fn clef_flash(account_id: impl AsRef<str>) -> Self {
Self::workers_ai(account_id, "clef-flash")
}
fn workers_ai(account_id: impl AsRef<str>, model: &str) -> Self {
let backend = SystemOneBackend::workers_ai(account_id, format!("@cf/cloudflare/{model}"));
Self {
backend: Slot::SystemOne(backend),
model: model.into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::CloudflareList,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn gpt_6_luna() -> Self {
Self::from_openai_backend(OpenAiDecisionBackend::new(), "gpt-6-luna")
}
pub fn from_openai_backend(backend: OpenAiDecisionBackend, model: impl Into<String>) -> Self {
Self {
backend: Slot::OpenAi(backend),
model: model.into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::OpenAiDecisionsList,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn local(base_url: impl Into<String>) -> Self {
let backend = SystemOneBackend::new(base_url)
.with_capabilities(Capabilities::new(QuestionKind::all()).with_local(true));
Self {
backend: Slot::SystemOne(backend),
model: "jev-latest".into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::Fixed(CostConfig::new(0.0, 0.0)),
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn from_backend(backend: impl DecisionBackend + 'static, model: impl Into<String>) -> Self {
Self::from_arc(Arc::new(backend), model)
}
pub fn from_arc(backend: Arc<dyn DecisionBackend>, model: impl Into<String>) -> Self {
Self {
backend: Slot::Custom(backend),
model: model.into(),
timeout: DEFAULT_TIMEOUT,
pricing: Pricing::Unpriced,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn logprobs(base_url: impl Into<String>, model: impl Into<String>) -> Self {
Self::from_logprob_backend(LogprobBackend::new(base_url), model)
}
pub fn from_logprob_backend(backend: LogprobBackend, model: impl Into<String>) -> Self {
let pricing = if backend.is_local() {
Pricing::Fixed(CostConfig::new(0.0, 0.0))
} else {
Pricing::Unpriced
};
Self {
backend: Slot::Logprobs(backend),
model: model.into(),
timeout: DEFAULT_TIMEOUT,
pricing,
attempt_timeout: None,
fallbacks: Vec::new(),
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = model.into();
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn with_attempt_timeout(mut self, timeout: Duration) -> Self {
self.attempt_timeout = Some(timeout);
self
}
pub fn or(mut self, fallback: DecisionModel) -> Self {
let mut fallback = fallback;
let rest = std::mem::take(&mut fallback.fallbacks);
self.fallbacks.push(fallback);
self.fallbacks.extend(rest);
self
}
pub fn fallback_models(&self) -> impl Iterator<Item = &str> + '_ {
self.fallbacks.iter().map(|f| f.model.as_str())
}
pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
match self.backend {
Slot::SystemOne(b) => self.backend = Slot::SystemOne(b.with_api_key(key)),
Slot::Logprobs(b) => self.backend = Slot::Logprobs(b.with_api_key(key)),
Slot::OpenAi(b) => self.backend = Slot::OpenAi(b.with_api_key(key)),
Slot::Custom(_) => {
tracing::warn!(
"DecisionModel::with_api_key ignored: custom backends own their keys"
)
}
}
self
}
pub fn with_retry(mut self, retry: crate::retry::RetryConfig) -> Self {
match self.backend {
Slot::SystemOne(b) => self.backend = Slot::SystemOne(b.with_retry(retry)),
Slot::Logprobs(b) => self.backend = Slot::Logprobs(b.with_retry(retry)),
Slot::OpenAi(b) => self.backend = Slot::OpenAi(b.with_retry(retry)),
Slot::Custom(_) => {
tracing::warn!(
"DecisionModel::with_retry ignored: custom backends own their retries"
)
}
}
self
}
pub fn with_cost(mut self, cost: Option<CostConfig>) -> Self {
self.pricing = match cost {
Some(c) => Pricing::Fixed(c),
None => Pricing::Unpriced,
};
self
}
pub fn model(&self) -> &str {
&self.model
}
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn capabilities(&self) -> Capabilities {
self.backend.get().capabilities()
}
pub(crate) fn cost_usd(&self, model: &str, usage: &DecisionUsage) -> Option<f64> {
let cost = match &self.pricing {
Pricing::TypeSafeList => {
let on_typesafe =
matches!(&self.backend, Slot::SystemOne(b) if b.is_typesafe_host());
if !on_typesafe {
return None;
}
crate::provider::prices::global::resolved_cost(TYPESAFE_PRICE_PROVIDER, model)?
}
Pricing::CloudflareList => {
let on_cloudflare =
matches!(&self.backend, Slot::SystemOne(b) if b.is_cloudflare_host());
if !on_cloudflare {
return None;
}
let id = model.strip_prefix("@cf/cloudflare/").unwrap_or(model);
let cost =
crate::provider::prices::global::resolved_cost(CLOUDFLARE_PRICE_PROVIDER, id);
if cost.is_none()
&& crate::provider::prices::global::pricing_enabled()
&& warn_once(format!("unlisted:{CLOUDFLARE_PRICE_PROVIDER}/{id}"))
{
tracing::warn!(
model = %id,
"no price for {CLOUDFLARE_PRICE_PROVIDER}/{id} in the price table; \
its evaluations are unpriced"
);
}
cost?
}
Pricing::OpenAiDecisionsList => {
let on_openai = matches!(&self.backend, Slot::OpenAi(b) if b.is_openai_host());
if !on_openai {
return None;
}
let cost = crate::provider::prices::global::resolved_cost(
OPENAI_DECISIONS_PRICE_PROVIDER,
model,
);
if cost.is_none()
&& crate::provider::prices::global::pricing_enabled()
&& warn_once(format!(
"unlisted:{OPENAI_DECISIONS_PRICE_PROVIDER}/{model}"
))
{
tracing::warn!(
model = %model,
"no price for {OPENAI_DECISIONS_PRICE_PROVIDER}/{model} in the price \
table; its evaluations are unpriced"
);
}
cost?
}
Pricing::Fixed(c) => c.clone(),
Pricing::Unpriced => return None,
};
Some(cost.cost_usd(&usage.to_usage()))
}
pub fn ask(&self, state: impl Into<Value>) -> Ask<'_> {
Ask {
model: self,
state: state.into(),
questions: Vec::new(),
}
}
pub async fn noul(
&self,
state: impl Into<Value>,
instructions: impl Into<Value>,
) -> Result<NoulAnswer, DecisionError> {
let eval = self.ask(state).noul("q", instructions).send().await?;
eval.noul("q")
.cloned()
.ok_or_else(|| DecisionError::BadResponse("no noul answer".into()))
}
pub async fn choice<I, S>(
&self,
state: impl Into<Value>,
instructions: impl Into<Value>,
options: I,
) -> Result<ChoiceAnswer, DecisionError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let eval = self
.ask(state)
.choice("q", instructions, options)
.send()
.await?;
eval.choice("q")
.cloned()
.ok_or_else(|| DecisionError::BadResponse("no choice answer".into()))
}
pub async fn score<I, S>(
&self,
state: impl Into<Value>,
instructions: impl Into<Value>,
levels: I,
) -> Result<ScoreAnswer, DecisionError>
where
I: IntoIterator<Item = S>,
S: Into<Value>,
{
let eval = self
.ask(state)
.score("q", instructions, levels)
.send()
.await?;
eval.score("q")
.cloned()
.ok_or_else(|| DecisionError::BadResponse("no score answer".into()))
}
pub async fn evaluate(
&self,
state: impl Into<Value>,
questions: Vec<(String, Question)>,
) -> Result<Evaluation, DecisionError> {
let mut request = Request::new(self.model.clone(), state);
request.questions = questions;
self.evaluate_request(request).await
}
pub async fn evaluate_request(&self, request: Request) -> Result<Evaluation, DecisionError> {
if !self.fallbacks.is_empty() {
return self.evaluate_chain(request).await;
}
let result = self
.evaluate_unrecorded(request, self.attempt_limit(self.timeout))
.await;
record_attempt(&result);
result
}
fn attempt_limit(&self, remaining: Duration) -> Duration {
self.attempt_timeout.map_or(remaining, |t| t.min(remaining))
}
async fn evaluate_chain(&self, request: Request) -> Result<Evaluation, DecisionError> {
if let Err(e) = request.validate(&Capabilities::unlimited()) {
let result = Err(e);
record_attempt(&result);
return result;
}
let deadline = crate::rt::Instant::now() + self.timeout;
let mut attempts: Vec<FallbackAttempt> = Vec::new();
let mut sent = false;
let members = std::iter::once(self).chain(self.fallbacks.iter());
for (i, member) in members.enumerate() {
let mut req = request.clone();
if i > 0 {
req.model = member.model.clone();
}
if let Err(e) = req.validate(&member.backend.get().capabilities()) {
tracing::debug!(model = %req.model, "decision fallback: skipped a model: {e}");
attempts.push(FallbackAttempt::new(req.model, e, false));
continue;
}
let now = crate::rt::Instant::now();
if now >= deadline {
let result = Err(DecisionError::Timeout(self.timeout));
if !sent {
record_attempt(&result);
}
return result;
}
let limit = member.attempt_limit(deadline - now);
let model = req.model.clone();
let result = member.evaluate_unrecorded(req, limit).await;
sent = true;
record_attempt(&result);
match result {
Ok(eval) => return Ok(eval),
Err(e) if e.is_refusal() => return Err(e),
Err(_) if crate::rt::Instant::now() >= deadline => {
return Err(DecisionError::Timeout(self.timeout))
}
Err(e) => {
tracing::warn!(model = %model, "decision model failed; trying its fallback: {e}");
attempts.push(FallbackAttempt::new(model, e, true));
}
}
}
let result = Err(DecisionError::AllFailed { attempts });
if !sent {
record_attempt(&result);
}
result
}
fn price(&self, eval: &Evaluation) -> Option<f64> {
match &self.pricing {
Pricing::Unpriced => return eval.cost_usd,
Pricing::Fixed(c) if is_free(c) => return Some(0.0),
_ => {}
}
if !eval.usage_reported {
if warn_once(format!("no-usage:{}", eval.model)) {
tracing::warn!(
model = %eval.model,
"decision backend reported no usage; its evaluations are unpriced"
);
}
return None;
}
self.cost_usd(&eval.model, &eval.usage)
}
async fn evaluate_singles(
&self,
backend: &Slot,
request: &Request,
concurrency: usize,
billed: &std::sync::Mutex<Option<Evaluation>>,
) -> Result<Vec<Evaluation>, DecisionError> {
use futures::StreamExt;
#[cfg(not(target_arch = "wasm32"))]
type Single<'a> = std::pin::Pin<
Box<
dyn std::future::Future<Output = (usize, Result<Evaluation, DecisionError>)>
+ Send
+ 'a,
>,
>;
#[cfg(target_arch = "wasm32")]
type Single<'a> = std::pin::Pin<
Box<dyn std::future::Future<Output = (usize, Result<Evaluation, DecisionError>)> + 'a>,
>;
let mut pending: Vec<Single<'_>> = Vec::with_capacity(request.questions.len());
for (i, (id, q)) in request.questions.iter().enumerate() {
pending.push(Box::pin(async move {
let single = Request {
model: request.model.clone(),
state: request.state.clone(),
questions: vec![(id.clone(), q.clone())],
};
let result = match backend.evaluate_billed(&single, billed).await {
Ok(mut eval) => {
add_billed(billed, &eval);
check_complete(&single, &mut eval).map(|()| eval)
}
Err(e) => Err(e),
};
(i, result)
}));
}
let mut queue = pending.into_iter();
let mut running = futures::stream::FuturesUnordered::new();
for next in queue.by_ref().take(concurrency.max(1)) {
running.push(next);
}
let mut done: Vec<Option<Evaluation>> = request.questions.iter().map(|_| None).collect();
let mut failed: Option<(usize, DecisionError)> = None;
while let Some((i, result)) = running.next().await {
match result {
Ok(eval) => done[i] = Some(eval),
Err(e) => {
let earlier = match &failed {
Some((j, _)) => i < *j,
None => true,
};
if earlier {
failed = Some((i, e));
}
}
}
if failed.is_none() {
if let Some(next) = queue.next() {
running.push(next);
}
}
}
if let Some((_, e)) = failed {
return Err(e);
}
Ok(done.into_iter().flatten().collect())
}
async fn evaluate_unrecorded(
&self,
request: Request,
limit: Duration,
) -> Result<Evaluation, DecisionError> {
let backend = &self.backend;
let caps = backend.get().capabilities();
request.validate(&caps)?;
let span = tracing::debug_span!(
"decision",
model = %request.model,
questions = request.questions.len(),
tokens_in = tracing::field::Empty,
cost_usd = tracing::field::Empty,
);
let billed: std::sync::Mutex<Option<Evaluation>> = std::sync::Mutex::new(None);
let work = async {
if caps.batching || request.questions.len() == 1 {
let mut eval = backend.evaluate_billed(&request, &billed).await?;
check_complete(&request, &mut eval).inspect_err(|_| add_billed(&billed, &eval))?;
Ok(eval)
} else {
let answers = self
.evaluate_singles(backend, &request, caps.max_concurrent_requests, &billed)
.await?;
let mut merged: Option<Evaluation> = None;
for eval in answers {
merged = Some(match merged {
None => eval,
Some(mut m) => {
m.usage.input_tokens += eval.usage.input_tokens;
m.usage.output_tokens += eval.usage.output_tokens;
m.usage_reported &= eval.usage_reported;
m.cost_usd = match (m.cost_usd, eval.cost_usd) {
(Some(a), Some(b)) => Some(a + b),
_ => None,
};
m.answers.extend(eval.answers);
m
}
});
}
merged.ok_or_else(|| DecisionError::Invalid("questions: empty".into()))
}
};
let result = {
use tracing::Instrument;
crate::rt::timeout(limit, work.instrument(span.clone())).await
};
let result = match result {
Ok(inner) => inner,
Err(_) => Err(DecisionError::Timeout(limit)),
};
if result.is_err() {
let done = billed.lock().unwrap_or_else(|e| e.into_inner()).take();
if let Some(done) = done {
let cost = self.price(&done);
crate::agent_loop::record_decision(|stats| {
stats.record_billed(done.usage.input_tokens, done.usage.output_tokens, cost)
});
}
}
let mut eval = result?;
eval.cost_usd = self.price(&eval);
span.record("tokens_in", eval.usage.input_tokens);
if let Some(c) = eval.cost_usd {
span.record("cost_usd", c);
}
Ok(eval)
}
}
pub(crate) fn billed_only(model: &str, usage: DecisionUsage, reported: bool) -> Evaluation {
let mut eval = Evaluation::new(model, usage);
eval.usage_reported = reported;
eval
}
pub(crate) fn add_billed(billed: &std::sync::Mutex<Option<Evaluation>>, eval: &Evaluation) {
let mut total = billed.lock().unwrap_or_else(|e| e.into_inner());
let step = Evaluation {
answers: Vec::new(),
..eval.clone()
};
*total = Some(match total.take() {
None => step,
Some(mut t) => {
t.usage.input_tokens += step.usage.input_tokens;
t.usage.output_tokens += step.usage.output_tokens;
t.usage_reported &= step.usage_reported;
t.cost_usd = match (t.cost_usd, step.cost_usd) {
(Some(a), Some(b)) => Some(a + b),
_ => None,
};
t
}
});
}
fn record_attempt(result: &Result<Evaluation, DecisionError>) {
crate::agent_loop::record_decision(|stats| match result {
Ok(eval) => stats.record_success(
eval.usage.input_tokens,
eval.usage.output_tokens,
eval.cost_usd,
),
Err(e) => stats.record_failure(matches!(e, DecisionError::Timeout(_))),
});
}
fn is_free(c: &CostConfig) -> bool {
c.input_per_million == 0.0
&& c.output_per_million == 0.0
&& c.cache_read_per_million == 0.0
&& c.cache_write_per_million == 0.0
&& c.context_tiers.iter().all(|t| {
t.input_per_million == 0.0
&& t.output_per_million == 0.0
&& t.cache_read_per_million == 0.0
&& t.cache_write_per_million == 0.0
})
}
pub(crate) fn warn_once(key: String) -> bool {
static WARNED: std::sync::Mutex<std::collections::BTreeSet<String>> =
std::sync::Mutex::new(std::collections::BTreeSet::new());
WARNED.lock().unwrap_or_else(|e| e.into_inner()).insert(key)
}
fn check_complete(request: &Request, eval: &mut Evaluation) -> Result<(), DecisionError> {
let mut ordered = Vec::with_capacity(request.questions.len());
for (id, q) in &request.questions {
let Some(at) = eval.answers.iter().position(|(k, _)| k == id) else {
return Err(DecisionError::BadResponse(format!("answers.{id}: missing")));
};
let (key, answer) = eval.answers.swap_remove(at);
answer.validate(id, q)?;
ordered.push((key, answer));
}
eval.answers = ordered;
Ok(())
}
#[must_use = "a request does nothing until .send().await"]
pub struct Ask<'a> {
model: &'a DecisionModel,
state: Value,
questions: Vec<(String, Question)>,
}
impl Ask<'_> {
pub fn noul(self, id: impl Into<String>, instructions: impl Into<Value>) -> Self {
self.question(id, Question::noul(instructions))
}
pub fn choice<I, S>(
self,
id: impl Into<String>,
instructions: impl Into<Value>,
options: I,
) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.question(id, Question::choice(instructions, options))
}
pub fn score<I, S>(
self,
id: impl Into<String>,
instructions: impl Into<Value>,
levels: I,
) -> Self
where
I: IntoIterator<Item = S>,
S: Into<Value>,
{
self.question(id, Question::score(instructions, levels))
}
pub fn question(mut self, id: impl Into<String>, question: Question) -> Self {
self.questions.push((id.into(), question));
self
}
pub async fn send(self) -> Result<Evaluation, DecisionError> {
self.model.evaluate(self.state, self.questions).await
}
}
pub(crate) fn wire(
advisory: Option<&Advisory>,
gate: Option<&ToolGate>,
skills: &crate::skills::SkillSet,
) -> Vec<Arc<dyn crate::Extension>> {
let mut extensions: Vec<Arc<dyn crate::Extension>> = Vec::new();
if let Some(a) = advisory {
extensions.push(Arc::new(advisory::AdvisorExtension::new(
advisory::Advisor::new(a.clone(), skills),
)));
}
if let Some(g) = gate {
extensions.push(Arc::new(g.clone()));
}
extensions
}
#[cfg(test)]
mod pricing_tests {
use super::*;
#[test]
fn clef_prices_by_model_only_on_cloudflare() {
crate::provider::prices::enable_bundled();
let usage = DecisionUsage::new(2_000_000, 500);
let clef = DecisionModel::clef("acct");
let cost = clef.cost_usd("clef", &usage).unwrap();
assert!((cost - 0.48).abs() < 1e-12, "{cost}");
let cost = clef.cost_usd("@cf/cloudflare/clef", &usage).unwrap();
assert!((cost - 0.48).abs() < 1e-12, "{cost}");
let flash = DecisionModel::clef_flash("acct");
let cost = flash.cost_usd("clef-flash", &usage).unwrap();
assert!((cost - 0.18).abs() < 1e-12, "{cost}");
assert_eq!(clef.cost_usd("clef-9", &usage), None, "unlisted unpriced");
let proxied = DecisionModel {
backend: Slot::SystemOne(
SystemOneBackend::workers_ai("acct", "x")
.with_endpoint_url("https://proxy.example/ai/run/@cf/cloudflare/clef"),
),
..DecisionModel::clef("acct")
};
assert_eq!(proxied.cost_usd("clef", &usage), None);
}
#[test]
fn gpt_6_luna_prices_by_reported_model_only_on_openai() {
crate::provider::prices::enable_bundled();
let usage = DecisionUsage::new(2_000_000, 500);
let luna = DecisionModel::gpt_6_luna();
let cost = luna.cost_usd("gpt-6-luna", &usage).unwrap();
assert!((cost - 0.2).abs() < 1e-12, "{cost}");
assert_eq!(luna.cost_usd("gpt-6-luna-x", &usage), None, "unlisted");
assert_ne!(
crate::provider::prices::global::resolved_cost("openai", "gpt-6-luna"),
crate::provider::prices::global::resolved_cost(
OPENAI_DECISIONS_PRICE_PROVIDER,
"gpt-6-luna"
)
);
let proxied = DecisionModel::from_openai_backend(
OpenAiDecisionBackend::new().with_base_url("https://proxy.example/v1"),
"gpt-6-luna",
);
assert_eq!(proxied.cost_usd("gpt-6-luna", &usage), None);
}
#[test]
fn jev_prices_by_reported_version_only_on_typesafe() {
crate::provider::prices::enable_bundled();
let usage = DecisionUsage::new(2_000_000, 500);
let jev = DecisionModel {
backend: Slot::SystemOne(
SystemOneBackend::typesafe().with_base_url("https://api.typesafe.ai"),
),
..DecisionModel::jev()
};
let cost = jev.cost_usd("jev-1.13.0", &usage).unwrap();
assert!((cost - 0.084).abs() < 1e-12, "{cost}");
assert_eq!(jev.cost_usd("jev-latest", &usage), None, "aliases unpriced");
assert_eq!(jev.cost_usd("jev-9.9.9", &usage), None, "unlisted unpriced");
let proxied = DecisionModel {
backend: Slot::SystemOne(
SystemOneBackend::typesafe().with_base_url("https://proxy.example"),
),
..DecisionModel::jev()
};
assert_eq!(proxied.cost_usd("jev-1.13.0", &usage), None);
assert_eq!(
DecisionModel::jev_opencode().cost_usd("jev-1.13.0", &usage),
None
);
assert_eq!(
DecisionModel::local("http://localhost:1").cost_usd("x", &usage),
Some(0.0)
);
let mock = DecisionModel::from_backend(MockBackend::neutral(), "m");
assert_eq!(mock.cost_usd("m", &usage), None, "from_backend is unpriced");
assert_eq!(
mock.with_cost(Some(CostConfig::new(1.0, 0.0)))
.cost_usd("m", &usage),
Some(2.0)
);
}
#[test]
fn logprobs_are_free_only_on_loopback() {
let usage = DecisionUsage::new(2_000_000, 500);
for local in [
"http://localhost:8080",
"http://127.0.0.1:1/v1",
"http://[::1]:9",
] {
let m = DecisionModel::logprobs(local, "m");
assert!(m.capabilities().local, "{local}");
assert_eq!(m.cost_usd("m", &usage), Some(0.0), "{local}");
}
for remote in [
"https://api.openai.com/v1",
"http://10.1.2.3:8000",
"http://gpu-box:8080",
] {
let m = DecisionModel::logprobs(remote, "m");
assert!(!m.capabilities().local, "{remote}");
assert_eq!(m.cost_usd("m", &usage), None, "{remote}: unpriced");
assert_eq!(
m.with_cost(Some(CostConfig::new(1.0, 0.0)))
.cost_usd("m", &usage),
Some(2.0)
);
}
}
}