diff --git a/go/ai/document.go b/go/ai/document.go index 95ca54c176..f4c94f1be9 100644 --- a/go/ai/document.go +++ b/go/ai/document.go @@ -94,8 +94,15 @@ func NewCustomPart(customData map[string]any) *Part { } // NewReasoningPart returns a Part containing reasoning text -func NewReasoningPart(text string) *Part { - return &Part{Kind: PartReasoning, ContentType: "plain/text", Text: text} +func NewReasoningPart(text string, signature []byte) *Part { + return &Part{ + Kind: PartReasoning, + ContentType: "plain/text", + Text: text, + Metadata: map[string]any{ + "signature": signature, + }, + } } // IsText reports whether the [Part] contains plain text. diff --git a/go/go.mod b/go/go.mod index 2a0fb5564d..370e946a5d 100644 --- a/go/go.mod +++ b/go/go.mod @@ -35,7 +35,7 @@ require ( golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/tools v0.32.0 google.golang.org/api v0.230.0 - google.golang.org/genai v1.5.0 + google.golang.org/genai v1.8.0 ) require ( diff --git a/go/go.sum b/go/go.sum index 7a5cec1edd..d4605238e3 100644 --- a/go/go.sum +++ b/go/go.sum @@ -418,8 +418,8 @@ google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM= google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genai v1.5.0 h1:6wB3MCW4JpCMHURJH2gBNxCU/9iN1YjKYQj362mDTbY= -google.golang.org/genai v1.5.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= +google.golang.org/genai v1.8.0 h1:unX2CNWSiKDO2MSTKK3RstXg/vHp9hr42LIcL6f3Cik= +google.golang.org/genai v1.8.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 h1:6GUHKGv2huWOHKmDXLMNE94q3fBDlEHI+oTRIZSebK0= google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= diff --git a/go/plugins/googlegenai/gemini.go b/go/plugins/googlegenai/gemini.go index 51890953c7..b50527d245 100644 --- a/go/plugins/googlegenai/gemini.go +++ b/go/plugins/googlegenai/gemini.go @@ -786,12 +786,13 @@ func translateCandidate(cand *genai.Candidate) *ai.ModelResponse { var p *ai.Part partFound := 0 - if part.Text != "" { + if part.Thought { + p = ai.NewReasoningPart(part.Text, part.ThoughtSignature) partFound++ + } + if part.Text != "" { p = ai.NewTextPart(part.Text) - if part.Thought { - p = ai.NewReasoningPart(part.Text) - } + partFound++ } if part.InlineData != nil { partFound++ @@ -871,9 +872,20 @@ func toGeminiParts(parts []*ai.Part) ([]*genai.Part, error) { // toGeminiPart converts a [ai.Part] to a [genai.Part]. func toGeminiPart(p *ai.Part) (*genai.Part, error) { switch { - case p.IsText(): - return genai.NewPartFromText(p.Text), nil case p.IsReasoning(): + // TODO: go-genai does not support genai.NewPartFromThought() + signature := []byte{} + if p.Metadata != nil { + if sig, ok := p.Metadata["signature"].([]byte); ok { + signature = sig + } + } + return &genai.Part{ + Thought: true, + Text: p.Text, + ThoughtSignature: signature, + }, nil + case p.IsText(): return genai.NewPartFromText(p.Text), nil case p.IsMedia(): if strings.HasPrefix(p.Text, "data:") { diff --git a/go/plugins/googlegenai/googleai_live_test.go b/go/plugins/googlegenai/googleai_live_test.go index b7af41df5f..94dad0a32f 100644 --- a/go/plugins/googlegenai/googleai_live_test.go +++ b/go/plugins/googlegenai/googleai_live_test.go @@ -166,6 +166,28 @@ func TestGoogleAILive(t *testing.T) { t.Errorf("got %q, expecting it to contain %q", out, want) } }) + t.Run("tool with thinking", func(t *testing.T) { + m := googlegenai.GoogleAIModel(g, "gemini-2.5-flash-preview-04-17") + resp, err := genkit.Generate(ctx, g, + ai.WithConfig(&googlegenai.GeminiConfig{ + ThinkingConfig: &googlegenai.ThinkingConfig{ + ThinkingBudget: 1000, + }, + }), + ai.WithModel(m), + ai.WithPrompt("what is a gablorken of 2 over 3.5?"), + ai.WithTools(gablorkenTool)) + if err != nil { + t.Fatal(err) + } + + out := resp.Message.Content[0].Text + const want = "11.31" + if !strings.Contains(out, want) { + t.Errorf("got %q, expecting it to contain %q", out, want) + } + }) + t.Run("tool with json output", func(t *testing.T) { type weatherQuery struct { Location string `json:"location"` @@ -414,7 +436,7 @@ func TestGoogleAILive(t *testing.T) { Temperature: 0.4, ThinkingConfig: &googlegenai.ThinkingConfig{ IncludeThoughts: true, - ThinkingBudget: 100, + ThinkingBudget: 1000, }, }), ai.WithModel(m), @@ -425,8 +447,8 @@ func TestGoogleAILive(t *testing.T) { if resp == nil { t.Fatal("nil response obtanied") } - if resp.Usage.ThoughtsTokens == 0 || resp.Usage.ThoughtsTokens > 100 { - t.Fatal("thoughts tokens should not be zero or greater than 100") + if resp.Usage.ThoughtsTokens == 0 || resp.Usage.ThoughtsTokens > 1000 { + t.Fatalf("thoughts tokens should not be zero or greater than 100, got: %d", resp.Usage.ThoughtsTokens) } }) t.Run("thinking disabled", func(t *testing.T) {