Skip to content

Commit

Permalink
feat: add Proxy channel type and relay mode
Browse files Browse the repository at this point in the history
Add the Proxy channel type and relay mode to support proxying requests to custom upstream services.
  • Loading branch information
Laisky committed Jul 21, 2024
1 parent 2a892c1 commit d4703df
Show file tree
Hide file tree
Showing 17 changed files with 292 additions and 106 deletions.
5 changes: 5 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ func relayHelper(c *gin.Context, relayMode int) *model.ErrorWithStatusCode {
fallthrough
case relaymode.AudioTranscription:
err = controller.RelayAudioHelper(c, relayMode)
case relaymode.Proxy:
err = controller.RelayProxyHelper(c, relayMode)

Check warning on line 38 in controller/relay.go

View check run for this annotation

Codecov / codecov/patch

controller/relay.go#L37-L38

Added lines #L37 - L38 were not covered by tests
default:
err = controller.RelayTextHelper(c)
}
Expand Down Expand Up @@ -85,12 +87,15 @@ func Relay(c *gin.Context) {
channelId := c.GetInt(ctxkey.ChannelId)
lastFailedChannelId = channelId
channelName := c.GetString(ctxkey.ChannelName)
// BUG: bizErr is in race condition
go processChannelRelayError(ctx, userId, channelId, channelName, bizErr)
}
if bizErr != nil {
if bizErr.StatusCode == http.StatusTooManyRequests {
bizErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
}

// BUG: bizErr is in race condition
bizErr.Error.Message = helper.MessageWithRequestId(bizErr.Error.Message, requestId)
c.JSON(bizErr.StatusCode, gin.H{
"error": bizErr.Error,
Expand Down
6 changes: 6 additions & 0 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ func TokenAuth() func(c *gin.Context) {
return
}
}

// set channel id for proxy relay
if channelId := c.Param("channelid"); channelId != "" {
c.Set(ctxkey.SpecificChannelId, channelId)

Check warning on line 146 in middleware/auth.go

View check run for this annotation

Codecov / codecov/patch

middleware/auth.go#L145-L146

Added lines #L145 - L146 were not covered by tests
}

c.Next()
}
}
Expand Down
3 changes: 3 additions & 0 deletions relay/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/songquanpeng/one-api/relay/adaptor/ollama"
"github.com/songquanpeng/one-api/relay/adaptor/openai"
"github.com/songquanpeng/one-api/relay/adaptor/palm"
"github.com/songquanpeng/one-api/relay/adaptor/proxy"
"github.com/songquanpeng/one-api/relay/adaptor/tencent"
"github.com/songquanpeng/one-api/relay/adaptor/vertexai"
"github.com/songquanpeng/one-api/relay/adaptor/xunfei"
Expand Down Expand Up @@ -58,6 +59,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor {
return &deepl.Adaptor{}
case apitype.VertexAI:
return &vertexai.Adaptor{}
case apitype.Proxy:
return &proxy.Adaptor{}
}
return nil
}
89 changes: 89 additions & 0 deletions relay/adaptor/proxy/adaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package proxy

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

"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/songquanpeng/one-api/relay/adaptor"
channelhelper "github.com/songquanpeng/one-api/relay/adaptor"
"github.com/songquanpeng/one-api/relay/meta"
"github.com/songquanpeng/one-api/relay/model"
relaymodel "github.com/songquanpeng/one-api/relay/model"
)

var _ adaptor.Adaptor = new(Adaptor)

const channelName = "proxy"

type Adaptor struct{}

func (a *Adaptor) Init(meta *meta.Meta) {

Check warning on line 24 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L24

Added line #L24 was not covered by tests
}

func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) {
return nil, errors.New("notimplement")

Check warning on line 28 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L27-L28

Added lines #L27 - L28 were not covered by tests
}

func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) {
for k, v := range resp.Header {
for _, vv := range v {
c.Writer.Header().Set(k, vv)

Check warning on line 34 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L31-L34

Added lines #L31 - L34 were not covered by tests
}
}

c.Writer.WriteHeader(resp.StatusCode)
if _, gerr := io.Copy(c.Writer, resp.Body); gerr != nil {
return nil, &relaymodel.ErrorWithStatusCode{
StatusCode: http.StatusInternalServerError,
Error: relaymodel.Error{
Message: gerr.Error(),
},

Check warning on line 44 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L38-L44

Added lines #L38 - L44 were not covered by tests
}
}

return nil, nil

Check warning on line 48 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L48

Added line #L48 was not covered by tests
}

func (a *Adaptor) GetModelList() (models []string) {
return nil

Check warning on line 52 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L51-L52

Added lines #L51 - L52 were not covered by tests
}

