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
|
apiType = constant.APITypeReplicate
|
||||||
case constant.ChannelTypeCodex:
|
case constant.ChannelTypeCodex:
|
||||||
apiType = constant.APITypeCodex
|
apiType = constant.APITypeCodex
|
||||||
|
case constant.ChannelTypeCodexCLIProxy:
|
||||||
|
apiType = constant.APITypeOpenAI
|
||||||
|
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||||
|
apiType = constant.APITypeAnthropic
|
||||||
|
case constant.ChannelTypeGeminiCLIProxy:
|
||||||
|
apiType = constant.APITypeOpenAI
|
||||||
}
|
}
|
||||||
if apiType == -1 {
|
if apiType == -1 {
|
||||||
return constant.APITypeOpenAI, false
|
return constant.APITypeOpenAI, false
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
|
|||||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||||
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
|
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
|
||||||
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", 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"},
|
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
|
|||||||
case constant.ChannelTypeAws:
|
case constant.ChannelTypeAws:
|
||||||
fallthrough
|
fallthrough
|
||||||
case constant.ChannelTypeAnthropic:
|
case constant.ChannelTypeAnthropic:
|
||||||
|
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||||
endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
|
endpointTypes = []constant.EndpointType{constant.EndpointTypeAnthropic, constant.EndpointTypeOpenAI}
|
||||||
case constant.ChannelTypeVertexAi:
|
case constant.ChannelTypeVertexAi:
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ const (
|
|||||||
ChannelTypeSora = 55
|
ChannelTypeSora = 55
|
||||||
ChannelTypeReplicate = 56
|
ChannelTypeReplicate = 56
|
||||||
ChannelTypeCodex = 57
|
ChannelTypeCodex = 57
|
||||||
|
ChannelTypeCodexCLIProxy = 58
|
||||||
|
ChannelTypeClaudeCodeCLIProxy = 59
|
||||||
|
ChannelTypeGeminiCLIProxy = 60
|
||||||
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
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.openai.com", //55
|
||||||
"https://api.replicate.com", //56
|
"https://api.replicate.com", //56
|
||||||
"https://chatgpt.com", //57
|
"https://chatgpt.com", //57
|
||||||
|
"", //58
|
||||||
|
"", //59
|
||||||
|
"", //60
|
||||||
}
|
}
|
||||||
|
|
||||||
var ChannelTypeNames = map[int]string{
|
var ChannelTypeNames = map[int]string{
|
||||||
@@ -175,6 +181,9 @@ var ChannelTypeNames = map[int]string{
|
|||||||
ChannelTypeSora: "Sora",
|
ChannelTypeSora: "Sora",
|
||||||
ChannelTypeReplicate: "Replicate",
|
ChannelTypeReplicate: "Replicate",
|
||||||
ChannelTypeCodex: "Codex",
|
ChannelTypeCodex: "Codex",
|
||||||
|
ChannelTypeCodexCLIProxy: "CodexCLIProxy",
|
||||||
|
ChannelTypeClaudeCodeCLIProxy: "ClaudeCodeCLIProxy",
|
||||||
|
ChannelTypeGeminiCLIProxy: "GeminiCLIProxy",
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetChannelTypeName(channelType int) string {
|
func GetChannelTypeName(channelType int) string {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
EndpointTypeGemini EndpointType = "gemini"
|
EndpointTypeGemini EndpointType = "gemini"
|
||||||
EndpointTypeJinaRerank EndpointType = "jina-rerank"
|
EndpointTypeJinaRerank EndpointType = "jina-rerank"
|
||||||
EndpointTypeImageGeneration EndpointType = "image-generation"
|
EndpointTypeImageGeneration EndpointType = "image-generation"
|
||||||
|
EndpointTypeImageEdit EndpointType = "image-edit"
|
||||||
EndpointTypeEmbeddings EndpointType = "embeddings"
|
EndpointTypeEmbeddings EndpointType = "embeddings"
|
||||||
EndpointTypeOpenAIVideo EndpointType = "openai-video"
|
EndpointTypeOpenAIVideo EndpointType = "openai-video"
|
||||||
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
|
//EndpointTypeMidjourney EndpointType = "midjourney-proxy"
|
||||||
|
|||||||
@@ -136,6 +136,14 @@ func GetClaudeAuthHeader(token string) http.Header {
|
|||||||
return h
|
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) {
|
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) {
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -42,6 +45,56 @@ type testResult struct {
|
|||||||
newAPIError *types.NewAPIError
|
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 {
|
func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointType string) string {
|
||||||
normalized := strings.TrimSpace(endpointType)
|
normalized := strings.TrimSpace(endpointType)
|
||||||
if normalized != "" {
|
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("Authorization", "Bearer "+channel.Key)
|
||||||
c.Request.Header.Set("Content-Type", "application/json")
|
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("channel", channel.Type)
|
||||||
c.Set("base_url", channel.GetBaseURL())
|
c.Set("base_url", channel.GetBaseURL())
|
||||||
group, _ := model.GetUserGroup(1, false)
|
group, _ := model.GetUserGroup(1, false)
|
||||||
@@ -184,7 +247,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
|||||||
relayFormat = types.RelayFormatGemini
|
relayFormat = types.RelayFormatGemini
|
||||||
case constant.EndpointTypeJinaRerank:
|
case constant.EndpointTypeJinaRerank:
|
||||||
relayFormat = types.RelayFormatRerank
|
relayFormat = types.RelayFormatRerank
|
||||||
case constant.EndpointTypeImageGeneration:
|
case constant.EndpointTypeImageGeneration, constant.EndpointTypeImageEdit:
|
||||||
relayFormat = types.RelayFormatOpenAIImage
|
relayFormat = types.RelayFormatOpenAIImage
|
||||||
case constant.EndpointTypeEmbeddings:
|
case constant.EndpointTypeEmbeddings:
|
||||||
relayFormat = types.RelayFormatEmbedding
|
relayFormat = types.RelayFormatEmbedding
|
||||||
@@ -200,6 +263,9 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
|||||||
if c.Request.URL.Path == "/v1/images/generations" {
|
if c.Request.URL.Path == "/v1/images/generations" {
|
||||||
relayFormat = types.RelayFormatOpenAIImage
|
relayFormat = types.RelayFormatOpenAIImage
|
||||||
}
|
}
|
||||||
|
if c.Request.URL.Path == "/v1/images/edits" {
|
||||||
|
relayFormat = types.RelayFormatOpenAIImage
|
||||||
|
}
|
||||||
if c.Request.URL.Path == "/v1/messages" {
|
if c.Request.URL.Path == "/v1/messages" {
|
||||||
relayFormat = types.RelayFormatClaude
|
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),
|
newAPIError: types.NewError(errors.New("invalid embedding request type"), types.ErrorCodeConvertRequestFailed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case relayconstant.RelayModeImagesGenerations:
|
case relayconstant.RelayModeImagesGenerations, relayconstant.RelayModeImagesEdits:
|
||||||
// 图像生成请求 - request 已经是正确的类型
|
// 图像生成请求 - request 已经是正确的类型
|
||||||
if imageReq, ok := request.(*dto.ImageRequest); ok {
|
if imageReq, ok := request.(*dto.ImageRequest); ok {
|
||||||
convertedRequest, err = adaptor.ConvertImageRequest(c, info, *imageReq)
|
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),
|
newAPIError: types.NewError(err, types.ErrorCodeConvertRequestFailed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsonData, err := common.Marshal(convertedRequest)
|
var requestBody *bytes.Buffer
|
||||||
if err != nil {
|
switch req := convertedRequest.(type) {
|
||||||
return testResult{
|
case *bytes.Buffer:
|
||||||
context: c,
|
requestBody = bytes.NewBuffer(req.Bytes())
|
||||||
localErr: err,
|
case bytes.Buffer:
|
||||||
newAPIError: types.NewError(err, types.ErrorCodeJsonMarshalFailed),
|
requestBody = bytes.NewBuffer(req.Bytes())
|
||||||
}
|
default:
|
||||||
}
|
jsonData, marshalErr := common.Marshal(convertedRequest)
|
||||||
|
if marshalErr != nil {
|
||||||
//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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return testResult{
|
return testResult{
|
||||||
context: c,
|
context: c,
|
||||||
localErr: err,
|
localErr: marshalErr,
|
||||||
newAPIError: types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid),
|
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)
|
resp, err := adaptor.DoRequest(c, info, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return testResult{
|
return testResult{
|
||||||
@@ -618,6 +693,14 @@ func buildTestRequest(model string, endpointType string, channel *model.Channel,
|
|||||||
N: lo.ToPtr(uint(1)),
|
N: lo.ToPtr(uint(1)),
|
||||||
Size: "1024x1024",
|
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:
|
case constant.EndpointTypeJinaRerank:
|
||||||
// 返回 RerankRequest
|
// 返回 RerankRequest
|
||||||
return &dto.RerankRequest{
|
return &dto.RerankRequest{
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ func buildFetchModelsHeaders(channel *model.Channel, key string) (http.Header, e
|
|||||||
switch channel.Type {
|
switch channel.Type {
|
||||||
case constant.ChannelTypeAnthropic:
|
case constant.ChannelTypeAnthropic:
|
||||||
headers = GetClaudeAuthHeader(key)
|
headers = GetClaudeAuthHeader(key)
|
||||||
|
case constant.ChannelTypeClaudeCodeCLIProxy:
|
||||||
|
headers = GetClaudeCodeAuthHeader(key)
|
||||||
default:
|
default:
|
||||||
headers = GetAuthHeader(key)
|
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)
|
// Codex OAuth key validation (optional, only when JSON object is provided)
|
||||||
if channel.Type == constant.ChannelTypeCodex {
|
if channel.Type == constant.ChannelTypeCodex {
|
||||||
trimmedKey := strings.TrimSpace(channel.Key)
|
trimmedKey := strings.TrimSpace(channel.Key)
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
|
|||||||
} else {
|
} else {
|
||||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||||
}
|
}
|
||||||
|
case constant.ChannelTypeCodexCLIProxy, constant.ChannelTypeGeminiCLIProxy:
|
||||||
|
url = fmt.Sprintf("%s/models", baseURL)
|
||||||
default:
|
default:
|
||||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,96 +1,27 @@
|
|||||||
# New-API Docker Compose Configuration
|
version: '3.4'
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
new-api:
|
new-api:
|
||||||
image: calciumion/new-api:latest
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: local/new-api:hk88-src-20260416-173717
|
||||||
container_name: new-api
|
container_name: new-api
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
command: --log-dir /app/logs
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- /docker/new-api/data:/data
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
environment:
|
||||||
- SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
|
- SQL_DSN=new_api:Ll@12331100@tcp(hdy-hk-8-8.311169.xyz:3306)/new_api
|
||||||
# - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service, uncomment if using MySQL
|
- REDIS_CONN_STRING=redis://:Ll12331100@hdy-hk-8-8.311169.xyz:6379
|
||||||
- REDIS_CONN_STRING=redis://redis
|
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录 (Whether to enable error log recording)
|
- SESSION_SECRET=ai-ikmkj-session-20260317
|
||||||
- BATCH_UPDATE_ENABLED=true # 是否启用批量更新 (Whether to enable batch update)
|
- CRYPTO_SECRET=ai-ikmkj-crypto-20260317
|
||||||
# - 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
|
|
||||||
networks:
|
networks:
|
||||||
- new-api-network
|
- default
|
||||||
healthcheck:
|
- biji-houduan_biji-net
|
||||||
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:
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
new-api-network:
|
biji-houduan_biji-net:
|
||||||
driver: bridge
|
external: true
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/constant"
|
||||||
"github.com/QuantumNous/new-api/dto"
|
"github.com/QuantumNous/new-api/dto"
|
||||||
"github.com/QuantumNous/new-api/relay/channel"
|
"github.com/QuantumNous/new-api/relay/channel"
|
||||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
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 {
|
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||||
channel.SetupApiRequestHeader(info, c, req)
|
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")
|
anthropicVersion := c.Request.Header.Get("anthropic-version")
|
||||||
if anthropicVersion == "" {
|
if anthropicVersion == "" {
|
||||||
anthropicVersion = "2023-06-01"
|
anthropicVersion = "2023-06-01"
|
||||||
|
|||||||
@@ -301,24 +301,27 @@ func (info *RelayInfo) ToString() string {
|
|||||||
|
|
||||||
// 定义支持流式选项的通道类型
|
// 定义支持流式选项的通道类型
|
||||||
var streamSupportedChannels = map[int]bool{
|
var streamSupportedChannels = map[int]bool{
|
||||||
constant.ChannelTypeOpenAI: true,
|
constant.ChannelTypeOpenAI: true,
|
||||||
constant.ChannelTypeAnthropic: true,
|
constant.ChannelTypeCodexCLIProxy: true,
|
||||||
constant.ChannelTypeAws: true,
|
constant.ChannelTypeGeminiCLIProxy: true,
|
||||||
constant.ChannelTypeGemini: true,
|
constant.ChannelTypeAnthropic: true,
|
||||||
constant.ChannelCloudflare: true,
|
constant.ChannelTypeClaudeCodeCLIProxy: true,
|
||||||
constant.ChannelTypeAzure: true,
|
constant.ChannelTypeAws: true,
|
||||||
constant.ChannelTypeVolcEngine: true,
|
constant.ChannelTypeGemini: true,
|
||||||
constant.ChannelTypeOllama: true,
|
constant.ChannelCloudflare: true,
|
||||||
constant.ChannelTypeXai: true,
|
constant.ChannelTypeAzure: true,
|
||||||
constant.ChannelTypeDeepSeek: true,
|
constant.ChannelTypeVolcEngine: true,
|
||||||
constant.ChannelTypeBaiduV2: true,
|
constant.ChannelTypeOllama: true,
|
||||||
constant.ChannelTypeZhipu_v4: true,
|
constant.ChannelTypeXai: true,
|
||||||
constant.ChannelTypeAli: true,
|
constant.ChannelTypeDeepSeek: true,
|
||||||
constant.ChannelTypeSubmodel: true,
|
constant.ChannelTypeBaiduV2: true,
|
||||||
constant.ChannelTypeCodex: true,
|
constant.ChannelTypeZhipu_v4: true,
|
||||||
constant.ChannelTypeMoonshot: true,
|
constant.ChannelTypeAli: true,
|
||||||
constant.ChannelTypeMiniMax: true,
|
constant.ChannelTypeSubmodel: true,
|
||||||
constant.ChannelTypeSiliconFlow: true,
|
constant.ChannelTypeCodex: true,
|
||||||
|
constant.ChannelTypeMoonshot: true,
|
||||||
|
constant.ChannelTypeMiniMax: true,
|
||||||
|
constant.ChannelTypeSiliconFlow: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
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"))
|
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
|
return fullRequestURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ import {
|
|||||||
verifyJSON,
|
verifyJSON,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
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 {
|
import {
|
||||||
SideSheet,
|
SideSheet,
|
||||||
Space,
|
Space,
|
||||||
@@ -128,7 +131,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan';
|
|||||||
|
|
||||||
// 支持并且已适配通过接口获取模型列表的渠道类型
|
// 支持并且已适配通过接口获取模型列表的渠道类型
|
||||||
const MODEL_FETCHABLE_TYPES = new Set([
|
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) {
|
function type2secretPrompt(type) {
|
||||||
@@ -277,7 +280,8 @@ const EditChannelModal = (props) => {
|
|||||||
[inputs.upstream_model_update_last_detected_models],
|
[inputs.upstream_model_update_last_detected_models],
|
||||||
);
|
);
|
||||||
const upstreamDetectedModelsPreview = useMemo(
|
const upstreamDetectedModelsPreview = useMemo(
|
||||||
() => upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
() =>
|
||||||
|
upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
||||||
[upstreamDetectedModels],
|
[upstreamDetectedModels],
|
||||||
);
|
);
|
||||||
const upstreamDetectedModelsOmittedCount =
|
const upstreamDetectedModelsOmittedCount =
|
||||||
@@ -310,9 +314,7 @@ const EditChannelModal = (props) => {
|
|||||||
return {
|
return {
|
||||||
tagLabel: t('不更改'),
|
tagLabel: t('不更改'),
|
||||||
tagColor: 'grey',
|
tagColor: 'grey',
|
||||||
preview: t(
|
preview: t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'),
|
||||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!verifyJSON(raw)) {
|
if (!verifyJSON(raw)) {
|
||||||
@@ -1612,7 +1614,7 @@ const EditChannelModal = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
localInputs.type === 45 &&
|
[45, 58, 59, 60].includes(localInputs.type) &&
|
||||||
(!localInputs.base_url || localInputs.base_url.trim() === '')
|
(!localInputs.base_url || localInputs.base_url.trim() === '')
|
||||||
) {
|
) {
|
||||||
showInfo(t('请输入API地址!'));
|
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 && (
|
{inputs.type === 20 && (
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='is_enterprise_account'
|
field='is_enterprise_account'
|
||||||
@@ -3001,6 +3036,9 @@ const EditChannelModal = (props) => {
|
|||||||
inputs.type !== 8 &&
|
inputs.type !== 8 &&
|
||||||
inputs.type !== 22 &&
|
inputs.type !== 22 &&
|
||||||
inputs.type !== 36 &&
|
inputs.type !== 36 &&
|
||||||
|
inputs.type !== 58 &&
|
||||||
|
inputs.type !== 59 &&
|
||||||
|
inputs.type !== 60 &&
|
||||||
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
|
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
|
||||||
<div>
|
<div>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
@@ -3057,6 +3095,66 @@ const EditChannelModal = (props) => {
|
|||||||
</div>
|
</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 && (
|
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
||||||
<div>
|
<div>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
@@ -3478,79 +3576,81 @@ const EditChannelModal = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='upstream_model_update_auto_sync_enabled'
|
field='upstream_model_update_auto_sync_enabled'
|
||||||
label={t('是否自动同步上游模型更新')}
|
label={t('是否自动同步上游模型更新')}
|
||||||
checkedText={t('开')}
|
checkedText={t('开')}
|
||||||
uncheckedText={t('关')}
|
uncheckedText={t('关')}
|
||||||
disabled={!inputs.upstream_model_update_check_enabled}
|
disabled={!inputs.upstream_model_update_check_enabled}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleChannelOtherSettingsChange(
|
handleChannelOtherSettingsChange(
|
||||||
'upstream_model_update_auto_sync_enabled',
|
'upstream_model_update_auto_sync_enabled',
|
||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
extraText={t(
|
extraText={t(
|
||||||
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='text-xs text-gray-500 mb-3'>
|
<div className='text-xs text-gray-500 mb-3'>
|
||||||
{t('上次检测到可加入模型')}:
|
{t('上次检测到可加入模型')}:
|
||||||
{upstreamDetectedModels.length === 0 ? (
|
{upstreamDetectedModels.length === 0 ? (
|
||||||
t('暂无')
|
t('暂无')
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position='topLeft'
|
position='topLeft'
|
||||||
content={
|
content={
|
||||||
<div className='max-w-[640px] break-all text-xs leading-5'>
|
<div className='max-w-[640px] break-all text-xs leading-5'>
|
||||||
{upstreamDetectedModels.join(', ')}
|
{upstreamDetectedModels.join(', ')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className='cursor-help break-all'>
|
<span className='cursor-help break-all'>
|
||||||
{upstreamDetectedModelsPreview.join(', ')}
|
{upstreamDetectedModelsPreview.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className='ml-1 text-gray-400'>
|
<span className='ml-1 text-gray-400'>
|
||||||
{upstreamDetectedModelsOmittedCount > 0
|
{upstreamDetectedModelsOmittedCount > 0
|
||||||
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
||||||
total: upstreamDetectedModels.length,
|
total: upstreamDetectedModels.length,
|
||||||
omit: upstreamDetectedModelsOmittedCount,
|
omit: upstreamDetectedModelsOmittedCount,
|
||||||
})
|
})
|
||||||
: t('(共 {{total}} 个)', {
|
: t('(共 {{total}} 个)', {
|
||||||
total: upstreamDetectedModels.length,
|
total: upstreamDetectedModels.length,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mb-4'>
|
<div className='mb-4'>
|
||||||
<div className='flex items-center justify-between gap-2 mb-1'>
|
<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>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='primary'
|
type='primary'
|
||||||
icon={<IconCode size={14} />}
|
icon={<IconCode size={14} />}
|
||||||
onClick={() => setParamOverrideEditorVisible(true)}
|
onClick={() => setParamOverrideEditorVisible(true)}
|
||||||
>
|
>
|
||||||
{t('可视化编辑')}
|
{t('可视化编辑')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
applyParamOverrideTemplate('operations', 'fill')
|
applyParamOverrideTemplate('operations', 'fill')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('填充新模板')}
|
{t('填充新模板')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
applyParamOverrideTemplate('legacy', 'fill')
|
applyParamOverrideTemplate('legacy', 'fill')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('填充旧模板')}
|
{t('填充旧模板')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3564,14 +3664,16 @@ const EditChannelModal = (props) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Text type='tertiary' size='small'>
|
<Text type='tertiary' size='small'>
|
||||||
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
{t(
|
||||||
|
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<div
|
<div
|
||||||
className='mt-2 rounded-xl p-3'
|
className='mt-2 rounded-xl p-3'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--semi-color-fill-0)',
|
backgroundColor: 'var(--semi-color-fill-0)',
|
||||||
border: '1px solid var(--semi-color-fill-2)',
|
border: '1px solid var(--semi-color-fill-2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center justify-between mb-2'>
|
<div className='flex items-center justify-between mb-2'>
|
||||||
<Tag color={paramOverrideMeta.tagColor}>
|
<Tag color={paramOverrideMeta.tagColor}>
|
||||||
@@ -3579,17 +3681,19 @@ const EditChannelModal = (props) => {
|
|||||||
</Tag>
|
</Tag>
|
||||||
<Space spacing={8}>
|
<Space spacing={8}>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
icon={<IconCopy />}
|
icon={<IconCopy />}
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={copyParamOverrideJson}
|
onClick={copyParamOverrideJson}
|
||||||
>
|
>
|
||||||
{t('复制')}
|
{t('复制')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='tertiary'
|
type='tertiary'
|
||||||
onClick={() => setParamOverrideEditorVisible(true)}
|
onClick={() =>
|
||||||
|
setParamOverrideEditorVisible(true)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t('编辑')}
|
{t('编辑')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -3602,80 +3706,80 @@ const EditChannelModal = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
field='header_override'
|
field='header_override'
|
||||||
label={t('请求头覆盖')}
|
label={t('请求头覆盖')}
|
||||||
placeholder={
|
placeholder={
|
||||||
t('此项可选,用于覆盖请求头参数') +
|
t('此项可选,用于覆盖请求头参数') +
|
||||||
'\n' +
|
'\n' +
|
||||||
t('格式示例:') +
|
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}'
|
'\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
|
autosize
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
handleInputChange('header_override', value)
|
handleInputChange('header_override', value)
|
||||||
}
|
}
|
||||||
extraText={
|
extraText={
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='flex gap-2 flex-wrap items-center'>
|
<div className='flex gap-2 flex-wrap items-center'>
|
||||||
<Text
|
<Text
|
||||||
className='!text-semi-color-primary cursor-pointer'
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
'header_override',
|
'header_override',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
'*': true,
|
'*': true,
|
||||||
're:^X-Trace-.*$': true,
|
're:^X-Trace-.*$': true,
|
||||||
'X-Foo': '{client_header:X-Foo}',
|
'X-Foo': '{client_header:X-Foo}',
|
||||||
Authorization: 'Bearer {api_key}',
|
Authorization: 'Bearer {api_key}',
|
||||||
'User-Agent':
|
'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',
|
'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,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('填入模板')}
|
{t('填入模板')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
className='!text-semi-color-primary cursor-pointer'
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
'header_override',
|
'header_override',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
'*': true,
|
'*': true,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('填入透传模版')}
|
{t('填入透传模版')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
className='!text-semi-color-primary cursor-pointer'
|
className='!text-semi-color-primary cursor-pointer'
|
||||||
onClick={() => formatJsonField('header_override')}
|
onClick={() => formatJsonField('header_override')}
|
||||||
>
|
>
|
||||||
{t('格式化')}
|
{t('格式化')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Text type='tertiary' size='small'>
|
<Text type='tertiary' size='small'>
|
||||||
{t('支持变量:')}
|
{t('支持变量:')}
|
||||||
</Text>
|
</Text>
|
||||||
<div className='text-xs text-tertiary ml-2'>
|
<div className='text-xs text-tertiary ml-2'>
|
||||||
<div>
|
<div>
|
||||||
{t('渠道密钥')}: {'{api_key}'}
|
{t('渠道密钥')}: {'{api_key}'}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
showClear
|
}
|
||||||
|
showClear
|
||||||
/>
|
/>
|
||||||
<JSONEditor
|
<JSONEditor
|
||||||
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
|
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const ModelTestModal = ({
|
|||||||
const streamToggleDisabled = [
|
const streamToggleDisabled = [
|
||||||
'embeddings',
|
'embeddings',
|
||||||
'image-generation',
|
'image-generation',
|
||||||
|
'image-edit',
|
||||||
'jina-rerank',
|
'jina-rerank',
|
||||||
'openai-response-compact',
|
'openai-response-compact',
|
||||||
].includes(selectedEndpointType);
|
].includes(selectedEndpointType);
|
||||||
@@ -96,6 +97,10 @@ const ModelTestModal = ({
|
|||||||
value: 'image-generation',
|
value: 'image-generation',
|
||||||
label: t('图像生成') + ' (/v1/images/generations)',
|
label: t('图像生成') + ' (/v1/images/generations)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'image-edit',
|
||||||
|
label: 'OpenAI Image Edit (/v1/images/edits)',
|
||||||
|
},
|
||||||
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
|
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const ENDPOINT_TEMPLATE = {
|
|||||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||||
|
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameRuleOptions = [
|
const nameRuleOptions = [
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const ENDPOINT_TEMPLATE = {
|
|||||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||||
|
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditPrefillGroupModal = ({
|
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',
|
color: 'blue',
|
||||||
label: 'Codex (OpenAI OAuth)',
|
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.
|
// Channel types that support upstream model list fetching in UI.
|
||||||
export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
|
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;
|
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||||
|
|||||||
Reference in New Issue
Block a user