1use crate::{
4 Decimal,
5 definition::{BackendReq, ItemSource, Literal},
6 ids::{ArtifactId, DescriptorId, PromptTemplateId},
7};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
12#[serde(deny_unknown_fields)]
13pub struct Generate {
14 pub when: GenerateWhen,
15 pub for_each: ModelForEach,
16 pub template: TemplateRef,
17 pub language: Language,
18 pub projection: Vec<String>,
23 #[serde(deserialize_with = "carry")]
24 pub carry: Vec<String>,
25 pub vocabularies: BTreeMap<String, Vocabulary>,
26 pub output: OutputSchema,
27 pub backstop: Backstop,
28 pub repair: Repair,
29 pub screen: GenerateScreen,
30 pub limits: ModelLimits,
31 pub backend: BackendReq,
32}
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
34#[serde(rename_all = "snake_case", deny_unknown_fields)]
35pub enum GenerateWhen {
36 AnyPropose,
37 Actions(Vec<String>),
38}
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
40#[serde(rename_all = "snake_case", deny_unknown_fields)]
41pub enum ModelForEach {
42 None,
43 #[serde(untagged)]
44 List(#[serde(deserialize_with = "object")] ModelList),
45}
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct ModelList {
49 pub over: String,
50 pub bind: String,
51 pub items: ItemSource,
52 pub max_items: ModelBound<256>,
53}
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
55#[serde(rename_all = "snake_case", deny_unknown_fields)]
56pub enum ModelBound<const MAX: u64> {
57 Literal(#[serde(deserialize_with = "bound::<_, MAX>")] u64),
58 Param(String),
59}
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
61#[serde(untagged, deny_unknown_fields)]
62pub enum Language {
63 Literal(String),
64 Parameter(#[serde(deserialize_with = "object")] crate::parameter::ParamRef),
65}
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
67#[serde(untagged, deny_unknown_fields)]
68pub enum Vocabulary {
69 Literal(#[serde(deserialize_with = "options")] Vec<String>),
70 Parameter(#[serde(deserialize_with = "object")] crate::parameter::ParamRef),
71}
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
73#[serde(deny_unknown_fields)]
74pub struct TemplateRef {
75 #[serde(deserialize_with = "template_name")]
76 pub id: String,
77 pub digest: PromptTemplateId,
78 pub params: BTreeMap<String, TemplateBinding>,
79}
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
81#[serde(untagged, deny_unknown_fields)]
82pub enum TemplateBinding {
83 Literal { literal: Literal },
84 Parameter(#[serde(deserialize_with = "object")] crate::parameter::ParamRef),
85 Reference(String),
86}
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
88#[serde(deny_unknown_fields)]
89pub struct OutputSchema {
90 #[serde(deserialize_with = "fields")]
91 pub fields: Vec<GeneratedField>,
92}
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
94#[serde(deny_unknown_fields)]
95pub struct GeneratedField {
96 pub name: String,
97 #[serde(rename = "type")]
98 pub ty: GeneratedType,
99 pub required: bool,
100}
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
102#[serde(rename_all = "snake_case", deny_unknown_fields)]
103pub enum GeneratedType {
104 Text {
105 #[serde(deserialize_with = "bound::<_, 16384>")]
106 max_bytes: u64,
107 lines: Lines,
108 },
109 List {
110 vocabulary: String,
111 #[serde(deserialize_with = "bound::<_, 64>")]
112 max_items: u64,
113 },
114}
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
116#[serde(rename_all = "snake_case", deny_unknown_fields)]
117pub enum Lines {
118 Single,
119 Multi,
120}
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
122#[serde(deny_unknown_fields)]
123pub struct Backstop {
124 pub locale: String,
125 pub escape: Escape,
126 pub overflow: Overflow,
127 pub fields: BTreeMap<String, BackstopField>,
128}
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
130#[serde(rename_all = "snake_case", deny_unknown_fields)]
131pub enum Escape {
132 Text,
133 Html,
134}
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
136#[serde(rename_all = "snake_case", deny_unknown_fields)]
137pub enum Overflow {
138 Refuse,
139 Truncate,
140}
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
142#[serde(rename_all = "snake_case", deny_unknown_fields)]
143pub enum BackstopField {
144 Text(String),
145 List(Vec<String>),
146}
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
148#[serde(deny_unknown_fields)]
149pub struct Repair {
150 #[serde(deserialize_with = "repair_attempts")]
151 pub max_attempts: u64,
152 pub only_missing_fields: bool,
153}
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct GenerateScreen {
157 pub input: ScreenInput,
158 pub output: ScreenOutput,
159}
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
161#[serde(rename_all = "snake_case", deny_unknown_fields)]
162pub enum ScreenInput {
163 UntrustedFields,
164}
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
166#[serde(rename_all = "snake_case", deny_unknown_fields)]
167pub enum ScreenOutput {
168 None,
169 Screener { backend: BackendReq },
170}
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
172#[serde(deny_unknown_fields)]
173pub struct ModelLimits {
174 #[serde(deserialize_with = "deadline_share")]
175 pub deadline_share: Decimal,
176 pub per_item_deadline_ms: ModelBound<600000>,
177 #[serde(deserialize_with = "bound::<_, 64>")]
178 pub concurrency: u64,
179 pub max_cost_units: CostLimit,
180}
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
182#[serde(rename_all = "snake_case", deny_unknown_fields)]
183pub enum CostLimit {
184 None,
185 Units {
186 #[serde(deserialize_with = "bound::<_, {u64::MAX}>")]
187 n: u64,
188 },
189}
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
191#[serde(deny_unknown_fields)]
192pub struct PromptTemplate {
193 pub schema: String,
194 #[serde(deserialize_with = "template_name")]
195 pub id: String,
196 #[serde(deserialize_with = "version")]
197 pub version: String,
198 #[serde(deserialize_with = "instructions")]
199 pub instructions: String,
200 #[serde(deserialize_with = "params")]
201 pub params: Vec<PromptParam>,
202}
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
204#[serde(deny_unknown_fields)]
205pub struct PromptParam {
206 pub name: String,
207 #[serde(rename = "type")]
208 pub ty: PromptParamType,
209}
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
211#[serde(rename_all = "snake_case", deny_unknown_fields)]
212pub enum PromptParamType {
213 Bool,
214 Integer,
215 Decimal,
216 Text {
217 #[serde(deserialize_with = "bound::<_, 1024>")]
218 max_bytes: u64,
219 },
220 Enum {
221 #[serde(deserialize_with = "options")]
222 options: Vec<String>,
223 },
224}
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
226#[serde(deny_unknown_fields)]
227pub struct GenerateBinding {
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub output_screener: Option<crate::screening::ScreenerBinding>,
230 pub template_id: PromptTemplateId,
231 pub template: PromptTemplate,
232 pub helpers_version: String,
233 pub backend_id: String,
234 pub artifact: ArtifactId,
235 pub descriptor: DescriptorId,
236 pub output: OutputSchema,
237 pub vocabularies: BTreeMap<String, Vec<String>>,
238 pub max_items: u64,
239 pub worst_case_requests: u64,
240 pub worst_case_projection_bytes: u64,
241}
242
243crate::document::document!(
244 PromptTemplate,
245 crate::schema::PROMPT_TEMPLATE,
246 crate::limits::DEFINITION_V1,
247 PromptTemplateId
248);
249
250pub(crate) fn bound<'de, D: Deserializer<'de>, const MAX: u64>(d: D) -> Result<u64, D::Error> {
251 let n = u64::deserialize(d)?;
252 if !(1..=MAX).contains(&n) {
253 return Err(serde::de::Error::custom("generation bound out of range"));
254 }
255 Ok(n)
256}
257fn repair_attempts<'de, D: Deserializer<'de>>(d: D) -> Result<u64, D::Error> {
258 let n = u64::deserialize(d)?;
259 if n > 2 {
260 return Err(serde::de::Error::custom("repair.max_attempts exceeds 2"));
261 }
262 Ok(n)
263}
264pub(crate) fn deadline_share<'de, D: Deserializer<'de>>(d: D) -> Result<Decimal, D::Error> {
265 let text = String::deserialize(d)?;
266 let n: Decimal = Decimal::parse(&text).map_err(serde::de::Error::custom)?;
267 if n <= Decimal::ZERO
268 || n > Decimal::from_i64(1)
269 || text.split_once('.').is_some_and(|(_, f)| f.len() > 4)
270 {
271 return Err(serde::de::Error::custom(
272 "deadline_share outside (0,1] or over four fractional digits",
273 ));
274 }
275 Ok(n)
276}
277fn carry<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
278 let v = Vec::deserialize(d)?;
279 if v.len() > 16 {
280 return Err(serde::de::Error::custom("carry exceeds 16 references"));
281 }
282 Ok(v)
283}
284fn options<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
285 let v = Vec::<String>::deserialize(d)?;
286 if !(1..=256).contains(&v.len()) || v.iter().any(|x| !(1..=64).contains(&x.len())) {
287 return Err(serde::de::Error::custom(
288 "vocabulary outside generation bounds",
289 ));
290 }
291 Ok(v)
292}
293fn fields<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<GeneratedField>, D::Error> {
294 let v = Vec::<GeneratedField>::deserialize(d)?;
295 let mut names = BTreeSet::new();
296 if !(1..=32).contains(&v.len())
297 || v.iter()
298 .any(|f| !crate::parameter::is_parameter_name(&f.name) || !names.insert(&f.name))
299 {
300 return Err(serde::de::Error::custom(
301 "invalid or duplicate generated field name or field count",
302 ));
303 }
304 Ok(v)
305}
306fn template_name<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
307 let s = String::deserialize(d)?;
308 if !(1..=128).contains(&s.len())
309 || !s
310 .bytes()
311 .next()
312 .is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
313 || !s
314 .bytes()
315 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"._-".contains(&b))
316 {
317 return Err(serde::de::Error::custom("invalid prompt template id"));
318 }
319 Ok(s)
320}
321fn version<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
322 let s = String::deserialize(d)?;
323 if s.split('.').count() != 3
324 || s.split('.').any(|p| {
325 p.is_empty()
326 || (p.len() > 1 && p.starts_with('0'))
327 || !p.bytes().all(|b| b.is_ascii_digit())
328 })
329 {
330 return Err(serde::de::Error::custom("invalid prompt template version"));
331 }
332 Ok(s)
333}
334fn instructions<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
335 let s = String::deserialize(d)?;
336 if s.len() > 16384 {
338 return Err(serde::de::Error::custom(
339 "prompt instructions exceed 16 KiB",
340 ));
341 }
342 Ok(s)
343}
344fn params<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<PromptParam>, D::Error> {
345 let v = Vec::<PromptParam>::deserialize(d)?;
346 let mut names = BTreeSet::new();
347 if v.len() > 32
348 || v.iter()
349 .any(|p| !crate::parameter::is_parameter_name(&p.name) || !names.insert(&p.name))
350 {
351 return Err(serde::de::Error::custom("invalid prompt parameters"));
352 }
353 Ok(v)
354}
355
356pub(crate) fn steps<'de, D: Deserializer<'de>>(
357 d: D,
358) -> Result<Vec<crate::definition::StepDecl>, D::Error> {
359 let v = Vec::<crate::definition::StepDecl>::deserialize(d)?;
360 if v.iter()
361 .filter(|s| matches!(s.body, crate::definition::StepBody::Generate(_)))
362 .count()
363 > 8
364 {
365 return Err(serde::de::Error::custom("more than eight generate steps"));
366 }
367 if v.iter()
368 .filter(|s| matches!(s.body, crate::definition::StepBody::Extract(_)))
369 .count()
370 > 8
371 {
372 return Err(serde::de::Error::custom("more than eight extract steps"));
373 }
374 Ok(v)
375}
376
377pub(crate) fn object<'de, D: Deserializer<'de>, T: serde::de::DeserializeOwned>(
379 d: D,
380) -> Result<T, D::Error> {
381 let v = serde_json::Value::deserialize(d)?;
382 if !v.is_object() {
383 return Err(serde::de::Error::custom("expected an object"));
384 }
385 serde_json::from_value(v).map_err(serde::de::Error::custom)
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
390#[serde(deny_unknown_fields)]
391pub struct GenerateRequest {
392 pub schema: String,
393 pub step: String,
394 pub instance: Vec<String>,
395 pub template: String,
396 pub template_id: PromptTemplateId,
397 pub instructions: String,
398 pub language: String,
399 pub projection: BTreeMap<String, serde_json::Value>,
400 pub output: OutputSchema,
401 pub vocabularies: BTreeMap<String, Vec<String>>,
402 pub repair: RepairRequest,
403}
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
405#[serde(rename_all = "snake_case", deny_unknown_fields)]
406pub enum RepairRequest {
407 None,
408 #[serde(untagged)]
414 Round {
415 round: u64,
416 fields: Vec<RepairField>,
417 },
418}
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
420#[serde(deny_unknown_fields)]
421pub struct RepairField {
422 pub name: String,
423 pub reason: FieldInvalid,
424}
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
426#[serde(rename_all = "snake_case")]
427pub enum FieldInvalid {
428 Missing,
429 WrongType,
430 Empty,
431 TooLong,
432 ControlCharacter,
433 NotInVocabulary,
434 DuplicateItem,
435 TooManyItems,
436}
437crate::document::document!(
438 GenerateRequest,
439 crate::schema::GENERATE_REQUEST,
440 crate::limits::RECORD_V1
441);
442
443pub fn output_screen_step(step: &str, field: &str) -> String {
445 format!("screen-output:{step}:{field}")
446}
447
448pub fn output_screen_binding<'a>(
450 plan: &'a crate::plan::Plan,
451 step: &str,
452) -> Option<(&'a str, &'a crate::screening::ScreenerBinding)> {
453 plan.steps.iter().find_map(|s| {
454 let crate::plan::PlanStepDetail::Generate(g) = &s.detail else {
455 return None;
456 };
457 let binding = g.output_screener.as_ref()?;
458 g.output
459 .fields
460 .iter()
461 .any(|f| {
462 matches!(f.ty, GeneratedType::Text { .. })
463 && output_screen_step(&s.id, &f.name) == step
464 })
465 .then_some((s.id.as_str(), binding))
466 })
467}
468
469#[cfg(test)]
470mod tests {
471 use super::RepairRequest;
472 use serde_json::json;
473
474 #[test]
475 fn repair_round_rejects_unknown_fields() {
476 let round = json!({"round": 1, "fields": []});
477 let parsed: RepairRequest = serde_json::from_value(round.clone()).unwrap();
478 assert!(matches!(parsed, RepairRequest::Round { round: 1, .. }));
479 assert_eq!(serde_json::to_value(parsed).unwrap(), round);
480
481 let mut extra = round;
482 extra["unexpected"] = json!(true);
483 assert!(serde_json::from_str::<RepairRequest>(&extra.to_string()).is_err());
484 assert!(serde_json::from_value::<RepairRequest>(extra).is_err());
485 }
486}