#![allow(clippy::panic)]
use crate::chat::message::Message;
use crate::chat::request::ChatRequest;
use crate::chat::stream::Transcript;
use crate::cost::usage::UsageCoverage;
use crate::model::ModelId;
use crate::provider::{Access, Provider};
#[cfg(feature = "embeddings")]
use crate::embed::{EmbedRequest, Embedder};
pub async fn assert_provider_contract(provider: &impl Provider, known_model: &str) {
let id = provider.id().to_string();
assert!(!id.trim().is_empty(), "a provider with no id");
assert!(
!id.contains(char::is_whitespace),
"{id:?} has whitespace in it. This goes into records and reports beside every call, \
and a name with a space in it splits when somebody parses a line"
);
let unknown = ModelId::from("llmr-contract-no-such-model");
assert!(
provider.capabilities(&unknown).is_none(),
"{id} claims to know a model that does not exist. None means unknown; a capability \
set with everything off means known and unable"
);
let model = ModelId::from(known_model);
if let Some(caps) = provider.capabilities(&model) {
assert!(
caps.max_output <= caps.context_window || caps.context_window == 0,
"{id} says {known_model} produces more tokens than fit in its window"
);
}
let access = provider.validate(&model).await;
assert!(
!access.is_denied(),
"{id} says {known_model} cannot be reached ({access}), and this suite is about to \
reach it. Denied is for a settled no, so one of the two is wrong"
);
assert!(
!provider.validate(&unknown).await.is_ready(),
"{id} says a model that does not exist is reachable. That is the same failure as a \
provider claiming to know every model name, one method along: it turns a typo into \
something a router will happily select"
);
let again = provider.validate(&model).await;
assert_eq!(
access.as_str(),
again.as_str(),
"{id} answered {access} and then {again} for the same model. Either it cached the \
first answer or it is holding state between calls"
);
let reply = provider
.chat(
ChatRequest::new(
model.clone(),
vec![Message::user("Reply with the word ok.")],
)
.with_max_tokens(16),
)
.await;
let reply = match reply {
Ok(reply) => reply,
Err(e) => panic!("{id} could not answer a one word request: {e}"),
};
assert!(
!reply.message.content.is_empty(),
"{id} returned a reply with no content. An unreadable reply is an error, never an \
empty answer, because a caller cannot tell an empty answer from a failure"
);
assert!(
!reply.model.as_str().trim().is_empty(),
"{id} did not say which model served the request. Price against this one, not \
against what was asked for, so it has to be there"
);
if reply.usage.coverage() != UsageCoverage::Absent {
assert!(
reply.usage.output_tokens.unwrap_or(1) > 0 || !reply.text().is_empty(),
"{id} reported zero output tokens for a reply that has text in it"
);
}
let streamed = provider
.stream(
ChatRequest::new(
model.clone(),
vec![Message::user("Reply with the word ok.")],
)
.with_max_tokens(16),
)
.await;
match streamed {
Err(e) => panic!("{id} could not answer the same request as a stream: {e}"),
Ok(stream) => {
let mut transcript = Transcript::new(model.clone());
if let Err(e) = transcript.drain(stream).await {
panic!("{id} broke partway through a stream: {e}");
}
let assembled = transcript.finish();
assert!(
!assembled.message.content.is_empty(),
"{id} streamed a reply with no content in it. The same rule as `chat`: an \
unreadable reply is an error, never an empty answer"
);
assert!(
assembled.stop_reason != crate::chat::message::StopReason::Interrupted,
"{id} ended a stream without ever saying why it stopped. That reads as a \
connection that broke, so a caller cannot tell this apart from one that did"
);
assert_eq!(
assembled.usage.coverage(),
reply.usage.coverage(),
"{id} reports usage differently depending on whether the reply was streamed. \
A streamed call that reports nothing where a whole one reports numbers \
becomes a free call in every report that adds it up"
);
assert!(
!assembled.model.as_str().trim().is_empty(),
"{id} did not say which model served the streamed request"
);
}
}
let again = provider
.chat(
ChatRequest::new(model, vec![Message::user("Reply with the word ok.")])
.with_max_tokens(16),
)
.await;
assert!(
again.is_ok(),
"{id} answered once and failed the second time, which usually means state left \
over from the first call"
);
}
pub async fn assert_a_bad_credential_is_denied(provider: &impl Provider, model: &str) {
let id = provider.id();
let access = provider.validate(&ModelId::from(model)).await;
match access {
Access::Denied { .. } => {}
Access::Ready => panic!(
"{id} says it is ready with a credential that will be rejected. Whatever this \
checked, it was not the credential"
),
Access::Unknown { ref why } => panic!(
"{id} reports a rejected credential as unknown ({why}). Unknown means ask again \
later, so a router keeps this provider and a retry loop keeps trying it. A key \
nobody is told about is a key nobody fixes"
),
}
}
#[cfg(feature = "embeddings")]
#[cfg_attr(docsrs, doc(cfg(feature = "embeddings")))]
pub async fn assert_embedder_contract(embedder: &impl Embedder, known_model: &str) {
let id = embedder.id().to_string();
assert!(!id.trim().is_empty(), "an embedder with no id");
assert!(
!id.contains(char::is_whitespace),
"{id:?} has whitespace in it. This goes into records beside every call, and a name \
with a space in it splits when somebody parses a line"
);
let unknown = ModelId::from("llmr-contract-no-such-model");
assert!(
embedder.capabilities(&unknown).is_none(),
"{id} claims to know a model that does not exist. None means unknown; a capability \
set with everything off means known and unable"
);
let model = ModelId::from(known_model);
let access = embedder.validate(&model).await;
assert!(
!access.is_denied(),
"{id} says {known_model} cannot be reached ({access}), and this suite is about to \
reach it. Denied is for a settled no, so one of the two is wrong"
);
let inputs = vec![
"The tide came in over the harbour wall.".to_string(),
"Compile times are dominated by monomorphisation.".to_string(),
"She poached the eggs for four minutes exactly.".to_string(),
];
let reply = embedder
.embed(EmbedRequest::new(model.clone(), inputs.clone()))
.await;
let reply = match reply {
Ok(reply) => reply,
Err(e) => panic!("{id} could not embed with {known_model}: {e}"),
};
assert_eq!(
reply.len(),
inputs.len(),
"{id} was sent {} inputs and answered {} vectors. A batch that cannot be lined up \
with its inputs has to fail rather than come back short",
inputs.len(),
reply.len()
);
assert!(
reply.dimensions().is_some(),
"{id} answered vectors of more than one length in a single batch. Nothing downstream \
can hold them"
);
for (position, vector) in reply.vectors.iter().enumerate() {
assert!(
vector.dimensions() > 0,
"{id} answered an empty vector at position {position}"
);
assert_eq!(
vector.model, reply.model,
"{id} answered a vector at position {position} filed under a different model \
from the reply's. A vector's model is what stops it being compared against a \
space it does not belong to"
);
}
for (position, input) in inputs.iter().enumerate() {
let alone = match embedder
.embed(EmbedRequest::one(model.clone(), input.clone()))
.await
{
Ok(alone) => alone,
Err(e) => panic!("{id} could not embed one input on its own: {e}"),
};
let alone = match alone.vectors.first() {
Some(vector) => vector.clone(),
None => panic!("{id} answered no vector for one input"),
};
let mine = match reply.get(position).and_then(|v| alone.similarity(v)) {
Some(score) => score,
None => panic!(
"{id}: the vector for input {position} could not be compared with the batch's. \
Either they are filed under different models or they are different lengths"
),
};
for (other, vector) in reply.vectors.iter().enumerate() {
if other == position {
continue;
}
let theirs = alone.similarity(vector).unwrap_or(f32::NAN);
assert!(
mine > theirs,
"{id} answered a batch that is not in the order it was sent. Input {position} \
looks more like the vector at position {other} ({theirs}) than like the one \
at its own position ({mine}). Sort by the index the reply carries rather \
than trusting arrival order"
);
}
}
let coverage = reply.usage.coverage();
if coverage == UsageCoverage::Absent {
assert_eq!(
reply.usage.prompt_tokens(),
None,
"{id} reports usage coverage as absent while carrying a token count"
);
}
}