1use std::num::NonZeroUsize;
2use std::sync::Arc;
3
4use crate::{AttachmentRef, SchemaContract};
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum LlmTerminalReason {
9 Stop,
10 ToolUse,
11 OutputLimit,
12 ContextOverflow,
13 ContentFilter,
14 ProviderError,
15 Cancelled,
16 #[default]
17 Unknown,
18}
19
20impl LlmTerminalReason {
21 pub fn code(self) -> &'static str {
22 match self {
23 Self::Stop => "stop",
24 Self::ToolUse => "tool_use",
25 Self::OutputLimit => "output_limit",
26 Self::ContextOverflow => "context_overflow",
27 Self::ContentFilter => "content_filter",
28 Self::ProviderError => "provider_error",
29 Self::Cancelled => "cancelled",
30 Self::Unknown => "unknown",
31 }
32 }
33}
34
35#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
36pub struct ResponseTextMeta {
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub id: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub status: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub phase: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub provider_payload: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub origin_provider: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub origin_model: Option<String>,
55}
56
57impl ResponseTextMeta {
58 pub fn phase_is(&self, expected: &str) -> bool {
59 self.phase
60 .as_deref()
61 .is_some_and(|phase| phase.eq_ignore_ascii_case(expected))
62 }
63
64 pub fn is_final_answer_phase(&self) -> bool {
65 self.phase_is("final_answer")
66 }
67
68 pub fn is_commentary_phase(&self) -> bool {
69 self.phase_is("commentary")
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
74pub struct LlmToolSpec {
75 pub name: String,
76 pub description: String,
77 pub input_schema: SchemaContract,
78 pub output_schema: SchemaContract,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
82pub enum LlmToolChoice {
83 #[default]
84 Auto,
85 None,
86 Required,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
90pub struct ProviderReplayMeta {
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub item_id: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub opaque: Option<String>,
95}
96
97impl ProviderReplayMeta {
98 pub fn is_empty(&self) -> bool {
99 self.item_id.is_none() && self.opaque.is_none()
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
104pub struct ProviderReasoningReplay {
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub item_id: Option<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub encrypted_content: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub signature: Option<String>,
111 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112 pub redacted: bool,
113 #[serde(default, skip_serializing_if = "Vec::is_empty")]
114 pub summary: Vec<String>,
115}
116
117impl ProviderReasoningReplay {
118 pub fn is_empty(&self) -> bool {
119 self.item_id.is_none()
120 && self.encrypted_content.is_none()
121 && self.signature.is_none()
122 && !self.redacted
123 && self.summary.is_empty()
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
128pub enum LlmOutputPart {
129 Text {
130 text: String,
131 response_meta: Option<ResponseTextMeta>,
132 },
133 Reasoning {
140 text: String,
141 replay: Option<ProviderReasoningReplay>,
142 },
143 ToolCall {
144 call_id: String,
145 tool_name: String,
146 input_json: String,
147 replay: Option<ProviderReplayMeta>,
150 },
151}
152
153#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
154pub enum LlmRole {
155 User,
156 Assistant,
157 System,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164pub enum LlmContentBlock {
165 Text {
166 text: Arc<str>,
167 response_meta: Option<ResponseTextMeta>,
168 cache_breakpoint: bool,
169 },
170 Image { attachment_idx: usize },
174 ToolCall {
176 call_id: String,
177 tool_name: String,
178 input_json: String,
179 replay: Option<ProviderReplayMeta>,
180 },
181 ToolResult {
184 call_id: String,
185 content: String,
186 tool_name: Option<String>,
189 },
190 Reasoning {
194 text: String,
195 replay: Option<ProviderReasoningReplay>,
196 },
197}
198
199#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
204pub struct LlmMessage {
205 pub role: LlmRole,
206 pub blocks: Arc<Vec<LlmContentBlock>>,
207}
208
209impl LlmMessage {
210 pub fn new(role: LlmRole, blocks: Vec<LlmContentBlock>) -> Self {
211 Self {
212 role,
213 blocks: Arc::new(blocks),
214 }
215 }
216
217 pub fn text(role: LlmRole, text: impl Into<Arc<str>>) -> Self {
219 Self {
220 role,
221 blocks: Arc::new(vec![LlmContentBlock::Text {
222 text: text.into(),
223 response_meta: None,
224 cache_breakpoint: false,
225 }]),
226 }
227 }
228
229 pub fn is_blank(&self) -> bool {
231 self.blocks.iter().all(|b| match b {
232 LlmContentBlock::Text { text, .. } => text.trim().is_empty(),
233 _ => false,
234 })
235 }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
239pub struct LlmRequestScope {
240 pub session_id: String,
242 pub agent_frame_id: String,
246 pub request_id: String,
248}
249
250impl LlmRequestScope {
251 pub fn new(
252 session_id: impl Into<String>,
253 agent_frame_id: impl Into<String>,
254 request_id: impl Into<String>,
255 ) -> Self {
256 Self {
257 session_id: session_id.into(),
258 agent_frame_id: agent_frame_id.into(),
259 request_id: request_id.into(),
260 }
261 }
262
263 pub fn continuation_key(&self) -> String {
264 format!("{}::{}", self.session_id, self.agent_frame_id)
265 }
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
269pub struct LlmAttachment {
270 pub mime: String,
271 pub data: Vec<u8>,
272 pub reference: Option<AttachmentRef>,
273}
274
275impl LlmAttachment {
276 pub fn bytes(mime: impl Into<String>, data: Vec<u8>) -> Self {
277 Self {
278 mime: mime.into(),
279 data,
280 reference: None,
281 }
282 }
283
284 pub fn reference(reference: AttachmentRef) -> Self {
285 Self {
286 mime: reference.canonical_mime().to_string(),
287 data: Vec::new(),
288 reference: Some(reference),
289 }
290 }
291
292 pub fn is_resolved(&self) -> bool {
293 !self.data.is_empty() || self.reference.is_none()
294 }
295}
296
297#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
298pub struct LlmJsonSchema {
299 pub name: String,
300 pub schema: SchemaContract,
301 pub strict: bool,
302}
303
304#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
305pub enum LlmOutputSpec {
306 JsonObject,
307 JsonSchema(LlmJsonSchema),
308}
309
310#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct GenerationOptions {
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub output_token_cap: Option<NonZeroUsize>,
315}
316
317impl GenerationOptions {
318 pub fn output_token_cap_u64(&self) -> Option<u64> {
319 self.output_token_cap
320 .map(NonZeroUsize::get)
321 .map(|value| value as u64)
322 }
323}
324
325#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
326pub struct LlmRequest {
327 pub model: String,
328 pub messages: Vec<LlmMessage>,
329 pub attachments: Vec<LlmAttachment>,
330 pub tools: Arc<Vec<LlmToolSpec>>,
331 pub tool_choice: LlmToolChoice,
332 pub model_variant: Option<String>,
333 #[serde(default)]
334 pub generation: GenerationOptions,
335 pub scope: LlmRequestScope,
336 pub output_spec: Option<LlmOutputSpec>,
337 #[serde(default, skip)]
338 pub stream_events: Option<LlmEventSender>,
339 #[serde(default, skip)]
340 pub provider_trace: Option<LlmProviderTraceSender>,
341}
342
343impl LlmRequest {
344 pub fn session_id(&self) -> &str {
345 self.scope.session_id.as_str()
346 }
347
348 pub fn agent_frame_id(&self) -> &str {
349 self.scope.agent_frame_id.as_str()
350 }
351
352 pub fn request_id(&self) -> &str {
353 self.scope.request_id.as_str()
354 }
355
356 pub fn continuation_key(&self) -> String {
357 self.scope.continuation_key()
358 }
359}
360
361#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
362pub struct LlmUsage {
363 pub input_tokens: i64,
364 pub output_tokens: i64,
365 pub cached_input_tokens: i64,
366 pub reasoning_tokens: i64,
367}
368
369#[derive(Clone, Debug)]
370pub enum LlmStreamEvent {
371 Delta(String),
374 ReasoningDelta(String),
378 Part(LlmOutputPart),
381 Usage(LlmUsage),
382 RetryStatus {
383 wait_seconds: u64,
384 attempt: usize,
385 max_attempts: usize,
386 reason: String,
387 },
388}
389
390#[derive(Clone)]
391pub struct LlmEventSender(Arc<dyn Fn(LlmStreamEvent) + Send + Sync>);
392
393impl LlmEventSender {
394 pub fn new<F>(send: F) -> Self
395 where
396 F: Fn(LlmStreamEvent) + Send + Sync + 'static,
397 {
398 Self(Arc::new(send))
399 }
400
401 pub fn send(&self, event: LlmStreamEvent) {
402 (self.0)(event);
403 }
404}
405
406#[derive(Clone, Debug)]
407pub struct LlmProviderTraceEvent {
408 pub provider: &'static str,
409 pub event_name: String,
410 pub raw: String,
411}
412
413#[derive(Clone)]
414pub struct LlmProviderTraceSender(Arc<dyn Fn(LlmProviderTraceEvent) + Send + Sync>);
415
416impl LlmProviderTraceSender {
417 pub fn new<F>(send: F) -> Self
418 where
419 F: Fn(LlmProviderTraceEvent) + Send + Sync + 'static,
420 {
421 Self(Arc::new(send))
422 }
423
424 pub fn send(&self, event: LlmProviderTraceEvent) {
425 (self.0)(event);
426 }
427}
428
429impl std::fmt::Debug for LlmProviderTraceSender {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 f.debug_struct("LlmProviderTraceSender")
432 .finish_non_exhaustive()
433 }
434}
435
436impl std::fmt::Debug for LlmEventSender {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 f.debug_struct("LlmEventSender").finish_non_exhaustive()
439 }
440}
441
442#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
443pub struct LlmResponse {
444 pub full_text: String,
445 pub parts: Vec<LlmOutputPart>,
446 pub usage: LlmUsage,
447 pub terminal_reason: LlmTerminalReason,
448 pub terminal_diagnostic: Option<String>,
449 pub provider_usage: Option<serde_json::Value>,
450 pub request_body: Option<String>,
451 pub http_summary: Option<String>,
452}
453
454#[derive(Clone, Debug)]
455pub struct ModelSelection {
456 pub model: &'static str,
457 pub variant: Option<&'static str>,
458}