feat: codex channel (#2652)
* feat: codex channel * feat: codex channel * feat: codex oauth flow * feat: codex refresh cred * feat: codex usage * fix: codex err message detail * fix: codex setting ui * feat: codex refresh cred task * fix: import err * fix: codex store must be false * fix: chat -> responses tool call * fix: chat -> responses tool call
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package codex
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openai"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
|
||||
return nil, errors.New("codex channel: endpoint not supported")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
if info != nil && info.ChannelSetting.SystemPrompt != "" {
|
||||
systemPrompt := info.ChannelSetting.SystemPrompt
|
||||
|
||||
if len(request.Instructions) == 0 {
|
||||
if b, err := common.Marshal(systemPrompt); err == nil {
|
||||
request.Instructions = b
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else if info.ChannelSetting.SystemPromptOverride {
|
||||
var existing string
|
||||
if err := common.Unmarshal(request.Instructions, &existing); err == nil {
|
||||
existing = strings.TrimSpace(existing)
|
||||
if existing == "" {
|
||||
if b, err := common.Marshal(systemPrompt); err == nil {
|
||||
request.Instructions = b
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if b, err := common.Marshal(systemPrompt + "\n" + existing); err == nil {
|
||||
request.Instructions = b
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if b, err := common.Marshal(systemPrompt); err == nil {
|
||||
request.Instructions = b
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// codex: store must be false
|
||||
request.Store = json.RawMessage("false")
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
if info.RelayMode != relayconstant.RelayModeResponses {
|
||||
return nil, types.NewError(errors.New("codex channel: endpoint not supported"), types.ErrorCodeInvalidRequest)
|
||||
}
|
||||
|
||||
if info.IsStream {
|
||||
return openai.OaiResponsesStreamHandler(c, info, resp)
|
||||
}
|
||||
return openai.OaiResponsesHandler(c, info, resp)
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
if info.RelayMode != relayconstant.RelayModeResponses {
|
||||
return "", errors.New("codex channel: only /v1/responses is supported")
|
||||
}
|
||||
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/backend-api/codex/responses", info.ChannelType), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
|
||||
key := strings.TrimSpace(info.ApiKey)
|
||||
if !strings.HasPrefix(key, "{") {
|
||||
return errors.New("codex channel: key must be a JSON object")
|
||||
}
|
||||
|
||||
oauthKey, err := ParseOAuthKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
accessToken := strings.TrimSpace(oauthKey.AccessToken)
|
||||
accountID := strings.TrimSpace(oauthKey.AccountID)
|
||||
|
||||
if accessToken == "" {
|
||||
return errors.New("codex channel: access_token is required")
|
||||
}
|
||||
if accountID == "" {
|
||||
return errors.New("codex channel: account_id is required")
|
||||
}
|
||||
|
||||
req.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Set("chatgpt-account-id", accountID)
|
||||
|
||||
if req.Get("OpenAI-Beta") == "" {
|
||||
req.Set("OpenAI-Beta", "responses=experimental")
|
||||
}
|
||||
if req.Get("originator") == "" {
|
||||
req.Set("originator", "codex_cli_rs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package codex
|
||||
|
||||
var ModelList = []string{
|
||||
"gpt-5", "gpt-5-codex", "gpt-5-codex-mini",
|
||||
"gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
|
||||
"gpt-5.2", "gpt-5.2-codex",
|
||||
}
|
||||
|
||||
const ChannelName = "codex"
|
||||
@@ -0,0 +1,30 @@
|
||||
package codex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
type OAuthKey struct {
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
|
||||
AccountID string `json:"account_id,omitempty"`
|
||||
LastRefresh string `json:"last_refresh,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Expired string `json:"expired,omitempty"`
|
||||
}
|
||||
|
||||
func ParseOAuthKey(raw string) (*OAuthKey, error) {
|
||||
if raw == "" {
|
||||
return nil, errors.New("codex channel: empty oauth key")
|
||||
}
|
||||
var key OAuthKey
|
||||
if err := common.Unmarshal([]byte(raw), &key); err != nil {
|
||||
return nil, errors.New("codex channel: invalid oauth key json")
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
@@ -26,14 +26,10 @@ func OaiResponsesToChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
|
||||
defer service.CloseResponseBodyGracefully(resp)
|
||||
|
||||
var responsesResp dto.OpenAIResponsesResponse
|
||||
const maxResponseBodyBytes = 10 << 20 // 10MB
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBodyBytes+1))
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
|
||||
}
|
||||
if int64(len(body)) > maxResponseBodyBytes {
|
||||
return nil, types.NewOpenAIError(fmt.Errorf("response body exceeds %d bytes", maxResponseBodyBytes), types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := common.Unmarshal(body, &responsesResp); err != nil {
|
||||
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
||||
@@ -77,12 +73,99 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
|
||||
var (
|
||||
usage = &dto.Usage{}
|
||||
textBuilder strings.Builder
|
||||
outputText strings.Builder
|
||||
usageText strings.Builder
|
||||
sentStart bool
|
||||
sentStop bool
|
||||
sawToolCall bool
|
||||
streamErr *types.NewAPIError
|
||||
)
|
||||
|
||||
toolCallIndexByID := make(map[string]int)
|
||||
toolCallNameByID := make(map[string]string)
|
||||
toolCallArgsByID := make(map[string]string)
|
||||
toolCallNameSent := make(map[string]bool)
|
||||
toolCallCanonicalIDByItemID := make(map[string]string)
|
||||
|
||||
sendStartIfNeeded := func() bool {
|
||||
if sentStart {
|
||||
return true
|
||||
}
|
||||
if err := helper.ObjectData(c, helper.GenerateStartEmptyResponse(responseId, createAt, model, nil)); err != nil {
|
||||
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
sentStart = true
|
||||
return true
|
||||
}
|
||||
|
||||
sendToolCallDelta := func(callID string, name string, argsDelta string) bool {
|
||||
if callID == "" {
|
||||
return true
|
||||
}
|
||||
if outputText.Len() > 0 {
|
||||
// Prefer streaming assistant text over tool calls to match non-stream behavior.
|
||||
return true
|
||||
}
|
||||
if !sendStartIfNeeded() {
|
||||
return false
|
||||
}
|
||||
|
||||
idx, ok := toolCallIndexByID[callID]
|
||||
if !ok {
|
||||
idx = len(toolCallIndexByID)
|
||||
toolCallIndexByID[callID] = idx
|
||||
}
|
||||
if name != "" {
|
||||
toolCallNameByID[callID] = name
|
||||
}
|
||||
if toolCallNameByID[callID] != "" {
|
||||
name = toolCallNameByID[callID]
|
||||
}
|
||||
|
||||
tool := dto.ToolCallResponse{
|
||||
ID: callID,
|
||||
Type: "function",
|
||||
Function: dto.FunctionResponse{
|
||||
Arguments: argsDelta,
|
||||
},
|
||||
}
|
||||
tool.SetIndex(idx)
|
||||
if name != "" && !toolCallNameSent[callID] {
|
||||
tool.Function.Name = name
|
||||
toolCallNameSent[callID] = true
|
||||
}
|
||||
|
||||
chunk := &dto.ChatCompletionsStreamResponse{
|
||||
Id: responseId,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: createAt,
|
||||
Model: model,
|
||||
Choices: []dto.ChatCompletionsStreamResponseChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
|
||||
ToolCalls: []dto.ToolCallResponse{tool},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := helper.ObjectData(c, chunk); err != nil {
|
||||
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
sawToolCall = true
|
||||
|
||||
// Include tool call data in the local builder for fallback token estimation.
|
||||
if tool.Function.Name != "" {
|
||||
usageText.WriteString(tool.Function.Name)
|
||||
}
|
||||
if argsDelta != "" {
|
||||
usageText.WriteString(argsDelta)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||
if streamErr != nil {
|
||||
return false
|
||||
@@ -106,16 +189,13 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
}
|
||||
|
||||
case "response.output_text.delta":
|
||||
if !sentStart {
|
||||
if err := helper.ObjectData(c, helper.GenerateStartEmptyResponse(responseId, createAt, model, nil)); err != nil {
|
||||
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
sentStart = true
|
||||
if !sendStartIfNeeded() {
|
||||
return false
|
||||
}
|
||||
|
||||
if streamResp.Delta != "" {
|
||||
textBuilder.WriteString(streamResp.Delta)
|
||||
outputText.WriteString(streamResp.Delta)
|
||||
usageText.WriteString(streamResp.Delta)
|
||||
delta := streamResp.Delta
|
||||
chunk := &dto.ChatCompletionsStreamResponse{
|
||||
Id: responseId,
|
||||
@@ -137,6 +217,59 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
}
|
||||
}
|
||||
|
||||
case "response.output_item.added", "response.output_item.done":
|
||||
if streamResp.Item == nil {
|
||||
break
|
||||
}
|
||||
if streamResp.Item.Type != "function_call" {
|
||||
break
|
||||
}
|
||||
|
||||
itemID := strings.TrimSpace(streamResp.Item.ID)
|
||||
callID := strings.TrimSpace(streamResp.Item.CallId)
|
||||
if callID == "" {
|
||||
callID = itemID
|
||||
}
|
||||
if itemID != "" && callID != "" {
|
||||
toolCallCanonicalIDByItemID[itemID] = callID
|
||||
}
|
||||
name := strings.TrimSpace(streamResp.Item.Name)
|
||||
if name != "" {
|
||||
toolCallNameByID[callID] = name
|
||||
}
|
||||
|
||||
newArgs := streamResp.Item.Arguments
|
||||
prevArgs := toolCallArgsByID[callID]
|
||||
argsDelta := ""
|
||||
if newArgs != "" {
|
||||
if strings.HasPrefix(newArgs, prevArgs) {
|
||||
argsDelta = newArgs[len(prevArgs):]
|
||||
} else {
|
||||
argsDelta = newArgs
|
||||
}
|
||||
toolCallArgsByID[callID] = newArgs
|
||||
}
|
||||
|
||||
if !sendToolCallDelta(callID, name, argsDelta) {
|
||||
return false
|
||||
}
|
||||
|
||||
case "response.function_call_arguments.delta":
|
||||
itemID := strings.TrimSpace(streamResp.ItemID)
|
||||
callID := toolCallCanonicalIDByItemID[itemID]
|
||||
if callID == "" {
|
||||
callID = itemID
|
||||
}
|
||||
if callID == "" {
|
||||
break
|
||||
}
|
||||
toolCallArgsByID[callID] += streamResp.Delta
|
||||
if !sendToolCallDelta(callID, "", streamResp.Delta) {
|
||||
return false
|
||||
}
|
||||
|
||||
case "response.function_call_arguments.done":
|
||||
|
||||
case "response.completed":
|
||||
if streamResp.Response != nil {
|
||||
if streamResp.Response.Model != "" {
|
||||
@@ -170,15 +303,15 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
}
|
||||
}
|
||||
|
||||
if !sentStart {
|
||||
if err := helper.ObjectData(c, helper.GenerateStartEmptyResponse(responseId, createAt, model, nil)); err != nil {
|
||||
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
}
|
||||
sentStart = true
|
||||
if !sendStartIfNeeded() {
|
||||
return false
|
||||
}
|
||||
if !sentStop {
|
||||
stop := helper.GenerateStopResponse(responseId, createAt, model, "stop")
|
||||
finishReason := "stop"
|
||||
if sawToolCall && outputText.Len() == 0 {
|
||||
finishReason = "tool_calls"
|
||||
}
|
||||
stop := helper.GenerateStopResponse(responseId, createAt, model, finishReason)
|
||||
if err := helper.ObjectData(c, stop); err != nil {
|
||||
streamErr = types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
@@ -196,8 +329,6 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
streamErr = types.NewOpenAIError(fmt.Errorf("responses stream error: %s", streamResp.Type), types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
return false
|
||||
|
||||
case "response.output_item.added", "response.output_item.done":
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -209,7 +340,7 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
}
|
||||
|
||||
if usage.TotalTokens == 0 {
|
||||
usage = service.ResponseText2Usage(c, textBuilder.String(), info.UpstreamModelName, info.GetEstimatePromptTokens())
|
||||
usage = service.ResponseText2Usage(c, usageText.String(), info.UpstreamModelName, info.GetEstimatePromptTokens())
|
||||
}
|
||||
|
||||
if !sentStart {
|
||||
@@ -218,7 +349,11 @@ func OaiResponsesToChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo
|
||||
}
|
||||
}
|
||||
if !sentStop {
|
||||
stop := helper.GenerateStopResponse(responseId, createAt, model, "stop")
|
||||
finishReason := "stop"
|
||||
if sawToolCall && outputText.Len() == 0 {
|
||||
finishReason = "tool_calls"
|
||||
}
|
||||
stop := helper.GenerateStopResponse(responseId, createAt, model, finishReason)
|
||||
if err := helper.ObjectData(c, stop); err != nil {
|
||||
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -274,6 +274,7 @@ var streamSupportedChannels = map[int]bool{
|
||||
constant.ChannelTypeZhipu_v4: true,
|
||||
constant.ChannelTypeAli: true,
|
||||
constant.ChannelTypeSubmodel: true,
|
||||
constant.ChannelTypeCodex: true,
|
||||
}
|
||||
|
||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
|
||||
@@ -75,10 +75,11 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
|
||||
}
|
||||
adaptor.Init(info)
|
||||
|
||||
passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled
|
||||
if info.RelayMode == relayconstant.RelayModeChatCompletions &&
|
||||
!model_setting.GetGlobalSettings().PassThroughRequestEnabled &&
|
||||
!passThroughGlobal &&
|
||||
!info.ChannelSetting.PassThroughBodyEnabled &&
|
||||
service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.OriginModelName) {
|
||||
shouldChatCompletionsViaResponses(info) {
|
||||
applySystemPromptIfNeeded(c, info, request)
|
||||
usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request)
|
||||
if newApiErr != nil {
|
||||
@@ -98,7 +99,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
|
||||
|
||||
var requestBody io.Reader
|
||||
|
||||
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
|
||||
if passThroughGlobal || info.ChannelSetting.PassThroughBodyEnabled {
|
||||
body, err := common.GetRequestBody(c)
|
||||
if err != nil {
|
||||
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
||||
@@ -216,6 +217,16 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldChatCompletionsViaResponses(info *relaycommon.RelayInfo) bool {
|
||||
if info == nil {
|
||||
return false
|
||||
}
|
||||
if info.RelayMode != relayconstant.RelayModeChatCompletions {
|
||||
return false
|
||||
}
|
||||
return service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.OriginModelName)
|
||||
}
|
||||
|
||||
func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, extraContent ...string) {
|
||||
if usage == nil {
|
||||
usage = &dto.Usage{
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel/baidu_v2"
|
||||
"github.com/QuantumNous/new-api/relay/channel/claude"
|
||||
"github.com/QuantumNous/new-api/relay/channel/cloudflare"
|
||||
"github.com/QuantumNous/new-api/relay/channel/codex"
|
||||
"github.com/QuantumNous/new-api/relay/channel/cohere"
|
||||
"github.com/QuantumNous/new-api/relay/channel/coze"
|
||||
"github.com/QuantumNous/new-api/relay/channel/deepseek"
|
||||
@@ -117,6 +118,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
|
||||
return &minimax.Adaptor{}
|
||||
case constant.APITypeReplicate:
|
||||
return &replicate.Adaptor{}
|
||||
case constant.APITypeCodex:
|
||||
return &codex.Adaptor{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user