use serde_json::Value;
use crate::{unsupported_feature, ApiError};
const SERVES_SEVERAL_CHOICES: &[&str] = &[
frink_api::routes::V1_COMPLETIONS,
frink_api::routes::V1_CHAT_COMPLETIONS,
];
#[derive(Debug, Default, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub(crate) struct UnimplementedFields {
pub(crate) n: Option<u32>,
pub(crate) best_of: Option<u32>,
pub(crate) prompt_logprobs: Option<Value>,
pub(crate) echo: Option<bool>,
pub(crate) use_beam_search: Option<bool>,
pub(crate) truncate_prompt_tokens: Option<i64>,
pub(crate) prompt_embeds: Option<Value>,
pub(crate) allowed_token_ids: Option<Vec<u32>>,
pub(crate) bad_words: Option<Vec<String>>,
pub(crate) cache_salt: Option<String>,
pub(crate) skip_special_tokens: Option<bool>,
pub(crate) return_tokens_as_token_ids: Option<bool>,
}
impl UnimplementedFields {
pub(crate) fn candidates(&self) -> usize {
let n = self.n.unwrap_or(1).max(1) as usize;
let k = self.best_of.unwrap_or(0) as usize;
n.max(k)
}
pub(crate) fn truncate_prompt_tokens(&self) -> Option<usize> {
self.truncate_prompt_tokens
.filter(|&k| k >= 1)
.map(|k| k as usize)
}
pub(crate) fn token_mask(&self) -> crate::token_mask::TokenMask {
crate::token_mask::TokenMask::requested(
self.allowed_token_ids
.as_ref()
.map(|ids| ids.iter().map(|&i| i as usize).collect()),
self.bad_words.clone().unwrap_or_default(),
)
}
pub(crate) fn ranks_candidates(&self) -> bool {
self.candidates() > self.n.unwrap_or(1).max(1) as usize
}
pub(crate) fn refuse(&self, route: &str) -> Result<(), ApiError> {
let UnimplementedFields {
n,
best_of,
prompt_logprobs,
echo,
use_beam_search,
truncate_prompt_tokens,
prompt_embeds,
allowed_token_ids,
bad_words,
cache_salt,
skip_special_tokens,
return_tokens_as_token_ids,
} = self;
if n.is_some_and(|v| v > 1) && !SERVES_SEVERAL_CHOICES.contains(&route) {
return Err(refusal(
route,
"n",
"more than one completion per request on this wire, which has no `choices` array \
to return them in; use /v1/completions or send the request again",
));
}
if best_of.is_some_and(|v| v > 1) && !SERVES_SEVERAL_CHOICES.contains(&route) {
return Err(refusal(
route,
"best_of",
"generating several completions and returning the best-scoring one on this wire, \
which has no `choices` array to return them in",
));
}
if let (Some(k), Some(n)) = (best_of, n) {
if k < n {
return Err(crate::invalid_request(
&format!("`best_of` is {k} and `n` is {n}; best_of must be at least n"),
"best_of",
));
}
}
if prompt_logprobs.is_some() && route != frink_api::routes::V1_COMPLETIONS {
return Err(refusal(
route,
"prompt_logprobs",
"logprobs for the PROMPT's own tokens on this wire, which has no field for them",
));
}
if echo == &Some(true) && route != frink_api::routes::V1_COMPLETIONS {
return Err(refusal(
route,
"echo",
"prepending the prompt to the completion on this wire, which returns a message \
rather than a continuation of the prompt",
));
}
if use_beam_search == &Some(true) {
return Err(refusal(
route,
"use_beam_search",
"beam search; this server samples",
));
}
if truncate_prompt_tokens.is_some_and(|k| k < 1) {
return Err(crate::invalid_request(
"`truncate_prompt_tokens` must be at least 1: it is how many of the prompt's \
last tokens to keep",
"truncate_prompt_tokens",
));
}
if prompt_embeds.is_some() {
return Err(refusal(
route,
"prompt_embeds",
"embeddings as input in place of text",
));
}
if allowed_token_ids.as_ref().is_some_and(|ids| ids.is_empty()) {
return Err(crate::invalid_request(
"`allowed_token_ids` is empty, so there is no token this request could draw",
"allowed_token_ids",
));
}
let _ = bad_words;
let _ = cache_salt;
if skip_special_tokens == &Some(false) {
return Err(refusal(
route,
"skip_special_tokens",
"returning special tokens in the text; this server always skips them",
));
}
if return_tokens_as_token_ids == &Some(true) {
return Err(refusal(
route,
"return_tokens_as_token_ids",
"returning token ids in place of text pieces; `/v1/tokenize` returns ids",
));
}
Ok(())
}
}
fn refusal(route: &str, field: &str, what: &str) -> ApiError {
unsupported_feature(&format!(
"`{field}` is not implemented on {route}: {what} (see docs/API.md)"
))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(body: serde_json::Value) -> UnimplementedFields {
serde_json::from_value(body).expect("the struct is all-optional")
}
#[test]
fn spelling_out_the_defaults_is_not_a_refusal() {
for body in [
serde_json::json!({ "n": 1 }),
serde_json::json!({ "best_of": 1 }),
serde_json::json!({ "echo": false }),
serde_json::json!({ "use_beam_search": false }),
serde_json::json!({ "skip_special_tokens": true }),
serde_json::json!({ "return_tokens_as_token_ids": false }),
serde_json::json!({}),
] {
assert!(
parse(body.clone())
.refuse(frink_api::routes::COMPLETION)
.is_ok(),
"{body} should be served"
);
}
}
#[test]
fn every_field_refuses_by_name() {
let cases: [(&str, serde_json::Value); 7] = [
("n", serde_json::json!({ "n": 2 })),
("best_of", serde_json::json!({ "best_of": 2 })),
(
"prompt_logprobs",
serde_json::json!({ "prompt_logprobs": 1 }),
),
(
"use_beam_search",
serde_json::json!({ "use_beam_search": true }),
),
(
"prompt_embeds",
serde_json::json!({ "prompt_embeds": "AA==" }),
),
(
"skip_special_tokens",
serde_json::json!({ "skip_special_tokens": false }),
),
(
"return_tokens_as_token_ids",
serde_json::json!({ "return_tokens_as_token_ids": true }),
),
];
const SERVED: [&str; 5] = [
"cache_salt",
"allowed_token_ids",
"bad_words",
"echo",
"truncate_prompt_tokens",
];
assert_eq!(
cases.len() + SERVED.len(),
serde_json::to_value(UnimplementedFields::default())
.expect("serializes")
.as_object()
.expect("an object")
.len(),
"every field of the struct must be refused above or listed in SERVED"
);
for (field, body) in cases {
let err = parse(body)
.refuse(frink_api::routes::COMPLETION)
.expect_err("{field} must refuse");
let msg = format!("{err:?}");
assert!(msg.contains(field), "{field} not named in {msg}");
}
}
#[test]
fn a_route_with_a_choices_array_serves_n() {
let four = parse(serde_json::json!({ "n": 4 }));
for route in [
frink_api::routes::V1_COMPLETIONS,
frink_api::routes::V1_CHAT_COMPLETIONS,
] {
assert!(
four.refuse(route).is_ok(),
"{route} renders several choices and must serve `n`"
);
}
let err = four
.refuse(frink_api::routes::COMPLETION)
.expect_err("no choices array");
assert!(format!("{err:?}").contains('n'));
let one = parse(serde_json::json!({ "n": 1 }));
for route in [
frink_api::routes::COMPLETION,
frink_api::routes::V1_CHAT_COMPLETIONS,
frink_api::routes::V1_COMPLETIONS,
] {
assert!(one.refuse(route).is_ok(), "{route} refused n = 1");
}
}
#[test]
fn the_message_names_the_route() {
let err = parse(serde_json::json!({ "echo": true }))
.refuse("/v1/chat/completions")
.expect_err("refuses");
assert!(format!("{err:?}").contains("/v1/chat/completions"));
}
}