Fixed: #1670
test(translator): add unit tests for OpenAI to Claude requests and tool result handling - Introduced tests for converting OpenAI requests to Claude with text, base64 images, and URL images in tool results. - Refactored `convertClaudeToolResultContent` and related functionality to properly handle raw content with images and text. - Updated conversion logic to streamline image handling for both base64 and URL formats.
This commit is contained in:
@@ -203,46 +203,9 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
partType := part.Get("type").String()
|
claudePart := convertOpenAIContentPartToClaudePart(part)
|
||||||
|
if claudePart != "" {
|
||||||
switch partType {
|
msg, _ = sjson.SetRaw(msg, "content.-1", claudePart)
|
||||||
case "text":
|
|
||||||
textPart := `{"type":"text","text":""}`
|
|
||||||
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", textPart)
|
|
||||||
|
|
||||||
case "image_url":
|
|
||||||
// Convert OpenAI image format to Claude Code format
|
|
||||||
imageURL := part.Get("image_url.url").String()
|
|
||||||
if strings.HasPrefix(imageURL, "data:") {
|
|
||||||
// Extract base64 data and media type from data URL
|
|
||||||
parts := strings.Split(imageURL, ",")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
mediaTypePart := strings.Split(parts[0], ";")[0]
|
|
||||||
mediaType := strings.TrimPrefix(mediaTypePart, "data:")
|
|
||||||
data := parts[1]
|
|
||||||
|
|
||||||
imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
|
||||||
imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
|
|
||||||
imagePart, _ = sjson.Set(imagePart, "source.data", data)
|
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", imagePart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case "file":
|
|
||||||
fileData := part.Get("file.file_data").String()
|
|
||||||
if strings.HasPrefix(fileData, "data:") {
|
|
||||||
semicolonIdx := strings.Index(fileData, ";")
|
|
||||||
commaIdx := strings.Index(fileData, ",")
|
|
||||||
if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx {
|
|
||||||
mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:")
|
|
||||||
data := fileData[commaIdx+1:]
|
|
||||||
docPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}`
|
|
||||||
docPart, _ = sjson.Set(docPart, "source.media_type", mediaType)
|
|
||||||
docPart, _ = sjson.Set(docPart, "source.data", data)
|
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", docPart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -291,11 +254,16 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
case "tool":
|
case "tool":
|
||||||
// Handle tool result messages conversion
|
// Handle tool result messages conversion
|
||||||
toolCallID := message.Get("tool_call_id").String()
|
toolCallID := message.Get("tool_call_id").String()
|
||||||
content := message.Get("content").String()
|
toolContentResult := message.Get("content")
|
||||||
|
|
||||||
msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
|
msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
|
||||||
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
||||||
msg, _ = sjson.Set(msg, "content.0.content", content)
|
toolResultContent, toolResultContentRaw := convertOpenAIToolResultContent(toolContentResult)
|
||||||
|
if toolResultContentRaw {
|
||||||
|
msg, _ = sjson.SetRaw(msg, "content.0.content", toolResultContent)
|
||||||
|
} else {
|
||||||
|
msg, _ = sjson.Set(msg, "content.0.content", toolResultContent)
|
||||||
|
}
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||||
messageIndex++
|
messageIndex++
|
||||||
}
|
}
|
||||||
@@ -358,3 +326,110 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
return []byte(out)
|
return []byte(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertOpenAIContentPartToClaudePart(part gjson.Result) string {
|
||||||
|
switch part.Get("type").String() {
|
||||||
|
case "text":
|
||||||
|
textPart := `{"type":"text","text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
||||||
|
return textPart
|
||||||
|
|
||||||
|
case "image_url":
|
||||||
|
return convertOpenAIImageURLToClaudePart(part.Get("image_url.url").String())
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
fileData := part.Get("file.file_data").String()
|
||||||
|
if strings.HasPrefix(fileData, "data:") {
|
||||||
|
semicolonIdx := strings.Index(fileData, ";")
|
||||||
|
commaIdx := strings.Index(fileData, ",")
|
||||||
|
if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx {
|
||||||
|
mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:")
|
||||||
|
data := fileData[commaIdx+1:]
|
||||||
|
docPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}`
|
||||||
|
docPart, _ = sjson.Set(docPart, "source.media_type", mediaType)
|
||||||
|
docPart, _ = sjson.Set(docPart, "source.data", data)
|
||||||
|
return docPart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertOpenAIImageURLToClaudePart(imageURL string) string {
|
||||||
|
if imageURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(imageURL, "data:") {
|
||||||
|
parts := strings.SplitN(imageURL, ",", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaTypePart := strings.SplitN(parts[0], ";", 2)[0]
|
||||||
|
mediaType := strings.TrimPrefix(mediaTypePart, "data:")
|
||||||
|
if mediaType == "" {
|
||||||
|
mediaType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
||||||
|
imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
|
||||||
|
imagePart, _ = sjson.Set(imagePart, "source.data", parts[1])
|
||||||
|
return imagePart
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePart := `{"type":"image","source":{"type":"url","url":""}}`
|
||||||
|
imagePart, _ = sjson.Set(imagePart, "source.url", imageURL)
|
||||||
|
return imagePart
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertOpenAIToolResultContent(content gjson.Result) (string, bool) {
|
||||||
|
if !content.Exists() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.Type == gjson.String {
|
||||||
|
return content.String(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.IsArray() {
|
||||||
|
claudeContent := "[]"
|
||||||
|
partCount := 0
|
||||||
|
|
||||||
|
content.ForEach(func(_, part gjson.Result) bool {
|
||||||
|
if part.Type == gjson.String {
|
||||||
|
textPart := `{"type":"text","text":""}`
|
||||||
|
textPart, _ = sjson.Set(textPart, "text", part.String())
|
||||||
|
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", textPart)
|
||||||
|
partCount++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
claudePart := convertOpenAIContentPartToClaudePart(part)
|
||||||
|
if claudePart != "" {
|
||||||
|
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart)
|
||||||
|
partCount++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if partCount > 0 || len(content.Array()) == 0 {
|
||||||
|
return claudeContent, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.Raw, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if content.IsObject() {
|
||||||
|
claudePart := convertOpenAIContentPartToClaudePart(content)
|
||||||
|
if claudePart != "" {
|
||||||
|
claudeContent := "[]"
|
||||||
|
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart)
|
||||||
|
return claudeContent, true
|
||||||
|
}
|
||||||
|
return content.Raw, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.Raw, false
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package chat_completions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertOpenAIRequestToClaude_ToolResultTextAndBase64Image(t *testing.T) {
|
||||||
|
inputJSON := `{
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "do_work",
|
||||||
|
"arguments": "{\"a\":1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_1",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "tool ok"},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
|
||||||
|
resultJSON := gjson.ParseBytes(result)
|
||||||
|
messages := resultJSON.Get("messages").Array()
|
||||||
|
|
||||||
|
if len(messages) != 2 {
|
||||||
|
t.Fatalf("Expected 2 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolResult := messages[1].Get("content.0")
|
||||||
|
if got := toolResult.Get("type").String(); got != "tool_result" {
|
||||||
|
t.Fatalf("Expected content[0].type %q, got %q", "tool_result", got)
|
||||||
|
}
|
||||||
|
if got := toolResult.Get("tool_use_id").String(); got != "call_1" {
|
||||||
|
t.Fatalf("Expected tool_use_id %q, got %q", "call_1", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolContent := toolResult.Get("content")
|
||||||
|
if !toolContent.IsArray() {
|
||||||
|
t.Fatalf("Expected tool_result content array, got %s", toolContent.Raw)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.type").String(); got != "text" {
|
||||||
|
t.Fatalf("Expected first tool_result part type %q, got %q", "text", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.text").String(); got != "tool ok" {
|
||||||
|
t.Fatalf("Expected first tool_result part text %q, got %q", "tool ok", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.type").String(); got != "image" {
|
||||||
|
t.Fatalf("Expected second tool_result part type %q, got %q", "image", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.source.type").String(); got != "base64" {
|
||||||
|
t.Fatalf("Expected image source type %q, got %q", "base64", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.source.media_type").String(); got != "image/png" {
|
||||||
|
t.Fatalf("Expected image media type %q, got %q", "image/png", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.source.data").String(); got != "iVBORw0KGgoAAAANSUhEUg==" {
|
||||||
|
t.Fatalf("Unexpected base64 image data: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertOpenAIRequestToClaude_ToolResultURLImageOnly(t *testing.T) {
|
||||||
|
inputJSON := `{
|
||||||
|
"model": "gpt-4.1",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "do_work",
|
||||||
|
"arguments": "{\"a\":1}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_1",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": "https://example.com/tool.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-5", []byte(inputJSON), false)
|
||||||
|
resultJSON := gjson.ParseBytes(result)
|
||||||
|
messages := resultJSON.Get("messages").Array()
|
||||||
|
|
||||||
|
if len(messages) != 2 {
|
||||||
|
t.Fatalf("Expected 2 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolContent := messages[1].Get("content.0.content")
|
||||||
|
if !toolContent.IsArray() {
|
||||||
|
t.Fatalf("Expected tool_result content array, got %s", toolContent.Raw)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.type").String(); got != "image" {
|
||||||
|
t.Fatalf("Expected tool_result part type %q, got %q", "image", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.source.type").String(); got != "url" {
|
||||||
|
t.Fatalf("Expected image source type %q, got %q", "url", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.source.url").String(); got != "https://example.com/tool.png" {
|
||||||
|
t.Fatalf("Unexpected image URL: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,7 +183,12 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
// Collect tool_result to emit after the main message (ensures tool results follow tool_calls)
|
// Collect tool_result to emit after the main message (ensures tool results follow tool_calls)
|
||||||
toolResultJSON := `{"role":"tool","tool_call_id":"","content":""}`
|
toolResultJSON := `{"role":"tool","tool_call_id":"","content":""}`
|
||||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
toolResultJSON, _ = sjson.Set(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
||||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "content", convertClaudeToolResultContentToString(part.Get("content")))
|
toolResultContent, toolResultContentRaw := convertClaudeToolResultContent(part.Get("content"))
|
||||||
|
if toolResultContentRaw {
|
||||||
|
toolResultJSON, _ = sjson.SetRaw(toolResultJSON, "content", toolResultContent)
|
||||||
|
} else {
|
||||||
|
toolResultJSON, _ = sjson.Set(toolResultJSON, "content", toolResultContent)
|
||||||
|
}
|
||||||
toolResults = append(toolResults, toolResultJSON)
|
toolResults = append(toolResults, toolResultJSON)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -374,21 +379,41 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertClaudeToolResultContentToString(content gjson.Result) string {
|
func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
||||||
if !content.Exists() {
|
if !content.Exists() {
|
||||||
return ""
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
if content.Type == gjson.String {
|
if content.Type == gjson.String {
|
||||||
return content.String()
|
return content.String(), false
|
||||||
}
|
}
|
||||||
|
|
||||||
if content.IsArray() {
|
if content.IsArray() {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
contentJSON := "[]"
|
||||||
|
hasImagePart := false
|
||||||
content.ForEach(func(_, item gjson.Result) bool {
|
content.ForEach(func(_, item gjson.Result) bool {
|
||||||
switch {
|
switch {
|
||||||
case item.Type == gjson.String:
|
case item.Type == gjson.String:
|
||||||
parts = append(parts, item.String())
|
text := item.String()
|
||||||
|
parts = append(parts, text)
|
||||||
|
textContent := `{"type":"text","text":""}`
|
||||||
|
textContent, _ = sjson.Set(textContent, "text", text)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
||||||
|
case item.IsObject() && item.Get("type").String() == "text":
|
||||||
|
text := item.Get("text").String()
|
||||||
|
parts = append(parts, text)
|
||||||
|
textContent := `{"type":"text","text":""}`
|
||||||
|
textContent, _ = sjson.Set(textContent, "text", text)
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
||||||
|
case item.IsObject() && item.Get("type").String() == "image":
|
||||||
|
contentItem, ok := convertClaudeContentPart(item)
|
||||||
|
if ok {
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
||||||
|
hasImagePart = true
|
||||||
|
} else {
|
||||||
|
parts = append(parts, item.Raw)
|
||||||
|
}
|
||||||
case item.IsObject() && item.Get("text").Exists() && item.Get("text").Type == gjson.String:
|
case item.IsObject() && item.Get("text").Exists() && item.Get("text").Type == gjson.String:
|
||||||
parts = append(parts, item.Get("text").String())
|
parts = append(parts, item.Get("text").String())
|
||||||
default:
|
default:
|
||||||
@@ -397,19 +422,31 @@ func convertClaudeToolResultContentToString(content gjson.Result) string {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if hasImagePart {
|
||||||
|
return contentJSON, true
|
||||||
|
}
|
||||||
|
|
||||||
joined := strings.Join(parts, "\n\n")
|
joined := strings.Join(parts, "\n\n")
|
||||||
if strings.TrimSpace(joined) != "" {
|
if strings.TrimSpace(joined) != "" {
|
||||||
return joined
|
return joined, false
|
||||||
}
|
}
|
||||||
return content.Raw
|
return content.Raw, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if content.IsObject() {
|
if content.IsObject() {
|
||||||
if text := content.Get("text"); text.Exists() && text.Type == gjson.String {
|
if content.Get("type").String() == "image" {
|
||||||
return text.String()
|
contentItem, ok := convertClaudeContentPart(content)
|
||||||
|
if ok {
|
||||||
|
contentJSON := "[]"
|
||||||
|
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
||||||
|
return contentJSON, true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return content.Raw
|
if text := content.Get("text"); text.Exists() && text.Type == gjson.String {
|
||||||
|
return text.String(), false
|
||||||
|
}
|
||||||
|
return content.Raw, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.Raw
|
return content.Raw, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,6 +488,114 @@ func TestConvertClaudeRequestToOpenAI_ToolResultObjectContent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToOpenAI_ToolResultTextAndImageContent(t *testing.T) {
|
||||||
|
inputJSON := `{
|
||||||
|
"model": "claude-3-opus",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [
|
||||||
|
{"type": "tool_use", "id": "call_1", "name": "do_work", "input": {"a": 1}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": "call_1",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": "tool ok"},
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": "image/png",
|
||||||
|
"data": "iVBORw0KGgoAAAANSUhEUg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := ConvertClaudeRequestToOpenAI("test-model", []byte(inputJSON), false)
|
||||||
|
resultJSON := gjson.ParseBytes(result)
|
||||||
|
messages := resultJSON.Get("messages").Array()
|
||||||
|
|
||||||
|
if len(messages) != 2 {
|
||||||
|
t.Fatalf("Expected 2 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolContent := messages[1].Get("content")
|
||||||
|
if !toolContent.IsArray() {
|
||||||
|
t.Fatalf("Expected tool content array, got %s", toolContent.Raw)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.type").String(); got != "text" {
|
||||||
|
t.Fatalf("Expected first tool content type %q, got %q", "text", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.text").String(); got != "tool ok" {
|
||||||
|
t.Fatalf("Expected first tool content text %q, got %q", "tool ok", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.type").String(); got != "image_url" {
|
||||||
|
t.Fatalf("Expected second tool content type %q, got %q", "image_url", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("1.image_url.url").String(); got != "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==" {
|
||||||
|
t.Fatalf("Unexpected image_url: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertClaudeRequestToOpenAI_ToolResultURLImageOnly(t *testing.T) {
|
||||||
|
inputJSON := `{
|
||||||
|
"model": "claude-3-opus",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [
|
||||||
|
{"type": "tool_use", "id": "call_1", "name": "do_work", "input": {"a": 1}}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tool_result",
|
||||||
|
"tool_use_id": "call_1",
|
||||||
|
"content": {
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "url",
|
||||||
|
"url": "https://example.com/tool.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := ConvertClaudeRequestToOpenAI("test-model", []byte(inputJSON), false)
|
||||||
|
resultJSON := gjson.ParseBytes(result)
|
||||||
|
messages := resultJSON.Get("messages").Array()
|
||||||
|
|
||||||
|
if len(messages) != 2 {
|
||||||
|
t.Fatalf("Expected 2 messages, got %d. Messages: %s", len(messages), resultJSON.Get("messages").Raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
toolContent := messages[1].Get("content")
|
||||||
|
if !toolContent.IsArray() {
|
||||||
|
t.Fatalf("Expected tool content array, got %s", toolContent.Raw)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.type").String(); got != "image_url" {
|
||||||
|
t.Fatalf("Expected tool content type %q, got %q", "image_url", got)
|
||||||
|
}
|
||||||
|
if got := toolContent.Get("0.image_url.url").String(); got != "https://example.com/tool.png" {
|
||||||
|
t.Fatalf("Unexpected image_url: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvertClaudeRequestToOpenAI_AssistantTextToolUseTextOrder(t *testing.T) {
|
func TestConvertClaudeRequestToOpenAI_AssistantTextToolUseTextOrder(t *testing.T) {
|
||||||
inputJSON := `{
|
inputJSON := `{
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
|
|||||||
Reference in New Issue
Block a user