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<Value>,
pub(crate) prompt_embeds: Option<Value>,
pub(crate) allowed_token_ids: Option<Value>,
pub(crate) bad_words: Option<Value>,
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 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,
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() {
return Err(refusal(
route,
"prompt_logprobs",
"logprobs for the PROMPT's own tokens",
));
}
if echo == &Some(true) {
return Err(refusal(
route,
"echo",
"prepending the prompt to the completion",
));
}
if use_beam_search == &Some(true) {
return Err(refusal(
route,
"use_beam_search",
"beam search; this server samples",
));
}
if truncate_prompt_tokens.is_some() {
return Err(refusal(
route,
"truncate_prompt_tokens",
"truncating the prompt server-side -- send the prompt you want answered",
));
}
if prompt_embeds.is_some() {
return Err(refusal(
route,
"prompt_embeds",
"embeddings as input in place of text",
));
}
if allowed_token_ids.is_some() {
return Err(refusal(
route,
"allowed_token_ids",
"restricting sampling to a token-id set; `response_format` constrains output here",
));
}
if bad_words.is_some() {
return Err(refusal(
route,
"bad_words",
"forbidding strings during sampling; `stop` ends a generation but does not steer it",
));
}
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); 11] = [
("n", serde_json::json!({ "n": 2 })),
("best_of", serde_json::json!({ "best_of": 2 })),
(
"prompt_logprobs",
serde_json::json!({ "prompt_logprobs": 1 }),
),
("echo", serde_json::json!({ "echo": true })),
(
"use_beam_search",
serde_json::json!({ "use_beam_search": true }),
),
(
"truncate_prompt_tokens",
serde_json::json!({ "truncate_prompt_tokens": 8 }),
),
(
"prompt_embeds",
serde_json::json!({ "prompt_embeds": "AA==" }),
),
(
"allowed_token_ids",
serde_json::json!({ "allowed_token_ids": [1, 2] }),
),
("bad_words", serde_json::json!({ "bad_words": ["x"] })),
(
"skip_special_tokens",
serde_json::json!({ "skip_special_tokens": false }),
),
(
"return_tokens_as_token_ids",
serde_json::json!({ "return_tokens_as_token_ids": true }),
),
];
assert_eq!(
cases.len(),
serde_json::to_value(UnimplementedFields::default())
.expect("serializes")
.as_object()
.expect("an object")
.len(),
"every field of the struct needs a case"
);
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"));
}
}