Skip to content
Closed
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
25 changes: 24 additions & 1 deletion src/llm/rig_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ impl<M: CompletionModel> RigAdapter<M> {

// -- Type conversion helpers --

/// Round an f32 to f64 without precision artifacts.
///
/// Direct `f32 as f64` preserves the binary representation, producing values
/// like `0.699999988079071` instead of `0.7`. Some providers (e.g. Zhipu/GLM)
/// reject these values with a 400 error. Rounding to 6 decimal places removes
/// the artifact while preserving all meaningful precision for temperature/top_p.
fn round_f32_to_f64(val: f32) -> f64 {
let s = format!("{:.6}", val);
s.parse::<f64>().unwrap_or(val as f64)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of unwrap_or with a fallback to val as f64 could silently re-introduce the precision artifact issue if s.parse::<f64>() were to fail for some unforeseen reason. Since format!("{:.6}", val) should always produce a string that can be parsed back into a float, it's safer to treat this as an infallible operation. Using .expect() would cause a panic if parsing fails, making any unexpected behavior immediately obvious during development and testing, which is preferable to silently using the incorrect value.

Suggested change
s.parse::<f64>().unwrap_or(val as f64)
s.parse::<f64>().expect("formatted f32 should always be parsable to f64")
References
  1. Prefer expect() to explicitly fail with a clear message if an operation's setup or expected outcome is not met, especially when unwrap_or() could silently lead to incorrect logic. This principle extends beyond tests to general code robustness, ensuring immediate detection of unexpected behavior.

}

/// Normalize a JSON Schema for OpenAI strict mode compliance.
///
/// OpenAI strict function calling requires:
Expand Down Expand Up @@ -542,7 +553,7 @@ fn build_rig_request(
chat_history,
documents: Vec::new(),
tools,
temperature: temperature.map(|t| t as f64),
temperature: temperature.map(|t| round_f32_to_f64(t)),
max_tokens: max_tokens.map(|t| t as u64),
tool_choice,
additional_params,
Expand Down Expand Up @@ -767,6 +778,18 @@ fn normalize_tool_name(name: &str, known_tools: &HashSet<String>) -> String {
mod tests {
use super::*;

#[test]
fn test_round_f32_to_f64_no_precision_artifacts() {
// Direct f32->f64 cast produces 0.699999988079071 instead of 0.7
assert_eq!(round_f32_to_f64(0.7_f32), 0.7_f64);
assert_eq!(round_f32_to_f64(0.5_f32), 0.5_f64);
assert_eq!(round_f32_to_f64(1.0_f32), 1.0_f64);
assert_eq!(round_f32_to_f64(0.0_f32), 0.0_f64);
// Original cast produces artifacts — our fix should not
assert_ne!(0.7_f32 as f64, 0.7_f64);
assert_eq!(round_f32_to_f64(0.7_f32), 0.7_f64);
}

#[test]
fn test_convert_messages_system_to_preamble() {
let messages = vec![
Expand Down
Loading