Skip to content

Commit 6399689

Browse files
committed
Add ToolChoice enum to control function calling behavior and improve Ollama thinking mode handling
- Add ToolChoice enum (Auto, Required, None) to providers/common.rs for controlling tool execution policy - Extend GenerateOptions with tool_choice field and with_tool_choice() builder method - Update Ollama provider: add note about tool_choice parameter not yet supported by Ollama API - Improve parse_response() in Ollama completion: handle thinking mode content properly, log thinking field when present but
1 parent aa7cd03 commit 6399689

File tree

2 files changed

+36
-0
lines changed

2 files changed

+36
-0
lines changed

machi/src/providers/common.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ pub type ModelStream =
122122
#[cfg(target_arch = "wasm32")]
123123
pub type ModelStream = Pin<Box<dyn Stream<Item = Result<ChatMessageStreamDelta, AgentError>>>>;
124124

125+
/// Tool choice mode for function calling.
126+
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
127+
#[serde(rename_all = "lowercase")]
128+
pub enum ToolChoice {
129+
/// Model decides whether to call tools.
130+
#[default]
131+
Auto,
132+
/// Model must call at least one tool.
133+
Required,
134+
/// Model should not call any tools.
135+
None,
136+
}
137+
125138
/// Options for model generation requests.
126139
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
127140
pub struct GenerateOptions {
@@ -131,6 +144,9 @@ pub struct GenerateOptions {
131144
/// Available tools for function calling.
132145
#[serde(skip_serializing_if = "Option::is_none")]
133146
pub tools: Option<Vec<ToolDefinition>>,
147+
/// Tool choice mode - controls whether model must/can/cannot call tools.
148+
#[serde(skip_serializing_if = "Option::is_none")]
149+
pub tool_choice: Option<ToolChoice>,
134150
/// Temperature for sampling (0.0 to 2.0).
135151
#[serde(skip_serializing_if = "Option::is_none")]
136152
pub temperature: Option<f32>,
@@ -166,6 +182,13 @@ impl GenerateOptions {
166182
self
167183
}
168184

185+
/// Set tool choice mode.
186+
#[must_use]
187+
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
188+
self.tool_choice = Some(choice);
189+
self
190+
}
191+
169192
/// Set temperature.
170193
#[must_use]
171194
pub const fn with_temperature(mut self, temp: f32) -> Self {

machi/src/providers/ollama/completion.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ impl CompletionModel {
201201
// Enable thinking mode for models like qwen3 that need it for tool calling
202202
// When think=true, response has "thinking" field with reasoning and "tool_calls" with calls
203203
body["think"] = serde_json::json!(true);
204+
// Note: Ollama doesn't support tool_choice parameter yet, but we handle
205+
// native tool_calls in parse_response() which takes priority over text parsing
204206
}
205207

206208
body
@@ -209,7 +211,18 @@ impl CompletionModel {
209211
/// Parse the API response into a `ModelResponse`.
210212
fn parse_response(&self, json: Value) -> Result<ModelResponse, AgentError> {
211213
let message_json = &json["message"];
214+
215+
// Get content - in think mode, actual response may be in "thinking" field
212216
let content = message_json["content"].as_str().map(String::from);
217+
218+
// If content is empty but thinking exists, log it for debugging
219+
// The tool_calls should still be present when think=true
220+
if content.as_ref().is_none_or(|c| c.is_empty()) {
221+
if let Some(thinking) = message_json["thinking"].as_str() {
222+
debug!(thinking_len = thinking.len(), "Model returned thinking content");
223+
// Don't use thinking as content - tool_calls should be parsed below
224+
}
225+
}
213226

214227
// Parse tool calls
215228
let tool_calls = if message_json["tool_calls"].is_array() {

0 commit comments

Comments
 (0)