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
2 changes: 2 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func relayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewAPIErro
err = relay.EmbeddingHelper(c, info)
case relayconstant.RelayModeResponses, relayconstant.RelayModeResponsesCompact:
err = relay.ResponsesHelper(c, info)
case relayconstant.RelayModeCodexAlphaSearch:
err = relay.AlphaSearchHelper(c, info)
default:
err = relay.TextHelper(c, info)
}
Expand Down
45 changes: 45 additions & 0 deletions dto/codex_alpha_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dto

import (
"encoding/json"
"strings"

"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
)

// CodexAlphaSearchRequest is the standalone search request used by Codex CLI.
// It is not an OpenAI Responses request; unknown fields must be preserved by
// the relay path and forwarded as-is.
type CodexAlphaSearchRequest struct {
Model string `json:"model,omitempty"`
ID string `json:"id,omitempty"`
Query string `json:"query,omitempty"`
Commands json.RawMessage `json:"commands,omitempty"`
Input json.RawMessage `json:"input,omitempty"`
Settings json.RawMessage `json:"settings,omitempty"`
}

func (r *CodexAlphaSearchRequest) GetTokenCountMeta() *types.TokenCountMeta {
parts := make([]string, 0, 4)
if strings.TrimSpace(r.Query) != "" {
parts = append(parts, r.Query)
}
for _, raw := range []json.RawMessage{r.Commands, r.Input, r.Settings} {
if len(raw) > 0 {
parts = append(parts, string(raw))
}
}
return &types.TokenCountMeta{
CombineText: strings.Join(parts, "\n"),
TokenType: types.TokenTypeTokenizer,
}
}

func (r *CodexAlphaSearchRequest) IsStream(c *gin.Context) bool {
return false
}

func (r *CodexAlphaSearchRequest) SetModelName(modelName string) {
r.Model = modelName
}
4 changes: 4 additions & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group)
}

if relayconstant.Path2RelayMode(c.Request.URL.Path) == relayconstant.RelayModeCodexAlphaSearch {
c.Set("relay_mode", relayconstant.RelayModeCodexAlphaSearch)
}

if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" {
modelRequest.Model = ratio_setting.WithCompactModelSuffix(modelRequest.Model)
}
Expand Down
154 changes: 154 additions & 0 deletions relay/alpha_search_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package relay

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"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"
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"

"github.com/gin-gonic/gin"
)

var codexAlphaSearchUnsupportedBodyFields = [...]string{
// Codex alpha/search is a standalone SearchRequest protocol, not a
// /v1/responses subrequest. Codex clients and proxy chains may carry these
// Responses-only fields; ChatGPT alpha/search rejects them as unknown.
"prompt_cache_key",
"prompt_cache_retention",
}

func buildCodexAlphaSearchBody(body []byte, model string) ([]byte, error) {
if len(body) == 0 {
return body, nil
}
var obj map[string]json.RawMessage
if err := common.Unmarshal(body, &obj); err != nil || obj == nil {
return body, err
}

changed := false
for _, field := range codexAlphaSearchUnsupportedBodyFields {
if _, ok := obj[field]; ok {
delete(obj, field)
changed = true
}
}

model = strings.TrimSpace(model)
if _, hasModel := obj["model"]; hasModel && model != "" {
marshaledModel, err := common.Marshal(model)
if err != nil {
return nil, err
}
if string(obj["model"]) != string(marshaledModel) {
obj["model"] = marshaledModel
changed = true
}
}

if !changed {
return body, nil
}
out, err := common.Marshal(obj)
if err != nil {
return nil, err
}
return out, nil
}

func alphaSearchRelaySupported(apiType int) bool {
switch apiType {
case constant.APITypeOpenAI,
constant.APITypeOpenRouter,
constant.APITypeXinference,
constant.APITypeCodex,
constant.APITypeAdvancedCustom:
return true
default:
return false
}
}

func AlphaSearchHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
info.InitChannelMeta(c)
if !alphaSearchRelaySupported(info.ApiType) {
return types.NewErrorWithStatusCode(
fmt.Errorf("unsupported endpoint %q for api type %d", "/v1/alpha/search", info.ApiType),
types.ErrorCodeInvalidRequest,
http.StatusBadRequest,
types.ErrOptionWithSkipRetry(),
)
}

