use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[cfg(feature = "openapi")]
use utoipa::ToSchema;
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmGenerationOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmRequestOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache: Option<LlmPromptCacheInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_search: Option<LlmToolSearchInfo>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub provider_options: HashMap<String, Value>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
}
impl LlmRequestOptions {
pub fn is_empty(&self) -> bool {
self.temperature.is_none()
&& self.max_tokens.is_none()
&& self.reasoning_effort.is_none()
&& self.stream.is_none()
&& self.prompt_cache.is_none()
&& self.tool_search.is_none()
&& self.provider_options.is_empty()
&& self.metadata.is_empty()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmPromptCacheInfo {
pub enabled: bool,
pub strategy: crate::driver_registry::PromptCacheStrategy,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmToolSearchInfo {
pub enabled: bool,
pub threshold: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmGenerationMetadata {
#[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<TokenUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = 312u64))]
pub time_to_first_token_ms: Option<u64>,
#[cfg_attr(feature = "openapi", schema(example = true))]
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
pub finish_reasons: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
pub response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<LlmRetryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction: Option<LlmCompactionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_options: Option<LlmRequestOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmRetryInfo {
pub attempts: u32,
pub total_wait_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmCompactionInfo {
pub compacted: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens_before: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens_after: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_usd: Option<f64>,
}
impl LlmCompactionInfo {
pub fn new(
input_tokens_before: Option<u32>,
input_tokens_after: Option<u32>,
duration_ms: Option<u64>,
cost_usd: Option<f64>,
) -> Self {
Self {
compacted: true,
input_tokens_before,
input_tokens_after,
duration_ms,
cost_usd,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(ToSchema))]
pub struct LlmGenerationData {
pub messages: Vec<Message>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ToolDefinitionSummary>,
pub output: LlmGenerationOutput,
pub metadata: LlmGenerationMetadata,
}
impl LlmGenerationData {
#[allow(clippy::too_many_arguments)]
pub fn success(
messages: Vec<Message>,
tools: Vec<ToolDefinitionSummary>,
text: Option<String>,
tool_calls: Vec<ToolCall>,
model: String,
provider: Option<String>,
usage: Option<TokenUsage>,
duration_ms: Option<u64>,
time_to_first_token_ms: Option<u64>,
) -> Self {
let finish_reasons = if !tool_calls.is_empty() {
Some(vec!["tool_calls".to_string()])
} else {
Some(vec!["stop".to_string()])
};
Self {
messages,
tools,
output: LlmGenerationOutput { text, tool_calls },
metadata: LlmGenerationMetadata {
model,
provider,
usage,
duration_ms,
time_to_first_token_ms,
success: true,
error: None,
finish_reasons,
response_id: None,
retry: None,
compaction: None,
request_options: None,
},
}
}
#[allow(clippy::too_many_arguments)]
pub fn success_with_metadata(
messages: Vec<Message>,
tools: Vec<ToolDefinitionSummary>,
text: Option<String>,
tool_calls: Vec<ToolCall>,
model: String,
provider: Option<String>,
usage: Option<TokenUsage>,
duration_ms: Option<u64>,
time_to_first_token_ms: Option<u64>,
finish_reasons: Option<Vec<String>>,
response_id: Option<String>,
) -> Self {
Self {
messages,
tools,
output: LlmGenerationOutput { text, tool_calls },
metadata: LlmGenerationMetadata {
model,
provider,
usage,
duration_ms,
time_to_first_token_ms,
success: true,
error: None,
finish_reasons,
response_id,
retry: None,
compaction: None,
request_options: None,
},
}
}
#[allow(clippy::too_many_arguments)]
pub fn success_with_retry(
messages: Vec<Message>,
tools: Vec<ToolDefinitionSummary>,
text: Option<String>,
tool_calls: Vec<ToolCall>,
model: String,
provider: Option<String>,
usage: Option<TokenUsage>,
duration_ms: Option<u64>,
time_to_first_token_ms: Option<u64>,
finish_reasons: Option<Vec<String>>,
response_id: Option<String>,
retry: Option<LlmRetryInfo>,
) -> Self {
Self {
messages,
tools,
output: LlmGenerationOutput { text, tool_calls },
metadata: LlmGenerationMetadata {
model,
provider,
usage,
duration_ms,
time_to_first_token_ms,
success: true,
error: None,
finish_reasons,
response_id,
retry,
compaction: None,
request_options: None,
},
}
}
pub fn failure(
messages: Vec<Message>,
tools: Vec<ToolDefinitionSummary>,
model: String,
provider: Option<String>,
error: String,
duration_ms: Option<u64>,
time_to_first_token_ms: Option<u64>,
) -> Self {
Self {
messages,
tools,
output: LlmGenerationOutput {
text: None,
tool_calls: vec![],
},
metadata: LlmGenerationMetadata {
model,
provider,
usage: None,
duration_ms,
time_to_first_token_ms,
success: false,
error: Some(error),
finish_reasons: Some(vec!["error".to_string()]),
response_id: None,
retry: None,
compaction: None,
request_options: None,
},
}
}
pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
self.metadata.compaction = Some(compaction);
self
}
pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
self.metadata.retry = Some(retry);
self
}
pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
if !request_options.is_empty() {
self.metadata.request_options = Some(request_options);
}
self
}
}