From addac5bd8581ff25fb90e9702f987195182316f0 Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Thu, 22 Jan 2026 11:52:56 -0800 Subject: [PATCH] shelley: update Gemini models and add thought signature support Prompt: I think using Gemini isn't really working with shelley. Use the gemini key to see if you can get it to work. Use the pointers to the gemini3 series models to add to the model drop down etc. Related to https://github.com/boldsoftware/shelley/issues/42 - Update default model from gemini-2.5-pro-preview-03-25 to gemini-2.5-pro (the preview model is no longer available) - Add Gemini 3 models: gemini-3-pro and gemini-3-flash - Add Gemini 2.5 Flash model - Update context window size mappings for new models - Add thought signature support required by Gemini 3 for function calling: - Added ThoughtSignature field to gemini.Part struct - Capture thought signature when receiving function call responses - Pass thought signature back when building requests with function calls - Update tests to use current model names Gemini 3 models require thought signatures to be passed back during function calling workflows, otherwise they return a 400 error. All models have been tested and work with both simple text completion and multi-turn tool usage. Co-authored-by: Shelley --- llm/gem/gem.go | 27 ++++++++++++++-------- llm/gem/gem_test.go | 24 +++++++++++++++++-- llm/gem/gemini/gemini.go | 3 +++ models/models.go | 50 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/llm/gem/gem.go b/llm/gem/gem.go index 91469882adad070f77690e69ddc7425727dedd27..94ef9a0abc829317240b1c45120196ebd312d5e5 100644 --- a/llm/gem/gem.go +++ b/llm/gem/gem.go @@ -16,7 +16,7 @@ import ( ) const ( - DefaultModel = "gemini-2.5-pro-preview-03-25" + DefaultModel = "gemini-2.5-pro" GeminiAPIKeyEnv = "GEMINI_API_KEY" ) @@ -210,13 +210,16 @@ func (s *Service) buildGeminiRequest(req *llm.Request) (*gemini.Request, error) slog.DebugContext(context.Background(), "gemini_preparing_tool_use", "tool_name", c.ToolName, "tool_id", c.ID, - "input", string(c.ToolInput)) + "input", string(c.ToolInput), + "thought_signature", c.Signature) content.Parts = append(content.Parts, gemini.Part{ FunctionCall: &gemini.FunctionCall{ Name: c.ToolName, Args: args, }, + // Gemini 3 requires thought signatures to be passed back for function calls + ThoughtSignature: c.Signature, }) case llm.ContentTypeToolResult: // Tool result becomes a function response @@ -320,15 +323,16 @@ func convertGeminiResponseToContent(res *gemini.Response) []llm.Content { if part.Text != "" { // Simple text response contents = append(contents, llm.Content{ - Type: llm.ContentTypeText, - Text: part.Text, + Type: llm.ContentTypeText, + Text: part.Text, + Signature: part.ThoughtSignature, // Capture thought signature for text parts too }) } else if part.FunctionCall != nil { // Function call (tool use) args, err := json.Marshal(part.FunctionCall.Args) if err != nil { // If we can't marshal, use empty args - slog.DebugContext(context.Background(), "gemini_failed_to_markshal_args", + slog.DebugContext(context.Background(), "gemini_failed_to_marshal_args", "tool_name", part.FunctionCall.Name, "args", string(args), "err", err.Error(), @@ -345,12 +349,15 @@ func convertGeminiResponseToContent(res *gemini.Response) []llm.Content { Type: llm.ContentTypeToolUse, ToolName: part.FunctionCall.Name, ToolInput: json.RawMessage(args), + // Capture thought signature - required for Gemini 3 function calling + Signature: part.ThoughtSignature, }) slog.DebugContext(context.Background(), "gemini_tool_call", "tool_id", toolID, "tool_name", part.FunctionCall.Name, - "args", string(args)) + "args", string(args), + "thought_signature", part.ThoughtSignature) } else if part.FunctionResponse != nil { // We shouldn't normally get function responses from the model, but just in case respData, _ := json.Marshal(part.FunctionResponse.Response) @@ -446,9 +453,11 @@ func (s *Service) TokenContextWindow() int { // Gemini models generally have large context windows switch model { - case "gemini-2.5-pro-preview-03-25": - return 1000000 // 1M tokens for Gemini 2.5 Pro - case "gemini-2.0-flash-exp": + case "gemini-3-pro-preview", "gemini-3-flash-preview": + return 1000000 // 1M tokens for Gemini 3 + case "gemini-2.5-pro", "gemini-2.5-flash": + return 1000000 // 1M tokens for Gemini 2.5 + case "gemini-2.0-flash-exp", "gemini-2.0-flash": return 1000000 // 1M tokens for Gemini 2.0 Flash case "gemini-1.5-pro", "gemini-1.5-pro-latest": return 2000000 // 2M tokens for Gemini 1.5 Pro diff --git a/llm/gem/gem_test.go b/llm/gem/gem_test.go index 6ca3eff984111c39b1d2127f115db60c4347a2c7..eb582075e7c405d87e1a81e89ae4bd32bcea7b73 100644 --- a/llm/gem/gem_test.go +++ b/llm/gem/gem_test.go @@ -372,8 +372,23 @@ func TestTokenContextWindow(t *testing.T) { expected int }{ { - name: "gemini-2.5-pro-preview-03-25", - model: "gemini-2.5-pro-preview-03-25", + name: "gemini-3-pro-preview", + model: "gemini-3-pro-preview", + expected: 1000000, + }, + { + name: "gemini-3-flash-preview", + model: "gemini-3-flash-preview", + expected: 1000000, + }, + { + name: "gemini-2.5-pro", + model: "gemini-2.5-pro", + expected: 1000000, + }, + { + name: "gemini-2.5-flash", + model: "gemini-2.5-flash", expected: 1000000, }, { @@ -381,6 +396,11 @@ func TestTokenContextWindow(t *testing.T) { model: "gemini-2.0-flash-exp", expected: 1000000, }, + { + name: "gemini-2.0-flash", + model: "gemini-2.0-flash", + expected: 1000000, + }, { name: "gemini-1.5-pro", model: "gemini-1.5-pro", diff --git a/llm/gem/gemini/gemini.go b/llm/gem/gemini/gemini.go index 609fa8e5da19c68a45a4a4bc4e108e83bfc3d600..5fc61b26d94a578d287d52b614defe6770c1c5c7 100644 --- a/llm/gem/gemini/gemini.go +++ b/llm/gem/gemini/gemini.go @@ -50,6 +50,9 @@ type Part struct { FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"` ExecutableCode *ExecutableCode `json:"executableCode,omitempty"` CodeExecutionResult *CodeExecutionResult `json:"codeExecutionResult,omitempty"` + // ThoughtSignature is required for Gemini 3 models when using function calling. + // It must be passed back exactly as received when sending the conversation history. + ThoughtSignature string `json:"thoughtSignature,omitempty"` // TODO inlineData // TODO fileData } diff --git a/models/models.go b/models/models.go index 8c8a1263b52d4eba9ed42a331e763167e9494ae1..9bf92e18e81b20af00432f5b8bb1acfcb2d86dbc 100644 --- a/models/models.go +++ b/models/models.go @@ -227,6 +227,38 @@ func All() []Model { return svc, nil }, }, + { + ID: "gemini-3-pro", + Provider: ProviderGemini, + Description: "Gemini 3 Pro", + RequiredEnvVars: []string{"GEMINI_API_KEY"}, + Factory: func(config *Config, httpc *http.Client) (llm.Service, error) { + if config.GeminiAPIKey == "" { + return nil, fmt.Errorf("gemini-3-pro requires GEMINI_API_KEY") + } + svc := &gem.Service{APIKey: config.GeminiAPIKey, Model: "gemini-3-pro-preview", HTTPC: httpc} + if url := config.getGeminiURL(); url != "" { + svc.URL = url + } + return svc, nil + }, + }, + { + ID: "gemini-3-flash", + Provider: ProviderGemini, + Description: "Gemini 3 Flash", + RequiredEnvVars: []string{"GEMINI_API_KEY"}, + Factory: func(config *Config, httpc *http.Client) (llm.Service, error) { + if config.GeminiAPIKey == "" { + return nil, fmt.Errorf("gemini-3-flash requires GEMINI_API_KEY") + } + svc := &gem.Service{APIKey: config.GeminiAPIKey, Model: "gemini-3-flash-preview", HTTPC: httpc} + if url := config.getGeminiURL(); url != "" { + svc.URL = url + } + return svc, nil + }, + }, { ID: "gemini-2.5-pro", Provider: ProviderGemini, @@ -236,7 +268,23 @@ func All() []Model { if config.GeminiAPIKey == "" { return nil, fmt.Errorf("gemini-2.5-pro requires GEMINI_API_KEY") } - svc := &gem.Service{APIKey: config.GeminiAPIKey, Model: gem.DefaultModel, HTTPC: httpc} + svc := &gem.Service{APIKey: config.GeminiAPIKey, Model: "gemini-2.5-pro", HTTPC: httpc} + if url := config.getGeminiURL(); url != "" { + svc.URL = url + } + return svc, nil + }, + }, + { + ID: "gemini-2.5-flash", + Provider: ProviderGemini, + Description: "Gemini 2.5 Flash", + RequiredEnvVars: []string{"GEMINI_API_KEY"}, + Factory: func(config *Config, httpc *http.Client) (llm.Service, error) { + if config.GeminiAPIKey == "" { + return nil, fmt.Errorf("gemini-2.5-flash requires GEMINI_API_KEY") + } + svc := &gem.Service{APIKey: config.GeminiAPIKey, Model: "gemini-2.5-flash", HTTPC: httpc} if url := config.getGeminiURL(); url != "" { svc.URL = url }