use std::num::NonZeroUsize;
use std::sync::Arc;
use crate::{AttachmentRef, SchemaProjectionOverride};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LlmTerminalReason {
Stop,
ToolUse,
OutputLimit,
ContextOverflow,
ContentFilter,
ProviderError,
Cancelled,
#[default]
Unknown,
}
impl LlmTerminalReason {
pub fn code(self) -> &'static str {
match self {
Self::Stop => "stop",
Self::ToolUse => "tool_use",
Self::OutputLimit => "output_limit",
Self::ContextOverflow => "context_overflow",
Self::ContentFilter => "content_filter",
Self::ProviderError => "provider_error",
Self::Cancelled => "cancelled",
Self::Unknown => "unknown",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ResponseTextMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LlmToolSpec {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
pub output_schema: serde_json::Value,
pub input_schema_projections: Vec<SchemaProjectionOverride>,
pub output_schema_projections: Vec<SchemaProjectionOverride>,
}
#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum LlmToolChoice {
#[default]
Auto,
None,
Required,
}
#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct ProviderReplayMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opaque: Option<String>,
}
impl ProviderReplayMeta {
pub fn is_empty(&self) -> bool {
self.item_id.is_none() && self.opaque.is_none()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub struct ProviderReasoningReplay {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encrypted_content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub redacted: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub summary: Vec<String>,
}
impl ProviderReasoningReplay {
pub fn is_empty(&self) -> bool {
self.item_id.is_none()
&& self.encrypted_content.is_none()
&& self.signature.is_none()
&& !self.redacted
&& self.summary.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LlmOutputPart {
Text {
text: String,
response_meta: Option<ResponseTextMeta>,
},
Reasoning {
text: String,
replay: Option<ProviderReasoningReplay>,
},
ToolCall {
call_id: String,
tool_name: String,
input_json: String,
replay: Option<ProviderReplayMeta>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LlmRole {
User,
Assistant,
System,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LlmContentBlock {
Text {
text: Arc<str>,
response_meta: Option<ResponseTextMeta>,
cache_breakpoint: bool,
},
Image { attachment_idx: usize },
ToolCall {
call_id: String,
tool_name: String,
input_json: String,
replay: Option<ProviderReplayMeta>,
},
ToolResult {
call_id: String,
content: String,
tool_name: Option<String>,
},
Reasoning {
text: String,
replay: Option<ProviderReasoningReplay>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LlmMessage {
pub role: LlmRole,
pub blocks: Arc<Vec<LlmContentBlock>>,
}
impl LlmMessage {
pub fn new(role: LlmRole, blocks: Vec<LlmContentBlock>) -> Self {
Self {
role,
blocks: Arc::new(blocks),
}
}
pub fn text(role: LlmRole, text: impl Into<Arc<str>>) -> Self {
Self {
role,
blocks: Arc::new(vec![LlmContentBlock::Text {
text: text.into(),
response_meta: None,
cache_breakpoint: false,
}]),
}
}
pub fn is_blank(&self) -> bool {
self.blocks.iter().all(|b| match b {
LlmContentBlock::Text { text, .. } => text.trim().is_empty(),
_ => false,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LlmAttachment {
pub mime: String,
pub data: Vec<u8>,
pub reference: Option<AttachmentRef>,
}
impl LlmAttachment {
pub fn bytes(mime: impl Into<String>, data: Vec<u8>) -> Self {
Self {
mime: mime.into(),
data,
reference: None,
}
}
pub fn reference(reference: AttachmentRef) -> Self {
Self {
mime: reference.canonical_mime().to_string(),
data: Vec::new(),
reference: Some(reference),
}
}
pub fn is_resolved(&self) -> bool {
!self.data.is_empty() || self.reference.is_none()
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LlmJsonSchema {
pub name: String,
pub schema: serde_json::Value,
pub strict: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LlmOutputSpec {
JsonObject,
JsonSchema(LlmJsonSchema),
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GenerationOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_token_cap: Option<NonZeroUsize>,
}
impl GenerationOptions {
pub fn output_token_cap_u64(&self) -> Option<u64> {
self.output_token_cap
.map(NonZeroUsize::get)
.map(|value| value as u64)
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct LlmRequest {
pub model: String,
pub messages: Vec<LlmMessage>,
pub attachments: Vec<LlmAttachment>,
pub tools: Arc<Vec<LlmToolSpec>>,
pub tool_choice: LlmToolChoice,
pub model_variant: Option<String>,
#[serde(default)]
pub generation: GenerationOptions,
pub session_id: Option<String>,
pub output_spec: Option<LlmOutputSpec>,
#[serde(default, skip)]
pub stream_events: Option<LlmEventSender>,
#[serde(default, skip)]
pub provider_trace: Option<LlmProviderTraceSender>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct LlmUsage {
pub input_tokens: i64,
pub output_tokens: i64,
pub cached_input_tokens: i64,
pub reasoning_tokens: i64,
}
#[derive(Clone, Debug)]
pub enum LlmStreamEvent {
Delta(String),
ReasoningDelta(String),
Part(LlmOutputPart),
Usage(LlmUsage),
RetryStatus {
wait_seconds: u64,
attempt: usize,
max_attempts: usize,
reason: String,
},
}
#[derive(Clone)]
pub struct LlmEventSender(Arc<dyn Fn(LlmStreamEvent) + Send + Sync>);
impl LlmEventSender {
pub fn new<F>(send: F) -> Self
where
F: Fn(LlmStreamEvent) + Send + Sync + 'static,
{
Self(Arc::new(send))
}
pub fn send(&self, event: LlmStreamEvent) {
(self.0)(event);
}
}
#[derive(Clone, Debug)]
pub struct LlmProviderTraceEvent {
pub provider: &'static str,
pub event_name: String,
pub raw: String,
}
#[derive(Clone)]
pub struct LlmProviderTraceSender(Arc<dyn Fn(LlmProviderTraceEvent) + Send + Sync>);
impl LlmProviderTraceSender {
pub fn new<F>(send: F) -> Self
where
F: Fn(LlmProviderTraceEvent) + Send + Sync + 'static,
{
Self(Arc::new(send))
}
pub fn send(&self, event: LlmProviderTraceEvent) {
(self.0)(event);
}
}
impl std::fmt::Debug for LlmProviderTraceSender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LlmProviderTraceSender")
.finish_non_exhaustive()
}
}
impl std::fmt::Debug for LlmEventSender {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LlmEventSender").finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct LlmResponse {
pub full_text: String,
pub parts: Vec<LlmOutputPart>,
pub usage: LlmUsage,
pub terminal_reason: LlmTerminalReason,
pub terminal_diagnostic: Option<String>,
pub provider_usage: Option<serde_json::Value>,
pub request_body: Option<String>,
pub http_summary: Option<String>,
}
#[derive(Clone, Debug)]
pub struct ModelSelection {
pub model: &'static str,
pub variant: Option<&'static str>,
}