feat: support Responses to Chat (#5787)

* 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
This commit is contained in:
Calcium-Ion
2026-06-28 14:25:47 +08:00
committed by GitHub
parent 3a506f50f0
commit 2d5a041639
35 changed files with 2731 additions and 40 deletions
+15 -2
View File
@@ -104,10 +104,18 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
if err != nil {
return nil, err
}
if converter != dto.AdvancedCustomConverterNone {
switch converter {
case dto.AdvancedCustomConverterNone:
return a.convertOpenAICompatibleResponsesRequest(c, info, request)
case dto.AdvancedCustomConverterOpenAIResponsesToOpenAIChatCompletions:
chatReq, err := service.ResponsesRequestToChatCompletionsRequest(&request)
if err != nil {
return nil, err
}
return a.convertOpenAICompatibleRequest(c, info, chatReq)
default:
return nil, fmt.Errorf("converter %q does not support OpenAI Responses requests", converter)
}
return a.convertOpenAICompatibleResponsesRequest(c, info, request)
}
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
@@ -221,6 +229,11 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
return openai.OaiResponsesToChatStreamHandler(c, info, resp)
}
return openai.OaiResponsesToChatHandler(c, info, resp)
case dto.AdvancedCustomConverterOpenAIResponsesToOpenAIChatCompletions:
if info.IsStream {
return openai.OaiChatToResponsesStreamHandler(c, info, resp)
}
return openai.OaiChatToResponsesHandler(c, info, resp)
default:
return nil, types.NewOpenAIError(fmt.Errorf("unsupported advanced custom converter: %s", a.converter), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
@@ -6,6 +6,7 @@ import (
"net/url"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
relaycommon "github.com/QuantumNous/new-api/relay/common"
@@ -279,6 +280,44 @@ func TestAdaptorMatchesGeminiIncomingPathTemplate(t *testing.T) {
}
}
func TestAdaptorConvertsResponsesRequestToOpenAIChatUpstream(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/responses",
UpstreamPath: "/v1/chat/completions",
Converter: dto.AdvancedCustomConverterOpenAIResponsesToOpenAIChatCompletions,
},
},
})
info.RelayMode = relayconstant.RelayModeResponses
info.RequestURLPath = "/v1/responses"
c := advancedCustomGinContext("/v1/responses")
converted, err := adaptor.ConvertOpenAIResponsesRequest(c, info, dto.OpenAIResponsesRequest{
Model: "gpt-test",
Instructions: mustAdvancedCustomRawMessage(t, "system rules"),
Input: mustAdvancedCustomRawMessage(t, "hello"),
})
require.NoError(t, err)
chatReq, ok := converted.(*dto.GeneralOpenAIRequest)
require.True(t, ok)
assert.Equal(t, "gpt-test", chatReq.Model)
require.Len(t, chatReq.Messages, 2)
assert.Equal(t, "system", chatReq.Messages[0].Role)
assert.Equal(t, "system rules", chatReq.Messages[0].StringContent())
assert.Equal(t, "user", chatReq.Messages[1].Role)
assert.Equal(t, "hello", chatReq.Messages[1].StringContent())
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, "/v1/chat/completions", parsedURL.Path)
}
func advancedCustomRelayInfo(config *dto.AdvancedCustomConfig) *relaycommon.RelayInfo {
return &relaycommon.RelayInfo{
RelayFormat: types.RelayFormatOpenAI,
@@ -302,3 +341,10 @@ func advancedCustomGinContext(path string) *gin.Context {
c.Request.Header.Set("Content-Type", "application/json")
return c
}
func mustAdvancedCustomRawMessage(t *testing.T, value any) []byte {
t.Helper()
raw, err := common.Marshal(value)
require.NoError(t, err)
return raw
}