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
61 changes: 61 additions & 0 deletions crates/goose-provider-types/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use async_trait::async_trait;
use futures::Stream;
use rmcp::model::Tool;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::pin::Pin;

use crate::{
Expand Down Expand Up @@ -216,6 +218,21 @@ impl ConfigKey {
}
}

/// How a model's thinking is replayed back to the provider on subsequent turns.
///
/// Cerebras rejects requests that replay `messages[].reasoning_content`, so such models
/// declare an inline `content` form instead.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingPreservationFormat {
/// Prepend the thinking to the message content as plain text.
ContentPrepend,
/// Prepend the thinking to the message content wrapped in `<think>` tags.
ContentXml,
/// Replay in the separate `reasoning_content` field, the OpenAI-compatible default.
ReasoningContent,
}

/// Information about a model's capabilities
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelInfo {
Expand All @@ -237,6 +254,11 @@ pub struct ModelInfo {
/// Whether this model supports reasoning/thinking controls
#[serde(default)]
pub reasoning: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_preservation_format: Option<ThinkingPreservationFormat>,
/// Static params merged into the request body for this model.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_params: Option<HashMap<String, Value>>,
}

impl ModelInfo {
Expand All @@ -251,6 +273,8 @@ impl ModelInfo {
currency: None,
supports_cache_control: None,
reasoning: false,
thinking_preservation_format: None,
request_params: None,
}
}

Expand All @@ -270,6 +294,8 @@ impl ModelInfo {
currency: Some("$".to_string()),
supports_cache_control: None,
reasoning: false,
thinking_preservation_format: None,
request_params: None,
}
}
}
Expand Down Expand Up @@ -315,6 +341,8 @@ pub fn model_info_for_provider_model(provider_name: &str, model_name: &str) -> M
currency: None,
supports_cache_control: None,
reasoning,
thinking_preservation_format: None,
request_params: None,
}
}

Expand Down Expand Up @@ -775,6 +803,8 @@ mod tests {
currency: None,
supports_cache_control: None,
reasoning: false,
thinking_preservation_format: None,
request_params: None,
};
assert_eq!(info.context_limit, 1000);

Expand All @@ -788,6 +818,8 @@ mod tests {
currency: None,
supports_cache_control: None,
reasoning: false,
thinking_preservation_format: None,
request_params: None,
};
assert_eq!(info, info2);

Expand All @@ -801,10 +833,39 @@ mod tests {
currency: None,
supports_cache_control: None,
reasoning: false,
thinking_preservation_format: None,
request_params: None,
};
assert_ne!(info, info3);
}

#[test]
fn test_model_info_deserializes_thinking_preservation_and_request_params() {
let info: ModelInfo = serde_json::from_str(
r#"{
"name": "zai-glm-4.7",
"context_limit": 131072,
"thinking_preservation_format": "content_xml",
"request_params": {"reasoning_format": "parsed"}
}"#,
)
.unwrap();

assert_eq!(
info.thinking_preservation_format,
Some(ThinkingPreservationFormat::ContentXml)
);
assert_eq!(
info.request_params.unwrap().get("reasoning_format"),
Some(&serde_json::json!("parsed"))
);

let bare: ModelInfo =
serde_json::from_str(r#"{"name": "gpt-4o", "context_limit": 128000}"#).unwrap();
assert_eq!(bare.thinking_preservation_format, None);
assert_eq!(bare.request_params, None);
}

#[test]
fn test_model_info_with_cost() {
let info = ModelInfo::with_cost("gpt-4o", 128000, 0.0000025, 0.00001);
Expand Down
Loading