From 58c0258362f8871e3c4bb7881998d6a73bc4a6de Mon Sep 17 00:00:00 2001 From: OpenClaw Task Bot Date: Mon, 27 Apr 2026 08:56:38 +0800 Subject: [PATCH] feat: sync remaining channel and relay updates --- common/api_type.go | 6 + common/endpoint_defaults.go | 1 + common/endpoint_type.go | 1 + constant/channel.go | 9 + constant/endpoint_type.go | 1 + controller/channel-billing.go | 8 + controller/channel-test.go | 151 +++++-- controller/channel.go | 8 + controller/channel_upstream_update.go | 2 + docker-compose.yml | 99 +---- relay/channel/claude/adaptor.go | 8 +- relay/common/relay_info.go | 39 +- relay/common/relay_utils.go | 9 + .../channels/modals/EditChannelModal.jsx | 370 +++++++++++------- .../table/channels/modals/ModelTestModal.jsx | 5 + .../table/models/modals/EditModelModal.jsx | 1 + .../models/modals/EditPrefillGroupModal.jsx | 1 + web/src/constants/channel.constants.js | 17 +- 18 files changed, 465 insertions(+), 271 deletions(-) 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 && ( + + )} + + {inputs.type === 59 && ( + + )} + + {inputs.type === 60 && ( + + )} + {inputs.type === 20 && ( { inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && + inputs.type !== 58 && + inputs.type !== 59 && + inputs.type !== 60 && (inputs.type !== 45 || doubaoApiEditUnlocked) && (
{
)} + {inputs.type === 58 && ( +
+ + handleInputChange('base_url', value) + } + showClear + disabled={isIonetLocked} + extraText={t( + '此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。', + )} + /> +
+ )} + + {inputs.type === 59 && ( +
+ + handleInputChange('base_url', value) + } + showClear + disabled={isIonetLocked} + extraText={t( + '此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。', + )} + /> +
+ )} + + {inputs.type === 60 && ( +
+ + handleInputChange('base_url', value) + } + showClear + disabled={isIonetLocked} + extraText={t( + '此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。', + )} + /> +
+ )} + {inputs.type === 45 && !doubaoApiEditUnlocked && (
{ /> - 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( + '开启后检测到新增模型会自动加入当前渠道模型列表', + )} />
{t('上次检测到可加入模型')}:  {upstreamDetectedModels.length === 0 ? ( - t('暂无') + t('暂无') ) : ( - <> - - {upstreamDetectedModels.join(', ')} -
- } - > + <> + + {upstreamDetectedModels.join(', ')} +
+ } + > {upstreamDetectedModelsPreview.join(', ')} - - + + {upstreamDetectedModelsOmittedCount > 0 - ? t('(共 {{total}} 个,省略 {{omit}} 个)', { + ? t('(共 {{total}} 个,省略 {{omit}} 个)', { total: upstreamDetectedModels.length, omit: upstreamDetectedModelsOmittedCount, }) - : t('(共 {{total}} 个)', { + : t('(共 {{total}} 个)', { total: upstreamDetectedModels.length, })} - + )}
- {t('参数覆盖')} + + {t('参数覆盖')} + @@ -3564,14 +3664,16 @@ const EditChannelModal = (props) => {
- {t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')} + {t( + '此项可选,用于覆盖请求参数。不支持覆盖 stream 参数', + )}
@@ -3579,17 +3681,19 @@ const EditChannelModal = (props) => { @@ -3602,80 +3706,80 @@ const EditChannelModal = (props) => {
- handleInputChange('header_override', value) - } - extraText={ -
-
- - 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('填入模板')} - - - handleInputChange( - 'header_override', - JSON.stringify( - { - '*': true, - }, - null, - 2, - ), - ) - } - > - {t('填入透传模版')} - - formatJsonField('header_override')} - > - {t('格式化')} - -
-
- - {t('支持变量:')} - -
-
- {t('渠道密钥')}: {'{api_key}'} -
+ 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={ +
+
+ + 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('填入模板')} + + + handleInputChange( + 'header_override', + JSON.stringify( + { + '*': true, + }, + null, + 2, + ), + ) + } + > + {t('填入透传模版')} + + formatJsonField('header_override')} + > + {t('格式化')} + +
+
+ + {t('支持变量:')} + +
+
+ {t('渠道密钥')}: {'{api_key}'}
- } - showClear +
+ } + showClear />