From 9b794a8aec2ae95ab29f50feb6ef4817099bfcf6 Mon Sep 17 00:00:00 2001 From: Futureppo Date: Mon, 13 Jul 2026 14:30:50 +0800 Subject: [PATCH] Fix responses assistant history content --- internal/openai/responses_compat.go | 49 ++++++++++++++++++ internal/openai/responses_compat_test.go | 65 ++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/internal/openai/responses_compat.go b/internal/openai/responses_compat.go index cd65fec..869503e 100644 --- a/internal/openai/responses_compat.go +++ b/internal/openai/responses_compat.go @@ -202,9 +202,58 @@ func sanitizeInputItem(item map[string]any) map[string]any { out := clone(item) delete(out, "internal_chat_message_metadata_passthrough") delete(out, "phase") + if content, exists := out["content"]; exists && String(out, "type", "") == "message" { + out["content"] = sanitizeMessageContent(content) + } + return out +} + +func sanitizeMessageContent(content any) any { + parts, ok := content.([]any) + if !ok { + return content + } + out := make([]any, 0, len(parts)) + for _, raw := range parts { + part, ok := raw.(map[string]any) + if !ok { + out = append(out, raw) + continue + } + clean := clone(part) + switch String(clean, "type", "") { + case "output_text": + clean["type"] = "input_text" + case "image": + if image := anthropicImageContent(clean); image != nil { + clean = image + } + } + out = append(out, clean) + } return out } +func anthropicImageContent(part map[string]any) map[string]any { + source, ok := part["source"].(map[string]any) + if !ok { + return nil + } + var url string + switch String(source, "type", "") { + case "url": + url = String(source, "url", "") + case "base64": + url = "data:" + String(source, "media_type", "") + ";base64," + String(source, "data", "") + default: + return nil + } + if url == "" { + return nil + } + return map[string]any{"type": "input_image", "image_url": url, "detail": "auto"} +} + func hasReasoningText(item map[string]any) bool { for _, key := range []string{"summary", "content"} { if parts, ok := item[key].([]any); ok && len(parts) > 0 { diff --git a/internal/openai/responses_compat_test.go b/internal/openai/responses_compat_test.go index 87c4003..65cd99e 100644 --- a/internal/openai/responses_compat_test.go +++ b/internal/openai/responses_compat_test.go @@ -126,6 +126,71 @@ func TestPrepareCompatibleResponsesDropsUnknownInput(t *testing.T) { } } +func TestPrepareCompatibleResponsesRewritesAssistantOutputTextHistory(t *testing.T) { + wire, _, err := PrepareCompatibleResponses(map[string]any{ + "model": "grok-4.5", + "input": []any{ + map[string]any{ + "type": "message", "role": "assistant", "id": "msg_1", "status": "completed", + "content": []any{map[string]any{"type": "output_text", "text": "first response"}}, + }, + map[string]any{"type": "message", "role": "user", "content": []any{map[string]any{"type": "input_text", "text": "continue"}}}, + }, + }) + if err != nil { + t.Fatal(err) + } + input := wire["input"].([]any) + message := input[0].(map[string]any) + content := message["content"].([]any) + part := content[0].(map[string]any) + if part["type"] != "input_text" || part["text"] != "first response" { + t.Fatalf("assistant history content = %#v", content) + } +} + +func TestPrepareCompatibleResponsesDoesNotAddMissingMessageContent(t *testing.T) { + wire, _, err := PrepareCompatibleResponses(map[string]any{ + "model": "grok-4.5", + "input": []any{ + map[string]any{"type": "message", "role": "assistant", "tool_calls": []any{map[string]any{"id": "call_1", "type": "function"}}}, + }, + }) + if err != nil { + t.Fatal(err) + } + input := wire["input"].([]any) + message := input[0].(map[string]any) + if _, exists := message["content"]; exists { + t.Fatalf("missing content was added: %#v", message) + } +} + +func TestPrepareCompatibleResponsesRewritesAnthropicImageContent(t *testing.T) { + wire, _, err := PrepareCompatibleResponses(map[string]any{ + "model": "grok-4.5", + "input": []any{ + map[string]any{ + "type": "message", "role": "user", + "content": []any{ + map[string]any{"type": "input_text", "text": "describe"}, + map[string]any{"type": "image", "source": map[string]any{"type": "base64", "media_type": "image/png", "data": "AAAA"}}, + }, + }, + }, + }) + if err != nil { + t.Fatal(err) + } + input := wire["input"].([]any) + message := input[0].(map[string]any) + content := message["content"].([]any) + image := content[1].(map[string]any) + if image["type"] != "input_image" || image["image_url"] != "data:image/png;base64,AAAA" { + t.Fatalf("image content = %#v", image) + } +} + func TestPrepareCompatibleResponsesNormalizesCodexHostedTools(t *testing.T) { wire, _, err := PrepareCompatibleResponses(map[string]any{ "model": "grok-4.5", "input": "hello",