1use super::transport::{
6 BatchEndpoint, BatchItem, BatchTransport, EmbeddingResponseBody, SubmitBatch, TransportError,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Serialize)]
13pub struct Message {
14 pub role: String,
15 pub content: String,
16}
17
18impl Message {
19 pub fn system(content: impl Into<String>) -> Self {
20 Self {
21 role: "system".to_string(),
22 content: content.into(),
23 }
24 }
25
26 pub fn user(content: impl Into<String>) -> Self {
27 Self {
28 role: "user".to_string(),
29 content: content.into(),
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
38pub struct StructuredParams {
39 pub schema: serde_json::Value,
41 pub schema_name: String,
43 pub max_tokens: u32,
44 pub disable_reasoning: bool,
48 pub require_parameters: bool,
53 pub reasoning_max_tokens: Option<u32>,
58}
59
60pub fn structured_body(
64 params: &StructuredParams,
65 messages: Vec<Message>,
66) -> Result<Value, serde_json::Error> {
67 let mut schema = serde_json::Map::new();
68 schema.insert("name".to_string(), params.schema_name.clone().into());
69 schema.insert("strict".to_string(), true.into());
70 schema.insert("schema".to_string(), params.schema.clone());
71
72 let mut response_format = serde_json::Map::new();
73 response_format.insert("type".to_string(), "json_schema".into());
74 response_format.insert("json_schema".to_string(), Value::Object(schema));
75
76 let mut body = serde_json::Map::new();
77 body.insert("messages".to_string(), serde_json::to_value(messages)?);
78 body.insert("max_tokens".to_string(), params.max_tokens.into());
79 body.insert(
80 "response_format".to_string(),
81 Value::Object(response_format),
82 );
83 if params.disable_reasoning {
84 body.insert(
85 "reasoning".to_string(),
86 serde_json::json!({"enabled": false}),
87 );
88 }
89 if params.require_parameters {
90 body.insert(
91 "provider".to_string(),
92 serde_json::json!({"require_parameters": true}),
93 );
94 }
95 if let Some(cap) = params.reasoning_max_tokens {
96 body.insert(
97 "reasoning".to_string(),
98 serde_json::json!({"max_tokens": cap}),
99 );
100 }
101 Ok(Value::Object(body))
102}
103
104pub async fn submit_structured(
111 transport: &dyn BatchTransport,
112 model: &str,
113 idempotency_key: &str,
114 params: &StructuredParams,
115 items: Vec<(String, Vec<Message>)>,
116) -> Result<super::transport::BatchObject, TransportError> {
117 for (custom_id, _) in &items {
121 if custom_id.len() > 64 {
122 return Err(TransportError::CustomIdTooLong {
123 custom_id: custom_id.clone(),
124 });
125 }
126 }
127
128 let mut schema = serde_json::Map::new();
129 schema.insert("name".to_string(), params.schema_name.clone().into());
130 schema.insert("strict".to_string(), true.into());
131 schema.insert("schema".to_string(), params.schema.clone());
132
133 let mut response_format = serde_json::Map::new();
134 response_format.insert("type".to_string(), "json_schema".into());
135 response_format.insert("json_schema".to_string(), Value::Object(schema));
136
137 let requests = items
138 .into_iter()
139 .map(|(custom_id, messages)| {
140 Ok(BatchItem {
141 custom_id,
142 body: structured_body(params, messages)?,
143 })
144 })
145 .collect::<Result<Vec<_>, serde_json::Error>>()?;
146
147 transport
148 .submit(
149 &SubmitBatch::new(BatchEndpoint::ChatCompletions, model, requests),
150 idempotency_key,
151 )
152 .await
153}
154
155#[derive(Debug, thiserror::Error)]
158pub enum EmbeddingDecodeError {
159 #[error("embedding response decode: {source}")]
160 Envelope {
161 #[from]
162 source: serde_json::Error,
163 },
164 #[error("embedding response count: expected {expected}, received {actual}")]
165 CountMismatch { expected: usize, actual: usize },
166 #[error("embedding response index {0} is negative")]
167 NegativeIndex(i64),
168 #[error("embedding response index {0} exceeds input count {1}")]
169 IndexOutOfRange(usize, usize),
170 #[error("embedding response has duplicate index {0}")]
171 DuplicateIndex(usize),
172 #[error("embedding response has no index {0}")]
173 MissingIndex(usize),
174}
175
176pub fn embedding_vectors(
183 raw: &serde_json::Value,
184 expected: usize,
185) -> Result<Vec<Vec<f32>>, EmbeddingDecodeError> {
186 let response = EmbeddingResponseBody::deserialize(raw)?;
187 if response.data.len() != expected {
188 return Err(EmbeddingDecodeError::CountMismatch {
189 expected,
190 actual: response.data.len(),
191 });
192 }
193 let mut indexed: Vec<Option<Vec<f32>>> = vec![None; expected];
194 for data in response.data {
195 let index = usize::try_from(data.index)
196 .map_err(|_| EmbeddingDecodeError::NegativeIndex(data.index))?;
197 if index >= expected {
198 return Err(EmbeddingDecodeError::IndexOutOfRange(index, expected));
199 }
200 if indexed[index].replace(data.embedding).is_some() {
201 return Err(EmbeddingDecodeError::DuplicateIndex(index));
202 }
203 }
204 indexed
205 .into_iter()
206 .enumerate()
207 .map(|(index, vector)| vector.ok_or(EmbeddingDecodeError::MissingIndex(index)))
208 .collect()
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn structured_body_caps_reasoning_tokens_only_when_set() {
217 let base = StructuredParams {
218 schema: serde_json::json!({"type": "object"}),
219 schema_name: "reply".to_string(),
220 max_tokens: 100,
221 disable_reasoning: false,
222 require_parameters: false,
223 reasoning_max_tokens: Some(1024),
224 };
225 let body = structured_body(&base, vec![Message::user("hi")]).unwrap();
226 assert_eq!(
227 body.get("reasoning"),
228 Some(&serde_json::json!({"max_tokens": 1024}))
229 );
230
231 let off = StructuredParams {
232 reasoning_max_tokens: None,
233 ..base
234 };
235 let body = structured_body(&off, vec![Message::user("hi")]).unwrap();
236 assert!(body.get("reasoning").is_none());
237 }
238
239 #[test]
240 fn structured_body_requires_provider_parameters_only_when_opted_in() {
241 let base = StructuredParams {
242 schema: serde_json::json!({"type": "object"}),
243 schema_name: "reply".to_string(),
244 max_tokens: 100,
245 disable_reasoning: false,
246 require_parameters: true,
247 reasoning_max_tokens: None,
248 };
249 let body = structured_body(&base, vec![Message::user("hi")]).unwrap();
250 assert_eq!(
251 body.get("provider"),
252 Some(&serde_json::json!({"require_parameters": true}))
253 );
254
255 let off = StructuredParams {
256 require_parameters: false,
257 ..base
258 };
259 let body = structured_body(&off, vec![Message::user("hi")]).unwrap();
260 assert!(body.get("provider").is_none());
261 }
262
263 #[test]
264 fn vectors_are_reordered_to_match_input_indexes() {
265 let raw = serde_json::json!({
266 "id": "embed-request",
267 "data": [
268 {"index": 1, "embedding": [2.0]},
269 {"index": 0, "embedding": [1.0]},
270 ]
271 });
272 let vectors = embedding_vectors(&raw, 2).unwrap();
273 assert_eq!(vectors, vec![vec![1.0], vec![2.0]]);
274 }
275
276 #[test]
277 fn vector_decode_rejects_count_and_index_defects() {
278 let short = serde_json::json!({"data": [{"index": 0, "embedding": [1.0]}]});
279 assert!(matches!(
280 embedding_vectors(&short, 2),
281 Err(EmbeddingDecodeError::CountMismatch {
282 expected: 2,
283 actual: 1
284 })
285 ));
286 let duplicate = serde_json::json!({
287 "data": [
288 {"index": 0, "embedding": [1.0]},
289 {"index": 0, "embedding": [2.0]},
290 ]
291 });
292 assert!(matches!(
293 embedding_vectors(&duplicate, 2),
294 Err(EmbeddingDecodeError::DuplicateIndex(0))
295 ));
296 }
297}