request, ok := info.Request.(*dto.CodexAlphaSearchRequest)
if !ok {
return types.NewErrorWithStatusCode(
fmt.Errorf("invalid request type, expected dto.CodexAlphaSearchRequest, got %T", info.Request),
types.ErrorCodeInvalidRequest,
http.StatusBadRequest,
types.ErrOptionWithSkipRetry(),
)
}
requestCopy := *request
if err := helper.ModelMappedHelper(c, info, &requestCopy); err != nil {
return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry())
}

adaptor := GetAdaptor(info.ApiType)
if adaptor == nil {
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
}
adaptor.Init(info)

storage, err := common.GetBodyStorage(c)
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
bodyBytes, err := storage.Bytes()
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
outboundBytes, err := buildCodexAlphaSearchBody(bodyBytes, requestCopy.Model)
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}

requestBody, size, closer, err := relaycommon.NewOutboundJSONBody(outboundBytes)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
defer closer.Close()
info.UpstreamRequestBodySize = size

resp, err := adaptor.DoRequest(c, info, requestBody)
if err != nil {
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
httpResp, ok := resp.(*http.Response)
if !ok || httpResp == nil {
return types.NewOpenAIError(fmt.Errorf("invalid alpha search response type: %T", resp), types.ErrorCodeBadResponse, http.StatusInternalServerError)
}

statusCodeMappingStr := c.GetString("status_code_mapping")
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
return newAPIError
}

if _, newAPIError = adaptor.DoResponse(c, httpResp, info); newAPIError != nil {
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
return newAPIError
}

service.PostCodexAlphaSearchConsumeQuota(c, info)
return nil
}
47 changes: 41 additions & 6 deletions relay/channel/codex/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,16 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
}

func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if info.RelayMode != relayconstant.RelayModeResponses && info.RelayMode != relayconstant.RelayModeResponsesCompact {
if info.RelayMode != relayconstant.RelayModeResponses &&
info.RelayMode != relayconstant.RelayModeResponsesCompact &&
info.RelayMode != relayconstant.RelayModeCodexAlphaSearch {
return nil, types.NewError(errors.New("codex channel: endpoint not supported"), types.ErrorCodeInvalidRequest)
}

if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
return openai.OaiAlphaSearchHandler(c, resp)
}

if info.RelayMode == relayconstant.RelayModeResponsesCompact {
return openai.OaiResponsesCompactionHandler(c, resp)
}
Expand All @@ -135,11 +141,18 @@ func (a *Adaptor) GetChannelName() string {
}

func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode != relayconstant.RelayModeResponses && info.RelayMode != relayconstant.RelayModeResponsesCompact {
return "", errors.New("codex channel: only /v1/responses and /v1/responses/compact are supported")
if info.RelayMode != relayconstant.RelayModeResponses &&
info.RelayMode != relayconstant.RelayModeResponsesCompact &&
info.RelayMode != relayconstant.RelayModeCodexAlphaSearch {
return "", errors.New("codex channel: only /v1/responses, /v1/responses/compact and /v1/alpha/search are supported")
}
path := "/backend-api/codex/responses"
if info.RelayMode == relayconstant.RelayModeResponsesCompact {
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
path = "/backend-api/codex/alpha/search"
if idx := strings.Index(info.RequestURLPath, "?"); idx >= 0 {
path += info.RequestURLPath[idx:]
}
} else if info.RelayMode == relayconstant.RelayModeResponsesCompact {
path = "/backend-api/codex/responses/compact"
}
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, path, info.ChannelType), nil
Expand Down Expand Up @@ -171,7 +184,27 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
req.Set("Authorization", "Bearer "+accessToken)
req.Set("chatgpt-account-id", accountID)

