package relayconvert import ( "errors" "fmt" "sort" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/dto" ) const ( chatFinishReasonLength = "length" chatFinishReasonContentFilter = "content_filter" ) func ChatCompletionsResponseToResponsesResponse(resp *dto.OpenAITextResponse, id string) (*dto.OpenAIResponsesResponse, *dto.Usage, error) { if resp == nil { return nil, nil, errors.New("response is nil") } usage := UsageFromChatUsage(&resp.Usage) out := &dto.OpenAIResponsesResponse{ ID: id, Object: "response", CreatedAt: chatCreatedAt(resp.Created), Status: []byte(`"completed"`), Model: resp.Model, Output: make([]dto.ResponsesOutput, 0), Usage: usage, } if len(resp.Choices) == 0 { return out, usage, nil } choice := resp.Choices[0] if status, details := ResponsesStatusFromChatFinishReason(choice.FinishReason); status != "" { out.Status = []byte(fmt.Sprintf("%q", status)) out.IncompleteDetails = details } if text := choice.Message.StringContent(); text != "" { out.Output = append(out.Output, dto.ResponsesOutput{ Type: responsesOutputTypeMessage, ID: fmt.Sprintf("%s_msg_0", id), Status: responseOutputStatus(out), Role: "assistant", Content: []dto.ResponsesOutputContent{ { Type: "output_text", Text: text, Annotations: []interface{}{}, }, }, }) } if reasoning := choice.Message.GetReasoningContent(); reasoning != "" { out.Output = append(out.Output, dto.ResponsesOutput{ Type: responsesOutputTypeReasoning, ID: fmt.Sprintf("%s_reasoning_0", id), Status: responseOutputStatus(out), Content: []dto.ResponsesOutputContent{ { Type: "summary_text", Text: reasoning, }, }, }) } for i, toolCall := range choice.Message.ParseToolCalls() { toolOutput, err := chatToolCallToResponsesOutput(toolCall, id, i, responseOutputStatus(out)) if err != nil { return nil, nil, err } out.Output = append(out.Output, toolOutput) } return out, usage, nil } func ResponsesStatusFromChatFinishReason(finishReason string) (string, *dto.IncompleteDetails) { switch strings.TrimSpace(finishReason) { case chatFinishReasonLength: return "incomplete", &dto.IncompleteDetails{Reason: responsesIncompleteReasonMaxTokens} case chatFinishReasonContentFilter: return "incomplete", &dto.IncompleteDetails{Reason: responsesIncompleteReasonContentFilter} default: return "completed", nil } } func UsageFromChatUsage(src *dto.Usage) *dto.Usage { usage := &dto.Usage{} if src == nil { return usage } if src.PromptTokens != 0 { usage.PromptTokens = src.PromptTokens usage.InputTokens = src.PromptTokens } if src.CompletionTokens != 0 { usage.CompletionTokens = src.CompletionTokens usage.OutputTokens = src.CompletionTokens } if src.TotalTokens != 0 { usage.TotalTokens = src.TotalTokens } else { usage.TotalTokens = usage.InputTokens + usage.OutputTokens } if src.PromptTokensDetails.CachedTokens != 0 || src.PromptTokensDetails.ImageTokens != 0 || src.PromptTokensDetails.AudioTokens != 0 || src.PromptTokensDetails.CachedCreationTokens != 0 || src.PromptTokensDetails.TextTokens != 0 { details := src.PromptTokensDetails usage.InputTokensDetails = &details } if src.CompletionTokenDetails.ReasoningTokens != 0 || src.CompletionTokenDetails.TextTokens != 0 || src.CompletionTokenDetails.AudioTokens != 0 || src.CompletionTokenDetails.ImageTokens != 0 { usage.CompletionTokenDetails = src.CompletionTokenDetails } return usage } type ChatToResponsesStreamEvent struct { Type string Payload dto.ResponsesStreamResponse } type ChatToResponsesStreamState struct { ID string Model string Created int64 Usage *dto.Usage status string incompleteDetails *dto.IncompleteDetails sentCreated bool textOutputIndex int textStarted bool textDone bool reasoningIndex int reasoningStarted bool reasoningDone bool finalized bool nextOutputIndex int toolsByIndex map[int]*chatToResponsesStreamTool outputOrder []chatToResponsesOutputRef text strings.Builder reasoning strings.Builder } type chatToResponsesStreamTool struct { ChatIndex int OutputIndex int ID string Name string Arguments strings.Builder Done bool } type chatToResponsesOutputRef struct { Kind string ToolIndex int } func NewChatToResponsesStreamState(id string, model string) *ChatToResponsesStreamState { return &ChatToResponsesStreamState{ ID: id, Model: model, Created: time.Now().Unix(), Usage: &dto.Usage{}, status: "completed", textOutputIndex: -1, reasoningIndex: -1, toolsByIndex: make(map[int]*chatToResponsesStreamTool), } } func ChatCompletionsStreamChunkToResponsesEvents(chunk *dto.ChatCompletionsStreamResponse, state *ChatToResponsesStreamState) ([]ChatToResponsesStreamEvent, error) { if chunk == nil || state == nil { return nil, nil } if state.ID == "" { state.ID = chunk.Id } if state.Model == "" { state.Model = chunk.Model } if state.Created == 0 { state.Created = chunk.Created } if chunk.Usage != nil { state.Usage = UsageFromChatUsage(chunk.Usage) } events := make([]ChatToResponsesStreamEvent, 0) if !state.sentCreated { state.sentCreated = true events = append(events, responsesStreamEvent(responsesEventCreated, dto.ResponsesStreamResponse{ Type: responsesEventCreated, Response: state.createdResponse(), })) } for _, choice := range chunk.Choices { if choice.Delta.GetReasoningContent() != "" { events = append(events, state.appendReasoningDelta(choice.Delta.GetReasoningContent())...) } if choice.Delta.GetContentString() != "" { events = append(events, state.appendTextDelta(choice.Delta.GetContentString())...) } for _, toolCall := range choice.Delta.ToolCalls { toolEvents, err := state.appendToolCallDelta(toolCall) if err != nil { return nil, err } events = append(events, toolEvents...) } if choice.FinishReason != nil && strings.TrimSpace(*choice.FinishReason) != "" { state.applyFinishReason(*choice.FinishReason) events = append(events, state.doneDeltaEvents()...) } } return events, nil } func FinalizeChatCompletionsStreamToResponses(state *ChatToResponsesStreamState) []ChatToResponsesStreamEvent { if state == nil || state.finalized { return nil } events := state.doneDeltaEvents() state.finalized = true resp := state.finalResponse() eventType := responsesEventCompleted if state.status == "incomplete" { eventType = responsesEventIncomplete } events = append(events, responsesStreamEvent(eventType, dto.ResponsesStreamResponse{ Type: eventType, Response: resp, })) return events } func (s *ChatToResponsesStreamState) UsageText() string { if s == nil { return "" } return s.text.String() } func (s *ChatToResponsesStreamState) appendTextDelta(delta string) []ChatToResponsesStreamEvent { events := make([]ChatToResponsesStreamEvent, 0, 2) if !s.textStarted { s.textStarted = true s.textOutputIndex = s.nextIndex("message", -1) events = append(events, responsesStreamEvent(responsesEventOutputItemAdded, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemAdded, OutputIndex: intPtr(s.textOutputIndex), Item: &dto.ResponsesOutput{ Type: responsesOutputTypeMessage, ID: s.messageID(), Status: "in_progress", Role: "assistant", Content: []dto.ResponsesOutputContent{}, }, })) } s.text.WriteString(delta) events = append(events, responsesStreamEvent(responsesEventOutputTextDelta, dto.ResponsesStreamResponse{ Type: responsesEventOutputTextDelta, OutputIndex: intPtr(s.textOutputIndex), ContentIndex: intPtr(0), Delta: delta, ItemID: s.messageID(), })) return events } func (s *ChatToResponsesStreamState) appendReasoningDelta(delta string) []ChatToResponsesStreamEvent { events := make([]ChatToResponsesStreamEvent, 0, 2) if !s.reasoningStarted { s.reasoningStarted = true s.reasoningIndex = s.nextIndex("reasoning", -1) events = append(events, responsesStreamEvent(responsesEventOutputItemAdded, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemAdded, OutputIndex: intPtr(s.reasoningIndex), Item: &dto.ResponsesOutput{ Type: responsesOutputTypeReasoning, ID: s.reasoningID(), Status: "in_progress", Content: []dto.ResponsesOutputContent{}, }, })) } s.reasoning.WriteString(delta) events = append(events, responsesStreamEvent(responsesEventReasoningSummaryDelta, dto.ResponsesStreamResponse{ Type: responsesEventReasoningSummaryDelta, OutputIndex: intPtr(s.reasoningIndex), SummaryIndex: intPtr(0), Delta: delta, ItemID: s.reasoningID(), })) return events } func (s *ChatToResponsesStreamState) appendToolCallDelta(toolCall dto.ToolCallResponse) ([]ChatToResponsesStreamEvent, error) { chatIndex := 0 if toolCall.Index != nil { chatIndex = *toolCall.Index } tool := s.toolsByIndex[chatIndex] events := make([]ChatToResponsesStreamEvent, 0, 2) if tool == nil { tool = &chatToResponsesStreamTool{ ChatIndex: chatIndex, OutputIndex: s.nextIndex("tool", chatIndex), ID: strings.TrimSpace(toolCall.ID), Name: strings.TrimSpace(toolCall.Function.Name), } if tool.ID == "" { tool.ID = fmt.Sprintf("%s_call_%d", s.ID, chatIndex) } s.toolsByIndex[chatIndex] = tool events = append(events, responsesStreamEvent(responsesEventOutputItemAdded, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemAdded, OutputIndex: intPtr(tool.OutputIndex), ItemID: tool.ID, Item: &dto.ResponsesOutput{ Type: responsesOutputTypeFunctionCall, ID: tool.ID, Status: "in_progress", CallId: tool.ID, Name: tool.Name, Arguments: []byte(`""`), }, })) } if strings.TrimSpace(toolCall.ID) != "" { tool.ID = strings.TrimSpace(toolCall.ID) } if strings.TrimSpace(toolCall.Function.Name) != "" { tool.Name = strings.TrimSpace(toolCall.Function.Name) } if toolCall.Function.Arguments != "" { tool.Arguments.WriteString(toolCall.Function.Arguments) events = append(events, responsesStreamEvent(responsesEventFunctionArgsDelta, dto.ResponsesStreamResponse{ Type: responsesEventFunctionArgsDelta, OutputIndex: intPtr(tool.OutputIndex), ItemID: tool.ID, Delta: toolCall.Function.Arguments, })) } return events, nil } func (s *ChatToResponsesStreamState) doneDeltaEvents() []ChatToResponsesStreamEvent { events := make([]ChatToResponsesStreamEvent, 0) status := s.outputStatus() if s.textStarted && !s.textDone { s.textDone = true events = append(events, responsesStreamEvent("response.output_text.done", dto.ResponsesStreamResponse{ Type: "response.output_text.done", OutputIndex: intPtr(s.textOutputIndex), ContentIndex: intPtr(0), ItemID: s.messageID(), })) events = append(events, responsesStreamEvent(responsesEventOutputItemDone, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemDone, OutputIndex: intPtr(s.textOutputIndex), Item: s.messageOutput(status), })) } if s.reasoningStarted && !s.reasoningDone { s.reasoningDone = true events = append(events, responsesStreamEvent(responsesEventReasoningSummaryDone, dto.ResponsesStreamResponse{ Type: responsesEventReasoningSummaryDone, OutputIndex: intPtr(s.reasoningIndex), SummaryIndex: intPtr(0), ItemID: s.reasoningID(), Part: &dto.ResponsesReasoningSummaryPart{ Type: "summary_text", Text: s.reasoning.String(), }, })) events = append(events, responsesStreamEvent(responsesEventOutputItemDone, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemDone, OutputIndex: intPtr(s.reasoningIndex), Item: s.reasoningOutput(status), })) } for _, tool := range s.sortedTools() { if tool.Done { continue } tool.Done = true events = append(events, responsesStreamEvent(responsesEventFunctionArgsDone, dto.ResponsesStreamResponse{ Type: responsesEventFunctionArgsDone, OutputIndex: intPtr(tool.OutputIndex), ItemID: tool.ID, })) events = append(events, responsesStreamEvent(responsesEventOutputItemDone, dto.ResponsesStreamResponse{ Type: responsesEventOutputItemDone, OutputIndex: intPtr(tool.OutputIndex), Item: s.toolOutput(tool, status), })) } return events } func (s *ChatToResponsesStreamState) applyFinishReason(finishReason string) { if status, details := ResponsesStatusFromChatFinishReason(finishReason); status != "" { s.status = status s.incompleteDetails = details } } func (s *ChatToResponsesStreamState) finalResponse() *dto.OpenAIResponsesResponse { output := make([]dto.ResponsesOutput, 0, len(s.outputOrder)) status := s.outputStatus() for _, ref := range s.outputOrder { switch ref.Kind { case "message": output = append(output, *s.messageOutput(status)) case "reasoning": output = append(output, *s.reasoningOutput(status)) case "tool": if tool := s.toolsByIndex[ref.ToolIndex]; tool != nil { output = append(output, *s.toolOutput(tool, status)) } } } return &dto.OpenAIResponsesResponse{ ID: s.ID, Object: "response", CreatedAt: int(s.Created), Status: []byte(fmt.Sprintf("%q", s.status)), IncompleteDetails: s.incompleteDetails, Model: s.Model, Output: output, Usage: s.Usage, } } func (s *ChatToResponsesStreamState) createdResponse() *dto.OpenAIResponsesResponse { return &dto.OpenAIResponsesResponse{ ID: s.ID, Object: "response", CreatedAt: int(s.Created), Status: []byte(`"in_progress"`), Model: s.Model, Output: []dto.ResponsesOutput{}, } } func (s *ChatToResponsesStreamState) nextIndex(kind string, toolIndex int) int { index := s.nextOutputIndex s.nextOutputIndex++ s.outputOrder = append(s.outputOrder, chatToResponsesOutputRef{Kind: kind, ToolIndex: toolIndex}) return index } func (s *ChatToResponsesStreamState) sortedTools() []*chatToResponsesStreamTool { indexes := make([]int, 0, len(s.toolsByIndex)) for index := range s.toolsByIndex { indexes = append(indexes, index) } sort.Ints(indexes) tools := make([]*chatToResponsesStreamTool, 0, len(indexes)) for _, index := range indexes { tools = append(tools, s.toolsByIndex[index]) } return tools } func (s *ChatToResponsesStreamState) outputStatus() string { if s.status == "incomplete" { return "incomplete" } return "completed" } func (s *ChatToResponsesStreamState) messageID() string { return fmt.Sprintf("%s_msg_0", s.ID) } func (s *ChatToResponsesStreamState) reasoningID() string { return fmt.Sprintf("%s_reasoning_0", s.ID) } func (s *ChatToResponsesStreamState) messageOutput(status string) *dto.ResponsesOutput { return &dto.ResponsesOutput{ Type: responsesOutputTypeMessage, ID: s.messageID(), Status: status, Role: "assistant", Content: []dto.ResponsesOutputContent{ { Type: "output_text", Text: s.text.String(), Annotations: []interface{}{}, }, }, } } func (s *ChatToResponsesStreamState) reasoningOutput(status string) *dto.ResponsesOutput { return &dto.ResponsesOutput{ Type: responsesOutputTypeReasoning, ID: s.reasoningID(), Status: status, Content: []dto.ResponsesOutputContent{ { Type: "summary_text", Text: s.reasoning.String(), }, }, } } func (s *ChatToResponsesStreamState) toolOutput(tool *chatToResponsesStreamTool, status string) *dto.ResponsesOutput { return &dto.ResponsesOutput{ Type: responsesOutputTypeFunctionCall, ID: tool.ID, Status: status, CallId: tool.ID, Name: tool.Name, Arguments: chatArgumentsRawMessage(tool.Arguments.String()), } } func responseOutputStatus(resp *dto.OpenAIResponsesResponse) string { if resp == nil || responseStatusString(resp) != "incomplete" { return "completed" } return "incomplete" } func chatToolCallToResponsesOutput(toolCall dto.ToolCallRequest, responseID string, index int, status string) (dto.ResponsesOutput, error) { callID := strings.TrimSpace(toolCall.ID) if callID == "" { callID = fmt.Sprintf("%s_call_%d", responseID, index) } if toolCall.Type == "" || toolCall.Type == "function" { return dto.ResponsesOutput{ Type: responsesOutputTypeFunctionCall, ID: callID, Status: status, CallId: callID, Name: toolCall.Function.Name, Arguments: chatArgumentsRawMessage(toolCall.Function.Arguments), }, nil } return dto.ResponsesOutput{ Type: toolCall.Type, ID: callID, Status: status, CallId: callID, Arguments: toolCall.Custom, }, nil } func chatArgumentsRawMessage(arguments string) []byte { raw, err := common.Marshal(arguments) if err != nil { return []byte(`""`) } return raw } func chatCreatedAt(created any) int { switch v := created.(type) { case int: return v case int64: return int(v) case float64: return int(v) case float32: return int(v) case string: if parsed := common.String2Int(v); parsed != 0 { return parsed } } return int(time.Now().Unix()) } func responsesStreamEvent(eventType string, payload dto.ResponsesStreamResponse) ChatToResponsesStreamEvent { payload.Type = eventType return ChatToResponsesStreamEvent{ Type: eventType, Payload: payload, } } func intPtr(v int) *int { return &v }