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    /// Provider-assigned thought signature (e.g. Google Gemini
116    /// `thoughtSignature`). Must be echoed back verbatim on the follow-up turn
117    /// when the tool result is sent; thinking models reject the request
118    /// otherwise.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub thought_signature: Option<String>,
121}
122
123/// The result of executing a tool call, to be sent back to the model.
124#[derive(Debug, Clone, Serialize, Deserialize, TS)]
125#[ts(export)]
126pub struct ToolResult {
127    /// Must match the corresponding `ToolCall::tool_call_id`.
128    pub tool_call_id: String,
129    /// The tool's output (usually a JSON-serializable value or plain text).
130    pub result: Value,
131    /// Whether the result is an error or error message.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub is_error: Option<bool>,
134    /// Whether the result is preliminary (replaces prior, e.g. image previews).
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub preliminary: Option<bool>,
137}
138
139/// How the model should choose tools.
140///
141/// Wire format aligns with Vercel AI SDK `toolChoice`:
142/// `"auto" | "none" | "required" | { "type": "tool", "toolName": "..." }`.
143/// This is a mixed tagged/untagged shape (unit variants as bare strings,
144/// `Tool` variant as a tagged object), so serde is implemented by hand.
145#[derive(Debug, Clone, Default, PartialEq, TS)]
146#[ts(
147    export,
148    type = "\"auto\" | \"none\" | \"required\" | { type: \"tool\"; toolName: string }"
149)]
150pub enum ToolChoice {
151    /// Model decides whether to call a tool (default).
152    #[default]
153    Auto,
154    /// Model must not call any tools.
155    None,
156    /// Model must call at least one tool.
157    Required,
158    /// Model must call the specified tool.
159    Tool { tool_name: String },
160}
161
162impl Serialize for ToolChoice {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: serde::Serializer,
166    {
167        use serde::ser::SerializeStruct;
168
169        match self {
170            ToolChoice::Auto => serializer.serialize_str("auto"),
171            ToolChoice::None => serializer.serialize_str("none"),
172            ToolChoice::Required => serializer.serialize_str("required"),
173            ToolChoice::Tool { tool_name } => {
174                let mut s = serializer.serialize_struct("ToolChoice", 2)?;
175                s.serialize_field("type", "tool")?;
176                s.serialize_field("toolName", tool_name)?;
177                s.end()
178            }
179        }
180    }
181}
182
183impl<'de> Deserialize<'de> for ToolChoice {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        use serde::de::Error;
189
190        #[derive(Deserialize)]
191        #[serde(untagged)]
192        enum Repr {
193            Str(String),
194            Tool {
195                #[serde(rename = "type")]
196                typ: String,
197                #[serde(rename = "toolName")]
198                tool_name: Option<String>,
199            },
200        }
201
202        match Repr::deserialize(deserializer)? {
203            Repr::Str(s) => match s.as_str() {
204                "auto" => Ok(ToolChoice::Auto),
205                "none" => Ok(ToolChoice::None),
206                "required" => Ok(ToolChoice::Required),
207                other => Err(D::Error::custom(format!(
208                    "unknown toolChoice '{other}' (expected auto/none/required or {{type:\"tool\",toolName}})"
209                ))),
210            },
211            Repr::Tool { typ, tool_name } => {
212                if typ != "tool" {
213                    return Err(D::Error::custom(format!(
214                        "unknown toolChoice type '{typ}' (expected \"tool\")"
215                    )));
216                }
217                let tool_name = tool_name.ok_or_else(|| {
218                    D::Error::custom("toolChoice {type:\"tool\"} requires a toolName field")
219                })?;
220                Ok(ToolChoice::Tool { tool_name })
221            }
222        }
223    }
224}