func (a *Adaptor) GetChannelName() string {
return channelName

Check warning on line 56 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L55-L56

Added lines #L55 - L56 were not covered by tests
}

// GetRequestURL remove static prefix, and return the real request url to the upstream service
func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
prefix := fmt.Sprintf("/v1/oneapi/proxy/%d", meta.ChannelId)
return meta.BaseURL + strings.TrimPrefix(meta.RequestURLPath, prefix), nil

Check warning on line 62 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L60-L62

Added lines #L60 - L62 were not covered by tests

}

func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error {
for k, v := range c.Request.Header {
req.Header.Set(k, v[0])

Check warning on line 68 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L66-L68

Added lines #L66 - L68 were not covered by tests
}

// remove unnecessary headers
req.Header.Del("Host")
req.Header.Del("Content-Length")
req.Header.Del("Accept-Encoding")
req.Header.Del("Connection")

Check warning on line 75 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L72-L75

Added lines #L72 - L75 were not covered by tests

// set authorization header
req.Header.Set("Authorization", meta.APIKey)

Check warning on line 78 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L78

Added line #L78 was not covered by tests

return nil

Check warning on line 80 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L80

Added line #L80 was not covered by tests
}

func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) {
return nil, errors.Errorf("not implement")

Check warning on line 84 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L83-L84

Added lines #L83 - L84 were not covered by tests
}

func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) {
return channelhelper.DoRequestHelper(a, c, meta, requestBody)

Check warning on line 88 in relay/adaptor/proxy/adaptor.go

View check run for this annotation

Codecov / codecov/patch

relay/adaptor/proxy/adaptor.go#L87-L88

Added lines #L87 - L88 were not covered by tests
}
1 change: 1 addition & 0 deletions relay/apitype/define.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
Cloudflare
DeepL
VertexAI
Proxy

Dummy // this one is only for count, do not add any channel after this
)
1 change: 1 addition & 0 deletions relay/channeltype/define.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ const (
Doubao
Novita
VertextAI
Proxy
Dummy
)
2 changes: 2 additions & 0 deletions relay/channeltype/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func ToAPIType(channelType int) int {
apiType = apitype.DeepL
case VertextAI:
apiType = apitype.VertexAI
case Proxy:
apiType = apitype.Proxy

Check warning on line 41 in relay/channeltype/helper.go

View check run for this annotation

Codecov / codecov/patch

relay/channeltype/helper.go#L40-L41

Added lines #L40 - L41 were not covered by tests
}

return apiType
Expand Down
1 change: 1 addition & 0 deletions relay/channeltype/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var ChannelBaseURLs = []string{
"https://ark.cn-beijing.volces.com", // 40
"https://api.novita.ai/v3/openai", // 41
"", // 42
"", // 43
}

func init() {
Expand Down
41 changes: 41 additions & 0 deletions relay/controller/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Package controller is a package for handling the relay controller
package controller

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/relay"
"github.com/songquanpeng/one-api/relay/adaptor/openai"
"github.com/songquanpeng/one-api/relay/meta"
relaymodel "github.com/songquanpeng/one-api/relay/model"
)

// RelayProxyHelper is a helper function to proxy the request to the upstream service
func RelayProxyHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode {
ctx := c.Request.Context()
meta := meta.GetByContext(c)

Check warning on line 19 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L17-L19

Added lines #L17 - L19 were not covered by tests

adaptor := relay.GetAdaptor(meta.APIType)
if adaptor == nil {
return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest)

Check warning on line 23 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L21-L23

Added lines #L21 - L23 were not covered by tests
}
adaptor.Init(meta)

Check warning on line 25 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L25

Added line #L25 was not covered by tests

resp, err := adaptor.DoRequest(c, meta, c.Request.Body)
if err != nil {
logger.Errorf(ctx, "DoRequest failed: %s", err.Error())
return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)

Check warning on line 30 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L27-L30

Added lines #L27 - L30 were not covered by tests
}

// do response
_, respErr := adaptor.DoResponse(c, resp, meta)
if respErr != nil {
logger.Errorf(ctx, "respErr is not nil: %+v", respErr)
return respErr

Check warning on line 37 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L34-L37

Added lines #L34 - L37 were not covered by tests
}

return nil

Check warning on line 40 in relay/controller/proxy.go

View check run for this annotation

Codecov / codecov/patch

relay/controller/proxy.go#L40

