diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ef57d..be6c228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,12 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`. ### English +- Accept `tool_reference` blocks inside Anthropic tool results and keep them as text instead of rejecting the whole request + ### 中文 +- 接受 Anthropic 工具结果中的 `tool_reference` 块并保留为文本,不再整条请求报错 + ## 0.4.10 - 2026-09-12 ### English diff --git a/internal/translate/compat.go b/internal/translate/compat.go index 13a4888..b90614d 100644 --- a/internal/translate/compat.go +++ b/internal/translate/compat.go @@ -271,6 +271,16 @@ func anthropicToolResultContent(raw json.RawMessage) (string, []any, error) { switch rawMapString(source, "type") { case "text": texts = append(texts, rawMapContentString(source, "text")) + case "tool_reference": + // Claude Code's tool search returns references to tools it has + // just made available. The Qoder upstream has no equivalent + // block, so keep the information as text instead of rejecting + // the whole request. + name := rawMapString(source, "tool_name") + if name == "" { + name = "unknown" + } + texts = append(texts, "Tool available: "+name) case "image": image, err := anthropicImagePart(source) if err != nil { diff --git a/internal/translate/compat_test.go b/internal/translate/compat_test.go index 6a751d5..5893ea5 100644 --- a/internal/translate/compat_test.go +++ b/internal/translate/compat_test.go @@ -2,6 +2,7 @@ package translate import ( "encoding/json" + "strings" "testing" ) @@ -125,3 +126,42 @@ func TestTranslatedImageOnlyRequestsProduceContentSessionSeed(t *testing.T) { t.Fatal("different anthropic images must not share a seed") } } + +func TestTranslateAnthropicToolReferenceDegradesToText(t *testing.T) { + request := AnthropicMessagesRequest{ + Model: "deepseek-flash", + Messages: []AnthropicMessage{ + {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"toolu_1","name":"ToolSearch","input":{"query":"stop task"}}]`)}, + {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"toolu_1","content":[{"type":"tool_reference","tool_name":"TaskStop"}]}]`)}, + {Role: "user", Content: json.RawMessage(`[{"type":"text","text":"continue"}]`)}, + }, + } + chat, err := TranslateAnthropicMessages(request) + if err != nil { + t.Fatal(err) + } + if len(chat.Messages) != 3 || chat.Messages[1].Role != "tool" { + t.Fatalf("messages=%#v", chat.Messages) + } + got := ContentToString(chat.Messages[1].Content) + if !strings.Contains(got, "TaskStop") { + t.Fatalf("tool reference lost: %#v", chat.Messages[1].Content) + } + if chat.Messages[2].Role != "user" || ContentToString(chat.Messages[2].Content) != "continue" { + t.Fatalf("trailing user message=%#v", chat.Messages[2]) + } + + unnamed := AnthropicMessagesRequest{ + Model: "deepseek-flash", + Messages: []AnthropicMessage{ + {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"toolu_2","content":[{"type":"tool_reference"}]}]`)}, + }, + } + chat, err = TranslateAnthropicMessages(unnamed) + if err != nil { + t.Fatal(err) + } + if ContentToString(chat.Messages[0].Content) == "" { + t.Fatalf("unnamed tool reference must still render text: %#v", chat.Messages[0].Content) + } +}