if req.Get("OpenAI-Beta") == "" {
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
req.Del("OpenAI-Beta")
req.Del("Session_ID")
req.Del("Conversation_ID")
req.Del("X-Codex-Beta-Features")
req.Del("X-Codex-Turn-State")

if c != nil && c.Request != nil {
for _, name := range []string{
"Version",
"User-Agent",
"Session_id",
"X-Client-Request-Id",
"X-Codex-Turn-Metadata",
} {
if value := strings.TrimSpace(c.GetHeader(name)); value != "" {
req.Set(name, value)
}
}
}
} else if req.Get("OpenAI-Beta") == "" {
req.Set("OpenAI-Beta", "responses=experimental")
}
if req.Get("originator") == "" {
Expand All @@ -182,7 +215,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
// Clients may omit it or include parameters like `application/json; charset=utf-8`,
// which can be rejected by the upstream. Force the exact media type.
req.Set("Content-Type", "application/json")
if info.IsStream {
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
req.Set("Accept", "application/json")
} else if info.IsStream {
req.Set("Accept", "text/event-stream")
} else if req.Get("Accept") == "" {
req.Set("Accept", "application/json")
Expand Down
18 changes: 18 additions & 0 deletions relay/channel/openai/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,18 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
info.ChannelBaseUrl = baseUrl
}
}
alphaSearchPath := func() string {
path := "/v1/alpha/search"
if idx := strings.Index(info.RequestURLPath, "?"); idx >= 0 {
path += info.RequestURLPath[idx:]
}
return path
}
switch info.ChannelType {
case constant.ChannelTypeAzure:
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
return "", fmt.Errorf("azure channel does not support /v1/alpha/search")
}
apiVersion := info.ApiVersion
if apiVersion == "" {
apiVersion = constant.AzureDefaultAPIVersion
Expand Down Expand Up @@ -167,10 +177,16 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
//case constant.ChannelTypeMiniMax:
// return minimax.GetRequestURL(info)
case constant.ChannelTypeCustom:
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, alphaSearchPath(), info.ChannelType), nil
}
url := info.ChannelBaseUrl
url = strings.Replace(url, "{model}", info.UpstreamModelName, -1)
return url, nil
default:
if info.RelayMode == relayconstant.RelayModeCodexAlphaSearch {
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, alphaSearchPath(), info.ChannelType), nil
}
if (info.RelayFormat == types.RelayFormatClaude || info.RelayFormat == types.RelayFormatGemini) &&
info.RelayMode != relayconstant.RelayModeResponses &&
info.RelayMode != relayconstant.RelayModeResponsesCompact {
Expand Down Expand Up @@ -651,6 +667,8 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
}
case relayconstant.RelayModeResponsesCompact:
usage, err = OaiResponsesCompactionHandler(c, resp)
case relayconstant.RelayModeCodexAlphaSearch:
usage, err = OaiAlphaSearchHandler(c, resp)
default:
if info.IsStream {
usage, err = OaiStreamHandler(c, info, resp)
Expand Down
26 changes: 26 additions & 0 deletions relay/channel/openai/relay_alpha_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package openai

import (
"fmt"
"io"
"net/http"

"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
)

func OaiAlphaSearchHandler(c *gin.Context, 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)

responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
}
service.IOCopyBytesGracefully(c, resp, responseBody)
return &dto.Usage{}, nil
}
12 changes: 12 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,13 @@ func GenRelayInfoResponses(c *gin.Context, request *dto.OpenAIResponsesRequest)
return info
}

func GenRelayInfoCodexAlphaSearch(c *gin.Context, request *dto.CodexAlphaSearchRequest) *RelayInfo {
info := genBaseRelayInfo(c, request)
info.RelayMode = relayconstant.RelayModeCodexAlphaSearch
info.RelayFormat = types.RelayFormatCodexAlphaSearch
return info
}

func GenRelayInfoGemini(c *gin.Context, request dto.Request) *RelayInfo {
info := genBaseRelayInfo(c, request)
info.RelayFormat = types.RelayFormatGemini
Expand Down Expand Up @@ -575,6 +582,11 @@ func GenRelayInfo(c *gin.Context, relayFormat types.RelayFormat, request dto.Req
return GenRelayInfoResponsesCompaction(c, request), nil
}
return nil, errors.New("request is not a OpenAIResponsesCompactionRequest")
case types.RelayFormatCodexAlphaSearch:
if request, ok := request.(*dto.CodexAlphaSearchRequest); ok {
return GenRelayInfoCodexAlphaSearch(c, request), nil
}
return nil, errors.New("request is not a CodexAlphaSearchRequest")
case types.RelayFormatTask:
info = genBaseRelayInfo(c, nil)
info.TaskRelayInfo = &TaskRelayInfo{}
Expand Down
Loading