Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,18 +494,14 @@
> Ollama's OpenAI-compatible `/v1/chat/completions` endpoint. Dropped
> five inline structs (`Req`, `Msg`, `Opts`, `Resp`, `RespMsg`).
>
> **Behavioural caveat:** the old Ollama-native payload set
> `options.temperature = 0.0` and `options.num_predict = 16` on the
> classification request. `MessageRequest` exposes `max_tokens` (kept
> as 16) but no `temperature`, so the classifier no longer pins
> determinism. The existing keyword fallback already handles
> misclassifications via the `Unknown label` arm, so accuracy
> degradation surfaces as a fallback rather than a wrong target. Built
> an explicit 8 s `tokio::time::timeout` to replace the old reqwest
> builder timeout, and `with_retry_policy(0, ...)` to preserve
> one-shot semantics. Adding `temperature` to `api::MessageRequest`
> is the proper fix for the determinism regression and is out of
> scope for this PR.
> **Behavioural caveat (resolved 2026-05-20):** the old Ollama-native
> payload set `options.temperature = 0.0` and `options.num_predict = 16`
> on the classification request. `MessageRequest` exposes `max_tokens`
> (kept as 16) but originally had no `temperature`. PR #24 added
> `temperature: Option<f32>` to `api::MessageRequest` and `llm_classify`
> now pins `temperature: Some(0.0)` again. Built an explicit 8 s
> `tokio::time::timeout` to replace the old reqwest builder timeout, and
> `with_retry_policy(0, ...)` to preserve one-shot semantics.
>
> **Still to migrate (with notes on each):**
> 2. `src/grok_reasoning.rs` — uses xAI's `/responses` endpoint
Expand Down
1 change: 1 addition & 0 deletions crates/api/src/prompt_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ mod tests {
system: Some("system".to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/api/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,7 @@ mod tests {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
40 changes: 40 additions & 0 deletions crates/api/src/providers/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ fn build_chat_completion_request(request: &MessageRequest, config: OpenAiCompatC
"stream": request.stream,
});

if let Some(temperature) = request.temperature {
payload["temperature"] = json!(temperature);
}

if request.stream && should_request_stream_usage(config) {
payload["stream_options"] = json!({ "include_usage": true });
}
Expand Down Expand Up @@ -987,6 +991,7 @@ mod tests {
input_schema: json!({"type": "object"}),
}]),
tool_choice: Some(ToolChoice::Auto),
temperature: None,
stream: false,
},
OpenAiCompatConfig::xai(),
Expand All @@ -1009,6 +1014,7 @@ mod tests {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: true,
},
OpenAiCompatConfig::openai(),
Expand All @@ -1017,6 +1023,39 @@ mod tests {
assert_eq!(payload["stream_options"], json!({"include_usage": true}));
}

#[test]
fn temperature_is_included_in_payload_when_set() {
let payload = build_chat_completion_request(
&MessageRequest {
model: "grok-3".to_string(),
max_tokens: 16,
messages: vec![InputMessage::user_text("hi")],
system: None,
tools: None,
tool_choice: None,
temperature: Some(0.0),
stream: false,
},
OpenAiCompatConfig::xai(),
);
assert_eq!(payload["temperature"], json!(0.0));

let payload_none = build_chat_completion_request(
&MessageRequest {
model: "grok-3".to_string(),
max_tokens: 16,
messages: vec![InputMessage::user_text("hi")],
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
},
OpenAiCompatConfig::xai(),
);
assert!(payload_none.get("temperature").is_none());
}

