| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- package openaicompat
- import (
- "encoding/json"
- "errors"
- "fmt"
- "strings"
- "github.com/QuantumNous/new-api/common"
- "github.com/QuantumNous/new-api/dto"
- )
- func normalizeChatImageURLToString(v any) any {
- switch vv := v.(type) {
- case string:
- return vv
- case map[string]any:
- if url := common.Interface2String(vv["url"]); url != "" {
- return url
- }
- return v
- case dto.MessageImageUrl:
- if vv.Url != "" {
- return vv.Url
- }
- return v
- case *dto.MessageImageUrl:
- if vv != nil && vv.Url != "" {
- return vv.Url
- }
- return v
- default:
- return v
- }
- }
- func convertChatResponseFormatToResponsesText(reqFormat *dto.ResponseFormat) json.RawMessage {
- if reqFormat == nil || strings.TrimSpace(reqFormat.Type) == "" {
- return nil
- }
- format := map[string]any{
- "type": reqFormat.Type,
- }
- if reqFormat.Type == "json_schema" && len(reqFormat.JsonSchema) > 0 {
- var chatSchema map[string]any
- if err := common.Unmarshal(reqFormat.JsonSchema, &chatSchema); err == nil {
- for key, value := range chatSchema {
- if key == "type" {
- continue
- }
- format[key] = value
- }
- if nested, ok := format["json_schema"].(map[string]any); ok {
- for key, value := range nested {
- if _, exists := format[key]; !exists {
- format[key] = value
- }
- }
- delete(format, "json_schema")
- }
- } else {
- format["json_schema"] = reqFormat.JsonSchema
- }
- }
- textRaw, _ := common.Marshal(map[string]any{
- "format": format,
- })
- return textRaw
- }
- func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) {
- if req == nil {
- return nil, errors.New("request is nil")
- }
- if req.Model == "" {
- return nil, errors.New("model is required")
- }
- if req.N > 1 {
- return nil, fmt.Errorf("n>1 is not supported in responses compatibility mode")
- }
- var instructionsParts []string
- inputItems := make([]map[string]any, 0, len(req.Messages))
- for _, msg := range req.Messages {
- role := strings.TrimSpace(msg.Role)
- if role == "" {
- continue
- }
- if role == "tool" || role == "function" {
- callID := strings.TrimSpace(msg.ToolCallId)
- var output any
- if msg.Content == nil {
- output = ""
- } else if msg.IsStringContent() {
- output = msg.StringContent()
- } else {
- if b, err := common.Marshal(msg.Content); err == nil {
- output = string(b)
- } else {
- output = fmt.Sprintf("%v", msg.Content)
- }
- }
- if callID == "" {
- inputItems = append(inputItems, map[string]any{
- "role": "user",
- "content": fmt.Sprintf("[tool_output_missing_call_id] %v", output),
- })
- continue
- }
- inputItems = append(inputItems, map[string]any{
- "type": "function_call_output",
- "call_id": callID,
- "output": output,
- })
- continue
- }
- // Prefer mapping system/developer messages into `instructions`.
- if role == "system" || role == "developer" {
- if msg.Content == nil {
- continue
- }
- if msg.IsStringContent() {
- if s := strings.TrimSpace(msg.StringContent()); s != "" {
- instructionsParts = append(instructionsParts, s)
- }
- continue
- }
- parts := msg.ParseContent()
- var sb strings.Builder
- for _, part := range parts {
- if part.Type == dto.ContentTypeText && strings.TrimSpace(part.Text) != "" {
- if sb.Len() > 0 {
- sb.WriteString("\n")
- }
- sb.WriteString(part.Text)
- }
- }
- if s := strings.TrimSpace(sb.String()); s != "" {
- instructionsParts = append(instructionsParts, s)
- }
- continue
- }
- item := map[string]any{
- "role": role,
- }
- if msg.Content == nil {
- item["content"] = ""
- inputItems = append(inputItems, item)
- if role == "assistant" {
- for _, tc := range msg.ParseToolCalls() {
- if strings.TrimSpace(tc.ID) == "" {
- continue
- }
- if tc.Type != "" && tc.Type != "function" {
- continue
- }
- name := strings.TrimSpace(tc.Function.Name)
- if name == "" {
- continue
- }
- inputItems = append(inputItems, map[string]any{
- "type": "function_call",
- "call_id": tc.ID,
- "name": name,
- "arguments": tc.Function.Arguments,
- })
- }
- }
- continue
- }
- if msg.IsStringContent() {
- item["content"] = msg.StringContent()
- inputItems = append(inputItems, item)
- if role == "assistant" {
- for _, tc := range msg.ParseToolCalls() {
- if strings.TrimSpace(tc.ID) == "" {
- continue
- }
- if tc.Type != "" && tc.Type != "function" {
- continue
- }
- name := strings.TrimSpace(tc.Function.Name)
- if name == "" {
- continue
- }
- inputItems = append(inputItems, map[string]any{
- "type": "function_call",
- "call_id": tc.ID,
- "name": name,
- "arguments": tc.Function.Arguments,
- })
- }
- }
- continue
- }
- parts := msg.ParseContent()
- contentParts := make([]map[string]any, 0, len(parts))
- for _, part := range parts {
- switch part.Type {
- case dto.ContentTypeText:
- textType := "input_text"
- if role == "assistant" {
- textType = "output_text"
- }
- contentParts = append(contentParts, map[string]any{
- "type": textType,
- "text": part.Text,
- })
- case dto.ContentTypeImageURL:
- contentParts = append(contentParts, map[string]any{
- "type": "input_image",
- "image_url": normalizeChatImageURLToString(part.ImageUrl),
- })
- case dto.ContentTypeInputAudio:
- contentParts = append(contentParts, map[string]any{
- "type": "input_audio",
- "input_audio": part.InputAudio,
- })
- case dto.ContentTypeFile:
- contentParts = append(contentParts, map[string]any{
- "type": "input_file",
- "file": part.File,
- })
- case dto.ContentTypeVideoUrl:
- contentParts = append(contentParts, map[string]any{
- "type": "input_video",
- "video_url": part.VideoUrl,
- })
- default:
- contentParts = append(contentParts, map[string]any{
- "type": part.Type,
- })
- }
- }
- item["content"] = contentParts
- inputItems = append(inputItems, item)
- if role == "assistant" {
- for _, tc := range msg.ParseToolCalls() {
- if strings.TrimSpace(tc.ID) == "" {
- continue
- }
- if tc.Type != "" && tc.Type != "function" {
- continue
- }
- name := strings.TrimSpace(tc.Function.Name)
- if name == "" {
- continue
- }
- inputItems = append(inputItems, map[string]any{
- "type": "function_call",
- "call_id": tc.ID,
- "name": name,
- "arguments": tc.Function.Arguments,
- })
- }
- }
- }
- inputRaw, err := common.Marshal(inputItems)
- if err != nil {
- return nil, err
- }
- var instructionsRaw json.RawMessage
- if len(instructionsParts) > 0 {
- instructions := strings.Join(instructionsParts, "\n\n")
- instructionsRaw, _ = common.Marshal(instructions)
- }
- var toolsRaw json.RawMessage
- if req.Tools != nil {
- tools := make([]map[string]any, 0, len(req.Tools))
- for _, tool := range req.Tools {
- switch tool.Type {
- case "function":
- tools = append(tools, map[string]any{
- "type": "function",
- "name": tool.Function.Name,
- "description": tool.Function.Description,
- "parameters": tool.Function.Parameters,
- })
- default:
- // Best-effort: keep original tool shape for unknown types.
- var m map[string]any
- if b, err := common.Marshal(tool); err == nil {
- _ = common.Unmarshal(b, &m)
- }
- if len(m) == 0 {
- m = map[string]any{"type": tool.Type}
- }
- tools = append(tools, m)
- }
- }
- toolsRaw, _ = common.Marshal(tools)
- }
- var toolChoiceRaw json.RawMessage
- if req.ToolChoice != nil {
- switch v := req.ToolChoice.(type) {
- case string:
- toolChoiceRaw, _ = common.Marshal(v)
- default:
- var m map[string]any
- if b, err := common.Marshal(v); err == nil {
- _ = common.Unmarshal(b, &m)
- }
- if m == nil {
- toolChoiceRaw, _ = common.Marshal(v)
- } else if t, _ := m["type"].(string); t == "function" {
- // Chat: {"type":"function","function":{"name":"..."}}
- // Responses: {"type":"function","name":"..."}
- if name, ok := m["name"].(string); ok && name != "" {
- toolChoiceRaw, _ = common.Marshal(map[string]any{
- "type": "function",
- "name": name,
- })
- } else if fn, ok := m["function"].(map[string]any); ok {
- if name, ok := fn["name"].(string); ok && name != "" {
- toolChoiceRaw, _ = common.Marshal(map[string]any{
- "type": "function",
- "name": name,
- })
- } else {
- toolChoiceRaw, _ = common.Marshal(v)
- }
- } else {
- toolChoiceRaw, _ = common.Marshal(v)
- }
- } else {
- toolChoiceRaw, _ = common.Marshal(v)
- }
- }
- }
- var parallelToolCallsRaw json.RawMessage
- if req.ParallelTooCalls != nil {
- parallelToolCallsRaw, _ = common.Marshal(*req.ParallelTooCalls)
- }
- textRaw := convertChatResponseFormatToResponsesText(req.ResponseFormat)
- maxOutputTokens := req.MaxTokens
- if req.MaxCompletionTokens > maxOutputTokens {
- maxOutputTokens = req.MaxCompletionTokens
- }
- // OpenAI Responses API rejects max_output_tokens < 16 when explicitly provided.
- //if maxOutputTokens > 0 && maxOutputTokens < 16 {
- // maxOutputTokens = 16
- //}
- var topP *float64
- if req.TopP != 0 {
- topP = common.GetPointer(req.TopP)
- }
- out := &dto.OpenAIResponsesRequest{
- Model: req.Model,
- Input: inputRaw,
- Instructions: instructionsRaw,
- MaxOutputTokens: maxOutputTokens,
- Stream: req.Stream,
- Temperature: req.Temperature,
- Text: textRaw,
- ToolChoice: toolChoiceRaw,
- Tools: toolsRaw,
- TopP: topP,
- User: req.User,
- ParallelToolCalls: parallelToolCallsRaw,
- Store: req.Store,
- Metadata: req.Metadata,
- }
- if req.ReasoningEffort != "" {
- out.Reasoning = &dto.Reasoning{
- Effort: req.ReasoningEffort,
- Summary: "detailed",
- }
- }
- return out, nil
- }
|