use crate::constants::{
DEFAULT_API_VERSION, DEFAULT_CONNECT_TIMEOUT, DEFAULT_MAX_LOOP_ITERATIONS,
DEFAULT_REQUEST_TIMEOUT, OPENAI_DEFAULT_API_URL, VALID_API_URL_PREFIXES,
};
use crate::error::{SwarmError, SwarmResult};
use crate::phase::TerminationReason;
use serde::{
de::{self},
Deserialize, Deserializer, Serialize, Serializer,
};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use url::Url;
pub type ContextVariables = HashMap<String, String>;
pub type AgentFuture = Pin<Box<dyn Future<Output = Result<ResultType, SwarmError>> + Send>>;
pub type AgentFunctionHandler = dyn Fn(ContextVariables) -> AgentFuture + Send + Sync;
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct AgentRef(String);
impl AgentRef {
pub fn new(name: impl Into<String>) -> Self {
AgentRef(name.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for AgentRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for AgentRef {
fn from(s: &str) -> Self {
AgentRef(s.to_owned())
}
}
impl From<String> for AgentRef {
fn from(s: String) -> Self {
AgentRef(s)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ApiKey(String);
impl ApiKey {
pub fn new(value: impl Into<String>) -> SwarmResult<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(SwarmError::ValidationError(
"API key cannot be empty".to_string(),
));
}
if !value.starts_with("sk-") {
return Err(SwarmError::ValidationError(
"Invalid API key format".to_string(),
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn redacted(&self) -> String {
if self.0.len() <= 7 {
"sk-***".to_string()
} else {
let suffix = &self.0[self.0.len() - 4..];
format!("sk-***{}", suffix)
}
}
}
impl AsRef<str> for ApiKey {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ApiKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.redacted())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ApiUrlPrefix(String);
impl ApiUrlPrefix {
pub fn new(value: impl Into<String>) -> SwarmResult<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(SwarmError::ValidationError(
"API URL prefix cannot be empty".to_string(),
));
}
Ok(Self(value))
}
pub fn matches(&self, url: &str) -> bool {
url.starts_with(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ApiUrlPrefix {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ApiUrl(String);
impl ApiUrl {
pub fn new(value: impl Into<String>, allowed_prefixes: &[ApiUrlPrefix]) -> SwarmResult<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(SwarmError::ValidationError(
"API URL cannot be empty".to_string(),
));
}
let parsed = Url::parse(&value)
.map_err(|e| SwarmError::ValidationError(format!("Invalid API URL format: {}", e)))?;
let host = parsed.host_str();
let is_localhost = matches!(host, Some("localhost") | Some("127.0.0.1"));
if !is_localhost && !value.starts_with("https://") {
return Err(SwarmError::ValidationError(
"API URL must start with https:// (except for localhost)".to_string(),
));
}
if !is_localhost && !allowed_prefixes.iter().any(|prefix| prefix.matches(&value)) {
return Err(SwarmError::ValidationError(format!(
"API URL must start with one of: {}",
allowed_prefixes
.iter()
.map(ApiUrlPrefix::as_str)
.collect::<Vec<_>>()
.join(", ")
)));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ApiUrl {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ApiUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ModelPrefix(String);
impl ModelPrefix {
pub fn new(value: impl Into<String>) -> SwarmResult<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Model prefix cannot be empty".to_string(),
));
}
Ok(Self(value))
}
pub fn matches(&self, model: &str) -> bool {
model.starts_with(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ModelPrefix {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ModelId(String);
impl ModelId {
pub fn new(value: impl Into<String>, prefixes: &[ModelPrefix]) -> SwarmResult<Self> {
let value = value.into();
if value.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Agent model cannot be empty".to_string(),
));
}
if !prefixes.iter().any(|prefix| prefix.matches(&value)) {
return Err(SwarmError::ValidationError(format!(
"Invalid model prefix. Model must start with one of: {:?}",
prefixes.iter().map(ModelPrefix::as_str).collect::<Vec<_>>()
)));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for ModelId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RequestTimeoutSeconds(u64);
impl RequestTimeoutSeconds {
pub fn new(value: u64) -> SwarmResult<Self> {
if value == 0 {
return Err(SwarmError::ValidationError(
"request_timeout must be greater than 0".to_string(),
));
}
if !(crate::constants::MIN_REQUEST_TIMEOUT..=crate::constants::MAX_REQUEST_TIMEOUT)
.contains(&value)
{
return Err(SwarmError::ValidationError(format!(
"request_timeout must be between {} and {} seconds",
crate::constants::MIN_REQUEST_TIMEOUT,
crate::constants::MAX_REQUEST_TIMEOUT
)));
}
Ok(Self(value))
}
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConnectTimeoutSeconds(u64);
impl ConnectTimeoutSeconds {
pub fn new(value: u64) -> SwarmResult<Self> {
if value == 0 {
return Err(SwarmError::ValidationError(
"connect_timeout must be greater than 0".to_string(),
));
}
Ok(Self(value))
}
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RetryLimit(u32);
impl RetryLimit {
pub fn new(value: u32) -> SwarmResult<Self> {
if value == 0 {
return Err(SwarmError::ValidationError(
"max_retries must be greater than 0".to_string(),
));
}
Ok(Self(value))
}
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct LoopIterationLimit(u32);
impl LoopIterationLimit {
pub fn new(value: u32) -> SwarmResult<Self> {
if value == 0 {
return Err(SwarmError::ValidationError(
"default_max_iterations must be greater than 0".to_string(),
));
}
Ok(Self(value))
}
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Clone)]
pub enum Instructions {
Text(String),
Function(Arc<dyn Fn(ContextVariables) -> String + Send + Sync>),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum FunctionCallPolicy {
Disabled,
Auto,
Named(String),
}
impl FunctionCallPolicy {
pub fn to_wire_value(&self) -> Option<String> {
match self {
Self::Disabled => None,
Self::Auto => Some("auto".to_string()),
Self::Named(name) => Some(name.clone()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ToolCallExecution {
Serial,
Parallel,
}
impl ToolCallExecution {
pub fn is_parallel(self) -> bool {
matches!(self, Self::Parallel)
}
}
#[derive(Clone)]
pub struct Agent {
pub(crate) name: String,
pub(crate) model: String,
pub(crate) instructions: Instructions,
pub(crate) functions: Vec<AgentFunction>,
pub(crate) function_call: FunctionCallPolicy,
pub(crate) parallel_tool_calls: ToolCallExecution,
pub(crate) expected_response_fields: Vec<String>,
pub(crate) capabilities: Vec<String>,
}
impl fmt::Debug for Agent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Agent {{ name: {}, model: {}, function_call: {:?}, parallel_tool_calls: {} }}",
self.name,
self.model,
self.function_call,
self.parallel_tool_calls.is_parallel()
)
}
}
impl Agent {
pub fn new(
name: impl Into<String>,
model: impl Into<String>,
instructions: Instructions,
) -> SwarmResult<Self> {
let agent = Self {
name: name.into(),
model: model.into(),
instructions,
functions: Vec::new(),
function_call: FunctionCallPolicy::Disabled,
parallel_tool_calls: ToolCallExecution::Serial,
expected_response_fields: Vec::new(),
capabilities: Vec::new(),
};
agent.validate_intrinsic_fields()?;
Ok(agent)
}
pub fn with_functions(mut self, functions: Vec<AgentFunction>) -> Self {
self.functions = functions;
self
}
pub fn with_function_call_policy(mut self, function_call: FunctionCallPolicy) -> Self {
self.function_call = function_call;
self
}
pub fn with_tool_call_execution(mut self, parallel_tool_calls: ToolCallExecution) -> Self {
self.parallel_tool_calls = parallel_tool_calls;
self
}
pub fn with_expected_response_fields(
mut self,
expected_response_fields: Vec<String>,
) -> SwarmResult<Self> {
if expected_response_fields
.iter()
.any(|field| field.trim().is_empty())
{
return Err(SwarmError::ValidationError(
"Expected response fields cannot contain empty entries".to_string(),
));
}
self.expected_response_fields = expected_response_fields;
Ok(self)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn model(&self) -> &str {
&self.model
}
pub fn instructions(&self) -> &Instructions {
&self.instructions
}
pub fn functions(&self) -> &[AgentFunction] {
&self.functions
}
pub fn function_call(&self) -> &FunctionCallPolicy {
&self.function_call
}
pub fn tool_call_execution(&self) -> ToolCallExecution {
self.parallel_tool_calls
}
pub fn expected_response_fields(&self) -> &[String] {
&self.expected_response_fields
}
pub fn capabilities(&self) -> &[String] {
&self.capabilities
}
pub fn has_capability(&self, cap: &str) -> bool {
self.capabilities.iter().any(|c| c == cap)
}
pub fn agent_ref(&self) -> AgentRef {
AgentRef::new(&self.name)
}
pub fn with_capabilities(mut self, capabilities: Vec<String>) -> Self {
self.capabilities = capabilities;
self
}
pub(crate) fn validate_intrinsic_fields(&self) -> SwarmResult<()> {
if self.name.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Agent name cannot be empty".to_string(),
));
}
if self.model.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Agent model cannot be empty".to_string(),
));
}
match &self.instructions {
Instructions::Text(text) if text.trim().is_empty() => {
return Err(SwarmError::ValidationError(
"Agent instructions cannot be empty".to_string(),
));
}
Instructions::Function(_) => {}
_ => {}
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
struct AgentTransport {
name: String,
model: String,
instructions: AgentInstructionsTransport,
#[serde(default)]
functions: Vec<AgentFunctionTransport>,
#[serde(default)]
function_call: Option<String>,
#[serde(default)]
parallel_tool_calls: bool,
#[serde(default)]
expected_response_fields: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct AgentInstructionsTransport {
text: String,
}
#[derive(Serialize, Deserialize)]
struct AgentFunctionTransport {
name: String,
accepts_context_variables: bool,
}
impl TryFrom<AgentTransport> for Agent {
type Error = SwarmError;
fn try_from(value: AgentTransport) -> Result<Self, Self::Error> {
if !value.functions.is_empty() {
return Err(SwarmError::ValidationError(
"Agent deserialization does not support runtime function closures".to_string(),
));
}
let function_call = match value.function_call {
None => FunctionCallPolicy::Disabled,
Some(policy) if policy == "auto" => FunctionCallPolicy::Auto,
Some(policy) if policy.trim().is_empty() => {
return Err(SwarmError::ValidationError(
"Agent function_call policy cannot be empty".to_string(),
));
}
Some(policy) => FunctionCallPolicy::Named(policy),
};
Agent::new(
value.name,
value.model,
Instructions::Text(value.instructions.text),
)?
.with_function_call_policy(function_call)
.with_tool_call_execution(if value.parallel_tool_calls {
ToolCallExecution::Parallel
} else {
ToolCallExecution::Serial
})
.with_expected_response_fields(value.expected_response_fields)
}
}
impl Serialize for Agent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if !self.functions.is_empty() {
return Err(serde::ser::Error::custom(
"Agent serialization does not support runtime function closures",
));
}
let instructions = match &self.instructions {
Instructions::Text(text) => AgentInstructionsTransport { text: text.clone() },
Instructions::Function(_) => {
return Err(serde::ser::Error::custom(
"Agent serialization does not support function-based instructions",
))
}
};
AgentTransport {
name: self.name.clone(),
model: self.model.clone(),
instructions,
functions: Vec::new(),
function_call: self.function_call.to_wire_value(),
parallel_tool_calls: self.parallel_tool_calls.is_parallel(),
expected_response_fields: self.expected_response_fields.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Agent {
fn deserialize<D>(deserializer: D) -> Result<Agent, D::Error>
where
D: Deserializer<'de>,
{
let dto = AgentTransport::deserialize(deserializer)?;
Agent::try_from(dto).map_err(de::Error::custom)
}
}
#[derive(Clone, Debug)]
pub enum ResultType {
Value(String),
Agent(Agent),
ContextVariables(ContextVariables),
Termination(TerminationReason),
}
impl ResultType {
pub fn into_value(self) -> Option<String> {
match self {
ResultType::Value(value) => Some(value),
_ => None,
}
}
pub fn into_agent(self) -> Option<Agent> {
if let ResultType::Agent(agent) = self {
Some(agent)
} else {
None
}
}
pub fn into_context_variables(self) -> Option<ContextVariables> {
if let ResultType::ContextVariables(vars) = self {
Some(vars)
} else {
None
}
}
pub fn into_termination_reason(self) -> Option<TerminationReason> {
if let ResultType::Termination(reason) = self {
Some(reason)
} else {
None
}
}
}
#[derive(Clone)]
pub struct AgentFunction {
name: String,
pub(crate) function: Arc<AgentFunctionHandler>,
accepts_context_variables: bool,
description: String,
parameters_schema: Value,
}
impl AgentFunction {
pub fn new(
name: impl Into<String>,
function: Arc<AgentFunctionHandler>,
accepts_context_variables: bool,
) -> SwarmResult<Self> {
let name = name.into();
if name.trim().is_empty() {
return Err(SwarmError::ValidationError(
"AgentFunction name cannot be empty".to_string(),
));
}
Ok(Self {
name,
function,
accepts_context_variables,
description: String::new(),
parameters_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": [],
}),
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn accepts_context_variables(&self) -> bool {
self.accepts_context_variables
}
pub fn description(&self) -> &str {
&self.description
}
pub fn parameters_schema(&self) -> &Value {
&self.parameters_schema
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_parameters_schema(mut self, schema: Value) -> SwarmResult<Self> {
if !schema.is_object() {
return Err(SwarmError::ValidationError(
"AgentFunction parameter schema must be a JSON Schema object \
(e.g. {\"type\":\"object\",\"properties\":{...}})"
.to_string(),
));
}
if let Some(type_val) = schema.get("type") {
if type_val.as_str() != Some("object") {
return Err(SwarmError::ValidationError(format!(
"AgentFunction parameter schema must declare type \"object\", \
got {:?} — scalar and array root schemas are not supported; \
tool arguments are always dispatched as key-value pairs",
type_val
)));
}
}
self.parameters_schema = schema;
Ok(self)
}
pub async fn invoke(&self, args: ContextVariables) -> Result<ResultType, SwarmError> {
let actual_args = if self.accepts_context_variables {
args
} else {
ContextVariables::new()
};
(self.function)(actual_args).await
}
}
#[derive(Clone, Debug, Default)]
pub struct RuntimeLimits {
pub token_budget: Option<u32>,
pub max_tokens_per_request: Option<u32>,
pub max_wall_time_secs: Option<u64>,
pub max_tool_calls: Option<u32>,
pub max_depth: Option<u32>,
}
impl RuntimeLimits {
pub fn any_limit_set(&self) -> bool {
self.token_budget.is_some()
|| self.max_tokens_per_request.is_some()
|| self.max_wall_time_secs.is_some()
|| self.max_tool_calls.is_some()
|| self.max_depth.is_some()
}
}
#[derive(Clone, Debug)]
pub struct SwarmConfig {
api_url: ApiUrl,
api_version: String,
request_timeout: RequestTimeoutSeconds,
connect_timeout: ConnectTimeoutSeconds,
max_retries: RetryLimit,
max_loop_iterations: LoopIterationLimit,
valid_model_prefixes: Vec<ModelPrefix>,
valid_api_url_prefixes: Vec<ApiUrlPrefix>,
loop_control: LoopControl,
api_settings: ApiSettings,
pub runtime_limits: RuntimeLimits,
}
#[derive(Clone, Debug)]
pub struct LoopControl {
default_max_iterations: u32,
iteration_delay: Duration,
break_conditions: Vec<String>,
}
impl LoopControl {
pub fn new(
default_max_iterations: u32,
iteration_delay: Duration,
break_conditions: Vec<String>,
) -> SwarmResult<Self> {
if default_max_iterations == 0 {
return Err(SwarmError::ValidationError(
"default_max_iterations must be greater than 0".to_string(),
));
}
Ok(Self {
default_max_iterations,
iteration_delay,
break_conditions,
})
}
pub fn default_max_iterations(&self) -> u32 {
self.default_max_iterations
}
pub fn iteration_delay(&self) -> Duration {
self.iteration_delay
}
pub fn break_conditions(&self) -> &[String] {
&self.break_conditions
}
pub(crate) fn set_default_max_iterations(&mut self, value: u32) -> SwarmResult<()> {
if value == 0 {
return Err(SwarmError::ValidationError(
"default_max_iterations must be greater than 0".to_string(),
));
}
self.default_max_iterations = value;
Ok(())
}
}
impl Default for LoopControl {
fn default() -> Self {
Self::new(10, Duration::from_millis(100), vec!["end_loop".to_string()])
.expect("SAFETY: 10 > 0")
}
}
#[derive(Clone, Debug)]
pub struct ApiSettings {
retry_strategy: RetryStrategy,
timeout_settings: TimeoutSettings,
}
impl ApiSettings {
pub fn retry_strategy(&self) -> &RetryStrategy {
&self.retry_strategy
}
pub fn timeout_settings(&self) -> &TimeoutSettings {
&self.timeout_settings
}
pub(crate) fn retry_strategy_mut(&mut self) -> &mut RetryStrategy {
&mut self.retry_strategy
}
pub(crate) fn timeout_settings_mut(&mut self) -> &mut TimeoutSettings {
&mut self.timeout_settings
}
}
impl Default for ApiSettings {
fn default() -> Self {
ApiSettings {
retry_strategy: RetryStrategy::new(
3,
Duration::from_secs(1),
Duration::from_secs(30),
2.0,
)
.expect("SAFETY: default retry strategy values are all valid"),
timeout_settings: TimeoutSettings::new(
Duration::from_secs(30),
Duration::from_secs(10),
Duration::from_secs(30),
Duration::from_secs(30),
)
.expect("SAFETY: default timeout values are all non-zero"),
}
}
}
impl Default for SwarmConfig {
fn default() -> Self {
let valid_model_prefixes = vec!["gpt-", "deepseek-", "claude-", "openai-", "openrouter-"]
.into_iter()
.map(|prefix| {
ModelPrefix::new(prefix).expect("SAFETY: literal prefix is a non-empty string")
})
.collect();
let valid_api_url_prefixes = VALID_API_URL_PREFIXES
.iter()
.map(|&prefix| {
ApiUrlPrefix::new(prefix)
.expect("SAFETY: literal URL prefix from VALID_API_URL_PREFIXES")
})
.collect::<Vec<_>>();
SwarmConfig {
api_url: ApiUrl::new(OPENAI_DEFAULT_API_URL.to_string(), &valid_api_url_prefixes)
.expect("SAFETY: OPENAI_DEFAULT_API_URL is a well-formed URL in valid_api_url_prefixes"),
api_version: DEFAULT_API_VERSION.to_string(),
request_timeout: RequestTimeoutSeconds::new(DEFAULT_REQUEST_TIMEOUT)
.expect("SAFETY: DEFAULT_REQUEST_TIMEOUT=30 is within [MIN_REQUEST_TIMEOUT, MAX_REQUEST_TIMEOUT]"),
connect_timeout: ConnectTimeoutSeconds::new(DEFAULT_CONNECT_TIMEOUT)
.expect("SAFETY: DEFAULT_CONNECT_TIMEOUT=10 is a positive integer"),
max_retries: RetryLimit::new(3).expect("SAFETY: 3 > 0"),
max_loop_iterations: LoopIterationLimit::new(DEFAULT_MAX_LOOP_ITERATIONS)
.expect("SAFETY: DEFAULT_MAX_LOOP_ITERATIONS=10 > 0"),
valid_model_prefixes,
valid_api_url_prefixes,
loop_control: LoopControl::default(),
api_settings: ApiSettings::default(),
runtime_limits: RuntimeLimits::default(),
}
}
}
impl SwarmConfig {
pub fn api_url(&self) -> &str {
self.api_url.as_str()
}
pub fn api_version(&self) -> &str {
&self.api_version
}
pub fn request_timeout(&self) -> u64 {
self.request_timeout.get()
}
pub fn connect_timeout(&self) -> u64 {
self.connect_timeout.get()
}
pub fn max_retries(&self) -> u32 {
self.max_retries.get()
}
pub fn max_loop_iterations(&self) -> u32 {
self.max_loop_iterations.get()
}
pub fn valid_model_prefixes(&self) -> &[ModelPrefix] {
&self.valid_model_prefixes
}
pub fn valid_api_url_prefixes(&self) -> &[ApiUrlPrefix] {
&self.valid_api_url_prefixes
}
pub fn loop_control(&self) -> &LoopControl {
&self.loop_control
}
pub fn api_settings(&self) -> &ApiSettings {
&self.api_settings
}
pub fn runtime_limits(&self) -> &RuntimeLimits {
&self.runtime_limits
}
pub(crate) fn set_runtime_limits(&mut self, limits: RuntimeLimits) {
self.runtime_limits = limits;
}
pub(crate) fn set_api_url(&mut self, api_url: impl Into<String>) -> SwarmResult<()> {
self.api_url = ApiUrl::new(api_url, &self.valid_api_url_prefixes)?;
Ok(())
}
pub(crate) fn set_api_version(&mut self, api_version: impl Into<String>) -> SwarmResult<()> {
let api_version = api_version.into();
if api_version.trim().is_empty() {
return Err(SwarmError::ValidationError(
"API version cannot be empty".to_string(),
));
}
self.api_version = api_version;
Ok(())
}
pub(crate) fn set_request_timeout(&mut self, request_timeout: u64) -> SwarmResult<()> {
let request_timeout = RequestTimeoutSeconds::new(request_timeout)?;
self.request_timeout = request_timeout;
self.api_settings
.timeout_settings_mut()
.set_request_timeout(Duration::from_secs(request_timeout.get()))?;
Ok(())
}
pub(crate) fn set_connect_timeout(&mut self, connect_timeout: u64) -> SwarmResult<()> {
let connect_timeout = ConnectTimeoutSeconds::new(connect_timeout)?;
self.connect_timeout = connect_timeout;
self.api_settings
.timeout_settings_mut()
.set_connect_timeout(Duration::from_secs(connect_timeout.get()))?;
Ok(())
}
pub(crate) fn set_max_retries(&mut self, max_retries: u32) -> SwarmResult<()> {
self.max_retries = RetryLimit::new(max_retries)?;
self.api_settings
.retry_strategy_mut()
.set_max_retries(max_retries)?;
Ok(())
}
pub(crate) fn set_max_loop_iterations(&mut self, max_loop_iterations: u32) -> SwarmResult<()> {
let max_loop_iterations = LoopIterationLimit::new(max_loop_iterations)?;
self.max_loop_iterations = max_loop_iterations;
self.loop_control
.set_default_max_iterations(max_loop_iterations.get())?;
Ok(())
}
pub(crate) fn set_valid_model_prefixes(
&mut self,
valid_model_prefixes: Vec<String>,
) -> SwarmResult<()> {
if valid_model_prefixes.is_empty() {
return Err(SwarmError::ValidationError(
"valid_model_prefixes cannot be empty".to_string(),
));
}
self.valid_model_prefixes = valid_model_prefixes
.into_iter()
.map(ModelPrefix::new)
.collect::<SwarmResult<Vec<_>>>()?;
Ok(())
}
pub(crate) fn set_valid_api_url_prefixes(
&mut self,
valid_api_url_prefixes: Vec<String>,
) -> SwarmResult<()> {
if valid_api_url_prefixes.is_empty() {
return Err(SwarmError::ValidationError(
"valid_api_url_prefixes cannot be empty".to_string(),
));
}
let valid_api_url_prefixes = valid_api_url_prefixes
.into_iter()
.map(ApiUrlPrefix::new)
.collect::<SwarmResult<Vec<_>>>()?;
let current_api_url = self.api_url.as_str().to_string();
self.valid_api_url_prefixes = valid_api_url_prefixes;
self.api_url = ApiUrl::new(current_api_url, &self.valid_api_url_prefixes)?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum MessageRole {
System,
User,
Assistant,
Function,
Tool,
}
impl MessageRole {
pub fn as_str(self) -> &'static str {
match self {
Self::System => "system",
Self::User => "user",
Self::Assistant => "assistant",
Self::Function => "function",
Self::Tool => "tool",
}
}
}
impl fmt::Display for MessageRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct FunctionCall {
name: String,
arguments: String,
}
#[derive(Deserialize)]
struct FunctionCallDto {
name: String,
arguments: String,
}
impl FunctionCall {
pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> SwarmResult<Self> {
let function_call = Self {
name: name.into(),
arguments: arguments.into(),
};
function_call.validate()?;
Ok(function_call)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn arguments(&self) -> &str {
&self.arguments
}
pub(crate) fn from_parts_unchecked(name: String, arguments: String) -> Self {
Self { name, arguments }
}
pub(crate) fn merge_delta(&mut self, delta: &Value) {
if let Some(name) = delta.get("name").and_then(|value| value.as_str()) {
self.name.push_str(name);
}
if let Some(arguments) = delta.get("arguments").and_then(|value| value.as_str()) {
self.arguments.push_str(arguments);
}
}
fn validate(&self) -> SwarmResult<()> {
if self.name.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Function call name cannot be empty".to_string(),
));
}
if self.arguments.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Function call arguments cannot be empty".to_string(),
));
}
serde_json::from_str::<Value>(&self.arguments).map_err(|error| {
SwarmError::ValidationError(format!(
"Function call arguments must be valid JSON: {}",
error
))
})?;
Ok(())
}
}
impl<'de> Deserialize<'de> for FunctionCall {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let dto = FunctionCallDto::deserialize(deserializer)?;
Self::new(dto.name, dto.arguments).map_err(de::Error::custom)
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct ToolCall {
id: String,
#[serde(rename = "type")]
call_type: String,
function: FunctionCall,
}
#[derive(Deserialize)]
struct ToolCallDto {
#[serde(default)]
id: String,
#[serde(rename = "type", default)]
call_type: String,
function: FunctionCall,
}
impl ToolCall {
pub fn new(id: impl Into<String>, function: FunctionCall) -> SwarmResult<Self> {
let id = id.into();
if id.trim().is_empty() {
return Err(SwarmError::ValidationError(
"ToolCall id cannot be empty".to_string(),
));
}
Ok(Self {
id,
call_type: "function".to_string(),
function,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn call_type(&self) -> &str {
&self.call_type
}
pub fn function(&self) -> &FunctionCall {
&self.function
}
}
impl<'de> Deserialize<'de> for ToolCall {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let dto = ToolCallDto::deserialize(deserializer)?;
Ok(Self {
id: dto.id,
call_type: if dto.call_type.is_empty() {
"function".to_string()
} else {
dto.call_type
},
function: dto.function,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ToolCallAccumulator {
id: String,
call_type: String,
name: String,
arguments: String,
}
impl ToolCallAccumulator {
fn new() -> Self {
Self {
id: String::new(),
call_type: "function".to_string(),
name: String::new(),
arguments: String::new(),
}
}
fn merge_delta(&mut self, delta: &Value) {
if let Some(id) = delta.get("id").and_then(|v| v.as_str()) {
self.id.push_str(id);
}
if let Some(ct) = delta.get("type").and_then(|v| v.as_str()) {
if !ct.is_empty() {
self.call_type = ct.to_string();
}
}
if let Some(func) = delta.get("function") {
if let Some(name) = func.get("name").and_then(|v| v.as_str()) {
self.name.push_str(name);
}
if let Some(args) = func.get("arguments").and_then(|v| v.as_str()) {
self.arguments.push_str(args);
}
}
}
fn into_tool_call(self) -> ToolCall {
ToolCall {
id: self.id,
call_type: self.call_type,
function: FunctionCall::from_parts_unchecked(self.name, self.arguments),
}
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct Message {
role: MessageRole,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
function_call: Option<FunctionCall>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
#[serde(skip)]
tool_call_accumulators: HashMap<usize, ToolCallAccumulator>,
}
#[derive(Deserialize)]
struct MessageDto {
role: MessageRole,
content: Option<String>,
name: Option<String>,
function_call: Option<FunctionCall>,
#[serde(default)]
tool_calls: Option<Vec<ToolCall>>,
#[serde(default)]
tool_call_id: Option<String>,
}
impl Message {
pub fn new(
role: MessageRole,
content: Option<String>,
name: Option<String>,
function_call: Option<FunctionCall>,
) -> SwarmResult<Self> {
let message = Self {
role,
content,
name,
function_call,
tool_calls: None,
tool_call_id: None,
tool_call_accumulators: HashMap::new(),
};
message.validate()?;
Ok(message)
}
pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> SwarmResult<Self> {
if tool_calls.is_empty() {
return Err(SwarmError::ValidationError(
"assistant_tool_calls requires at least one tool call".to_string(),
));
}
let message = Self {
role: MessageRole::Assistant,
content: None,
name: None,
function_call: None,
tool_calls: Some(tool_calls),
tool_call_id: None,
tool_call_accumulators: HashMap::new(),
};
message.validate()?;
Ok(message)
}
pub fn tool_result(
tool_call_id: impl Into<String>,
content: impl Into<String>,
) -> SwarmResult<Self> {
let message = Self {
role: MessageRole::Tool,
content: Some(content.into()),
name: None,
function_call: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
tool_call_accumulators: HashMap::new(),
};
message.validate()?;
Ok(message)
}
pub fn system(content: impl Into<String>) -> SwarmResult<Self> {
Self::new(MessageRole::System, Some(content.into()), None, None)
}
pub fn user(content: impl Into<String>) -> SwarmResult<Self> {
Self::new(MessageRole::User, Some(content.into()), None, None)
}
pub fn assistant(content: impl Into<String>) -> SwarmResult<Self> {
Self::new(MessageRole::Assistant, Some(content.into()), None, None)
}
pub fn assistant_named(
name: impl Into<String>,
content: impl Into<String>,
) -> SwarmResult<Self> {
Self::new(
MessageRole::Assistant,
Some(content.into()),
Some(name.into()),
None,
)
}
pub fn assistant_function_call(function_call: FunctionCall) -> SwarmResult<Self> {
Self::new(MessageRole::Assistant, None, None, Some(function_call))
}
pub fn function(name: impl Into<String>, content: impl Into<String>) -> SwarmResult<Self> {
Self::new(
MessageRole::Function,
Some(content.into()),
Some(name.into()),
None,
)
}
pub fn role(&self) -> MessageRole {
self.role
}
pub fn content(&self) -> Option<&str> {
self.content.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn function_call(&self) -> Option<&FunctionCall> {
self.function_call.as_ref()
}
pub fn tool_calls(&self) -> Option<&[ToolCall]> {
self.tool_calls.as_deref()
}
pub fn tool_call_id(&self) -> Option<&str> {
self.tool_call_id.as_deref()
}
pub fn validate(&self) -> SwarmResult<()> {
if let Some(content) = &self.content {
if content.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Message content cannot be empty".to_string(),
));
}
}
if let Some(name) = &self.name {
if name.trim().is_empty() {
return Err(SwarmError::ValidationError(
"Message name cannot be empty".to_string(),
));
}
}
match self.role {
MessageRole::System | MessageRole::User => {
if self.content.is_none() {
return Err(SwarmError::ValidationError(format!(
"{} messages require content",
self.role
)));
}
if self.name.is_some() {
return Err(SwarmError::ValidationError(format!(
"{} messages cannot set name",
self.role
)));
}
if self.function_call.is_some() {
return Err(SwarmError::ValidationError(format!(
"{} messages cannot include function calls",
self.role
)));
}
if self.tool_calls.is_some() {
return Err(SwarmError::ValidationError(format!(
"{} messages cannot include tool calls",
self.role
)));
}
if self.tool_call_id.is_some() {
return Err(SwarmError::ValidationError(format!(
"{} messages cannot include tool_call_id",
self.role
)));
}
}
MessageRole::Assistant => {
let has_content = self.content.is_some();
let has_function_call = self.function_call.is_some();
let has_tool_calls = self
.tool_calls
.as_ref()
.map(|v| !v.is_empty())
.unwrap_or(false);
let variant_count =
has_content as u8 + has_function_call as u8 + has_tool_calls as u8;
if variant_count != 1 {
return Err(SwarmError::ValidationError(
"Assistant messages must contain exactly one of: content, function_call, or tool_calls"
.to_string(),
));
}
if (has_function_call || has_tool_calls) && self.name.is_some() {
return Err(SwarmError::ValidationError(
"Assistant tool-call messages cannot set name".to_string(),
));
}
if self.tool_call_id.is_some() {
return Err(SwarmError::ValidationError(
"Assistant messages cannot include tool_call_id".to_string(),
));
}
}
MessageRole::Function => {
if self.content.is_none() {
return Err(SwarmError::ValidationError(
"Function messages require content".to_string(),
));
}
if self.name.is_none() {
return Err(SwarmError::ValidationError(
"Function messages require a name".to_string(),
));
}
if self.function_call.is_some() {
return Err(SwarmError::ValidationError(
"Function messages cannot include function calls".to_string(),
));
}
if self.tool_calls.is_some() {
return Err(SwarmError::ValidationError(
"Function messages cannot include tool_calls".to_string(),
));
}
if self.tool_call_id.is_some() {
return Err(SwarmError::ValidationError(
"Function messages cannot include tool_call_id".to_string(),
));
}
}
MessageRole::Tool => {
if self.content.is_none() {
return Err(SwarmError::ValidationError(
"Tool messages require content".to_string(),
));
}
if self.tool_call_id.is_none() {
return Err(SwarmError::ValidationError(
"Tool messages require tool_call_id".to_string(),
));
}
if self.name.is_some() {
return Err(SwarmError::ValidationError(
"Tool messages cannot set name".to_string(),
));
}
if self.function_call.is_some() {
return Err(SwarmError::ValidationError(
"Tool messages cannot include function_call".to_string(),
));
}
if self.tool_calls.is_some() {
return Err(SwarmError::ValidationError(
"Tool messages cannot include tool_calls".to_string(),
));
}
}
}
Ok(())
}
pub(crate) fn from_parts_unchecked(
role: MessageRole,
content: Option<String>,
name: Option<String>,
function_call: Option<FunctionCall>,
) -> Self {
Self {
role,
content,
name,
function_call,
tool_calls: None,
tool_call_id: None,
tool_call_accumulators: HashMap::new(),
}
}
pub(crate) fn append_content_fragment(&mut self, fragment: &str) {
if fragment.is_empty() {
return;
}
if let Some(existing_content) = &mut self.content {
existing_content.push_str(fragment);
} else {
self.content = Some(fragment.to_string());
}
}
pub(crate) fn merge_function_call_delta(&mut self, delta: &Value) {
let function_call = self.function_call.get_or_insert_with(|| {
FunctionCall::from_parts_unchecked(String::new(), String::new())
});
function_call.merge_delta(delta);
}
pub(crate) fn merge_tool_call_delta(&mut self, index: usize, delta: &Value) {
self.tool_call_accumulators
.entry(index)
.or_insert_with(ToolCallAccumulator::new)
.merge_delta(delta);
}
pub(crate) fn finalize_tool_calls(&mut self) {
if self.tool_call_accumulators.is_empty() {
return;
}
let mut indices: Vec<usize> = self.tool_call_accumulators.keys().copied().collect();
indices.sort_unstable();
let calls: Vec<ToolCall> = indices
.into_iter()
.filter_map(|i| self.tool_call_accumulators.remove(&i))
.map(ToolCallAccumulator::into_tool_call)
.collect();
if !calls.is_empty() {
self.tool_calls = Some(calls);
}
}
}
impl<'de> Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let dto = MessageDto::deserialize(deserializer)?;
let msg = Self {
role: dto.role,
content: dto.content,
name: dto.name,
function_call: dto.function_call,
tool_calls: dto.tool_calls,
tool_call_id: dto.tool_call_id,
tool_call_accumulators: HashMap::new(),
};
msg.validate().map_err(de::Error::custom)?;
Ok(msg)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ChatCompletionResponse {
id: String,
object: String,
created: u64,
choices: Vec<Choice>,
usage: Option<Usage>,
}
impl ChatCompletionResponse {
pub(crate) fn accumulator() -> Self {
Self {
id: String::new(),
object: "chat.completion".to_string(),
created: 0,
choices: Vec::new(),
usage: None,
}
}
pub fn choices(&self) -> &[Choice] {
&self.choices
}
pub fn into_choices(self) -> Vec<Choice> {
self.choices
}
pub(crate) fn extend_choices(&mut self, new_choices: Vec<Choice>) {
self.choices.extend(new_choices);
}
pub fn usage(&self) -> Option<&Usage> {
self.usage.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FinishReason {
Stop,
Length,
ContentFilter,
ToolCalls,
FunctionCall,
Unknown(String),
}
impl<'de> Deserialize<'de> for FinishReason {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"stop" => Self::Stop,
"length" => Self::Length,
"content_filter" => Self::ContentFilter,
"tool_calls" => Self::ToolCalls,
"function_call" => Self::FunctionCall,
other => Self::Unknown(other.to_string()),
})
}
}
impl Serialize for FinishReason {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match self {
Self::Stop => "stop",
Self::Length => "length",
Self::ContentFilter => "content_filter",
Self::ToolCalls => "tool_calls",
Self::FunctionCall => "function_call",
Self::Unknown(s) => s.as_str(),
})
}
}
#[derive(Serialize, Clone, Debug)]
pub struct Choice {
pub index: u32,
pub message: Message,
pub finish_reason: Option<FinishReason>,
}
impl<'de> Deserialize<'de> for Choice {
fn deserialize<D>(deserializer: D) -> Result<Choice, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let index = value
.get("index")
.and_then(|v| v.as_u64())
.ok_or_else(|| de::Error::missing_field("index"))? as u32;
let finish_reason = value
.get("finish_reason")
.filter(|v| !v.is_null())
.map(|v| serde_json::from_value::<FinishReason>(v.clone()))
.transpose()
.map_err(de::Error::custom)?;
let message = if let Some(msg_val) = value.get("message") {
serde_json::from_value(msg_val.clone()).map_err(de::Error::custom)?
} else if let Some(delta_val) = value.get("delta") {
let role = delta_val
.get("role")
.cloned()
.map(serde_json::from_value::<MessageRole>)
.transpose()
.map_err(de::Error::custom)?
.unwrap_or(MessageRole::Assistant);
let content = delta_val
.get("content")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut message = Message::from_parts_unchecked(role, content, None, None);
if let Some(function_call_delta) = delta_val.get("function_call") {
message.merge_function_call_delta(function_call_delta);
}
message
} else {
return Err(de::Error::missing_field("message (or delta)"));
};
Ok(Choice {
index,
message,
finish_reason,
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Clone, Debug)]
pub struct Response {
pub messages: Vec<Message>,
pub agent: Option<Agent>,
pub context_variables: ContextVariables,
pub termination_reason: Option<TerminationReason>,
pub tokens_used: u32,
}
#[derive(Debug, Deserialize)]
pub struct Steps {
#[serde(rename = "step", default)]
pub steps: Vec<Step>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StepAction {
RunOnce,
Loop,
}
impl fmt::Display for StepAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RunOnce => write!(f, "run_once"),
Self::Loop => write!(f, "loop"),
}
}
}
#[derive(Debug, Deserialize)]
pub struct Step {
#[serde(rename = "@number")]
pub number: usize,
#[serde(rename = "@action")]
pub action: StepAction,
#[serde(rename = "@agent")]
pub agent: Option<String>,
pub prompt: String,
}
#[derive(Clone, Debug)]
pub struct RetryStrategy {
max_retries: u32,
initial_delay: Duration,
max_delay: Duration,
backoff_factor: f32,
}
impl RetryStrategy {
pub fn new(
max_retries: u32,
initial_delay: Duration,
max_delay: Duration,
backoff_factor: f32,
) -> SwarmResult<Self> {
if max_retries == 0 {
return Err(SwarmError::ValidationError(
"max_retries must be greater than 0".to_string(),
));
}
if initial_delay.is_zero() {
return Err(SwarmError::ValidationError(
"initial_delay must be greater than zero".to_string(),
));
}
if max_delay < initial_delay {
return Err(SwarmError::ValidationError(
"max_delay must be >= initial_delay".to_string(),
));
}
if !backoff_factor.is_finite() || backoff_factor < 1.0 {
return Err(SwarmError::ValidationError(
"backoff_factor must be a finite number >= 1.0".to_string(),
));
}
Ok(Self {
max_retries,
initial_delay,
max_delay,
backoff_factor,
})
}
pub fn max_retries(&self) -> u32 {
self.max_retries
}
pub fn initial_delay(&self) -> Duration {
self.initial_delay
}
pub fn max_delay(&self) -> Duration {
self.max_delay
}
pub fn backoff_factor(&self) -> f32 {
self.backoff_factor
}
pub(crate) fn set_max_retries(&mut self, value: u32) -> SwarmResult<()> {
if value == 0 {
return Err(SwarmError::ValidationError(
"max_retries must be greater than 0".to_string(),
));
}
self.max_retries = value;
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct TimeoutSettings {
request_timeout: Duration,
connect_timeout: Duration,
read_timeout: Duration,
write_timeout: Duration,
}
impl TimeoutSettings {
pub fn new(
request_timeout: Duration,
connect_timeout: Duration,
read_timeout: Duration,
write_timeout: Duration,
) -> SwarmResult<Self> {
if request_timeout.is_zero() {
return Err(SwarmError::ValidationError(
"request_timeout must be greater than zero".to_string(),
));
}
if connect_timeout.is_zero() {
return Err(SwarmError::ValidationError(
"connect_timeout must be greater than zero".to_string(),
));
}
if read_timeout.is_zero() {
return Err(SwarmError::ValidationError(
"read_timeout must be greater than zero".to_string(),
));
}
if write_timeout.is_zero() {
return Err(SwarmError::ValidationError(
"write_timeout must be greater than zero".to_string(),
));
}
Ok(Self {
request_timeout,
connect_timeout,
read_timeout,
write_timeout,
})
}
pub fn request_timeout(&self) -> Duration {
self.request_timeout
}
pub fn connect_timeout(&self) -> Duration {
self.connect_timeout
}
pub fn read_timeout(&self) -> Duration {
self.read_timeout
}
pub fn write_timeout(&self) -> Duration {
self.write_timeout
}
pub(crate) fn set_request_timeout(&mut self, value: Duration) -> SwarmResult<()> {
if value.is_zero() {
return Err(SwarmError::ValidationError(
"request_timeout must be greater than zero".to_string(),
));
}
self.request_timeout = value;
Ok(())
}
pub(crate) fn set_connect_timeout(&mut self, value: Duration) -> SwarmResult<()> {
if value.is_zero() {
return Err(SwarmError::ValidationError(
"connect_timeout must be greater than zero".to_string(),
));
}
self.connect_timeout = value;
Ok(())
}
}
#[derive(Debug, Deserialize)]
pub struct OpenAIErrorResponse {
pub error: OpenAIError,
}
#[derive(Debug, Deserialize)]
pub struct OpenAIError {
pub message: String,
#[serde(rename = "type")]
pub error_type: String,
pub param: Option<String>,
pub code: Option<String>,
}