fix(providers/openai): include name on tool role messages (Gemini 400 fix)

Google Gemini's OpenAI-compat shim maps role=tool messages to native
FunctionResponse{name, response}; an empty name trips HTTP 400
("Name cannot be empty"). Build raw-ID → tool-name index at serialize
time and populate the name field on role=tool wire messages.

Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
This commit is contained in:
viettranx committed 2026-04-15 12:35:47 +07:00
1 parent 7773fa01b8
commit 498abb27d9
1 file changed
+31
+31
View File
@@ -25,6 +25,13 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
inputMessages = collapseToolCallsWithoutSig(inputMessages)
}
// Build raw-ID → tool-name index for role="tool" serialization.
// Google Gemini's OpenAI-compat shim maps role=tool messages to native
// FunctionResponse{name, response}; an empty name trips HTTP 400 ("Name
// cannot be empty"). Lookup uses the raw ToolCallID to match history before
// any wire-truncation. Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd.
toolNameByID := buildToolNameIndex(inputMessages)
// Detect native OpenAI endpoint to enable developer role.
// GPT-4o+ models prioritize "developer" messages over "system" for instruction
// adherence. Non-OpenAI backends (proxies, Qwen, DeepSeek, etc.) reject "developer".
@@ -105,6 +112,12 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
if m.ToolCallID != "" {
msg["tool_call_id"] = p.wireToolCallID(m.ToolCallID)
if name := toolNameByID[m.ToolCallID]; name != "" {
msg["name"] = name
} else if m.Role == "tool" {
slog.Warn("openai: tool msg without matching tool_call",
"provider", p.name, "tool_call_id", m.ToolCallID)
}
}
msgs = append(msgs, msg)
@@ -188,6 +201,24 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream
return body
}
// buildToolNameIndex returns a raw-ID → tool-name map drawn from every assistant
// message's ToolCalls. Used at serialize time to populate role=tool wire messages
// with their originating tool's name (required by Google Gemini OpenAI-compat shim).
func buildToolNameIndex(msgs []Message) map[string]string {
idx := map[string]string{}
for _, m := range msgs {
if m.Role != "assistant" {
continue
}
for _, tc := range m.ToolCalls {
if tc.ID != "" && tc.Name != "" {
idx[tc.ID] = tc.Name
}
}
}
return idx
}
// modelFamily strips provider prefixes (for example "openai/o3-mini") so capability
// gates apply to the actual model family rather than the transport-specific wrapper.
func modelFamily(model string) string {