feat: add support for google reasoning

kujtimiihoxha created

Change summary

go.mod                        |  2 +-
internal/agent/agent.go       |  6 ++++++
internal/agent/coordinator.go |  7 +++++++
internal/agent/tools/ls.go    |  2 +-
internal/message/content.go   | 33 ++++++++++++++++++++++++++++-----
5 files changed, 43 insertions(+), 7 deletions(-)

Detailed changes

go.mod 🔗

@@ -47,7 +47,7 @@ require (
 	mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5
 )
 
-replace charm.land/fantasy => ../fantasy/
+replace charm.land/fantasy => ../../fantasy/main/
 
 require (
 	cloud.google.com/go v0.116.0 // indirect

internal/agent/agent.go 🔗

@@ -15,6 +15,7 @@ import (
 	"charm.land/fantasy"
 	"charm.land/fantasy/providers/anthropic"
 	"charm.land/fantasy/providers/bedrock"
+	"charm.land/fantasy/providers/google"
 	"charm.land/fantasy/providers/openai"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 	"github.com/charmbracelet/crush/internal/agent/tools"
@@ -243,6 +244,11 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy
 					currentAssistant.AppendReasoningSignature(reasoning.Signature)
 				}
 			}
+			if googleData, ok := reasoning.ProviderMetadata[google.Name]; ok {
+				if reasoning, ok := googleData.(*google.ReasoningMetadata); ok {
+					currentAssistant.AppendReasoningSignature(reasoning.Signature)
+				}
+			}
 			if openaiData, ok := reasoning.ProviderMetadata[openai.Name]; ok {
 				if reasoning, ok := openaiData.(*openai.ResponsesReasoningMetadata); ok {
 					currentAssistant.SetReasoningResponsesData(reasoning)

internal/agent/coordinator.go 🔗

@@ -218,6 +218,13 @@ func getProviderOptions(model Model, tp catwalk.Type) fantasy.ProviderOptions {
 			options[openrouter.Name] = parsed
 		}
 	case google.Name:
+		_, hasReasoning := mergedOptions["thinking_config"]
+		if !hasReasoning {
+			mergedOptions["thinking_config"] = map[string]any{
+				"thinking_budget":  2000,
+				"include_thoughts": true,
+			}
+		}
 		parsed, err := google.ParseOptions(mergedOptions)
 		if err == nil {
 			options[google.Name] = parsed

internal/agent/tools/ls.go 🔗

@@ -16,7 +16,7 @@ import (
 )
 
 type LSParams struct {
-	Path   string   `json:"path" description:"The path to the directory to list (defaults to current working directory)"`
+	Path   string   `json:"path,omitempty" description:"The path to the directory to list (defaults to current working directory)"`
 	Ignore []string `json:"ignore,omitempty" description:"List of glob patterns to ignore"`
 	Depth  int      `json:"depth,omitempty" description:"The maximum depth to traverse"`
 }

internal/message/content.go 🔗

@@ -9,6 +9,7 @@ import (
 
 	"charm.land/fantasy"
 	"charm.land/fantasy/providers/anthropic"
+	"charm.land/fantasy/providers/google"
 	"charm.land/fantasy/providers/openai"
 	"github.com/charmbracelet/catwalk/pkg/catwalk"
 )
@@ -41,11 +42,12 @@ type ContentPart interface {
 }
 
 type ReasoningContent struct {
-	Thinking      string                             `json:"thinking"`
-	Signature     string                             `json:"signature"`
-	ResponsesData *openai.ResponsesReasoningMetadata `json:"responses_data"`
-	StartedAt     int64                              `json:"started_at,omitempty"`
-	FinishedAt    int64                              `json:"finished_at,omitempty"`
+	Thinking         string                             `json:"thinking"`
+	Signature        string                             `json:"signature"`
+	ThoughtSignature string                             `json:"thought_signature"` // Used for google
+	ResponsesData    *openai.ResponsesReasoningMetadata `json:"responses_data"`
+	StartedAt        int64                              `json:"started_at,omitempty"`
+	FinishedAt       int64                              `json:"finished_at,omitempty"`
 }
 
 func (tc ReasoningContent) String() string {
@@ -259,6 +261,22 @@ func (m *Message) AppendReasoningContent(delta string) {
 	}
 }
 
+func (m *Message) AppendThoughtSignature(signature string) {
+	for i, part := range m.Parts {
+		if c, ok := part.(ReasoningContent); ok {
+			m.Parts[i] = ReasoningContent{
+				Thinking:         c.Thinking,
+				ThoughtSignature: c.ThoughtSignature + signature,
+				Signature:        c.Signature,
+				StartedAt:        c.StartedAt,
+				FinishedAt:       c.FinishedAt,
+			}
+			return
+		}
+	}
+	m.Parts = append(m.Parts, ReasoningContent{ThoughtSignature: signature})
+}
+
 func (m *Message) AppendReasoningSignature(signature string) {
 	for i, part := range m.Parts {
 		if c, ok := part.(ReasoningContent); ok {
@@ -443,6 +461,11 @@ func (m *Message) ToAIMessage() []fantasy.Message {
 			if reasoning.ResponsesData != nil {
 				reasoningPart.ProviderOptions[openai.Name] = reasoning.ResponsesData
 			}
+			if reasoning.ThoughtSignature != "" {
+				reasoningPart.ProviderOptions[google.Name] = &google.ReasoningMetadata{
+					Signature: reasoning.ThoughtSignature,
+				}
+			}
 			parts = append(parts, reasoningPart)
 		}
 		for _, call := range m.ToolCalls() {