fix
This commit is contained in:
parent
55fdc09d34
commit
6693e1f884
2 changed files with 81 additions and 4 deletions
|
|
@ -302,13 +302,20 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
|||
name = tc.Function.Name
|
||||
if tc.Function.Arguments != "" {
|
||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {
|
||||
// JSON is malformed (likely truncated due to max_tokens). Log and signal truncation.
|
||||
// JSON is malformed (likely truncated due to max_tokens or LLM stopping early).
|
||||
// Try to repair it by appending closing characters.
|
||||
if repairedArgs, repairErr := repairJSON(tc.Function.Arguments); repairErr == nil {
|
||||
arguments = repairedArgs
|
||||
log.Printf("openai_compat: recovered tool call arguments for %q (auto-repaired)", name)
|
||||
} else {
|
||||
// JSON is too malformed to repair. Log and signal truncation.
|
||||
log.Printf("openai_compat: failed to decode tool call arguments for %q: %v", name, err)
|
||||
truncated = true
|
||||
continue // Skip this malformed tool call entirely
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build ToolCall with ExtraContent for Gemini 3 thought_signature persistence
|
||||
toolCall := ToolCall{
|
||||
|
|
@ -458,3 +465,25 @@ func asFloat(v any) (float64, bool) {
|
|||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// repairJSON attempts to fix commonly truncated JSON objects by appending closing characters.
|
||||
func repairJSON(s string) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
|
||||
// Fast path: try closing suffixes for flat JSON objects
|
||||
suffixes := []string{
|
||||
"}",
|
||||
"\"}",
|
||||
"\"}}",
|
||||
"\"}]}",
|
||||
"]}",
|
||||
}
|
||||
|
||||
for _, suffix := range suffixes {
|
||||
if err := json.Unmarshal([]byte(s+suffix), &result); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to repair json")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,3 +513,51 @@ func TestSerializeMessages_StripsSystemParts(t *testing.T) {
|
|||
t.Fatal("system_parts should not appear in serialized output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderChat_RepairsTruncatedToolCall(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := map[string]any{
|
||||
"choices": []map[string]any{
|
||||
{
|
||||
"message": map[string]any{
|
||||
"content": "",
|
||||
"tool_calls": []map[string]any{
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": map[string]any{
|
||||
"name": "read_file",
|
||||
"arguments": "{\"path\": \"/my/file.txt\"", // missing }
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"finish_reason": "length",
|
||||
},
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
p := NewProvider("key", server.URL, "")
|
||||
out, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "hi"}}, nil, "gpt-4o", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Chat() error = %v", err)
|
||||
}
|
||||
if len(out.ToolCalls) != 1 {
|
||||
t.Fatalf("len(ToolCalls) = %d, want 1", len(out.ToolCalls))
|
||||
}
|
||||
if out.ToolCalls[0].Name != "read_file" {
|
||||
t.Fatalf("ToolCalls[0].Name = %q, want %q", out.ToolCalls[0].Name, "read_file")
|
||||
}
|
||||
if out.ToolCalls[0].Arguments["path"] != "/my/file.txt" {
|
||||
t.Fatalf("ToolCalls[0].Arguments[path] = %v, want /my/file.txt", out.ToolCalls[0].Arguments["path"])
|
||||
}
|
||||
// Even though it was repaired, the finish reason should still be truncated because the LLM originally returned length or we truncated it?
|
||||
// Actually, if finish_reason was "length", parseResponse will set finishReason to "truncated" anyway.
|
||||
if out.FinishReason != "truncated" {
|
||||
t.Fatalf("FinishReason = %q, want %q", out.FinishReason, "truncated")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue