◐ Shell
clean mode source ↗

fix: preserve gemini thought signatures (#25933) (#26169) · coder/coder@9595e6c

@@ -6,16 +6,13 @@ import (

66

"io"

77

"net/http"

88

"strings"

9+10+

"github.com/coder/coder/v2/internal/googleopenai"

911

)

10121113

// OpenAI-compatible providers share an API shape but differ in the exact JSON

1214

// they accept. These patches adjust Fantasy's serialized request body at the

1315

// transport boundary so higher-level generation code can stay provider agnostic.

14-

//

15-

// googleOpenAICompatDummyThoughtSignature is Google's documented last-resort

16-

// bypass for callers that cannot preserve a real Gemini thought signature.

17-

// See https://ai.google.dev/gemini-api/docs/thought-signatures.

18-

const googleOpenAICompatDummyThoughtSignature = "skip_thought_signature_validator"

19162017

func withOpenAICompatRequestPatches(

2118

client *http.Client,

@@ -91,8 +88,8 @@ func patchOpenAICompatChatCompletionsBody(body []byte, baseURL string, modelID s

9188

}

92899390

changed := rewriteOpenAICompatSingleToolChoice(payload)

94-

if shouldAddGoogleOpenAICompatThoughtSignatures(baseURL, modelID) {

95-

changed = addGoogleOpenAICompatThoughtSignatures(payload) || changed

91+

if googleopenai.ShouldPatchOpenAICompatRequest(baseURL, modelID) {

92+

changed = googleopenai.AddThoughtSignaturesToLatestTurn(payload) || changed

9693

}

9794

if !changed {

9895

return body

@@ -144,93 +141,3 @@ func rewriteOpenAICompatSingleToolChoice(payload map[string]any) bool {

144141

payload["tool_choice"] = "required"

145142

return true

146143

}

147-148-

// shouldAddGoogleOpenAICompatThoughtSignatures detects direct Gemini OpenAI

149-

// endpoints and Coder AI Bridge Gemini routes. Other gateways, such as Vercel,

150-

// keep their own provider-specific compatibility behavior.

151-

func shouldAddGoogleOpenAICompatThoughtSignatures(baseURL string, modelID string) bool {

152-

parsed, ok := parseProviderBaseURL(baseURL)

153-

if !ok {

154-

return false

155-

}

156-

host := strings.ToLower(parsed.Hostname())

157-

path := strings.ToLower(parsed.EscapedPath())

158-

if host == "generativelanguage.googleapis.com" && strings.Contains(path, "/openai") {

159-

return true

160-

}

161-

return host == "coder-aibridge" && isGeminiModelID(modelID)

162-

}

163-164-

func isGeminiModelID(modelID string) bool {

165-

modelID = strings.ToLower(strings.TrimSpace(modelID))

166-

return strings.HasPrefix(modelID, "gemini-") || strings.Contains(modelID, "/gemini-")

167-

}

168-169-

// addGoogleOpenAICompatThoughtSignatures adds a dummy thought signature to the

170-

// first tool call on each assistant tool-call message in the latest user turn.

171-

// Gemini validates tool-call history with thought signatures, but

172-

// OpenAI-compatible serialization can drop the original provider metadata.

173-

func addGoogleOpenAICompatThoughtSignatures(payload map[string]any) bool {

174-

messages, ok := payload["messages"].([]any)

175-

if !ok {

176-

return false

177-

}

178-179-

currentTurnStart := -1

180-

for i, raw := range messages {

181-

message, ok := raw.(map[string]any)

182-

if !ok {

183-

continue

184-

}

185-

if role, _ := message["role"].(string); role == "user" {

186-

currentTurnStart = i

187-

}

188-

}

189-190-

if currentTurnStart == -1 {

191-

return false

192-

}

193-194-

changed := false

195-

for _, raw := range messages[currentTurnStart+1:] {

196-

message, ok := raw.(map[string]any)

197-

if !ok || !isOpenAICompatAssistantRole(message["role"]) {

198-

continue

199-

}

200-

toolCalls, ok := message["tool_calls"].([]any)

201-

if !ok || len(toolCalls) == 0 {

202-

continue

203-

}

204-

firstToolCall, ok := toolCalls[0].(map[string]any)

205-

if !ok {

206-

continue

207-

}

208-

if ensureGoogleOpenAICompatThoughtSignature(firstToolCall) {

209-

changed = true

210-

}

211-

}

212-

return changed

213-

}

214-215-

func isOpenAICompatAssistantRole(role any) bool {

216-

roleValue, _ := role.(string)

217-

return roleValue == "assistant" || roleValue == "model"

218-

}

219-220-

func ensureGoogleOpenAICompatThoughtSignature(toolCall map[string]any) bool {

221-

extraContent, _ := toolCall["extra_content"].(map[string]any)

222-

google, _ := extraContent["google"].(map[string]any)

223-

if signature, _ := google["thought_signature"].(string); signature != "" {

224-

return false

225-

}

226-

if extraContent == nil {

227-

extraContent = map[string]any{}

228-

toolCall["extra_content"] = extraContent

229-

}

230-

if google == nil {

231-

google = map[string]any{}

232-

extraContent["google"] = google

233-

}

234-

google["thought_signature"] = googleOpenAICompatDummyThoughtSignature

235-

return true

236-

}