feat: sync remaining channel and relay updates
This commit is contained in:
@@ -75,6 +75,12 @@ func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType = constant.APITypeReplicate
|
||||
case constant.ChannelTypeCodex:
|
||||
apiType = constant.APITypeCodex
|
||||
case constant.ChannelTypeCodexCLIProxy:
|
||||
apiType = constant.APITypeOpenAI
|
||||
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||
apiType = constant.APITypeAnthropic
|
||||
case constant.ChannelTypeGeminiCLIProxy:
|
||||
apiType = constant.APITypeOpenAI
|
||||
}
|
||||
if apiType == -1 {
|
||||
return constant.APITypeOpenAI, false
|
||||
|
||||
@@ -24,6 +24,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
|
||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
|
||||
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
|
||||
constant.EndpointTypeImageEdit: {Path: "/v1/images/edits", Method: "POST"},
|
||||
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
|
||||
case constant.ChannelTypeAws:
|
||||
fallthrough
|
||||
case constant.ChannelTypeAnthropic:
|
||||
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||
endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
|
||||
case constant.ChannelTypeVertexAi:
|
||||
fallthrough
|
||||
|
||||
@@ -55,6 +55,9 @@ const (
|
||||
ChannelTypeSora = 55
|
||||
ChannelTypeReplicate = 56
|
||||
ChannelTypeCodex = 57
|
||||
ChannelTypeCodexCLIProxy = 58
|
||||
ChannelTypeClaudeCodeCLIProxy = 59
|
||||
ChannelTypeGeminiCLIProxy = 60
|
||||
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
||||
|
||||
)
|
||||
@@ -118,6 +121,9 @@ var ChannelBaseURLs = []string{
|
||||
"https://api.openai.com", //55
|
||||
"https://api.replicate.com", //56
|
||||
"https://chatgpt.com", //57
|
||||
"", //58
|
||||
"", //59
|
||||
"", //60
|
||||
}
|
||||
|
||||
var ChannelTypeNames = map[int]string{
|
||||
@@ -175,6 +181,9 @@ var ChannelTypeNames = map[int]string{
|
||||
ChannelTypeSora: "Sora",
|
||||
ChannelTypeReplicate: "Replicate",
|
||||
ChannelTypeCodex: "Codex",
|
||||
ChannelTypeCodexCLIProxy: "CodexCLIProxy",
|
||||
ChannelTypeClaudeCodeCLIProxy: "ClaudeCodeCLIProxy",
|
||||
ChannelTypeGeminiCLIProxy: "GeminiCLIProxy",
|
||||
}
|
||||
|
||||
func GetChannelTypeName(channelType int) string {
|
||||
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
EndpointTypeGemini EndpointType = "gemini"
|
||||
EndpointTypeJinaRerank EndpointType = "jina-rerank"
|
||||
EndpointTypeImageGeneration EndpointType = "image-generation"
|
||||
EndpointTypeImageEdit EndpointType = "image-edit"
|
||||
EndpointTypeEmbeddings EndpointType = "embeddings"
|
||||
EndpointTypeOpenAIVideo EndpointType = "openai-video"
|
||||
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
|
||||
|
||||
@@ -136,6 +136,14 @@ func GetClaudeAuthHeader(token string) http.Header {
|
||||
return h
|
||||
}
|
||||
|
||||
func GetClaudeCodeAuthHeader(token string) http.Header {
|
||||
h := http.Header{}
|
||||
h.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
h.Add("x-api-key", token)
|
||||
h.Add("anthropic-version", "2023-06-01")
|
||||
return h
|
||||
}
|
||||
|
||||
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,13 +2,16 @@ package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -42,6 +45,56 @@ type testResult struct {
|
||||
newAPIError *types.NewAPIError
|
||||
}
|
||||
|
||||
const imageEditTestPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wn7Zc8AAAAASUVORK5CYII="
|
||||
|
||||
func buildImageEditTestMultipartRequest(requestPath, model string) (*http.Request, error) {
|
||||
var requestBody bytes.Buffer
|
||||
writer := multipart.NewWriter(&requestBody)
|
||||
|
||||
if err := writer.WriteField("model", model); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writer.WriteField("prompt", "edit this image"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writer.WriteField("size", "1024x1024"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writer.WriteField("n", "1"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageBytes, err := base64.StdEncoding.DecodeString(imageEditTestPngBase64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Set("Content-Disposition", `form-data; name="image"; filename="channel-test.png"`)
|
||||
header.Set("Content-Type", "image/png")
|
||||
part, err := writer.CreatePart(header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = part.Write(imageBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{Path: requestPath},
|
||||
Body: io.NopCloser(bytes.NewReader(requestBody.Bytes())),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.ContentLength = int64(requestBody.Len())
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointType string) string {
|
||||
normalized := strings.TrimSpace(endpointType)
|
||||
if normalized != "" {
|
||||
@@ -153,6 +206,16 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
|
||||
//c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
if requestPath == "/v1/images/edits" {
|
||||
c.Request, err = buildImageEditTestMultipartRequest(requestPath, testModel)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeReadRequestBodyFailed),
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Set("channel", channel.Type)
|
||||
c.Set("base_url", channel.GetBaseURL())
|
||||
group, _ := model.GetUserGroup(1, false)
|
||||
@@ -184,7 +247,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
relayFormat = types.RelayFormatGemini
|
||||
case constant.EndpointTypeJinaRerank:
|
||||
relayFormat = types.RelayFormatRerank
|
||||
case constant.EndpointTypeImageGeneration:
|
||||
case constant.EndpointTypeImageGeneration, constant.EndpointTypeImageEdit:
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
case constant.EndpointTypeEmbeddings:
|
||||
relayFormat = types.RelayFormatEmbedding
|
||||
@@ -200,6 +263,9 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
if c.Request.URL.Path == "/v1/images/generations" {
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/images/edits" {
|
||||
relayFormat = types.RelayFormatOpenAIImage
|
||||
}
|
||||
if c.Request.URL.Path == "/v1/messages" {
|
||||
relayFormat = types.RelayFormatClaude
|
||||
}
|
||||
@@ -294,7 +360,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
case relayconstant.RelayModeImagesGenerations:
|
||||
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
|
||||
// 图像生成请求 - request 已经是正确的类型
|
||||
if imageReq, ok := request.(*dto.ImageRequest); ok {
|
||||
convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
|
||||
@@ -366,44 +432,53 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
|
||||
}
|
||||
}
|
||||
jsonData, err := common.Marshal(convertedRequest)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed),
|
||||
}
|
||||
}
|
||||
|
||||
//jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
//if err != nil {
|
||||
// return testResult{
|
||||
// context: c,
|
||||
// localErr: err,
|
||||
// newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
|
||||
// }
|
||||
//}
|
||||
|
||||
if len(info.ParamOverride) > 0 {
|
||||
jsonData, err = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
|
||||
if err != nil {
|
||||
if fixedErr, ok := relaycommon.AsParamOverrideReturnError(err); ok {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: fixedErr,
|
||||
newAPIError: relaycommon.NewAPIErrorFromParamOverride(fixedErr),
|
||||
}
|
||||
}
|
||||
var requestBody *bytes.Buffer
|
||||
switch req := convertedRequest.(type) {
|
||||
case *bytes.Buffer:
|
||||
requestBody = bytes.NewBuffer(req.Bytes())
|
||||
case bytes.Buffer:
|
||||
requestBody = bytes.NewBuffer(req.Bytes())
|
||||
default:
|
||||
jsonData, marshalErr := common.Marshal(convertedRequest)
|
||||
if marshalErr != nil {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid),
|
||||
localErr: marshalErr,
|
||||
newAPIError: types.NewError(marshalErr, types.ErrorCodeJsonMarshalFailed),
|
||||
}
|
||||
}
|
||||
|
||||
//jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
//if err != nil {
|
||||
// return testResult{
|
||||
// context: c,
|
||||
// localErr: err,
|
||||
// newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
|
||||
// }
|
||||
//}
|
||||
|
||||
if len(info.ParamOverride) > 0 {
|
||||
jsonData, marshalErr = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
|
||||
if marshalErr != nil {
|
||||
if fixedErr, ok := relaycommon.AsParamOverrideReturnError(marshalErr); ok {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: fixedErr,
|
||||
newAPIError: relaycommon.NewAPIErrorFromParamOverride(fixedErr),
|
||||
}
|
||||
}
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: marshalErr,
|
||||
newAPIError: types.NewError(marshalErr, types.ErrorCodeChannelParamOverrideInvalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestBody = bytes.NewBuffer(jsonData)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
|
||||
}
|
||||
|
||||
requestBody := bytes.NewBuffer(jsonData)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonData))
|
||||
resp, err := adaptor.DoRequest(c, info, requestBody)
|
||||
if err != nil {
|
||||
return testResult{
|
||||
@@ -618,6 +693,14 @@ func buildTestRequest(model string, endpointType string, channel *model.Channel,
|
||||
N: lo.ToPtr(uint(1)),
|
||||
Size: "1024x1024",
|
||||
}
|
||||
case constant.EndpointTypeImageEdit:
|
||||
// 返回 ImageRequest
|
||||
return &dto.ImageRequest{
|
||||
Model: model,
|
||||
Prompt: "edit this image",
|
||||
N: lo.ToPtr(uint(1)),
|
||||
Size: "1024x1024",
|
||||
}
|
||||
case constant.EndpointTypeJinaRerank:
|
||||
// 返回 RerankRequest
|
||||
return &dto.RerankRequest{
|
||||
|
||||
@@ -178,6 +178,8 @@ func buildFetchModelsHeaders(channel *model.Channel, key string) (http.Header, e
|
||||
switch channel.Type {
|
||||
case constant.ChannelTypeAnthropic:
|
||||
headers = GetClaudeAuthHeader(key)
|
||||
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||
headers = GetClaudeCodeAuthHeader(key)
|
||||
default:
|
||||
headers = GetAuthHeader(key)
|
||||
}
|
||||
@@ -469,6 +471,12 @@ func validateChannel(channel *model.Channel, isAdd bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.Type == constant.ChannelTypeCodexCLIProxy ||
|
||||
channel.Type == constant.ChannelTypeClaudeCodeCLIProxy ||
|
||||
channel.Type == constant.ChannelTypeGeminiCLIProxy) && strings.TrimSpace(channel.GetBaseURL()) == "" {
|
||||
return fmt.Errorf("proxy-compatible channel requires base_url")
|
||||
}
|
||||
|
||||
// Codex OAuth key validation (optional, only when JSON object is provided)
|
||||
if channel.Type == constant.ChannelTypeCodex {
|
||||
trimmedKey := strings.TrimSpace(channel.Key)
|
||||
|
||||
@@ -291,6 +291,8 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
|
||||
} else {
|
||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||
}
|
||||
case constant.ChannelTypeCodexCLIProxy, constant.ChannelTypeGeminiCLIProxy:
|
||||
url = fmt.Sprintf("%s/models", baseURL)
|
||||
default:
|
||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||
}
|
||||
|
||||
@@ -1,96 +1,27 @@
|
||||
# New-API Docker Compose Configuration
|
||||
#
|
||||
# Quick Start:
|
||||
# 1. docker-compose up -d
|
||||
# 2. Access at http://localhost:3000
|
||||
#
|
||||
# Using MySQL instead of PostgreSQL:
|
||||
# 1. Comment out the postgres service and SQL_DSN line 15
|
||||
# 2. Uncomment the mysql service and SQL_DSN line 16
|
||||
# 3. Uncomment mysql in depends_on (line 28)
|
||||
# 4. Uncomment mysql_data in volumes section (line 64)
|
||||
#
|
||||
# ⚠️ IMPORTANT: Change all default passwords before deploying to production!
|
||||
|
||||
version: '3.4' # For compatibility with older Docker versions
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
new-api:
|
||||
image: calciumion/new-api:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: local/new-api:hk88-src-20260416-173717
|
||||
container_name: new-api
|
||||
restart: always
|
||||
command: --log-dir /app/logs
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./logs:/app/logs
|
||||
- /docker/new-api/data:/data
|
||||
environment:
|
||||
- SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
|
||||
# - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service, uncomment if using MySQL
|
||||
- REDIS_CONN_STRING=redis://redis
|
||||
- SQL_DSN=new_api:Ll@12331100@tcp(hdy-hk-8-8.311169.xyz:3306)/new_api
|
||||
- REDIS_CONN_STRING=redis://:Ll12331100@hdy-hk-8-8.311169.xyz:6379
|
||||
- TZ=Asia/Shanghai
|
||||
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
|
||||
- BATCH_UPDATE_ENABLED=true # 是否启用批量更新 (Whether to enable batch update)
|
||||
# - STREAMING_TIMEOUT=300 # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 (Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions)
|
||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!! (multi-node deployment, set this to a random string!!!!!!!)
|
||||
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||
# - GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX # Google Analytics 的测量 ID (Google Analytics Measurement ID)
|
||||
# - UMAMI_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Umami 网站 ID (Umami Website ID)
|
||||
# - UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js # Umami 脚本 URL,默认为官方地址 (Umami Script URL, defaults to official URL)
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
# - mysql # Uncomment if using MySQL
|
||||
- SESSION_SECRET=ai-ikmkj-session-20260317
|
||||
- CRYPTO_SECRET=ai-ikmkj-crypto-20260317
|
||||
networks:
|
||||
- new-api-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
restart: always
|
||||
networks:
|
||||
- new-api-network
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
|
||||
POSTGRES_DB: new-api
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- new-api-network
|
||||
# ports:
|
||||
# - "5432:5432" # Uncomment if you need to access PostgreSQL from outside Docker
|
||||
|
||||
# mysql:
|
||||
# image: mysql:8.2
|
||||
# container_name: mysql
|
||||
# restart: always
|
||||
# environment:
|
||||
# MYSQL_ROOT_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
|
||||
# MYSQL_DATABASE: new-api
|
||||
# volumes:
|
||||
# - mysql_data:/var/lib/mysql
|
||||
# networks:
|
||||
# - new-api-network
|
||||
# ports:
|
||||
# - "3306:3306" # Uncomment if you need to access MySQL from outside Docker
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
# mysql_data:
|
||||
- default
|
||||
- biji-houduan_biji-net
|
||||
|
||||
networks:
|
||||
new-api-network:
|
||||
driver: bridge
|
||||
biji-houduan_biji-net:
|
||||
external: true
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
@@ -81,7 +82,12 @@ func CommonClaudeHeadersOperation(c *gin.Context, req *http.Header, info *relayc
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
req.Set("x-api-key", info.ApiKey)
|
||||
if info.ChannelType == constant.ChannelTypeClaudeCodeCLIProxy {
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
req.Set("x-api-key", info.ApiKey)
|
||||
} else {
|
||||
req.Set("x-api-key", info.ApiKey)
|
||||
}
|
||||
anthropicVersion := c.Request.Header.Get("anthropic-version")
|
||||
if anthropicVersion == "" {
|
||||
anthropicVersion = "2023-06-01"
|
||||
|
||||
@@ -301,24 +301,27 @@ func (info *RelayInfo) ToString() string {
|
||||
|
||||
// 定义支持流式选项的通道类型
|
||||
var streamSupportedChannels = map[int]bool{
|
||||
constant.ChannelTypeOpenAI: true,
|
||||
constant.ChannelTypeAnthropic: true,
|
||||
constant.ChannelTypeAws: true,
|
||||
constant.ChannelTypeGemini: true,
|
||||
constant.ChannelCloudflare: true,
|
||||
constant.ChannelTypeAzure: true,
|
||||
constant.ChannelTypeVolcEngine: true,
|
||||
constant.ChannelTypeOllama: true,
|
||||
constant.ChannelTypeXai: true,
|
||||
constant.ChannelTypeDeepSeek: true,
|
||||
constant.ChannelTypeBaiduV2: true,
|
||||
constant.ChannelTypeZhipu_v4: true,
|
||||
constant.ChannelTypeAli: true,
|
||||
constant.ChannelTypeSubmodel: true,
|
||||
constant.ChannelTypeCodex: true,
|
||||
constant.ChannelTypeMoonshot: true,
|
||||
constant.ChannelTypeMiniMax: true,
|
||||
constant.ChannelTypeSiliconFlow: true,
|
||||
constant.ChannelTypeOpenAI: true,
|
||||
constant.ChannelTypeCodexCLIProxy: true,
|
||||
constant.ChannelTypeGeminiCLIProxy: true,
|
||||
constant.ChannelTypeAnthropic: true,
|
||||
constant.ChannelTypeClaudeCodeCLIProxy: true,
|
||||
constant.ChannelTypeAws: true,
|
||||
constant.ChannelTypeGemini: true,
|
||||
constant.ChannelCloudflare: true,
|
||||
constant.ChannelTypeAzure: true,
|
||||
constant.ChannelTypeVolcEngine: true,
|
||||
constant.ChannelTypeOllama: true,
|
||||
constant.ChannelTypeXai: true,
|
||||
constant.ChannelTypeDeepSeek: true,
|
||||
constant.ChannelTypeBaiduV2: true,
|
||||
constant.ChannelTypeZhipu_v4: true,
|
||||
constant.ChannelTypeAli: true,
|
||||
constant.ChannelTypeSubmodel: true,
|
||||
constant.ChannelTypeCodex: true,
|
||||
constant.ChannelTypeMoonshot: true,
|
||||
constant.ChannelTypeMiniMax: true,
|
||||
constant.ChannelTypeSiliconFlow: true,
|
||||
}
|
||||
|
||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
|
||||
@@ -33,6 +33,15 @@ func GetFullRequestURL(baseURL string, requestURL string, channelType int) strin
|
||||
fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments"))
|
||||
}
|
||||
}
|
||||
|
||||
if channelType == constant.ChannelTypeCodexCLIProxy ||
|
||||
channelType == constant.ChannelTypeGeminiCLIProxy {
|
||||
trimmedRequestURL := strings.TrimPrefix(requestURL, "/v1")
|
||||
if trimmedRequestURL == "" {
|
||||
trimmedRequestURL = "/"
|
||||
}
|
||||
fullRequestURL = fmt.Sprintf("%s%s", baseURL, trimmedRequestURL)
|
||||
}
|
||||
return fullRequestURL
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
verifyJSON,
|
||||
} from '../../../../helpers';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
import { CHANNEL_OPTIONS, MODEL_FETCHABLE_CHANNEL_TYPES } from '../../../../constants';
|
||||
import {
|
||||
CHANNEL_OPTIONS,
|
||||
MODEL_FETCHABLE_CHANNEL_TYPES,
|
||||
} from '../../../../constants';
|
||||
import {
|
||||
SideSheet,
|
||||
Space,
|
||||
@@ -128,7 +131,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan';
|
||||
|
||||
// 支持并且已适配通过接口获取模型列表的渠道类型
|
||||
const MODEL_FETCHABLE_TYPES = new Set([
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
|
||||
]);
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
@@ -277,7 +280,8 @@ const EditChannelModal = (props) => {
|
||||
[inputs.upstream_model_update_last_detected_models],
|
||||
);
|
||||
const upstreamDetectedModelsPreview = useMemo(
|
||||
() => upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
||||
() =>
|
||||
upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
||||
[upstreamDetectedModels],
|
||||
);
|
||||
const upstreamDetectedModelsOmittedCount =
|
||||
@@ -310,9 +314,7 @@ const EditChannelModal = (props) => {
|
||||
return {
|
||||
tagLabel: t('不更改'),
|
||||
tagColor: 'grey',
|
||||
preview: t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||
),
|
||||
preview: t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'),
|
||||
};
|
||||
}
|
||||
if (!verifyJSON(raw)) {
|
||||
@@ -1612,7 +1614,7 @@ const EditChannelModal = (props) => {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
localInputs.type === 45 &&
|
||||
[45, 58, 59, 60].includes(localInputs.type) &&
|
||||
(!localInputs.base_url || localInputs.base_url.trim() === '')
|
||||
) {
|
||||
showInfo(t('请输入API地址!'));
|
||||
@@ -2257,6 +2259,39 @@ const EditChannelModal = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 58 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 59 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL,例如:https://gateway.example.com/anthropic;转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 60 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 20 && (
|
||||
<Form.Switch
|
||||
field='is_enterprise_account'
|
||||
@@ -3001,6 +3036,9 @@ const EditChannelModal = (props) => {
|
||||
inputs.type !== 8 &&
|
||||
inputs.type !== 22 &&
|
||||
inputs.type !== 36 &&
|
||||
inputs.type !== 58 &&
|
||||
inputs.type !== 59 &&
|
||||
inputs.type !== 60 &&
|
||||
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
|
||||
<div>
|
||||
<Form.Input
|
||||
@@ -3057,6 +3095,66 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 58 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 OpenAI / Codex Base URL')}
|
||||
placeholder={t(
|
||||
'请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 59 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 Claude Code / Anthropic Base URL')}
|
||||
placeholder={t(
|
||||
'请输入上游提供给 Claude Code 使用的 Anthropic Base URL,例如:https://gateway.example.com/anthropic',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 60 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 Gemini OpenAI Compat Base URL')}
|
||||
placeholder={t(
|
||||
'请输入 Gemini OpenAI 兼容 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
||||
<div>
|
||||
<Form.Select
|
||||
@@ -3478,79 +3576,81 @@ const EditChannelModal = (props) => {
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='upstream_model_update_auto_sync_enabled'
|
||||
label={t('是否自动同步上游模型更新')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
disabled={!inputs.upstream_model_update_check_enabled}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'upstream_model_update_auto_sync_enabled',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
||||
)}
|
||||
field='upstream_model_update_auto_sync_enabled'
|
||||
label={t('是否自动同步上游模型更新')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
disabled={!inputs.upstream_model_update_check_enabled}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'upstream_model_update_auto_sync_enabled',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='text-xs text-gray-500 mb-3'>
|
||||
{t('上次检测到可加入模型')}:
|
||||
{upstreamDetectedModels.length === 0 ? (
|
||||
t('暂无')
|
||||
t('暂无')
|
||||
) : (
|
||||
<>
|
||||
<Tooltip
|
||||
position='topLeft'
|
||||
content={
|
||||
<div className='max-w-[640px] break-all text-xs leading-5'>
|
||||
{upstreamDetectedModels.join(', ')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Tooltip
|
||||
position='topLeft'
|
||||
content={
|
||||
<div className='max-w-[640px] break-all text-xs leading-5'>
|
||||
{upstreamDetectedModels.join(', ')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className='cursor-help break-all'>
|
||||
{upstreamDetectedModelsPreview.join(', ')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span className='ml-1 text-gray-400'>
|
||||
</Tooltip>
|
||||
<span className='ml-1 text-gray-400'>
|
||||
{upstreamDetectedModelsOmittedCount > 0
|
||||
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
||||
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
||||
total: upstreamDetectedModels.length,
|
||||
omit: upstreamDetectedModelsOmittedCount,
|
||||
})
|
||||
: t('(共 {{total}} 个)', {
|
||||
: t('(共 {{total}} 个)', {
|
||||
total: upstreamDetectedModels.length,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<div className='flex items-center justify-between gap-2 mb-1'>
|
||||
<Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
|
||||
<Text className='text-sm font-medium'>
|
||||
{t('参数覆盖')}
|
||||
</Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
icon={<IconCode size={14} />}
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
size='small'
|
||||
type='primary'
|
||||
icon={<IconCode size={14} />}
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
>
|
||||
{t('可视化编辑')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('operations', 'fill')
|
||||
}
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('operations', 'fill')
|
||||
}
|
||||
>
|
||||
{t('填充新模板')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('legacy', 'fill')
|
||||
}
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('legacy', 'fill')
|
||||
}
|
||||
>
|
||||
{t('填充旧模板')}
|
||||
</Button>
|
||||
@@ -3564,14 +3664,16 @@ const EditChannelModal = (props) => {
|
||||
</Space>
|
||||
</div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
||||
{t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||
)}
|
||||
</Text>
|
||||
<div
|
||||
className='mt-2 rounded-xl p-3'
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
className='mt-2 rounded-xl p-3'
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<Tag color={paramOverrideMeta.tagColor}>
|
||||
@@ -3579,17 +3681,19 @@ const EditChannelModal = (props) => {
|
||||
</Tag>
|
||||
<Space spacing={8}>
|
||||
<Button
|
||||
size='small'
|
||||
icon={<IconCopy />}
|
||||
type='tertiary'
|
||||
onClick={copyParamOverrideJson}
|
||||
size='small'
|
||||
icon={<IconCopy />}
|
||||
type='tertiary'
|
||||
onClick={copyParamOverrideJson}
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() =>
|
||||
setParamOverrideEditorVisible(true)
|
||||
}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
@@ -3602,80 +3706,80 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
|
||||
<Form.TextArea
|
||||
field='header_override'
|
||||
label={t('请求头覆盖')}
|
||||
placeholder={
|
||||
t('此项可选,用于覆盖请求头参数') +
|
||||
'\n' +
|
||||
t('格式示例:') +
|
||||
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
|
||||
}
|
||||
autosize
|
||||
onChange={(value) =>
|
||||
handleInputChange('header_override', value)
|
||||
}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex gap-2 flex-wrap items-center'>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
're:^X-Trace-.*$': true,
|
||||
'X-Foo': '{client_header:X-Foo}',
|
||||
Authorization: 'Bearer {api_key}',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入透传模版')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() => formatJsonField('header_override')}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('支持变量:')}
|
||||
</Text>
|
||||
<div className='text-xs text-tertiary ml-2'>
|
||||
<div>
|
||||
{t('渠道密钥')}: {'{api_key}'}
|
||||
</div>
|
||||
field='header_override'
|
||||
label={t('请求头覆盖')}
|
||||
placeholder={
|
||||
t('此项可选,用于覆盖请求头参数') +
|
||||
'\n' +
|
||||
t('格式示例:') +
|
||||
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
|
||||
}
|
||||
autosize
|
||||
onChange={(value) =>
|
||||
handleInputChange('header_override', value)
|
||||
}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex gap-2 flex-wrap items-center'>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
're:^X-Trace-.*$': true,
|
||||
'X-Foo': '{client_header:X-Foo}',
|
||||
Authorization: 'Bearer {api_key}',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入透传模版')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() => formatJsonField('header_override')}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('支持变量:')}
|
||||
</Text>
|
||||
<div className='text-xs text-tertiary ml-2'>
|
||||
<div>
|
||||
{t('渠道密钥')}: {'{api_key}'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
<JSONEditor
|
||||
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
|
||||
|
||||
@@ -60,6 +60,7 @@ const ModelTestModal = ({
|
||||
const streamToggleDisabled = [
|
||||
'embeddings',
|
||||
'image-generation',
|
||||
'image-edit',
|
||||
'jina-rerank',
|
||||
'openai-response-compact',
|
||||
].includes(selectedEndpointType);
|
||||
@@ -96,6 +97,10 @@ const ModelTestModal = ({
|
||||
value: 'image-generation',
|
||||
label: t('图像生成') + ' (/v1/images/generations)',
|
||||
},
|
||||
{
|
||||
value: 'image-edit',
|
||||
label: 'OpenAI Image Edit (/v1/images/edits)',
|
||||
},
|
||||
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
|
||||
];
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ const ENDPOINT_TEMPLATE = {
|
||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||
};
|
||||
|
||||
const nameRuleOptions = [
|
||||
|
||||
@@ -48,6 +48,7 @@ const ENDPOINT_TEMPLATE = {
|
||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||
};
|
||||
|
||||
const EditPrefillGroupModal = ({
|
||||
|
||||
17
web/src/constants/channel.constants.js
vendored
17
web/src/constants/channel.constants.js
vendored
@@ -189,11 +189,26 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'blue',
|
||||
label: 'Codex (OpenAI OAuth)',
|
||||
},
|
||||
{
|
||||
value: 58,
|
||||
color: 'cyan',
|
||||
label: 'OpenAI Official / Codex CLI (/openai BaseURL)',
|
||||
},
|
||||
{
|
||||
value: 59,
|
||||
color: 'indigo',
|
||||
label: 'Claude Code / Anthropic Gateway',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
color: 'green',
|
||||
label: 'Gemini CLI / OpenAI Compat',
|
||||
},
|
||||
];
|
||||
|
||||
// Channel types that support upstream model list fetching in UI.
|
||||
export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
|
||||
]);
|
||||
|
||||
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
Reference in New Issue
Block a user