fix
This commit is contained in:
parent
55fdc09d34
commit
6693e1f884
2 changed files with 81 additions and 4 deletions
|
|
@ -302,10 +302,17 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
||||||
name = tc.Function.Name
|
name = tc.Function.Name
|
||||||
if tc.Function.Arguments != "" {
|
if tc.Function.Arguments != "" {
|
||||||
if err := json.Unmarshal([]byte(tc.Function.Arguments), &arguments); err != nil {
|
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).
|
||||||
log.Printf("openai_compat: failed to decode tool call arguments for %q: %v", name, err)
|
// Try to repair it by appending closing characters.
|
||||||
truncated = true
|
if repairedArgs, repairErr := repairJSON(tc.Function.Arguments); repairErr == nil {
|
||||||
continue // Skip this malformed tool call entirely
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,3 +465,25 @@ func asFloat(v any) (float64, bool) {
|
||||||
return 0, false
|
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")
|
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