Skip to main content

aimux_core/
tool.rs

1//! Tool / function-calling types.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use ts_rs::TS;
8
9/// A tool definition passed to the model in `CallOptions.tools`.
10#[derive(Debug, Clone, Serialize, Deserialize, TS)]
11#[ts(export)]
12pub struct FunctionTool {
13    /// Unique tool name.
14    pub name: String,
15    /// Human-readable description.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub description: Option<String>,
18    /// JSON Schema describing the tool's parameters.
19    pub input_schema: Value,
20    /// Whether the tool supports strict schema enforcement (OpenAI strict mode).
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub strict: Option<bool>,
23    /// Provider-specific options, keyed by provider name
24    /// (e.g. `{"anthropic": {"eagerInputStreaming": true}}`). Aligned with the
25    /// V4 `providerOptions` field on function tools.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub provider_options: Option<HashMap<String, Value>>,
28    /// Example inputs for the tool (V4 `inputExamples`), used by some providers
29    /// to emit `input_examples` in the request body.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub input_examples: Option<Vec<Value>>,
32}
33
34impl FunctionTool {
35    /// Create a `FunctionTool` with the required fields and all optional fields
36    /// set to `None`. Combined with struct-update syntax this avoids spelling
37    /// out every `None` field in tests.
38    pub fn new(name: impl Into<String>, input_schema: Value) -> Self {
39        FunctionTool {
40            name: name.into(),
41            description: None,
42            input_schema,
43            strict: None,
44            provider_options: None,
45            input_examples: None,
46        }
47    }
48
49    /// Builder-style setter for `description`.
50    pub fn with_description(mut self, description: impl Into<String>) -> Self {
51        self.description = Some(description.into());
52        self
53    }
54
55    /// Builder-style setter for `strict`.
56    pub fn with_strict(mut self, strict: bool) -> Self {
57        self.strict = Some(strict);
58        self
59    }
60}
61
62/// A provider-defined tool (e.g. `anthropic.web_search_20250305`,
63/// `google.googleSearch`). Mirrors the V4 `LanguageModelV4ProviderTool`.
64///
65/// Provider tools are specific to a certain provider. The input and output
66/// schemas are defined by the provider, and some are executed on the provider's
67/// servers (e.g. web search, code execution).
68#[derive(Debug, Clone, Serialize, Deserialize, TS)]
69#[ts(export)]
70pub struct ProviderTool {
71    /// The ID of the tool, following the format `<provider-id>.<unique-tool-name>`.
72    pub id: String,
73    /// The name of the tool, unique within this model call.
74    pub name: String,
75    /// The arguments for configuring the tool. Must match the expected
76    /// arguments defined by the provider for this tool.
77    pub args: Value,
78}
79
80/// A tool that can be either a user-defined function tool or a
81/// provider-defined tool. Mirrors the V4
82/// `Array<LanguageModelV4FunctionTool | LanguageModelV4ProviderTool>`.
83#[derive(Debug, Clone, Serialize, Deserialize, TS)]
84#[serde(tag = "type", rename_all = "snake_case")]
85#[ts(export)]
86pub enum Tool {
87    /// A user-defined function tool.
88    Function(FunctionTool),
89    /// A provider-defined tool.
90    Provider(ProviderTool),
91}
92
93impl From<FunctionTool> for Tool {
94    fn from(ft: FunctionTool) -> Self {
95        Tool::Function(ft)
96    }
97}
98
99/// A tool call requested by the model.
100#[derive(Debug, Clone, Serialize, Deserialize, TS)]
101#[ts(export)]
102pub struct ToolCall {
103    /// Provider-assigned call id.
104    pub tool_call_id: String,
105    /// Tool name.
106    pub tool_name: String,
107    /// Arguments as a JSON value (usually an object).
108    pub input: Value,
109    /// Whether the tool call will be executed by the provider.
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub provider_executed: Option<bool>,
112    /// Whether the tool is dynamic (defined at runtime, e.g. MCP tools).
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub dynamic: Option<bool>,
115}
116
117/// The result of executing a tool call, to be sent back to the model.
118#[derive(Debug, Clone, Serialize, Deserialize, TS)]
119#[ts(export)]
120pub struct ToolResult {
121    /// Must match the corresponding `ToolCall::tool_call_id`.
122    pub tool_call_id: String,
123    /// The tool's output (usually a JSON-serializable value or plain text).
124    pub result: Value,
125    /// Whether the result is an error or error message.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub is_error: Option<bool>,
128    /// Whether the result is preliminary (replaces prior, e.g. image previews).
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub preliminary: Option<bool>,
131}
132
133/// How the model should choose tools.
134///
135/// Wire format aligns with Vercel AI SDK `toolChoice`:
136/// `"auto" | "none" | "required" | { "type": "tool", "toolName": "..." }`.
137/// This is a mixed tagged/untagged shape (unit variants as bare strings,
138/// `Tool` variant as a tagged object), so serde is implemented by hand.
139#[derive(Debug, Clone, Default, PartialEq, TS)]
140#[ts(
141    export,
142    type = "\"auto\" | \"none\" | \"required\" | { type: \"tool\"; toolName: string }"
143)]
144pub enum ToolChoice {
145    /// Model decides whether to call a tool (default).
146    #[default]
147    Auto,
148    /// Model must not call any tools.
149    None,
150    /// Model must call at least one tool.
151    Required,
152    /// Model must call the specified tool.
153    Tool { tool_name: String },
154}
155
156impl Serialize for ToolChoice {
157    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
158    where
159        S: serde::Serializer,
160    {
161        use serde::ser::SerializeStruct;
162
163        match self {
164            ToolChoice::Auto => serializer.serialize_str("auto"),
165            ToolChoice::None => serializer.serialize_str("none"),
166            ToolChoice::Required => serializer.serialize_str("required"),
167            ToolChoice::Tool { tool_name } => {
168                let mut s = serializer.serialize_struct("ToolChoice", 2)?;
169                s.serialize_field("type", "tool")?;
170                s.serialize_field("toolName", tool_name)?;
171                s.end()
172            }
173        }
174    }
175}
176
177impl<'de> Deserialize<'de> for ToolChoice {
178    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179    where
180        D: serde::Deserializer<'de>,
181    {
182        use serde::de::Error;
183
184        #[derive(Deserialize)]
185        #[serde(untagged)]
186        enum Repr {
187            Str(String),
188            Tool {
189                #[serde(rename = "type")]
190                typ: String,
191                #[serde(rename = "toolName")]
192                tool_name: Option<String>,
193            },
194        }
195
196        match Repr::deserialize(deserializer)? {
197            Repr::Str(s) => match s.as_str() {
198                "auto" => Ok(ToolChoice::Auto),
199                "none" => Ok(ToolChoice::None),
200                "required" => Ok(ToolChoice::Required),
201                other => Err(D::Error::custom(format!(
202                    "unknown toolChoice '{other}' (expected auto/none/required or {{type:\"tool\",toolName}})"
203                ))),
204            },
205            Repr::Tool { typ, tool_name } => {
206                if typ != "tool" {
207                    return Err(D::Error::custom(format!(
208                        "unknown toolChoice type '{typ}' (expected \"tool\")"
209                    )));
210                }
211                let tool_name = tool_name.ok_or_else(|| {
212                    D::Error::custom("toolChoice {type:\"tool\"} requires a toolName field")
213                })?;
214                Ok(ToolChoice::Tool { tool_name })
215            }
216        }
217    }
218}