use super::transport::{
BatchEndpoint, BatchItem, BatchTransport, EmbeddingResponseBody, SubmitBatch, TransportError,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize)]
pub struct Message {
pub role: String,
pub content: String,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: "system".to_string(),
content: content.into(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: "user".to_string(),
content: content.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct StructuredParams {
pub schema: serde_json::Value,
pub schema_name: String,
pub max_tokens: u32,
pub disable_reasoning: bool,
pub require_parameters: bool,
pub reasoning_max_tokens: Option<u32>,
}
pub fn structured_body(
params: &StructuredParams,
messages: Vec<Message>,
) -> Result<Value, serde_json::Error> {
let mut schema = serde_json::Map::new();
schema.insert("name".to_string(), params.schema_name.clone().into());
schema.insert("strict".to_string(), true.into());
schema.insert("schema".to_string(), params.schema.clone());
let mut response_format = serde_json::Map::new();
response_format.insert("type".to_string(), "json_schema".into());
response_format.insert("json_schema".to_string(), Value::Object(schema));
let mut body = serde_json::Map::new();
body.insert("messages".to_string(), serde_json::to_value(messages)?);
body.insert("max_tokens".to_string(), params.max_tokens.into());
body.insert(
"response_format".to_string(),
Value::Object(response_format),
);
if params.disable_reasoning {
body.insert(
"reasoning".to_string(),
serde_json::json!({"enabled": false}),
);
}
if params.require_parameters {
body.insert(
"provider".to_string(),
serde_json::json!({"require_parameters": true}),
);
}
if let Some(cap) = params.reasoning_max_tokens {
body.insert(
"reasoning".to_string(),
serde_json::json!({"max_tokens": cap}),
);
}
Ok(Value::Object(body))
}
pub async fn submit_structured(
transport: &dyn BatchTransport,
model: &str,
idempotency_key: &str,
params: &StructuredParams,
items: Vec<(String, Vec<Message>)>,
) -> Result<super::transport::BatchObject, TransportError> {
for (custom_id, _) in &items {
if custom_id.len() > 64 {
return Err(TransportError::CustomIdTooLong {
custom_id: custom_id.clone(),
});
}
}
let mut schema = serde_json::Map::new();
schema.insert("name".to_string(), params.schema_name.clone().into());
schema.insert("strict".to_string(), true.into());
schema.insert("schema".to_string(), params.schema.clone());
let mut response_format = serde_json::Map::new();
response_format.insert("type".to_string(), "json_schema".into());
response_format.insert("json_schema".to_string(), Value::Object(schema));
let requests = items
.into_iter()
.map(|(custom_id, messages)| {
Ok(BatchItem {
custom_id,
body: structured_body(params, messages)?,
})
})
.collect::<Result<Vec<_>, serde_json::Error>>()?;
transport
.submit(
&SubmitBatch::new(BatchEndpoint::ChatCompletions, model, requests),
idempotency_key,
)
.await
}
#[derive(Debug, thiserror::Error)]
pub enum EmbeddingDecodeError {
#[error("embedding response decode: {source}")]
Envelope {
#[from]
source: serde_json::Error,
},
#[error("embedding response count: expected {expected}, received {actual}")]
CountMismatch { expected: usize, actual: usize },
#[error("embedding response index {0} is negative")]
NegativeIndex(i64),
#[error("embedding response index {0} exceeds input count {1}")]
IndexOutOfRange(usize, usize),
#[error("embedding response has duplicate index {0}")]
DuplicateIndex(usize),
#[error("embedding response has no index {0}")]
MissingIndex(usize),
}
pub fn embedding_vectors(
raw: &serde_json::Value,
expected: usize,
) -> Result<Vec<Vec<f32>>, EmbeddingDecodeError> {
let response = EmbeddingResponseBody::deserialize(raw)?;
if response.data.len() != expected {
return Err(EmbeddingDecodeError::CountMismatch {
expected,
actual: response.data.len(),
});
}
let mut indexed: Vec<Option<Vec<f32>>> = vec![None; expected];
for data in response.data {
let index = usize::try_from(data.index)
.map_err(|_| EmbeddingDecodeError::NegativeIndex(data.index))?;
if index >= expected {
return Err(EmbeddingDecodeError::IndexOutOfRange(index, expected));
}
if indexed[index].replace(data.embedding).is_some() {
return Err(EmbeddingDecodeError::DuplicateIndex(index));
}
}
indexed
.into_iter()
.enumerate()
.map(|(index, vector)| vector.ok_or(EmbeddingDecodeError::MissingIndex(index)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn structured_body_caps_reasoning_tokens_only_when_set() {
let base = StructuredParams {
schema: serde_json::json!({"type": "object"}),
schema_name: "reply".to_string(),
max_tokens: 100,
disable_reasoning: false,
require_parameters: false,
reasoning_max_tokens: Some(1024),
};
let body = structured_body(&base, vec![Message::user("hi")]).unwrap();
assert_eq!(
body.get("reasoning"),
Some(&serde_json::json!({"max_tokens": 1024}))
);
let off = StructuredParams {
reasoning_max_tokens: None,
..base
};
let body = structured_body(&off, vec![Message::user("hi")]).unwrap();
assert!(body.get("reasoning").is_none());
}
#[test]
fn structured_body_requires_provider_parameters_only_when_opted_in() {
let base = StructuredParams {
schema: serde_json::json!({"type": "object"}),
schema_name: "reply".to_string(),
max_tokens: 100,
disable_reasoning: false,
require_parameters: true,
reasoning_max_tokens: None,
};
let body = structured_body(&base, vec![Message::user("hi")]).unwrap();
assert_eq!(
body.get("provider"),
Some(&serde_json::json!({"require_parameters": true}))
);
let off = StructuredParams {
require_parameters: false,
..base
};
let body = structured_body(&off, vec![Message::user("hi")]).unwrap();
assert!(body.get("provider").is_none());
}
#[test]
fn vectors_are_reordered_to_match_input_indexes() {
let raw = serde_json::json!({
"id": "embed-request",
"data": [
{"index": 1, "embedding": [2.0]},
{"index": 0, "embedding": [1.0]},
]
});
let vectors = embedding_vectors(&raw, 2).unwrap();
assert_eq!(vectors, vec![vec![1.0], vec![2.0]]);
}
#[test]
fn vector_decode_rejects_count_and_index_defects() {
let short = serde_json::json!({"data": [{"index": 0, "embedding": [1.0]}]});
assert!(matches!(
embedding_vectors(&short, 2),
Err(EmbeddingDecodeError::CountMismatch {
expected: 2,
actual: 1
})
));
let duplicate = serde_json::json!({
"data": [
{"index": 0, "embedding": [1.0]},
{"index": 0, "embedding": [2.0]},
]
});
assert!(matches!(
embedding_vectors(&duplicate, 2),
Err(EmbeddingDecodeError::DuplicateIndex(0))
));
}
}