#[test]
fn xai_streaming_requests_skip_openai_specific_usage_opt_in() {
let payload = build_chat_completion_request(
Expand All @@ -1027,6 +1066,7 @@ mod tests {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: true,
},
OpenAiCompatConfig::xai(),
Expand Down
36 changes: 35 additions & 1 deletion crates/api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct MessageRequest {
pub tools: Option<Vec<ToolDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub stream: bool,
}
Expand All @@ -23,6 +25,12 @@ impl MessageRequest {
self.stream = true;
self
}

#[must_use]
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Expand Down Expand Up @@ -248,8 +256,9 @@ pub enum StreamEvent {
#[cfg(test)]
mod tests {
use runtime::format_usd;
use serde_json::Value;

use super::{MessageResponse, Usage};
use super::{InputMessage, MessageRequest, MessageResponse, Usage};

#[test]
fn usage_total_tokens_includes_cache_tokens() {
Expand Down Expand Up @@ -287,4 +296,29 @@ mod tests {
assert_eq!(format_usd(cost.total_cost_usd()), "$54.6750");
assert_eq!(response.total_tokens(), 1_800_000);
}

#[test]
fn temperature_serializes_when_set_and_is_omitted_when_none() {
let mut request = MessageRequest {
model: "claude-sonnet-4-6".to_string(),
max_tokens: 32,
messages: vec![InputMessage::user_text("hi")],
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

// None → field absent from the serialized JSON
let json_none: Value = serde_json::to_value(&request).unwrap();
assert!(json_none.get("temperature").is_none(), "got: {json_none}");

// Builder method sets the field
request = request.with_temperature(0.0);
assert_eq!(request.temperature, Some(0.0));

let json_some: Value = serde_json::to_value(&request).unwrap();
assert_eq!(json_some.get("temperature").and_then(Value::as_f64), Some(0.0));
}
}
2 changes: 2 additions & 0 deletions crates/api/tests/client_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ async fn live_stream_smoke_test() {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
})
.await
Expand Down Expand Up @@ -787,6 +788,7 @@ fn sample_request(stream: bool) -> MessageRequest {
}),
}]),
tool_choice: Some(ToolChoice::Auto),
temperature: None,
stream,
}
}
1 change: 1 addition & 0 deletions crates/api/tests/openai_compat_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ fn sample_request(stream: bool) -> MessageRequest {
}),
}]),
tool_choice: Some(ToolChoice::Auto),
temperature: None,
stream,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/rusty-claude-cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ impl CliApp {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: true,
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/rusty-claude-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4549,6 +4549,7 @@ impl ApiClient for AnthropicRuntimeClient {
.enable_tools
.then(|| filter_tool_specs(&self.tool_registry, self.allowed_tools.as_ref())),
tool_choice: self.enable_tools.then_some(ToolChoice::Auto),
temperature: None,
stream: true,
};

Expand Down
2 changes: 2 additions & 0 deletions crates/rusty-claude-cli/tests/api_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ fn scenario_request(model: &str, scenario: &str) -> MessageRequest {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
}
}
Expand Down Expand Up @@ -129,6 +130,7 @@ fn openai_compat_client_consumes_openai_shaped_response() {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
2 changes: 2 additions & 0 deletions crates/tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3465,6 +3465,7 @@ impl ApiClient for ProviderRuntimeClient {
system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
tools: (!tools.is_empty()).then_some(tools),
tool_choice: (!self.allowed_tools.is_empty()).then_some(ToolChoice::Auto),
temperature: None,
stream: true,
};

Expand Down Expand Up @@ -3548,6 +3549,7 @@ impl ApiClient for ProviderRuntimeClient {
let response = self
.client
.send_message(&MessageRequest {
temperature: None,
stream: false,
..message_request.clone()
})
Expand Down
5 changes: 5 additions & 0 deletions src/agent/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ impl AgentPipeline {
system: Some(system),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -530,6 +531,7 @@ impl AgentPipeline {
system: Some(EXECUTOR_SYSTEM_PROMPT.to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -579,6 +581,7 @@ impl AgentPipeline {
system: Some(EXECUTOR_SYSTEM_PROMPT.to_string()),
tools: Some(tool_defs.clone()),
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -721,6 +724,7 @@ impl AgentPipeline {
system: Some(REVIEWER_SYSTEM_PROMPT.to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -791,6 +795,7 @@ impl AgentPipeline {
system: Some(CONSOLIDATION_SYSTEM_PROMPT.to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
2 changes: 2 additions & 0 deletions src/api/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ async fn handle_streaming(
system,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -1465,6 +1466,7 @@ async fn dispatch_claude(
system: req.system_prompt.clone(),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
1 change: 1 addition & 0 deletions src/grok_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ Code:
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
1 change: 1 addition & 0 deletions src/llm/grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl GrokAnalyzer {
system: Some(system_prompt.to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
11 changes: 7 additions & 4 deletions src/model_router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
//
// Note on the Ollama classifier: `llm_classify` talks to Ollama's
// OpenAI-compatible `/v1/chat/completions` endpoint via `api::OpenAiCompatClient`
// rather than the Ollama-native `/api/chat` endpoint. The compat endpoint
// doesn't surface `temperature` / `options.num_predict` knobs, so the classifier
// no longer pins `temperature: 0.0` — the keyword fallback handles any
// misclassifications via the `Unknown label` arm.
// rather than the Ollama-native `/api/chat` endpoint. The classifier pins
// `temperature: 0.0` for deterministic label selection; `options.num_predict`
// is replaced by `max_tokens: 16` which the compat endpoint translates back to
// the same Ollama option.

use api::{InputMessage, MessageRequest, OpenAiCompatClient, OpenAiCompatConfig, OutputContentBlock};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -253,6 +253,9 @@ RepoQuestion | ArchitecturalReason | CodeReview | Unknown";
system: Some(CLASSIFY_SYSTEM.to_string()),
tools: None,
tool_choice: None,
// Deterministic classification — restored after the
// /api/chat → /v1/chat/completions migration in PR #23.
temperature: Some(0.0),
stream: false,
};

Expand Down
2 changes: 2 additions & 0 deletions src/simple_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl GrokClient {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down Expand Up @@ -100,6 +101,7 @@ impl GrokClient {
system: Some(system.to_string()),
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down
1 change: 1 addition & 0 deletions tests/test_grok_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ async fn test_claude_switch_live_completion() {
system: None,
tools: None,
tool_choice: None,
temperature: None,
stream: false,
};

Expand Down