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

@@ -3,6 +3,7 @@ package oairesponses
import (
"context"
"fmt"
"slices"
"strings"
"github.com/QuantumNous/new-api/relaykit/dto"
@@ -12,6 +13,7 @@ import (
sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude"
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
"github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning"
"github.com/QuantumNous/new-api/relaykit/types"
)
func convertOpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Meta, request any) (any, error) {
@@ -94,11 +96,26 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
itemType := strings.TrimSpace(kitutil.Interface2String(item["type"]))
switch itemType {
case ResponsesInputTypeFunctionCall:
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, "arguments"))
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item))
case ResponsesInputTypeCustomToolCall:
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, "input"))
name := responsesCallName(item)
if name == "" {
return nil, fmt.Errorf("custom_tool_call item is missing name")
}
// The custom tool is declared as a function taking one string
// argument, so its raw input is replayed in that shape.
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, dto.ClaudeMediaMessage{
Type: "tool_use",
Id: CallID(item),
Name: name,
Input: map[string]any{convmeta.CustomToolInputArgument: responsesArgumentsString(item["input"])},
})
case ResponsesInputTypeFunctionCallOutput, ResponsesInputTypeCustomToolOutput:
claudeRequest.Messages = appendClaudeToolResult(claudeRequest.Messages, responsesFunctionOutputItemToClaudeToolResult(item))
claudeRequest.Messages = appendClaudeToolResult(claudeRequest.Messages, dto.ClaudeMediaMessage{
Type: "tool_result",
ToolUseId: CallID(item),
Content: responsesToolOutputToClaude(c, item["output"]),
})
default:
sourceRole := strings.TrimSpace(kitutil.Interface2String(item["role"]))
role := responsesClaudeRole(sourceRole)
@@ -131,6 +148,11 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
})
}
}
claudeRequest.Messages = omitTrailingAssistantText(c, claudeRequest.Messages,
func(message dto.ClaudeMessage) bool { return message.Role == "user" },
func(message dto.ClaudeMessage) bool {
return message.Role == "assistant" && !slices.ContainsFunc(claudeMessageContentParts(message.Content), func(part dto.ClaudeMediaMessage) bool { return part.Type == "tool_use" })
})
if len(systemMessages) > 0 {
claudeRequest.System = systemMessages
@@ -181,50 +203,81 @@ func responsesInputContentToClaudeMediaMessages(c context.Context, content any)
if source == nil {
continue
}
base64Data, mimeType, err := relaymedia.ResolveBase64Data(c, source, "formatting Responses input for Claude")
if err != nil {
return nil, fmt.Errorf("get file data failed: %s", err.Error())
// Claude content blocks carry images, PDF documents, and text
// documents; audio and video have no Claude block.
reason := fmt.Sprintf("Claude Messages cannot carry %s content", partType)
var block dto.ClaudeMediaMessage
if partType == "input_image" || partType == "input_file" {
base64Data, mimeType, err := relaymedia.ResolveBase64Data(c, source, "formatting Responses input for Claude")
if err != nil {
return nil, fmt.Errorf("get file data failed: %s", err.Error())
}
block, reason = sharedclaude.MediaBlock(base64Data, mimeType, partType == "input_image")
}
claudePart := dto.ClaudeMediaMessage{
Source: &dto.ClaudeMessageSource{
Type: "base64",
MediaType: mimeType,
Data: base64Data,
},
if reason != "" {
convdiag.Add(c, types.ConversionDiagnostic{
Code: "unsupported_media_type",
Path: "input.content",
Message: reason + "; the part was omitted",
Severity: types.ConversionDiagnosticError,
})
continue
}
if strings.HasPrefix(mimeType, "application/pdf") {
claudePart.Type = "document"
} else {
claudePart.Type = "image"
}
parts = append(parts, claudePart)
parts = append(parts, block)
}
}
return parts, nil
}
func responsesFunctionCallItemToClaudeToolUse(item map[string]any, inputKey string) dto.ClaudeMediaMessage {
func responsesFunctionCallItemToClaudeToolUse(item map[string]any) dto.ClaudeMediaMessage {
return dto.ClaudeMediaMessage{
Type: "tool_use",
Id: CallID(item),
Name: strings.TrimSpace(kitutil.Interface2String(item["name"])),
Input: ObjectValue(item[inputKey], inputKey),
Name: responsesCallName(item),
Input: ObjectValue(item["arguments"], "arguments"),
}
}
func responsesFunctionOutputItemToClaudeToolResult(item map[string]any) dto.ClaudeMediaMessage {
return dto.ClaudeMediaMessage{
Type: "tool_result",
ToolUseId: CallID(item),
Content: responsesToolOutputValue(item["output"]),
// responsesToolOutputToClaude maps a function_call_output payload onto Claude
// tool_result content. A Responses content-part array becomes the text, image,
// and document blocks tool_result accepts; stringifying it instead would hand
// base64 media to the upstream text tokenizer. An array holding any other part
// type, or media that cannot be resolved, keeps the stringified form, and other
// payload shapes pass through unchanged.
func responsesToolOutputToClaude(c context.Context, value any) any {
rawParts, ok := value.([]any)
if !ok || len(rawParts) == 0 {
if value == nil {
return ""
}
return value
}
}
func responsesToolOutputValue(value any) any {
if value == nil {
return ""
labels := make([]string, 0, len(rawParts))
for _, rawPart := range rawParts {
part, isMap := rawPart.(map[string]any)
if !isMap {
return responseToolOutputToChatContent(value)
}
switch partType := strings.TrimSpace(kitutil.Interface2String(part["type"])); partType {
case "input_text", "output_text", "text":
case "input_image", "input_file", "input_audio", "input_video":
if label := "[" + strings.TrimPrefix(partType, "input_") + "]"; !slices.Contains(labels, label) {
labels = append(labels, label)
}
default:
return responseToolOutputToChatContent(value)
}
}
return value
blocks, err := responsesInputContentToClaudeMediaMessages(c, rawParts)
if err != nil {
return responseToolOutputToChatContent(value)
}
if len(blocks) == 0 {
// Every media part was omitted with a diagnostic; name what was there
// instead of sending the payload as text.
return strings.Join(labels, " ")
}
return blocks
}
func appendClaudeToolUse(messages []dto.ClaudeMessage, toolUse dto.ClaudeMediaMessage) []dto.ClaudeMessage {