fix(gemini): align thinking-off and system prompt semantics
This commit is contained in:
parent
459e78c076
commit
83e93ca572
2 changed files with 93 additions and 14 deletions
|
|
@ -174,13 +174,13 @@ func (p *GeminiProvider) buildRequestBody(
|
||||||
) map[string]any {
|
) map[string]any {
|
||||||
contents := make([]geminiContent, 0, len(messages))
|
contents := make([]geminiContent, 0, len(messages))
|
||||||
toolCallNames := make(map[string]string)
|
toolCallNames := make(map[string]string)
|
||||||
var systemInstruction *geminiContent
|
systemPrompts := make([]string, 0, 1)
|
||||||
|
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
switch msg.Role {
|
switch msg.Role {
|
||||||
case "system":
|
case "system":
|
||||||
if strings.TrimSpace(msg.Content) != "" {
|
if strings.TrimSpace(msg.Content) != "" {
|
||||||
systemInstruction = &geminiContent{Parts: []geminiPart{{Text: msg.Content}}}
|
systemPrompts = append(systemPrompts, msg.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "user":
|
case "user":
|
||||||
|
|
@ -248,8 +248,12 @@ func (p *GeminiProvider) buildRequestBody(
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"contents": contents,
|
"contents": contents,
|
||||||
}
|
}
|
||||||
if systemInstruction != nil {
|
if len(systemPrompts) > 0 {
|
||||||
body["systemInstruction"] = systemInstruction
|
systemParts := make([]geminiPart, 0, len(systemPrompts))
|
||||||
|
for _, prompt := range systemPrompts {
|
||||||
|
systemParts = append(systemParts, geminiPart{Text: prompt})
|
||||||
|
}
|
||||||
|
body["systemInstruction"] = &geminiContent{Parts: systemParts}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 {
|
||||||
|
|
@ -331,12 +335,19 @@ func buildGeminiThinkingConfig(model string, options map[string]any) map[string]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config := map[string]any{"includeThoughts": true}
|
config := map[string]any{}
|
||||||
rawLevel, _ := options["thinking_level"].(string)
|
rawLevel, _ := options["thinking_level"].(string)
|
||||||
rawLevel = strings.ToLower(strings.TrimSpace(rawLevel))
|
rawLevel = strings.ToLower(strings.TrimSpace(rawLevel))
|
||||||
|
if rawLevel == "" {
|
||||||
|
// Align with agent-level default: unset means ThinkingOff.
|
||||||
|
rawLevel = "off"
|
||||||
|
}
|
||||||
|
|
||||||
|
includeThoughts := rawLevel != "off" && rawLevel != "minimal"
|
||||||
|
config["includeThoughts"] = includeThoughts
|
||||||
|
|
||||||
if isGemini25Model(model) {
|
if isGemini25Model(model) {
|
||||||
if budget, ok := mapGeminiThinkingBudget(rawLevel, model); ok {
|
if budget, ok := mapGeminiThinkingBudget(rawLevel); ok {
|
||||||
config["thinkingBudget"] = budget
|
config["thinkingBudget"] = budget
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
@ -358,7 +369,7 @@ func isGemini25Model(model string) bool {
|
||||||
return strings.Contains(lowerModel, "gemini-2.5") || strings.Contains(lowerModel, "gemini-25")
|
return strings.Contains(lowerModel, "gemini-2.5") || strings.Contains(lowerModel, "gemini-25")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapGeminiThinkingBudget(level string, model string) (int, bool) {
|
func mapGeminiThinkingBudget(level string) (int, bool) {
|
||||||
level = strings.ToLower(strings.TrimSpace(level))
|
level = strings.ToLower(strings.TrimSpace(level))
|
||||||
if level == "" {
|
if level == "" {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
|
@ -368,15 +379,8 @@ func mapGeminiThinkingBudget(level string, model string) (int, bool) {
|
||||||
case "adaptive":
|
case "adaptive":
|
||||||
return -1, true
|
return -1, true
|
||||||
case "minimal":
|
case "minimal":
|
||||||
if strings.Contains(strings.ToLower(model), "pro") {
|
|
||||||
return 128, true
|
|
||||||
}
|
|
||||||
return 0, true
|
return 0, true
|
||||||
case "off":
|
case "off":
|
||||||
if strings.Contains(strings.ToLower(model), "pro") {
|
|
||||||
// Gemini 2.5 Pro cannot disable thinking; use the lowest supported budget.
|
|
||||||
return 128, true
|
|
||||||
}
|
|
||||||
return 0, true
|
return 0, true
|
||||||
case "low":
|
case "low":
|
||||||
return 1024, true
|
return 1024, true
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,81 @@ func TestGeminiProvider_BuildRequestBody_OmitsThinkingConfigForGemini20(t *testi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGeminiProvider_BuildRequestBody_DefaultsThinkingOffForGemini25(t *testing.T) {
|
||||||
|
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
||||||
|
body := provider.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hello"}},
|
||||||
|
nil,
|
||||||
|
"gemini-2.5-flash",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
generationConfig, ok := body["generationConfig"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("generationConfig = %#v, want map", body["generationConfig"])
|
||||||
|
}
|
||||||
|
thinkingConfig, ok := generationConfig["thinkingConfig"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("thinkingConfig = %#v, want map", generationConfig["thinkingConfig"])
|
||||||
|
}
|
||||||
|
if got := thinkingConfig["thinkingBudget"]; got != 0 {
|
||||||
|
t.Fatalf("thinkingBudget = %#v, want 0 for default/off", got)
|
||||||
|
}
|
||||||
|
if includeThoughts, ok := thinkingConfig["includeThoughts"].(bool); !ok || includeThoughts {
|
||||||
|
t.Fatalf("includeThoughts = %#v, want false for default/off", thinkingConfig["includeThoughts"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeminiProvider_BuildRequestBody_DefaultsThinkingOffForGemini3(t *testing.T) {
|
||||||
|
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
||||||
|
body := provider.buildRequestBody(
|
||||||
|
[]Message{{Role: "user", Content: "hello"}},
|
||||||
|
nil,
|
||||||
|
"gemini-3-flash-preview",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
generationConfig, ok := body["generationConfig"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("generationConfig = %#v, want map", body["generationConfig"])
|
||||||
|
}
|
||||||
|
thinkingConfig, ok := generationConfig["thinkingConfig"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("thinkingConfig = %#v, want map", generationConfig["thinkingConfig"])
|
||||||
|
}
|
||||||
|
if got := thinkingConfig["thinkingLevel"]; got != "minimal" {
|
||||||
|
t.Fatalf("thinkingLevel = %#v, want minimal for default/off", got)
|
||||||
|
}
|
||||||
|
if includeThoughts, ok := thinkingConfig["includeThoughts"].(bool); !ok || includeThoughts {
|
||||||
|
t.Fatalf("includeThoughts = %#v, want false for default/off", thinkingConfig["includeThoughts"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeminiProvider_BuildRequestBody_PreservesMultipleSystemMessages(t *testing.T) {
|
||||||
|
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
||||||
|
body := provider.buildRequestBody(
|
||||||
|
[]Message{
|
||||||
|
{Role: "system", Content: "You are helpful."},
|
||||||
|
{Role: "system", Content: "Be concise."},
|
||||||
|
{Role: "user", Content: "hello"},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
"gemini-3-flash-preview",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
systemInstruction, ok := body["systemInstruction"].(*geminiContent)
|
||||||
|
if !ok || systemInstruction == nil {
|
||||||
|
t.Fatalf("systemInstruction = %#v, want *geminiContent", body["systemInstruction"])
|
||||||
|
}
|
||||||
|
if len(systemInstruction.Parts) != 2 {
|
||||||
|
t.Fatalf("systemInstruction.Parts len = %d, want 2", len(systemInstruction.Parts))
|
||||||
|
}
|
||||||
|
if systemInstruction.Parts[0].Text != "You are helpful." || systemInstruction.Parts[1].Text != "Be concise." {
|
||||||
|
t.Fatalf("systemInstruction.Parts = %#v, want ordered system prompts", systemInstruction.Parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGeminiProvider_BuildRequestBody_PreservesToolResponseMedia(t *testing.T) {
|
func TestGeminiProvider_BuildRequestBody_PreservesToolResponseMedia(t *testing.T) {
|
||||||
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
provider := NewGeminiProvider("test-key", "https://example.com/v1beta", "", "", 0, nil, nil)
|
||||||
body := provider.buildRequestBody(
|
body := provider.buildRequestBody(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue