feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

@@ -7,8 +7,10 @@ import (
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
"github.com/QuantumNous/new-api/relaykit/relayconvert/internal/convdiag"
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
"github.com/QuantumNous/new-api/relaykit/types"
)
const (
@@ -154,7 +156,13 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
}
}
for _, claudeMessage := range claudeRequest.Messages {
type unnamedToolResult struct {
index int
id string
}
toolNames := make(map[string]string)
var unnamedToolResults []unnamedToolResult
for messageIndex, claudeMessage := range claudeRequest.Messages {
openAIMessage := dto.Message{
Role: claudeMessage.Role,
}
@@ -168,7 +176,10 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
var toolCalls []dto.ToolCallRequest
mediaMessages := make([]dto.MediaContent, 0, len(content))
for _, mediaMsg := range content {
for blockIndex, mediaMsg := range content {
if _, exists := toolNames[mediaMsg.Id]; !exists {
toolNames[mediaMsg.Id] = mediaMsg.Name
}
switch mediaMsg.Type {
case "text", "input_text":
message := dto.MediaContent{
@@ -178,12 +189,50 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
}
mediaMessages = append(mediaMessages, message)
case "image":
imageData := fmt.Sprintf("data:%s;base64,%s", mediaMsg.Source.MediaType, mediaMsg.Source.Data)
mediaMessage := dto.MediaContent{
Type: "image_url",
ImageUrl: &dto.MessageImageUrl{Url: imageData},
url := claudeImageSourceURL(mediaMsg.Source)
if url == "" {
sourceType := ""
if mediaMsg.Source != nil {
sourceType = mediaMsg.Source.Type
}
convdiag.Add(ctx, types.ConversionDiagnostic{
Code: "unsupported_media_source",
Path: fmt.Sprintf("messages[%d].content[%d].source", messageIndex, blockIndex),
Message: fmt.Sprintf("OpenAI Chat Completions cannot reference a Claude image with source type %q; the image was omitted", sourceType),
Severity: types.ConversionDiagnosticError,
})
continue
}
mediaMessages = append(mediaMessages, mediaMessage)
mediaMessages = append(mediaMessages, dto.MediaContent{
Type: "image_url",
ImageUrl: &dto.MessageImageUrl{Url: url},
})
case "document":
if mediaMsg.Source != nil && mediaMsg.Source.Type == "text" {
// A plain-text document keeps its content as a Chat text part.
mediaMessages = append(mediaMessages, dto.MediaContent{Type: "text", Text: kitutil.Interface2String(mediaMsg.Source.Data)})
continue
}
if mediaMsg.Source == nil || mediaMsg.Source.Type != "base64" || mediaMsg.Source.MediaType != "application/pdf" {
sourceType, mediaType := "", ""
if mediaMsg.Source != nil {
sourceType, mediaType = mediaMsg.Source.Type, mediaMsg.Source.MediaType
}
convdiag.Add(ctx, types.ConversionDiagnostic{
Code: "unsupported_media_source",
Path: fmt.Sprintf("messages[%d].content[%d].source", messageIndex, blockIndex),
Message: fmt.Sprintf("OpenAI Chat Completions carries only base64 PDF and plain-text documents, not a %q source of type %q; the document was omitted", sourceType, mediaType),
Severity: types.ConversionDiagnosticError,
})
continue
}
mediaMessages = append(mediaMessages, dto.MediaContent{
Type: dto.ContentTypeFile,
File: &dto.MessageFile{
FileName: "document.pdf",
FileData: "data:application/pdf;base64," + kitutil.Interface2String(mediaMsg.Source.Data),
},
})
case "tool_use":
toolCall := dto.ToolCallRequest{
ID: mediaMsg.Id,
@@ -197,7 +246,7 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
case "tool_result":
toolName := mediaMsg.Name
if toolName == "" {
toolName = claudeRequest.SearchToolNameByToolCallId(mediaMsg.ToolUseId)
unnamedToolResults = append(unnamedToolResults, unnamedToolResult{index: len(openAIMessages), id: mediaMsg.ToolUseId})
}
oaiToolMessage := dto.Message{
Role: "tool",
@@ -207,9 +256,12 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
if mediaMsg.IsStringContent() {
oaiToolMessage.SetStringContent(mediaMsg.GetStringContent())
} else {
mediaContents := mediaMsg.ParseMediaContent()
encodedJSON, _ := kitutil.Marshal(mediaContents)
oaiToolMessage.SetStringContent(string(encodedJSON))
content, media := claudeToolResultToChat(mediaMsg.ParseMediaContent())
oaiToolMessage.SetStringContent(content)
// A Chat tool message only carries text. Images from the tool result
// join the user message this Claude message becomes, which lands right
// after the tool batch and keeps the tool messages contiguous.
mediaMessages = append(mediaMessages, media...)
}
openAIMessages = append(openAIMessages, oaiToolMessage)
}
@@ -218,7 +270,7 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
if len(toolCalls) > 0 {
openAIMessage.SetToolCalls(toolCalls)
}
if len(mediaMessages) > 0 && len(toolCalls) == 0 {
if len(mediaMessages) > 0 {
openAIMessage.SetMediaContent(mediaMessages)
}
}
@@ -226,11 +278,66 @@ func ClaudeMessagesRequestToOpenAIChat(ctx context.Context, claudeRequest dto.Cl
openAIMessages = append(openAIMessages, openAIMessage)
}
}
for _, result := range unnamedToolResults {
*openAIMessages[result.index].Name = toolNames[result.id]
}
openAIRequest.Messages = openAIMessages
return &openAIRequest, nil
}
// claudeToolResultToChat maps a structured tool_result content array onto Chat Completions,
// where a tool message may only carry text. Text blocks stay on the tool message and image
// blocks come back in Chat shape for the caller to place on the following user message;
// stringifying them instead would hand base64 image data to the upstream text tokenizer.
// Arrays holding any other block type keep the historical stringified form so nothing is lost.
func claudeToolResultToChat(blocks []dto.ClaudeMediaMessage) (string, []dto.MediaContent) {
texts := make([]string, 0, len(blocks))
media := make([]dto.MediaContent, 0, len(blocks))
for _, block := range blocks {
switch {
case block.Type == "text" || block.Type == "input_text":
if text := block.GetText(); text != "" {
texts = append(texts, text)
}
case block.Type == "image":
url := claudeImageSourceURL(block.Source)
if url == "" {
return requestToJSONString(blocks), nil
}
media = append(media, dto.MediaContent{Type: "image_url", ImageUrl: &dto.MessageImageUrl{Url: url}})
default:
return requestToJSONString(blocks), nil
}
}
switch {
case len(texts) == 0 && len(media) == 0:
return requestToJSONString(blocks), nil
case len(texts) == 0:
// Upstreams reject empty tool content; the images ride on the following user message.
return "[image]", media
default:
return strings.Join(texts, "\n"), media
}
}
// claudeImageSourceURL is the Chat image_url for a Claude image source: a data
// URL for base64 data or the URL of a url source. Other sources, such as Files
// API references, have no Chat equivalent and yield "".
func claudeImageSourceURL(source *dto.ClaudeMessageSource) string {
if source == nil || source.Type == "file" {
return ""
}
if source.Url != "" {
return source.Url
}
data := kitutil.Interface2String(source.Data)
if data == "" {
return ""
}
return fmt.Sprintf("data:%s;base64,%s", source.MediaType, data)
}
func requestToJSONString(v any) string {
b, err := kitutil.Marshal(v)
if err != nil {