Generate a structured JSON object using the given LLM client. Selects the best mode based on `req.mode`, calls the LLM, validates against the schema, and retries with repair prompts if validation fails.
(
client: &dyn LlmClient,
req: &StructuredRequest,
)
| 110 | /// Selects the best mode based on `req.mode`, calls the LLM, validates against |
| 111 | /// the schema, and retries with repair prompts if validation fails. |
| 112 | pub async fn generate_blocking( |
| 113 | client: &dyn LlmClient, |
| 114 | req: &StructuredRequest, |
| 115 | ) -> Result<StructuredResult> { |
| 116 | let mode = resolve_mode(req.mode, client.native_structured_support()); |
| 117 | let mut messages = build_initial_messages(req, mode); |
| 118 | let system = build_system_prompt(req, mode); |
| 119 | let tools = build_tools(req, mode); |
| 120 | let directive = build_directive(req, mode); |
| 121 | |
| 122 | let mut total_usage = TokenUsage::default(); |
| 123 | let mut repair_rounds: u8 = 0; |
| 124 | |
| 125 | loop { |
| 126 | let resp = client |
| 127 | .complete_structured(&messages, Some(&system), &tools, &directive) |
| 128 | .await |
| 129 | .context("LLM call failed during structured generation")?; |
| 130 | |
| 131 | accumulate_usage(&mut total_usage, &resp.usage); |
| 132 | |
| 133 | // Mine the object from every place a model might have parked it (tool call, |
| 134 | // text content, AND the reasoning channel), trying each balanced JSON |
| 135 | // candidate against the schema. Reasoning models routinely leave `content` |
| 136 | // empty and emit the object inside `reasoning`, so without the reasoning |
| 137 | // fallback generate_object failed with "no structured output" across models. |
| 138 | let candidates = extract_raw_candidates(&resp.message, mode); |
| 139 | let resolution = resolve_structured(&candidates, &req.schema); |
| 140 | |
| 141 | if let Some((value, raw)) = resolution.valid { |
| 142 | return Ok(StructuredResult { |
| 143 | object: value, |
| 144 | raw_text: Some(raw), |
| 145 | usage: total_usage, |
| 146 | repair_rounds, |
| 147 | mode_used: mode, |
| 148 | }); |
| 149 | } |
| 150 | |
| 151 | if repair_rounds >= req.max_repair_attempts { |
| 152 | return Err(match resolution.invalid { |
| 153 | Some((_, errors)) => anyhow::anyhow!( |
| 154 | "Structured output failed schema validation after {} repair attempts. Errors: {}", |
| 155 | repair_rounds, |
| 156 | errors.join("; ") |
| 157 | ), |
| 158 | None => anyhow::anyhow!( |
| 159 | "Structured output parsing failed after {} repair attempts: no JSON object found in tool call, text content, or reasoning channel", |
| 160 | repair_rounds |
| 161 | ), |
| 162 | }); |
| 163 | } |
| 164 | |
| 165 | repair_rounds += 1; |
| 166 | let (repair_msg, raw_for_ctx) = match resolution.invalid { |
| 167 | Some((raw, errors)) => (build_repair_message(&raw, &errors), raw), |
| 168 | None => { |
| 169 | let raw = resolution.raw_seen.unwrap_or_default(); |