diff --git a/common/api_type.go b/common/api_type.go index c198ffc0e03d..1644a45a95e1 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -77,6 +77,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeCodex case constant.ChannelTypeAdvancedCustom: apiType = constant.APITypeAdvancedCustom + case constant.ChannelTypeAwsOpenAI: + apiType = constant.APITypeOpenAI } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/common/bedrock_openai_test.go b/common/bedrock_openai_test.go new file mode 100644 index 000000000000..5b33c51dc915 --- /dev/null +++ b/common/bedrock_openai_test.go @@ -0,0 +1,56 @@ +package common + +import ( + "testing" + + "github.com/QuantumNous/new-api/constant" + "github.com/stretchr/testify/require" +) + +func TestBedrockOpenAIModelClassification(t *testing.T) { + tests := []struct { + name string + model string + responsesOnly bool + mantleChat bool + runtimeChat bool + }{ + {name: "frontier", model: "openai.gpt-5.6-sol", responsesOnly: true}, + {name: "frontier effort suffix", model: "openai.gpt-5.5-xhigh", responsesOnly: true}, + {name: "mantle gpt oss", model: "openai.gpt-oss-120b", mantleChat: true}, + {name: "safeguard chat only", model: "openai.gpt-oss-safeguard-20b"}, + {name: "runtime gpt oss", model: "openai.gpt-oss-20b-1:0", runtimeChat: true}, + {name: "unrelated", model: "gpt-5.6-sol"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.responsesOnly, IsBedrockOpenAIResponsesModel(tt.model)) + require.Equal(t, tt.mantleChat, IsBedrockOpenAIMantleChatModel(tt.model)) + require.Equal(t, tt.runtimeChat, IsBedrockOpenAIRuntimeChatModel(tt.model)) + }) + } +} + +func TestBedrockOpenAIChannelMetadata(t *testing.T) { + apiType, ok := ChannelType2APIType(constant.ChannelTypeAwsOpenAI) + require.True(t, ok) + require.Equal(t, constant.APITypeOpenAI, apiType) + + require.Equal(t, + []constant.EndpointType{constant.EndpointTypeOpenAIResponse}, + GetEndpointTypesByChannelType(constant.ChannelTypeAwsOpenAI, "openai.gpt-5.4"), + ) + require.Equal(t, + []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse}, + GetEndpointTypesByChannelType(constant.ChannelTypeAwsOpenAI, "openai.gpt-oss-120b"), + ) + require.Equal(t, + []constant.EndpointType{constant.EndpointTypeOpenAI}, + GetEndpointTypesByChannelType(constant.ChannelTypeAwsOpenAI, "openai.gpt-oss-120b-1:0"), + ) + require.Equal(t, + []constant.EndpointType{constant.EndpointTypeOpenAI}, + GetEndpointTypesByChannelType(constant.ChannelTypeAwsOpenAI, "openai.gpt-oss-safeguard-120b"), + ) +} diff --git a/common/endpoint_type.go b/common/endpoint_type.go index a5e2ff8412e8..3f94afc6d1f7 100644 --- a/common/endpoint_type.go +++ b/common/endpoint_type.go @@ -28,6 +28,14 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI} case constant.ChannelTypeXai: endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse} + case constant.ChannelTypeAwsOpenAI: + if IsBedrockOpenAIResponsesModel(modelName) { + endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIResponse} + } else if IsBedrockOpenAIMantleChatModel(modelName) { + endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse} + } else { + endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI} + } case constant.ChannelTypeSora: endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo} default: diff --git a/common/model.go b/common/model.go index 4ebc7b532d74..47c00f641444 100644 --- a/common/model.go +++ b/common/model.go @@ -24,8 +24,65 @@ var ( "o4", "chatgpt", } + // BedrockOpenAIResponsesModels use the model-specific /openai/v1/responses + // path on the Bedrock Mantle endpoint and do not support Chat Completions. + BedrockOpenAIResponsesModels = []string{ + "openai.gpt-5.6-sol", + "openai.gpt-5.6-terra", + "openai.gpt-5.6-luna", + "openai.gpt-5.5", + "openai.gpt-5.4", + } + // BedrockOpenAIMantleChatModels support both Chat Completions and Responses + // through the standard /v1 path on the Bedrock Mantle endpoint. + BedrockOpenAIMantleChatModels = []string{ + "openai.gpt-oss-120b", + "openai.gpt-oss-20b", + } + // BedrockOpenAIChatOnlyModels support Chat Completions through both the + // Bedrock Mantle and Runtime endpoints. + BedrockOpenAIChatOnlyModels = []string{ + "openai.gpt-oss-safeguard-120b", + "openai.gpt-oss-safeguard-20b", + } + // BedrockOpenAIRuntimeChatModels use the OpenAI-compatible Chat Completions + // endpoint exposed by bedrock-runtime. + BedrockOpenAIRuntimeChatModels = []string{ + "openai.gpt-oss-120b-1:0", + "openai.gpt-oss-20b-1:0", + } ) +func modelNameInList(modelName string, models []string) bool { + for _, model := range models { + if strings.EqualFold(modelName, model) { + return true + } + } + return false +} + +func normalizeBedrockOpenAIModelName(modelName string) string { + for _, suffix := range []string{"-xhigh", "-medium", "-minimal", "-high", "-low", "-none"} { + if strings.HasSuffix(modelName, suffix) { + return strings.TrimSuffix(modelName, suffix) + } + } + return modelName +} + +func IsBedrockOpenAIResponsesModel(modelName string) bool { + return modelNameInList(normalizeBedrockOpenAIModelName(modelName), BedrockOpenAIResponsesModels) +} + +func IsBedrockOpenAIMantleChatModel(modelName string) bool { + return modelNameInList(normalizeBedrockOpenAIModelName(modelName), BedrockOpenAIMantleChatModels) +} + +func IsBedrockOpenAIRuntimeChatModel(modelName string) bool { + return modelNameInList(normalizeBedrockOpenAIModelName(modelName), BedrockOpenAIRuntimeChatModels) +} + func IsOpenAIResponseOnlyModel(modelName string) bool { for _, m := range OpenAIResponseOnlyModels { if strings.Contains(modelName, m) { diff --git a/constant/channel.go b/constant/channel.go index 45ec9a44e49a..700f971b2674 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -56,6 +56,7 @@ const ( ChannelTypeReplicate = 56 ChannelTypeCodex = 57 ChannelTypeAdvancedCustom = 58 + ChannelTypeAwsOpenAI = 59 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -120,6 +121,7 @@ var ChannelBaseURLs = []string{ "https://api.replicate.com", //56 "https://chatgpt.com", //57 "", //58 + "", //59 } var ChannelTypeNames = map[int]string{ @@ -178,6 +180,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeReplicate: "Replicate", ChannelTypeCodex: "ChatGPT Subscription (Codex)", ChannelTypeAdvancedCustom: "Advanced Custom", + ChannelTypeAwsOpenAI: "AWS OpenAI", } func GetChannelTypeName(channelType int) string { diff --git a/controller/channel-test.go b/controller/channel-test.go index 4ba3698bd54c..b6fdc4645959 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -52,6 +52,22 @@ func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointTyp if channel != nil && channel.Type == constant.ChannelTypeCodex { return string(constant.EndpointTypeOpenAIResponse) } + if channel != nil && channel.Type == constant.ChannelTypeAwsOpenAI { + upstreamModelName := modelName + modelMapping := normalizeChannelModelMapping(channel) + visited := map[string]bool{upstreamModelName: true} + for { + mappedModelName, ok := modelMapping[upstreamModelName] + if !ok || visited[mappedModelName] { + break + } + visited[mappedModelName] = true + upstreamModelName = mappedModelName + } + if common.IsBedrockOpenAIResponsesModel(upstreamModelName) { + return string(constant.EndpointTypeOpenAIResponse) + } + } return normalized } diff --git a/controller/channel_test_internal_test.go b/controller/channel_test_internal_test.go index 56810fdc0ba5..476eaee5a7a8 100644 --- a/controller/channel_test_internal_test.go +++ b/controller/channel_test_internal_test.go @@ -211,6 +211,28 @@ func TestResolveChannelTestUserIDUsesRequestUser(t *testing.T) { require.Equal(t, 2, userID) } +func TestNormalizeChannelTestEndpointForBedrockOpenAI(t *testing.T) { + modelMapping := `{"bedrock-frontier":"openai.gpt-5.5"}` + channel := &model.Channel{ + Type: constant.ChannelTypeAwsOpenAI, + ModelMapping: &modelMapping, + } + + require.Equal(t, + string(constant.EndpointTypeOpenAIResponse), + normalizeChannelTestEndpoint(channel, "openai.gpt-5.5", ""), + ) + require.Empty(t, normalizeChannelTestEndpoint(channel, "openai.gpt-oss-120b", "")) + require.Equal(t, + string(constant.EndpointTypeOpenAIResponse), + normalizeChannelTestEndpoint(channel, "bedrock-frontier", ""), + ) + require.Equal(t, + string(constant.EndpointTypeOpenAI), + normalizeChannelTestEndpoint(channel, "openai.gpt-5.5", string(constant.EndpointTypeOpenAI)), + ) +} + func TestSelectChannelsForAutomaticTestPassiveRecoveryOnlyUsesAutoDisabled(t *testing.T) { channels := []*model.Channel{ {Id: 1, Status: common.ChannelStatusEnabled}, diff --git a/controller/model.go b/controller/model.go index 09ea70fc8902..e56a7b0a05dc 100644 --- a/controller/model.go +++ b/controller/model.go @@ -15,6 +15,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel/lingyiwanwu" "github.com/QuantumNous/new-api/relay/channel/minimax" "github.com/QuantumNous/new-api/relay/channel/moonshot" + "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/helper" "github.com/QuantumNous/new-api/service" @@ -80,6 +81,14 @@ func init() { OwnedBy: minimax.ChannelName, }) } + for _, modelName := range openai.BedrockOpenAIModelList { + openAIModels = append(openAIModels, dto.OpenAIModels{ + Id: modelName, + Object: "model", + Created: 1626777600, + OwnedBy: openai.BedrockOpenAIChannelName, + }) + } for modelName, _ := range constant.MidjourneyModel2Action { openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, diff --git a/controller/model_owned_by_test.go b/controller/model_owned_by_test.go index bc2ef32f135c..21b0aab4a80c 100644 --- a/controller/model_owned_by_test.go +++ b/controller/model_owned_by_test.go @@ -31,6 +31,11 @@ func TestChannelOwnerNameUsesAdaptorChannelName(t *testing.T) { channelType: constant.ChannelTypeOpenRouter, expected: "openrouter", }, + { + name: "bedrock openai", + channelType: constant.ChannelTypeAwsOpenAI, + expected: "AWS OpenAI", + }, { name: "azure fallback", channelType: constant.ChannelTypeAzure, @@ -57,6 +62,12 @@ func TestBuildOpenAIModelFallsBackToCustomForUnknownModels(t *testing.T) { require.Equal(t, "custom", modelItem.OwnedBy) } +func TestBedrockOpenAIModelsAreRegistered(t *testing.T) { + modelItem, ok := openAIModelsMap["openai.gpt-5.6-sol"] + require.True(t, ok) + require.Equal(t, "AWS OpenAI", modelItem.OwnedBy) +} + func TestGetModelListGroupsUsesUserGroupWhenTokenGroupIsEmpty(t *testing.T) { gin.SetMode(gin.TestMode) ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index d2f7c6bb6d5a..d9544fa761d2 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -31,6 +31,13 @@ type Adaptor interface { ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) } +// RequestSigner signs a fully constructed outbound HTTP request after header +// overrides have been applied. Adaptors that use request-bound authentication, +// such as AWS SigV4, can implement this optional interface. +type RequestSigner interface { + SignRequest(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error +} + type TaskAdaptor interface { Init(info *relaycommon.RelayInfo) diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 62de6a32026b..f8700e6601b3 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -304,6 +304,17 @@ func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]s } } +func signRequestIfNeeded(a Adaptor, c *gin.Context, req *http.Request, info *common.RelayInfo) error { + signer, ok := a.(RequestSigner) + if !ok { + return nil + } + if err := signer.SignRequest(c, req, info); err != nil { + return fmt.Errorf("sign request failed: %w", err) + } + return nil +} + func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) { fullRequestURL, err := a.GetRequestURL(info) if err != nil { @@ -327,6 +338,9 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody return nil, err } applyHeaderOverrideToRequest(req, headerOverride) + if err = signRequestIfNeeded(a, c, req, info); err != nil { + return nil, err + } resp, err := doRequest(c, req, info) if err != nil { return nil, fmt.Errorf("do request failed: %w", err) @@ -359,6 +373,9 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod return nil, err } applyHeaderOverrideToRequest(req, headerOverride) + if err = signRequestIfNeeded(a, c, req, info); err != nil { + return nil, err + } resp, err := doRequest(c, req, info) if err != nil { return nil, fmt.Errorf("do request failed: %w", err) diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 3fd24665c939..bcfe152d7e1b 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -115,6 +115,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { } } switch info.ChannelType { + case constant.ChannelTypeAwsOpenAI: + return getBedrockOpenAIRequestURL(info) case constant.ChannelTypeAzure: apiVersion := info.ApiVersion if apiVersion == "" { @@ -182,6 +184,16 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error { channel.SetupApiRequestHeader(info, c, header) + if info.ChannelType == constant.ChannelTypeAwsOpenAI { + credentials, err := parseBedrockOpenAICredentials(info) + if err != nil { + return err + } + if credentials.keyType == dto.AwsKeyTypeApiKey { + header.Set("Authorization", "Bearer "+credentials.apiKey) + } + return nil + } if info.ChannelType == constant.ChannelTypeAzure { header.Set("api-key", info.ApiKey) return nil @@ -245,7 +257,9 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn if request == nil { return nil, errors.New("request is nil") } - if info.ChannelType != constant.ChannelTypeOpenAI && info.ChannelType != constant.ChannelTypeAzure { + if info.ChannelType != constant.ChannelTypeOpenAI && + info.ChannelType != constant.ChannelTypeAzure && + info.ChannelType != constant.ChannelTypeAwsOpenAI { request.StreamOptions = nil } if info.ChannelType == constant.ChannelTypeOpenRouter { @@ -680,6 +694,8 @@ func (a *Adaptor) GetModelList() []string { return xinference.ModelList case constant.ChannelTypeOpenRouter: return openrouter.ModelList + case constant.ChannelTypeAwsOpenAI: + return BedrockOpenAIModelList default: return ModelList } @@ -697,6 +713,8 @@ func (a *Adaptor) GetChannelName() string { return xinference.ChannelName case constant.ChannelTypeOpenRouter: return openrouter.ChannelName + case constant.ChannelTypeAwsOpenAI: + return BedrockOpenAIChannelName default: return ChannelName } diff --git a/relay/channel/openai/bedrock_openai.go b/relay/channel/openai/bedrock_openai.go new file mode 100644 index 000000000000..1bd1e27e5ea8 --- /dev/null +++ b/relay/channel/openai/bedrock_openai.go @@ -0,0 +1,236 @@ +package openai + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/aws/aws-sdk-go-v2/aws" + awsv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + "github.com/gin-gonic/gin" +) + +const BedrockOpenAIChannelName = "AWS OpenAI" + +type bedrockOpenAICredentials struct { + keyType dto.AwsKeyType + accessKeyID string + secretAccessKey string + apiKey string + region string +} + +var BedrockOpenAIModelList = func() []string { + models := make([]string, 0, len(common.BedrockOpenAIResponsesModels)+len(common.BedrockOpenAIMantleChatModels)+len(common.BedrockOpenAIChatOnlyModels)+len(common.BedrockOpenAIRuntimeChatModels)) + models = append(models, common.BedrockOpenAIResponsesModels...) + models = append(models, common.BedrockOpenAIMantleChatModels...) + models = append(models, common.BedrockOpenAIChatOnlyModels...) + models = append(models, common.BedrockOpenAIRuntimeChatModels...) + return models +}() + +func parseBedrockOpenAICredentials(info *relaycommon.RelayInfo) (bedrockOpenAICredentials, error) { + if info == nil { + return bedrockOpenAICredentials{}, fmt.Errorf("AWS OpenAI relay info is missing") + } + + keyType := info.ChannelOtherSettings.AwsKeyType + if keyType == "" { + keyType = dto.AwsKeyTypeAKSK + } + parts := strings.Split(info.ApiKey, "|") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + + credentials := bedrockOpenAICredentials{keyType: keyType} + switch keyType { + case dto.AwsKeyTypeAKSK: + if len(parts) != 3 || parts[0] == "" || parts[1] == "" || parts[2] == "" { + return bedrockOpenAICredentials{}, fmt.Errorf("invalid AWS OpenAI credentials: expected AccessKey|SecretAccessKey|Region") + } + credentials.accessKeyID = parts[0] + credentials.secretAccessKey = parts[1] + credentials.region = parts[2] + case dto.AwsKeyTypeApiKey: + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return bedrockOpenAICredentials{}, fmt.Errorf("invalid AWS OpenAI API key: expected APIKey|Region") + } + credentials.apiKey = parts[0] + credentials.region = parts[1] + default: + return bedrockOpenAICredentials{}, fmt.Errorf("unsupported AWS OpenAI key type: %s", keyType) + } + + if !isValidBedrockOpenAIRegion(credentials.region) { + return bedrockOpenAICredentials{}, fmt.Errorf("invalid AWS region: %s", credentials.region) + } + return credentials, nil +} + +func isValidBedrockOpenAIRegion(region string) bool { + if len(region) < 3 || len(region) > 64 || strings.HasPrefix(region, "-") || strings.HasSuffix(region, "-") { + return false + } + parts := strings.Split(region, "-") + if len(parts) < 3 { + return false + } + for _, part := range parts { + if part == "" { + return false + } + for _, char := range part { + if (char < 'a' || char > 'z') && (char < '0' || char > '9') { + return false + } + } + } + for _, char := range parts[len(parts)-1] { + if char < '0' || char > '9' { + return false + } + } + return true +} + +func isBedrockOpenAIMantleEndpoint(hostname string) bool { + hostname = strings.ToLower(hostname) + return strings.HasPrefix(hostname, "bedrock-mantle.") && strings.HasSuffix(hostname, ".api.aws") +} + +func isBedrockOpenAIRuntimeEndpoint(hostname string) bool { + hostname = strings.ToLower(hostname) + return strings.HasPrefix(hostname, "bedrock-runtime.") && + (strings.HasSuffix(hostname, ".amazonaws.com") || strings.HasSuffix(hostname, ".amazonaws.com.cn")) +} + +func defaultBedrockOpenAIBaseURL(region, modelName string) string { + if common.IsBedrockOpenAIRuntimeChatModel(modelName) { + return fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com", region) + } + return fmt.Sprintf("https://bedrock-mantle.%s.api.aws", region) +} + +func getBedrockOpenAIRequestURL(info *relaycommon.RelayInfo) (string, error) { + credentials, err := parseBedrockOpenAICredentials(info) + if err != nil { + return "", err + } + + baseURL := strings.TrimRight(info.ChannelBaseUrl, "/") + if baseURL == "" { + baseURL = defaultBedrockOpenAIBaseURL(credentials.region, info.UpstreamModelName) + } + parsedBaseURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("invalid AWS OpenAI base URL: %w", err) + } + if parsedBaseURL.Host == "" || (parsedBaseURL.Scheme != "http" && parsedBaseURL.Scheme != "https") { + return "", fmt.Errorf("invalid AWS OpenAI base URL: %s", baseURL) + } + + hostname := strings.ToLower(parsedBaseURL.Hostname()) + isMantleEndpoint := isBedrockOpenAIMantleEndpoint(hostname) + isRuntimeEndpoint := isBedrockOpenAIRuntimeEndpoint(hostname) + + if isRuntimeEndpoint && (info.RelayMode == relayconstant.RelayModeResponses || info.RelayMode == relayconstant.RelayModeResponsesCompact) { + return "", fmt.Errorf("AWS Bedrock Runtime does not support the Responses API; use a bedrock-mantle base URL") + } + if isRuntimeEndpoint && common.IsBedrockOpenAIResponsesModel(info.UpstreamModelName) { + return "", fmt.Errorf("model %s is only available through the AWS Bedrock Mantle Responses API", info.UpstreamModelName) + } + + if isMantleEndpoint { + // Accept both New API-style host URLs and the /v1 base URLs shown in + // AWS SDK examples. + baseURL = strings.TrimSuffix(baseURL, "/v1") + // GPT-5.4 and later frontier models use /openai/v1/responses, while + // gpt-oss models use the standard /v1 Chat Completions/Responses paths. + baseURL = strings.TrimSuffix(baseURL, "/openai") + if common.IsBedrockOpenAIResponsesModel(info.UpstreamModelName) { + baseURL += "/openai" + } + } else if isRuntimeEndpoint { + baseURL = strings.TrimSuffix(baseURL, "/v1") + } + + return relaycommon.GetFullRequestURL(baseURL, info.RequestURLPath, info.ChannelType), nil +} + +func getBedrockOpenAISigningService(req *http.Request, info *relaycommon.RelayInfo) string { + if req != nil && req.URL != nil { + if isBedrockOpenAIRuntimeEndpoint(req.URL.Hostname()) { + return "bedrock" + } + if isBedrockOpenAIMantleEndpoint(req.URL.Hostname()) { + return "bedrock-mantle" + } + } + if info != nil && common.IsBedrockOpenAIRuntimeChatModel(info.UpstreamModelName) { + return "bedrock" + } + return "bedrock-mantle" +} + +func hashAndRestoreBedrockOpenAIRequestBody(req *http.Request) (string, error) { + body := []byte{} + if req.Body != nil { + var err error + body, err = io.ReadAll(req.Body) + if err != nil { + return "", fmt.Errorf("read AWS OpenAI request body: %w", err) + } + _ = req.Body.Close() + req.Body = io.NopCloser(bytes.NewReader(body)) + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(body)), nil + } + req.ContentLength = int64(len(body)) + } + payloadHash := sha256.Sum256(body) + return hex.EncodeToString(payloadHash[:]), nil +} + +func (a *Adaptor) SignRequest(_ *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + if info == nil || info.ChannelType != constant.ChannelTypeAwsOpenAI { + return nil + } + if req == nil { + return fmt.Errorf("AWS OpenAI request is missing") + } + credentials, err := parseBedrockOpenAICredentials(info) + if err != nil { + return err + } + if credentials.keyType == dto.AwsKeyTypeApiKey { + return nil + } + payloadHash, err := hashAndRestoreBedrockOpenAIRequestBody(req) + if err != nil { + return err + } + return awsv4.NewSigner().SignHTTP( + req.Context(), + aws.Credentials{ + AccessKeyID: credentials.accessKeyID, + SecretAccessKey: credentials.secretAccessKey, + }, + req, + payloadHash, + getBedrockOpenAISigningService(req, info), + credentials.region, + time.Now().UTC(), + ) +} diff --git a/relay/channel/openai/bedrock_openai_test.go b/relay/channel/openai/bedrock_openai_test.go new file mode 100644 index 000000000000..57273b31eca6 --- /dev/null +++ b/relay/channel/openai/bedrock_openai_test.go @@ -0,0 +1,195 @@ +package openai + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestGetBedrockOpenAIRequestURL(t *testing.T) { + tests := []struct { + name string + baseURL string + model string + path string + relayMode int + key string + keyType dto.AwsKeyType + expectedURL string + wantErr bool + }{ + { + name: "frontier mantle base is derived from region", + model: "openai.gpt-5.6-sol", + path: "/v1/responses", + relayMode: relayconstant.RelayModeResponses, + key: "AKID|SECRET|us-east-2", + expectedURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses", + }, + { + name: "frontier mantle avoids duplicate segment", + baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1/", + model: "openai.gpt-5.4", + path: "/v1/responses", + relayMode: relayconstant.RelayModeResponses, + expectedURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses", + }, + { + name: "gpt oss mantle chat path", + baseURL: "https://bedrock-mantle.us-east-1.api.aws/openai", + model: "openai.gpt-oss-120b", + path: "/v1/chat/completions", + relayMode: relayconstant.RelayModeChatCompletions, + expectedURL: "https://bedrock-mantle.us-east-1.api.aws/v1/chat/completions", + }, + { + name: "gpt oss mantle responses path", + baseURL: "https://bedrock-mantle.us-west-2.api.aws", + model: "openai.gpt-oss-20b", + path: "/v1/responses", + relayMode: relayconstant.RelayModeResponses, + expectedURL: "https://bedrock-mantle.us-west-2.api.aws/v1/responses", + }, + { + name: "gpt oss runtime base is derived from region", + model: "openai.gpt-oss-20b-1:0", + path: "/v1/chat/completions", + relayMode: relayconstant.RelayModeChatCompletions, + key: "bedrock-api-key|us-west-2", + keyType: dto.AwsKeyTypeApiKey, + expectedURL: "https://bedrock-runtime.us-west-2.amazonaws.com/v1/chat/completions", + }, + { + name: "runtime rejects responses", + baseURL: "https://bedrock-runtime.us-east-1.amazonaws.com", + model: "openai.gpt-oss-120b-1:0", + path: "/v1/responses", + relayMode: relayconstant.RelayModeResponses, + wantErr: true, + }, + { + name: "custom proxy is not rewritten", + baseURL: "https://bedrock.example.com/provider", + model: "openai.gpt-5.5", + path: "/v1/responses", + relayMode: relayconstant.RelayModeResponses, + expectedURL: "https://bedrock.example.com/provider/v1/responses", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := tt.key + if key == "" { + key = "AKID|SECRET|us-east-1" + } + info := &relaycommon.RelayInfo{ + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeAwsOpenAI, + ChannelBaseUrl: tt.baseURL, + ApiKey: key, + ChannelOtherSettings: dto.ChannelOtherSettings{ + AwsKeyType: tt.keyType, + }, + UpstreamModelName: tt.model, + }, + RelayMode: tt.relayMode, + RequestURLPath: tt.path, + } + + requestURL, err := getBedrockOpenAIRequestURL(info) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.expectedURL, requestURL) + }) + } +} + +func TestParseBedrockOpenAICredentialsRejectsInvalidInput(t *testing.T) { + info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ + ApiKey: "AKID|SECRET|https://example.com", + }} + + _, err := parseBedrockOpenAICredentials(info) + require.ErrorContains(t, err, "invalid AWS region") +} + +func TestBedrockOpenAISetupRequestHeaderUsesOnlyBearerToken(t *testing.T) { + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + ctx.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil) + info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeAwsOpenAI, + ApiKey: "bedrock-api-key|us-east-1", + ChannelOtherSettings: dto.ChannelOtherSettings{ + AwsKeyType: dto.AwsKeyTypeApiKey, + }, + }} + header := http.Header{} + + err := (&Adaptor{}).SetupRequestHeader(ctx, &header, info) + require.NoError(t, err) + require.Equal(t, "Bearer bedrock-api-key", header.Get("Authorization")) +} + +func TestBedrockOpenAISignRequestUsesEndpointSpecificService(t *testing.T) { + tests := []struct { + name string + requestURL string + model string + expectedService string + }{ + { + name: "mantle", + requestURL: "https://bedrock-mantle.us-east-1.api.aws/openai/v1/responses", + model: "openai.gpt-5.5", + expectedService: "bedrock-mantle", + }, + { + name: "runtime", + requestURL: "https://bedrock-runtime.us-east-1.amazonaws.com/v1/chat/completions", + model: "openai.gpt-oss-20b-1:0", + expectedService: "bedrock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestBody := `{"model":"` + tt.model + `","input":"hello"}` + req := httptest.NewRequest(http.MethodPost, tt.requestURL, strings.NewReader(requestBody)) + req.Header.Set("Content-Type", "application/json") + info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeAwsOpenAI, + ApiKey: "AKID|SECRET|us-east-1", + UpstreamModelName: tt.model, + }} + + err := (&Adaptor{}).SignRequest(nil, req, info) + require.NoError(t, err) + require.Contains(t, req.Header.Get("Authorization"), "/us-east-1/"+tt.expectedService+"/aws4_request") + require.NotEmpty(t, req.Header.Get("X-Amz-Date")) + body, readErr := io.ReadAll(req.Body) + require.NoError(t, readErr) + require.Equal(t, requestBody, string(body)) + }) + } +} + +func TestBedrockOpenAIAdaptorMetadata(t *testing.T) { + adaptor := &Adaptor{ChannelType: constant.ChannelTypeAwsOpenAI} + require.Equal(t, BedrockOpenAIChannelName, adaptor.GetChannelName()) + require.Len(t, adaptor.GetModelList(), len(common.BedrockOpenAIResponsesModels)+len(common.BedrockOpenAIMantleChatModels)+len(common.BedrockOpenAIChatOnlyModels)+len(common.BedrockOpenAIRuntimeChatModels)) +} diff --git a/relay/claude_handler.go b/relay/claude_handler.go index e4a4920306f1..46803bbe1f01 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -132,9 +132,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ } } - if !model_setting.GetGlobalSettings().PassThroughRequestEnabled && - !info.ChannelSetting.PassThroughBodyEnabled && - service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) { + if service.ShouldChatCompletionsUseResponsesForRelay(info, model_setting.GetGlobalSettings().PassThroughRequestEnabled) { result, convErr := service.ConvertRequest(c, info, types.RelayFormatOpenAI, request) if convErr != nil { return types.NewError(convErr, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 9f460ce5c6a7..fffebc49667a 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -342,6 +342,7 @@ var streamSupportedChannels = map[int]bool{ constant.ChannelTypeMiniMax: true, constant.ChannelTypeSiliconFlow: true, constant.ChannelTypeAdvancedCustom: true, + constant.ChannelTypeAwsOpenAI: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index a68cfe730f60..b6dea35982ce 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -72,9 +72,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled if info.RelayMode == relayconstant.RelayModeChatCompletions && - !passThroughGlobal && - !info.ChannelSetting.PassThroughBodyEnabled && - service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) { + service.ShouldChatCompletionsUseResponsesForRelay(info, passThroughGlobal) { applySystemPromptIfNeeded(c, info, request) usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request) if newApiErr != nil { diff --git a/service/openai_chat_responses_mode.go b/service/openai_chat_responses_mode.go index 4db3727906cc..fdc818f9e7ef 100644 --- a/service/openai_chat_responses_mode.go +++ b/service/openai_chat_responses_mode.go @@ -1,6 +1,9 @@ package service import ( + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/service/relayconvert" "github.com/QuantumNous/new-api/setting/model_setting" ) @@ -12,3 +15,16 @@ func ShouldChatCompletionsUseResponsesPolicy(policy model_setting.ChatCompletion func ShouldChatCompletionsUseResponsesGlobal(channelID int, channelType int, model string) bool { return relayconvert.ShouldChatCompletionsUseResponsesGlobal(channelID, channelType, model) } + +func ShouldChatCompletionsUseResponsesForRelay(info *relaycommon.RelayInfo, passThroughGlobal bool) bool { + if info == nil { + return false + } + if info.ChannelType == constant.ChannelTypeAwsOpenAI && common.IsBedrockOpenAIResponsesModel(info.UpstreamModelName) { + return true + } + if passThroughGlobal || info.ChannelSetting.PassThroughBodyEnabled { + return false + } + return ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) +} diff --git a/service/openai_chat_responses_mode_test.go b/service/openai_chat_responses_mode_test.go new file mode 100644 index 000000000000..5ffc216da185 --- /dev/null +++ b/service/openai_chat_responses_mode_test.go @@ -0,0 +1,36 @@ +package service + +import ( + "testing" + + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/stretchr/testify/require" +) + +func TestShouldChatCompletionsUseResponsesForRelayRequiresBedrockFrontierConversion(t *testing.T) { + info := &relaycommon.RelayInfo{ + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeAwsOpenAI, + UpstreamModelName: "openai.gpt-5.6-terra", + ChannelSetting: dto.ChannelSettings{ + PassThroughBodyEnabled: true, + }, + }, + } + + require.True(t, ShouldChatCompletionsUseResponsesForRelay(info, true)) +} + +func TestShouldChatCompletionsUseResponsesForRelayDoesNotForceGptOss(t *testing.T) { + info := &relaycommon.RelayInfo{ + ChannelMeta: &relaycommon.ChannelMeta{ + ChannelType: constant.ChannelTypeAwsOpenAI, + UpstreamModelName: "openai.gpt-oss-120b", + }, + } + + require.False(t, ShouldChatCompletionsUseResponsesForRelay(info, true)) + require.False(t, ShouldChatCompletionsUseResponsesForRelay(nil, false)) +} diff --git a/setting/ratio_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go index 6e874b5bc5f9..58782c67c1d2 100644 --- a/setting/ratio_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -36,6 +36,11 @@ var defaultCacheRatio = map[string]float64{ "gpt-5-mini-2025-08-07": 0.1, "gpt-5-nano": 0.1, "gpt-5-nano-2025-08-07": 0.1, + "openai.gpt-5.6-sol": 0.1, + "openai.gpt-5.6-terra": 0.1, + "openai.gpt-5.6-luna": 0.1, + "openai.gpt-5.5": 0.1, + "openai.gpt-5.4": 0.1, "deepseek-chat": 0.25, "deepseek-reasoner": 0.25, "deepseek-coder": 0.25, @@ -84,6 +89,9 @@ var defaultCreateCacheRatio = map[string]float64{ "gpt-5.6-sol": 1.25, "gpt-5.6-terra": 1.25, "gpt-5.6-luna": 1.25, + "openai.gpt-5.6-sol": 1.25, + "openai.gpt-5.6-terra": 1.25, + "openai.gpt-5.6-luna": 1.25, "claude-3-sonnet-20240229": 1.25, "claude-3-opus-20240229": 1.25, "claude-3-haiku-20240307": 1.25, diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 829e0794a157..b5ef0febecf3 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -100,6 +100,17 @@ var defaultModelRatio = map[string]float64{ "gpt-5.6-sol": 2.5, "gpt-5.6-terra": 1.25, "gpt-5.6-luna": 0.5, + "openai.gpt-5.6-sol": 2.75, // AWS US in-region: $5.50 / 1M input tokens + "openai.gpt-5.6-terra": 1.375, // AWS US in-region: $2.75 / 1M input tokens + "openai.gpt-5.6-luna": 0.55, // AWS US in-region: $1.10 / 1M input tokens + "openai.gpt-5.5": 2.75, // AWS US in-region: $5.50 / 1M input tokens + "openai.gpt-5.4": 1.375, // AWS US in-region: $2.75 / 1M input tokens + "openai.gpt-oss-120b": 0.075, // AWS US standard: $0.15 / 1M input tokens + "openai.gpt-oss-20b": 0.035, // AWS US standard: $0.07 / 1M input tokens + "openai.gpt-oss-safeguard-120b": 0.075, // AWS US on-demand: $0.15 / 1M input tokens + "openai.gpt-oss-safeguard-20b": 0.035, // AWS US on-demand: $0.07 / 1M input tokens + "openai.gpt-oss-120b-1:0": 0.075, + "openai.gpt-oss-20b-1:0": 0.035, "gpt-3.5-turbo": 0.25, "gpt-3.5-turbo-0613": 0.75, "gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens @@ -326,10 +337,21 @@ var modelRatioMap = types.NewRWMap[string, float64]() var completionRatioMap = types.NewRWMap[string, float64]() var defaultCompletionRatio = map[string]float64{ - "gpt-4-gizmo-*": 2, - "gpt-4o-gizmo-*": 3, - "gpt-4-all": 2, - "gpt-image-1": 8, + "gpt-4-gizmo-*": 2, + "gpt-4o-gizmo-*": 3, + "gpt-4-all": 2, + "gpt-image-1": 8, + "openai.gpt-5.6-sol": 6, + "openai.gpt-5.6-terra": 6, + "openai.gpt-5.6-luna": 6, + "openai.gpt-5.5": 6, + "openai.gpt-5.4": 6, + "openai.gpt-oss-120b": 4, + "openai.gpt-oss-20b": 4.285714285714286, + "openai.gpt-oss-safeguard-120b": 4, + "openai.gpt-oss-safeguard-20b": 2.857142857142857, + "openai.gpt-oss-120b-1:0": 4, + "openai.gpt-oss-20b-1:0": 4.285714285714286, } // InitRatioSettings initializes all model related settings maps diff --git a/web/scripts/sync-i18n.mjs b/web/scripts/sync-i18n.mjs index 51dd38974dae..42b3ae406d72 100644 --- a/web/scripts/sync-i18n.mjs +++ b/web/scripts/sync-i18n.mjs @@ -37,6 +37,7 @@ const BRAND_AND_LITERAL_KEYS = new Set([ 'API URL', 'API2GPT', 'AccessKey / SecretAccessKey', + 'AWS OpenAI', 'AZURE_OPENAI_ENDPOINT *', 'Baidu V2', 'CC Switch', @@ -229,8 +230,9 @@ function isLikelyUntranslated({ locale, baseValue, value }) { if (locale === 'ru') return true // For fr/vi: still useful but noisier; keep it conservative. - if (locale === 'fr' || locale === 'vi') + if (locale === 'fr' || locale === 'vi') { return /\b(the|and|or|to|with|please)\b/i.test(s) + } return false } diff --git a/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx index fc872579af1a..6e62ce27f2fa 100644 --- a/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -2284,8 +2284,8 @@ export function ChannelMutateDrawer({ /> )} - {/* AWS (type 33) */} - {currentType === 33 && ( + {/* AWS (types 33 and 59) */} + {[33, 59].includes(currentType) && ( { @@ -391,10 +392,12 @@ export const TYPE_TO_KEY_PROMPT: Record = { 50: 'Format: AccessKey|SecretKey (or just ApiKey if upstream is New API)', 51: 'Format: Access Key ID|Secret Access Key', 57: 'Paste Codex OAuth JSON credential (access_token / refresh_token / account_id)', + 59: 'Format: AccessKey|SecretAccessKey|Region', } export const CHANNEL_TYPE_WARNINGS: Record = { 3: 'For channels added after May 10, 2025, no need to remove "." from model names during deployment', 8: 'If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing', 37: 'Dify channels only support chatflow and agent, and agent does not support images', + 59: 'The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.', } diff --git a/web/src/features/channels/lib/channel-form.ts b/web/src/features/channels/lib/channel-form.ts index e7e36827c307..847153b3d532 100644 --- a/web/src/features/channels/lib/channel-form.ts +++ b/web/src/features/channels/lib/channel-form.ts @@ -585,8 +585,8 @@ function buildSettingsJSON(formData: ChannelFormValues): string { delete settingsObj.openrouter_enterprise } - // Add aws_key_type for AWS channels (type 33) - if (formData.type === 33) { + // Add aws_key_type for AWS channels (types 33 and 59) + if (formData.type === 33 || formData.type === 59) { settingsObj.aws_key_type = formData.aws_key_type || 'ak_sk' } else if ('aws_key_type' in settingsObj) { delete settingsObj.aws_key_type diff --git a/web/src/features/channels/lib/channel-type-config.ts b/web/src/features/channels/lib/channel-type-config.ts index 56bf4bce61b8..4d2dc89d2033 100644 --- a/web/src/features/channels/lib/channel-type-config.ts +++ b/web/src/features/channels/lib/channel-type-config.ts @@ -144,6 +144,16 @@ export const CHANNEL_TYPE_CONFIGS: Record = { models: 'Models exposed by this channel', }, }, + 59: { + id: 59, + name: CHANNEL_TYPES[59], + icon: 'aws', + hints: { + baseUrl: 'Generated automatically from the credential Region', + key: 'AccessKey|SecretAccessKey|Region or APIKey|Region', + models: 'OpenAI model IDs exposed by Amazon Bedrock', + }, + }, } /** diff --git a/web/src/features/channels/lib/channel-utils.ts b/web/src/features/channels/lib/channel-utils.ts index dc143a66d97c..78285cca75f6 100644 --- a/web/src/features/channels/lib/channel-utils.ts +++ b/web/src/features/channels/lib/channel-utils.ts @@ -64,6 +64,7 @@ export function getChannelTypeIcon(type: number): string { // Cloud providers 33: 'Aws', // AWS + 59: 'Aws', // AWS OpenAI 39: 'Cloudflare', // Cloudflare // Chinese providers diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 609e72eb08c8..6b8883820e98 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -243,6 +243,8 @@ "Advanced": "Advanced", "Advanced Configuration": "Advanced Configuration", "Advanced Custom": "Advanced Custom", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.", "Advanced custom configuration is required": "Advanced custom configuration is required", "Advanced custom configuration requires at least one route": "Advanced custom configuration requires at least one route", "Advanced Custom Routes": "Advanced Custom Routes", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 443b3fa1b55f..51d27ba0f398 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -243,6 +243,8 @@ "Advanced": "Avancé", "Advanced Configuration": "Configuration avancée", "Advanced Custom": "Personnalisé avancé", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "Le point de terminaison AWS est généré à partir de Region. Le mode AK/SK utilise AccessKey|SecretAccessKey|Region ; le mode API Key utilise APIKey|Region.", "Advanced custom configuration is required": "La configuration personnalisée avancée est requise", "Advanced custom configuration requires at least one route": "La configuration personnalisée avancée nécessite au moins une route", "Advanced Custom Routes": "Routes personnalisées avancées", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index c15de5534b82..93e17fc0fbd7 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -243,6 +243,8 @@ "Advanced": "高度な設定", "Advanced Configuration": "詳細設定", "Advanced Custom": "高度なカスタム", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "AWS エンドポイントは Region から自動生成されます。AK/SK モードは AccessKey|SecretAccessKey|Region、API Key モードは APIKey|Region を使用します。", "Advanced custom configuration is required": "高度なカスタム設定が必要です", "Advanced custom configuration requires at least one route": "高度なカスタム設定には少なくとも1つのルートが必要です", "Advanced Custom Routes": "高度なカスタムルート", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index a1c18a35657e..d1314d920ebb 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -243,6 +243,8 @@ "Advanced": "Расширенные", "Advanced Configuration": "Расширенная конфигурация", "Advanced Custom": "Расширенный пользовательский", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "Конечная точка AWS создаётся из Region. Режим AK/SK использует AccessKey|SecretAccessKey|Region; режим API Key использует APIKey|Region.", "Advanced custom configuration is required": "Требуется расширенная пользовательская конфигурация", "Advanced custom configuration requires at least one route": "Расширенной пользовательской конфигурации нужен хотя бы один маршрут", "Advanced Custom Routes": "Расширенные пользовательские маршруты", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 19cdddb1621d..1fdc398f134a 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -243,6 +243,8 @@ "Advanced": "Nâng cao", "Advanced Configuration": "Cấu hình nâng cao", "Advanced Custom": "Tùy chỉnh nâng cao", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "Điểm cuối AWS được tạo từ Region. Chế độ AK/SK dùng AccessKey|SecretAccessKey|Region; chế độ API Key dùng APIKey|Region.", "Advanced custom configuration is required": "Bắt buộc cấu hình tùy chỉnh nâng cao", "Advanced custom configuration requires at least one route": "Cấu hình tùy chỉnh nâng cao cần ít nhất một route", "Advanced Custom Routes": "Route tùy chỉnh nâng cao", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index d895a2b3d3fa..7825c822cdb0 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -243,6 +243,8 @@ "Advanced": "進階", "Advanced Configuration": "進階設定", "Advanced Custom": "進階自訂", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "AWS 端點將根據 Region 自動產生。AK/SK 模式使用 AccessKey|SecretAccessKey|Region;API Key 模式使用 APIKey|Region。", "Advanced custom configuration is required": "必須設定進階自訂", "Advanced custom configuration requires at least one route": "進階自訂至少需要一個路由", "Advanced Custom Routes": "進階自訂路由", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index ee80b960bd2c..e988a9a961a3 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -243,6 +243,8 @@ "Advanced": "高级", "Advanced Configuration": "高级配置", "Advanced Custom": "高级自定义", + "AWS OpenAI": "AWS OpenAI", + "The AWS endpoint is generated from Region. AK/SK mode uses AccessKey|SecretAccessKey|Region; API Key mode uses APIKey|Region.": "AWS 端点将根据 Region 自动生成。AK/SK 模式使用 AccessKey|SecretAccessKey|Region;API Key 模式使用 APIKey|Region。", "Advanced custom configuration is required": "必须配置高级自定义", "Advanced custom configuration requires at least one route": "高级自定义至少需要一个路由", "Advanced Custom Routes": "高级自定义路由",