Separate model reasoning selections and reduce agent notification noise
This commit is contained in:
@@ -2,15 +2,15 @@ use std::collections::HashSet;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::StreamExt;
|
||||
use galaxy_agent_core::{
|
||||
AgentError, AgentErrorKind, AgentEventStream, AgentRuntime, RuntimeCapabilities,
|
||||
RuntimeDescriptor, RuntimeKind, TurnControl, TurnRequest,
|
||||
};
|
||||
use rig_core::client::{CompletionClient, ModelListingClient};
|
||||
use rig_core::providers::gemini;
|
||||
use rig_core::http_client::{self, HttpClientExt};
|
||||
use rig_core::providers::gemini;
|
||||
|
||||
use crate::anthropic_auth::{anthropic_client, prepare_subscription_request, restore_tool_names};
|
||||
use crate::request::build_completion_request;
|
||||
@@ -22,6 +22,7 @@ pub struct RigModelInfo {
|
||||
pub display_name: String,
|
||||
pub context_size: Option<u32>,
|
||||
pub max_output_tokens: Option<u32>,
|
||||
pub reasoning_efforts: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn discover_anthropic_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||
@@ -30,30 +31,48 @@ pub async fn discover_anthropic_models(api_key: &str) -> Result<Vec<RigModelInfo
|
||||
let mut cursors = HashSet::new();
|
||||
let mut path = "/v1/models?limit=1000".to_string();
|
||||
loop {
|
||||
let request = client.get(&path).and_then(|request| Ok(request.body(http_client::NoBody)?))
|
||||
let request = client
|
||||
.get(&path)
|
||||
.and_then(|request| Ok(request.body(http_client::NoBody)?))
|
||||
.map_err(|_| "Could not build the Anthropic models request.".to_string())?;
|
||||
let response = client.send::<_, Vec<u8>>(request).await
|
||||
let response = client
|
||||
.send::<_, Vec<u8>>(request)
|
||||
.await
|
||||
.map_err(|_| "Could not retrieve Anthropic models.".to_string())?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("Anthropic model discovery returned HTTP {}.", response.status()));
|
||||
return Err(format!(
|
||||
"Anthropic model discovery returned HTTP {}.",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
let body = response.into_body().await
|
||||
let body = response
|
||||
.into_body()
|
||||
.await
|
||||
.map_err(|_| "Could not read the Anthropic models response.".to_string())?;
|
||||
let page: serde_json::Value = serde_json::from_slice(&body)
|
||||
.map_err(|_| "Invalid Anthropic models response.".to_string())?;
|
||||
let entries = page["data"].as_array()
|
||||
let entries = page["data"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "Anthropic models response is missing its model list.".to_string())?;
|
||||
for entry in entries {
|
||||
models.push(anthropic_model_info(entry)?);
|
||||
}
|
||||
match page["has_more"].as_bool() {
|
||||
Some(false) => return Ok(models),
|
||||
Some(true) => {},
|
||||
None => return Err("Anthropic models response is missing pagination state.".to_string()),
|
||||
Some(true) => {}
|
||||
None => {
|
||||
return Err("Anthropic models response is missing pagination state.".to_string())
|
||||
}
|
||||
}
|
||||
let cursor = page["last_id"].as_str().filter(|cursor| {
|
||||
!cursor.is_empty() && cursor.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
|
||||
}).ok_or_else(|| "Invalid Anthropic model pagination cursor.".to_string())?;
|
||||
let cursor = page["last_id"]
|
||||
.as_str()
|
||||
.filter(|cursor| {
|
||||
!cursor.is_empty()
|
||||
&& cursor.bytes().all(|byte| {
|
||||
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| "Invalid Anthropic model pagination cursor.".to_string())?;
|
||||
if !cursors.insert(cursor.to_string()) {
|
||||
return Err("Anthropic model pagination did not advance.".to_string());
|
||||
}
|
||||
@@ -62,17 +81,24 @@ pub async fn discover_anthropic_models(api_key: &str) -> Result<Vec<RigModelInfo
|
||||
}
|
||||
|
||||
fn positive_token_limit(value: &serde_json::Value) -> Option<u32> {
|
||||
value.as_u64().and_then(|tokens| u32::try_from(tokens).ok()).filter(|tokens| *tokens > 0)
|
||||
value
|
||||
.as_u64()
|
||||
.and_then(|tokens| u32::try_from(tokens).ok())
|
||||
.filter(|tokens| *tokens > 0)
|
||||
}
|
||||
|
||||
fn anthropic_model_info(entry: &serde_json::Value) -> Result<RigModelInfo, String> {
|
||||
let id = entry["id"].as_str().filter(|id| !id.is_empty())
|
||||
let id = entry["id"]
|
||||
.as_str()
|
||||
.filter(|id| !id.is_empty())
|
||||
.ok_or_else(|| "Anthropic returned a model without an ID.".to_string())?;
|
||||
// Read live limits first. Rig's generic model lister drops these fields.
|
||||
// The models.dev snapshot is metadata only, never an availability allowlist.
|
||||
static LIMITS: OnceLock<serde_json::Value> = OnceLock::new();
|
||||
let limits = LIMITS.get_or_init(|| serde_json::from_str(include_str!("../data/anthropic_limits.json"))
|
||||
.expect("bundled Anthropic limits must be valid JSON"));
|
||||
let limits = LIMITS.get_or_init(|| {
|
||||
serde_json::from_str(include_str!("../data/anthropic_limits.json"))
|
||||
.expect("bundled Anthropic limits must be valid JSON")
|
||||
});
|
||||
let fallback = &limits["models"][id];
|
||||
Ok(RigModelInfo {
|
||||
id: id.to_string(),
|
||||
@@ -81,9 +107,22 @@ fn anthropic_model_info(entry: &serde_json::Value) -> Result<RigModelInfo, Strin
|
||||
.or_else(|| positive_token_limit(&fallback["context"])),
|
||||
max_output_tokens: positive_token_limit(&entry["max_tokens"])
|
||||
.or_else(|| positive_token_limit(&fallback["output"])),
|
||||
reasoning_efforts: anthropic_reasoning_efforts(entry),
|
||||
})
|
||||
}
|
||||
|
||||
fn anthropic_reasoning_efforts(entry: &serde_json::Value) -> Vec<String> {
|
||||
let effort = &entry["capabilities"]["effort"];
|
||||
if effort["supported"].as_bool() != Some(true) {
|
||||
return Vec::new();
|
||||
}
|
||||
["low", "medium", "high", "xhigh", "max"]
|
||||
.into_iter()
|
||||
.filter(|level| effort[*level]["supported"].as_bool() == Some(true))
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn discover_gemini_models(api_key: &str) -> Result<Vec<RigModelInfo>, String> {
|
||||
let client = gemini::Client::new(api_key).map_err(|error| error.to_string())?;
|
||||
let models = client
|
||||
@@ -98,6 +137,7 @@ pub async fn discover_gemini_models(api_key: &str) -> Result<Vec<RigModelInfo>,
|
||||
id: model.id,
|
||||
context_size: model.context_length,
|
||||
max_output_tokens: None,
|
||||
reasoning_efforts: Vec::new(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -125,6 +165,7 @@ pub fn vertex_ai_model_catalog() -> Vec<RigModelInfo> {
|
||||
display_name: display_name.to_string(),
|
||||
context_size: None,
|
||||
max_output_tokens: None,
|
||||
reasoning_efforts: Vec::new(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -168,6 +209,7 @@ pub struct AnthropicRuntime {
|
||||
config: AnthropicRuntimeConfig,
|
||||
descriptor: RuntimeDescriptor,
|
||||
credential_resolver: Option<AnthropicCredentialResolver>,
|
||||
reasoning_effort: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnthropicRuntime {
|
||||
@@ -181,12 +223,18 @@ impl std::fmt::Debug for AnthropicRuntime {
|
||||
}
|
||||
|
||||
impl AnthropicRuntime {
|
||||
pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
|
||||
self.reasoning_effort = effort;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(config: AnthropicRuntimeConfig) -> Self {
|
||||
let descriptor = native_descriptor("anthropic", &config.model);
|
||||
Self {
|
||||
config,
|
||||
descriptor,
|
||||
credential_resolver: None,
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +272,15 @@ impl AgentRuntime for AnthropicRuntime {
|
||||
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||
let mut model = client.completion_model(&self.config.model);
|
||||
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||
let mut completion_request =
|
||||
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||
let mut completion_request = build_completion_request(
|
||||
request,
|
||||
max_output_tokens,
|
||||
true,
|
||||
true,
|
||||
self.reasoning_effort
|
||||
.as_deref()
|
||||
.map(anthropic_reasoning_params),
|
||||
)?;
|
||||
let aliases = if crate::is_claude_subscription_token(&credential) {
|
||||
model = model.with_prompt_caching();
|
||||
Some(prepare_subscription_request(&mut completion_request)?)
|
||||
@@ -256,14 +311,24 @@ pub struct GeminiRuntimeConfig {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GeminiRuntime {
|
||||
reasoning_effort: Option<String>,
|
||||
config: GeminiRuntimeConfig,
|
||||
descriptor: RuntimeDescriptor,
|
||||
}
|
||||
|
||||
impl GeminiRuntime {
|
||||
pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
|
||||
self.reasoning_effort = effort;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(config: GeminiRuntimeConfig) -> Self {
|
||||
let descriptor = native_descriptor("gemini", &config.model);
|
||||
Self { config, descriptor }
|
||||
Self {
|
||||
config,
|
||||
descriptor,
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,8 +353,15 @@ impl AgentRuntime for GeminiRuntime {
|
||||
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||
let model = client.completion_model(&self.config.model);
|
||||
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||
let completion_request =
|
||||
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||
let completion_request = build_completion_request(
|
||||
request,
|
||||
max_output_tokens,
|
||||
true,
|
||||
true,
|
||||
self.reasoning_effort
|
||||
.as_deref()
|
||||
.map(gemini_reasoning_params),
|
||||
)?;
|
||||
start_model_turn(model, completion_request, control, max_output_tokens).await
|
||||
}
|
||||
}
|
||||
@@ -304,14 +376,24 @@ pub struct VertexAiRuntimeConfig {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VertexAiRuntime {
|
||||
reasoning_effort: Option<String>,
|
||||
config: VertexAiRuntimeConfig,
|
||||
descriptor: RuntimeDescriptor,
|
||||
}
|
||||
|
||||
impl VertexAiRuntime {
|
||||
pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
|
||||
self.reasoning_effort = effort;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(config: VertexAiRuntimeConfig) -> Self {
|
||||
let descriptor = native_descriptor("vertex-ai", &config.model);
|
||||
Self { config, descriptor }
|
||||
Self {
|
||||
config,
|
||||
descriptor,
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,12 +425,27 @@ impl AgentRuntime for VertexAiRuntime {
|
||||
.map_err(|error| AgentError::new(AgentErrorKind::Configuration, error.to_string()))?;
|
||||
let model = client.completion_model(&self.config.model);
|
||||
let max_output_tokens = request.max_output_tokens.or(self.config.max_output_tokens);
|
||||
let completion_request =
|
||||
build_completion_request(request, max_output_tokens, true, true, None)?;
|
||||
let completion_request = build_completion_request(
|
||||
request,
|
||||
max_output_tokens,
|
||||
true,
|
||||
true,
|
||||
self.reasoning_effort
|
||||
.as_deref()
|
||||
.map(gemini_reasoning_params),
|
||||
)?;
|
||||
start_model_completion(model, completion_request, control, max_output_tokens).await
|
||||
}
|
||||
}
|
||||
|
||||
fn anthropic_reasoning_params(effort: &str) -> serde_json::Value {
|
||||
serde_json::json!({ "output_config": { "effort": effort } })
|
||||
}
|
||||
|
||||
fn gemini_reasoning_params(effort: &str) -> serde_json::Value {
|
||||
serde_json::json!({ "generationConfig": { "thinkingConfig": { "thinkingLevel": effort } } })
|
||||
}
|
||||
|
||||
fn native_descriptor(provider: &str, model: &str) -> RuntimeDescriptor {
|
||||
RuntimeDescriptor {
|
||||
id: format!("rig-{provider}:{model}"),
|
||||
|
||||
@@ -21,10 +21,67 @@ fn anthropic_discovery_prefers_live_limits_over_catalog_defaults() {
|
||||
fn anthropic_discovery_uses_exact_catalog_limits_when_api_omits_them() {
|
||||
let model = super::anthropic_model_info(&serde_json::json!({
|
||||
"id": "claude-opus-4-6", "max_input_tokens": null, "max_tokens": null
|
||||
})).unwrap();
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(model.context_size, Some(1000000));
|
||||
assert_eq!(model.max_output_tokens, Some(128000));
|
||||
let unknown = super::anthropic_model_info(&serde_json::json!({"id": "claude-unknown"})).unwrap();
|
||||
let unknown =
|
||||
super::anthropic_model_info(&serde_json::json!({"id": "claude-unknown"})).unwrap();
|
||||
assert_eq!(unknown.context_size, None);
|
||||
assert_eq!(unknown.max_output_tokens, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_discovery_reads_only_supported_effort_capabilities() {
|
||||
let model = super::anthropic_model_info(&serde_json::json!({
|
||||
"id": "claude-future", "capabilities": {"effort": {
|
||||
"supported": true, "low": {"supported": true},
|
||||
"medium": {"supported": false}, "high": {"supported": true},
|
||||
"xhigh": {"supported": true}, "max": {"supported": false}
|
||||
}}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(model.reasoning_efforts, ["low", "high", "xhigh"]);
|
||||
assert!(super::anthropic_reasoning_efforts(&serde_json::json!({
|
||||
"capabilities": {"effort": {"supported": false, "low": {"supported": true}}}
|
||||
}))
|
||||
.is_empty());
|
||||
assert!(super::anthropic_reasoning_efforts(&serde_json::json!({
|
||||
"id": "claude-opus-4-6"
|
||||
}))
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_reasoning_selections_use_provider_request_fields() {
|
||||
assert_eq!(
|
||||
super::anthropic_reasoning_params("high"),
|
||||
serde_json::json!({
|
||||
"output_config": {"effort": "high"}
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
super::gemini_reasoning_params("low"),
|
||||
serde_json::json!({
|
||||
"generationConfig": {"thinkingConfig": {"thinkingLevel": "low"}}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_reasoning_parameters_survive_rig_typed_conversion() {
|
||||
use rig_core::providers::gemini::completion::gemini_api_types::{
|
||||
AdditionalParameters, ThinkingLevel,
|
||||
};
|
||||
let params: AdditionalParameters =
|
||||
serde_json::from_value(super::gemini_reasoning_params("medium")).unwrap();
|
||||
assert_eq!(
|
||||
params
|
||||
.generation_config
|
||||
.unwrap()
|
||||
.thinking_config
|
||||
.unwrap()
|
||||
.thinking_level,
|
||||
Some(ThinkingLevel::Medium)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,15 @@ pub struct OpenAICompatibleRuntimeConfig {
|
||||
pub struct OpenAICompatibleRuntime {
|
||||
config: OpenAICompatibleRuntimeConfig,
|
||||
descriptor: RuntimeDescriptor,
|
||||
reasoning_effort: Option<String>,
|
||||
}
|
||||
|
||||
impl OpenAICompatibleRuntime {
|
||||
pub fn with_reasoning_effort(mut self, effort: Option<String>) -> Self {
|
||||
self.reasoning_effort = effort;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new(config: OpenAICompatibleRuntimeConfig) -> Self {
|
||||
let descriptor = RuntimeDescriptor {
|
||||
id: format!("rig-openai-compatible:{}", config.model),
|
||||
@@ -33,7 +39,11 @@ impl OpenAICompatibleRuntime {
|
||||
kind: RuntimeKind::Provider,
|
||||
capabilities: RuntimeCapabilities::provider(),
|
||||
};
|
||||
Self { config, descriptor }
|
||||
Self {
|
||||
config,
|
||||
descriptor,
|
||||
reasoning_effort: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +72,7 @@ impl AgentRuntime for OpenAICompatibleRuntime {
|
||||
control,
|
||||
self.config.max_output_tokens,
|
||||
self.config.supports_system_messages,
|
||||
self.reasoning_effort.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -73,6 +84,7 @@ async fn start_model_turn<M>(
|
||||
control: TurnControl,
|
||||
configured_max_output_tokens: Option<u64>,
|
||||
supports_system_messages: bool,
|
||||
reasoning_effort: Option<&str>,
|
||||
) -> Result<AgentEventStream, AgentError>
|
||||
where
|
||||
M: CompletionModel + Send + Sync + 'static,
|
||||
@@ -82,6 +94,7 @@ where
|
||||
request,
|
||||
configured_max_output_tokens,
|
||||
supports_system_messages,
|
||||
reasoning_effort,
|
||||
)?;
|
||||
start_provider_model_turn(model, completion_request, control, max_output_tokens).await
|
||||
}
|
||||
@@ -90,15 +103,18 @@ fn build_completion_request(
|
||||
request: TurnRequest,
|
||||
configured_max_output_tokens: Option<u64>,
|
||||
supports_system_messages: bool,
|
||||
reasoning_effort: Option<&str>,
|
||||
) -> Result<CompletionRequest, AgentError> {
|
||||
let mut params = serde_json::json!({ "stream_options": { "include_usage": true } });
|
||||
if let Some(effort) = reasoning_effort {
|
||||
params["reasoning_effort"] = effort.into();
|
||||
}
|
||||
build_provider_completion_request(
|
||||
request,
|
||||
configured_max_output_tokens,
|
||||
supports_system_messages,
|
||||
true,
|
||||
Some(serde_json::json!({
|
||||
"stream_options": { "include_usage": true }
|
||||
})),
|
||||
Some(params),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn rig_stream_maps_reasoning_text_usage_and_stop() {
|
||||
let model = client.completion_model("test-model");
|
||||
let (_, control) = galaxy_agent_core::turn_control();
|
||||
|
||||
let events = start_model_turn(model, text_request(), control, None, true)
|
||||
let events = start_model_turn(model, text_request(), control, None, true, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -104,7 +104,7 @@ async fn rig_stream_ignores_empty_reasoning_heartbeats() {
|
||||
let model = client.completion_model("test-model");
|
||||
let (_, control) = galaxy_agent_core::turn_control();
|
||||
|
||||
let events = start_model_turn(model, text_request(), control, None, true)
|
||||
let events = start_model_turn(model, text_request(), control, None, true, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -118,11 +118,9 @@ async fn rig_stream_ignores_empty_reasoning_heartbeats() {
|
||||
AgentEvent::ReasoningCompleted { text, signature }
|
||||
if text.is_empty() && signature.is_none()
|
||||
)));
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AgentEvent::TextDelta { text } if text == "done"))
|
||||
);
|
||||
assert!(events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AgentEvent::TextDelta { text } if text == "done")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -176,7 +174,7 @@ async fn usage_at_the_requested_limit_maps_to_max_tokens() {
|
||||
let mut request = text_request();
|
||||
request.max_output_tokens = Some(6);
|
||||
|
||||
let events = start_model_turn(model, request, control, None, true)
|
||||
let events = start_model_turn(model, request, control, None, true, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -219,7 +217,7 @@ async fn rig_stream_maps_complete_tool_call_without_executing_it() {
|
||||
input_schema: serde_json::json!({"type": "object"}),
|
||||
});
|
||||
|
||||
let events = start_model_turn(model, request, control, None, true)
|
||||
let events = start_model_turn(model, request, control, None, true, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -274,7 +272,7 @@ fn request_conversion_preserves_history_tools_and_limits() {
|
||||
}),
|
||||
});
|
||||
|
||||
let converted = build_completion_request(request, Some(999), true).unwrap();
|
||||
let converted = build_completion_request(request, Some(999), true, None).unwrap();
|
||||
|
||||
assert_eq!(converted.max_tokens, Some(123));
|
||||
assert_eq!(converted.tools.len(), 1);
|
||||
@@ -318,7 +316,7 @@ fn request_conversion_preserves_tool_call_and_denied_result_for_the_next_turn()
|
||||
],
|
||||
);
|
||||
|
||||
let converted = build_completion_request(request, None, true).unwrap();
|
||||
let converted = build_completion_request(request, None, true, None).unwrap();
|
||||
let messages = converted.chat_history.iter().collect::<Vec<_>>();
|
||||
let Message::Assistant { content, .. } = messages[0] else {
|
||||
panic!("expected assistant tool call");
|
||||
@@ -352,7 +350,7 @@ fn request_conversion_places_system_prompt_in_user_message_when_system_role_is_u
|
||||
let mut request = text_request();
|
||||
request.system_prompt = Some("Be useful".to_string());
|
||||
|
||||
let converted = build_completion_request(request, None, false).unwrap();
|
||||
let converted = build_completion_request(request, None, false, None).unwrap();
|
||||
let messages = converted.chat_history.iter().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(messages.len(), 2);
|
||||
@@ -371,3 +369,34 @@ fn request_conversion_places_system_prompt_in_user_message_when_system_role_is_u
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selected_reasoning_effort_is_sent_without_changing_the_model() {
|
||||
let request = build_completion_request(text_request(), None, true, Some("high")).unwrap();
|
||||
assert_eq!(
|
||||
request.additional_params,
|
||||
Some(serde_json::json!({
|
||||
"stream_options": {"include_usage": true}, "reasoning_effort": "high"
|
||||
}))
|
||||
);
|
||||
let default = build_completion_request(text_request(), None, true, None).unwrap();
|
||||
assert!(default
|
||||
.additional_params
|
||||
.unwrap()
|
||||
.get("reasoning_effort")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_survives_openai_request_serialization() {
|
||||
let request = build_completion_request(text_request(), None, true, Some("low")).unwrap();
|
||||
let wire = rig_core::providers::openai::completion::CompletionRequest::try_from((
|
||||
"test-model".to_string(),
|
||||
request,
|
||||
))
|
||||
.unwrap();
|
||||
let json = serde_json::to_value(wire).unwrap();
|
||||
assert_eq!(json["model"], "test-model");
|
||||
assert_eq!(json["reasoning_effort"], "low");
|
||||
assert_eq!(json["stream_options"]["include_usage"], true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user