diff --git a/common/api_type.go b/common/api_type.go
index 39c1fe9..76840a8 100644
--- a/common/api_type.go
+++ b/common/api_type.go
@@ -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
diff --git a/common/endpoint_defaults.go b/common/endpoint_defaults.go
index 11ec792..8aee021 100644
--- a/common/endpoint_defaults.go
+++ b/common/endpoint_defaults.go
@@ -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"},
}
diff --git a/common/endpoint_type.go b/common/endpoint_type.go
index a5e2ff8..28ddd8f 100644
--- a/common/endpoint_type.go
+++ b/common/endpoint_type.go
@@ -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
diff --git a/constant/channel.go b/constant/channel.go
index 48502be..5cc9f5e 100644
--- a/constant/channel.go
+++ b/constant/channel.go
@@ -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 {
diff --git a/constant/endpoint_type.go b/constant/endpoint_type.go
index 8681bf0..c5d1e3b 100644
--- a/constant/endpoint_type.go
+++ b/constant/endpoint_type.go
@@ -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"
diff --git a/controller/channel-billing.go b/controller/channel-billing.go
index 751ee36..ad57126 100644
--- a/controller/channel-billing.go
+++ b/controller/channel-billing.go
@@ -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 {
diff --git a/controller/channel-test.go b/controller/channel-test.go
index bdd67d2..bb939bc 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -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{
diff --git a/controller/channel.go b/controller/channel.go
index b0dd228..6efbac6 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -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)
diff --git a/controller/channel_upstream_update.go b/controller/channel_upstream_update.go
index 1d85194..bed9e1b 100644
--- a/controller/channel_upstream_update.go
+++ b/controller/channel_upstream_update.go
@@ -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)
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 3c56faf..05ec8aa 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go
index 6daf5b6..edcf342 100644
--- a/relay/channel/claude/adaptor.go
+++ b/relay/channel/claude/adaptor.go
@@ -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"
diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go
index ef1411a..2a7d590 100644
--- a/relay/common/relay_info.go
+++ b/relay/common/relay_info.go
@@ -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 {
diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go
index 3cbb18c..6acc4a4 100644
--- a/relay/common/relay_utils.go
+++ b/relay/common/relay_utils.go
@@ -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
}
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index 2f24044..7f7015e 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -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 && (
+