Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions relay/channel/codex/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/openai"
Expand All @@ -21,6 +22,18 @@ import (
type Adaptor struct {
}

func forceCodexResponsesStream(c *gin.Context, info *relaycommon.RelayInfo, request *dto.OpenAIResponsesRequest) {
if request != nil {
request.Stream = common.GetPointer(true)
}
if info != nil {
info.IsStream = true
}
if c != nil {
c.Set(string(constant.ContextKeyIsStream), true)
}
}

func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
return nil, errors.New("codex channel: endpoint not supported")
}
Expand Down Expand Up @@ -101,6 +114,9 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
}
// codex: store must be false
request.Store = json.RawMessage("false")
// Codex backend requires streaming responses. Keep RelayInfo in sync so
// the downstream response is handled as SSE too.
forceCodexResponsesStream(c, info, &request)
// rm max_output_tokens
request.MaxOutputTokens = nil
request.Temperature = nil
Expand Down
58 changes: 58 additions & 0 deletions relay/channel/codex/adaptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package codex

import (
"encoding/json"
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestConvertOpenAIResponsesRequestForcesStream(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
info := &relaycommon.RelayInfo{RelayMode: relayconstant.RelayModeResponses, ChannelMeta: &relaycommon.ChannelMeta{}}
stream := false

converted, err := (&Adaptor{}).ConvertOpenAIResponsesRequest(c, info, dto.OpenAIResponsesRequest{
Model: "gpt-5.3-codex",
Input: json.RawMessage(`"hi"`),
Stream: &stream,
})

require.NoError(t, err)
req, ok := converted.(dto.OpenAIResponsesRequest)
require.True(t, ok)
require.NotNil(t, req.Stream)
require.True(t, *req.Stream)
require.True(t, info.IsStream)
require.True(t, c.GetBool(string(constant.ContextKeyIsStream)))
require.Equal(t, json.RawMessage("false"), req.Store)
}

func TestConvertOpenAIResponsesCompactDoesNotForceStream(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
info := &relaycommon.RelayInfo{RelayMode: relayconstant.RelayModeResponsesCompact, ChannelMeta: &relaycommon.ChannelMeta{}}
stream := false

converted, err := (&Adaptor{}).ConvertOpenAIResponsesRequest(c, info, dto.OpenAIResponsesRequest{
Model: "gpt-5.3-codex",
Input: json.RawMessage(`"hi"`),
Stream: &stream,
})

require.NoError(t, err)
req, ok := converted.(dto.OpenAIResponsesRequest)
require.True(t, ok)
require.NotNil(t, req.Stream)
require.False(t, *req.Stream)
require.False(t, info.IsStream)
require.False(t, c.GetBool(string(constant.ContextKeyIsStream)))
}
32 changes: 31 additions & 1 deletion relay/responses_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,15 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
if err != nil {
return types.NewError(err, types.ErrorCodeReadRequestBodyFailed, types.ErrOptionWithSkipRetry())
}
requestBody = common.ReaderOnly(storage)
if info.ApiType == appconstant.APITypeCodex && info.RelayMode == relayconstant.RelayModeResponses {
jsonData, err := normalizeCodexResponsesPassthroughBody(c, info, storage)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
requestBody = bytes.NewBuffer(jsonData)
} else {
requestBody = common.ReaderOnly(storage)
}
} else {
convertedRequest, err := adaptor.ConvertOpenAIResponsesRequest(c, info, *request)
if err != nil {
Expand Down Expand Up @@ -158,3 +166,25 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
}
return nil
}

func normalizeCodexResponsesPassthroughBody(c *gin.Context, info *relaycommon.RelayInfo, storage common.BodyStorage) ([]byte, error) {
body, err := storage.Bytes()
if err != nil {
return nil, err
}
reqBody := make(map[string]interface{})
if err := common.Unmarshal(body, &reqBody); err != nil {
return nil, err
}

reqBody["stream"] = true
reqBody["store"] = false
if info != nil {
info.IsStream = true
}
if c != nil {
c.Set(string(appconstant.ContextKeyIsStream), true)
}

return common.Marshal(reqBody)
}
33 changes: 33 additions & 0 deletions relay/responses_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package relay

import (
"encoding/json"
"net/http/httptest"
"testing"

"github.com/QuantumNous/new-api/common"
appconstant "github.com/QuantumNous/new-api/constant"
relaycommon "github.com/QuantumNous/new-api/relay/common"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)

func TestNormalizeCodexResponsesPassthroughBodyForcesStream(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
info := &relaycommon.RelayInfo{}
storage, err := common.CreateBodyStorage([]byte(`{"model":"gpt-5.3-codex","input":"hi","stream":false,"store":true}`))
require.NoError(t, err)
defer storage.Close()

body, err := normalizeCodexResponsesPassthroughBody(c, info, storage)

require.NoError(t, err)
var req map[string]interface{}
require.NoError(t, json.Unmarshal(body, &req))
require.Equal(t, true, req["stream"])
require.Equal(t, false, req["store"])
require.True(t, info.IsStream)
require.True(t, c.GetBool(string(appconstant.ContextKeyIsStream)))
}