feat(runtime): add reasoning toggle for ollama
This commit is contained in:
parent
8f13fee4a6
commit
a5d7911923
10 changed files with 289 additions and 31 deletions
|
|
@ -50,6 +50,18 @@ Notes:
|
||||||
- Setting `max_tool_iterations = 0` falls back to safe default `10`.
|
- Setting `max_tool_iterations = 0` falls back to safe default `10`.
|
||||||
- If a channel message exceeds this value, the runtime returns: `Agent exceeded maximum tool iterations (<value>)`.
|
- If a channel message exceeds this value, the runtime returns: `Agent exceeded maximum tool iterations (<value>)`.
|
||||||
|
|
||||||
|
## `[runtime]`
|
||||||
|
|
||||||
|
| Key | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `reasoning_enabled` | unset (`None`) | Global reasoning/thinking override for providers that support explicit controls |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `reasoning_enabled = false` explicitly disables provider-side reasoning for supported providers (currently `ollama`, via request field `think: false`).
|
||||||
|
- `reasoning_enabled = true` explicitly requests reasoning for supported providers (`think: true` on `ollama`).
|
||||||
|
- Unset keeps provider defaults.
|
||||||
|
|
||||||
## `[gateway]`
|
## `[gateway]`
|
||||||
|
|
||||||
| Key | Default | Purpose |
|
| Key | Default | Purpose |
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ credential is not reused for fallback providers.
|
||||||
- Cross-region inference profiles supported (e.g., `us.anthropic.claude-*`).
|
- Cross-region inference profiles supported (e.g., `us.anthropic.claude-*`).
|
||||||
- Model IDs use Bedrock format: `anthropic.claude-sonnet-4-6`, `anthropic.claude-opus-4-6-v1`, etc.
|
- Model IDs use Bedrock format: `anthropic.claude-sonnet-4-6`, `anthropic.claude-opus-4-6-v1`, etc.
|
||||||
|
|
||||||
|
### Ollama Reasoning Toggle
|
||||||
|
|
||||||
|
You can control Ollama reasoning/thinking behavior from `config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[runtime]
|
||||||
|
reasoning_enabled = false
|
||||||
|
```
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- `false`: sends `think: false` to Ollama `/api/chat` requests.
|
||||||
|
- `true`: sends `think: true`.
|
||||||
|
- Unset: omits `think` and keeps Ollama/model defaults.
|
||||||
|
|
||||||
### Kimi Code Notes
|
### Kimi Code Notes
|
||||||
|
|
||||||
- Provider ID: `kimi-code`
|
- Provider ID: `kimi-code`
|
||||||
|
|
|
||||||
|
|
@ -1191,13 +1191,21 @@ pub async fn run(
|
||||||
.or(config.default_model.as_deref())
|
.or(config.default_model.as_deref())
|
||||||
.unwrap_or("anthropic/claude-sonnet-4");
|
.unwrap_or("anthropic/claude-sonnet-4");
|
||||||
|
|
||||||
let provider: Box<dyn Provider> = providers::create_routed_provider(
|
let provider_runtime_options = providers::ProviderRuntimeOptions {
|
||||||
|
auth_profile_override: None,
|
||||||
|
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
||||||
|
secrets_encrypt: config.secrets.encrypt,
|
||||||
|
reasoning_enabled: config.runtime.reasoning_enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider: Box<dyn Provider> = providers::create_routed_provider_with_options(
|
||||||
provider_name,
|
provider_name,
|
||||||
config.api_key.as_deref(),
|
config.api_key.as_deref(),
|
||||||
config.api_url.as_deref(),
|
config.api_url.as_deref(),
|
||||||
&config.reliability,
|
&config.reliability,
|
||||||
&config.model_routes,
|
&config.model_routes,
|
||||||
model_name,
|
model_name,
|
||||||
|
&provider_runtime_options,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
observer.record_event(&ObserverEvent::AgentStart {
|
observer.record_event(&ObserverEvent::AgentStart {
|
||||||
|
|
@ -1632,13 +1640,20 @@ pub async fn process_message(config: Config, message: &str) -> Result<String> {
|
||||||
.default_model
|
.default_model
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into());
|
.unwrap_or_else(|| "anthropic/claude-sonnet-4-20250514".into());
|
||||||
let provider: Box<dyn Provider> = providers::create_routed_provider(
|
let provider_runtime_options = providers::ProviderRuntimeOptions {
|
||||||
|
auth_profile_override: None,
|
||||||
|
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
||||||
|
secrets_encrypt: config.secrets.encrypt,
|
||||||
|
reasoning_enabled: config.runtime.reasoning_enabled,
|
||||||
|
};
|
||||||
|
let provider: Box<dyn Provider> = providers::create_routed_provider_with_options(
|
||||||
provider_name,
|
provider_name,
|
||||||
config.api_key.as_deref(),
|
config.api_key.as_deref(),
|
||||||
config.api_url.as_deref(),
|
config.api_url.as_deref(),
|
||||||
&config.reliability,
|
&config.reliability,
|
||||||
&config.model_routes,
|
&config.model_routes,
|
||||||
&model_name,
|
&model_name,
|
||||||
|
&provider_runtime_options,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let hardware_rag: Option<crate::rag::HardwareRag> = config
|
let hardware_rag: Option<crate::rag::HardwareRag> = config
|
||||||
|
|
|
||||||
|
|
@ -1672,6 +1672,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
|
||||||
auth_profile_override: None,
|
auth_profile_override: None,
|
||||||
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
||||||
secrets_encrypt: config.secrets.encrypt,
|
secrets_encrypt: config.secrets.encrypt,
|
||||||
|
reasoning_enabled: config.runtime.reasoning_enabled,
|
||||||
};
|
};
|
||||||
let provider: Arc<dyn Provider> = Arc::from(providers::create_resilient_provider_with_options(
|
let provider: Arc<dyn Provider> = Arc::from(providers::create_resilient_provider_with_options(
|
||||||
&provider_name,
|
&provider_name,
|
||||||
|
|
|
||||||
|
|
@ -1607,6 +1607,13 @@ pub struct RuntimeConfig {
|
||||||
/// Docker runtime settings (used when `kind = "docker"`).
|
/// Docker runtime settings (used when `kind = "docker"`).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub docker: DockerRuntimeConfig,
|
pub docker: DockerRuntimeConfig,
|
||||||
|
|
||||||
|
/// Global reasoning override for providers that expose explicit controls.
|
||||||
|
/// - `None`: provider default behavior
|
||||||
|
/// - `Some(true)`: request reasoning/thinking when supported
|
||||||
|
/// - `Some(false)`: disable reasoning/thinking when supported
|
||||||
|
#[serde(default)]
|
||||||
|
pub reasoning_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
@ -1679,6 +1686,7 @@ impl Default for RuntimeConfig {
|
||||||
Self {
|
Self {
|
||||||
kind: default_runtime_kind(),
|
kind: default_runtime_kind(),
|
||||||
docker: DockerRuntimeConfig::default(),
|
docker: DockerRuntimeConfig::default(),
|
||||||
|
reasoning_enabled: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2979,6 +2987,18 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reasoning override: ZEROCLAW_REASONING_ENABLED or REASONING_ENABLED
|
||||||
|
if let Ok(flag) = std::env::var("ZEROCLAW_REASONING_ENABLED")
|
||||||
|
.or_else(|_| std::env::var("REASONING_ENABLED"))
|
||||||
|
{
|
||||||
|
let normalized = flag.trim().to_ascii_lowercase();
|
||||||
|
match normalized.as_str() {
|
||||||
|
"1" | "true" | "yes" | "on" => self.runtime.reasoning_enabled = Some(true),
|
||||||
|
"0" | "false" | "no" | "off" => self.runtime.reasoning_enabled = Some(false),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Web search enabled: ZEROCLAW_WEB_SEARCH_ENABLED or WEB_SEARCH_ENABLED
|
// Web search enabled: ZEROCLAW_WEB_SEARCH_ENABLED or WEB_SEARCH_ENABLED
|
||||||
if let Ok(enabled) = std::env::var("ZEROCLAW_WEB_SEARCH_ENABLED")
|
if let Ok(enabled) = std::env::var("ZEROCLAW_WEB_SEARCH_ENABLED")
|
||||||
.or_else(|_| std::env::var("WEB_SEARCH_ENABLED"))
|
.or_else(|_| std::env::var("WEB_SEARCH_ENABLED"))
|
||||||
|
|
@ -3560,6 +3580,19 @@ connect_timeout_secs = 12
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_reasoning_enabled_deserializes() {
|
||||||
|
let raw = r#"
|
||||||
|
default_temperature = 0.7
|
||||||
|
|
||||||
|
[runtime]
|
||||||
|
reasoning_enabled = false
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let parsed: Config = toml::from_str(raw).unwrap();
|
||||||
|
assert_eq!(parsed.runtime.reasoning_enabled, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
async fn agent_config_defaults() {
|
async fn agent_config_defaults() {
|
||||||
let cfg = AgentConfig::default();
|
let cfg = AgentConfig::default();
|
||||||
|
|
@ -5001,6 +5034,36 @@ default_model = "legacy-model"
|
||||||
std::env::remove_var("ZEROCLAW_TEMPERATURE");
|
std::env::remove_var("ZEROCLAW_TEMPERATURE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn env_override_reasoning_enabled() {
|
||||||
|
let _env_guard = env_override_lock().await;
|
||||||
|
let mut config = Config::default();
|
||||||
|
assert_eq!(config.runtime.reasoning_enabled, None);
|
||||||
|
|
||||||
|
std::env::set_var("ZEROCLAW_REASONING_ENABLED", "false");
|
||||||
|
config.apply_env_overrides();
|
||||||
|
assert_eq!(config.runtime.reasoning_enabled, Some(false));
|
||||||
|
|
||||||
|
std::env::set_var("ZEROCLAW_REASONING_ENABLED", "true");
|
||||||
|
config.apply_env_overrides();
|
||||||
|
assert_eq!(config.runtime.reasoning_enabled, Some(true));
|
||||||
|
|
||||||
|
std::env::remove_var("ZEROCLAW_REASONING_ENABLED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
async fn env_override_reasoning_invalid_value_ignored() {
|
||||||
|
let _env_guard = env_override_lock().await;
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.runtime.reasoning_enabled = Some(false);
|
||||||
|
|
||||||
|
std::env::set_var("ZEROCLAW_REASONING_ENABLED", "maybe");
|
||||||
|
config.apply_env_overrides();
|
||||||
|
assert_eq!(config.runtime.reasoning_enabled, Some(false));
|
||||||
|
|
||||||
|
std::env::remove_var("ZEROCLAW_REASONING_ENABLED");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
async fn env_override_invalid_port_ignored() {
|
async fn env_override_invalid_port_ignored() {
|
||||||
let _env_guard = env_override_lock().await;
|
let _env_guard = env_override_lock().await;
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,7 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
|
||||||
auth_profile_override: None,
|
auth_profile_override: None,
|
||||||
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
zeroclaw_dir: config.config_path.parent().map(std::path::PathBuf::from),
|
||||||
secrets_encrypt: config.secrets.encrypt,
|
secrets_encrypt: config.secrets.encrypt,
|
||||||
|
reasoning_enabled: config.runtime.reasoning_enabled,
|
||||||
},
|
},
|
||||||
)?);
|
)?);
|
||||||
let model = config
|
let model = config
|
||||||
|
|
|
||||||
|
|
@ -630,6 +630,7 @@ pub struct ProviderRuntimeOptions {
|
||||||
pub auth_profile_override: Option<String>,
|
pub auth_profile_override: Option<String>,
|
||||||
pub zeroclaw_dir: Option<PathBuf>,
|
pub zeroclaw_dir: Option<PathBuf>,
|
||||||
pub secrets_encrypt: bool,
|
pub secrets_encrypt: bool,
|
||||||
|
pub reasoning_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProviderRuntimeOptions {
|
impl Default for ProviderRuntimeOptions {
|
||||||
|
|
@ -638,6 +639,7 @@ impl Default for ProviderRuntimeOptions {
|
||||||
auth_profile_override: None,
|
auth_profile_override: None,
|
||||||
zeroclaw_dir: None,
|
zeroclaw_dir: None,
|
||||||
secrets_encrypt: true,
|
secrets_encrypt: true,
|
||||||
|
reasoning_enabled: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -865,16 +867,26 @@ pub fn create_provider_with_options(
|
||||||
"openai-codex" | "openai_codex" | "codex" => {
|
"openai-codex" | "openai_codex" | "codex" => {
|
||||||
Ok(Box::new(openai_codex::OpenAiCodexProvider::new(options)))
|
Ok(Box::new(openai_codex::OpenAiCodexProvider::new(options)))
|
||||||
}
|
}
|
||||||
_ => create_provider_with_url(name, api_key, None),
|
_ => create_provider_with_url_and_options(name, api_key, None, options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factory: create the right provider from config with optional custom base URL
|
/// Factory: create the right provider from config with optional custom base URL
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn create_provider_with_url(
|
pub fn create_provider_with_url(
|
||||||
name: &str,
|
name: &str,
|
||||||
api_key: Option<&str>,
|
api_key: Option<&str>,
|
||||||
api_url: Option<&str>,
|
api_url: Option<&str>,
|
||||||
|
) -> anyhow::Result<Box<dyn Provider>> {
|
||||||
|
create_provider_with_url_and_options(name, api_key, api_url, &ProviderRuntimeOptions::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factory: create provider with optional base URL and runtime options.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn create_provider_with_url_and_options(
|
||||||
|
name: &str,
|
||||||
|
api_key: Option<&str>,
|
||||||
|
api_url: Option<&str>,
|
||||||
|
options: &ProviderRuntimeOptions,
|
||||||
) -> anyhow::Result<Box<dyn Provider>> {
|
) -> anyhow::Result<Box<dyn Provider>> {
|
||||||
let qwen_oauth_context = is_qwen_oauth_alias(name).then(|| resolve_qwen_oauth_context(api_key));
|
let qwen_oauth_context = is_qwen_oauth_alias(name).then(|| resolve_qwen_oauth_context(api_key));
|
||||||
|
|
||||||
|
|
@ -895,7 +907,11 @@ pub fn create_provider_with_url(
|
||||||
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))),
|
"anthropic" => Ok(Box::new(anthropic::AnthropicProvider::new(key))),
|
||||||
"openai" => Ok(Box::new(openai::OpenAiProvider::with_base_url(api_url, key))),
|
"openai" => Ok(Box::new(openai::OpenAiProvider::with_base_url(api_url, key))),
|
||||||
// Ollama uses api_url for custom base URL (e.g. remote Ollama instance)
|
// Ollama uses api_url for custom base URL (e.g. remote Ollama instance)
|
||||||
"ollama" => Ok(Box::new(ollama::OllamaProvider::new(api_url, key))),
|
"ollama" => Ok(Box::new(ollama::OllamaProvider::new_with_reasoning(
|
||||||
|
api_url,
|
||||||
|
key,
|
||||||
|
options.reasoning_enabled,
|
||||||
|
))),
|
||||||
"gemini" | "google" | "google-gemini" => {
|
"gemini" | "google" | "google-gemini" => {
|
||||||
Ok(Box::new(gemini::GeminiProvider::new(key)))
|
Ok(Box::new(gemini::GeminiProvider::new(key)))
|
||||||
}
|
}
|
||||||
|
|
@ -1109,7 +1125,7 @@ pub fn create_resilient_provider_with_options(
|
||||||
"openai-codex" | "openai_codex" | "codex" => {
|
"openai-codex" | "openai_codex" | "codex" => {
|
||||||
create_provider_with_options(primary_name, api_key, options)?
|
create_provider_with_options(primary_name, api_key, options)?
|
||||||
}
|
}
|
||||||
_ => create_provider_with_url(primary_name, api_key, api_url)?,
|
_ => create_provider_with_url_and_options(primary_name, api_key, api_url, options)?,
|
||||||
};
|
};
|
||||||
providers.push((primary_name.to_string(), primary_provider));
|
providers.push((primary_name.to_string(), primary_provider));
|
||||||
|
|
||||||
|
|
@ -1159,9 +1175,36 @@ pub fn create_routed_provider(
|
||||||
reliability: &crate::config::ReliabilityConfig,
|
reliability: &crate::config::ReliabilityConfig,
|
||||||
model_routes: &[crate::config::ModelRouteConfig],
|
model_routes: &[crate::config::ModelRouteConfig],
|
||||||
default_model: &str,
|
default_model: &str,
|
||||||
|
) -> anyhow::Result<Box<dyn Provider>> {
|
||||||
|
create_routed_provider_with_options(
|
||||||
|
primary_name,
|
||||||
|
api_key,
|
||||||
|
api_url,
|
||||||
|
reliability,
|
||||||
|
model_routes,
|
||||||
|
default_model,
|
||||||
|
&ProviderRuntimeOptions::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a routed provider using explicit runtime options.
|
||||||
|
pub fn create_routed_provider_with_options(
|
||||||
|
primary_name: &str,
|
||||||
|
api_key: Option<&str>,
|
||||||
|
api_url: Option<&str>,
|
||||||
|
reliability: &crate::config::ReliabilityConfig,
|
||||||
|
model_routes: &[crate::config::ModelRouteConfig],
|
||||||
|
default_model: &str,
|
||||||
|
options: &ProviderRuntimeOptions,
|
||||||
) -> anyhow::Result<Box<dyn Provider>> {
|
) -> anyhow::Result<Box<dyn Provider>> {
|
||||||
if model_routes.is_empty() {
|
if model_routes.is_empty() {
|
||||||
return create_resilient_provider(primary_name, api_key, api_url, reliability);
|
return create_resilient_provider_with_options(
|
||||||
|
primary_name,
|
||||||
|
api_key,
|
||||||
|
api_url,
|
||||||
|
reliability,
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect unique provider names needed
|
// Collect unique provider names needed
|
||||||
|
|
@ -1187,7 +1230,7 @@ pub fn create_routed_provider(
|
||||||
let key = routed_credential.or(api_key);
|
let key = routed_credential.or(api_key);
|
||||||
// Only use api_url for the primary provider
|
// Only use api_url for the primary provider
|
||||||
let url = if name == primary_name { api_url } else { None };
|
let url = if name == primary_name { api_url } else { None };
|
||||||
match create_resilient_provider(name, key, url, reliability) {
|
match create_resilient_provider_with_options(name, key, url, reliability, options) {
|
||||||
Ok(provider) => providers.push((name.clone(), provider)),
|
Ok(provider) => providers.push((name.clone(), provider)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if name == primary_name {
|
if name == primary_name {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use std::collections::HashMap;
|
||||||
pub struct OllamaProvider {
|
pub struct OllamaProvider {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
|
reasoning_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Request Structures ───────────────────────────────────────────────────────
|
// ─── Request Structures ───────────────────────────────────────────────────────
|
||||||
|
|
@ -18,6 +19,8 @@ struct ChatRequest {
|
||||||
stream: bool,
|
stream: bool,
|
||||||
options: Options,
|
options: Options,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
think: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
tools: Option<Vec<serde_json::Value>>,
|
tools: Option<Vec<serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,6 +88,14 @@ struct OllamaFunction {
|
||||||
|
|
||||||
impl OllamaProvider {
|
impl OllamaProvider {
|
||||||
pub fn new(base_url: Option<&str>, api_key: Option<&str>) -> Self {
|
pub fn new(base_url: Option<&str>, api_key: Option<&str>) -> Self {
|
||||||
|
Self::new_with_reasoning(base_url, api_key, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_reasoning(
|
||||||
|
base_url: Option<&str>,
|
||||||
|
api_key: Option<&str>,
|
||||||
|
reasoning_enabled: Option<bool>,
|
||||||
|
) -> Self {
|
||||||
let api_key = api_key.and_then(|value| {
|
let api_key = api_key.and_then(|value| {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||||
|
|
@ -96,6 +107,7 @@ impl OllamaProvider {
|
||||||
.trim_end_matches('/')
|
.trim_end_matches('/')
|
||||||
.to_string(),
|
.to_string(),
|
||||||
api_key,
|
api_key,
|
||||||
|
reasoning_enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +149,23 @@ impl OllamaProvider {
|
||||||
serde_json::from_str(arguments).unwrap_or_else(|_| serde_json::json!({}))
|
serde_json::from_str(arguments).unwrap_or_else(|_| serde_json::json!({}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_chat_request(
|
||||||
|
&self,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
model: &str,
|
||||||
|
temperature: f64,
|
||||||
|
tools: Option<&[serde_json::Value]>,
|
||||||
|
) -> ChatRequest {
|
||||||
|
ChatRequest {
|
||||||
|
model: model.to_string(),
|
||||||
|
messages,
|
||||||
|
stream: false,
|
||||||
|
options: Options { temperature },
|
||||||
|
think: self.reasoning_enabled,
|
||||||
|
tools: tools.map(|t| t.to_vec()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert internal chat history format to Ollama's native tool-call message schema.
|
/// Convert internal chat history format to Ollama's native tool-call message schema.
|
||||||
///
|
///
|
||||||
/// `run_tool_call_loop` stores native assistant/tool entries as JSON strings in
|
/// `run_tool_call_loop` stores native assistant/tool entries as JSON strings in
|
||||||
|
|
@ -235,22 +264,17 @@ impl OllamaProvider {
|
||||||
should_auth: bool,
|
should_auth: bool,
|
||||||
tools: Option<&[serde_json::Value]>,
|
tools: Option<&[serde_json::Value]>,
|
||||||
) -> anyhow::Result<ApiChatResponse> {
|
) -> anyhow::Result<ApiChatResponse> {
|
||||||
let request = ChatRequest {
|
let request = self.build_chat_request(messages, model, temperature, tools);
|
||||||
model: model.to_string(),
|
|
||||||
messages,
|
|
||||||
stream: false,
|
|
||||||
options: Options { temperature },
|
|
||||||
tools: tools.map(|t| t.to_vec()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = format!("{}/api/chat", self.base_url);
|
let url = format!("{}/api/chat", self.base_url);
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Ollama request: url={} model={} message_count={} temperature={} tool_count={}",
|
"Ollama request: url={} model={} message_count={} temperature={} think={:?} tool_count={}",
|
||||||
url,
|
url,
|
||||||
model,
|
model,
|
||||||
request.messages.len(),
|
request.messages.len(),
|
||||||
temperature,
|
temperature,
|
||||||
|
request.think,
|
||||||
request.tools.as_ref().map_or(0, |t| t.len()),
|
request.tools.as_ref().map_or(0, |t| t.len()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -645,6 +669,44 @@ mod tests {
|
||||||
assert!(!should_auth);
|
assert!(!should_auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_omits_think_when_reasoning_not_configured() {
|
||||||
|
let provider = OllamaProvider::new(None, None);
|
||||||
|
let request = provider.build_chat_request(
|
||||||
|
vec![Message {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: Some("hello".to_string()),
|
||||||
|
tool_calls: None,
|
||||||
|
tool_name: None,
|
||||||
|
}],
|
||||||
|
"llama3",
|
||||||
|
0.7,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_value(request).unwrap();
|
||||||
|
assert!(json.get("think").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_includes_think_when_reasoning_configured() {
|
||||||
|
let provider = OllamaProvider::new_with_reasoning(None, None, Some(false));
|
||||||
|
let request = provider.build_chat_request(
|
||||||
|
vec![Message {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: Some("hello".to_string()),
|
||||||
|
tool_calls: None,
|
||||||
|
tool_name: None,
|
||||||
|
}],
|
||||||
|
"llama3",
|
||||||
|
0.7,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let json = serde_json::to_value(request).unwrap();
|
||||||
|
assert_eq!(json.get("think"), Some(&serde_json::json!(false)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn response_deserializes() {
|
fn response_deserializes() {
|
||||||
let json = r#"{"message":{"role":"assistant","content":"Hello from Ollama!"}}"#;
|
let json = r#"{"message":{"role":"assistant","content":"Hello from Ollama!"}}"#;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ pub struct DelegateTool {
|
||||||
security: Arc<SecurityPolicy>,
|
security: Arc<SecurityPolicy>,
|
||||||
/// Global credential fallback (from config.api_key)
|
/// Global credential fallback (from config.api_key)
|
||||||
fallback_credential: Option<String>,
|
fallback_credential: Option<String>,
|
||||||
|
/// Provider runtime options inherited from root config.
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions,
|
||||||
/// Depth at which this tool instance lives in the delegation chain.
|
/// Depth at which this tool instance lives in the delegation chain.
|
||||||
depth: u32,
|
depth: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -30,11 +32,26 @@ impl DelegateTool {
|
||||||
agents: HashMap<String, DelegateAgentConfig>,
|
agents: HashMap<String, DelegateAgentConfig>,
|
||||||
fallback_credential: Option<String>,
|
fallback_credential: Option<String>,
|
||||||
security: Arc<SecurityPolicy>,
|
security: Arc<SecurityPolicy>,
|
||||||
|
) -> Self {
|
||||||
|
Self::new_with_options(
|
||||||
|
agents,
|
||||||
|
fallback_credential,
|
||||||
|
security,
|
||||||
|
providers::ProviderRuntimeOptions::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_options(
|
||||||
|
agents: HashMap<String, DelegateAgentConfig>,
|
||||||
|
fallback_credential: Option<String>,
|
||||||
|
security: Arc<SecurityPolicy>,
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
agents: Arc::new(agents),
|
agents: Arc::new(agents),
|
||||||
security,
|
security,
|
||||||
fallback_credential,
|
fallback_credential,
|
||||||
|
provider_runtime_options,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +64,28 @@ impl DelegateTool {
|
||||||
fallback_credential: Option<String>,
|
fallback_credential: Option<String>,
|
||||||
security: Arc<SecurityPolicy>,
|
security: Arc<SecurityPolicy>,
|
||||||
depth: u32,
|
depth: u32,
|
||||||
|
) -> Self {
|
||||||
|
Self::with_depth_and_options(
|
||||||
|
agents,
|
||||||
|
fallback_credential,
|
||||||
|
security,
|
||||||
|
depth,
|
||||||
|
providers::ProviderRuntimeOptions::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_depth_and_options(
|
||||||
|
agents: HashMap<String, DelegateAgentConfig>,
|
||||||
|
fallback_credential: Option<String>,
|
||||||
|
security: Arc<SecurityPolicy>,
|
||||||
|
depth: u32,
|
||||||
|
provider_runtime_options: providers::ProviderRuntimeOptions,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
agents: Arc::new(agents),
|
agents: Arc::new(agents),
|
||||||
security,
|
security,
|
||||||
fallback_credential,
|
fallback_credential,
|
||||||
|
provider_runtime_options,
|
||||||
depth,
|
depth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -190,20 +224,23 @@ impl Tool for DelegateTool {
|
||||||
#[allow(clippy::option_as_ref_deref)]
|
#[allow(clippy::option_as_ref_deref)]
|
||||||
let provider_credential = provider_credential_owned.as_ref().map(String::as_str);
|
let provider_credential = provider_credential_owned.as_ref().map(String::as_str);
|
||||||
|
|
||||||
let provider: Box<dyn Provider> =
|
let provider: Box<dyn Provider> = match providers::create_provider_with_options(
|
||||||
match providers::create_provider(&agent_config.provider, provider_credential) {
|
&agent_config.provider,
|
||||||
Ok(p) => p,
|
provider_credential,
|
||||||
Err(e) => {
|
&self.provider_runtime_options,
|
||||||
return Ok(ToolResult {
|
) {
|
||||||
success: false,
|
Ok(p) => p,
|
||||||
output: String::new(),
|
Err(e) => {
|
||||||
error: Some(format!(
|
return Ok(ToolResult {
|
||||||
"Failed to create provider '{}' for agent '{agent_name}': {e}",
|
success: false,
|
||||||
agent_config.provider
|
output: String::new(),
|
||||||
)),
|
error: Some(format!(
|
||||||
});
|
"Failed to create provider '{}' for agent '{agent_name}': {e}",
|
||||||
}
|
agent_config.provider
|
||||||
};
|
)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Build the message
|
// Build the message
|
||||||
let full_prompt = if context.is_empty() {
|
let full_prompt = if context.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -227,10 +227,19 @@ pub fn all_tools_with_runtime(
|
||||||
let trimmed_value = value.trim();
|
let trimmed_value = value.trim();
|
||||||
(!trimmed_value.is_empty()).then(|| trimmed_value.to_owned())
|
(!trimmed_value.is_empty()).then(|| trimmed_value.to_owned())
|
||||||
});
|
});
|
||||||
tools.push(Box::new(DelegateTool::new(
|
tools.push(Box::new(DelegateTool::new_with_options(
|
||||||
delegate_agents,
|
delegate_agents,
|
||||||
delegate_fallback_credential,
|
delegate_fallback_credential,
|
||||||
security.clone(),
|
security.clone(),
|
||||||
|
crate::providers::ProviderRuntimeOptions {
|
||||||
|
auth_profile_override: None,
|
||||||
|
zeroclaw_dir: root_config
|
||||||
|
.config_path
|
||||||
|
.parent()
|
||||||
|
.map(std::path::PathBuf::from),
|
||||||
|
secrets_encrypt: root_config.secrets.encrypt,
|
||||||
|
reasoning_enabled: root_config.runtime.reasoning_enabled,
|
||||||
|
},
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue