use serde_json::{json, Value};
#[cfg(any(feature = "ollama", feature = "extract"))]
pub(crate) fn base_url(configured: &str) -> String {
let trimmed = configured.trim().trim_end_matches('/');
match trimmed.strip_suffix("/v1") {
Some(base) if !base.is_empty() && !base.ends_with('/') => base.to_owned(),
_ => trimmed.to_owned(),
}
}
#[cfg(feature = "ollama")]
pub(crate) const EMBEDDINGS_PATH: &str = "/v1/embeddings";
#[cfg(feature = "extract")]
pub(crate) const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions";
#[cfg(feature = "ollama")]
pub(crate) fn embeddings_body(model: &str, input: &str) -> String {
json!({ "model": model, "input": input }).to_string()
}
#[cfg(feature = "extract")]
pub(crate) fn chat_body(model: &str, prompt: &str, max_tokens: u32) -> String {
json!({
"model": model,
"messages": [{ "role": "user", "content": prompt }],
"temperature": 0,
"max_tokens": max_tokens,
})
.to_string()
}
#[cfg(feature = "ollama")]
pub(crate) fn parse_embeddings_response(payload: &str) -> Result<Vec<f32>, String> {
let value = parse_json(payload)?;
let response: EmbeddingsResponse = serde_json::from_value(value).map_err(|err| {
format!(
"response has no `data[0].embedding` array ({err}): {}",
preview(payload)
)
})?;
response
.data
.into_iter()
.next()
.map(|datum| datum.embedding)
.ok_or_else(|| {
format!(
"response has no `data[0].embedding` array: {}",
preview(payload)
)
})
}
#[cfg(feature = "ollama")]
#[derive(serde::Deserialize)]
struct EmbeddingsResponse {
data: Vec<EmbeddingDatum>,
}
#[cfg(feature = "ollama")]
#[derive(serde::Deserialize)]
struct EmbeddingDatum {
embedding: Vec<f32>,
}
#[cfg(feature = "extract")]
pub(crate) fn parse_chat_response(payload: &str) -> Result<String, String> {
let value = parse_json(payload)?;
value
.get("choices")
.and_then(|choices| choices.get(0))
.and_then(|first| first.get("message"))
.and_then(|message| message.get("content"))
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| {
format!(
"response has no `choices[0].message.content` string: {}",
preview(payload)
)
})
}
fn parse_json(payload: &str) -> Result<Value, String> {
let value: Value = serde_json::from_str(payload)
.map_err(|err| format!("response is not JSON ({err}): {}", preview(payload)))?;
if let Some(message) = value
.get("error")
.and_then(|error| error.get("message"))
.and_then(Value::as_str)
{
return Err(format!("the server refused the request: {message}"));
}
Ok(value)
}
fn preview(payload: &str) -> String {
let cut = payload
.char_indices()
.map(|(at, _)| at)
.take_while(|at| *at <= 200)
.last()
.unwrap_or(0);
if cut < payload.len() {
format!("{}…", &payload[..cut])
} else {
payload.to_owned()
}
}
#[cfg(test)]
#[path = "openai_tests.rs"]
mod tests;