Download crates/forge_app/src/dto/anthropic/request.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 28.8 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/dto/anthropic/request.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_app/src/dto/anthropic/request.rs
-
curl -L -o request.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/dto/anthropic/request.rs
28.8 kB
| use derive_setters::Setters; | |
| use forge_domain::{ContextMessage, Image}; | |
| use serde::{Deserialize, Serialize}; | |
| pub struct Request { | |
| pub max_tokens: u64, | |
| pub messages: Vec<Message>, | |
| pub model: Option<String>, | |
| pub metadata: Option<Metadata>, | |
| pub stop_sequence: Option<String>, | |
| pub stream: Option<bool>, | |
| pub system: Option<Vec<SystemMessage>>, | |
| pub temperature: Option<f32>, | |
| pub tool_choice: Option<ToolChoice>, | |
| pub tools: Vec<ToolDefinition>, | |
| pub top_k: Option<u64>, | |
| pub top_p: Option<f32>, | |
| pub thinking: Option<Thinking>, | |
| pub output_config: Option<OutputConfig>, | |
| pub output_format: Option<OutputFormat>, | |
| pub anthropic_version: Option<String>, | |
| } | |
| pub struct SystemMessage { | |
| pub r#type: String, | |
| pub text: String, | |
| pub cache_control: Option<CacheControl>, | |
| } | |
| impl SystemMessage { | |
| pub fn cached(mut self, cached: bool) -> Self { | |
| self.cache_control = if cached { | |
| Some(CacheControl::Ephemeral) | |
| } else { | |
| None | |
| }; | |
| self | |
| } | |
| pub fn is_cached(&self) -> bool { | |
| self.cache_control.is_some() | |
| } | |
| } | |
| /// Anthropic's `thinking` request field. Opus 4.7 rejects the `Enabled` shape | |
| /// and the orchestrator applies model-specific reasoning normalization before | |
| /// request conversion. | |
| pub enum Thinking { | |
| Enabled { | |
| budget_tokens: u64, | |
| }, | |
| Adaptive { | |
| display: Option<ThinkingDisplay>, | |
| }, | |
| Disabled, | |
| } | |
| /// On Opus 4.7 adaptive thinking content is omitted from responses unless | |
| /// `Summarized` is requested explicitly. | |
| pub enum ThinkingDisplay { | |
| Summarized, | |
| Omitted, | |
| } | |
| pub enum OutputEffort { | |
| Low, | |
| Medium, | |
| High, | |
| XHigh, | |
| Max, | |
| } | |
| pub struct OutputConfig { | |
| pub effort: OutputEffort, | |
| } | |
| pub enum OutputFormat { | |
| JsonSchema { schema: schemars::Schema }, | |
| } | |
| impl TryFrom<forge_domain::Context> for Request { | |
| type Error = anyhow::Error; | |
| fn try_from(request: forge_domain::Context) -> std::result::Result<Self, Self::Error> { | |
| let system_messages = request | |
| .messages | |
| .iter() | |
| .filter_map(|msg| match &**msg { | |
| ContextMessage::Text(msg) if msg.has_role(forge_domain::Role::System) => { | |
| Some(SystemMessage { | |
| r#type: "text".to_string(), | |
| text: msg.content.clone(), | |
| cache_control: None, | |
| }) | |
| } | |
| _ => None, | |
| }) | |
| .collect::<Vec<_>>(); | |
| // Gate on the domain rule so inherited configs with `enabled: None` but | |
| // a positive effort / `max_tokens` still emit reasoning on the wire. | |
| let reasoning_on = request.is_reasoning_supported(); | |
| let (thinking, output_config) = if reasoning_on && let Some(reasoning) = request.reasoning { | |
| // Adaptive thinking on 4.7 hides reasoning content by default; opting | |
| // into reasoning should surface it unless the caller set `exclude`. | |
| let adaptive_display = if reasoning.exclude == Some(true) { | |
| Some(ThinkingDisplay::Omitted) | |
| } else { | |
| Some(ThinkingDisplay::Summarized) | |
| }; | |
| let thinking = if let Some(budget) = reasoning.max_tokens { | |
| Thinking::Enabled { budget_tokens: budget as u64 } | |
| } else { | |
| Thinking::Adaptive { display: adaptive_display } | |
| }; | |
| // `Effort::None` is an explicit opt-out; `is_reasoning_supported` | |
| // already filters it, but guard here so it can never become a stray | |
| // `output_config.effort`. | |
| let output_config = reasoning.effort.and_then(|effort| { | |
| let output_effort = match effort { | |
| forge_domain::Effort::None => return None, | |
| forge_domain::Effort::Minimal | forge_domain::Effort::Low => OutputEffort::Low, | |
| forge_domain::Effort::Medium => OutputEffort::Medium, | |
| forge_domain::Effort::High => OutputEffort::High, | |
| forge_domain::Effort::XHigh => OutputEffort::XHigh, | |
| forge_domain::Effort::Max => OutputEffort::Max, | |
| }; | |
| Some(OutputConfig { effort: output_effort }) | |
| }); | |
| (Some(thinking), output_config) | |
| } else { | |
| (None, None) | |
| }; | |
| Ok(Self { | |
| messages: request | |
| .messages | |
| .into_iter() | |
| .filter(|message| !message.has_role(forge_domain::Role::System)) | |
| .map(|msg| Message::try_from(msg.message)) | |
| .collect::<std::result::Result<Vec<_>, _>>()?, | |
| tools: request | |
| .tools | |
| .into_iter() | |
| .map(ToolDefinition::try_from) | |
| .collect::<std::result::Result<Vec<_>, _>>()?, | |
| system: Some(system_messages), | |
| temperature: request.temperature.map(|t| t.value()), | |
| top_p: request.top_p.map(|t| t.value()), | |
| top_k: request.top_k.map(|t| t.value() as u64), | |
| tool_choice: request.tool_choice.map(ToolChoice::from), | |
| stream: Some(request.stream.unwrap_or(true)), | |
| thinking, | |
| output_config, | |
| output_format: request.response_format.and_then(|rf| match rf { | |
| forge_domain::ResponseFormat::Text => { | |
| // Anthropic doesn't have a "text" output format, so we skip it | |
| None | |
| } | |
| forge_domain::ResponseFormat::JsonSchema(schema) => { | |
| Some(OutputFormat::JsonSchema { schema: *schema }) | |
| } | |
| }), | |
| ..Default::default() | |
| }) | |
| } | |
| } | |
| impl Request { | |
| /// Get a reference to the messages | |
| pub fn get_messages(&self) -> &[Message] { | |
| &self.messages | |
| } | |
| /// Get a mutable reference to the messages | |
| pub fn get_messages_mut(&mut self) -> &mut Vec<Message> { | |
| &mut self.messages | |
| } | |
| } | |
| pub struct Metadata { | |
| pub user_id: Option<String>, | |
| } | |
| pub struct Message { | |
| pub content: Vec<Content>, | |
| pub role: Role, | |
| } | |
| impl TryFrom<ContextMessage> for Message { | |
| type Error = anyhow::Error; | |
| fn try_from(value: ContextMessage) -> std::result::Result<Self, Self::Error> { | |
| Ok(match value { | |
| ContextMessage::Text(chat_message) => { | |
| let mut content = Vec::with_capacity( | |
| chat_message | |
| .tool_calls | |
| .as_ref() | |
| .map(|tc| tc.len()) | |
| .unwrap_or_default() | |
| + 1, | |
| ); | |
| if let Some(reasoning) = chat_message.reasoning_details | |
| && let Some((sig, text)) = reasoning.into_iter().find_map(|reasoning| { | |
| match (reasoning.signature, reasoning.text) { | |
| (Some(sig), Some(text)) => Some((sig, text)), | |
| _ => None, | |
| } | |
| }) | |
| { | |
| content.push(Content::Thinking { signature: Some(sig), thinking: Some(text) }); | |
| } | |
| if !chat_message.content.is_empty() { | |
| // NOTE: Anthropic does not allow empty text content. | |
| content.push(Content::Text { text: chat_message.content, cache_control: None }); | |
| } | |
| if let Some(tool_calls) = chat_message.tool_calls { | |
| for tool_call in tool_calls { | |
| content.push(tool_call.try_into()?); | |
| } | |
| } | |
| match chat_message.role { | |
| forge_domain::Role::User => Message { role: Role::User, content }, | |
| forge_domain::Role::Assistant => Message { role: Role::Assistant, content }, | |
| forge_domain::Role::System => { | |
| // note: Anthropic doesn't support system role messages and they're already | |
| // filtered out. so this state is unreachable. | |
| return Err( | |
| forge_domain::Error::UnsupportedRole("System".to_string()).into() | |
| ); | |
| } | |
| } | |
| } | |
| ContextMessage::Tool(tool_result) => { | |
| Message { role: Role::User, content: vec![tool_result.try_into()?] } | |
| } | |
| ContextMessage::Image(img) => { | |
| Message { content: vec![Content::from(img)], role: Role::User } | |
| } | |
| }) | |
| } | |
| } | |
| impl Message { | |
| pub fn cached(mut self, enable_cache: bool) -> Self { | |
| // Reset cache control on all content items first | |
| for content in &mut self.content { | |
| *content = std::mem::take(content).cached(false); | |
| } | |
| // If enabling cache, set cache control on the last cacheable content item | |
| if enable_cache | |
| && let Some(last_cacheable_idx) = | |
| self.content | |
| .iter() | |
| .enumerate() | |
| .rev() | |
| .find_map(|(idx, content)| match content { | |
| Content::Text { .. } | |
| | Content::Image { .. } | |
| | Content::ToolUse { .. } | |
| | Content::ToolResult { .. } => Some(idx), | |
| _ => None, | |
| }) | |
| && let Some(content) = self.content.get_mut(last_cacheable_idx) | |
| { | |
| *content = std::mem::take(content).cached(true); | |
| } | |
| self | |
| } | |
| pub fn is_cached(&self) -> bool { | |
| self.content.iter().any(|content| content.is_cached()) | |
| } | |
| } | |
| impl Default for Message { | |
| fn default() -> Self { | |
| Message { content: vec![], role: Role::User } | |
| } | |
| } | |
| impl From<Image> for Content { | |
| fn from(value: Image) -> Self { | |
| Content::Image { | |
| source: ImageSource { | |
| type_: "base64".to_string(), | |
| media_type: Some(value.mime_type().to_string()), | |
| data: Some(value.data().into()), | |
| url: None, | |
| }, | |
| cache_control: None, | |
| } | |
| } | |
| } | |
| pub struct ImageSource { | |
| pub type_: String, | |
| pub media_type: Option<String>, | |
| pub data: Option<String>, | |
| pub url: Option<String>, | |
| } | |
| pub enum Content { | |
| Image { | |
| source: ImageSource, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| Text { | |
| text: String, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| ToolUse { | |
| id: String, | |
| input: Option<serde_json::Value>, | |
| name: String, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| ToolResult { | |
| tool_use_id: String, | |
| content: Option<String>, | |
| is_error: Option<bool>, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| Thinking { | |
| signature: Option<String>, | |
| thinking: Option<String>, | |
| }, | |
| } | |
| impl Default for Content { | |
| fn default() -> Self { | |
| Content::Thinking { signature: None, thinking: None } | |
| } | |
| } | |
| impl Content { | |
| pub fn cached(self, enable_cache: bool) -> Self { | |
| let cache_control = enable_cache.then_some(CacheControl::Ephemeral); | |
| match self { | |
| Content::Text { text, .. } => Content::Text { text, cache_control }, | |
| Content::ToolUse { id, input, name, .. } => { | |
| Content::ToolUse { id, input, name, cache_control } | |
| } | |
| Content::ToolResult { tool_use_id, content, is_error, .. } => { | |
| Content::ToolResult { tool_use_id, content, is_error, cache_control } | |
| } | |
| Content::Image { source, .. } => Content::Image { source, cache_control }, | |
| // TODO: verify this Thinking variants don't support cache control | |
| Content::Thinking { signature, thinking } => Content::Thinking { signature, thinking }, | |
| } | |
| } | |
| pub fn is_cached(&self) -> bool { | |
| match self { | |
| Content::Text { cache_control, .. } => cache_control.is_some(), | |
| Content::ToolUse { cache_control, .. } => cache_control.is_some(), | |
| Content::ToolResult { cache_control, .. } => cache_control.is_some(), | |
| Content::Image { cache_control, .. } => cache_control.is_some(), | |
| Content::Thinking { .. } => false, | |
| } | |
| } | |
| } | |
| impl TryFrom<forge_domain::ToolCallFull> for Content { | |
| type Error = anyhow::Error; | |
| fn try_from(value: forge_domain::ToolCallFull) -> std::result::Result<Self, Self::Error> { | |
| let call_id = value | |
| .call_id | |
| .as_ref() | |
| .ok_or(forge_domain::Error::ToolCallMissingId)?; | |
| Ok(Content::ToolUse { | |
| id: call_id.as_str().to_string(), | |
| input: serde_json::to_value(value.arguments).ok(), | |
| name: value.name.to_string(), | |
| cache_control: None, | |
| }) | |
| } | |
| } | |
| impl TryFrom<forge_domain::ToolResult> for Content { | |
| type Error = anyhow::Error; | |
| fn try_from(value: forge_domain::ToolResult) -> std::result::Result<Self, Self::Error> { | |
| let call_id = value | |
| .call_id | |
| .as_ref() | |
| .ok_or(forge_domain::Error::ToolCallMissingId)?; | |
| Ok(Content::ToolResult { | |
| tool_use_id: call_id.as_str().to_string(), | |
| cache_control: None, | |
| content: value | |
| .output | |
| .values | |
| .iter() | |
| .filter_map(|item| item.as_str().map(|s| s.to_string())) | |
| .next(), | |
| is_error: Some(value.is_error()), | |
| }) | |
| } | |
| } | |
| pub enum CacheControl { | |
| Ephemeral, | |
| } | |
| pub enum Role { | |
| User, | |
| Assistant, | |
| } | |
| pub enum ToolChoice { | |
| Auto { | |
| disable_parallel_tool_use: Option<bool>, | |
| }, | |
| Any { | |
| disable_parallel_tool_use: Option<bool>, | |
| }, | |
| Tool { | |
| name: String, | |
| disable_parallel_tool_use: Option<bool>, | |
| }, | |
| } | |
| // To understand the mappings refer: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#controlling-claudes-output | |
| impl From<forge_domain::ToolChoice> for ToolChoice { | |
| fn from(value: forge_domain::ToolChoice) -> Self { | |
| match value { | |
| forge_domain::ToolChoice::Auto => ToolChoice::Auto { disable_parallel_tool_use: None }, | |
| forge_domain::ToolChoice::Call(tool_name) => { | |
| ToolChoice::Tool { name: tool_name.to_string(), disable_parallel_tool_use: None } | |
| } | |
| forge_domain::ToolChoice::Required => { | |
| ToolChoice::Any { disable_parallel_tool_use: None } | |
| } | |
| forge_domain::ToolChoice::None => ToolChoice::Auto { disable_parallel_tool_use: None }, | |
| } | |
| } | |
| } | |
| pub struct ToolDefinition { | |
| pub name: String, | |
| pub description: Option<String>, | |
| pub cache_control: Option<CacheControl>, | |
| pub input_schema: serde_json::Value, | |
| } | |
| impl TryFrom<forge_domain::ToolDefinition> for ToolDefinition { | |
| type Error = anyhow::Error; | |
| fn try_from(value: forge_domain::ToolDefinition) -> std::result::Result<Self, Self::Error> { | |
| Ok(ToolDefinition { | |
| name: value.name.to_string(), | |
| description: Some(value.description), | |
| cache_control: None, | |
| input_schema: serde_json::to_value(value.input_schema)?, | |
| }) | |
| } | |
| } | |
| mod tests { | |
| use forge_domain::{Context, ReasoningConfig}; | |
| use pretty_assertions::assert_eq; | |
| use super::*; | |
| fn test_thinking_enabled_serializes_with_budget() { | |
| let thinking = Thinking::Enabled { budget_tokens: 5000 }; | |
| let actual = serde_json::to_value(&thinking).unwrap(); | |
| let expected = serde_json::json!({ | |
| "type": "enabled", | |
| "budget_tokens": 5000 | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_thinking_adaptive_serializes_without_display_when_none() { | |
| let thinking = Thinking::Adaptive { display: None }; | |
| let actual = serde_json::to_value(&thinking).unwrap(); | |
| let expected = serde_json::json!({"type": "adaptive"}); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_thinking_adaptive_serializes_with_summarized_display() { | |
| let thinking = Thinking::Adaptive { display: Some(ThinkingDisplay::Summarized) }; | |
| let actual = serde_json::to_value(&thinking).unwrap(); | |
| let expected = serde_json::json!({ | |
| "type": "adaptive", | |
| "display": "summarized" | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_thinking_disabled_serializes() { | |
| let thinking = Thinking::Disabled; | |
| let actual = serde_json::to_value(&thinking).unwrap(); | |
| let expected = serde_json::json!({"type": "disabled"}); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_reasoning_enabled_with_max_tokens_creates_enabled_thinking() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(true), | |
| max_tokens: Some(8000), | |
| effort: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Enabled { budget_tokens: 8000 }) | |
| ); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_reasoning_max_tokens_and_effort_emit_both() { | |
| // Effort and budget are independent knobs — neither should hide the other. | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| effort: Some(forge_domain::Effort::Low), | |
| enabled: Some(true), | |
| max_tokens: Some(8000), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Enabled { budget_tokens: 8000 }) | |
| ); | |
| assert_eq!( | |
| actual.output_config, | |
| Some(OutputConfig { effort: OutputEffort::Low }) | |
| ); | |
| } | |
| fn test_reasoning_max_tokens_alone_emits_enabled_only() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| effort: None, | |
| enabled: Some(true), | |
| max_tokens: Some(8000), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Enabled { budget_tokens: 8000 }) | |
| ); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_reasoning_effort_without_budget_creates_output_config_and_adaptive() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| effort: Some(forge_domain::Effort::Low), | |
| enabled: Some(true), | |
| max_tokens: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.output_config, | |
| Some(OutputConfig { effort: OutputEffort::Low }) | |
| ); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Adaptive { display: Some(ThinkingDisplay::Summarized) }) | |
| ); | |
| } | |
| fn test_reasoning_effort_with_exclude_emits_adaptive_omitted() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| effort: Some(forge_domain::Effort::High), | |
| enabled: Some(true), | |
| max_tokens: None, | |
| exclude: Some(true), | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Adaptive { display: Some(ThinkingDisplay::Omitted) }) | |
| ); | |
| } | |
| fn test_reasoning_xhigh_effort_maps_to_xhigh() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| effort: Some(forge_domain::Effort::XHigh), | |
| enabled: Some(true), | |
| max_tokens: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.output_config, | |
| Some(OutputConfig { effort: OutputEffort::XHigh }) | |
| ); | |
| } | |
| fn test_reasoning_enabled_without_budget_or_effort_defaults_to_adaptive_summarized() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(true), | |
| max_tokens: None, | |
| effort: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Adaptive { display: Some(ThinkingDisplay::Summarized) }) | |
| ); | |
| } | |
| fn test_reasoning_enabled_with_exclude_uses_omitted_display() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(true), | |
| max_tokens: None, | |
| effort: None, | |
| exclude: Some(true), | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Adaptive { display: Some(ThinkingDisplay::Omitted) }) | |
| ); | |
| } | |
| fn test_reasoning_disabled_does_not_create_thinking() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(false), | |
| max_tokens: Some(8000), | |
| effort: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| } | |
| fn test_reasoning_enabled_none_with_max_tokens_still_emits_thinking() { | |
| // Matches the domain's `is_reasoning_supported` rule: enabled: None with a | |
| // positive budget counts as on, so inherited/merged configs don't silently | |
| // disable reasoning on the wire. | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: None, | |
| max_tokens: Some(8000), | |
| effort: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Enabled { budget_tokens: 8000 }) | |
| ); | |
| } | |
| fn test_reasoning_enabled_none_with_effort_still_emits_output_config() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: None, | |
| max_tokens: None, | |
| effort: Some(forge_domain::Effort::High), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!( | |
| actual.output_config, | |
| Some(OutputConfig { effort: OutputEffort::High }) | |
| ); | |
| assert_eq!( | |
| actual.thinking, | |
| Some(Thinking::Adaptive { display: Some(ThinkingDisplay::Summarized) }) | |
| ); | |
| } | |
| fn test_reasoning_enabled_none_with_zero_max_tokens_does_not_emit() { | |
| // Matches `is_reasoning_supported`: max_tokens > 0 is required. | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: None, | |
| max_tokens: Some(0), | |
| effort: None, | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_reasoning_effort_none_does_not_emit_anything() { | |
| // Effort::None is an explicit opt-out — no thinking, no output_config. | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: None, | |
| max_tokens: None, | |
| effort: Some(forge_domain::Effort::None), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_reasoning_effort_none_overrides_enabled_and_max_tokens() { | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(true), | |
| max_tokens: Some(8000), | |
| effort: Some(forge_domain::Effort::None), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_reasoning_enabled_false_overrides_effort() { | |
| // Explicit opt-out beats inferred enablement. | |
| let fixture = Context::default().reasoning(ReasoningConfig { | |
| enabled: Some(false), | |
| max_tokens: None, | |
| effort: Some(forge_domain::Effort::High), | |
| exclude: None, | |
| }); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| assert_eq!(actual.output_config, None); | |
| } | |
| fn test_no_reasoning_config_does_not_create_thinking() { | |
| let fixture = Context::default(); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.thinking, None); | |
| } | |
| fn test_context_conversion_stream_defaults_to_true() { | |
| let fixture = Context::default(); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.stream, Some(true)); | |
| } | |
| fn test_context_conversion_stream_explicit_true() { | |
| let fixture = Context::default().stream(true); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.stream, Some(true)); | |
| } | |
| fn test_context_conversion_stream_explicit_false() { | |
| let fixture = Context::default().stream(false); | |
| let actual = Request::try_from(fixture).unwrap(); | |
| assert_eq!(actual.stream, Some(false)); | |
| } | |
| } | |