use std::time::Duration;
use chat_engine_sdk::error::PluginError;
use serde::{Deserialize, Serialize};
use toolkit_macros::domain_model;
pub mod schema_ids {
pub const LLM_PLUGIN_CONFIG_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway_plugin_config.v1~";
pub const LLM_SUMMARIZATION_SETTINGS_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.summarization_settings.v1~";
pub const LLM_MESSAGE_METADATA_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.message_metadata.v1~";
pub const LLM_USAGE_SCHEMA_ID: &str = "gtx.cf.chat_engine.llm_gateway.usage.v1~";
pub const LLM_MESSAGE_SCHEMA_ID: &str = "gtx.cf.chat_engine.llm_gateway.message.v1~";
pub const LLM_MESSAGE_GET_RESPONSE_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.message_get_response.v1~";
pub const LLM_MESSAGE_NEW_RESPONSE_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.message_new_response.v1~";
pub const LLM_MESSAGE_RECREATE_RESPONSE_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.message_recreate_response.v1~";
pub const LLM_STREAMING_COMPLETE_EVENT_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.streaming_complete_event.v1~";
pub const LLM_MESSAGE_NEW_EVENT_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.message_new_event.v1~";
pub const LLM_SESSION_SUMMARY_EVENT_SCHEMA_ID: &str =
"gtx.cf.chat_engine.llm_gateway.session_summary_event.v1~";
}
pub const RECENT_MESSAGES_TO_KEEP_MIN: u32 = 2;
pub const RECENT_MESSAGES_TO_KEEP_DEFAULT: u32 = 10;
pub const DEFAULT_RETRY_COUNT: u32 = 3;
pub const DEFAULT_RETRY_DELAY_MS: u32 = 1000;
pub const DEFAULT_TIMEOUT_MS: u32 = 30_000;
pub const DEFAULT_CIRCUIT_BREAKER_FAILURE_THRESHOLD: u32 = 5;
pub const DEFAULT_CIRCUIT_BREAKER_COOLDOWN_MS: u32 = 60_000;
#[domain_model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LlmPluginConfig {
pub gateway_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summarization_settings: Option<LlmSummarizationSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry_delay_ms: Option<u32>,
}
impl LlmPluginConfig {
#[must_use]
pub fn effective_retry_count(&self) -> u32 {
self.retry_count.unwrap_or(DEFAULT_RETRY_COUNT)
}
#[must_use]
pub fn effective_retry_delay(&self) -> Duration {
Duration::from_millis(u64::from(
self.retry_delay_ms.unwrap_or(DEFAULT_RETRY_DELAY_MS),
))
}
#[must_use]
pub fn summarization_enabled(&self) -> bool {
self.summarization_settings.is_some()
}
}
#[domain_model]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct LlmSummarizationSettings {
#[serde(default = "default_recent_messages_to_keep")]
pub recent_messages_to_keep: u32,
}
impl LlmSummarizationSettings {
#[must_use]
pub fn keep_count(&self) -> u32 {
self.recent_messages_to_keep
.max(RECENT_MESSAGES_TO_KEEP_MIN)
}
}
impl Default for LlmSummarizationSettings {
fn default() -> Self {
Self {
recent_messages_to_keep: RECENT_MESSAGES_TO_KEEP_DEFAULT,
}
}
}
fn default_recent_messages_to_keep() -> u32 {
RECENT_MESSAGES_TO_KEEP_DEFAULT
}
#[domain_model]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
Stop,
Length,
ContentFilter,
ToolCalls,
Error,
}
impl FinishReason {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Stop => "stop",
Self::Length => "length",
Self::ContentFilter => "content_filter",
Self::ToolCalls => "tool_calls",
Self::Error => "error",
}
}
}
#[domain_model]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct LlmUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cached_tokens: Option<u32>,
}
#[domain_model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LlmMessageMetadata {
pub model_used: String,
pub finish_reason: FinishReason,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature_used: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<LlmUsage>,
}
impl LlmMessageMetadata {
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
}
}
pub fn validate_plugin_config(json: &serde_json::Value) -> Result<LlmPluginConfig, PluginError> {
let parsed: LlmPluginConfig = serde_json::from_value(json.clone())
.map_err(|e| PluginError::invalid_input_with("invalid LlmPluginConfig blob", e))?;
crate::infra::url_guard::validate_outbound_url(&parsed.gateway_url, "gateway_url")?;
if let Some(s) = parsed.summarization_settings
&& s.recent_messages_to_keep < RECENT_MESSAGES_TO_KEEP_MIN
{
return Err(PluginError::invalid_input(format!(
"LlmPluginConfig.summarization_settings.recent_messages_to_keep must be >= {RECENT_MESSAGES_TO_KEEP_MIN}",
)));
}
Ok(parsed)
}
#[cfg(test)]
#[path = "llm_config_tests.rs"]
mod llm_config_tests;