Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
583 lines
16 KiB
Go
583 lines
16 KiB
Go
package oairesponses
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
responsesInputTypeFunctionCall = "function_call"
|
|
responsesInputTypeFunctionCallOutput = "function_call_output"
|
|
responsesInputTypeCustomToolCall = "custom_tool_call"
|
|
responsesInputTypeCustomToolOutput = "custom_tool_call_output"
|
|
)
|
|
|
|
const (
|
|
ResponsesInputTypeFunctionCall = responsesInputTypeFunctionCall
|
|
ResponsesInputTypeFunctionCallOutput = responsesInputTypeFunctionCallOutput
|
|
ResponsesInputTypeCustomToolCall = responsesInputTypeCustomToolCall
|
|
ResponsesInputTypeCustomToolOutput = responsesInputTypeCustomToolOutput
|
|
)
|
|
|
|
func ResponsesRequestToChatCompletionsRequest(ctx context.Context, req *dto.OpenAIResponsesRequest) (*dto.GeneralOpenAIRequest, error) {
|
|
if req == nil {
|
|
return nil, errors.New("request is nil")
|
|
}
|
|
if req.Model == "" {
|
|
return nil, errors.New("model is required")
|
|
}
|
|
if err := validateResponsesRequestChatUnsupportedFields(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
messages, err := responsesRequestMessagesToChat(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tools, err := responsesRequestToolsToChat(req.Tools)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toolChoice, err := responsesRequestToolChoiceToChat(req.ToolChoice)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
responseFormat, err := responsesRequestTextToChatResponseFormat(req.Text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := &dto.GeneralOpenAIRequest{
|
|
Model: req.Model,
|
|
Messages: messages,
|
|
Stream: req.Stream,
|
|
StreamOptions: req.StreamOptions,
|
|
MaxCompletionTokens: req.MaxOutputTokens,
|
|
Temperature: req.Temperature,
|
|
TopP: req.TopP,
|
|
TopLogProbs: req.TopLogProbs,
|
|
ResponseFormat: responseFormat,
|
|
Tools: tools,
|
|
ToolChoice: toolChoice,
|
|
User: req.User,
|
|
Store: req.Store,
|
|
Metadata: req.Metadata,
|
|
SafetyIdentifier: req.SafetyIdentifier,
|
|
PromptCacheRetention: req.PromptCacheRetention,
|
|
EnableThinking: req.EnableThinking,
|
|
ThinkingBudget: req.ThinkingBudget,
|
|
}
|
|
|
|
out.FrequencyPenalty, err = responsesRawFloat(req.FrequencyPenalty)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid frequency_penalty: %w", err)
|
|
}
|
|
out.PresencePenalty, err = responsesRawFloat(req.PresencePenalty)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid presence_penalty: %w", err)
|
|
}
|
|
|
|
reasoningIntent, diagnostics, err := reasoning.FromOpenAIResponses(req)
|
|
if err != nil {
|
|
return nil, reasoning.AsClientError(err)
|
|
}
|
|
convdiag.Add(ctx, diagnostics...)
|
|
if err := reasoning.ApplyToOpenAIChat(out, reasoningIntent); err != nil {
|
|
return nil, reasoning.AsClientError(err)
|
|
}
|
|
if req.ServiceTier != "" {
|
|
out.ServiceTier, _ = kitutil.Marshal(req.ServiceTier)
|
|
}
|
|
if len(req.ParallelToolCalls) > 0 && kitutil.GetJsonType(req.ParallelToolCalls) == "boolean" {
|
|
var parallelToolCalls bool
|
|
if err := kitutil.Unmarshal(req.ParallelToolCalls, ¶llelToolCalls); err == nil {
|
|
out.ParallelTooCalls = ¶llelToolCalls
|
|
}
|
|
}
|
|
if len(req.PromptCacheKey) > 0 && kitutil.GetJsonType(req.PromptCacheKey) == "string" {
|
|
var promptCacheKey string
|
|
if err := kitutil.Unmarshal(req.PromptCacheKey, &promptCacheKey); err == nil {
|
|
out.PromptCacheKey = promptCacheKey
|
|
}
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func validateResponsesRequestChatUnsupportedFields(req *dto.OpenAIResponsesRequest) error {
|
|
unsupported := make([]string, 0, 4)
|
|
if rawJSONPresent(req.Conversation) {
|
|
unsupported = append(unsupported, "conversation")
|
|
}
|
|
if strings.TrimSpace(req.PreviousResponseID) != "" {
|
|
unsupported = append(unsupported, "previous_response_id")
|
|
}
|
|
if rawJSONPresent(req.Prompt) {
|
|
unsupported = append(unsupported, "prompt")
|
|
}
|
|
if rawJSONPresent(req.ContextManagement) {
|
|
unsupported = append(unsupported, "context_management")
|
|
}
|
|
if len(unsupported) > 0 {
|
|
return fmt.Errorf("responses to chat conversion does not support stateful fields: %s", strings.Join(unsupported, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateRequestChatUnsupportedFields(req *dto.OpenAIResponsesRequest) error {
|
|
return validateResponsesRequestChatUnsupportedFields(req)
|
|
}
|
|
|
|
func responsesRequestMessagesToChat(req *dto.OpenAIResponsesRequest) ([]dto.Message, error) {
|
|
messages := make([]dto.Message, 0)
|
|
if rawJSONPresent(req.Instructions) {
|
|
instructions, err := responsesJSONString(req.Instructions)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid instructions: %w", err)
|
|
}
|
|
if strings.TrimSpace(instructions) != "" {
|
|
messages = append(messages, dto.Message{Role: "system", Content: instructions})
|
|
}
|
|
}
|
|
|
|
if !rawJSONPresent(req.Input) {
|
|
return messages, nil
|
|
}
|
|
|
|
switch kitutil.GetJsonType(req.Input) {
|
|
case "string":
|
|
input, err := responsesJSONString(req.Input)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid input string: %w", err)
|
|
}
|
|
messages = append(messages, dto.Message{Role: "user", Content: input})
|
|
return messages, nil
|
|
case "array":
|
|
var items []map[string]any
|
|
if err := kitutil.Unmarshal(req.Input, &items); err != nil {
|
|
return nil, fmt.Errorf("invalid input array: %w", err)
|
|
}
|
|
for _, item := range items {
|
|
nextMessages, err := responsesInputItemToChatMessages(item, messages)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
messages = nextMessages
|
|
}
|
|
return messages, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported responses input type %q", kitutil.GetJsonType(req.Input))
|
|
}
|
|
}
|
|
|
|
func responsesInputItemToChatMessages(item map[string]any, messages []dto.Message) ([]dto.Message, error) {
|
|
itemType := strings.TrimSpace(kitutil.Interface2String(item["type"]))
|
|
switch itemType {
|
|
case responsesInputTypeFunctionCall:
|
|
toolCall, err := responsesFunctionCallItemToChatToolCall(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return appendToolCallToLastAssistant(messages, toolCall), nil
|
|
case responsesInputTypeCustomToolCall:
|
|
toolCall, err := responsesCustomToolCallItemToChatToolCall(item)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return appendToolCallToLastAssistant(messages, toolCall), nil
|
|
case responsesInputTypeFunctionCallOutput:
|
|
callID := strings.TrimSpace(kitutil.Interface2String(item["call_id"]))
|
|
content := responseToolOutputToChatContent(item["output"])
|
|
return append(messages, dto.Message{Role: "tool", ToolCallId: callID, Content: content}), nil
|
|
}
|
|
|
|
role := strings.TrimSpace(kitutil.Interface2String(item["role"]))
|
|
if role == "" {
|
|
role = "user"
|
|
}
|
|
content, err := responsesInputContentToChatContent(item["content"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(messages, dto.Message{Role: role, Content: content}), nil
|
|
}
|
|
|
|
func responsesInputContentToChatContent(content any) (any, error) {
|
|
if content == nil {
|
|
return "", nil
|
|
}
|
|
|
|
switch value := content.(type) {
|
|
case string:
|
|
return value, nil
|
|
case []any:
|
|
return responsesContentPartsToChatContent(value)
|
|
case []map[string]any:
|
|
parts := make([]any, 0, len(value))
|
|
for _, part := range value {
|
|
parts = append(parts, part)
|
|
}
|
|
return responsesContentPartsToChatContent(parts)
|
|
default:
|
|
return content, nil
|
|
}
|
|
}
|
|
|
|
func responsesContentPartsToChatContent(parts []any) (any, error) {
|
|
chatParts := make([]any, 0, len(parts))
|
|
var textOnly strings.Builder
|
|
onlyText := true
|
|
|
|
for _, rawPart := range parts {
|
|
part, ok := rawPart.(map[string]any)
|
|
if !ok {
|
|
onlyText = false
|
|
chatParts = append(chatParts, rawPart)
|
|
continue
|
|
}
|
|
|
|
partType := strings.TrimSpace(kitutil.Interface2String(part["type"]))
|
|
switch partType {
|
|
case "input_text", "output_text", "text":
|
|
text := kitutil.Interface2String(part["text"])
|
|
textOnly.WriteString(text)
|
|
chatParts = append(chatParts, map[string]any{
|
|
"type": dto.ContentTypeText,
|
|
"text": text,
|
|
})
|
|
case "input_image":
|
|
onlyText = false
|
|
chatParts = append(chatParts, map[string]any{
|
|
"type": dto.ContentTypeImageURL,
|
|
"image_url": responsesImagePartToChatImageURL(part),
|
|
})
|
|
case "input_file":
|
|
onlyText = false
|
|
chatParts = append(chatParts, map[string]any{
|
|
"type": dto.ContentTypeFile,
|
|
"file": responsesFilePartToChatFile(part),
|
|
})
|
|
case "input_audio":
|
|
onlyText = false
|
|
chatParts = append(chatParts, map[string]any{
|
|
"type": dto.ContentTypeInputAudio,
|
|
"input_audio": responsesPartPayload(part, "input_audio"),
|
|
})
|
|
case "input_video":
|
|
onlyText = false
|
|
chatParts = append(chatParts, map[string]any{
|
|
"type": dto.ContentTypeVideoUrl,
|
|
"video_url": responsesVideoPartToChatVideoURL(part),
|
|
})
|
|
default:
|
|
onlyText = false
|
|
chatParts = append(chatParts, part)
|
|
}
|
|
}
|
|
|
|
if onlyText {
|
|
return textOnly.String(), nil
|
|
}
|
|
return chatParts, nil
|
|
}
|
|
|
|
func responsesFunctionCallItemToChatToolCall(item map[string]any) (dto.ToolCallRequest, error) {
|
|
name := strings.TrimSpace(kitutil.Interface2String(item["name"]))
|
|
if name == "" {
|
|
return dto.ToolCallRequest{}, errors.New("function_call item is missing name")
|
|
}
|
|
return dto.ToolCallRequest{
|
|
ID: responsesCallID(item),
|
|
Type: "function",
|
|
Function: dto.FunctionRequest{
|
|
Name: name,
|
|
Arguments: responsesArgumentsString(item["arguments"]),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func responsesCustomToolCallItemToChatToolCall(item map[string]any) (dto.ToolCallRequest, error) {
|
|
raw, err := kitutil.Marshal(item)
|
|
if err != nil {
|
|
return dto.ToolCallRequest{}, err
|
|
}
|
|
return dto.ToolCallRequest{
|
|
ID: responsesCallID(item),
|
|
Type: dto.CustomType,
|
|
Custom: raw,
|
|
Function: dto.FunctionRequest{
|
|
Name: strings.TrimSpace(kitutil.Interface2String(item["name"])),
|
|
Arguments: responsesArgumentsString(item["input"]),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func appendToolCallToLastAssistant(messages []dto.Message, toolCall dto.ToolCallRequest) []dto.Message {
|
|
if len(messages) == 0 || messages[len(messages)-1].Role != "assistant" {
|
|
messages = append(messages, dto.Message{Role: "assistant"})
|
|
}
|
|
|
|
idx := len(messages) - 1
|
|
toolCalls := messages[idx].ParseToolCalls()
|
|
toolCalls = append(toolCalls, toolCall)
|
|
toolCallsRaw, _ := kitutil.Marshal(toolCalls)
|
|
messages[idx].ToolCalls = toolCallsRaw
|
|
return messages
|
|
}
|
|
|
|
func responsesRequestToolsToChat(raw json.RawMessage) ([]dto.ToolCallRequest, error) {
|
|
if !rawJSONPresent(raw) {
|
|
return nil, nil
|
|
}
|
|
|
|
var tools []map[string]any
|
|
if err := kitutil.Unmarshal(raw, &tools); err != nil {
|
|
return nil, fmt.Errorf("invalid tools: %w", err)
|
|
}
|
|
|
|
out := make([]dto.ToolCallRequest, 0, len(tools))
|
|
for _, tool := range tools {
|
|
toolType := strings.TrimSpace(kitutil.Interface2String(tool["type"]))
|
|
if toolType == "function" {
|
|
out = append(out, dto.ToolCallRequest{
|
|
Type: "function",
|
|
Function: dto.FunctionRequest{
|
|
Name: strings.TrimSpace(kitutil.Interface2String(tool["name"])),
|
|
Description: kitutil.Interface2String(tool["description"]),
|
|
Parameters: tool["parameters"],
|
|
},
|
|
})
|
|
continue
|
|
}
|
|
|
|
rawTool, err := kitutil.Marshal(tool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, dto.ToolCallRequest{
|
|
Type: toolType,
|
|
Custom: rawTool,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func responsesRequestToolChoiceToChat(raw json.RawMessage) (any, error) {
|
|
if !rawJSONPresent(raw) {
|
|
return nil, nil
|
|
}
|
|
if kitutil.GetJsonType(raw) == "string" {
|
|
var choice string
|
|
if err := kitutil.Unmarshal(raw, &choice); err != nil {
|
|
return nil, fmt.Errorf("invalid tool_choice: %w", err)
|
|
}
|
|
return choice, nil
|
|
}
|
|
|
|
var choice map[string]any
|
|
if err := kitutil.Unmarshal(raw, &choice); err != nil {
|
|
return nil, fmt.Errorf("invalid tool_choice: %w", err)
|
|
}
|
|
if kitutil.Interface2String(choice["type"]) == "function" {
|
|
name := strings.TrimSpace(kitutil.Interface2String(choice["name"]))
|
|
if name != "" {
|
|
return map[string]any{
|
|
"type": "function",
|
|
"function": map[string]any{
|
|
"name": name,
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return choice, nil
|
|
}
|
|
|
|
func RequestToolChoiceToChat(raw json.RawMessage) (any, error) {
|
|
return responsesRequestToolChoiceToChat(raw)
|
|
}
|
|
|
|
func responsesRequestTextToChatResponseFormat(raw json.RawMessage) (*dto.ResponseFormat, error) {
|
|
if !rawJSONPresent(raw) {
|
|
return nil, nil
|
|
}
|
|
|
|
var textConfig map[string]any
|
|
if err := kitutil.Unmarshal(raw, &textConfig); err != nil {
|
|
return nil, fmt.Errorf("invalid text config: %w", err)
|
|
}
|
|
format, ok := textConfig["format"].(map[string]any)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
formatType := strings.TrimSpace(kitutil.Interface2String(format["type"]))
|
|
if formatType == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
out := &dto.ResponseFormat{Type: formatType}
|
|
if formatType == "json_schema" {
|
|
schemaRaw, err := kitutil.Marshal(format)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out.JsonSchema = schemaRaw
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func RequestTextToChatResponseFormat(raw json.RawMessage) (*dto.ResponseFormat, error) {
|
|
return responsesRequestTextToChatResponseFormat(raw)
|
|
}
|
|
|
|
func responsesImagePartToChatImageURL(part map[string]any) any {
|
|
if imageURL, ok := part["image_url"]; ok {
|
|
return imageURL
|
|
}
|
|
imageURL := map[string]any{}
|
|
for _, key := range []string{"url", "file_id", "detail"} {
|
|
if value, ok := part[key]; ok {
|
|
imageURL[key] = value
|
|
}
|
|
}
|
|
if len(imageURL) == 0 {
|
|
return part
|
|
}
|
|
return imageURL
|
|
}
|
|
|
|
func responsesFilePartToChatFile(part map[string]any) any {
|
|
if file, ok := part["file"]; ok {
|
|
return file
|
|
}
|
|
file := map[string]any{}
|
|
for _, key := range []string{"file_id", "file_data", "filename", "file_url"} {
|
|
if value, ok := part[key]; ok {
|
|
file[key] = value
|
|
}
|
|
}
|
|
if len(file) == 0 {
|
|
return part
|
|
}
|
|
return file
|
|
}
|
|
|
|
func responsesVideoPartToChatVideoURL(part map[string]any) any {
|
|
if videoURL, ok := part["video_url"]; ok {
|
|
if videoURLMap, ok := videoURL.(map[string]any); ok {
|
|
if url := kitutil.Interface2String(videoURLMap["url"]); url != "" {
|
|
return url
|
|
}
|
|
}
|
|
return videoURL
|
|
}
|
|
if url := kitutil.Interface2String(part["url"]); url != "" {
|
|
return url
|
|
}
|
|
return responsesPartPayload(part, "video_url")
|
|
}
|
|
|
|
func responsesPartPayload(part map[string]any, key string) any {
|
|
if value, ok := part[key]; ok {
|
|
return value
|
|
}
|
|
payload := make(map[string]any, len(part))
|
|
for k, value := range part {
|
|
if k == "type" {
|
|
continue
|
|
}
|
|
payload[k] = value
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func responsesCallID(item map[string]any) string {
|
|
callID := strings.TrimSpace(kitutil.Interface2String(item["call_id"]))
|
|
if callID != "" {
|
|
return callID
|
|
}
|
|
return strings.TrimSpace(kitutil.Interface2String(item["id"]))
|
|
}
|
|
|
|
func CallID(item map[string]any) string {
|
|
return responsesCallID(item)
|
|
}
|
|
|
|
func responsesArgumentsString(value any) string {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return v
|
|
default:
|
|
raw, err := kitutil.Marshal(v)
|
|
if err != nil {
|
|
return kitutil.Interface2String(v)
|
|
}
|
|
return string(raw)
|
|
}
|
|
}
|
|
|
|
func responseToolOutputToChatContent(value any) any {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return v
|
|
default:
|
|
raw, err := kitutil.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
return string(raw)
|
|
}
|
|
}
|
|
|
|
func responsesRawFloat(raw json.RawMessage) (*float64, error) {
|
|
if !rawJSONPresent(raw) {
|
|
return nil, nil
|
|
}
|
|
var value float64
|
|
if err := kitutil.Unmarshal(raw, &value); err != nil {
|
|
return nil, err
|
|
}
|
|
return &value, nil
|
|
}
|
|
|
|
func responsesJSONString(raw json.RawMessage) (string, error) {
|
|
if kitutil.GetJsonType(raw) != "string" {
|
|
return string(raw), nil
|
|
}
|
|
var value string
|
|
if err := kitutil.Unmarshal(raw, &value); err != nil {
|
|
return "", err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func rawJSONPresent(raw json.RawMessage) bool {
|
|
if len(raw) == 0 {
|
|
return false
|
|
}
|
|
return kitutil.GetJsonType(raw) != "null"
|
|
}
|
|
|
|
func JSONString(raw json.RawMessage) (string, error) {
|
|
return responsesJSONString(raw)
|
|
}
|
|
|
|
func RawJSONPresent(raw json.RawMessage) bool {
|
|
return rawJSONPresent(raw)
|
|
}
|