fix(edge): Gemini 병렬 도구 인덱스를 정규화한다

This commit is contained in:
toki 2026-08-15 10:25:34 +09:00
parent 1e2aaf4c64
commit 03aff0b8f8
2 changed files with 38 additions and 4 deletions

View file

@ -510,9 +510,6 @@ func rewriteProviderJSONResponse(body []byte, model, toolCallWire string) []byte
}
func normalizeGeminiChatProviderResponse(body []byte) []byte {
if !bytes.Contains(body, []byte(`"thought_signature"`)) {
return body
}
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
var response map[string]any
@ -530,11 +527,22 @@ func normalizeGeminiChatProviderResponse(body []byte) []byte {
if !ok {
continue
}
for _, rawCall := range anySlice(message["tool_calls"]) {
toolCalls := anySlice(message["tool_calls"])
for index, rawCall := range toolCalls {
call, ok := rawCall.(map[string]any)
if !ok {
continue
}
// Gemini's Chat-compatible stream may emit several complete tool
// calls in one delta without OpenAI's per-call index. Preserve their
// positional identity so the stream decoder does not concatenate
// independent argument objects into index zero.
if messageKey == "delta" && len(toolCalls) > 1 {
if _, present := call["index"]; !present {
call["index"] = index
changed = true
}
}
id, idOK := call["id"].(string)
extra, extraOK := call["extra_content"].(map[string]any)
if !idOK || id == "" || !extraOK {

View file

@ -76,6 +76,32 @@ func TestProviderThoughtSignatureNormalizationIsGeminiProfileOnly(t *testing.T)
}
}
func TestGeminiChatProviderResponseAddsMissingParallelToolIndices(t *testing.T) {
response := []byte(`{"id":"chat-1","choices":[{"delta":{"tool_calls":[{"id":"call-1","function":{"name":"read_file","arguments":"{\"path\":\"plan.md\"}"}},{"id":"call-2","function":{"name":"read_file","arguments":"{\"path\":\"review.md\"}"}},{"id":"call-3","function":{"name":"bash","arguments":"{\"command\":\"test -f index.html\"}"}}]}}]}`)
normalized := normalizeGeminiChatProviderResponse(response)
var body map[string]any
if err := json.Unmarshal(normalized, &body); err != nil {
t.Fatal(err)
}
choice := anySlice(body["choices"])[0].(map[string]any)
delta := choice["delta"].(map[string]any)
calls := anySlice(delta["tool_calls"])
for index, raw := range calls {
call := raw.(map[string]any)
if got := int(call["index"].(float64)); got != index {
t.Fatalf("tool call %d index = %d", index, got)
}
}
stage, err := decodeOpenAIPresetSSE([]byte("data: " + string(normalized) + "\n\ndata: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1,\"total_tokens\":2}}\n\ndata: [DONE]\n\n"))
if err != nil {
t.Fatal(err)
}
if len(stage.ToolCalls) != 3 {
t.Fatalf("decoded tool calls = %+v", stage.ToolCalls)
}
}
func TestProviderChatTokenLimitNormalizationUsesSelectedProfile(t *testing.T) {
tests := []struct {
name string