Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Claude 3.7 Sonnet with extended thinking #1370

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
11 changes: 11 additions & 0 deletions crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ pub fn render_message(message: &Message) {
MessageContent::Image(image) => {
println!("Image: [data: {}, type: {}]", image.data, image.mime_type);
}
MessageContent::Thinking(thinking) => {
if std::env::var("GOOSE_CLI_SHOW_THINKING").is_ok() {
println!("\n{}", style("Thinking:").dim().italic());
print_markdown(&thinking.thinking, theme);
}
}
MessageContent::RedactedThinking(_) => {
// For redacted thinking, print thinking was redacted
println!("\n{}", style("Thinking:").dim().italic());
print_markdown("Thinking was redacted", theme);
}
}
}
println!();
Expand Down
32 changes: 32 additions & 0 deletions crates/goose-server/src/routes/reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ fn convert_messages(incoming: Vec<IncomingMessage>) -> Vec<Message> {
}

// Protocol-specific message formatting
// Based on https://sdk.vercel.ai/docs/ai-sdk-ui/stream-protocol#data-stream-protocol
struct ProtocolFormatter;

impl ProtocolFormatter {
Expand Down Expand Up @@ -166,6 +167,25 @@ impl ProtocolFormatter {
format!("3:{}\n", encoded_error)
}

fn format_reasoning(reasoning_text: &str) -> String {
let encoded_text = serde_json::to_string(reasoning_text).unwrap_or_else(|_| String::new());
format!("g:{}\n", encoded_text)
}

fn format_reasoning_signature(signature: &str) -> String {
let response = json!({
"signature": signature
});
format!("j:{}\n", response)
}

fn format_redacted_reasoning(data: &str) -> String {
let response = json!({
"data": data
});
format!("i:{}\n", response)
}

fn format_finish(reason: &str) -> String {
// Finish messages start with "d:"
let finish = json!({
Expand Down Expand Up @@ -247,6 +267,18 @@ async fn stream_message(
.await?;
}
}
MessageContent::Thinking(content) => {
tx.send(ProtocolFormatter::format_reasoning(&content.thinking))
.await?;
tx.send(ProtocolFormatter::format_reasoning_signature(
&content.signature,
))
.await?;
}
MessageContent::RedactedThinking(content) => {
tx.send(ProtocolFormatter::format_redacted_reasoning(&content.data))
.await?;
}
MessageContent::ToolConfirmationRequest(_) => {
// skip tool confirmation requests
}
Expand Down
54 changes: 54 additions & 0 deletions crates/goose/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ pub struct ToolConfirmationRequest {
pub prompt: Option<String>,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ThinkingContent {
pub thinking: String,
pub signature: String,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RedactedThinkingContent {
pub data: String,
}

#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
/// Content passed inside a message, which can be both simple content and tool content
pub enum MessageContent {
Expand All @@ -42,6 +53,8 @@ pub enum MessageContent {
ToolRequest(ToolRequest),
ToolResponse(ToolResponse),
ToolConfirmationRequest(ToolConfirmationRequest),
Thinking(ThinkingContent),
RedactedThinking(RedactedThinkingContent),
}

impl MessageContent {
Expand Down Expand Up @@ -87,6 +100,17 @@ impl MessageContent {
prompt,
})
}

pub fn thinking<S1: Into<String>, S2: Into<String>>(thinking: S1, signature: S2) -> Self {
MessageContent::Thinking(ThinkingContent {
thinking: thinking.into(),
signature: signature.into(),
})
}

pub fn redacted_thinking<S: Into<String>>(data: S) -> Self {
MessageContent::RedactedThinking(RedactedThinkingContent { data: data.into() })
}
pub fn as_tool_request(&self) -> Option<&ToolRequest> {
if let MessageContent::ToolRequest(ref tool_request) = self {
Some(tool_request)
Expand Down Expand Up @@ -133,6 +157,22 @@ impl MessageContent {
_ => None,
}
}

/// Get the thinking content if this is a ThinkingContent variant
pub fn as_thinking(&self) -> Option<&ThinkingContent> {
match self {
MessageContent::Thinking(thinking) => Some(thinking),
_ => None,
}
}

/// Get the redacted thinking content if this is a RedactedThinkingContent variant
pub fn as_redacted_thinking(&self) -> Option<&RedactedThinkingContent> {
match self {
MessageContent::RedactedThinking(redacted) => Some(redacted),
_ => None,
}
}
}

impl From<Content> for MessageContent {
Expand Down Expand Up @@ -222,6 +262,20 @@ impl Message {
))
}

/// Add thinking content to the message
pub fn with_thinking<S1: Into<String>, S2: Into<String>>(
self,
thinking: S1,
signature: S2,
) -> Self {
self.with_content(MessageContent::thinking(thinking, signature))
}

/// Add redacted thinking content to the message
pub fn with_redacted_thinking<S: Into<String>>(self, data: S) -> Self {
self.with_content(MessageContent::redacted_thinking(data))
}

/// Get the concatenated text content of the message, separated by newlines
pub fn as_concat_text(&self) -> String {
self.content
Expand Down
16 changes: 16 additions & 0 deletions crates/goose/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub const ANTHROPIC_KNOWN_MODELS: &[&str] = &[
"claude-3-5-sonnet-latest",
"claude-3-5-haiku-latest",
"claude-3-opus-latest",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
];

pub const ANTHROPIC_DOC_URL: &str = "https://docs.anthropic.com/en/docs/about-claude/models";
Expand Down Expand Up @@ -64,6 +66,13 @@ impl AnthropicProvider {
ProviderError::RequestFailed(format!("Failed to construct endpoint URL: {e}"))
})?;

if std::env::var("GOOSE_DEBUG").is_ok() {
println!(
"\nRequest:\n{}\n",
serde_json::to_string_pretty(&payload).unwrap()
);
}

let response = self
.client
.post(url)
Expand All @@ -76,6 +85,13 @@ impl AnthropicProvider {
let status = response.status();
let payload: Option<Value> = response.json().await.ok();

if std::env::var("GOOSE_DEBUG").is_ok() {
println!(
"\nResponse:\n{}\n",
serde_json::to_string_pretty(&payload).unwrap()
);
}

// https://docs.anthropic.com/en/api/errors
match status {
StatusCode::OK => payload.ok_or_else( || ProviderError::RequestFailed("Response body is not valid JSON".to_string()) ),
Expand Down
Loading
Loading