mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-23 09:47:43 +08:00
Fix Claude tool block normalization and tool_result fidelity
This commit is contained in:
@@ -98,6 +98,38 @@ func TestNormalizeClaudeMessagesToolUseToAssistantToolCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeClaudeMessagesDoesNotPromoteUserToolUse(t *testing.T) {
|
||||||
|
msgs := []any{
|
||||||
|
map[string]any{
|
||||||
|
"role": "user",
|
||||||
|
"content": []any{
|
||||||
|
map[string]any{
|
||||||
|
"type": "tool_use",
|
||||||
|
"id": "call_unsafe",
|
||||||
|
"name": "dangerous_tool",
|
||||||
|
"input": map[string]any{"value": "x"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeClaudeMessages(msgs)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected one normalized message, got %d", len(got))
|
||||||
|
}
|
||||||
|
m := got[0].(map[string]any)
|
||||||
|
if m["role"] != "user" {
|
||||||
|
t.Fatalf("expected user role preserved, got %#v", m["role"])
|
||||||
|
}
|
||||||
|
if _, ok := m["tool_calls"]; ok {
|
||||||
|
t.Fatalf("expected no tool_calls promotion for user message, got %#v", m["tool_calls"])
|
||||||
|
}
|
||||||
|
content, _ := m["content"].(string)
|
||||||
|
if !containsStr(content, `"type":"tool_use"`) || !containsStr(content, "dangerous_tool") {
|
||||||
|
t.Fatalf("expected raw tool_use block preserved in user content, got %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeClaudeMessagesSkipsNonMap(t *testing.T) {
|
func TestNormalizeClaudeMessagesSkipsNonMap(t *testing.T) {
|
||||||
msgs := []any{"not a map", 42}
|
msgs := []any{"not a map", 42}
|
||||||
got := normalizeClaudeMessages(msgs)
|
got := normalizeClaudeMessages(msgs)
|
||||||
@@ -149,6 +181,47 @@ func TestNormalizeClaudeMessagesMixedContentBlocks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeClaudeMessagesToolResultNonTextPayloadStringified(t *testing.T) {
|
||||||
|
msgs := []any{
|
||||||
|
map[string]any{
|
||||||
|
"role": "user",
|
||||||
|
"content": []any{
|
||||||
|
map[string]any{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": "call_image_1",
|
||||||
|
"name": "vision_tool",
|
||||||
|
"content": []any{
|
||||||
|
map[string]any{"type": "text", "text": "image analysis"},
|
||||||
|
map[string]any{
|
||||||
|
"type": "image",
|
||||||
|
"source": map[string]any{"type": "base64", "media_type": "image/png", "data": strings.Repeat("B", 2048)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := normalizeClaudeMessages(msgs)
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected one normalized message, got %d", len(got))
|
||||||
|
}
|
||||||
|
m := got[0].(map[string]any)
|
||||||
|
if m["role"] != "tool" {
|
||||||
|
t.Fatalf("expected tool role, got %#v", m["role"])
|
||||||
|
}
|
||||||
|
content, _ := m["content"].(string)
|
||||||
|
if !containsStr(content, `"type":"tool_result"`) || !containsStr(content, `"type":"image"`) {
|
||||||
|
t.Fatalf("expected non-text tool_result payload to be JSON stringified, got %q", content)
|
||||||
|
}
|
||||||
|
if !containsStr(content, omittedBinaryMarker) {
|
||||||
|
t.Fatalf("expected binary data to be sanitized with omitted marker, got %q", content)
|
||||||
|
}
|
||||||
|
if containsStr(content, strings.Repeat("B", 100)) {
|
||||||
|
t.Fatalf("expected raw base64 payload not to be included, got %q", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── buildClaudeToolPrompt ───────────────────────────────────────────
|
// ─── buildClaudeToolPrompt ───────────────────────────────────────────
|
||||||
|
|
||||||
func TestBuildClaudeToolPromptSingleTool(t *testing.T) {
|
func TestBuildClaudeToolPromptSingleTool(t *testing.T) {
|
||||||
|
|||||||
@@ -39,9 +39,15 @@ func normalizeClaudeMessages(messages []any) []any {
|
|||||||
textParts = append(textParts, t)
|
textParts = append(textParts, t)
|
||||||
}
|
}
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
flushText()
|
if role == "assistant" {
|
||||||
if toolMsg := normalizeClaudeToolUseToAssistant(b); toolMsg != nil {
|
flushText()
|
||||||
out = append(out, toolMsg)
|
if toolMsg := normalizeClaudeToolUseToAssistant(b); toolMsg != nil {
|
||||||
|
out = append(out, toolMsg)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if raw := strings.TrimSpace(formatClaudeUnknownBlockForPrompt(b)); raw != "" {
|
||||||
|
textParts = append(textParts, raw)
|
||||||
}
|
}
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
flushText()
|
flushText()
|
||||||
@@ -159,7 +165,7 @@ func normalizeClaudeToolResultToToolMessage(block map[string]any) map[string]any
|
|||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": toolCallID,
|
"tool_call_id": toolCallID,
|
||||||
"content": block["content"],
|
"content": normalizeClaudeToolResultContent(block["content"]),
|
||||||
}
|
}
|
||||||
if name := strings.TrimSpace(fmt.Sprintf("%v", block["name"])); name != "" {
|
if name := strings.TrimSpace(fmt.Sprintf("%v", block["name"])); name != "" {
|
||||||
out["name"] = name
|
out["name"] = name
|
||||||
@@ -167,6 +173,21 @@ func normalizeClaudeToolResultToToolMessage(block map[string]any) map[string]any
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeClaudeToolResultContent(content any) any {
|
||||||
|
if text, ok := content.(string); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
payload := map[string]any{
|
||||||
|
"type": "tool_result",
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(sanitizeClaudeBlockForPrompt(payload))
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(fmt.Sprintf("%v", content))
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func formatClaudeBlockRaw(block map[string]any) string {
|
func formatClaudeBlockRaw(block map[string]any) string {
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user