use ferrox_models::sampler_order::{SamplerOrder, SamplerOrderError};
use serde_json::Value;
use crate::{invalid_request, unsupported_feature, ApiError};
pub(crate) fn parse_sampler_order(
value: Option<&Value>,
route: &str,
) -> Result<Option<SamplerOrder>, ApiError> {
let names: Vec<String> = match value {
None | Some(Value::Null) => return Ok(None),
Some(Value::Array(items)) => {
if items.is_empty() {
return Ok(None);
}
items
.iter()
.map(|item| match item {
Value::String(s) => Ok(s.clone()),
other => Err(invalid_request(
&format!("`samplers` must be a list of sampler names; got {other}"),
"samplers",
)),
})
.collect::<Result<_, _>>()?
}
Some(Value::String(s)) => s.split(';').map(str::to_string).collect(),
Some(other) => {
return Err(invalid_request(
&format!(
"`samplers` must be a list of sampler names or a `;`-separated \
string; got {other}"
),
"samplers",
))
}
};
match SamplerOrder::from_names(names) {
Ok(order) => Ok(Some(order)),
Err(err @ SamplerOrderError::Unimplemented { .. }) => Err(unsupported_feature(&format!(
"`samplers` on {route}: {err}"
))),
Err(err) => Err(invalid_request(
&format!("`samplers` on {route}: {err}"),
"samplers",
)),
}
}
pub(crate) fn refuse_logit_bias(value: Option<&Value>, route: &str) -> Result<(), ApiError> {
let Some(value) = value else {
return Ok(());
};
if value.is_null() || value.as_object().is_some_and(|m| m.is_empty()) {
return Ok(());
}
Err(unsupported_feature(&format!(
"`logit_bias` is not implemented on {route} (see docs/API.md). \
It is refused rather than ignored: a dropped bias is \
indistinguishable from an honoured one."
)))
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
#[test]
fn a_real_bias_is_refused_by_name() {
let bias = serde_json::json!({"50256": -100.0});
let (status, body) =
refuse_logit_bias(Some(&bias), "/v1/chat/completions").expect_err("refused");
assert_eq!(status, StatusCode::NOT_IMPLEMENTED);
let message = body["error"]["message"].as_str().expect("message");
assert!(message.contains("logit_bias"), "{message}");
assert!(message.contains("/v1/chat/completions"), "{message}");
}
#[test]
fn an_empty_or_absent_bias_is_served() {
assert!(refuse_logit_bias(None, "/v1/completions").is_ok());
assert!(refuse_logit_bias(Some(&serde_json::json!({})), "/v1/completions").is_ok());
}
#[test]
fn a_supported_chain_is_honoured_from_a_list_or_from_a_string() {
let from_list = parse_sampler_order(
Some(&serde_json::json!(["penalties", "temperature", "top_k"])),
"/completion",
)
.expect("supported")
.expect("present");
let from_string = parse_sampler_order(
Some(&serde_json::json!("penalties;temperature;top_k")),
"/completion",
)
.expect("supported")
.expect("present");
assert_eq!(from_list, from_string);
assert_eq!(from_list.to_string(), "penalties;temperature;top_k");
}
#[test]
fn an_absent_or_empty_list_means_the_default_chain() {
for silence in [
None,
Some(serde_json::json!(null)),
Some(serde_json::json!([])),
] {
assert!(
parse_sampler_order(silence.as_ref(), "/completion")
.expect("silence is served")
.is_none(),
"{silence:?} must resolve to the default chain"
);
}
let (status, _) = parse_sampler_order(Some(&serde_json::json!("")), "/completion")
.expect_err("an empty string is a value, not silence");
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn a_sampler_ferrox_lacks_is_refused_by_name_as_not_implemented() {
for name in ["mirostat", "infill"] {
let body = serde_json::json!([name, "temperature"]);
let (status, message) = parse_sampler_order(Some(&body), "/completion")
.expect_err("{name} must be refused, not skipped");
assert_eq!(status, StatusCode::NOT_IMPLEMENTED, "{name}");
let text = message["error"]["message"].as_str().expect("message");
assert!(text.contains(name), "{text}");
assert!(text.contains("/completion"), "{text}");
}
}
#[test]
fn an_unknown_sampler_is_a_client_error_naming_the_name() {
let (status, message) = parse_sampler_order(
Some(&serde_json::json!(["top_k", "top_kk", "temperature"])),
"/v1/completions",
)
.expect_err("no such sampler");
assert_eq!(status, StatusCode::BAD_REQUEST);
let text = message["error"]["message"].as_str().expect("message");
assert!(text.contains("top_kk"), "{text}");
let (status, _) = parse_sampler_order(Some(&serde_json::json!(7)), "/v1/completions")
.expect_err("a number is not a chain");
assert_eq!(status, StatusCode::BAD_REQUEST);
let (status, _) = parse_sampler_order(
Some(&serde_json::json!(["top_k", 7, "temperature"])),
"/v1/completions",
)
.expect_err("a list of not-names is not a chain");
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn every_route_answers_a_sampler_chain_the_same_way() {
for (samplers, expected_status) in [
(serde_json::json!(["top_k", "temperature"]), None),
(serde_json::json!([]), None),
(
serde_json::json!(["mirostat", "temperature"]),
Some(StatusCode::NOT_IMPLEMENTED),
),
(
serde_json::json!([
"penalties",
"dry",
"top_n_sigma",
"top_k",
"typ_p",
"top_p",
"min_p",
"xtc",
"temperature"
]),
None,
),
(
serde_json::json!(["top_kk", "temperature"]),
Some(StatusCode::BAD_REQUEST),
),
(serde_json::json!(["top_k"]), Some(StatusCode::BAD_REQUEST)),
] {
let chat: crate::ChatCompletionRequest = serde_json::from_value(serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": "hi"}],
"samplers": samplers,
}))
.expect("chat request");
let completions: crate::openai_extra::CompletionsRequest =
serde_json::from_value(serde_json::json!({
"prompt": "hi",
"samplers": samplers,
}))
.expect("completions request");
let native: crate::completion::CompletionRequest =
serde_json::from_value(serde_json::json!({
"prompt": "hi",
"samplers": samplers,
}))
.expect("completion request");
let chat_status = chat.validate_supported_fields().err().map(|(s, _)| s);
let completions_status = completions.validate().err().map(|(s, _)| s);
let native_status = native.validate(false).err().map(|(s, _)| s);
assert_eq!(
chat_status, completions_status,
"chat and /v1/completions disagree about {samplers}"
);
assert_eq!(
chat_status, native_status,
"chat and /completion disagree about {samplers}"
);
assert_eq!(chat_status, expected_status, "wrong verdict for {samplers}");
}
}
#[test]
fn a_malformed_bias_is_refused_rather_than_read_as_empty() {
assert!(refuse_logit_bias(Some(&serde_json::json!([])), "/v1/completions").is_err());
assert!(refuse_logit_bias(Some(&serde_json::json!("none")), "/v1/completions").is_err());
}
#[test]
fn both_routes_answer_a_logit_bias_the_same_way() {
for (bias, expected_refusal) in [
(serde_json::json!({"50256": -100.0}), true),
(serde_json::json!({}), false),
] {
let chat: crate::ChatCompletionRequest = serde_json::from_value(serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": "hi"}],
"logit_bias": bias,
}))
.expect("chat request");
let completion: crate::openai_extra::CompletionsRequest =
serde_json::from_value(serde_json::json!({
"prompt": "hi",
"logit_bias": bias,
}))
.expect("completions request");
let chat_status = chat.validate_supported_fields().err().map(|(s, _)| s);
let completion_status = completion.validate().err().map(|(s, _)| s);
assert_eq!(
chat_status, completion_status,
"the two routes disagree about logit_bias {bias}"
);
assert_eq!(
chat_status.is_some(),
expected_refusal,
"wrong verdict for logit_bias {bias}"
);
}
}
}