Added line #L40 was not covered by tests
}
11 changes: 6 additions & 5 deletions relay/meta/relay_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ type Meta struct {
UserId int
Group string
ModelMapping map[string]string
BaseURL string
APIKey string
APIType int
Config model.ChannelConfig
IsStream bool
// BaseURL is the proxy url set in the channel config
BaseURL string
APIKey string
APIType int
Config model.ChannelConfig
IsStream bool
// OriginModelName is the model name from the raw user request
OriginModelName string
// ActualModelName is the model name after mapping
Expand Down
2 changes: 2 additions & 0 deletions relay/relaymode/define.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ const (
AudioSpeech
AudioTranscription
AudioTranslation
// Proxy is a special relay mode for proxying requests to custom upstream
Proxy
)
2 changes: 2 additions & 0 deletions relay/relaymode/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func GetByPath(path string) int {
relayMode = AudioTranscription
} else if strings.HasPrefix(path, "/v1/audio/translations") {
relayMode = AudioTranslation
} else if strings.HasPrefix(path, "/v1/oneapi/proxy") {
relayMode = Proxy

Check warning on line 28 in relay/relaymode/helper.go

View check run for this annotation

Codecov / codecov/patch

relay/relaymode/helper.go#L27-L28

Added lines #L27 - L28 were not covered by tests
}
return relayMode
}
1 change: 1 addition & 0 deletions router/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func SetRelayRouter(router *gin.Engine) {
relayV1Router := router.Group("/v1")
relayV1Router.Use(middleware.RelayPanicRecover(), middleware.TokenAuth(), middleware.Distribute())
{
relayV1Router.Any("/oneapi/proxy/:channelid/*target", controller.Relay)

Check warning on line 22 in router/relay.go

View check run for this annotation

Codecov / codecov/patch

router/relay.go#L22

Added line #L22 was not covered by tests
relayV1Router.POST("/completions", controller.Relay)
relayV1Router.POST("/chat/completions", controller.Relay)
relayV1Router.POST("/edits", controller.Relay)
Expand Down
14 changes: 13 additions & 1 deletion web/air/src/constants/channel.constants.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
export const CHANNEL_OPTIONS = [
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
{ key: 33, text: 'AWS', value: 33, color: 'black' },
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
{ key: 24, text: 'Google Gemini', value: 24, color: 'orange' },
{ key: 28, text: 'Mistral AI', value: 28, color: 'orange' },
{ key: 41, text: 'Novita', value: 41, color: 'purple' },
{ key: 40, text: '字节跳动豆包', value: 40, color: 'blue' },
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
Expand All @@ -17,6 +20,15 @@ export const CHANNEL_OPTIONS = [
{ key: 29, text: 'Groq', value: 29, color: 'orange' },
{ key: 30, text: 'Ollama', value: 30, color: 'black' },
{ key: 31, text: '零一万物', value: 31, color: 'green' },
{ key: 32, text: '阶跃星辰', value: 32, color: 'blue' },
{ key: 34, text: 'Coze', value: 34, color: 'blue' },
{ key: 35, text: 'Cohere', value: 35, color: 'blue' },
{ key: 36, text: 'DeepSeek', value: 36, color: 'black' },
{ key: 37, text: 'Cloudflare', value: 37, color: 'orange' },
{ key: 38, text: 'DeepL', value: 38, color: 'black' },
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
Expand All @@ -34,4 +46,4 @@ export const CHANNEL_OPTIONS = [

for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
CHANNEL_OPTIONS[i].label = CHANNEL_OPTIONS[i].text;
}
}
6 changes: 6 additions & 0 deletions web/berry/src/constants/ChannelConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ export const CHANNEL_OPTIONS = {
value: 42,
color: 'primary'
},
43: {
key: 43,
text: 'Proxy',
value: 43,
color: 'primary'
},
41: {
key: 41,
text: 'Novita',
Expand Down
85 changes: 43 additions & 42 deletions web/default/src/constants/channel.constants.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
export const CHANNEL_OPTIONS = [
{key: 1, text: 'OpenAI', value: 1, color: 'green'},
{key: 14, text: 'Anthropic Claude', value: 14, color: 'black'},
{key: 33, text: 'AWS', value: 33, color: 'black'},
{key: 3, text: 'Azure OpenAI', value: 3, color: 'olive'},
{key: 11, text: 'Google PaLM2', value: 11, color: 'orange'},
{key: 24, text: 'Google Gemini', value: 24, color: 'orange'},
{key: 28, text: 'Mistral AI', value: 28, color: 'orange'},
{key: 41, text: 'Novita', value: 41, color: 'purple'},
{key: 40, text: '字节跳动豆包', value: 40, color: 'blue'},
{key: 15, text: '百度文心千帆', value: 15, color: 'blue'},
{key: 17, text: '阿里通义千问', value: 17, color: 'orange'},
{key: 18, text: '讯飞星火认知', value: 18, color: 'blue'},
{key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet'},
{key: 19, text: '360 智脑', value: 19, color: 'blue'},
{key: 25, text: 'Moonshot AI', value: 25, color: 'black'},
{key: 23, text: '腾讯混元', value: 23, color: 'teal'},
{key: 26, text: '百川大模型', value: 26, color: 'orange'},
{key: 27, text: 'MiniMax', value: 27, color: 'red'},
{key: 29, text: 'Groq', value: 29, color: 'orange'},
{key: 30, text: 'Ollama', value: 30, color: 'black'},
{key: 31, text: '零一万物', value: 31, color: 'green'},
{key: 32, text: '阶跃星辰', value: 32, color: 'blue'},
{key: 34, text: 'Coze', value: 34, color: 'blue'},
{key: 35, text: 'Cohere', value: 35, color: 'blue'},
{key: 36, text: 'DeepSeek', value: 36, color: 'black'},
{key: 37, text: 'Cloudflare', value: 37, color: 'orange'},
{key: 38, text: 'DeepL', value: 38, color: 'black'},
{key: 39, text: 'together.ai', value: 39, color: 'blue'},
{key: 42, text: 'VertexAI', value: 42, color: 'blue'},
{key: 8, text: '自定义渠道', value: 8, color: 'pink'},
{key: 22, text: '知识库:FastGPT', value: 22, color: 'blue'},
{key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple'},
{key: 20, text: '代理:OpenRouter', value: 20, color: 'black'},
{key: 2, text: '代理:API2D', value: 2, color: 'blue'},
{key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown'},
{key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple'},
{key: 10, text: '代理:AI Proxy', value: 10, color: 'purple'},
{key: 4, text: '代理:CloseAI', value: 4, color: 'teal'},
{key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet'},
{key: 9, text: '代理:AI.LS', value: 9, color: 'yellow'},
{key: 12, text: '代理:API2GPT', value: 12, color: 'blue'},
{key: 13, text: '代理:AIGC2D', value: 13, color: 'purple'}
{ key: 1, text: 'OpenAI', value: 1, color: 'green' },
{ key: 14, text: 'Anthropic Claude', value: 14, color: 'black' },
{ key: 33, text: 'AWS', value: 33, color: 'black' },
{ key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' },
{ key: 11, text: 'Google PaLM2', value: 11, color: 'orange' },
{ key: 24, text: 'Google Gemini', value: 24, color: 'orange' },
{ key: 28, text: 'Mistral AI', value: 28, color: 'orange' },
{ key: 41, text: 'Novita', value: 41, color: 'purple' },
{ key: 40, text: '字节跳动豆包', value: 40, color: 'blue' },
{ key: 15, text: '百度文心千帆', value: 15, color: 'blue' },
{ key: 17, text: '阿里通义千问', value: 17, color: 'orange' },
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
{ key: 25, text: 'Moonshot AI', value: 25, color: 'black' },
{ key: 23, text: '腾讯混元', value: 23, color: 'teal' },
{ key: 26, text: '百川大模型', value: 26, color: 'orange' },
{ key: 27, text: 'MiniMax', value: 27, color: 'red' },
{ key: 29, text: 'Groq', value: 29, color: 'orange' },
{ key: 30, text: 'Ollama', value: 30, color: 'black' },
{ key: 31, text: '零一万物', value: 31, color: 'green' },
{ key: 32, text: '阶跃星辰', value: 32, color: 'blue' },
{ key: 34, text: 'Coze', value: 34, color: 'blue' },
{ key: 35, text: 'Cohere', value: 35, color: 'blue' },
{ key: 36, text: 'DeepSeek', value: 36, color: 'black' },
{ key: 37, text: 'Cloudflare', value: 37, color: 'orange' },
{ key: 38, text: 'DeepL', value: 38, color: 'black' },
{ key: 39, text: 'together.ai', value: 39, color: 'blue' },
{ key: 42, text: 'VertexAI', value: 42, color: 'blue' },
{ key: 43, text: 'Proxy', value: 43, color: 'blue' },
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
{ key: 20, text: '代理:OpenRouter', value: 20, color: 'black' },
{ key: 2, text: '代理:API2D', value: 2, color: 'blue' },
{ key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' },
{ key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' },
{ key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' },
{ key: 4, text: '代理:CloseAI', value: 4, color: 'teal' },
{ key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' },
{ key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' },
{ key: 12, text: '代理:API2GPT', value: 12, color: 'blue' },
{ key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }
];
Loading

0 comments on commit d4703df

Please sign in to comment.