llm_token_visualizer/
interop.rs1#[cfg(feature = "async-openai")]
13#[cfg_attr(docsrs, doc(cfg(feature = "async-openai")))]
14pub mod async_openai {
15 use ::async_openai::types::chat::{ChatCompletionTokenLogprob, CreateChatCompletionResponse};
16 use anyhow::{bail, Result};
17
18 use crate::detect::{Alternative, LogprobToken};
19
20 impl From<&ChatCompletionTokenLogprob> for LogprobToken {
21 fn from(t: &ChatCompletionTokenLogprob) -> Self {
22 let mut top: Vec<Alternative> = t
23 .top_logprobs
24 .iter()
25 .map(|a| Alternative {
26 token: a.token.clone(),
27 logprob: f64::from(a.logprob),
28 })
29 .collect();
30 top.sort_by(|a, b| b.logprob.total_cmp(&a.logprob));
31 LogprobToken {
32 token: t.token.clone(),
33 logprob: f64::from(t.logprob),
34 top_logprobs: top,
35 }
36 }
37 }
38
39 pub fn tokens_from_response(resp: &CreateChatCompletionResponse) -> Result<Vec<LogprobToken>> {
48 let Some(choice) = resp.choices.first() else {
49 bail!("the response has no choices");
50 };
51 let Some(content) = choice.logprobs.as_ref().and_then(|l| l.content.as_ref()) else {
52 bail!("no token logprobs; request the completion with logprobs(true)");
53 };
54 let tokens: Vec<LogprobToken> = content
55 .iter()
56 .map(LogprobToken::from)
57 .filter(|t| !(t.token.starts_with("<|") && t.token.ends_with("|>")))
58 .collect();
59 if tokens.is_empty() {
60 bail!("logprobs contain no tokens");
61 }
62 Ok(tokens)
63 }
64
65 #[cfg(test)]
66 mod tests {
67 use super::*;
68
69 #[test]
70 fn same_tokens_as_the_json_parser() {
71 let json = include_str!("../examples/logprobs/cuyp.json");
72 let resp: CreateChatCompletionResponse = serde_json::from_str(json).unwrap();
73 let a = tokens_from_response(&resp).unwrap();
74 let b = crate::detect::parse_logprobs(json).unwrap();
75 assert_eq!(a.len(), b.len());
76 for (x, y) in a.iter().zip(&b) {
77 assert_eq!(x.token, y.token);
78 assert!((x.logprob - y.logprob).abs() < 1e-6);
80 }
81 let r = crate::detect::detect(&a, 0.6);
82 assert!(r.spans.iter().any(|s| s.text == " Dordrecht"));
83 }
84 }
85}