mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
* fix(openai): harden Chat-to-Responses compatibility Add a shared Responses-to-Chat stream state machine and use it from the OpenAI relay path. Preserve assistant text alongside tool calls, bind tool argument deltas by output_index, map incomplete finish reasons, support reasoning/custom tool events, and buffer upstream SSE for non-stream Chat clients. Add deterministic service tests and relay SSE tests for the conversion path. Related to #5745. * refactor: rename openaicompat to relayconvert for improved clarity * feat(gemini): support responses request conversion * feat: add responses to chat conversion support * fix: harden responses chat conversion edge cases
132 lines
4.4 KiB
Go
132 lines
4.4 KiB
Go
package openai
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/dto"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relay/helper"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/service/relayconvert"
|
|
"github.com/QuantumNous/new-api/types"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func OaiChatToResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
|
|
if resp == nil || resp.Body == nil {
|
|
return nil, types.NewOpenAIError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
|
}
|
|
defer service.CloseResponseBodyGracefully(resp)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
|
|
}
|
|
|
|
var chatResp dto.OpenAITextResponse
|
|
if err := common.Unmarshal(body, &chatResp); err != nil {
|
|
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
}
|
|
if oaiError := chatResp.GetOpenAIError(); oaiError != nil && oaiError.Type != "" {
|
|
return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
|
|
}
|
|
|
|
responseID := helper.GetResponseID(c)
|
|
responsesResp, usage, err := service.ChatCompletionsResponseToResponsesResponse(&chatResp, responseID)
|
|
if err != nil {
|
|
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
|
}
|
|
if usage == nil || usage.TotalTokens == 0 {
|
|
text := service.ExtractOutputTextFromResponses(responsesResp)
|
|
usage = service.ResponseText2Usage(c, text, info.UpstreamModelName, info.GetEstimatePromptTokens())
|
|
responsesResp.Usage = relayconvert.UsageFromChatUsage(usage)
|
|
}
|
|
|
|
responseBody, err := common.Marshal(responsesResp)
|
|
if err != nil {
|
|
return nil, types.NewOpenAIError(err, types.ErrorCodeJsonMarshalFailed, http.StatusInternalServerError)
|
|
}
|
|
|
|
service.IOCopyBytesGracefully(c, resp, responseBody)
|
|
return usage, nil
|
|
}
|
|
|
|
func OaiChatToResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
|
|
if resp == nil || resp.Body == nil {
|
|
return nil, types.NewOpenAIError(fmt.Errorf("invalid response"), types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
|
}
|
|
defer service.CloseResponseBodyGracefully(resp)
|
|
|
|
responseID := helper.GetResponseID(c)
|
|
state := relayconvert.NewChatToResponsesStreamState(responseID, info.UpstreamModelName)
|
|
streamErr := (*types.NewAPIError)(nil)
|
|
|
|
sendEvent := func(event relayconvert.ChatToResponsesStreamEvent) bool {
|
|
data, err := common.Marshal(event.Payload)
|
|
if err != nil {
|
|
streamErr = types.NewOpenAIError(err, types.ErrorCodeJsonMarshalFailed, http.StatusInternalServerError)
|
|
return false
|
|
}
|
|
helper.ResponseChunkData(c, dto.ResponsesStreamResponse{Type: event.Type}, string(data))
|
|
return true
|
|
}
|
|
|
|
helper.StreamScannerHandler(c, resp, info, func(data string, sr *helper.StreamResult) {
|
|
if streamErr != nil {
|
|
sr.Stop(streamErr)
|
|
return
|
|
}
|
|
|
|
var errorResp dto.OpenAITextResponse
|
|
if err := common.UnmarshalJsonStr(data, &errorResp); err == nil {
|
|
if oaiError := errorResp.GetOpenAIError(); oaiError != nil && oaiError.Type != "" {
|
|
streamErr = types.WithOpenAIError(*oaiError, resp.StatusCode)
|
|
sr.Stop(streamErr)
|
|
return
|
|
}
|
|
}
|
|
|
|
var chunk dto.ChatCompletionsStreamResponse
|
|
if err := common.UnmarshalJsonStr(data, &chunk); err != nil {
|
|
logger.LogError(c, "failed to unmarshal chat stream response: "+err.Error())
|
|
sr.Error(err)
|
|
return
|
|
}
|
|
|
|
events, err := relayconvert.ChatCompletionsStreamChunkToResponsesEvents(&chunk, state)
|
|
if err != nil {
|
|
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
|
sr.Stop(streamErr)
|
|
return
|
|
}
|
|
for _, event := range events {
|
|
if !sendEvent(event) {
|
|
sr.Stop(streamErr)
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
if streamErr != nil {
|
|
return nil, streamErr
|
|
}
|
|
|
|
usage := state.Usage
|
|
if usage == nil || usage.TotalTokens == 0 {
|
|
usage = service.ResponseText2Usage(c, state.UsageText(), info.UpstreamModelName, info.GetEstimatePromptTokens())
|
|
state.Usage = relayconvert.UsageFromChatUsage(usage)
|
|
}
|
|
|
|
for _, event := range relayconvert.FinalizeChatCompletionsStreamToResponses(state) {
|
|
if !sendEvent(event) {
|
|
return nil, streamErr
|
|
}
|
|
}
|
|
|
|
return usage, nil
|
|
}
|