Commit 3f2c0aed by Seefs Committed by GitHub

feat: advanced custom channel (#5590)

parent 21d4d18d
...@@ -75,6 +75,8 @@ func ChannelType2APIType(channelType int) (int, bool) { ...@@ -75,6 +75,8 @@ 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.ChannelTypeAdvancedCustom:
apiType = constant.APITypeAdvancedCustom
} }
if apiType == -1 { if apiType == -1 {
return constant.APITypeOpenAI, false return constant.APITypeOpenAI, false
......
...@@ -36,5 +36,6 @@ const ( ...@@ -36,5 +36,6 @@ const (
APITypeMiniMax APITypeMiniMax
APITypeReplicate APITypeReplicate
APITypeCodex APITypeCodex
APITypeAdvancedCustom
APITypeDummy // this one is only for count, do not add any channel after this APITypeDummy // this one is only for count, do not add any channel after this
) )
...@@ -55,6 +55,7 @@ const ( ...@@ -55,6 +55,7 @@ const (
ChannelTypeSora = 55 ChannelTypeSora = 55
ChannelTypeReplicate = 56 ChannelTypeReplicate = 56
ChannelTypeCodex = 57 ChannelTypeCodex = 57
ChannelTypeAdvancedCustom = 58
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 +119,7 @@ var ChannelBaseURLs = []string{ ...@@ -118,6 +119,7 @@ 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
} }
var ChannelTypeNames = map[int]string{ var ChannelTypeNames = map[int]string{
...@@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{ ...@@ -175,6 +177,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeSora: "Sora", ChannelTypeSora: "Sora",
ChannelTypeReplicate: "Replicate", ChannelTypeReplicate: "Replicate",
ChannelTypeCodex: "ChatGPT Subscription (Codex)", ChannelTypeCodex: "ChatGPT Subscription (Codex)",
ChannelTypeAdvancedCustom: "Advanced Custom",
} }
func GetChannelTypeName(channelType int) string { func GetChannelTypeName(channelType int) string {
......
package dto package dto
import (
"fmt"
"net/url"
"strings"
)
type ChannelSettings struct { type ChannelSettings struct {
ForceFormat bool `json:"force_format,omitempty"` ForceFormat bool `json:"force_format,omitempty"`
ThinkingToContent bool `json:"thinking_to_content,omitempty"` ThinkingToContent bool `json:"thinking_to_content,omitempty"`
...@@ -41,6 +47,7 @@ type ChannelOtherSettings struct { ...@@ -41,6 +47,7 @@ type ChannelOtherSettings struct {
UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型 UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型
UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型 UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型
UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型 UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型
AdvancedCustom *AdvancedCustomConfig `json:"advanced_custom,omitempty"`
} }
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool { func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
...@@ -49,3 +56,168 @@ func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool { ...@@ -49,3 +56,168 @@ func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
} }
return *s.OpenRouterEnterprise return *s.OpenRouterEnterprise
} }
const (
AdvancedCustomConverterNone = "none"
AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions = "anthropic_messages_to_openai_chat_completions"
AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages = "openai_chat_completions_to_anthropic_messages"
AdvancedCustomConverterOpenAIChatCompletionsToOpenAIResponses = "openai_chat_completions_to_openai_responses"
AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions = "gemini_generate_content_to_openai_chat_completions"
AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent = "openai_chat_completions_to_gemini_generate_content"
)
const (
AdvancedCustomAuthTypeNone = "none"
AdvancedCustomAuthTypeHeader = "header"
AdvancedCustomAuthTypeQuery = "query"
)
type AdvancedCustomConfig struct {
Routes []AdvancedCustomRoute `json:"advanced_routes,omitempty"`
Fallback AdvancedCustomFallback `json:"advanced_fallback,omitempty"`
}
type AdvancedCustomRoute struct {
IncomingPath string `json:"incoming_path,omitempty"`
UpstreamPath string `json:"upstream_path,omitempty"`
Converter string `json:"converter,omitempty"`
Auth *AdvancedCustomRouteAuth `json:"auth,omitempty"`
}
type AdvancedCustomFallback struct {
Enabled bool `json:"enabled,omitempty"`
}
type AdvancedCustomRouteAuth struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}
func IsAdvancedCustomConverterAllowed(converter string) bool {
switch converter {
case AdvancedCustomConverterNone,
AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions,
AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages,
AdvancedCustomConverterOpenAIChatCompletionsToOpenAIResponses,
AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions,
AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent:
return true
default:
return false
}
}
func (c *AdvancedCustomConfig) Validate() error {
if c == nil {
return fmt.Errorf("advanced_custom is required")
}
if len(c.Routes) == 0 && !c.Fallback.Enabled {
return fmt.Errorf("advanced_custom requires at least one route or enabled fallback")
}
seenPaths := make(map[string]struct{}, len(c.Routes))
for i := range c.Routes {
route := c.Routes[i]
route.IncomingPath = strings.TrimSpace(route.IncomingPath)
upstreamPath := strings.TrimSpace(route.UpstreamPath)
route.Converter = strings.TrimSpace(route.Converter)
if route.Converter == "" {
route.Converter = AdvancedCustomConverterNone
}
if route.IncomingPath == "" {
return fmt.Errorf("advanced_custom.advanced_routes[%d].incoming_path is required", i)
}
if !strings.HasPrefix(route.IncomingPath, "/") {
return fmt.Errorf("advanced_custom.advanced_routes[%d].incoming_path must start with /", i)
}
if strings.Contains(route.IncomingPath, "?") {
return fmt.Errorf("advanced_custom.advanced_routes[%d].incoming_path must not include query", i)
}
if _, exists := seenPaths[route.IncomingPath]; exists {
return fmt.Errorf("advanced_custom.advanced_routes[%d].incoming_path must be unique: %s", i, route.IncomingPath)
}
seenPaths[route.IncomingPath] = struct{}{}
if upstreamPath == "" {
return fmt.Errorf("advanced_custom.advanced_routes[%d].upstream_path is required", i)
}
if err := validateAdvancedCustomUpstreamTarget(i, upstreamPath); err != nil {
return err
}
if !IsAdvancedCustomConverterAllowed(route.Converter) {
return fmt.Errorf("advanced_custom.advanced_routes[%d].converter is not registered: %s", i, route.Converter)
}
if err := validateAdvancedCustomConverterPath(i, route.IncomingPath, route.Converter); err != nil {
return err
}
if err := validateAdvancedCustomRouteAuth(i, route.Auth); err != nil {
return err
}
}
return nil
}
func validateAdvancedCustomUpstreamTarget(index int, upstreamPath string) error {
if strings.HasPrefix(upstreamPath, "/") {
if strings.HasPrefix(upstreamPath, "//") {
return fmt.Errorf("advanced_custom.advanced_routes[%d].upstream_path must be a full URL or a path starting with /", index)
}
return nil
}
parsedURL, err := url.Parse(upstreamPath)
if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
return fmt.Errorf("advanced_custom.advanced_routes[%d].upstream_path must be a full URL or a path starting with /", index)
}
if !strings.EqualFold(parsedURL.Scheme, "http") && !strings.EqualFold(parsedURL.Scheme, "https") {
return fmt.Errorf("advanced_custom.advanced_routes[%d].upstream_path must use http or https", index)
}
return nil
}
func validateAdvancedCustomConverterPath(index int, incomingPath string, converter string) error {
switch converter {
case AdvancedCustomConverterNone:
return nil
case AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions:
if incomingPath == "/v1/messages" {
return nil
}
case AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages,
AdvancedCustomConverterOpenAIChatCompletionsToOpenAIResponses,
AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent:
if incomingPath == "/v1/chat/completions" {
return nil
}
case AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions:
if strings.Contains(incomingPath, ":generateContent") || strings.Contains(incomingPath, ":streamGenerateContent") {
return nil
}
}
return fmt.Errorf("advanced_custom.advanced_routes[%d].converter does not match incoming_path: %s", index, converter)
}
func validateAdvancedCustomRouteAuth(index int, auth *AdvancedCustomRouteAuth) error {
if auth == nil {
return nil
}
authType := strings.TrimSpace(auth.Type)
switch authType {
case AdvancedCustomAuthTypeNone:
return nil
case AdvancedCustomAuthTypeHeader, AdvancedCustomAuthTypeQuery:
if strings.TrimSpace(auth.Name) == "" {
return fmt.Errorf("advanced_custom.advanced_routes[%d].auth.name is required", index)
}
if strings.TrimSpace(auth.Value) == "" {
return fmt.Errorf("advanced_custom.advanced_routes[%d].auth.value is required", index)
}
return nil
default:
return fmt.Errorf("advanced_custom.advanced_routes[%d].auth.type is invalid: %s", index, auth.Type)
}
}
...@@ -945,6 +945,26 @@ func (channel *Channel) ValidateSettings() error { ...@@ -945,6 +945,26 @@ func (channel *Channel) ValidateSettings() error {
return err return err
} }
} }
channelOtherSettings := &dto.ChannelOtherSettings{}
if channel.OtherSettings != "" {
err := common.UnmarshalJsonStr(channel.OtherSettings, channelOtherSettings)
if err != nil {
return err
}
}
if channel.Type == constant.ChannelTypeAdvancedCustom {
if channelOtherSettings.AdvancedCustom == nil {
return fmt.Errorf("advanced_custom is required")
}
if channelOtherSettings.AdvancedCustom.Fallback.Enabled && (channel.BaseURL == nil || strings.TrimSpace(*channel.BaseURL) == "") {
return fmt.Errorf("base_url is required when advanced_custom advanced_fallback is enabled")
}
}
if channelOtherSettings.AdvancedCustom != nil {
if err := channelOtherSettings.AdvancedCustom.Validate(); err != nil {
return err
}
}
return nil return nil
} }
......
package advancedcustom
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/claude"
"github.com/QuantumNous/new-api/relay/channel/gemini"
"github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
)
const ChannelName = "advanced_custom"
const advancedCustomModelPlaceholder = "{model}"
type Adaptor struct {
openaiAdaptor openai.Adaptor
claudeAdaptor claude.Adaptor
geminiAdaptor gemini.Adaptor
resolved bool
fallback bool
converted bool
route dto.AdvancedCustomRoute
converter string
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
a.openaiAdaptor.Init(info)
a.claudeAdaptor.Init(info)
a.geminiAdaptor.Init(info)
}
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if a.fallback || converter == dto.AdvancedCustomConverterNone {
return a.convertOpenAICompatibleRequest(c, info, request)
}
switch converter {
case dto.AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages:
return a.claudeAdaptor.ConvertOpenAIRequest(c, info, request)
case dto.AdvancedCustomConverterOpenAIChatCompletionsToOpenAIResponses:
if request == nil {
return nil, errors.New("request is nil")
}
return service.ChatCompletionsRequestToResponsesRequest(request)
case dto.AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent:
return a.geminiAdaptor.ConvertOpenAIRequest(c, info, request)
default:
return nil, fmt.Errorf("converter %q does not support OpenAI chat completions requests", converter)
}
}
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if a.fallback {
return a.convertClaudeToOpenAICompatibleRequest(c, info, request)
}
switch converter {
case dto.AdvancedCustomConverterNone:
return a.claudeAdaptor.ConvertClaudeRequest(c, info, request)
case dto.AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions:
return a.convertClaudeToOpenAICompatibleRequest(c, info, request)
default:
return nil, fmt.Errorf("converter %q does not support Anthropic Messages requests", converter)
}
}
func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if a.fallback {
return a.convertGeminiToOpenAICompatibleRequest(c, info, request)
}
switch converter {
case dto.AdvancedCustomConverterNone:
return a.geminiAdaptor.ConvertGeminiRequest(c, info, request)
case dto.AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions:
return a.convertGeminiToOpenAICompatibleRequest(c, info, request)
default:
return nil, fmt.Errorf("converter %q does not support Gemini generateContent requests", converter)
}
}
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if converter != dto.AdvancedCustomConverterNone {
return nil, fmt.Errorf("converter %q does not support OpenAI Responses requests", converter)
}
return a.convertOpenAICompatibleResponsesRequest(c, info, request)
}
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if converter != dto.AdvancedCustomConverterNone {
return nil, fmt.Errorf("converter %q does not support embedding requests", converter)
}
return a.convertOpenAICompatibleEmbeddingRequest(c, info, request)
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if converter != dto.AdvancedCustomConverterNone {
return nil, fmt.Errorf("converter %q does not support audio requests", converter)
}
return a.convertOpenAICompatibleAudioRequest(c, info, request)
}
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
converter, err := a.resolveForConversion(c, info)
if err != nil {
return nil, err
}
if converter != dto.AdvancedCustomConverterNone {
return nil, fmt.Errorf("converter %q does not support image requests", converter)
}
return a.convertOpenAICompatibleImageRequest(c, info, request)
}
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
a.converted = true
return a.openaiAdaptor.ConvertRerankRequest(c, relayMode, request)
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if err := a.resolve(nil, info); err != nil {
return "", err
}
if a.fallback {
return a.withTemporaryChannelType(info, constant.ChannelTypeOpenAI, func() (string, error) {
return a.openaiAdaptor.GetRequestURL(info)
})
}
return a.routeURL(info)
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error {
if err := a.resolve(c, info); err != nil {
return err
}
if a.fallback {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
err := a.openaiAdaptor.SetupRequestHeader(c, header, info)
info.ChannelType = old
return err
}
channel.SetupApiRequestHeader(info, c, header)
auth := a.route.Auth
if auth == nil {
header.Set("Authorization", "Bearer "+info.ApiKey)
} else {
switch strings.TrimSpace(auth.Type) {
case dto.AdvancedCustomAuthTypeNone:
case dto.AdvancedCustomAuthTypeHeader:
header.Set(strings.TrimSpace(auth.Name), applyAuthTemplate(auth.Value, info.ApiKey))
case dto.AdvancedCustomAuthTypeQuery:
default:
return fmt.Errorf("invalid advanced custom auth type: %s", auth.Type)
}
}
if shouldApplyClaudeHeaders(a.converter, info) {
applyClaudeHeaders(c, header, info)
}
return nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
if err := a.resolve(c, info); err != nil {
return nil, err
}
if !a.converted && (a.fallback || a.converter != dto.AdvancedCustomConverterNone) {
return nil, errors.New("advanced custom converter routes cannot be used with pass-through request body")
}
if info.RelayMode == relayconstant.RelayModeAudioTranscription ||
info.RelayMode == relayconstant.RelayModeAudioTranslation ||
(info.RelayMode == relayconstant.RelayModeImagesEdits && !isJSONRequest(c)) {
return channel.DoFormRequest(a, c, info, requestBody)
}
if info.RelayMode == relayconstant.RelayModeRealtime {
return channel.DoWssRequest(a, c, info, requestBody)
}
return channel.DoApiRequest(a, c, info, requestBody)
}
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
if err := a.resolve(c, info); err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
if a.fallback {
return a.openaiAdaptor.DoResponse(c, resp, info)
}
switch a.converter {
case dto.AdvancedCustomConverterNone:
return a.doNativeResponse(c, resp, info)
case dto.AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions,
dto.AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions:
return a.openaiAdaptor.DoResponse(c, resp, info)
case dto.AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages:
return a.claudeAdaptor.DoResponse(c, resp, info)
case dto.AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent:
return a.geminiAdaptor.DoResponse(c, resp, info)
case dto.AdvancedCustomConverterOpenAIChatCompletionsToOpenAIResponses:
if info.IsStream {
return openai.OaiResponsesToChatStreamHandler(c, info, resp)
}
return openai.OaiResponsesToChatHandler(c, info, resp)
default:
return nil, types.NewOpenAIError(fmt.Errorf("unsupported advanced custom converter: %s", a.converter), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
}
func (a *Adaptor) GetModelList() []string {
models := make([]string, 0, len(openai.ModelList)+len(claude.ModelList)+len(gemini.ModelList))
models = append(models, openai.ModelList...)
models = append(models, claude.ModelList...)
models = append(models, gemini.ModelList...)
return lo.Uniq(models)
}
func (a *Adaptor) GetChannelName() string {
return ChannelName
}
func (a *Adaptor) doNativeResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (any, *types.NewAPIError) {
switch info.RelayFormat {
case types.RelayFormatClaude:
return a.claudeAdaptor.DoResponse(c, resp, info)
case types.RelayFormatGemini:
return a.geminiAdaptor.DoResponse(c, resp, info)
default:
return a.openaiAdaptor.DoResponse(c, resp, info)
}
}
func (a *Adaptor) resolveForConversion(c *gin.Context, info *relaycommon.RelayInfo) (string, error) {
if err := a.resolve(c, info); err != nil {
return "", err
}
a.converted = true
return a.converter, nil
}
func (a *Adaptor) resolve(c *gin.Context, info *relaycommon.RelayInfo) error {
if a.resolved {
return nil
}
if info == nil {
return errors.New("missing relay info")
}
config := info.ChannelOtherSettings.AdvancedCustom
if config == nil {
return errors.New("advanced_custom is required")
}
if err := config.Validate(); err != nil {
return err
}
incomingPath := incomingRequestPath(c, info)
route, ok := lo.Find(config.Routes, func(route dto.AdvancedCustomRoute) bool {
return matchIncomingPath(strings.TrimSpace(route.IncomingPath), incomingPath)
})
if ok {
route.Converter = strings.TrimSpace(route.Converter)
if route.Converter == "" {
route.Converter = dto.AdvancedCustomConverterNone
}
a.route = route
a.converter = route.Converter
a.resolved = true
return nil
}
if config.Fallback.Enabled {
a.fallback = true
a.converter = dto.AdvancedCustomConverterNone
a.resolved = true
return nil
}
return fmt.Errorf("advanced custom route not found for path: %s", incomingPath)
}
func incomingRequestPath(c *gin.Context, info *relaycommon.RelayInfo) string {
if c != nil && c.Request != nil && c.Request.URL != nil {
return c.Request.URL.Path
}
if info == nil {
return ""
}
return strings.Split(info.RequestURLPath, "?")[0]
}
func matchIncomingPath(configuredPath string, requestPath string) bool {
if matchIncomingPathTemplate(configuredPath, requestPath) {
return true
}
if strings.Contains(configuredPath, ":generateContent") {
streamPath := strings.Replace(configuredPath, ":generateContent", ":streamGenerateContent", 1)
return matchIncomingPathTemplate(streamPath, requestPath)
}
return false
}
func matchIncomingPathTemplate(configuredPath string, requestPath string) bool {
if !strings.Contains(configuredPath, advancedCustomModelPlaceholder) {
return configuredPath == requestPath
}
parts := strings.Split(configuredPath, advancedCustomModelPlaceholder)
if len(parts) != 2 {
return false
}
if !strings.HasPrefix(requestPath, parts[0]) || !strings.HasSuffix(requestPath, parts[1]) {
return false
}
model := strings.TrimSuffix(strings.TrimPrefix(requestPath, parts[0]), parts[1])
return model != "" && !strings.Contains(model, "/")
}
func (a *Adaptor) routeURL(info *relaycommon.RelayInfo) (string, error) {
parsedURL, err := resolveUpstreamTargetURL(applyUpstreamPathTemplate(strings.TrimSpace(a.route.UpstreamPath), info), info)
if err != nil {
return "", err
}
if shouldUseGeminiStreamURL(a.converter, info) {
useGeminiStreamGenerateContentURL(parsedURL)
}
if info != nil && info.RelayMode == relayconstant.RelayModeRealtime {
switch parsedURL.Scheme {
case "https":
parsedURL.Scheme = "wss"
case "http":
parsedURL.Scheme = "ws"
}
}
if a.route.Auth != nil && strings.TrimSpace(a.route.Auth.Type) == dto.AdvancedCustomAuthTypeQuery {
query := parsedURL.Query()
query.Set(strings.TrimSpace(a.route.Auth.Name), applyAuthTemplate(a.route.Auth.Value, info.ApiKey))
parsedURL.RawQuery = query.Encode()
}
return parsedURL.String(), nil
}
func resolveUpstreamTargetURL(upstreamPath string, info *relaycommon.RelayInfo) (*url.URL, error) {
if strings.HasPrefix(upstreamPath, "/") {
if strings.HasPrefix(upstreamPath, "//") {
return nil, errors.New("advanced custom upstream path must be a full URL or a path starting with /")
}
if info == nil || strings.TrimSpace(info.ChannelBaseUrl) == "" {
return nil, errors.New("channel base URL is required when advanced custom upstream path is relative")
}
return joinBaseURLAndUpstreamPath(info.ChannelBaseUrl, upstreamPath)
}
parsedURL, err := url.Parse(upstreamPath)
if err != nil {
return nil, err
}
if parsedURL.Scheme == "" || parsedURL.Host == "" {
return nil, errors.New("advanced custom upstream path must be a full URL or a path starting with /")
}
if !strings.EqualFold(parsedURL.Scheme, "http") && !strings.EqualFold(parsedURL.Scheme, "https") {
return nil, errors.New("advanced custom upstream path must use http or https")
}
return parsedURL, nil
}
func joinBaseURLAndUpstreamPath(baseURL string, upstreamPath string) (*url.URL, error) {
parsedBaseURL, err := url.Parse(strings.TrimSpace(baseURL))
if err != nil {
return nil, err
}
if parsedBaseURL.Scheme == "" || parsedBaseURL.Host == "" {
return nil, errors.New("channel base URL must be a full URL when advanced custom upstream path is relative")
}
if !strings.EqualFold(parsedBaseURL.Scheme, "http") && !strings.EqualFold(parsedBaseURL.Scheme, "https") {
return nil, errors.New("channel base URL must use http or https when advanced custom upstream path is relative")
}
parsedPath, err := url.Parse(upstreamPath)
if err != nil {
return nil, err
}
parsedBaseURL.Path = strings.TrimRight(parsedBaseURL.Path, "/") + "/" + strings.TrimLeft(parsedPath.Path, "/")
parsedBaseURL.RawPath = ""
parsedBaseURL.RawQuery = parsedPath.RawQuery
parsedBaseURL.Fragment = parsedPath.Fragment
return parsedBaseURL, nil
}
func applyUpstreamPathTemplate(upstreamPath string, info *relaycommon.RelayInfo) string {
if info == nil {
return upstreamPath
}
return strings.ReplaceAll(upstreamPath, advancedCustomModelPlaceholder, info.UpstreamModelName)
}
func shouldUseGeminiStreamURL(converter string, info *relaycommon.RelayInfo) bool {
return info != nil &&
info.IsStream &&
converter == dto.AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent
}
func useGeminiStreamGenerateContentURL(parsedURL *url.URL) {
if strings.Contains(parsedURL.Path, ":generateContent") {
parsedURL.Path = strings.Replace(parsedURL.Path, ":generateContent", ":streamGenerateContent", 1)
}
if strings.Contains(parsedURL.Path, ":streamGenerateContent") {
query := parsedURL.Query()
query.Set("alt", "sse")
parsedURL.RawQuery = query.Encode()
}
}
func shouldApplyClaudeHeaders(converter string, info *relaycommon.RelayInfo) bool {
return converter == dto.AdvancedCustomConverterOpenAIChatCompletionsToAnthropicMessages ||
(converter == dto.AdvancedCustomConverterNone && info != nil && info.RelayFormat == types.RelayFormatClaude)
}
func applyClaudeHeaders(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) {
anthropicVersion := ""
if c != nil && c.Request != nil {
anthropicVersion = c.Request.Header.Get("anthropic-version")
}
if anthropicVersion == "" {
anthropicVersion = "2023-06-01"
}
header.Set("anthropic-version", anthropicVersion)
if c != nil {
claude.CommonClaudeHeadersOperation(c, header, info)
}
}
func applyAuthTemplate(template string, apiKey string) string {
return strings.ReplaceAll(template, "{api_key}", apiKey)
}
func isJSONRequest(c *gin.Context) bool {
if c == nil || c.Request == nil {
return false
}
return strings.Contains(strings.ToLower(c.Request.Header.Get("Content-Type")), "application/json")
}
func (a *Adaptor) convertOpenAICompatibleRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertOpenAIRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertClaudeToOpenAICompatibleRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertClaudeRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertGeminiToOpenAICompatibleRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertGeminiRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertOpenAICompatibleResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertOpenAIResponsesRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertOpenAICompatibleEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertEmbeddingRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertOpenAICompatibleAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertAudioRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) convertOpenAICompatibleImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
converted, err := a.openaiAdaptor.ConvertImageRequest(c, info, request)
info.ChannelType = old
return converted, err
}
func (a *Adaptor) withTemporaryChannelType(info *relaycommon.RelayInfo, channelType int, fn func() (string, error)) (string, error) {
old := info.ChannelType
info.ChannelType = channelType
value, err := fn()
info.ChannelType = old
return value, err
}
package advancedcustom
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"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/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdaptorUsesExactRouteAndQueryAuth(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/messages",
UpstreamPath: "https://upstream.example/v1/chat/completions?existing=1",
Converter: dto.AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeQuery,
Name: "api_key",
Value: "{api_key}",
},
},
},
})
info.RequestURLPath = "/v1/messages?client=1"
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "upstream.example", parsedURL.Host)
assert.Equal(t, "/v1/chat/completions", parsedURL.Path)
assert.Equal(t, "1", parsedURL.Query().Get("existing"))
assert.Equal(t, "sk-test", parsedURL.Query().Get("api_key"))
}
func TestAdaptorJoinsUpstreamPathWithChannelBaseURL(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "/proxy/v1/chat/completions?existing=1",
Converter: dto.AdvancedCustomConverterNone,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeQuery,
Name: "api_key",
Value: "{api_key}",
},
},
},
})
info.ChannelBaseUrl = "https://gateway.example/base"
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "gateway.example", parsedURL.Host)
assert.Equal(t, "/base/proxy/v1/chat/completions", parsedURL.Path)
assert.Equal(t, "1", parsedURL.Query().Get("existing"))
assert.Equal(t, "sk-test", parsedURL.Query().Get("api_key"))
}
func TestAdaptorReturnsErrorWhenUpstreamPathNeedsMissingBaseURL(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "/v1/chat/completions",
Converter: dto.AdvancedCustomConverterNone,
},
},
})
info.ChannelBaseUrl = ""
_, err := adaptor.GetRequestURL(info)
require.Error(t, err)
assert.Contains(t, err.Error(), "base URL is required")
}
func TestAdaptorSetupRequestHeaderUsesDefaultBearerAuth(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "https://upstream.example/v1/chat/completions",
Converter: dto.AdvancedCustomConverterNone,
},
},
})
c := advancedCustomGinContext("/v1/chat/completions")
header := http.Header{}
require.NoError(t, adaptor.SetupRequestHeader(c, &header, info))
assert.Equal(t, "Bearer sk-test", header.Get("Authorization"))
}
func TestAdaptorSetupRequestHeaderUsesConfiguredHeaderAuth(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "https://upstream.example/v1/chat/completions",
Converter: dto.AdvancedCustomConverterNone,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeHeader,
Name: "x-api-key",
Value: "{api_key}",
},
},
},
})
c := advancedCustomGinContext("/v1/chat/completions")
header := http.Header{}
require.NoError(t, adaptor.SetupRequestHeader(c, &header, info))
assert.Empty(t, header.Get("Authorization"))
assert.Equal(t, "sk-test", header.Get("x-api-key"))
}
func TestAdaptorSetupRequestHeaderAddsClaudeDefaultHeaders(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/messages",
UpstreamPath: "https://api.anthropic.com/v1/messages",
Converter: dto.AdvancedCustomConverterNone,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeHeader,
Name: "x-api-key",
Value: "{api_key}",
},
},
},
})
info.RelayFormat = types.RelayFormatClaude
c := advancedCustomGinContext("/v1/messages")
header := http.Header{}
require.NoError(t, adaptor.SetupRequestHeader(c, &header, info))
assert.Equal(t, "sk-test", header.Get("x-api-key"))
assert.Equal(t, "2023-06-01", header.Get("anthropic-version"))
}
func TestAdaptorReturnsErrorWhenNoRouteAndFallbackDisabled(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/messages",
UpstreamPath: "https://upstream.example/v1/chat/completions",
Converter: dto.AdvancedCustomConverterAnthropicMessagesToOpenAIChatCompletions,
},
},
})
info.RequestURLPath = "/v1/chat/completions"
_, err := adaptor.GetRequestURL(info)
require.Error(t, err)
assert.Contains(t, err.Error(), "route not found")
}
func TestAdaptorFallbackUsesOpenAICompatibleBaseURL(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Fallback: dto.AdvancedCustomFallback{Enabled: true},
})
info.RequestURLPath = "/v1/messages"
info.RelayFormat = types.RelayFormatClaude
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
assert.Equal(t, "https://fallback.example/v1/chat/completions", requestURL)
}
func TestAdaptorReplacesModelPlaceholderInRouteURL(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent",
Converter: dto.AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeQuery,
Name: "key",
Value: "{api_key}",
},
},
},
})
info.UpstreamModelName = "gemini-2.5-flash"
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, "/v1beta/models/gemini-2.5-flash:generateContent", parsedURL.Path)
assert.Equal(t, "sk-test", parsedURL.Query().Get("key"))
assert.Empty(t, parsedURL.Query().Get("alt"))
}
func TestAdaptorSwitchesGeminiGenerateContentURLForStream(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1/chat/completions",
UpstreamPath: "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?existing=1",
Converter: dto.AdvancedCustomConverterOpenAIChatCompletionsToGeminiGenerateContent,
Auth: &dto.AdvancedCustomRouteAuth{
Type: dto.AdvancedCustomAuthTypeQuery,
Name: "key",
Value: "{api_key}",
},
},
},
})
info.UpstreamModelName = "gemini-2.5-pro"
info.IsStream = true
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, "/v1beta/models/gemini-2.5-pro:streamGenerateContent", parsedURL.Path)
assert.Equal(t, "sse", parsedURL.Query().Get("alt"))
assert.Equal(t, "1", parsedURL.Query().Get("existing"))
assert.Equal(t, "sk-test", parsedURL.Query().Get("key"))
}
func TestAdaptorMatchesGeminiIncomingPathTemplate(t *testing.T) {
tests := []struct {
name string
requestURLPath string
wantRequestPath string
}{
{
name: "generate content",
requestURLPath: "/v1beta/models/gemini-2.5-flash:generateContent",
wantRequestPath: "/v1/chat/completions",
},
{
name: "stream generate content",
requestURLPath: "/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse",
wantRequestPath: "/v1/chat/completions",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
Routes: []dto.AdvancedCustomRoute{
{
IncomingPath: "/v1beta/models/{model}:generateContent",
UpstreamPath: "https://upstream.example/v1/chat/completions",
Converter: dto.AdvancedCustomConverterGeminiGenerateContentToOpenAIChatCompletions,
},
},
})
info.RequestURLPath = tt.requestURLPath
requestURL, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
parsedURL, err := url.Parse(requestURL)
require.NoError(t, err)
assert.Equal(t, tt.wantRequestPath, parsedURL.Path)
})
}
}
func advancedCustomRelayInfo(config *dto.AdvancedCustomConfig) *relaycommon.RelayInfo {
return &relaycommon.RelayInfo{
RelayFormat: types.RelayFormatOpenAI,
RelayMode: relayconstant.RelayModeChatCompletions,
RequestURLPath: "/v1/chat/completions",
ChannelMeta: &relaycommon.ChannelMeta{
ApiKey: "sk-test",
ChannelBaseUrl: "https://fallback.example",
ChannelType: constant.ChannelTypeAdvancedCustom,
ChannelOtherSettings: dto.ChannelOtherSettings{
AdvancedCustom: config,
},
},
}
}
func advancedCustomGinContext(path string) *gin.Context {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, path, nil)
c.Request.Header.Set("Content-Type", "application/json")
return c
}
...@@ -336,6 +336,7 @@ var streamSupportedChannels = map[int]bool{ ...@@ -336,6 +336,7 @@ var streamSupportedChannels = map[int]bool{
constant.ChannelTypeMoonshot: true, constant.ChannelTypeMoonshot: true,
constant.ChannelTypeMiniMax: true, constant.ChannelTypeMiniMax: true,
constant.ChannelTypeSiliconFlow: true, constant.ChannelTypeSiliconFlow: true,
constant.ChannelTypeAdvancedCustom: true,
} }
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/advancedcustom"
"github.com/QuantumNous/new-api/relay/channel/ali" "github.com/QuantumNous/new-api/relay/channel/ali"
"github.com/QuantumNous/new-api/relay/channel/aws" "github.com/QuantumNous/new-api/relay/channel/aws"
"github.com/QuantumNous/new-api/relay/channel/baidu" "github.com/QuantumNous/new-api/relay/channel/baidu"
...@@ -120,6 +121,8 @@ func GetAdaptor(apiType int) channel.Adaptor { ...@@ -120,6 +121,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &replicate.Adaptor{} return &replicate.Adaptor{}
case constant.APITypeCodex: case constant.APITypeCodex:
return &codex.Adaptor{} return &codex.Adaptor{}
case constant.APITypeAdvancedCustom:
return &advancedcustom.Adaptor{}
} }
return nil return nil
} }
......
...@@ -159,7 +159,10 @@ function SelectItem({ ...@@ -159,7 +159,10 @@ function SelectItem({
)} )}
{...props} {...props}
> >
<SelectPrimitive.ItemText className='flex flex-1 shrink-0 gap-2 whitespace-nowrap'> <SelectPrimitive.ItemText
data-slot='select-item-text'
className='flex flex-1 shrink-0 gap-2 whitespace-nowrap'
>
{children} {children}
</SelectPrimitive.ItemText> </SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator <SelectPrimitive.ItemIndicator
......
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type ReactNode, useMemo, useState } from 'react'
import { Check, Plus, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { Dialog } from '@/components/dialog'
import {
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS,
ADVANCED_CUSTOM_CONVERTER_OPTIONS,
ADVANCED_CUSTOM_TEMPLATE_OPTIONS,
type AdvancedCustomAuthMode,
buildAdvancedCustomAuth,
createAdvancedCustomConfig,
createAdvancedCustomRoute,
getAdvancedCustomAuthMode,
getAdvancedCustomIncomingPathLabel,
getAdvancedCustomIncomingPathOptions,
getAdvancedCustomTemplateConfig,
getAdvancedCustomUpstreamPathPlaceholder,
getDefaultAdvancedCustomIncomingPath,
isAdvancedCustomIncomingPathAllowed,
normalizeAdvancedCustomConfig,
parseAdvancedCustomConfig,
stringifyAdvancedCustomConfig,
validateAdvancedCustomConfig,
} from '../../lib/advanced-custom'
import type {
AdvancedCustomAuthType,
AdvancedCustomConfig,
AdvancedCustomConverter,
AdvancedCustomRoute,
} from '../../types'
type AdvancedCustomEditorDialogProps = {
open: boolean
value: string
onOpenChange: (open: boolean) => void
onSave: (value: string) => void
}
type AdvancedCustomEditMode = 'visual' | 'json'
const longSelectContentClass = 'w-[360px] max-w-[calc(100vw-2rem)]'
const longSelectItemClass =
'items-start py-2 [&_[data-slot=select-item-text]]:min-w-0 [&_[data-slot=select-item-text]]:shrink [&_[data-slot=select-item-text]]:whitespace-normal'
function getOptionLabel(
options: ReadonlyArray<{ value: string; label: string }>,
value: string
) {
return options.find((option) => option.value === value)?.label || value
}
export function AdvancedCustomEditorDialog({
open,
value,
onOpenChange,
onSave,
}: AdvancedCustomEditorDialogProps) {
const { t } = useTranslation()
const [config, setConfig] = useState<AdvancedCustomConfig>(
() => parseAdvancedCustomConfig(value) || createAdvancedCustomConfig()
)
const [editMode, setEditMode] = useState<AdvancedCustomEditMode>('visual')
const [jsonText, setJsonText] = useState(() =>
stringifyAdvancedCustomConfig(
parseAdvancedCustomConfig(value) || createAdvancedCustomConfig()
)
)
const [jsonError, setJsonError] = useState('')
const [templateKey, setTemplateKey] = useState(
ADVANCED_CUSTOM_TEMPLATE_OPTIONS[0]?.value || ''
)
const templateLabel = useMemo(
() => getOptionLabel(ADVANCED_CUSTOM_TEMPLATE_OPTIONS, templateKey),
[templateKey]
)
const normalizedConfig = useMemo(
() => normalizeAdvancedCustomConfig(config),
[config]
)
const routes = normalizedConfig.advanced_routes || []
const validationError = useMemo(
() => validateAdvancedCustomConfig(normalizedConfig),
[normalizedConfig]
)
const updateRoute = (index: number, patch: Partial<AdvancedCustomRoute>) => {
setConfig((current) => {
const next = normalizeAdvancedCustomConfig(current)
const nextRoutes = [...(next.advanced_routes || [])]
nextRoutes[index] = { ...nextRoutes[index], ...patch }
return { ...next, advanced_routes: nextRoutes }
})
}
const addRoute = () => {
setConfig((current) => {
const next = normalizeAdvancedCustomConfig(current)
return {
...next,
advanced_routes: [
...(next.advanced_routes || []),
createAdvancedCustomRoute(),
],
}
})
}
const removeRoute = (index: number) => {
setConfig((current) => {
const next = normalizeAdvancedCustomConfig(current)
return {
...next,
advanced_routes: (next.advanced_routes || []).filter(
(_, routeIndex) => routeIndex !== index
),
}
})
}
const setFallbackEnabled = (enabled: boolean) => {
setConfig((current) => ({
...normalizeAdvancedCustomConfig(current),
advanced_fallback: { enabled },
}))
}
const parseJsonEditorConfig = (): AdvancedCustomConfig | null => {
const parsed = parseAdvancedCustomConfig(jsonText)
if (!parsed) {
setJsonError(t('Invalid JSON'))
return null
}
const error = validateAdvancedCustomConfig(parsed)
if (error) {
setJsonError(t(error.message))
return null
}
setJsonError('')
return parsed
}
const switchToVisualMode = () => {
const parsed = parseJsonEditorConfig()
if (!parsed) return
setConfig(parsed)
setEditMode('visual')
}
const switchToJsonMode = () => {
setJsonText(stringifyAdvancedCustomConfig(normalizedConfig))
setJsonError('')
setEditMode('json')
}
const handleJsonChange = (nextValue: string) => {
setJsonText(nextValue)
if (jsonError) setJsonError('')
}
const formatJson = () => {
const parsed = parseJsonEditorConfig()
if (!parsed) return
setJsonText(stringifyAdvancedCustomConfig(parsed))
}
const applyTemplate = (mode: 'fill' | 'append') => {
const templateConfig = getAdvancedCustomTemplateConfig(templateKey)
let nextConfig = templateConfig
if (mode === 'append') {
const baseConfig =
editMode === 'json' ? parseJsonEditorConfig() : normalizedConfig
if (!baseConfig) return
const base = normalizeAdvancedCustomConfig(baseConfig)
const template = normalizeAdvancedCustomConfig(templateConfig)
nextConfig = {
advanced_routes: [
...(base.advanced_routes || []),
...(template.advanced_routes || []),
],
advanced_fallback: {
enabled:
base.advanced_fallback?.enabled === true ||
template.advanced_fallback?.enabled === true,
},
}
}
const normalized = normalizeAdvancedCustomConfig(nextConfig)
setConfig(normalized)
setJsonText(stringifyAdvancedCustomConfig(normalized))
setJsonError('')
}
const saveConfig = () => {
if (editMode === 'json') {
const parsed = parseJsonEditorConfig()
if (!parsed) {
toast.error(t('Please fix JSON errors before saving'))
return
}
onSave(stringifyAdvancedCustomConfig(parsed))
onOpenChange(false)
return
}
if (validationError) {
toast.error(t(validationError.message))
return
}
onSave(stringifyAdvancedCustomConfig(normalizedConfig))
onOpenChange(false)
}
return (
<Dialog
open={open}
onOpenChange={onOpenChange}
title={t('Advanced Custom Routes')}
description={t('Advanced Custom')}
contentClassName='flex max-h-[90vh] flex-col gap-0 p-0 sm:max-w-5xl'
headerClassName='border-b px-6 py-4'
footerClassName='border-t px-6 py-4'
contentHeight='70vh'
footer={
<>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
{t('Cancel')}
</Button>
<Button type='button' onClick={saveConfig}>
<Check className='mr-2 h-4 w-4' />
{t('Save changes')}
</Button>
</>
}
>
<div className='bg-muted/30 border-b px-4 py-3'>
<div className='flex flex-wrap items-center gap-2'>
<span className='text-muted-foreground text-xs font-medium'>
{t('Mode')}
</span>
<Button
type='button'
variant={editMode === 'visual' ? 'default' : 'outline'}
size='sm'
onClick={switchToVisualMode}
>
{t('Visual')}
</Button>
<Button
type='button'
variant={editMode === 'json' ? 'default' : 'outline'}
size='sm'
onClick={switchToJsonMode}
>
{t('JSON Text')}
</Button>
<div className='bg-border mx-1 h-5 w-px' />
<span className='text-muted-foreground text-xs font-medium'>
{t('Template')}
</span>
<Select
value={templateKey}
onValueChange={(nextValue) =>
setTemplateKey(
nextValue || ADVANCED_CUSTOM_TEMPLATE_OPTIONS[0]?.value || ''
)
}
>
<SelectTrigger className='h-8 min-w-[260px] max-w-full flex-1 sm:w-[320px]'>
<SelectValue className='min-w-0 truncate'>
{t(templateLabel)}
</SelectValue>
</SelectTrigger>
<SelectContent
alignItemWithTrigger={false}
className={longSelectContentClass}
>
<SelectGroup>
{ADVANCED_CUSTOM_TEMPLATE_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
className={longSelectItemClass}
>
<span className='min-w-0 whitespace-normal break-words leading-snug'>
{t(option.label)}
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => applyTemplate('fill')}
>
{t('Fill Template')}
</Button>
<Button
type='button'
variant='ghost'
size='sm'
onClick={() => applyTemplate('append')}
>
{t('Append Template')}
</Button>
</div>
</div>
{editMode === 'visual' ? (
<div className='flex flex-col gap-5 p-4'>
<div className='flex flex-col gap-3 border-y py-4 sm:flex-row sm:items-center sm:justify-between'>
<div className='flex items-center gap-3'>
<Switch
checked={normalizedConfig.advanced_fallback?.enabled === true}
onCheckedChange={setFallbackEnabled}
/>
<div className='space-y-1'>
<div className='text-sm font-medium'>
{t('Fallback routing')}
</div>
<div className='text-muted-foreground max-w-2xl text-xs leading-relaxed'>
{t(
'When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.'
)}
</div>
</div>
</div>
<Button
type='button'
variant='outline'
size='sm'
onClick={addRoute}
>
<Plus className='mr-2 h-4 w-4' />
{t('Add route')}
</Button>
</div>
{validationError ? (
<Alert variant='destructive'>
<AlertDescription>
{validationError.routeIndex !== undefined
? `${t('Route')} ${validationError.routeIndex + 1}: `
: ''}
{t(validationError.message)}
</AlertDescription>
</Alert>
) : null}
<div className='flex flex-col gap-4'>
{routes.map((route, index) => (
<RouteEditor
key={index}
route={route}
index={index}
onChange={(patch) => updateRoute(index, patch)}
onRemove={() => removeRoute(index)}
/>
))}
</div>
</div>
) : (
<div className='p-4'>
<div className='mb-2 flex items-center gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={formatJson}
>
{t('Format')}
</Button>
<span className='text-muted-foreground text-xs'>
{t('Advanced text editing')}
</span>
</div>
<Textarea
value={jsonText}
onChange={(event) => handleJsonChange(event.target.value)}
placeholder={stringifyAdvancedCustomConfig(
getAdvancedCustomTemplateConfig(templateKey)
)}
rows={22}
className='min-h-[420px] font-mono text-xs'
/>
<p className='text-muted-foreground mt-2 text-xs'>
{t('Edit JSON text directly. Format will be validated on save.')}
</p>
{jsonError ? (
<p className='text-destructive mt-1 text-xs'>{jsonError}</p>
) : null}
</div>
)}
</Dialog>
)
}
function RouteEditor({
route,
index,
onChange,
onRemove,
}: {
route: AdvancedCustomRoute
index: number
onChange: (patch: Partial<AdvancedCustomRoute>) => void
onRemove: () => void
}) {
const { t } = useTranslation()
const converter = route.converter || 'none'
const authMode = getAdvancedCustomAuthMode(route)
const incomingPath =
route.incoming_path || getDefaultAdvancedCustomIncomingPath(converter)
const incomingPathOptions = useMemo(
() => getAdvancedCustomIncomingPathOptions(converter),
[converter]
)
const incomingPathLabel = getAdvancedCustomIncomingPathLabel(incomingPath)
const converterLabel =
getOptionLabel(ADVANCED_CUSTOM_CONVERTER_OPTIONS, converter)
const authLabel = getOptionLabel(ADVANCED_CUSTOM_AUTH_MODE_OPTIONS, authMode)
const setConverter = (nextConverter: AdvancedCustomConverter) => {
const patch: Partial<AdvancedCustomRoute> = { converter: nextConverter }
if (!isAdvancedCustomIncomingPathAllowed(incomingPath, nextConverter)) {
patch.incoming_path = getDefaultAdvancedCustomIncomingPath(nextConverter)
}
onChange(patch)
}
const setAuthMode = (mode: AdvancedCustomAuthMode) => {
onChange({ auth: buildAdvancedCustomAuth(mode, route.auth) })
}
const updateAuth = (
field: Exclude<keyof NonNullable<AdvancedCustomRoute['auth']>, 'type'>,
value: string
) => {
const currentAuth = route.auth
if (!currentAuth || currentAuth.type === 'none') return
onChange({
auth: {
type: currentAuth.type as AdvancedCustomAuthType,
name: currentAuth.name || '',
value: currentAuth.value || '',
[field]: value,
},
})
}
return (
<div className='border-border flex flex-col gap-4 rounded-md border p-4'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='min-w-0 space-y-2'>
<div className='flex flex-wrap items-center gap-2'>
<div className='text-sm font-medium'>
{t('Route')} {index + 1}
</div>
<Badge variant='secondary'>{t(converterLabel)}</Badge>
</div>
</div>
<Button type='button' variant='ghost' size='icon' onClick={onRemove}>
<Trash2 className='h-4 w-4' />
<span className='sr-only'>{t('Delete')}</span>
</Button>
</div>
<div className='grid gap-4 md:grid-cols-2'>
<FieldBlock label={t('Incoming path')}>
<Select
value={incomingPath}
onValueChange={(value) =>
onChange({
incoming_path:
value || getDefaultAdvancedCustomIncomingPath(converter),
})
}
>
<SelectTrigger className='w-full max-w-full'>
<SelectValue className='min-w-0 truncate'>
{`${t(incomingPathLabel)} · ${incomingPath}`}
</SelectValue>
</SelectTrigger>
<SelectContent
alignItemWithTrigger={false}
className={longSelectContentClass}
>
<SelectGroup>
{incomingPathOptions.map((option) => (
<SelectItem
key={option.value}
value={option.value}
className={longSelectItemClass}
>
<div className='flex min-w-0 flex-col gap-1 whitespace-normal leading-snug'>
<span>{t(option.label)}</span>
<span className='text-muted-foreground break-all font-mono text-xs'>
{option.value}
</span>
</div>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FieldBlock>
<FieldBlock label={t('Upstream path')}>
<Input
value={route.upstream_path || ''}
onChange={(event) =>
onChange({
upstream_path: event.target.value,
})
}
placeholder={getAdvancedCustomUpstreamPathPlaceholder(converter)}
/>
<p className='text-muted-foreground text-xs leading-relaxed'>
{t(
'Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.'
)}
</p>
</FieldBlock>
</div>
<div className='grid gap-4 md:grid-cols-2'>
<FieldBlock label={t('Converter')}>
<Select
value={converter}
onValueChange={(value) =>
setConverter(value as AdvancedCustomConverter)
}
>
<SelectTrigger className='w-full max-w-full'>
<SelectValue className='min-w-0 truncate'>
{t(converterLabel)}
</SelectValue>
</SelectTrigger>
<SelectContent
alignItemWithTrigger={false}
className={longSelectContentClass}
>
<SelectGroup>
{ADVANCED_CUSTOM_CONVERTER_OPTIONS.map((option) => (
<SelectItem
key={option.value}
value={option.value}
className={longSelectItemClass}
>
<span className='min-w-0 whitespace-normal break-words leading-snug'>
{t(option.label)}
</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FieldBlock>
<FieldBlock label={t('Auth')}>
<Select
value={authMode}
onValueChange={(value) =>
setAuthMode(value as AdvancedCustomAuthMode)
}
>
<SelectTrigger className='w-full max-w-full'>
<SelectValue className='min-w-0 truncate'>
{t(authLabel)}
</SelectValue>
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
{ADVANCED_CUSTOM_AUTH_MODE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{t(option.label)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FieldBlock>
</div>
{authMode === 'header' || authMode === 'query' ? (
<>
<Separator />
<div className='grid gap-4 md:grid-cols-2'>
<FieldBlock label={t('Auth name')}>
<Input
value={route.auth?.name || ''}
onChange={(event) => updateAuth('name', event.target.value)}
placeholder={
authMode === 'header' ? 'Authorization' : 'api_key'
}
/>
</FieldBlock>
<FieldBlock label={t('Auth value')}>
<Input
value={route.auth?.value || ''}
onChange={(event) => updateAuth('value', event.target.value)}
placeholder={
authMode === 'header' ? 'Bearer {api_key}' : '{api_key}'
}
/>
</FieldBlock>
</div>
</>
) : null}
</div>
)
}
function FieldBlock({
label,
children,
}: {
label: string
children: ReactNode
}) {
return (
<div className='flex min-w-0 flex-col gap-2'>
<span className='text-sm font-medium'>{label}</span>
{children}
</div>
)
}
...@@ -125,8 +125,10 @@ import { ...@@ -125,8 +125,10 @@ import {
import { useChannelMutateForm } from '../../hooks/use-channel-mutate-form' import { useChannelMutateForm } from '../../hooks/use-channel-mutate-form'
import { import {
CHANNEL_FORM_DEFAULT_VALUES, CHANNEL_FORM_DEFAULT_VALUES,
CHANNEL_TYPE_ADVANCED_CUSTOM,
channelFormSchema, channelFormSchema,
channelsQueryKeys, channelsQueryKeys,
getAdvancedCustomStats,
transformChannelToFormDefaults, transformChannelToFormDefaults,
type ChannelFormValues, type ChannelFormValues,
deduplicateKeys, deduplicateKeys,
...@@ -147,6 +149,7 @@ import { ...@@ -147,6 +149,7 @@ import {
} from '../../lib/status-code-risk-guard' } from '../../lib/status-code-risk-guard'
import type { Channel } from '../../types' import type { Channel } from '../../types'
import { useChannels } from '../channels-provider' import { useChannels } from '../channels-provider'
import { AdvancedCustomEditorDialog } from '../dialogs/advanced-custom-editor-dialog'
import { FetchModelsDialog } from '../dialogs/fetch-models-dialog' import { FetchModelsDialog } from '../dialogs/fetch-models-dialog'
import { import {
MissingModelsConfirmationDialog, MissingModelsConfirmationDialog,
...@@ -205,6 +208,7 @@ function hasAdvancedSettingsValues(values: ChannelFormValues): boolean { ...@@ -205,6 +208,7 @@ function hasAdvancedSettingsValues(values: ChannelFormValues): boolean {
return Boolean( return Boolean(
values.param_override?.trim() || values.param_override?.trim() ||
values.header_override?.trim() || values.header_override?.trim() ||
values.advanced_custom?.trim() ||
values.status_code_mapping?.trim() || values.status_code_mapping?.trim() ||
values.tag?.trim() || values.tag?.trim() ||
values.remark?.trim() || values.remark?.trim() ||
...@@ -298,6 +302,8 @@ export function ChannelMutateDrawer({ ...@@ -298,6 +302,8 @@ export function ChannelMutateDrawer({
>(null) >(null)
const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false) const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false)
const [paramOverrideEditorOpen, setParamOverrideEditorOpen] = useState(false) const [paramOverrideEditorOpen, setParamOverrideEditorOpen] = useState(false)
const [advancedCustomEditorOpen, setAdvancedCustomEditorOpen] =
useState(false)
const isEditing = Boolean(currentRow) const isEditing = Boolean(currentRow)
const channelId = currentRow?.id ?? null const channelId = currentRow?.id ?? null
...@@ -375,6 +381,7 @@ export function ChannelMutateDrawer({ ...@@ -375,6 +381,7 @@ export function ChannelMutateDrawer({
'upstream_model_update_check_enabled' 'upstream_model_update_check_enabled'
) )
const currentSettings = form.watch('settings') const currentSettings = form.watch('settings')
const currentAdvancedCustom = form.watch('advanced_custom')
const { const {
unlocked: doubaoApiEditUnlocked, unlocked: doubaoApiEditUnlocked,
handleClick: handleApiConfigSecretClick, handleClick: handleApiConfigSecretClick,
...@@ -407,6 +414,11 @@ export function ChannelMutateDrawer({ ...@@ -407,6 +414,11 @@ export function ChannelMutateDrawer({
[supportsMultiKeyAddMode] [supportsMultiKeyAddMode]
) )
const advancedCustomStats = useMemo(
() => getAdvancedCustomStats(currentAdvancedCustom),
[currentAdvancedCustom]
)
// Get all models list // Get all models list
const allModelsList = useMemo( const allModelsList = useMemo(
() => allModelsData?.data?.map((model) => model.id).filter(Boolean) || [], () => allModelsData?.data?.map((model) => model.id).filter(Boolean) || [],
...@@ -1817,6 +1829,62 @@ export function ChannelMutateDrawer({ ...@@ -1817,6 +1829,62 @@ export function ChannelMutateDrawer({
/> />
)} )}
{currentType === CHANNEL_TYPE_ADVANCED_CUSTOM && (
<FormField
control={form.control}
name='advanced_custom'
render={({ field }) => (
<FormItem className='space-y-3 border-y py-4'>
<div className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
<div className='space-y-2'>
<FormLabel>
{t('Advanced Custom Routes')}
</FormLabel>
<div className='flex flex-wrap gap-2'>
<Badge variant='secondary'>
{t('Routes')}:{' '}
{advancedCustomStats.routeCount}
</Badge>
<Badge
variant={
advancedCustomStats.fallbackEnabled
? 'default'
: 'outline'
}
>
{t('Fallback')}:{' '}
{advancedCustomStats.fallbackEnabled
? t('Enabled')
: t('Disabled')}
</Badge>
{!advancedCustomStats.valid && (
<Badge variant='destructive'>
{t('Incomplete')}
</Badge>
)}
</div>
</div>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setAdvancedCustomEditorOpen(true)
}
>
<Route className='mr-2 h-4 w-4' />
{t('Configure routes')}
</Button>
</div>
<FormControl>
<input type='hidden' {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ChannelAuthSection> <ChannelAuthSection>
{!isEditing && ( {!isEditing && (
<FormField <FormField
...@@ -3423,6 +3491,20 @@ export function ChannelMutateDrawer({ ...@@ -3423,6 +3491,20 @@ export function ChannelMutateDrawer({
/> />
)} )}
{advancedCustomEditorOpen && (
<AdvancedCustomEditorDialog
open={advancedCustomEditorOpen}
value={form.watch('advanced_custom') || ''}
onOpenChange={setAdvancedCustomEditorOpen}
onSave={(nextValue) => {
form.setValue('advanced_custom', nextValue, {
shouldDirty: true,
shouldValidate: true,
})
}}
/>
)}
{/* Fetch Models Dialog */} {/* Fetch Models Dialog */}
<FetchModelsDialog <FetchModelsDialog
open={fetchModelsDialogOpen} open={fetchModelsDialogOpen}
......
...@@ -76,12 +76,13 @@ export const CHANNEL_TYPES = { ...@@ -76,12 +76,13 @@ export const CHANNEL_TYPES = {
55: 'Sora', 55: 'Sora',
56: 'Replicate', 56: 'Replicate',
57: 'ChatGPT Subscription (Codex)', 57: 'ChatGPT Subscription (Codex)',
58: 'Advanced Custom',
} as const } as const
const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [ const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [
1, 14, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23, 1, 14, 33, 24, 43, 3, 41, 48, 42, 34, 20, 4, 40, 27, 25, 17, 26, 15, 46, 23,
18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21, 44, 2, 5, 36, 50, 18, 45, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 58, 57, 22, 21, 44, 2, 5, 36,
51, 52, 53, 54, 55, 56, 50, 51, 52, 53, 54, 55, 56,
] ]
export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => { export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => {
......
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type {
AdvancedCustomAuthType,
AdvancedCustomConfig,
AdvancedCustomConverter,
AdvancedCustomRoute,
AdvancedCustomRouteAuth,
} from '../types'
export const CHANNEL_TYPE_ADVANCED_CUSTOM = 58
export const ADVANCED_CUSTOM_CONVERTER_OPTIONS: Array<{
value: AdvancedCustomConverter
label: string
}> = [
{ value: 'none', label: 'Native forwarding' },
{
value: 'anthropic_messages_to_openai_chat_completions',
label: 'Anthropic Messages to OpenAI Chat',
},
{
value: 'openai_chat_completions_to_anthropic_messages',
label: 'OpenAI Chat to Anthropic Messages',
},
{
value: 'openai_chat_completions_to_openai_responses',
label: 'OpenAI Chat to OpenAI Responses',
},
{
value: 'gemini_generate_content_to_openai_chat_completions',
label: 'Gemini Generate Content to OpenAI Chat',
},
{
value: 'openai_chat_completions_to_gemini_generate_content',
label: 'OpenAI Chat to Gemini Generate Content',
},
]
export type AdvancedCustomAuthMode = 'default' | AdvancedCustomAuthType
export const ADVANCED_CUSTOM_AUTH_MODE_OPTIONS: Array<{
value: AdvancedCustomAuthMode
label: string
}> = [
{ value: 'default', label: 'Default Bearer' },
{ value: 'none', label: 'No Auth' },
{ value: 'header', label: 'Header' },
{ value: 'query', label: 'Query' },
]
export type AdvancedCustomIncomingPathOption = {
value: string
label: string
}
export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOption[] =
[
{
value: '/v1/chat/completions',
label: 'OpenAI Chat Completions',
},
{
value: '/v1/completions',
label: 'OpenAI Completions',
},
{
value: '/v1/responses',
label: 'OpenAI Responses',
},
{
value: '/v1/responses/compact',
label: 'OpenAI Responses Compact',
},
{
value: '/v1/embeddings',
label: 'OpenAI Embeddings',
},
{
value: '/v1/images/generations',
label: 'OpenAI Image Generations',
},
{
value: '/v1/images/edits',
label: 'OpenAI Image Edits',
},
{
value: '/v1/audio/speech',
label: 'OpenAI Audio Speech',
},
{
value: '/v1/audio/transcriptions',
label: 'OpenAI Audio Transcriptions',
},
{
value: '/v1/audio/translations',
label: 'OpenAI Audio Translations',
},
{
value: '/v1/rerank',
label: 'OpenAI Rerank',
},
{
value: '/v1/realtime',
label: 'OpenAI Realtime',
},
{
value: '/v1/messages',
label: 'Claude Messages',
},
{
value: '/v1beta/models/{model}:generateContent',
label: 'Gemini Generate Content',
},
{
value: '/v1beta/models/{model}:embedContent',
label: 'Gemini Embed Content',
},
{
value: '/v1beta/models/{model}:batchEmbedContents',
label: 'Gemini Batch Embed Contents',
},
]
export type AdvancedCustomValidationError = {
message: string
routeIndex?: number
}
export type AdvancedCustomTemplateOption = {
value: string
label: string
config: AdvancedCustomConfig
}
const bearerHeaderAuth = (): AdvancedCustomRouteAuth => ({
type: 'header',
name: 'Authorization',
value: 'Bearer {api_key}',
})
const apiKeyHeaderAuth = (): AdvancedCustomRouteAuth => ({
type: 'header',
name: 'x-api-key',
value: '{api_key}',
})
const geminiQueryAuth = (): AdvancedCustomRouteAuth => ({
type: 'query',
name: 'key',
value: '{api_key}',
})
export const ADVANCED_CUSTOM_TEMPLATE_OPTIONS: AdvancedCustomTemplateOption[] =
[
{
value: 'official_openai_chat',
label: 'Official OpenAI Chat',
config: {
advanced_routes: [
{
incoming_path: '/v1/chat/completions',
upstream_path: 'https://api.openai.com/v1/chat/completions',
converter: 'none',
auth: bearerHeaderAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_openai_responses',
label: 'Official OpenAI Responses',
config: {
advanced_routes: [
{
incoming_path: '/v1/responses',
upstream_path: 'https://api.openai.com/v1/responses',
converter: 'none',
auth: bearerHeaderAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_openai_embeddings',
label: 'Official OpenAI Embeddings',
config: {
advanced_routes: [
{
incoming_path: '/v1/embeddings',
upstream_path: 'https://api.openai.com/v1/embeddings',
converter: 'none',
auth: bearerHeaderAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_openai_images',
label: 'Official OpenAI Images',
config: {
advanced_routes: [
{
incoming_path: '/v1/images/generations',
upstream_path: 'https://api.openai.com/v1/images/generations',
converter: 'none',
auth: bearerHeaderAuth(),
},
{
incoming_path: '/v1/images/edits',
upstream_path: 'https://api.openai.com/v1/images/edits',
converter: 'none',
auth: bearerHeaderAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_claude_messages',
label: 'Official Claude Messages',
config: {
advanced_routes: [
{
incoming_path: '/v1/messages',
upstream_path: 'https://api.anthropic.com/v1/messages',
converter: 'none',
auth: apiKeyHeaderAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_gemini_native',
label: 'Official Gemini Native',
config: {
advanced_routes: [
{
incoming_path: '/v1beta/models/{model}:generateContent',
upstream_path:
'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
converter: 'none',
auth: geminiQueryAuth(),
},
{
incoming_path: '/v1beta/models/{model}:embedContent',
upstream_path:
'https://generativelanguage.googleapis.com/v1beta/models/{model}:embedContent',
converter: 'none',
auth: geminiQueryAuth(),
},
{
incoming_path: '/v1beta/models/{model}:batchEmbedContents',
upstream_path:
'https://generativelanguage.googleapis.com/v1beta/models/{model}:batchEmbedContents',
converter: 'none',
auth: geminiQueryAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
{
value: 'official_gemini_from_openai_chat',
label: 'Official Gemini from OpenAI Chat',
config: {
advanced_routes: [
{
incoming_path: '/v1/chat/completions',
upstream_path:
'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
converter: 'openai_chat_completions_to_gemini_generate_content',
auth: geminiQueryAuth(),
},
],
advanced_fallback: { enabled: false },
},
},
]
export function cloneAdvancedCustomConfig(
config: AdvancedCustomConfig
): AdvancedCustomConfig {
return JSON.parse(JSON.stringify(config)) as AdvancedCustomConfig
}
export function getAdvancedCustomTemplateConfig(
templateKey: string
): AdvancedCustomConfig {
const template =
ADVANCED_CUSTOM_TEMPLATE_OPTIONS.find(
(option) => option.value === templateKey
) || ADVANCED_CUSTOM_TEMPLATE_OPTIONS[0]
return cloneAdvancedCustomConfig(template.config)
}
export function createAdvancedCustomRoute(): AdvancedCustomRoute {
return {
incoming_path: '/v1/chat/completions',
upstream_path: '/v1/chat/completions',
converter: 'none',
}
}
export function createAdvancedCustomConfig(): AdvancedCustomConfig {
return {
advanced_routes: [createAdvancedCustomRoute()],
advanced_fallback: { enabled: false },
}
}
export function getAdvancedCustomUpstreamPathPlaceholder(
converter: AdvancedCustomConverter
): string {
if (converter === 'openai_chat_completions_to_gemini_generate_content') {
return '/v1beta/models/{model}:generateContent'
}
if (converter === 'openai_chat_completions_to_anthropic_messages') {
return '/v1/messages'
}
return '/v1/chat/completions'
}
export function getAdvancedCustomIncomingPathOptions(
converter: AdvancedCustomConverter
): AdvancedCustomIncomingPathOption[] {
return ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS.filter((option) =>
isConverterPathAllowed(option.value, converter)
)
}
export function getDefaultAdvancedCustomIncomingPath(
converter: AdvancedCustomConverter
): string {
return (
getAdvancedCustomIncomingPathOptions(converter)[0]?.value ||
'/v1/chat/completions'
)
}
export function isAdvancedCustomIncomingPathAllowed(
incomingPath: string,
converter: AdvancedCustomConverter
): boolean {
return getAdvancedCustomIncomingPathOptions(converter).some(
(option) => option.value === incomingPath
)
}
export function getAdvancedCustomIncomingPathLabel(value: string): string {
return (
ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS.find(
(option) => option.value === value
)?.label || value
)
}
export function parseAdvancedCustomConfig(
value: string | undefined
): AdvancedCustomConfig | null {
if (!value?.trim()) return null
try {
const parsed = JSON.parse(value)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return null
}
return normalizeAdvancedCustomConfig(parsed as AdvancedCustomConfig)
} catch {
return null
}
}
export function stringifyAdvancedCustomConfig(
config: AdvancedCustomConfig
): string {
return JSON.stringify(normalizeAdvancedCustomConfig(config), null, 2)
}
export function normalizeAdvancedCustomConfig(
config: AdvancedCustomConfig
): AdvancedCustomConfig {
const routes = Array.isArray(config.advanced_routes)
? config.advanced_routes.map(normalizeAdvancedCustomRoute)
: []
return {
advanced_routes: routes,
advanced_fallback: {
enabled: config.advanced_fallback?.enabled === true,
},
}
}
export function validateAdvancedCustomConfig(
config: AdvancedCustomConfig | null
): AdvancedCustomValidationError | null {
if (!config) {
return { message: 'Advanced custom configuration is required' }
}
const normalized = normalizeAdvancedCustomConfig(config)
const routes = normalized.advanced_routes || []
const fallbackEnabled = normalized.advanced_fallback?.enabled === true
if (routes.length === 0 && !fallbackEnabled) {
return {
message:
'Advanced custom configuration requires at least one route or fallback',
}
}
const seenPaths = new Set<string>()
for (let index = 0; index < routes.length; index += 1) {
const route = routes[index]
const incomingPath = route.incoming_path?.trim() || ''
const upstreamPath = getAdvancedCustomRouteUpstreamPath(route)
const converter = route.converter || 'none'
if (!incomingPath) {
return { routeIndex: index, message: 'Incoming path is required' }
}
if (!incomingPath.startsWith('/')) {
return { routeIndex: index, message: 'Incoming path must start with /' }
}
if (incomingPath.includes('?')) {
return {
routeIndex: index,
message: 'Incoming path must not include query',
}
}
if (seenPaths.has(incomingPath)) {
return { routeIndex: index, message: 'Incoming path must be unique' }
}
seenPaths.add(incomingPath)
if (!upstreamPath) {
return { routeIndex: index, message: 'Upstream path is required' }
}
if (!isFullHttpURLOrAbsolutePath(upstreamPath)) {
return {
routeIndex: index,
message: 'Upstream path must be a full URL or a path starting with /',
}
}
if (!isAdvancedCustomConverter(converter)) {
return { routeIndex: index, message: 'Converter is not registered' }
}
if (!isConverterPathAllowed(incomingPath, converter)) {
return {
routeIndex: index,
message: 'Converter does not match incoming path',
}
}
const authError = validateRouteAuth(route.auth)
if (authError) {
return { routeIndex: index, message: authError }
}
}
return null
}
export function advancedCustomConfigUsesRelativeUpstreamPath(
config: AdvancedCustomConfig | null
): boolean {
if (!config) return false
const normalized = normalizeAdvancedCustomConfig(config)
return (normalized.advanced_routes || []).some((route) =>
getAdvancedCustomRouteUpstreamPath(route).startsWith('/')
)
}
export function getAdvancedCustomStats(value: string | undefined): {
routeCount: number
fallbackEnabled: boolean
valid: boolean
} {
const config = parseAdvancedCustomConfig(value)
if (!config) {
return { routeCount: 0, fallbackEnabled: false, valid: false }
}
const normalized = normalizeAdvancedCustomConfig(config)
return {
routeCount: normalized.advanced_routes?.length || 0,
fallbackEnabled: normalized.advanced_fallback?.enabled === true,
valid: validateAdvancedCustomConfig(normalized) === null,
}
}
export function getAdvancedCustomAuthMode(
route: AdvancedCustomRoute
): AdvancedCustomAuthMode {
return route.auth?.type || 'default'
}
export function buildAdvancedCustomAuth(
mode: AdvancedCustomAuthMode,
previousAuth: AdvancedCustomRouteAuth | undefined
): AdvancedCustomRouteAuth | undefined {
if (mode === 'default') return undefined
if (mode === 'none') return { type: 'none' }
if (mode === 'header') {
return {
type: 'header',
name: previousAuth?.name || 'Authorization',
value: previousAuth?.value || 'Bearer {api_key}',
}
}
return {
type: 'query',
name: previousAuth?.name || 'api_key',
value: previousAuth?.value || '{api_key}',
}
}
function normalizeAdvancedCustomRoute(
route: AdvancedCustomRoute
): AdvancedCustomRoute {
const nextRoute: AdvancedCustomRoute = {
incoming_path: route.incoming_path || '',
upstream_path: getAdvancedCustomRouteUpstreamPath(route),
converter: route.converter || 'none',
}
if (route.auth) {
nextRoute.auth = {
type: route.auth.type,
name: route.auth.name || '',
value: route.auth.value || '',
}
}
return nextRoute
}
function getAdvancedCustomRouteUpstreamPath(route: AdvancedCustomRoute): string {
return (route.upstream_path || '').trim()
}
function isFullHttpURLOrAbsolutePath(value: string): boolean {
if (value.startsWith('/')) return !value.startsWith('//')
try {
const parsed = new URL(value)
return (
Boolean(parsed.host) &&
(parsed.protocol === 'http:' || parsed.protocol === 'https:')
)
} catch {
return false
}
}
function isAdvancedCustomConverter(
value: string
): value is AdvancedCustomConverter {
return ADVANCED_CUSTOM_CONVERTER_OPTIONS.some(
(option) => option.value === value
)
}
function isConverterPathAllowed(
incomingPath: string,
converter: AdvancedCustomConverter
): boolean {
if (converter === 'none') return true
if (converter === 'anthropic_messages_to_openai_chat_completions') {
return incomingPath === '/v1/messages'
}
if (
converter === 'openai_chat_completions_to_anthropic_messages' ||
converter === 'openai_chat_completions_to_openai_responses' ||
converter === 'openai_chat_completions_to_gemini_generate_content'
) {
return incomingPath === '/v1/chat/completions'
}
return (
incomingPath.includes(':generateContent') ||
incomingPath.includes(':streamGenerateContent')
)
}
function validateRouteAuth(
auth: AdvancedCustomRouteAuth | undefined
): string | null {
if (!auth) return null
if (auth.type === 'none') return null
if (auth.type !== 'header' && auth.type !== 'query') {
return 'Auth type is invalid'
}
if (!auth.name?.trim()) {
return 'Auth name is required'
}
if (!auth.value?.trim()) {
return 'Auth value is required'
}
return null
}
...@@ -33,6 +33,7 @@ const ADVANCED_SETTINGS_FIELDS = new Set<FieldPath<ChannelFormValues>>([ ...@@ -33,6 +33,7 @@ const ADVANCED_SETTINGS_FIELDS = new Set<FieldPath<ChannelFormValues>>([
'param_override', 'param_override',
'header_override', 'header_override',
'status_code_mapping', 'status_code_mapping',
'advanced_custom',
'force_format', 'force_format',
'thinking_to_content', 'thinking_to_content',
'pass_through_body_enabled', 'pass_through_body_enabled',
......
...@@ -23,6 +23,13 @@ import { ...@@ -23,6 +23,13 @@ import {
MODEL_FETCHABLE_TYPES, MODEL_FETCHABLE_TYPES,
} from '../constants' } from '../constants'
import type { Channel } from '../types' import type { Channel } from '../types'
import {
CHANNEL_TYPE_ADVANCED_CUSTOM,
advancedCustomConfigUsesRelativeUpstreamPath,
parseAdvancedCustomConfig,
stringifyAdvancedCustomConfig,
validateAdvancedCustomConfig,
} from './advanced-custom'
// ============================================================================ // ============================================================================
// Form Validation Schema // Form Validation Schema
...@@ -169,6 +176,7 @@ export const channelFormSchema = z ...@@ -169,6 +176,7 @@ export const channelFormSchema = z
.string() .string()
.optional() .optional()
.refine(isOptionalJsonObject, ERROR_MESSAGES.INVALID_JSON), .refine(isOptionalJsonObject, ERROR_MESSAGES.INVALID_JSON),
advanced_custom: z.string().optional(),
other: z.string().optional(), other: z.string().optional(),
// Multi-key options (not sent to backend directly) // Multi-key options (not sent to backend directly)
multi_key_mode: z.enum(['single', 'batch', 'multi_to_single']).optional(), multi_key_mode: z.enum(['single', 'batch', 'multi_to_single']).optional(),
...@@ -209,6 +217,37 @@ export const channelFormSchema = z ...@@ -209,6 +217,37 @@ export const channelFormSchema = z
) )
} }
if (data.type === CHANNEL_TYPE_ADVANCED_CUSTOM) {
const advancedCustomConfig = parseAdvancedCustomConfig(
data.advanced_custom
)
const advancedCustomError =
validateAdvancedCustomConfig(advancedCustomConfig)
if (advancedCustomError) {
addRequiredIssue(ctx, 'advanced_custom', advancedCustomError.message)
}
if (
advancedCustomConfig?.advanced_fallback?.enabled === true &&
!data.base_url?.trim()
) {
addRequiredIssue(
ctx,
'base_url',
'Base URL is required when fallback is enabled'
)
}
if (
advancedCustomConfigUsesRelativeUpstreamPath(advancedCustomConfig) &&
!data.base_url?.trim()
) {
addRequiredIssue(
ctx,
'base_url',
'Base URL is required when an advanced route uses an upstream path'
)
}
}
if ([3, 18, 21, 39, 41, 49].includes(data.type) && !data.other?.trim()) { if ([3, 18, 21, 39, 41, 49].includes(data.type) && !data.other?.trim()) {
addRequiredIssue( addRequiredIssue(
ctx, ctx,
...@@ -316,6 +355,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { ...@@ -316,6 +355,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = {
upstream_model_update_check_enabled: false, upstream_model_update_check_enabled: false,
upstream_model_update_auto_sync_enabled: false, upstream_model_update_auto_sync_enabled: false,
upstream_model_update_ignored_models: '', upstream_model_update_ignored_models: '',
advanced_custom: '',
} }
// ============================================================================ // ============================================================================
...@@ -370,6 +410,7 @@ export function transformChannelToFormDefaults( ...@@ -370,6 +410,7 @@ export function transformChannelToFormDefaults(
let upstreamModelUpdateCheckEnabled = false let upstreamModelUpdateCheckEnabled = false
let upstreamModelUpdateAutoSyncEnabled = false let upstreamModelUpdateAutoSyncEnabled = false
let upstreamModelUpdateIgnoredModels = '' let upstreamModelUpdateIgnoredModels = ''
let advancedCustom = ''
if (channel.settings) { if (channel.settings) {
try { try {
...@@ -394,6 +435,9 @@ export function transformChannelToFormDefaults( ...@@ -394,6 +435,9 @@ export function transformChannelToFormDefaults(
) )
? parsed.upstream_model_update_ignored_models.join(',') ? parsed.upstream_model_update_ignored_models.join(',')
: '' : ''
if (parsed.advanced_custom) {
advancedCustom = stringifyAdvancedCustomConfig(parsed.advanced_custom)
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Failed to parse channel settings:', error) console.error('Failed to parse channel settings:', error)
...@@ -443,6 +487,7 @@ export function transformChannelToFormDefaults( ...@@ -443,6 +487,7 @@ export function transformChannelToFormDefaults(
upstream_model_update_check_enabled: upstreamModelUpdateCheckEnabled, upstream_model_update_check_enabled: upstreamModelUpdateCheckEnabled,
upstream_model_update_auto_sync_enabled: upstreamModelUpdateAutoSyncEnabled, upstream_model_update_auto_sync_enabled: upstreamModelUpdateAutoSyncEnabled,
upstream_model_update_ignored_models: upstreamModelUpdateIgnoredModels, upstream_model_update_ignored_models: upstreamModelUpdateIgnoredModels,
advanced_custom: advancedCustom,
} }
} }
...@@ -567,6 +612,17 @@ function buildSettingsJSON(formData: ChannelFormValues): string { ...@@ -567,6 +612,17 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
} }
} }
if (formData.type === CHANNEL_TYPE_ADVANCED_CUSTOM) {
const advancedCustomConfig = parseAdvancedCustomConfig(
formData.advanced_custom
)
if (advancedCustomConfig) {
settingsObj.advanced_custom = advancedCustomConfig
}
} else if ('advanced_custom' in settingsObj) {
delete settingsObj.advanced_custom
}
return JSON.stringify(settingsObj) return JSON.stringify(settingsObj)
} }
......
...@@ -134,6 +134,16 @@ export const CHANNEL_TYPE_CONFIGS: Record<number, ChannelTypeConfig> = { ...@@ -134,6 +134,16 @@ export const CHANNEL_TYPE_CONFIGS: Record<number, ChannelTypeConfig> = {
baseUrl: 'Default: https://api.replicate.com', baseUrl: 'Default: https://api.replicate.com',
}, },
}, },
58: {
id: 58,
name: CHANNEL_TYPES[58],
icon: 'openai',
hints: {
baseUrl: 'Fallback base URL',
key: 'Used by route auth templates',
models: 'Models exposed by this channel',
},
},
} }
/** /**
......
...@@ -51,6 +51,7 @@ export function getChannelTypeIcon(type: number): string { ...@@ -51,6 +51,7 @@ export function getChannelTypeIcon(type: number): string {
6: 'OpenAI', // OpenAIMax 6: 'OpenAI', // OpenAIMax
7: 'OpenAI', // OhMyGPT 7: 'OpenAI', // OhMyGPT
8: 'OpenAI', // Custom 8: 'OpenAI', // Custom
58: 'OpenAI', // Advanced Custom
3: 'Azure', // Azure 3: 'Azure', // Azure
// Anthropic // Anthropic
......
...@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,6 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
// Re-export all library functions // Re-export all library functions
export * from './channel-actions' export * from './channel-actions'
export * from './advanced-custom'
export * from './channel-form-errors' export * from './channel-form-errors'
export * from './channel-form' export * from './channel-form'
export * from './channel-type-config' export * from './channel-type-config'
......
...@@ -105,8 +105,41 @@ export interface ChannelOtherSettings { ...@@ -105,8 +105,41 @@ export interface ChannelOtherSettings {
upstream_model_update_ignored_models?: string[] upstream_model_update_ignored_models?: string[]
upstream_model_update_last_check_time?: number upstream_model_update_last_check_time?: number
upstream_model_update_last_detected_models?: string[] upstream_model_update_last_detected_models?: string[]
advanced_custom?: AdvancedCustomConfig
} }
export interface AdvancedCustomConfig {
advanced_routes?: AdvancedCustomRoute[]
advanced_fallback?: AdvancedCustomFallback
}
export interface AdvancedCustomRoute {
incoming_path?: string
upstream_path?: string
converter?: AdvancedCustomConverter
auth?: AdvancedCustomRouteAuth
}
export interface AdvancedCustomFallback {
enabled?: boolean
}
export interface AdvancedCustomRouteAuth {
type?: AdvancedCustomAuthType
name?: string
value?: string
}
export type AdvancedCustomConverter =
| 'none'
| 'anthropic_messages_to_openai_chat_completions'
| 'openai_chat_completions_to_anthropic_messages'
| 'openai_chat_completions_to_openai_responses'
| 'gemini_generate_content_to_openai_chat_completions'
| 'openai_chat_completions_to_gemini_generate_content'
export type AdvancedCustomAuthType = 'none' | 'header' | 'query'
// ============================================================================ // ============================================================================
// API Response Types // API Response Types
// ============================================================================ // ============================================================================
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "Add Provider", "Add Provider": "Add Provider",
"Add Quota": "Add Quota", "Add Quota": "Add Quota",
"Add ratio override": "Add ratio override", "Add ratio override": "Add ratio override",
"Add route": "Add route",
"Add Row": "Add Row", "Add Row": "Add Row",
"Add Rule": "Add Rule", "Add Rule": "Add Rule",
"Add rule group": "Add rule group", "Add rule group": "Add rule group",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "Administrator username", "Administrator username": "Administrator username",
"Advanced": "Advanced", "Advanced": "Advanced",
"Advanced Configuration": "Advanced Configuration", "Advanced Configuration": "Advanced Configuration",
"Advanced Custom": "Advanced Custom",
"Advanced custom configuration is required": "Advanced custom configuration is required",
"Advanced custom configuration requires at least one route or fallback": "Advanced custom configuration requires at least one route or fallback",
"Advanced Custom Routes": "Advanced Custom Routes",
"Advanced options": "Advanced options", "Advanced options": "Advanced options",
"Advanced Options": "Advanced Options", "Advanced Options": "Advanced Options",
"Advanced platform configuration.": "Advanced platform configuration.", "Advanced platform configuration.": "Advanced platform configuration.",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "Answer", "Answer": "Answer",
"Answers for common access and billing questions": "Answers for common access and billing questions", "Answers for common access and billing questions": "Answers for common access and billing questions",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages to OpenAI Chat",
"Any Match (OR)": "Any Match (OR)", "Any Match (OR)": "Any Match (OR)",
"API": "API", "API": "API",
"API Access": "API Access", "API Access": "API Access",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "Audio Preview", "Audio Preview": "Audio Preview",
"Audio ratio": "Audio ratio", "Audio ratio": "Audio ratio",
"Audio Tokens": "Audio Tokens", "Audio Tokens": "Audio Tokens",
"Auth": "Auth",
"Auth configured": "Auth configured", "Auth configured": "Auth configured",
"Auth name": "Auth name",
"Auth name is required": "Auth name is required",
"Auth Style": "Auth Style", "Auth Style": "Auth Style",
"Auth type is invalid": "Auth type is invalid",
"Auth value": "Auth value",
"Auth value is required": "Auth value is required",
"auth.resetPasswordConfirm.backToLogin": "Return to login", "auth.resetPasswordConfirm.backToLogin": "Return to login",
"auth.resetPasswordConfirm.confirm": "Confirm reset password", "auth.resetPasswordConfirm.confirm": "Confirm reset password",
"auth.resetPasswordConfirm.description": "Confirm the reset request to generate a new password.", "auth.resetPasswordConfirm.description": "Confirm the reset request to generate a new password.",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "Base rate limit windows for this account.", "Base rate limit windows for this account.": "Base rate limit windows for this account.",
"Base URL": "Base URL", "Base URL": "Base URL",
"Base URL is required for this channel type": "Base URL is required for this channel type", "Base URL is required for this channel type": "Base URL is required for this channel type",
"Base URL is required when an advanced route uses an upstream path": "Base URL is required when an advanced route uses an upstream path",
"Base URL is required when fallback is enabled": "Base URL is required when fallback is enabled",
"Base URL of your Uptime Kuma instance": "Base URL of your Uptime Kuma instance", "Base URL of your Uptime Kuma instance": "Base URL of your Uptime Kuma instance",
"Basic Authentication": "Basic Authentication", "Basic Authentication": "Basic Authentication",
"Basic Configuration": "Basic Configuration", "Basic Configuration": "Basic Configuration",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "Changes are written to the settings draft on save.", "Changes are written to the settings draft on save.": "Changes are written to the settings draft on save.",
"Changing...": "Changing...", "Changing...": "Changing...",
"Channel": "Channel", "Channel": "Channel",
"Channel {{name}}": "Channel {{name}}",
"Channel {{name}} model {{model}}": "Channel {{name}} model {{model}}",
"Channel Affinity": "Channel Affinity", "Channel Affinity": "Channel Affinity",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit", "Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "Channel key", "Channel key": "Channel key",
"Channel key unlocked": "Channel key unlocked", "Channel key unlocked": "Channel key unlocked",
"Channel models": "Channel models", "Channel models": "Channel models",
"Channel {{name}}": "Channel {{name}}",
"Channel {{name}} model {{model}}": "Channel {{name}} model {{model}}",
"Channel name is required": "Channel name is required", "Channel name is required": "Channel name is required",
"Channel test completed": "Channel test completed", "Channel test completed": "Channel test completed",
"Channel type is required": "Channel type is required", "Channel type is required": "Channel type is required",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "Classic (Legacy Frontend)", "Classic (Legacy Frontend)": "Classic (Legacy Frontend)",
"Claude": "Claude", "Claude": "Claude",
"Claude CLI Header Passthrough": "Claude CLI Header Passthrough", "Claude CLI Header Passthrough": "Claude CLI Header Passthrough",
"Claude Messages": "Claude Messages",
"Clean": "Clean", "Clean": "Clean",
"Clean history logs": "Clean history logs", "Clean history logs": "Clean history logs",
"Clean logs": "Clean logs", "Clean logs": "Clean logs",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.",
"Configure pricing ratios for a specific model.": "Configure pricing ratios for a specific model.", "Configure pricing ratios for a specific model.": "Configure pricing ratios for a specific model.",
"Configure rate limiting rules for a specific user group.": "Configure rate limiting rules for a specific user group.", "Configure rate limiting rules for a specific user group.": "Configure rate limiting rules for a specific user group.",
"Configure routes": "Configure routes",
"Configure the ratio for this group.": "Configure the ratio for this group.", "Configure the ratio for this group.": "Configure the ratio for this group.",
"Configure upstream providers and routing.": "Configure upstream providers and routing.", "Configure upstream providers and routing.": "Configure upstream providers and routing.",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "Convert reasoning_content to <think> tag in content", "Convert reasoning_content to <think> tag in content": "Convert reasoning_content to <think> tag in content",
"Convert string to lowercase": "Convert string to lowercase", "Convert string to lowercase": "Convert string to lowercase",
"Convert string to uppercase": "Convert string to uppercase", "Convert string to uppercase": "Convert string to uppercase",
"Converter": "Converter",
"Converter does not match incoming path": "Converter does not match incoming path",
"Converter is not registered": "Converter is not registered",
"Copied": "Copied", "Copied": "Copied",
"Copied {{count}} key(s)": "Copied {{count}} key(s)", "Copied {{count}} key(s)": "Copied {{count}} key(s)",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "Default / range", "Default / range": "Default / range",
"Default API Version *": "Default API Version *", "Default API Version *": "Default API Version *",
"Default API version for this channel": "Default API version for this channel", "Default API version for this channel": "Default API version for this channel",
"Default Bearer": "Default Bearer",
"Default Collapse Sidebar": "Default Collapse Sidebar", "Default Collapse Sidebar": "Default Collapse Sidebar",
"Default consumption chart": "Default consumption chart", "Default consumption chart": "Default consumption chart",
"Default Max Tokens": "Default Max Tokens", "Default Max Tokens": "Default Max Tokens",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "Failed to update user", "Failed to update user": "Failed to update user",
"Failure keywords": "Failure keywords", "Failure keywords": "Failure keywords",
"Fair": "Fair", "Fair": "Fair",
"Fallback": "Fallback",
"Fallback base URL": "Fallback base URL",
"Fallback routing": "Fallback routing",
"Fallback tier": "Fallback tier", "Fallback tier": "Fallback tier",
"FAQ": "FAQ", "FAQ": "FAQ",
"FAQ added. Click \"Save Settings\" to apply.": "FAQ added. Click \"Save Settings\" to apply.", "FAQ added. Click \"Save Settings\" to apply.": "FAQ added. Click \"Save Settings\" to apply.",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC executed", "GC executed": "GC executed",
"GC execution failed": "GC execution failed", "GC execution failed": "GC execution failed",
"Gemini": "Gemini", "Gemini": "Gemini",
"Gemini Batch Embed Contents": "Gemini Batch Embed Contents",
"Gemini Embed Content": "Gemini Embed Content",
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content to OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K", "Gemini Image 4K": "Gemini Image 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.",
"General": "General", "General": "General",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "Include Rule Name", "Include Rule Name": "Include Rule Name",
"Includes request rules": "Includes request rules", "Includes request rules": "Includes request rules",
"Including failed requests, 0 = unlimited": "Including failed requests, 0 = unlimited", "Including failed requests, 0 = unlimited": "Including failed requests, 0 = unlimited",
"Incoming path": "Incoming path",
"Incoming path is required": "Incoming path is required",
"Incoming path must be unique": "Incoming path must be unique",
"Incoming path must not include query": "Incoming path must not include query",
"Incoming path must start with /": "Incoming path must start with /",
"Incomplete": "Incomplete",
"Increased user quota by {{quota}}": "Increased user quota by {{quota}}", "Increased user quota by {{quota}}": "Increased user quota by {{quota}}",
"Index": "Index", "Index": "Index",
"Initial quota given to new users": "Initial quota given to new users", "Initial quota given to new users": "Initial quota given to new users",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "Mode", "Mode": "Mode",
"model": "model", "model": "model",
"Model": "Model", "Model": "Model",
"Model {{model}}": "Model {{model}}",
"Model Access": "Model Access", "Model Access": "Model Access",
"Model Analytics": "Model Analytics", "Model Analytics": "Model Analytics",
"Model Analytics Defaults": "Model Analytics Defaults", "Model Analytics Defaults": "Model Analytics Defaults",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "Model mapping must be a JSON object with string values", "Model mapping must be a JSON object with string values": "Model mapping must be a JSON object with string values",
"Model mapping must be valid JSON": "Model mapping must be valid JSON", "Model mapping must be valid JSON": "Model mapping must be valid JSON",
"Model mapping values must be strings": "Model mapping values must be strings", "Model mapping values must be strings": "Model mapping values must be strings",
"Model {{model}}": "Model {{model}}",
"Model name": "Model name", "Model name": "Model name",
"Model Name": "Model Name", "Model Name": "Model Name",
"Model Name *": "Model Name *", "Model Name *": "Model Name *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "Models climbing the leaderboard fastest", "Models climbing the leaderboard fastest": "Models climbing the leaderboard fastest",
"Models directory": "Models directory", "Models directory": "Models directory",
"Models Directory": "Models Directory", "Models Directory": "Models Directory",
"Models exposed by this channel": "Models exposed by this channel",
"Models fetched successfully": "Models fetched successfully", "Models fetched successfully": "Models fetched successfully",
"Models filled to form": "Models filled to form", "Models filled to form": "Models filled to form",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "Name, provider type, and availability.", "Name, provider type, and availability.": "Name, provider type, and availability.",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "Native format", "Native format": "Native format",
"Native forwarding": "Native forwarding",
"Need a redemption code?": "Need a redemption code?", "Need a redemption code?": "Need a redemption code?",
"Needs API key": "Needs API key", "Needs API key": "Needs API key",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "No API routes configured", "No API routes configured": "No API routes configured",
"No app usage data available for this model.": "No app usage data available for this model.", "No app usage data available for this model.": "No app usage data available for this model.",
"No apps match the selected filters": "No apps match the selected filters", "No apps match the selected filters": "No apps match the selected filters",
"No Auth": "No Auth",
"No available models": "No available models", "No available models": "No available models",
"No available Web chat links": "No available Web chat links", "No available Web chat links": "No available Web chat links",
"No backup": "No backup", "No backup": "No backup",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "of 3:", "of 3:": "of 3:",
"off": "off", "off": "off",
"Official": "Official", "Official": "Official",
"Official Claude Messages": "Official Claude Messages",
"Official documentation": "Official documentation", "Official documentation": "Official documentation",
"Official Gemini from OpenAI Chat": "Official Gemini from OpenAI Chat",
"Official Gemini Native": "Official Gemini Native",
"Official OpenAI Chat": "Official OpenAI Chat",
"Official OpenAI Embeddings": "Official OpenAI Embeddings",
"Official OpenAI Images": "Official OpenAI Images",
"Official OpenAI Responses": "Official OpenAI Responses",
"Official Repository": "Official Repository", "Official Repository": "Official Repository",
"Official Sync": "Official Sync", "Official Sync": "Official Sync",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "Open theme settings", "Open theme settings": "Open theme settings",
"Open weights": "Open weights", "Open weights": "Open weights",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "OpenAI Audio Speech",
"OpenAI Audio Transcriptions": "OpenAI Audio Transcriptions",
"OpenAI Audio Translations": "OpenAI Audio Translations",
"OpenAI Chat Completions": "OpenAI Chat Completions",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat to Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat to Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat to OpenAI Responses",
"OpenAI Completions": "OpenAI Completions",
"OpenAI Compatible": "OpenAI Compatible", "OpenAI Compatible": "OpenAI Compatible",
"OpenAI Embeddings": "OpenAI Embeddings",
"OpenAI Image Edits": "OpenAI Image Edits",
"OpenAI Image Generations": "OpenAI Image Generations",
"OpenAI Organization": "OpenAI Organization", "OpenAI Organization": "OpenAI Organization",
"OpenAI Organization ID (optional)": "OpenAI Organization ID (optional)", "OpenAI Organization ID (optional)": "OpenAI Organization ID (optional)",
"OpenAI Realtime": "OpenAI Realtime",
"OpenAI Rerank": "OpenAI Rerank",
"OpenAI Responses": "OpenAI Responses",
"OpenAI Responses Compact": "OpenAI Responses Compact",
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.", "OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.",
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.", "OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "QR code is not configured. Please contact support.", "QR code is not configured. Please contact support.": "QR code is not configured. Please contact support.",
"Quantity": "Quantity", "Quantity": "Quantity",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "Query Balance", "Query Balance": "Query Balance",
"Query Param": "Query Param", "Query Param": "Query Param",
"Querying...": "Querying...", "Querying...": "Querying...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "Resolve Conflicts", "Resolve Conflicts": "Resolve Conflicts",
"Resource Configuration": "Resource Configuration", "Resource Configuration": "Resource Configuration",
"Response": "Response", "Response": "Response",
"Response time: {{duration}}": "Response time: {{duration}}",
"Response Time": "Response Time", "Response Time": "Response Time",
"Response time: {{duration}}": "Response time: {{duration}}",
"Responses API Version": "Responses API Version", "Responses API Version": "Responses API Version",
"Restore defaults": "Restore defaults", "Restore defaults": "Restore defaults",
"Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)", "Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "Route Description", "Route Description": "Route Description",
"Route is required": "Route is required", "Route is required": "Route is required",
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place", "Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
"Routes": "Routes",
"Routing & Overrides": "Routing & Overrides", "Routing & Overrides": "Routing & Overrides",
"Routing Strategy": "Routing Strategy", "Routing Strategy": "Routing Strategy",
"Rows per page": "Rows per page", "Rows per page": "Rows per page",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "Upstream Model Updates", "Upstream Model Updates": "Upstream Model Updates",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models",
"Upstream price sync": "Upstream price sync", "Upstream price sync": "Upstream price sync",
"Upstream path": "Upstream path",
"Upstream path is required": "Upstream path is required",
"Upstream path must be a full URL or a path starting with /": "Upstream path must be a full URL or a path starting with /",
"Upstream prices fetched successfully": "Upstream prices fetched successfully", "Upstream prices fetched successfully": "Upstream prices fetched successfully",
"Upstream ratios fetched successfully": "Upstream ratios fetched successfully", "Upstream ratios fetched successfully": "Upstream ratios fetched successfully",
"Upstream Request ID": "Upstream Request ID", "Upstream Request ID": "Upstream Request ID",
"Upstream Response": "Upstream Response", "Upstream Response": "Upstream Response",
"upstream services integrated": "upstream services integrated", "upstream services integrated": "upstream services integrated",
"Upstream Updates": "Upstream Updates", "Upstream Updates": "Upstream Updates",
"Upstream URL": "Upstream URL",
"Upstream URL must be a full URL": "Upstream URL must be a full URL",
"uptime": "uptime", "uptime": "uptime",
"Uptime": "Uptime", "Uptime": "Uptime",
"Uptime (30d)": "Uptime (30d)", "Uptime (30d)": "Uptime (30d)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "USD price per 1M input tokens.", "USD price per 1M input tokens.": "USD price per 1M input tokens.",
"USD price per 1M tokens.": "USD price per 1M tokens.", "USD price per 1M tokens.": "USD price per 1M tokens.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.",
"Use authenticator code": "Use authenticator code", "Use authenticator code": "Use authenticator code",
"Use backup code": "Use backup code", "Use backup code": "Use backup code",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "Used", "Used": "Used",
"Used / Remaining": "Used / Remaining", "Used / Remaining": "Used / Remaining",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.",
"Used by route auth templates": "Used by route auth templates",
"Used for load balancing. Higher weight = more requests": "Used for load balancing. Higher weight = more requests", "Used for load balancing. Higher weight = more requests": "Used for load balancing. Higher weight = more requests",
"Used in URLs and API routes": "Used in URLs and API routes", "Used in URLs and API routes": "Used in URLs and API routes",
"Used Quota": "Used Quota", "Used Quota": "Used Quota",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "When enabled, Midjourney callbacks are accepted (reveals server IP).", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "When enabled, Midjourney callbacks are accepted (reveals server IP).",
"When enabled, newly created tokens start in the first auto group.": "When enabled, newly created tokens start in the first auto group.", "When enabled, newly created tokens start in the first auto group.": "When enabled, newly created tokens start in the first auto group.",
"When enabled, prompts are scanned before reaching upstream models.": "When enabled, prompts are scanned before reaching upstream models.", "When enabled, prompts are scanned before reaching upstream models.": "When enabled, prompts are scanned before reaching upstream models.",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.",
"When enabled, the store field will be blocked": "When enabled, the store field will be blocked", "When enabled, the store field will be blocked": "When enabled, the store field will be blocked",
"When enabled, users can pick this group when creating tokens.": "When enabled, users can pick this group when creating tokens.", "When enabled, users can pick this group when creating tokens.": "When enabled, users can pick this group when creating tokens.",
"When enabled, violation requests will incur additional charges.": "When enabled, violation requests will incur additional charges.", "When enabled, violation requests will incur additional charges.": "When enabled, violation requests will incur additional charges.",
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "Ajouter un fournisseur", "Add Provider": "Ajouter un fournisseur",
"Add Quota": "Ajouter un quota", "Add Quota": "Ajouter un quota",
"Add ratio override": "Ajouter un remplacement de ratio", "Add ratio override": "Ajouter un remplacement de ratio",
"Add route": "Ajouter une route",
"Add Row": "Ajouter une ligne", "Add Row": "Ajouter une ligne",
"Add Rule": "Ajouter une règle", "Add Rule": "Ajouter une règle",
"Add rule group": "Ajouter un groupe de règles", "Add rule group": "Ajouter un groupe de règles",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "Nom d'utilisateur administrateur", "Administrator username": "Nom d'utilisateur administrateur",
"Advanced": "Avancé", "Advanced": "Avancé",
"Advanced Configuration": "Configuration avancée", "Advanced Configuration": "Configuration avancée",
"Advanced Custom": "Personnalisé avancé",
"Advanced custom configuration is required": "La configuration personnalisée avancée est requise",
"Advanced custom configuration requires at least one route or fallback": "La configuration personnalisée avancée nécessite au moins une route ou un fallback",
"Advanced Custom Routes": "Routes personnalisées avancées",
"Advanced options": "Options avancées", "Advanced options": "Options avancées",
"Advanced Options": "Options avancées", "Advanced Options": "Options avancées",
"Advanced platform configuration.": "Configuration avancée de la plateforme.", "Advanced platform configuration.": "Configuration avancée de la plateforme.",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "Réponse", "Answer": "Réponse",
"Answers for common access and billing questions": "Réponses aux questions courantes sur l'accès et la facturation", "Answers for common access and billing questions": "Réponses aux questions courantes sur l'accès et la facturation",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages vers OpenAI Chat",
"Any Match (OR)": "N'importe laquelle (OR)", "Any Match (OR)": "N'importe laquelle (OR)",
"API": "API", "API": "API",
"API Access": "Accès API", "API Access": "Accès API",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "Aperçu audio", "Audio Preview": "Aperçu audio",
"Audio ratio": "Ratio audio", "Audio ratio": "Ratio audio",
"Audio Tokens": "Jetons audio", "Audio Tokens": "Jetons audio",
"Auth": "Auth",
"Auth configured": "Authentification configurée", "Auth configured": "Authentification configurée",
"Auth name": "Nom auth",
"Auth name is required": "Le nom auth est requis",
"Auth Style": "Style d'authentification", "Auth Style": "Style d'authentification",
"Auth type is invalid": "Le type auth est invalide",
"Auth value": "Valeur auth",
"Auth value is required": "La valeur auth est requise",
"auth.resetPasswordConfirm.backToLogin": "Retour à la connexion", "auth.resetPasswordConfirm.backToLogin": "Retour à la connexion",
"auth.resetPasswordConfirm.confirm": "Confirmer la réinitialisation du mot de passe", "auth.resetPasswordConfirm.confirm": "Confirmer la réinitialisation du mot de passe",
"auth.resetPasswordConfirm.description": "Confirmez la demande de réinitialisation pour générer un nouveau mot de passe.", "auth.resetPasswordConfirm.description": "Confirmez la demande de réinitialisation pour générer un nouveau mot de passe.",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "Fenêtres de limitation de débit de base pour ce compte.", "Base rate limit windows for this account.": "Fenêtres de limitation de débit de base pour ce compte.",
"Base URL": "URL de base", "Base URL": "URL de base",
"Base URL is required for this channel type": "L'URL de base est requise pour ce type de canal", "Base URL is required for this channel type": "L'URL de base est requise pour ce type de canal",
"Base URL is required when an advanced route uses an upstream path": "La Base URL est requise lorsqu’une route avancée utilise un chemin amont",
"Base URL is required when fallback is enabled": "La Base URL est requise lorsque le fallback est active",
"Base URL of your Uptime Kuma instance": "URL de base de votre instance Uptime Kuma", "Base URL of your Uptime Kuma instance": "URL de base de votre instance Uptime Kuma",
"Basic Authentication": "Authentification de base", "Basic Authentication": "Authentification de base",
"Basic Configuration": "Configuration de base", "Basic Configuration": "Configuration de base",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "Les modifications sont écrites dans le brouillon des paramètres lors de l’enregistrement.", "Changes are written to the settings draft on save.": "Les modifications sont écrites dans le brouillon des paramètres lors de l’enregistrement.",
"Changing...": "Modification en cours...", "Changing...": "Modification en cours...",
"Channel": "Canal", "Channel": "Canal",
"Channel {{name}}": "Canal {{name}}",
"Channel {{name}} model {{model}}": "Canal {{name}} modèle {{model}}",
"Channel Affinity": "Affinité de canal", "Channel Affinity": "Affinité de canal",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont", "Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "Clé du canal", "Channel key": "Clé du canal",
"Channel key unlocked": "Clé de canal déverrouillée", "Channel key unlocked": "Clé de canal déverrouillée",
"Channel models": "Modèles de canaux", "Channel models": "Modèles de canaux",
"Channel {{name}}": "Canal {{name}}",
"Channel {{name}} model {{model}}": "Canal {{name}} modèle {{model}}",
"Channel name is required": "Le nom du canal est requis", "Channel name is required": "Le nom du canal est requis",
"Channel test completed": "Test du canal terminé", "Channel test completed": "Test du canal terminé",
"Channel type is required": "Le type de canal est requis", "Channel type is required": "Le type de canal est requis",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "Classique (Ancien frontend)", "Classic (Legacy Frontend)": "Classique (Ancien frontend)",
"Claude": "Claude", "Claude": "Claude",
"Claude CLI Header Passthrough": "Passthrough en-tête Claude CLI", "Claude CLI Header Passthrough": "Passthrough en-tête Claude CLI",
"Claude Messages": "Claude Messages",
"Clean": "Sans conflit", "Clean": "Sans conflit",
"Clean history logs": "Nettoyer les journaux d'historique", "Clean history logs": "Nettoyer les journaux d'historique",
"Clean logs": "Nettoyer les logs", "Clean logs": "Nettoyer les logs",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Définissez le prix unitaire de chaque outil ($/1K appels). Les modèles facturés à la requête n'entraînent pas de frais d'outils supplémentaires.", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Définissez le prix unitaire de chaque outil ($/1K appels). Les modèles facturés à la requête n'entraînent pas de frais d'outils supplémentaires.",
"Configure pricing ratios for a specific model.": "Configurer les ratios de tarification pour un modèle spécifique.", "Configure pricing ratios for a specific model.": "Configurer les ratios de tarification pour un modèle spécifique.",
"Configure rate limiting rules for a specific user group.": "Configurer les règles de limitation de débit pour un groupe d'utilisateurs spécifique.", "Configure rate limiting rules for a specific user group.": "Configurer les règles de limitation de débit pour un groupe d'utilisateurs spécifique.",
"Configure routes": "Configurer les routes",
"Configure the ratio for this group.": "Configurer le ratio pour ce groupe.", "Configure the ratio for this group.": "Configurer le ratio pour ce groupe.",
"Configure upstream providers and routing.": "Configurer les fournisseurs en amont et le routage.", "Configure upstream providers and routing.": "Configurer les fournisseurs en amont et le routage.",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Configurer l'intégration du parcours de paiement hébergé Waffo Pancake pour les rechargements en USD", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Configurer l'intégration du parcours de paiement hébergé Waffo Pancake pour les rechargements en USD",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "Convertir reasoning_content en balise <think> dans content", "Convert reasoning_content to <think> tag in content": "Convertir reasoning_content en balise <think> dans content",
"Convert string to lowercase": "Convertir la chaîne en minuscules", "Convert string to lowercase": "Convertir la chaîne en minuscules",
"Convert string to uppercase": "Convertir la chaîne en majuscules", "Convert string to uppercase": "Convertir la chaîne en majuscules",
"Converter": "Convertisseur",
"Converter does not match incoming path": "Le convertisseur ne correspond pas au chemin entrant",
"Converter is not registered": "Le convertisseur n’est pas enregistre",
"Copied": "Copié", "Copied": "Copié",
"Copied {{count}} key(s)": "{{count}} clé(s) copiée(s)", "Copied {{count}} key(s)": "{{count}} clé(s) copiée(s)",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Canal copié (ID source : {{sourceId}}) vers {{name}} (nouvel ID : {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Canal copié (ID source : {{sourceId}}) vers {{name}} (nouvel ID : {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "Défaut / plage", "Default / range": "Défaut / plage",
"Default API Version *": "Version API par défaut *", "Default API Version *": "Version API par défaut *",
"Default API version for this channel": "Version API par défaut pour ce canal", "Default API version for this channel": "Version API par défaut pour ce canal",
"Default Bearer": "Bearer par defaut",
"Default Collapse Sidebar": "Réduire la barre latérale par défaut", "Default Collapse Sidebar": "Réduire la barre latérale par défaut",
"Default consumption chart": "Graphique de consommation par défaut", "Default consumption chart": "Graphique de consommation par défaut",
"Default Max Tokens": "Jetons max par défaut", "Default Max Tokens": "Jetons max par défaut",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "Échec de la mise à jour de l'utilisateur", "Failed to update user": "Échec de la mise à jour de l'utilisateur",
"Failure keywords": "Mots-clés d'échec", "Failure keywords": "Mots-clés d'échec",
"Fair": "Correct", "Fair": "Correct",
"Fallback": "Fallback",
"Fallback base URL": "Base URL de fallback",
"Fallback routing": "Routage fallback",
"Fallback tier": "Palier de repli", "Fallback tier": "Palier de repli",
"FAQ": "FAQ", "FAQ": "FAQ",
"FAQ added. Click \"Save Settings\" to apply.": "FAQ ajouté. Cliquez sur \"Enregistrer les paramètres\" pour appliquer.", "FAQ added. Click \"Save Settings\" to apply.": "FAQ ajouté. Cliquez sur \"Enregistrer les paramètres\" pour appliquer.",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC exécuté", "GC executed": "GC exécuté",
"GC execution failed": "Échec de l'exécution du GC", "GC execution failed": "Échec de l'exécution du GC",
"Gemini": "Gemini", "Gemini": "Gemini",
"Gemini Batch Embed Contents": "Gemini Batch Embed Contents",
"Gemini Embed Content": "Gemini Embed Content",
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content vers OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K", "Gemini Image 4K": "Gemini Image 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini continuera à détecter automatiquement le mode de pensée même avec l'adaptateur désactivé. Activez ceci uniquement lorsque vous avez besoin d'un contrôle plus fin sur la tarification et le budget.", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini continuera à détecter automatiquement le mode de pensée même avec l'adaptateur désactivé. Activez ceci uniquement lorsque vous avez besoin d'un contrôle plus fin sur la tarification et le budget.",
"General": "Général", "General": "Général",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "Inclure le nom de la règle", "Include Rule Name": "Inclure le nom de la règle",
"Includes request rules": "Inclut des règles de requête", "Includes request rules": "Inclut des règles de requête",
"Including failed requests, 0 = unlimited": "Y compris les requêtes échouées, 0 = illimité", "Including failed requests, 0 = unlimited": "Y compris les requêtes échouées, 0 = illimité",
"Incoming path": "Chemin entrant",
"Incoming path is required": "Le chemin entrant est requis",
"Incoming path must be unique": "Le chemin entrant doit etre unique",
"Incoming path must not include query": "Le chemin entrant ne doit pas inclure de query",
"Incoming path must start with /": "Le chemin entrant doit commencer par /",
"Incomplete": "Incomplet",
"Increased user quota by {{quota}}": "Quota de l'utilisateur augmenté de {{quota}}", "Increased user quota by {{quota}}": "Quota de l'utilisateur augmenté de {{quota}}",
"Index": "Index", "Index": "Index",
"Initial quota given to new users": "Quota initial donné aux nouveaux utilisateurs", "Initial quota given to new users": "Quota initial donné aux nouveaux utilisateurs",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "Mode", "Mode": "Mode",
"model": "modèle", "model": "modèle",
"Model": "Modèle", "Model": "Modèle",
"Model {{model}}": "Modèle {{model}}",
"Model Access": "Accès au modèle", "Model Access": "Accès au modèle",
"Model Analytics": "Analyse des modèles", "Model Analytics": "Analyse des modèles",
"Model Analytics Defaults": "Paramètres par défaut de l’analyse des modèles", "Model Analytics Defaults": "Paramètres par défaut de l’analyse des modèles",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "Le mappage de modèles doit être un objet JSON avec des valeurs de chaîne", "Model mapping must be a JSON object with string values": "Le mappage de modèles doit être un objet JSON avec des valeurs de chaîne",
"Model mapping must be valid JSON": "La cartographie des modèles doit être un JSON valide", "Model mapping must be valid JSON": "La cartographie des modèles doit être un JSON valide",
"Model mapping values must be strings": "Les valeurs du mappage de modèles doivent être des chaînes", "Model mapping values must be strings": "Les valeurs du mappage de modèles doivent être des chaînes",
"Model {{model}}": "Modèle {{model}}",
"Model name": "Nom du modèle", "Model name": "Nom du modèle",
"Model Name": "Nom du modèle", "Model Name": "Nom du modèle",
"Model Name *": "Nom du modèle *", "Model Name *": "Nom du modèle *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "Modèles qui grimpent le plus vite au classement", "Models climbing the leaderboard fastest": "Modèles qui grimpent le plus vite au classement",
"Models directory": "Répertoire des modèles", "Models directory": "Répertoire des modèles",
"Models Directory": "Répertoire des modèles", "Models Directory": "Répertoire des modèles",
"Models exposed by this channel": "Modeles exposes par ce canal",
"Models fetched successfully": "Modèles récupérés avec succès", "Models fetched successfully": "Modèles récupérés avec succès",
"Models filled to form": "Modèles remplis pour le formulaire", "Models filled to form": "Modèles remplis pour le formulaire",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Les modèles listés ici n'ajouteront ni ne supprimeront automatiquement les suffixes -thinking / -nothinking.", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Les modèles listés ici n'ajouteront ni ne supprimeront automatiquement les suffixes -thinking / -nothinking.",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "Nom, type de fournisseur et disponibilité.", "Name, provider type, and availability.": "Nom, type de fournisseur et disponibilité.",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "Format natif", "Native format": "Format natif",
"Native forwarding": "Transfert natif",
"Need a redemption code?": "Besoin d'un code d'échange ?", "Need a redemption code?": "Besoin d'un code d'échange ?",
"Needs API key": "Clé API requise", "Needs API key": "Clé API requise",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "JSON imbriqué définissant des règles par groupe pour ajouter (+:), supprimer (-:), ou ajouter des groupes utilisables.", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "JSON imbriqué définissant des règles par groupe pour ajouter (+:), supprimer (-:), ou ajouter des groupes utilisables.",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "Aucune route API configurée", "No API routes configured": "Aucune route API configurée",
"No app usage data available for this model.": "Aucune donnée d'utilisation d'application n'est disponible pour ce modèle.", "No app usage data available for this model.": "Aucune donnée d'utilisation d'application n'est disponible pour ce modèle.",
"No apps match the selected filters": "Aucune application ne correspond aux filtres", "No apps match the selected filters": "Aucune application ne correspond aux filtres",
"No Auth": "Sans auth",
"No available models": "Aucun modèle disponible", "No available models": "Aucun modèle disponible",
"No available Web chat links": "Aucun lien de chat Web disponible", "No available Web chat links": "Aucun lien de chat Web disponible",
"No backup": "Pas de sauvegarde", "No backup": "Pas de sauvegarde",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "sur 3 :", "of 3:": "sur 3 :",
"off": "désactivé", "off": "désactivé",
"Official": "Officiel", "Official": "Officiel",
"Official Claude Messages": "Messages Claude officiels",
"Official documentation": "Documentation officielle", "Official documentation": "Documentation officielle",
"Official Gemini from OpenAI Chat": "Gemini officiel depuis OpenAI Chat",
"Official Gemini Native": "Gemini natif officiel",
"Official OpenAI Chat": "OpenAI Chat officiel",
"Official OpenAI Embeddings": "Embeddings OpenAI officiels",
"Official OpenAI Images": "Images OpenAI officielles",
"Official OpenAI Responses": "Responses OpenAI officiel",
"Official Repository": "Dépôt officiel", "Official Repository": "Dépôt officiel",
"Official Sync": "Synchronisation Officielle", "Official Sync": "Synchronisation Officielle",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "Ouvrir les paramètres du thème", "Open theme settings": "Ouvrir les paramètres du thème",
"Open weights": "Poids ouverts", "Open weights": "Poids ouverts",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "OpenAI Audio Speech",
"OpenAI Audio Transcriptions": "OpenAI Audio Transcriptions",
"OpenAI Audio Translations": "OpenAI Audio Translations",
"OpenAI Chat Completions": "OpenAI Chat Completions",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat vers Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat vers Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat vers OpenAI Responses",
"OpenAI Completions": "OpenAI Completions",
"OpenAI Compatible": "Compatible OpenAI", "OpenAI Compatible": "Compatible OpenAI",
"OpenAI Embeddings": "OpenAI Embeddings",
"OpenAI Image Edits": "OpenAI Image Edits",
"OpenAI Image Generations": "OpenAI Image Generations",
"OpenAI Organization": "Organisation OpenAI", "OpenAI Organization": "Organisation OpenAI",
"OpenAI Organization ID (optional)": "Identifiant d'organisation OpenAI (optionnel)", "OpenAI Organization ID (optional)": "Identifiant d'organisation OpenAI (optionnel)",
"OpenAI Realtime": "OpenAI Realtime",
"OpenAI Rerank": "OpenAI Rerank",
"OpenAI Responses": "OpenAI Responses",
"OpenAI Responses Compact": "OpenAI Responses Compact",
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.", "OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.",
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.", "OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "Le code QR n'est pas configuré. Veuillez contacter le support.", "QR code is not configured. Please contact support.": "Le code QR n'est pas configuré. Veuillez contacter le support.",
"Quantity": "Quantité", "Quantity": "Quantité",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "Solde des requêtes", "Query Balance": "Solde des requêtes",
"Query Param": "Paramètre de requête", "Query Param": "Paramètre de requête",
"Querying...": "Recherche en cours...", "Querying...": "Recherche en cours...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "Résoudre les conflits", "Resolve Conflicts": "Résoudre les conflits",
"Resource Configuration": "Configuration des ressources", "Resource Configuration": "Configuration des ressources",
"Response": "Réponse", "Response": "Réponse",
"Response time: {{duration}}": "Temps de réponse : {{duration}}",
"Response Time": "Temps de réponse", "Response Time": "Temps de réponse",
"Response time: {{duration}}": "Temps de réponse : {{duration}}",
"Responses API Version": "Version de l'API des réponses", "Responses API Version": "Version de l'API des réponses",
"Restore defaults": "Restaurer les paramètres par défaut", "Restore defaults": "Restaurer les paramètres par défaut",
"Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)", "Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "Description de la route", "Route Description": "Description de la route",
"Route is required": "La route est requise", "Route is required": "La route est requise",
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit", "Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
"Routes": "Routes",
"Routing & Overrides": "Routage et surcharges", "Routing & Overrides": "Routage et surcharges",
"Routing Strategy": "Stratégie de routage", "Routing Strategy": "Stratégie de routage",
"Rows per page": "Lignes par page", "Rows per page": "Lignes par page",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "Mises à jour des modèles en amont", "Upstream Model Updates": "Mises à jour des modèles en amont",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Mises à jour des modèles en amont appliquées : {{added}} ajoutés, {{removed}} supprimés, {{ignored}} ignorés cette fois, {{totalIgnored}} modèles ignorés au total", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Mises à jour des modèles en amont appliquées : {{added}} ajoutés, {{removed}} supprimés, {{ignored}} ignorés cette fois, {{totalIgnored}} modèles ignorés au total",
"Upstream price sync": "Synchronisation amont des prix", "Upstream price sync": "Synchronisation amont des prix",
"Upstream path": "Chemin amont",
"Upstream path is required": "Le chemin amont est requis",
"Upstream path must be a full URL or a path starting with /": "Le chemin amont doit être une URL complète ou un chemin commençant par /",
"Upstream prices fetched successfully": "Prix amont récupérés avec succès", "Upstream prices fetched successfully": "Prix amont récupérés avec succès",
"Upstream ratios fetched successfully": "Ratios en amont récupérés avec succès", "Upstream ratios fetched successfully": "Ratios en amont récupérés avec succès",
"Upstream Request ID": "ID de requête en amont", "Upstream Request ID": "ID de requête en amont",
"Upstream Response": "Réponse amont", "Upstream Response": "Réponse amont",
"upstream services integrated": "services en amont intégrés", "upstream services integrated": "services en amont intégrés",
"Upstream Updates": "Mises à jour en amont", "Upstream Updates": "Mises à jour en amont",
"Upstream URL": "URL amont",
"Upstream URL must be a full URL": "L’URL amont doit etre une URL complete",
"uptime": "disponibilité", "uptime": "disponibilité",
"Uptime": "Temps de fonctionnement", "Uptime": "Temps de fonctionnement",
"Uptime (30d)": "Disponibilité (30 j)", "Uptime (30d)": "Disponibilité (30 j)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "Prix en USD par million de tokens d’entrée.", "USD price per 1M input tokens.": "Prix en USD par million de tokens d’entrée.",
"USD price per 1M tokens.": "Prix en USD par million de tokens.", "USD price per 1M tokens.": "Prix en USD par million de tokens.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Utilisez +: pour ajouter un groupe, -: pour supprimer un groupe sélectionnable par défaut, ou aucun préfixe pour annexer un groupe.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Utilisez +: pour ajouter un groupe, -: pour supprimer un groupe sélectionnable par défaut, ou aucun préfixe pour annexer un groupe.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Utilisez un chemin pour l’ajouter à la Base URL du canal, ou saisissez une URL complète pour remplacer la Base URL pour cette route.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Utilisez un navigateur ou un appareil compatible avec l'authentification biométrique ou une clé de sécurité pour enregistrer une clé d'accès (Passkey).", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Utilisez un navigateur ou un appareil compatible avec l'authentification biométrique ou une clé de sécurité pour enregistrer une clé d'accès (Passkey).",
"Use authenticator code": "Utiliser le code de l'authentificateur", "Use authenticator code": "Utiliser le code de l'authentificateur",
"Use backup code": "Utiliser un code de secours", "Use backup code": "Utiliser un code de secours",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "Utilisé", "Used": "Utilisé",
"Used / Remaining": "Utilisé / Restant", "Used / Remaining": "Utilisé / Restant",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Utilisé comme SuccessURL sur le nouveau produit. Une confirmation vous sera demandée si ce champ est laissé vide.", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Utilisé comme SuccessURL sur le nouveau produit. Une confirmation vous sera demandée si ce champ est laissé vide.",
"Used by route auth templates": "Utilise par les modeles auth de route",
"Used for load balancing. Higher weight = more requests": "Utilisé pour l'équilibrage de charge. Poids plus élevé = plus de requêtes", "Used for load balancing. Higher weight = more requests": "Utilisé pour l'équilibrage de charge. Poids plus élevé = plus de requêtes",
"Used in URLs and API routes": "Utilisé dans les URLs et les routes API", "Used in URLs and API routes": "Utilisé dans les URLs et les routes API",
"Used Quota": "Quota utilisé", "Used Quota": "Quota utilisé",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Lorsque activé, les callbacks Midjourney sont acceptés (révèle l'IP du serveur).", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "Lorsque activé, les callbacks Midjourney sont acceptés (révèle l'IP du serveur).",
"When enabled, newly created tokens start in the first auto group.": "Lorsqu'elle est activée, les jetons nouvellement créés commencent dans le premier groupe automatique.", "When enabled, newly created tokens start in the first auto group.": "Lorsqu'elle est activée, les jetons nouvellement créés commencent dans le premier groupe automatique.",
"When enabled, prompts are scanned before reaching upstream models.": "Lorsqu'elle est activée, les invites sont scannées avant d'atteindre les modèles en amont.", "When enabled, prompts are scanned before reaching upstream models.": "Lorsqu'elle est activée, les invites sont scannées avant d'atteindre les modèles en amont.",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "Lorsque cette option est activée, les requêtes qui ne correspondent à aucune route avancée sont transférées vers l'URL de base du canal. Lorsqu'elle est désactivée, les requêtes sans correspondance renvoient une erreur.",
"When enabled, the store field will be blocked": "Lorsqu'il est activé, le champ de la boutique sera bloqué", "When enabled, the store field will be blocked": "Lorsqu'il est activé, le champ de la boutique sera bloqué",
"When enabled, users can pick this group when creating tokens.": "Une fois activé, les utilisateurs peuvent choisir ce groupe lors de la création de jetons.", "When enabled, users can pick this group when creating tokens.": "Une fois activé, les utilisateurs peuvent choisir ce groupe lors de la création de jetons.",
"When enabled, violation requests will incur additional charges.": "Lorsqu'activé, les requêtes en violation entraîneront des frais supplémentaires.", "When enabled, violation requests will incur additional charges.": "Lorsqu'activé, les requêtes en violation entraîneront des frais supplémentaires.",
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "プロバイダーを追加", "Add Provider": "プロバイダーを追加",
"Add Quota": "クォータを追加", "Add Quota": "クォータを追加",
"Add ratio override": "倍率オーバーライドを追加", "Add ratio override": "倍率オーバーライドを追加",
"Add route": "ルートを追加",
"Add Row": "行を追加", "Add Row": "行を追加",
"Add Rule": "ルールを追加", "Add Rule": "ルールを追加",
"Add rule group": "ルールグループを追加", "Add rule group": "ルールグループを追加",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "管理者ユーザー名", "Administrator username": "管理者ユーザー名",
"Advanced": "高度な設定", "Advanced": "高度な設定",
"Advanced Configuration": "詳細設定", "Advanced Configuration": "詳細設定",
"Advanced Custom": "高度なカスタム",
"Advanced custom configuration is required": "高度なカスタム設定が必要です",
"Advanced custom configuration requires at least one route or fallback": "高度なカスタム設定には少なくとも1つのルートまたはフォールバックが必要です",
"Advanced Custom Routes": "高度なカスタムルート",
"Advanced options": "詳細オプション", "Advanced options": "詳細オプション",
"Advanced Options": "詳細オプション", "Advanced Options": "詳細オプション",
"Advanced platform configuration.": "高度なプラットフォーム設定。", "Advanced platform configuration.": "高度なプラットフォーム設定。",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "回答", "Answer": "回答",
"Answers for common access and billing questions": "アクセスと請求に関するよくある質問への回答", "Answers for common access and billing questions": "アクセスと請求に関するよくある質問への回答",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages から OpenAI Chat",
"Any Match (OR)": "いずれか一致(OR)", "Any Match (OR)": "いずれか一致(OR)",
"API": "API", "API": "API",
"API Access": "API アクセス", "API Access": "API アクセス",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "音声プレビュー", "Audio Preview": "音声プレビュー",
"Audio ratio": "音声倍率", "Audio ratio": "音声倍率",
"Audio Tokens": "音声トークン", "Audio Tokens": "音声トークン",
"Auth": "認証",
"Auth configured": "認証設定済み", "Auth configured": "認証設定済み",
"Auth name": "認証名",
"Auth name is required": "認証名は必須です",
"Auth Style": "認証スタイル", "Auth Style": "認証スタイル",
"Auth type is invalid": "認証タイプが無効です",
"Auth value": "認証値",
"Auth value is required": "認証値は必須です",
"auth.resetPasswordConfirm.backToLogin": "ログインに戻る", "auth.resetPasswordConfirm.backToLogin": "ログインに戻る",
"auth.resetPasswordConfirm.confirm": "パスワードリセットを確認", "auth.resetPasswordConfirm.confirm": "パスワードリセットを確認",
"auth.resetPasswordConfirm.description": "新しいパスワードを生成するには、リセット要求を確認してください。", "auth.resetPasswordConfirm.description": "新しいパスワードを生成するには、リセット要求を確認してください。",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "このアカウント向けの基本レート制限ウィンドウ。", "Base rate limit windows for this account.": "このアカウント向けの基本レート制限ウィンドウ。",
"Base URL": "ベースURL", "Base URL": "ベースURL",
"Base URL is required for this channel type": "このチャネルタイプには Base URL が必要です", "Base URL is required for this channel type": "このチャネルタイプには Base URL が必要です",
"Base URL is required when an advanced route uses an upstream path": "高度なルートで上流パスを使う場合は Base URL が必要です",
"Base URL is required when fallback is enabled": "フォールバックを有効にする場合は Base URL が必要です",
"Base URL of your Uptime Kuma instance": "Uptime KumaインスタンスのベースURL", "Base URL of your Uptime Kuma instance": "Uptime KumaインスタンスのベースURL",
"Basic Authentication": "基本認証", "Basic Authentication": "基本認証",
"Basic Configuration": "基本設定", "Basic Configuration": "基本設定",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "保存すると変更は設定ドラフトに書き込まれます。", "Changes are written to the settings draft on save.": "保存すると変更は設定ドラフトに書き込まれます。",
"Changing...": "変更中...", "Changing...": "変更中...",
"Channel": "チャネル", "Channel": "チャネル",
"Channel {{name}}": "チャネル {{name}}",
"Channel {{name}} model {{model}}": "チャネル {{name}} モデル {{model}}",
"Channel Affinity": "チャネルアフィニティ", "Channel Affinity": "チャネルアフィニティ",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット", "Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "チャネルキー", "Channel key": "チャネルキー",
"Channel key unlocked": "チャネルキーが解除されました", "Channel key unlocked": "チャネルキーが解除されました",
"Channel models": "チャネルモデル", "Channel models": "チャネルモデル",
"Channel {{name}}": "チャネル {{name}}",
"Channel {{name}} model {{model}}": "チャネル {{name}} モデル {{model}}",
"Channel name is required": "チャネル名が必要です", "Channel name is required": "チャネル名が必要です",
"Channel test completed": "チャネルテストが完了しました", "Channel test completed": "チャネルテストが完了しました",
"Channel type is required": "チャネルタイプが必要です", "Channel type is required": "チャネルタイプが必要です",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "クラシック(旧フロントエンド)", "Classic (Legacy Frontend)": "クラシック(旧フロントエンド)",
"Claude": "Claude", "Claude": "Claude",
"Claude CLI Header Passthrough": "Claude CLI ヘッダーパススルー", "Claude CLI Header Passthrough": "Claude CLI ヘッダーパススルー",
"Claude Messages": "Claude メッセージ",
"Clean": "問題なし", "Clean": "問題なし",
"Clean history logs": "履歴ログをクリーンアップ", "Clean history logs": "履歴ログをクリーンアップ",
"Clean logs": "ログをクリア", "Clean logs": "ログをクリア",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "ツールごとの単価($/1K 回)を設定します。リクエスト課金モデルでは追加工具料金はかかりません。", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "ツールごとの単価($/1K 回)を設定します。リクエスト課金モデルでは追加工具料金はかかりません。",
"Configure pricing ratios for a specific model.": "特定のモデルの料金比率を設定します。", "Configure pricing ratios for a specific model.": "特定のモデルの料金比率を設定します。",
"Configure rate limiting rules for a specific user group.": "特定のユーザーグループのレート制限ルールを設定します。", "Configure rate limiting rules for a specific user group.": "特定のユーザーグループのレート制限ルールを設定します。",
"Configure routes": "ルートを設定",
"Configure the ratio for this group.": "このグループの比率を設定します。", "Configure the ratio for this group.": "このグループの比率を設定します。",
"Configure upstream providers and routing.": "アップストリームプロバイダーとルーティングを設定。", "Configure upstream providers and routing.": "アップストリームプロバイダーとルーティングを設定。",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "USD 建てのチャージ用に Waffo Pancake のホスト型チェックアウト連携を設定", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "USD 建てのチャージ用に Waffo Pancake のホスト型チェックアウト連携を設定",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "content内のreasoning_contentを<think>タグに変換", "Convert reasoning_content to <think> tag in content": "content内のreasoning_contentを<think>タグに変換",
"Convert string to lowercase": "文字列を小文字に変換", "Convert string to lowercase": "文字列を小文字に変換",
"Convert string to uppercase": "文字列を大文字に変換", "Convert string to uppercase": "文字列を大文字に変換",
"Converter": "コンバーター",
"Converter does not match incoming path": "コンバーターが受信パスと一致しません",
"Converter is not registered": "コンバーターが登録されていません",
"Copied": "コピーしました", "Copied": "コピーしました",
"Copied {{count}} key(s)": "{{count}} 件のキーをコピーしました", "Copied {{count}} key(s)": "{{count}} 件のキーをコピーしました",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "チャネルを複製しました(元 ID: {{sourceId}})→ {{name}}(新 ID: {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "チャネルを複製しました(元 ID: {{sourceId}})→ {{name}}(新 ID: {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "デフォルト / 範囲", "Default / range": "デフォルト / 範囲",
"Default API Version *": "デフォルトのAPIバージョン *", "Default API Version *": "デフォルトのAPIバージョン *",
"Default API version for this channel": "このチャネルのデフォルトのAPIバージョン", "Default API version for this channel": "このチャネルのデフォルトのAPIバージョン",
"Default Bearer": "既定の Bearer",
"Default Collapse Sidebar": "デフォルトのサイドバー折りたたみ", "Default Collapse Sidebar": "デフォルトのサイドバー折りたたみ",
"Default consumption chart": "デフォルトの消費チャート", "Default consumption chart": "デフォルトの消費チャート",
"Default Max Tokens": "デフォルトの最大トークン", "Default Max Tokens": "デフォルトの最大トークン",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "ユーザーの更新に失敗しました", "Failed to update user": "ユーザーの更新に失敗しました",
"Failure keywords": "失敗キーワード", "Failure keywords": "失敗キーワード",
"Fair": "公平", "Fair": "公平",
"Fallback": "フォールバック",
"Fallback base URL": "フォールバック Base URL",
"Fallback routing": "フォールバックルーティング",
"Fallback tier": "フォールバック段階", "Fallback tier": "フォールバック段階",
"FAQ": "FAQ", "FAQ": "FAQ",
"FAQ added. Click \"Save Settings\" to apply.": "FAQ が追加されました。「設定を保存」をクリックして適用してください。", "FAQ added. Click \"Save Settings\" to apply.": "FAQ が追加されました。「設定を保存」をクリックして適用してください。",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC 実行完了", "GC executed": "GC 実行完了",
"GC execution failed": "GC 実行失敗", "GC execution failed": "GC 実行失敗",
"Gemini": "Gemini", "Gemini": "Gemini",
"Gemini Batch Embed Contents": "Gemini 一括埋め込みコンテンツ",
"Gemini Embed Content": "Gemini 埋め込みコンテンツ",
"Gemini Generate Content": "Gemini コンテンツ生成",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content から OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K", "Gemini Image 4K": "Gemini Image 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "アダプターが無効になっていても、Geminiは思考モードを自動検出します。価格設定と予算編成をより細かく制御する必要がある場合にのみ、これを有効にしてください。", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "アダプターが無効になっていても、Geminiは思考モードを自動検出します。価格設定と予算編成をより細かく制御する必要がある場合にのみ、これを有効にしてください。",
"General": "一般", "General": "一般",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "ルール名を含む", "Include Rule Name": "ルール名を含む",
"Includes request rules": "リクエストルールを含む", "Includes request rules": "リクエストルールを含む",
"Including failed requests, 0 = unlimited": "失敗したリクエストを含む、0 = 無制限", "Including failed requests, 0 = unlimited": "失敗したリクエストを含む、0 = 無制限",
"Incoming path": "受信パス",
"Incoming path is required": "受信パスは必須です",
"Incoming path must be unique": "受信パスは一意である必要があります",
"Incoming path must not include query": "受信パスに query を含めることはできません",
"Incoming path must start with /": "受信パスは / で始める必要があります",
"Incomplete": "未完了",
"Increased user quota by {{quota}}": "ユーザーのクォータを {{quota}} 増やしました", "Increased user quota by {{quota}}": "ユーザーのクォータを {{quota}} 増やしました",
"Index": "インデックス", "Index": "インデックス",
"Initial quota given to new users": "新規ユーザーに付与される初期クォータ", "Initial quota given to new users": "新規ユーザーに付与される初期クォータ",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "モード", "Mode": "モード",
"model": "モデル", "model": "モデル",
"Model": "モデル", "Model": "モデル",
"Model {{model}}": "モデル {{model}}",
"Model Access": "モデルアクセス", "Model Access": "モデルアクセス",
"Model Analytics": "モデル分析", "Model Analytics": "モデル分析",
"Model Analytics Defaults": "モデル分析のデフォルト設定", "Model Analytics Defaults": "モデル分析のデフォルト設定",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "モデルマッピングは文字列値を持つ JSON オブジェクトである必要があります", "Model mapping must be a JSON object with string values": "モデルマッピングは文字列値を持つ JSON オブジェクトである必要があります",
"Model mapping must be valid JSON": "モデルマッピングは有効な JSON である必要があります", "Model mapping must be valid JSON": "モデルマッピングは有効な JSON である必要があります",
"Model mapping values must be strings": "モデルマッピングの値は文字列である必要があります", "Model mapping values must be strings": "モデルマッピングの値は文字列である必要があります",
"Model {{model}}": "モデル {{model}}",
"Model name": "モデル名", "Model name": "モデル名",
"Model Name": "モデル名", "Model Name": "モデル名",
"Model Name *": "モデル名 *", "Model Name *": "モデル名 *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "ランキングで最も急上昇しているモデル", "Models climbing the leaderboard fastest": "ランキングで最も急上昇しているモデル",
"Models directory": "モデルディレクトリ", "Models directory": "モデルディレクトリ",
"Models Directory": "モデルディレクトリ", "Models Directory": "モデルディレクトリ",
"Models exposed by this channel": "このチャンネルで公開するモデル",
"Models fetched successfully": "モデルが正常に取得されました", "Models fetched successfully": "モデルが正常に取得されました",
"Models filled to form": "フォームにモデルが記入されました", "Models filled to form": "フォームにモデルが記入されました",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "ここに記載されたモデルは、-thinking / -nothinking サフィックスの自動付与・削除を行いません。", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "ここに記載されたモデルは、-thinking / -nothinking サフィックスの自動付与・削除を行いません。",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "名前、プロバイダー種別、利用可否。", "Name, provider type, and availability.": "名前、プロバイダー種別、利用可否。",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "ネイティブ形式", "Native format": "ネイティブ形式",
"Native forwarding": "ネイティブ転送",
"Need a redemption code?": "引き換えコードが必要ですか?", "Need a redemption code?": "引き換えコードが必要ですか?",
"Needs API key": "API キーが必要", "Needs API key": "API キーが必要",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "追加 (+:)、削除 (-:)、または使用可能なグループの追加を行うグループごとのルールを定義するネストされたJSON。", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "追加 (+:)、削除 (-:)、または使用可能なグループの追加を行うグループごとのルールを定義するネストされたJSON。",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "APIルートが設定されていません", "No API routes configured": "APIルートが設定されていません",
"No app usage data available for this model.": "このモデルのアプリ利用データはまだありません。", "No app usage data available for this model.": "このモデルのアプリ利用データはまだありません。",
"No apps match the selected filters": "条件に一致するアプリはありません", "No apps match the selected filters": "条件に一致するアプリはありません",
"No Auth": "認証なし",
"No available models": "利用可能なモデルがありません", "No available models": "利用可能なモデルがありません",
"No available Web chat links": "利用可能なWebチャットリンクがありません", "No available Web chat links": "利用可能なWebチャットリンクがありません",
"No backup": "バックアップなし", "No backup": "バックアップなし",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "3のうち:", "of 3:": "3のうち:",
"off": "オフ", "off": "オフ",
"Official": "公式", "Official": "公式",
"Official Claude Messages": "公式 Claude Messages",
"Official documentation": "公式ドキュメント", "Official documentation": "公式ドキュメント",
"Official Gemini from OpenAI Chat": "OpenAI Chat から公式 Gemini",
"Official Gemini Native": "公式 Gemini ネイティブ",
"Official OpenAI Chat": "公式 OpenAI Chat",
"Official OpenAI Embeddings": "公式 OpenAI Embeddings",
"Official OpenAI Images": "公式 OpenAI Images",
"Official OpenAI Responses": "公式 OpenAI Responses",
"Official Repository": "公式リポジトリ", "Official Repository": "公式リポジトリ",
"Official Sync": "公式同期", "Official Sync": "公式同期",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "テーマ設定を開く", "Open theme settings": "テーマ設定を開く",
"Open weights": "公開ウェイト", "Open weights": "公開ウェイト",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "OpenAI 音声生成",
"OpenAI Audio Transcriptions": "OpenAI 音声文字起こし",
"OpenAI Audio Translations": "OpenAI 音声翻訳",
"OpenAI Chat Completions": "OpenAI チャット補完",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat から Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat から Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat から OpenAI Responses",
"OpenAI Completions": "OpenAI 補完",
"OpenAI Compatible": "OpenAI互換", "OpenAI Compatible": "OpenAI互換",
"OpenAI Embeddings": "OpenAI 埋め込み",
"OpenAI Image Edits": "OpenAI 画像編集",
"OpenAI Image Generations": "OpenAI 画像生成",
"OpenAI Organization": "OpenAI組織", "OpenAI Organization": "OpenAI組織",
"OpenAI Organization ID (optional)": "OpenAI 組織 ID (オプション)", "OpenAI Organization ID (optional)": "OpenAI 組織 ID (オプション)",
"OpenAI Realtime": "OpenAI リアルタイム",
"OpenAI Rerank": "OpenAI 再ランク付け",
"OpenAI Responses": "OpenAI レスポンス",
"OpenAI Responses Compact": "OpenAI レスポンス圧縮",
"OpenAI, Anthropic, etc.": "OpenAI、Anthropicなど", "OpenAI, Anthropic, etc.": "OpenAI、Anthropicなど",
"OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Googleなど", "OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Googleなど",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "QRコードが設定されていません。サポートにお問い合わせください。", "QR code is not configured. Please contact support.": "QRコードが設定されていません。サポートにお問い合わせください。",
"Quantity": "数量", "Quantity": "数量",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "クエリ残高", "Query Balance": "クエリ残高",
"Query Param": "クエリパラメータ", "Query Param": "クエリパラメータ",
"Querying...": "クエリ中...", "Querying...": "クエリ中...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "競合を解決", "Resolve Conflicts": "競合を解決",
"Resource Configuration": "リソース設定", "Resource Configuration": "リソース設定",
"Response": "レスポンス", "Response": "レスポンス",
"Response time: {{duration}}": "応答時間: {{duration}}",
"Response Time": "応答時間", "Response Time": "応答時間",
"Response time: {{duration}}": "応答時間: {{duration}}",
"Responses API Version": "応答APIバージョン", "Responses API Version": "応答APIバージョン",
"Restore defaults": "既定に戻す", "Restore defaults": "既定に戻す",
"Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)", "Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "ルートの説明", "Route Description": "ルートの説明",
"Route is required": "ルートは必須です", "Route is required": "ルートは必須です",
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約", "Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
"Routes": "ルート",
"Routing & Overrides": "ルーティングと上書き", "Routing & Overrides": "ルーティングと上書き",
"Routing Strategy": "ルーティング戦略", "Routing Strategy": "ルーティング戦略",
"Rows per page": "ページあたりの行数", "Rows per page": "ページあたりの行数",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "上流モデルの更新", "Upstream Model Updates": "上流モデルの更新",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "上流モデル更新を処理しました:{{added}} 個追加、{{removed}} 個削除、今回 {{ignored}} 個無視、合計 {{totalIgnored}} 個の無視モデル", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "上流モデル更新を処理しました:{{added}} 個追加、{{removed}} 個削除、今回 {{ignored}} 個無視、合計 {{totalIgnored}} 個の無視モデル",
"Upstream price sync": "アップストリーム価格同期", "Upstream price sync": "アップストリーム価格同期",
"Upstream path": "上流パス",
"Upstream path is required": "上流パスは必須です",
"Upstream path must be a full URL or a path starting with /": "上流パスは完全な URL、または / で始まるパスである必要があります",
"Upstream prices fetched successfully": "上流価格を正常に取得しました", "Upstream prices fetched successfully": "上流価格を正常に取得しました",
"Upstream ratios fetched successfully": "アップストリーム比率が正常に取得されました", "Upstream ratios fetched successfully": "アップストリーム比率が正常に取得されました",
"Upstream Request ID": "上流リクエストID", "Upstream Request ID": "上流リクエストID",
"Upstream Response": "アップストリームレスポンス", "Upstream Response": "アップストリームレスポンス",
"upstream services integrated": "アップストリームサービス連携", "upstream services integrated": "アップストリームサービス連携",
"Upstream Updates": "アップストリーム更新", "Upstream Updates": "アップストリーム更新",
"Upstream URL": "上流 URL",
"Upstream URL must be a full URL": "上流 URL は完全な URL である必要があります",
"uptime": "稼働時間", "uptime": "稼働時間",
"Uptime": "稼働時間", "Uptime": "稼働時間",
"Uptime (30d)": "稼働率 (30 日)", "Uptime (30d)": "稼働率 (30 日)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "100万入力トークンあたりのUSD価格。", "USD price per 1M input tokens.": "100万入力トークンあたりのUSD価格。",
"USD price per 1M tokens.": "100万トークンあたりのUSD価格。", "USD price per 1M tokens.": "100万トークンあたりのUSD価格。",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "+: はグループ追加、-: はデフォルト選択可能グループの削除、接頭辞なしはグループ追記に使います。", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "+: はグループ追加、-: はデフォルト選択可能グループの削除、接頭辞なしはグループ追記に使います。",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "パスを入力するとチャネルの Base URL に追加されます。完全な URL を入力すると、このルートでは Base URL を使わずその URL を使用します。",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "生体認証またはセキュリティキーを備えた互換性のあるブラウザまたはデバイスを使用して、パスキーを登録してください。", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "生体認証またはセキュリティキーを備えた互換性のあるブラウザまたはデバイスを使用して、パスキーを登録してください。",
"Use authenticator code": "認証コードを使用", "Use authenticator code": "認証コードを使用",
"Use backup code": "バックアップコードを使用", "Use backup code": "バックアップコードを使用",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "使用済み", "Used": "使用済み",
"Used / Remaining": "使用済み / 残り", "Used / Remaining": "使用済み / 残り",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "新しい商品の SuccessURL として使用されます。空のままにすると確認を求められます。", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "新しい商品の SuccessURL として使用されます。空のままにすると確認を求められます。",
"Used by route auth templates": "ルート認証テンプレートで使用",
"Used for load balancing. Higher weight = more requests": "ロードバランシングに使用されます。重みが高いほどリクエスト数が増えます", "Used for load balancing. Higher weight = more requests": "ロードバランシングに使用されます。重みが高いほどリクエスト数が増えます",
"Used in URLs and API routes": "URLとAPIルートで使用されます", "Used in URLs and API routes": "URLとAPIルートで使用されます",
"Used Quota": "使用済みクォータ", "Used Quota": "使用済みクォータ",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "有効にすると、Midjourney のコールバックを受け入れます (サーバーの IP を公開します)。", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "有効にすると、Midjourney のコールバックを受け入れます (サーバーの IP を公開します)。",
"When enabled, newly created tokens start in the first auto group.": "有効にすると、新しく作成されたトークンは最初の自動グループで開始されます。", "When enabled, newly created tokens start in the first auto group.": "有効にすると、新しく作成されたトークンは最初の自動グループで開始されます。",
"When enabled, prompts are scanned before reaching upstream models.": "有効にすると、プロンプトはアップストリームモデルに到達する前にスキャンされます。", "When enabled, prompts are scanned before reaching upstream models.": "有効にすると、プロンプトはアップストリームモデルに到達する前にスキャンされます。",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "有効にすると、高度なルートに一致しないリクエストはチャネルのベース URL へ転送されます。無効の場合、一致しないリクエストはエラーを返します。",
"When enabled, the store field will be blocked": "有効にすると、ストアフィールドはブロックされます", "When enabled, the store field will be blocked": "有効にすると、ストアフィールドはブロックされます",
"When enabled, users can pick this group when creating tokens.": "有効にすると、ユーザーはトークン作成時にこのグループを選択できます。", "When enabled, users can pick this group when creating tokens.": "有効にすると、ユーザーはトークン作成時にこのグループを選択できます。",
"When enabled, violation requests will incur additional charges.": "有効にすると、違反リクエストに追加料金が発生します。", "When enabled, violation requests will incur additional charges.": "有効にすると、違反リクエストに追加料金が発生します。",
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "Добавить поставщика", "Add Provider": "Добавить поставщика",
"Add Quota": "Добавить квоту", "Add Quota": "Добавить квоту",
"Add ratio override": "Добавить переопределение коэффициента", "Add ratio override": "Добавить переопределение коэффициента",
"Add route": "Добавить маршрут",
"Add Row": "Добавить строку", "Add Row": "Добавить строку",
"Add Rule": "Добавить правило", "Add Rule": "Добавить правило",
"Add rule group": "Добавить группу правил", "Add rule group": "Добавить группу правил",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "Имя пользователя администратора", "Administrator username": "Имя пользователя администратора",
"Advanced": "Расширенные", "Advanced": "Расширенные",
"Advanced Configuration": "Расширенная конфигурация", "Advanced Configuration": "Расширенная конфигурация",
"Advanced Custom": "Расширенный пользовательский",
"Advanced custom configuration is required": "Требуется расширенная пользовательская конфигурация",
"Advanced custom configuration requires at least one route or fallback": "Расширенной пользовательской конфигурации нужен хотя бы один маршрут или fallback",
"Advanced Custom Routes": "Расширенные пользовательские маршруты",
"Advanced options": "Расширенные параметры", "Advanced options": "Расширенные параметры",
"Advanced Options": "Расширенные параметры", "Advanced Options": "Расширенные параметры",
"Advanced platform configuration.": "Расширенная настройка платформы.", "Advanced platform configuration.": "Расширенная настройка платформы.",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "Ответ", "Answer": "Ответ",
"Answers for common access and billing questions": "Ответы на частые вопросы о доступе и оплате", "Answers for common access and billing questions": "Ответы на частые вопросы о доступе и оплате",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages в OpenAI Chat",
"Any Match (OR)": "Любое совпадение (OR)", "Any Match (OR)": "Любое совпадение (OR)",
"API": "API", "API": "API",
"API Access": "Доступ к API", "API Access": "Доступ к API",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "Предпросмотр аудио", "Audio Preview": "Предпросмотр аудио",
"Audio ratio": "Коэффициент аудио", "Audio ratio": "Коэффициент аудио",
"Audio Tokens": "Аудио токены", "Audio Tokens": "Аудио токены",
"Auth": "Аутентификация",
"Auth configured": "Аутентификация настроена", "Auth configured": "Аутентификация настроена",
"Auth name": "Имя auth",
"Auth name is required": "Имя auth обязательно",
"Auth Style": "Стиль аутентификации", "Auth Style": "Стиль аутентификации",
"Auth type is invalid": "Тип auth недействителен",
"Auth value": "Значение auth",
"Auth value is required": "Значение auth обязательно",
"auth.resetPasswordConfirm.backToLogin": "Вернуться ко входу", "auth.resetPasswordConfirm.backToLogin": "Вернуться ко входу",
"auth.resetPasswordConfirm.confirm": "Подтвердить сброс пароля", "auth.resetPasswordConfirm.confirm": "Подтвердить сброс пароля",
"auth.resetPasswordConfirm.description": "Подтвердите запрос на сброс, чтобы создать новый пароль.", "auth.resetPasswordConfirm.description": "Подтвердите запрос на сброс, чтобы создать новый пароль.",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "Окна базовых лимитов для этого аккаунта.", "Base rate limit windows for this account.": "Окна базовых лимитов для этого аккаунта.",
"Base URL": "Адрес API", "Base URL": "Адрес API",
"Base URL is required for this channel type": "Для этого типа канала требуется Base URL", "Base URL is required for this channel type": "Для этого типа канала требуется Base URL",
"Base URL is required when an advanced route uses an upstream path": "Base URL требуется, когда расширенный маршрут использует путь upstream",
"Base URL is required when fallback is enabled": "Base URL обязателен при включенном fallback",
"Base URL of your Uptime Kuma instance": "Базовый URL вашего экземпляра Uptime Kuma", "Base URL of your Uptime Kuma instance": "Базовый URL вашего экземпляра Uptime Kuma",
"Basic Authentication": "Базовая аутентификация", "Basic Authentication": "Базовая аутентификация",
"Basic Configuration": "Базовая конфигурация", "Basic Configuration": "Базовая конфигурация",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "Изменения будут записаны в черновик настроек при сохранении.", "Changes are written to the settings draft on save.": "Изменения будут записаны в черновик настроек при сохранении.",
"Changing...": "Изменение...", "Changing...": "Изменение...",
"Channel": "Канал", "Channel": "Канал",
"Channel {{name}}": "Канал {{name}}",
"Channel {{name}} model {{model}}": "Канал {{name}}, модель {{model}}",
"Channel Affinity": "Привязка к каналу", "Channel Affinity": "Привязка к каналу",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream", "Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "Ключ канала", "Channel key": "Ключ канала",
"Channel key unlocked": "Ключ канала разблокирован", "Channel key unlocked": "Ключ канала разблокирован",
"Channel models": "Модели каналов", "Channel models": "Модели каналов",
"Channel {{name}}": "Канал {{name}}",
"Channel {{name}} model {{model}}": "Канал {{name}}, модель {{model}}",
"Channel name is required": "Имя канала обязательно", "Channel name is required": "Имя канала обязательно",
"Channel test completed": "Тест канала завершён", "Channel test completed": "Тест канала завершён",
"Channel type is required": "Тип канала обязателен", "Channel type is required": "Тип канала обязателен",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "Классический (Старый интерфейс)", "Classic (Legacy Frontend)": "Классический (Старый интерфейс)",
"Claude": "Клод", "Claude": "Клод",
"Claude CLI Header Passthrough": "Проброс заголовков Claude CLI", "Claude CLI Header Passthrough": "Проброс заголовков Claude CLI",
"Claude Messages": "Сообщения Claude",
"Clean": "Без конфликта", "Clean": "Без конфликта",
"Clean history logs": "Очистить журналы истории", "Clean history logs": "Очистить журналы истории",
"Clean logs": "Очистить логи", "Clean logs": "Очистить логи",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Настройте стоимость единицы на инструмент ($/1K вызовов). Для моделей с оплатой за запрос доп. плата за инструменты не взимается.", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Настройте стоимость единицы на инструмент ($/1K вызовов). Для моделей с оплатой за запрос доп. плата за инструменты не взимается.",
"Configure pricing ratios for a specific model.": "Настроить коэффициенты ценообразования для конкретной модели.", "Configure pricing ratios for a specific model.": "Настроить коэффициенты ценообразования для конкретной модели.",
"Configure rate limiting rules for a specific user group.": "Настроить правила ограничения скорости для конкретной группы пользователей.", "Configure rate limiting rules for a specific user group.": "Настроить правила ограничения скорости для конкретной группы пользователей.",
"Configure routes": "Настроить маршруты",
"Configure the ratio for this group.": "Настроить коэффициент для этой группы.", "Configure the ratio for this group.": "Настроить коэффициент для этой группы.",
"Configure upstream providers and routing.": "Настроить провайдеров верхнего уровня и маршрутизацию.", "Configure upstream providers and routing.": "Настроить провайдеров верхнего уровня и маршрутизацию.",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Настроить хостовую интеграцию Waffo Pancake (hosted checkout) для пополнений в USD", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Настроить хостовую интеграцию Waffo Pancake (hosted checkout) для пополнений в USD",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "Преобразовать reasoning_content в тег <think> в content", "Convert reasoning_content to <think> tag in content": "Преобразовать reasoning_content в тег <think> в content",
"Convert string to lowercase": "Преобразовать строку в нижний регистр", "Convert string to lowercase": "Преобразовать строку в нижний регистр",
"Convert string to uppercase": "Преобразовать строку в верхний регистр", "Convert string to uppercase": "Преобразовать строку в верхний регистр",
"Converter": "Конвертер",
"Converter does not match incoming path": "Конвертер не соответствует входящему пути",
"Converter is not registered": "Конвертер не зарегистрирован",
"Copied": "Скопировано", "Copied": "Скопировано",
"Copied {{count}} key(s)": "Скопировано {{count}} ключ(ей)", "Copied {{count}} key(s)": "Скопировано {{count}} ключ(ей)",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Канал скопирован (исходный ID: {{sourceId}}) в {{name}} (новый ID: {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Канал скопирован (исходный ID: {{sourceId}}) в {{name}} (новый ID: {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "По умолчанию / диапазон", "Default / range": "По умолчанию / диапазон",
"Default API Version *": "Версия API по умолчанию *", "Default API Version *": "Версия API по умолчанию *",
"Default API version for this channel": "Версия API по умолчанию для этого канала", "Default API version for this channel": "Версия API по умолчанию для этого канала",
"Default Bearer": "Bearer по умолчанию",
"Default Collapse Sidebar": "Сворачивать боковую панель по умолчанию", "Default Collapse Sidebar": "Сворачивать боковую панель по умолчанию",
"Default consumption chart": "График потребления по умолчанию", "Default consumption chart": "График потребления по умолчанию",
"Default Max Tokens": "Максимальное количество токенов по умолчанию", "Default Max Tokens": "Максимальное количество токенов по умолчанию",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "Не удалось обновить пользователя", "Failed to update user": "Не удалось обновить пользователя",
"Failure keywords": "Ключевые слова сбоя", "Failure keywords": "Ключевые слова сбоя",
"Fair": "Удовлетворительно", "Fair": "Удовлетворительно",
"Fallback": "Резерв",
"Fallback base URL": "Base URL fallback",
"Fallback routing": "Fallback-маршрутизация",
"Fallback tier": "Резервный уровень", "Fallback tier": "Резервный уровень",
"FAQ": "Часто задаваемые вопросы", "FAQ": "Часто задаваемые вопросы",
"FAQ added. Click \"Save Settings\" to apply.": "FAQ добавлен. Нажмите \"Сохранить настройки\" чтобы применить.", "FAQ added. Click \"Save Settings\" to apply.": "FAQ добавлен. Нажмите \"Сохранить настройки\" чтобы применить.",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC выполнен", "GC executed": "GC выполнен",
"GC execution failed": "Ошибка выполнения GC", "GC execution failed": "Ошибка выполнения GC",
"Gemini": "Gemini", "Gemini": "Gemini",
"Gemini Batch Embed Contents": "Пакетные embeddings Gemini",
"Gemini Embed Content": "Embedding контента Gemini",
"Gemini Generate Content": "Генерация контента Gemini",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content в OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K", "Gemini Image 4K": "Gemini Image 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini продолжит автоматически определять режим мышления, даже если адаптер отключен. Включайте это только тогда, когда вам нужен более тонкий контроль над ценообразованием и бюджетированием.", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini продолжит автоматически определять режим мышления, даже если адаптер отключен. Включайте это только тогда, когда вам нужен более тонкий контроль над ценообразованием и бюджетированием.",
"General": "Общие", "General": "Общие",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "Включить имя правила", "Include Rule Name": "Включить имя правила",
"Includes request rules": "Включает правила запросов", "Includes request rules": "Включает правила запросов",
"Including failed requests, 0 = unlimited": "Включая неудачные запросы, 0 = без ограничений", "Including failed requests, 0 = unlimited": "Включая неудачные запросы, 0 = без ограничений",
"Incoming path": "Входящий путь",
"Incoming path is required": "Входящий путь обязателен",
"Incoming path must be unique": "Входящий путь должен быть уникальным",
"Incoming path must not include query": "Входящий путь не должен содержать query",
"Incoming path must start with /": "Входящий путь должен начинаться с /",
"Incomplete": "Не завершено",
"Increased user quota by {{quota}}": "Квота пользователя увеличена на {{quota}}", "Increased user quota by {{quota}}": "Квота пользователя увеличена на {{quota}}",
"Index": "Индекс", "Index": "Индекс",
"Initial quota given to new users": "Начальная квота, предоставляемая новым пользователям", "Initial quota given to new users": "Начальная квота, предоставляемая новым пользователям",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "Режим", "Mode": "Режим",
"model": "модель", "model": "модель",
"Model": "Модель", "Model": "Модель",
"Model {{model}}": "Модель {{model}}",
"Model Access": "Доступ к моделям", "Model Access": "Доступ к моделям",
"Model Analytics": "Аналитика моделей", "Model Analytics": "Аналитика моделей",
"Model Analytics Defaults": "Настройки аналитики моделей по умолчанию", "Model Analytics Defaults": "Настройки аналитики моделей по умолчанию",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "Сопоставление моделей должно быть JSON-объектом со строковыми значениями", "Model mapping must be a JSON object with string values": "Сопоставление моделей должно быть JSON-объектом со строковыми значениями",
"Model mapping must be valid JSON": "Сопоставление моделей должно быть допустимым JSON", "Model mapping must be valid JSON": "Сопоставление моделей должно быть допустимым JSON",
"Model mapping values must be strings": "Значения сопоставления моделей должны быть строками", "Model mapping values must be strings": "Значения сопоставления моделей должны быть строками",
"Model {{model}}": "Модель {{model}}",
"Model name": "Имя модели", "Model name": "Имя модели",
"Model Name": "Название модели", "Model Name": "Название модели",
"Model Name *": "Имя модели *", "Model Name *": "Имя модели *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "Модели, быстрее всех растущие в рейтинге", "Models climbing the leaderboard fastest": "Модели, быстрее всех растущие в рейтинге",
"Models directory": "Каталог моделей", "Models directory": "Каталог моделей",
"Models Directory": "Каталог моделей", "Models Directory": "Каталог моделей",
"Models exposed by this channel": "Модели, доступные через этот канал",
"Models fetched successfully": "Модели успешно получены", "Models fetched successfully": "Модели успешно получены",
"Models filled to form": "Модели заполнены в форму", "Models filled to form": "Модели заполнены в форму",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Модели из этого списка не будут автоматически добавлять или удалять суффиксы -thinking / -nothinking.", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Модели из этого списка не будут автоматически добавлять или удалять суффиксы -thinking / -nothinking.",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "Название, тип провайдера и доступность.", "Name, provider type, and availability.": "Название, тип провайдера и доступность.",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "Собственный формат", "Native format": "Собственный формат",
"Native forwarding": "Нативная пересылка",
"Need a redemption code?": "Нужен код активации?", "Need a redemption code?": "Нужен код активации?",
"Needs API key": "Нужен API-ключ", "Needs API key": "Нужен API-ключ",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "Вложенный JSON, определяющий правила для каждой группы для добавления (+:), удаления (-:) или добавления используемых групп.", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "Вложенный JSON, определяющий правила для каждой группы для добавления (+:), удаления (-:) или добавления используемых групп.",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "Нет настроенных маршрутов API", "No API routes configured": "Нет настроенных маршрутов API",
"No app usage data available for this model.": "Данные об использовании приложений для этой модели пока недоступны.", "No app usage data available for this model.": "Данные об использовании приложений для этой модели пока недоступны.",
"No apps match the selected filters": "Нет приложений, соответствующих фильтрам", "No apps match the selected filters": "Нет приложений, соответствующих фильтрам",
"No Auth": "Без auth",
"No available models": "Нет доступных моделей", "No available models": "Нет доступных моделей",
"No available Web chat links": "Нет доступных веб-ссылок для чата", "No available Web chat links": "Нет доступных веб-ссылок для чата",
"No backup": "Нет резервной копии", "No backup": "Нет резервной копии",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "из 3:", "of 3:": "из 3:",
"off": "выкл.", "off": "выкл.",
"Official": "Официальный", "Official": "Официальный",
"Official Claude Messages": "Официальные Claude Messages",
"Official documentation": "Официальная документация", "Official documentation": "Официальная документация",
"Official Gemini from OpenAI Chat": "Официальный Gemini из OpenAI Chat",
"Official Gemini Native": "Официальный Gemini Native",
"Official OpenAI Chat": "Официальный OpenAI Chat",
"Official OpenAI Embeddings": "Официальные OpenAI Embeddings",
"Official OpenAI Images": "Официальные OpenAI Images",
"Official OpenAI Responses": "Официальный OpenAI Responses",
"Official Repository": "Официальный репозиторий", "Official Repository": "Официальный репозиторий",
"Official Sync": "Официальная синхронизация", "Official Sync": "Официальная синхронизация",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "Открыть настройки темы", "Open theme settings": "Открыть настройки темы",
"Open weights": "Открытые веса", "Open weights": "Открытые веса",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "Синтез речи OpenAI",
"OpenAI Audio Transcriptions": "Транскрибация аудио OpenAI",
"OpenAI Audio Translations": "Перевод аудио OpenAI",
"OpenAI Chat Completions": "Чат-завершения OpenAI",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat в Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat в Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat в OpenAI Responses",
"OpenAI Completions": "Завершения OpenAI",
"OpenAI Compatible": "Совместимо с OpenAI", "OpenAI Compatible": "Совместимо с OpenAI",
"OpenAI Embeddings": "Векторные представления OpenAI",
"OpenAI Image Edits": "Редактирование изображений OpenAI",
"OpenAI Image Generations": "Генерация изображений OpenAI",
"OpenAI Organization": "Организация OpenAI", "OpenAI Organization": "Организация OpenAI",
"OpenAI Organization ID (optional)": "Идентификатор организации OpenAI (необязательно)", "OpenAI Organization ID (optional)": "Идентификатор организации OpenAI (необязательно)",
"OpenAI Realtime": "Реальное время OpenAI",
"OpenAI Rerank": "Реранжирование OpenAI",
"OpenAI Responses": "Ответы OpenAI",
"OpenAI Responses Compact": "Компактные ответы OpenAI",
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic и т.д.", "OpenAI, Anthropic, etc.": "OpenAI, Anthropic и т.д.",
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google и т.д.", "OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google и т.д.",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "QR-код не настроен. Пожалуйста, свяжитесь со службой поддержки.", "QR code is not configured. Please contact support.": "QR-код не настроен. Пожалуйста, свяжитесь со службой поддержки.",
"Quantity": "Количество", "Quantity": "Количество",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "Баланс запросов", "Query Balance": "Баланс запросов",
"Query Param": "Параметр запроса", "Query Param": "Параметр запроса",
"Querying...": "Выполняется запрос...", "Querying...": "Выполняется запрос...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "Разрешить конфликты", "Resolve Conflicts": "Разрешить конфликты",
"Resource Configuration": "Конфигурация ресурсов", "Resource Configuration": "Конфигурация ресурсов",
"Response": "Ответ", "Response": "Ответ",
"Response time: {{duration}}": "Время ответа: {{duration}}",
"Response Time": "Время ответа", "Response Time": "Время ответа",
"Response time: {{duration}}": "Время ответа: {{duration}}",
"Responses API Version": "Версия API ответов", "Responses API Version": "Версия API ответов",
"Restore defaults": "Сбросить к значениям по умолчанию", "Restore defaults": "Сбросить к значениям по умолчанию",
"Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)", "Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "Описание маршрута", "Route Description": "Описание маршрута",
"Route is required": "Маршрут обязателен", "Route is required": "Маршрут обязателен",
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте", "Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
"Routes": "Маршруты",
"Routing & Overrides": "Маршрутизация и переопределения", "Routing & Overrides": "Маршрутизация и переопределения",
"Routing Strategy": "Стратегия маршрутизации", "Routing Strategy": "Стратегия маршрутизации",
"Rows per page": "Строк на страницу", "Rows per page": "Строк на страницу",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "Обновления моделей источника", "Upstream Model Updates": "Обновления моделей источника",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Обновления моделей применены: {{added}} добавлено, {{removed}} удалено, {{ignored}} проигнорировано, всего {{totalIgnored}} проигнорированных моделей", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Обновления моделей применены: {{added}} добавлено, {{removed}} удалено, {{ignored}} проигнорировано, всего {{totalIgnored}} проигнорированных моделей",
"Upstream price sync": "Синхронизация цен upstream", "Upstream price sync": "Синхронизация цен upstream",
"Upstream path": "Путь upstream",
"Upstream path is required": "Путь upstream обязателен",
"Upstream path must be a full URL or a path starting with /": "Путь upstream должен быть полным URL или путем, начинающимся с /",
"Upstream prices fetched successfully": "Цены провайдера успешно получены", "Upstream prices fetched successfully": "Цены провайдера успешно получены",
"Upstream ratios fetched successfully": "Коэффициенты upstream успешно получены", "Upstream ratios fetched successfully": "Коэффициенты upstream успешно получены",
"Upstream Request ID": "ID вышестоящего запроса", "Upstream Request ID": "ID вышестоящего запроса",
"Upstream Response": "Ответ Upstream", "Upstream Response": "Ответ Upstream",
"upstream services integrated": "интеграций с вышестоящими сервисами", "upstream services integrated": "интеграций с вышестоящими сервисами",
"Upstream Updates": "Обновления вышестоящих моделей", "Upstream Updates": "Обновления вышестоящих моделей",
"Upstream URL": "URL вышестоящего сервиса",
"Upstream URL must be a full URL": "Upstream URL должен быть полным URL",
"uptime": "время бесперебойной работы", "uptime": "время бесперебойной работы",
"Uptime": "Время работы", "Uptime": "Время работы",
"Uptime (30d)": "Доступность (30 дн.)", "Uptime (30d)": "Доступность (30 дн.)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "Цена в USD за 1 млн входных токенов.", "USD price per 1M input tokens.": "Цена в USD за 1 млн входных токенов.",
"USD price per 1M tokens.": "Цена в USD за 1 млн токенов.", "USD price per 1M tokens.": "Цена в USD за 1 млн токенов.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Используйте +: для добавления группы, -: для удаления выбираемой по умолчанию группы, без префикса — для добавления в конец.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Используйте +: для добавления группы, -: для удаления выбираемой по умолчанию группы, без префикса — для добавления в конец.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Укажите путь, чтобы добавить его к Base URL канала, или введите полный URL, чтобы переопределить Base URL для этого маршрута.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Используйте совместимый браузер или устройство с биометрической аутентификацией или ключ безопасности для регистрации ключа доступа.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Используйте совместимый браузер или устройство с биометрической аутентификацией или ключ безопасности для регистрации ключа доступа.",
"Use authenticator code": "Использовать код аутентификатора", "Use authenticator code": "Использовать код аутентификатора",
"Use backup code": "Использовать резервный код", "Use backup code": "Использовать резервный код",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "Использовано", "Used": "Использовано",
"Used / Remaining": "Использовано / Осталось", "Used / Remaining": "Использовано / Осталось",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Используется как SuccessURL для нового продукта. Если оставить пустым, потребуется подтверждение.", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Используется как SuccessURL для нового продукта. Если оставить пустым, потребуется подтверждение.",
"Used by route auth templates": "Используется шаблонами auth маршрута",
"Used for load balancing. Higher weight = more requests": "Используется для балансировки нагрузки. Больший вес = больше запросов", "Used for load balancing. Higher weight = more requests": "Используется для балансировки нагрузки. Больший вес = больше запросов",
"Used in URLs and API routes": "Используется в URL и маршрутах API", "Used in URLs and API routes": "Используется в URL и маршрутах API",
"Used Quota": "Лимит потребления", "Used Quota": "Лимит потребления",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "При включении принимаются обратные вызовы Midjourney (раскрывает IP сервера).", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "При включении принимаются обратные вызовы Midjourney (раскрывает IP сервера).",
"When enabled, newly created tokens start in the first auto group.": "При включении вновь созданные токены начинаются в первой автогруппе.", "When enabled, newly created tokens start in the first auto group.": "При включении вновь созданные токены начинаются в первой автогруппе.",
"When enabled, prompts are scanned before reaching upstream models.": "При включении запросы сканируются перед достижением вышестоящих моделей.", "When enabled, prompts are scanned before reaching upstream models.": "При включении запросы сканируются перед достижением вышестоящих моделей.",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "Если включено, запросы, не соответствующие ни одному расширенному маршруту, перенаправляются на базовый URL канала. Если отключено, несопоставленные запросы возвращают ошибку.",
"When enabled, the store field will be blocked": "Если включено, поле магазина будет заблокировано", "When enabled, the store field will be blocked": "Если включено, поле магазина будет заблокировано",
"When enabled, users can pick this group when creating tokens.": "Если включено, пользователи могут выбрать эту группу при создании токенов.", "When enabled, users can pick this group when creating tokens.": "Если включено, пользователи могут выбрать эту группу при создании токенов.",
"When enabled, violation requests will incur additional charges.": "При включении за нарушения будут начисляться дополнительные расходы.", "When enabled, violation requests will incur additional charges.": "При включении за нарушения будут начисляться дополнительные расходы.",
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "Thêm nhà cung cấp", "Add Provider": "Thêm nhà cung cấp",
"Add Quota": "Thêm Hạn mức", "Add Quota": "Thêm Hạn mức",
"Add ratio override": "Thêm ghi đè tỷ lệ", "Add ratio override": "Thêm ghi đè tỷ lệ",
"Add route": "Thêm route",
"Add Row": "Thêm Hàng", "Add Row": "Thêm Hàng",
"Add Rule": "Thêm quy tắc", "Add Rule": "Thêm quy tắc",
"Add rule group": "Thêm nhóm quy tắc", "Add rule group": "Thêm nhóm quy tắc",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "Tên người dùng quản trị viên", "Administrator username": "Tên người dùng quản trị viên",
"Advanced": "Nâng cao", "Advanced": "Nâng cao",
"Advanced Configuration": "Cấu hình nâng cao", "Advanced Configuration": "Cấu hình nâng cao",
"Advanced Custom": "Tùy chỉnh nâng cao",
"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 or fallback": "Cấu hình tùy chỉnh nâng cao cần ít nhất một route hoặc fallback",
"Advanced Custom Routes": "Route tùy chỉnh nâng cao",
"Advanced options": "Tùy chọn nâng cao", "Advanced options": "Tùy chọn nâng cao",
"Advanced Options": "Tùy chọn nâng cao", "Advanced Options": "Tùy chọn nâng cao",
"Advanced platform configuration.": "Cấu hình nền tảng nâng cao.", "Advanced platform configuration.": "Cấu hình nền tảng nâng cao.",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "Trả lời", "Answer": "Trả lời",
"Answers for common access and billing questions": "Câu trả lời cho các câu hỏi thường gặp về truy cập và thanh toán", "Answers for common access and billing questions": "Câu trả lời cho các câu hỏi thường gặp về truy cập và thanh toán",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages sang OpenAI Chat",
"Any Match (OR)": "Bất kỳ khớp (OR)", "Any Match (OR)": "Bất kỳ khớp (OR)",
"API": "API", "API": "API",
"API Access": "Truy cập API", "API Access": "Truy cập API",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "Xem trước âm thanh", "Audio Preview": "Xem trước âm thanh",
"Audio ratio": "Tỷ lệ âm thanh", "Audio ratio": "Tỷ lệ âm thanh",
"Audio Tokens": "Token âm thanh", "Audio Tokens": "Token âm thanh",
"Auth": "Xác thực",
"Auth configured": "Đã cấu hình xác thực", "Auth configured": "Đã cấu hình xác thực",
"Auth name": "Tên xác thực",
"Auth name is required": "Bắt buộc tên xác thực",
"Auth Style": "Kiểu xác thực", "Auth Style": "Kiểu xác thực",
"Auth type is invalid": "Loại xác thực không hợp lệ",
"Auth value": "Giá trị xác thực",
"Auth value is required": "Bắt buộc giá trị xác thực",
"auth.resetPasswordConfirm.backToLogin": "Quay lại đăng nhập", "auth.resetPasswordConfirm.backToLogin": "Quay lại đăng nhập",
"auth.resetPasswordConfirm.confirm": "Xác nhận đặt lại mật khẩu", "auth.resetPasswordConfirm.confirm": "Xác nhận đặt lại mật khẩu",
"auth.resetPasswordConfirm.description": "Xác nhận yêu cầu đặt lại để tạo mật khẩu mới.", "auth.resetPasswordConfirm.description": "Xác nhận yêu cầu đặt lại để tạo mật khẩu mới.",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "Cửa sổ giới hạn tốc độ cơ bản cho tài khoản này.", "Base rate limit windows for this account.": "Cửa sổ giới hạn tốc độ cơ bản cho tài khoản này.",
"Base URL": "URL cơ sở", "Base URL": "URL cơ sở",
"Base URL is required for this channel type": "Loại kênh này yêu cầu Base URL", "Base URL is required for this channel type": "Loại kênh này yêu cầu Base URL",
"Base URL is required when an advanced route uses an upstream path": "Cần Base URL khi tuyến nâng cao dùng đường dẫn upstream",
"Base URL is required when fallback is enabled": "Cần Base URL khi bật fallback",
"Base URL of your Uptime Kuma instance": "URL cơ sở của phiên bản Uptime Kuma của bạn", "Base URL of your Uptime Kuma instance": "URL cơ sở của phiên bản Uptime Kuma của bạn",
"Basic Authentication": "Xác thực cơ bản", "Basic Authentication": "Xác thực cơ bản",
"Basic Configuration": "Cấu hình cơ bản", "Basic Configuration": "Cấu hình cơ bản",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "Các thay đổi sẽ được ghi vào bản nháp cài đặt khi lưu.", "Changes are written to the settings draft on save.": "Các thay đổi sẽ được ghi vào bản nháp cài đặt khi lưu.",
"Changing...": "Đang thay đổi...", "Changing...": "Đang thay đổi...",
"Channel": "Kênh", "Channel": "Kênh",
"Channel {{name}}": "Kênh {{name}}",
"Channel {{name}} model {{model}}": "Kênh {{name}} mô hình {{model}}",
"Channel Affinity": "Ưu tiên kênh", "Channel Affinity": "Ưu tiên kênh",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream", "Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "Khóa kênh", "Channel key": "Khóa kênh",
"Channel key unlocked": "Khóa kênh đã được mở khóa", "Channel key unlocked": "Khóa kênh đã được mở khóa",
"Channel models": "Mô hình kênh", "Channel models": "Mô hình kênh",
"Channel {{name}}": "Kênh {{name}}",
"Channel {{name}} model {{model}}": "Kênh {{name}} mô hình {{model}}",
"Channel name is required": "Tên kênh là bắt buộc", "Channel name is required": "Tên kênh là bắt buộc",
"Channel test completed": "Kiểm tra kênh hoàn tất", "Channel test completed": "Kiểm tra kênh hoàn tất",
"Channel type is required": "Loại kênh là bắt buộc", "Channel type is required": "Loại kênh là bắt buộc",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "Cổ điển (Frontend cũ)", "Classic (Legacy Frontend)": "Cổ điển (Frontend cũ)",
"Claude": "Claude", "Claude": "Claude",
"Claude CLI Header Passthrough": "Chuyển tiếp header Claude CLI", "Claude CLI Header Passthrough": "Chuyển tiếp header Claude CLI",
"Claude Messages": "Claude Messages",
"Clean": "Không xung đột", "Clean": "Không xung đột",
"Clean history logs": "Xóa nhật ký lịch sử", "Clean history logs": "Xóa nhật ký lịch sử",
"Clean logs": "Dọn dẹp nhật ký", "Clean logs": "Dọn dẹp nhật ký",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Cấu hình giá theo từng công cụ ($/1K lần gọi). Mô hình tính phí theo request không phát sinh thêm phí công cụ.", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Cấu hình giá theo từng công cụ ($/1K lần gọi). Mô hình tính phí theo request không phát sinh thêm phí công cụ.",
"Configure pricing ratios for a specific model.": "Cấu hình tỷ lệ định giá cho một mô hình cụ thể.", "Configure pricing ratios for a specific model.": "Cấu hình tỷ lệ định giá cho một mô hình cụ thể.",
"Configure rate limiting rules for a specific user group.": "Cấu hình quy tắc giới hạn tốc độ cho một nhóm người dùng cụ thể.", "Configure rate limiting rules for a specific user group.": "Cấu hình quy tắc giới hạn tốc độ cho một nhóm người dùng cụ thể.",
"Configure routes": "Cấu hình route",
"Configure the ratio for this group.": "Cấu hình tỷ lệ cho nhóm này.", "Configure the ratio for this group.": "Cấu hình tỷ lệ cho nhóm này.",
"Configure upstream providers and routing.": "Cấu hình nhà cung cấp upstream và định tuyến.", "Configure upstream providers and routing.": "Cấu hình nhà cung cấp upstream và định tuyến.",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Cấu hình tích hợp thanh toán Waffo Pancake (hosted checkout) cho nạp tiền theo USD", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "Cấu hình tích hợp thanh toán Waffo Pancake (hosted checkout) cho nạp tiền theo USD",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "Chuyển đổi reasoning_content thành thẻ <think> trong nội dung", "Convert reasoning_content to <think> tag in content": "Chuyển đổi reasoning_content thành thẻ <think> trong nội dung",
"Convert string to lowercase": "Chuyển chuỗi sang chữ thường", "Convert string to lowercase": "Chuyển chuỗi sang chữ thường",
"Convert string to uppercase": "Chuyển chuỗi sang chữ hoa", "Convert string to uppercase": "Chuyển chuỗi sang chữ hoa",
"Converter": "Bộ chuyển đổi",
"Converter does not match incoming path": "Bộ chuyển đổi không khớp path đầu vào",
"Converter is not registered": "Bộ chuyển đổi chưa được đăng ký",
"Copied": "Đã sao chép", "Copied": "Đã sao chép",
"Copied {{count}} key(s)": "Đã sao chép {{count}} khóa", "Copied {{count}} key(s)": "Đã sao chép {{count}} khóa",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Đã sao chép kênh (ID nguồn: {{sourceId}}) thành {{name}} (ID mới: {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "Đã sao chép kênh (ID nguồn: {{sourceId}}) thành {{name}} (ID mới: {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "Mặc định / khoảng", "Default / range": "Mặc định / khoảng",
"Default API Version *": "Phiên bản API mặc định *", "Default API Version *": "Phiên bản API mặc định *",
"Default API version for this channel": "Phiên bản API mặc định cho kênh này", "Default API version for this channel": "Phiên bản API mặc định cho kênh này",
"Default Bearer": "Bearer mặc định",
"Default Collapse Sidebar": "Mặc định Thu gọn Thanh bên", "Default Collapse Sidebar": "Mặc định Thu gọn Thanh bên",
"Default consumption chart": "Biểu đồ tiêu thụ mặc định", "Default consumption chart": "Biểu đồ tiêu thụ mặc định",
"Default Max Tokens": "Tokens Tối đa Mặc định", "Default Max Tokens": "Tokens Tối đa Mặc định",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "Không thể cập nhật người dùng", "Failed to update user": "Không thể cập nhật người dùng",
"Failure keywords": "Từ khóa thất bại", "Failure keywords": "Từ khóa thất bại",
"Fair": "Công bằng", "Fair": "Công bằng",
"Fallback": "Fallback",
"Fallback base URL": "Base URL fallback",
"Fallback routing": "Định tuyến fallback",
"Fallback tier": "Tầng dự phòng", "Fallback tier": "Tầng dự phòng",
"FAQ": "FAQ", "FAQ": "FAQ",
"FAQ added. Click \"Save Settings\" to apply.": "Đã thêm FAQ. Nhấp \"Lưu cài đặt\" để áp dụng.", "FAQ added. Click \"Save Settings\" to apply.": "Đã thêm FAQ. Nhấp \"Lưu cài đặt\" để áp dụng.",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC đã thực thi", "GC executed": "GC đã thực thi",
"GC execution failed": "Thực thi GC thất bại", "GC execution failed": "Thực thi GC thất bại",
"Gemini": "Song Tử", "Gemini": "Song Tử",
"Gemini Batch Embed Contents": "Gemini Batch Embed Contents",
"Gemini Embed Content": "Gemini Embed Content",
"Gemini Generate Content": "Gemini Generate Content",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content sang OpenAI Chat",
"Gemini Image 4K": "Gemini Image 4K", "Gemini Image 4K": "Gemini Image 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini sẽ tiếp tục tự động phát hiện chế độ suy nghĩ ngay cả khi bộ điều hợp bị tắt. Chỉ bật tính năng này khi bạn cần kiểm soát chi tiết hơn về giá cả và lập ngân sách.", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "Gemini sẽ tiếp tục tự động phát hiện chế độ suy nghĩ ngay cả khi bộ điều hợp bị tắt. Chỉ bật tính năng này khi bạn cần kiểm soát chi tiết hơn về giá cả và lập ngân sách.",
"General": "Chung", "General": "Chung",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "Bao gồm tên quy tắc", "Include Rule Name": "Bao gồm tên quy tắc",
"Includes request rules": "Bao gồm quy tắc yêu cầu", "Includes request rules": "Bao gồm quy tắc yêu cầu",
"Including failed requests, 0 = unlimited": "Bao gồm các yêu cầu thất bại, 0 = không giới hạn", "Including failed requests, 0 = unlimited": "Bao gồm các yêu cầu thất bại, 0 = không giới hạn",
"Incoming path": "Path đầu vào",
"Incoming path is required": "Bắt buộc path đầu vào",
"Incoming path must be unique": "Path đầu vào phải duy nhất",
"Incoming path must not include query": "Path đầu vào không được chứa query",
"Incoming path must start with /": "Path đầu vào phải bắt đầu bằng /",
"Incomplete": "Chưa hoàn tất",
"Increased user quota by {{quota}}": "Đã tăng hạn mức người dùng thêm {{quota}}", "Increased user quota by {{quota}}": "Đã tăng hạn mức người dùng thêm {{quota}}",
"Index": "Chỉ mục", "Index": "Chỉ mục",
"Initial quota given to new users": "Hạn mức ban đầu cấp cho người dùng mới", "Initial quota given to new users": "Hạn mức ban đầu cấp cho người dùng mới",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "Chế độ", "Mode": "Chế độ",
"model": "mô hình", "model": "mô hình",
"Model": "Mô hình", "Model": "Mô hình",
"Model {{model}}": "Mô hình {{model}}",
"Model Access": "Truy cập mô hình", "Model Access": "Truy cập mô hình",
"Model Analytics": "Phân tích mô hình", "Model Analytics": "Phân tích mô hình",
"Model Analytics Defaults": "Mặc định phân tích mô hình", "Model Analytics Defaults": "Mặc định phân tích mô hình",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "Ánh xạ mô hình phải là đối tượng JSON với giá trị chuỗi", "Model mapping must be a JSON object with string values": "Ánh xạ mô hình phải là đối tượng JSON với giá trị chuỗi",
"Model mapping must be valid JSON": "Ánh xạ mô hình phải là JSON hợp lệ", "Model mapping must be valid JSON": "Ánh xạ mô hình phải là JSON hợp lệ",
"Model mapping values must be strings": "Giá trị ánh xạ mô hình phải là chuỗi", "Model mapping values must be strings": "Giá trị ánh xạ mô hình phải là chuỗi",
"Model {{model}}": "Mô hình {{model}}",
"Model name": "Tên mẫu", "Model name": "Tên mẫu",
"Model Name": "Tên mẫu", "Model Name": "Tên mẫu",
"Model Name *": "Tên mẫu *", "Model Name *": "Tên mẫu *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "Mô hình tăng hạng nhanh nhất", "Models climbing the leaderboard fastest": "Mô hình tăng hạng nhanh nhất",
"Models directory": "Thư mục mô hình", "Models directory": "Thư mục mô hình",
"Models Directory": "Danh mục mô hình", "Models Directory": "Danh mục mô hình",
"Models exposed by this channel": "Model được kênh này công bố",
"Models fetched successfully": "Các mô hình đã được tải thành công", "Models fetched successfully": "Các mô hình đã được tải thành công",
"Models filled to form": "Mô hình đã được điền vào biểu mẫu", "Models filled to form": "Mô hình đã được điền vào biểu mẫu",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Các mô hình được liệt kê ở đây sẽ không tự động thêm hoặc xóa hậu tố -thinking / -nothinking.", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "Các mô hình được liệt kê ở đây sẽ không tự động thêm hoặc xóa hậu tố -thinking / -nothinking.",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "Tên, loại nhà cung cấp và trạng thái khả dụng.", "Name, provider type, and availability.": "Tên, loại nhà cung cấp và trạng thái khả dụng.",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "Định dạng gốc", "Native format": "Định dạng gốc",
"Native forwarding": "Chuyển tiếp nguyên bản",
"Need a redemption code?": "Cần mã đổi thưởng?", "Need a redemption code?": "Cần mã đổi thưởng?",
"Needs API key": "Cần khóa API", "Needs API key": "Cần khóa API",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "JSON lồng nhau xác định quy tắc theo nhóm để thêm (+:), xóa (-:), hoặc nối các nhóm có thể sử dụng.", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "JSON lồng nhau xác định quy tắc theo nhóm để thêm (+:), xóa (-:), hoặc nối các nhóm có thể sử dụng.",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "Chưa có tuyến API nào được cấu hình", "No API routes configured": "Chưa có tuyến API nào được cấu hình",
"No app usage data available for this model.": "Chưa có dữ liệu sử dụng ứng dụng cho mô hình này.", "No app usage data available for this model.": "Chưa có dữ liệu sử dụng ứng dụng cho mô hình này.",
"No apps match the selected filters": "Không có ứng dụng phù hợp bộ lọc", "No apps match the selected filters": "Không có ứng dụng phù hợp bộ lọc",
"No Auth": "Không xác thực",
"No available models": "Không có mô hình khả dụng", "No available models": "Không có mô hình khả dụng",
"No available Web chat links": "Không có liên kết Web chat khả dụng", "No available Web chat links": "Không có liên kết Web chat khả dụng",
"No backup": "Chưa sao lưu", "No backup": "Chưa sao lưu",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "of 3:", "of 3:": "of 3:",
"off": "off", "off": "off",
"Official": "Chính thức", "Official": "Chính thức",
"Official Claude Messages": "Claude Messages chính thức",
"Official documentation": "Tài liệu chính thức", "Official documentation": "Tài liệu chính thức",
"Official Gemini from OpenAI Chat": "Gemini chính thức từ OpenAI Chat",
"Official Gemini Native": "Gemini native chính thức",
"Official OpenAI Chat": "OpenAI Chat chính thức",
"Official OpenAI Embeddings": "OpenAI Embeddings chính thức",
"Official OpenAI Images": "OpenAI Images chính thức",
"Official OpenAI Responses": "OpenAI Responses chính thức",
"Official Repository": "Kho lưu trữ chính thức", "Official Repository": "Kho lưu trữ chính thức",
"Official Sync": "Official sync", "Official Sync": "Official sync",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "Mở cài đặt giao diện", "Open theme settings": "Mở cài đặt giao diện",
"Open weights": "Trọng số mở", "Open weights": "Trọng số mở",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "OpenAI Audio Speech",
"OpenAI Audio Transcriptions": "OpenAI Audio Transcriptions",
"OpenAI Audio Translations": "OpenAI Audio Translations",
"OpenAI Chat Completions": "OpenAI Chat Completions",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat sang Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat sang Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat sang OpenAI Responses",
"OpenAI Completions": "OpenAI Completions",
"OpenAI Compatible": "Tương thích OpenAI", "OpenAI Compatible": "Tương thích OpenAI",
"OpenAI Embeddings": "OpenAI Embeddings",
"OpenAI Image Edits": "OpenAI Image Edits",
"OpenAI Image Generations": "OpenAI Image Generations",
"OpenAI Organization": "Tổ chức OpenAI", "OpenAI Organization": "Tổ chức OpenAI",
"OpenAI Organization ID (optional)": "ID Tổ chức OpenAI (tùy chọn)", "OpenAI Organization ID (optional)": "ID Tổ chức OpenAI (tùy chọn)",
"OpenAI Realtime": "OpenAI Realtime",
"OpenAI Rerank": "OpenAI Rerank",
"OpenAI Responses": "OpenAI Responses",
"OpenAI Responses Compact": "OpenAI Responses Compact",
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, v.v.", "OpenAI, Anthropic, etc.": "OpenAI, Anthropic, v.v.",
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, v.v.", "OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, v.v.",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "Mã QR chưa được cấu hình. Vui lòng liên hệ bộ phận hỗ trợ.", "QR code is not configured. Please contact support.": "Mã QR chưa được cấu hình. Vui lòng liên hệ bộ phận hỗ trợ.",
"Quantity": "Số lượng", "Quantity": "Số lượng",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "Truy vấn số dư", "Query Balance": "Truy vấn số dư",
"Query Param": "Query param", "Query Param": "Query param",
"Querying...": "Đang truy vấn...", "Querying...": "Đang truy vấn...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "Giải quyết Xung đột", "Resolve Conflicts": "Giải quyết Xung đột",
"Resource Configuration": "Cấu hình tài nguyên", "Resource Configuration": "Cấu hình tài nguyên",
"Response": "Phản hồi", "Response": "Phản hồi",
"Response time: {{duration}}": "Thời gian phản hồi: {{duration}}",
"Response Time": "Thời gian phản hồi", "Response Time": "Thời gian phản hồi",
"Response time: {{duration}}": "Thời gian phản hồi: {{duration}}",
"Responses API Version": "Phiên bản API Phản hồi", "Responses API Version": "Phiên bản API Phản hồi",
"Restore defaults": "Khôi phục mặc định", "Restore defaults": "Khôi phục mặc định",
"Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)", "Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "Mô tả lộ trình", "Route Description": "Mô tả lộ trình",
"Route is required": "Đường dẫn là bắt buộc", "Route is required": "Đường dẫn là bắt buộc",
"Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi", "Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi",
"Routes": "Route",
"Routing & Overrides": "Định tuyến & ghi đè", "Routing & Overrides": "Định tuyến & ghi đè",
"Routing Strategy": "Chiến lược định tuyến", "Routing Strategy": "Chiến lược định tuyến",
"Rows per page": "Số hàng trên trang", "Rows per page": "Số hàng trên trang",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "Cập nhật mô hình upstream", "Upstream Model Updates": "Cập nhật mô hình upstream",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Đã áp dụng cập nhật mô hình upstream: {{added}} đã thêm, {{removed}} đã xóa, {{ignored}} bỏ qua lần này, {{totalIgnored}} tổng mô hình đã bỏ qua", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "Đã áp dụng cập nhật mô hình upstream: {{added}} đã thêm, {{removed}} đã xóa, {{ignored}} bỏ qua lần này, {{totalIgnored}} tổng mô hình đã bỏ qua",
"Upstream price sync": "Đồng bộ giá thượng nguồn", "Upstream price sync": "Đồng bộ giá thượng nguồn",
"Upstream path": "Đường dẫn upstream",
"Upstream path is required": "Cần nhập đường dẫn upstream",
"Upstream path must be a full URL or a path starting with /": "Đường dẫn upstream phải là URL đầy đủ hoặc đường dẫn bắt đầu bằng /",
"Upstream prices fetched successfully": "Lấy giá upstream thành công", "Upstream prices fetched successfully": "Lấy giá upstream thành công",
"Upstream ratios fetched successfully": "Đã lấy tỷ lệ upstream thành công", "Upstream ratios fetched successfully": "Đã lấy tỷ lệ upstream thành công",
"Upstream Request ID": "ID yêu cầu thượng nguồn", "Upstream Request ID": "ID yêu cầu thượng nguồn",
"Upstream Response": "Upstream feedback", "Upstream Response": "Upstream feedback",
"upstream services integrated": "dịch vụ thượng nguồn tích hợp", "upstream services integrated": "dịch vụ thượng nguồn tích hợp",
"Upstream Updates": "Cập nhật nguồn", "Upstream Updates": "Cập nhật nguồn",
"Upstream URL": "URL upstream",
"Upstream URL must be a full URL": "URL upstream phải là URL đầy đủ",
"uptime": "thời gian hoạt động", "uptime": "thời gian hoạt động",
"Uptime": "Thời gian hoạt động", "Uptime": "Thời gian hoạt động",
"Uptime (30d)": "Thời gian hoạt động (30 ngày)", "Uptime (30d)": "Thời gian hoạt động (30 ngày)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "Giá USD cho mỗi 1 triệu token đầu vào.", "USD price per 1M input tokens.": "Giá USD cho mỗi 1 triệu token đầu vào.",
"USD price per 1M tokens.": "Giá USD cho mỗi 1 triệu token.", "USD price per 1M tokens.": "Giá USD cho mỗi 1 triệu token.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Dùng +: để thêm nhóm, -: để xóa nhóm có thể chọn mặc định, hoặc không có tiền tố để nối nhóm.", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Dùng +: để thêm nhóm, -: để xóa nhóm có thể chọn mặc định, hoặc không có tiền tố để nối nhóm.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Dùng đường dẫn để nối vào Base URL của kênh, hoặc nhập URL đầy đủ để ghi đè Base URL cho tuyến này.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Sử dụng trình duyệt hoặc thiết bị tương thích có xác thực sinh trắc học hoặc khóa bảo mật để đăng ký Khóa truy cập.", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Sử dụng trình duyệt hoặc thiết bị tương thích có xác thực sinh trắc học hoặc khóa bảo mật để đăng ký Khóa truy cập.",
"Use authenticator code": "Sử dụng mã xác thực", "Use authenticator code": "Sử dụng mã xác thực",
"Use backup code": "Sử dụng mã dự phòng", "Use backup code": "Sử dụng mã dự phòng",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "Đã sử dụng", "Used": "Đã sử dụng",
"Used / Remaining": "Đã dùng / Còn lại", "Used / Remaining": "Đã dùng / Còn lại",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Được dùng làm SuccessURL trên sản phẩm mới. Bạn sẽ được yêu cầu xác nhận nếu để trống.", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "Được dùng làm SuccessURL trên sản phẩm mới. Bạn sẽ được yêu cầu xác nhận nếu để trống.",
"Used by route auth templates": "Dùng trong mẫu xác thực route",
"Used for load balancing. Higher weight = more requests": "Được sử dụng để cân bằng tải. Trọng số càng cao = càng nhiều yêu cầu", "Used for load balancing. Higher weight = more requests": "Được sử dụng để cân bằng tải. Trọng số càng cao = càng nhiều yêu cầu",
"Used in URLs and API routes": "Sử dụng trong URL và các tuyến API", "Used in URLs and API routes": "Sử dụng trong URL và các tuyến API",
"Used Quota": "Hạn mức đã sử dụng", "Used Quota": "Hạn mức đã sử dụng",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "Khi được bật, các callback của Midjourney được chấp nhận (lộ IP máy chủ).", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "Khi được bật, các callback của Midjourney được chấp nhận (lộ IP máy chủ).",
"When enabled, newly created tokens start in the first auto group.": "Khi được bật, các token mới được tạo sẽ bắt đầu trong nhóm tự động đầu tiên.", "When enabled, newly created tokens start in the first auto group.": "Khi được bật, các token mới được tạo sẽ bắt đầu trong nhóm tự động đầu tiên.",
"When enabled, prompts are scanned before reaching upstream models.": "Khi được bật,", "When enabled, prompts are scanned before reaching upstream models.": "Khi được bật,",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "Khi bật, các yêu cầu không khớp với tuyến nâng cao nào sẽ được chuyển tiếp đến URL cơ sở của kênh. Khi tắt, các yêu cầu không khớp sẽ trả về lỗi.",
"When enabled, the store field will be blocked": "Khi được bật, trường store sẽ bị chặn", "When enabled, the store field will be blocked": "Khi được bật, trường store sẽ bị chặn",
"When enabled, users can pick this group when creating tokens.": "Khi bật, người dùng có thể chọn nhóm này khi tạo token.", "When enabled, users can pick this group when creating tokens.": "Khi bật, người dùng có thể chọn nhóm này khi tạo token.",
"When enabled, violation requests will incur additional charges.": "Khi bật, các yêu cầu vi phạm sẽ phải chịu phí bổ sung.", "When enabled, violation requests will incur additional charges.": "Khi bật, các yêu cầu vi phạm sẽ phải chịu phí bổ sung.",
......
...@@ -193,6 +193,7 @@ ...@@ -193,6 +193,7 @@
"Add Provider": "添加提供商", "Add Provider": "添加提供商",
"Add Quota": "添加配额", "Add Quota": "添加配额",
"Add ratio override": "添加倍率覆盖", "Add ratio override": "添加倍率覆盖",
"Add route": "添加路由",
"Add Row": "添加行", "Add Row": "添加行",
"Add Rule": "添加规则", "Add Rule": "添加规则",
"Add rule group": "新增规则组", "Add rule group": "新增规则组",
...@@ -228,6 +229,10 @@ ...@@ -228,6 +229,10 @@
"Administrator username": "管理员用户名", "Administrator username": "管理员用户名",
"Advanced": "高级", "Advanced": "高级",
"Advanced Configuration": "高级配置", "Advanced Configuration": "高级配置",
"Advanced Custom": "高级自定义",
"Advanced custom configuration is required": "必须配置高级自定义",
"Advanced custom configuration requires at least one route or fallback": "高级自定义至少需要一个路由或启用兜底",
"Advanced Custom Routes": "高级自定义路由",
"Advanced options": "高级选项", "Advanced options": "高级选项",
"Advanced Options": "高级选项", "Advanced Options": "高级选项",
"Advanced platform configuration.": "高级平台配置。", "Advanced platform configuration.": "高级平台配置。",
...@@ -340,6 +345,7 @@ ...@@ -340,6 +345,7 @@
"Answer": "答案", "Answer": "答案",
"Answers for common access and billing questions": "访问与计费常见问题解答", "Answers for common access and billing questions": "访问与计费常见问题解答",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"Anthropic Messages to OpenAI Chat": "Anthropic Messages 到 OpenAI Chat",
"Any Match (OR)": "任一满足(OR)", "Any Match (OR)": "任一满足(OR)",
"API": "API", "API": "API",
"API Access": "API 访问", "API Access": "API 访问",
...@@ -442,8 +448,14 @@ ...@@ -442,8 +448,14 @@
"Audio Preview": "音乐预览", "Audio Preview": "音乐预览",
"Audio ratio": "音频倍率", "Audio ratio": "音频倍率",
"Audio Tokens": "语音 Token", "Audio Tokens": "语音 Token",
"Auth": "认证",
"Auth configured": "认证已配置", "Auth configured": "认证已配置",
"Auth name": "认证名称",
"Auth name is required": "认证名称不能为空",
"Auth Style": "认证方式", "Auth Style": "认证方式",
"Auth type is invalid": "认证类型无效",
"Auth value": "认证值",
"Auth value is required": "认证值不能为空",
"auth.resetPasswordConfirm.backToLogin": "返回登录", "auth.resetPasswordConfirm.backToLogin": "返回登录",
"auth.resetPasswordConfirm.confirm": "确认重置密码", "auth.resetPasswordConfirm.confirm": "确认重置密码",
"auth.resetPasswordConfirm.description": "确认重置请求以生成新密码。", "auth.resetPasswordConfirm.description": "确认重置请求以生成新密码。",
...@@ -529,6 +541,8 @@ ...@@ -529,6 +541,8 @@
"Base rate limit windows for this account.": "当前账号的基础额度窗口。", "Base rate limit windows for this account.": "当前账号的基础额度窗口。",
"Base URL": "API 地址", "Base URL": "API 地址",
"Base URL is required for this channel type": "此渠道类型需要填写 Base URL", "Base URL is required for this channel type": "此渠道类型需要填写 Base URL",
"Base URL is required when an advanced route uses an upstream path": "高级路由使用上游路径时必须填写 Base URL",
"Base URL is required when fallback is enabled": "启用兜底时必须填写 Base URL",
"Base URL of your Uptime Kuma instance": "您的 Uptime Kuma 实例的基础 URL", "Base URL of your Uptime Kuma instance": "您的 Uptime Kuma 实例的基础 URL",
"Basic Authentication": "基本身份验证", "Basic Authentication": "基本身份验证",
"Basic Configuration": "基本配置", "Basic Configuration": "基本配置",
...@@ -667,6 +681,8 @@ ...@@ -667,6 +681,8 @@
"Changes are written to the settings draft on save.": "保存后会写入设置草稿。", "Changes are written to the settings draft on save.": "保存后会写入设置草稿。",
"Changing...": "修改中...", "Changing...": "修改中...",
"Channel": "渠道", "Channel": "渠道",
"Channel {{name}}": "渠道 {{name}}",
"Channel {{name}} model {{model}}": "渠道 {{name}} 模型 {{model}}",
"Channel Affinity": "渠道亲和性", "Channel Affinity": "渠道亲和性",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中", "Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
...@@ -680,8 +696,6 @@ ...@@ -680,8 +696,6 @@
"Channel key": "渠道密钥", "Channel key": "渠道密钥",
"Channel key unlocked": "渠道密钥已解锁", "Channel key unlocked": "渠道密钥已解锁",
"Channel models": "渠道模型", "Channel models": "渠道模型",
"Channel {{name}}": "渠道 {{name}}",
"Channel {{name}} model {{model}}": "渠道 {{name}} 模型 {{model}}",
"Channel name is required": "渠道名称是必填的", "Channel name is required": "渠道名称是必填的",
"Channel test completed": "渠道测试完成", "Channel test completed": "渠道测试完成",
"Channel type is required": "渠道类型是必填的", "Channel type is required": "渠道类型是必填的",
...@@ -741,6 +755,7 @@ ...@@ -741,6 +755,7 @@
"Classic (Legacy Frontend)": "经典前端", "Classic (Legacy Frontend)": "经典前端",
"Claude": "Claude", "Claude": "Claude",
"Claude CLI Header Passthrough": "Claude CLI 请求头透传", "Claude CLI Header Passthrough": "Claude CLI 请求头透传",
"Claude Messages": "Claude 消息",
"Clean": "无冲突", "Clean": "无冲突",
"Clean history logs": "清理历史日志", "Clean history logs": "清理历史日志",
"Clean logs": "清理日志", "Clean logs": "清理日志",
...@@ -884,6 +899,7 @@ ...@@ -884,6 +899,7 @@
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "为每个工具配置单价($/1K 次调用)。按请求计费的模型不额外收取工具费用。", "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "为每个工具配置单价($/1K 次调用)。按请求计费的模型不额外收取工具费用。",
"Configure pricing ratios for a specific model.": "配置特定模型的定价比例。", "Configure pricing ratios for a specific model.": "配置特定模型的定价比例。",
"Configure rate limiting rules for a specific user group.": "配置特定用户分组的速率限制规则。", "Configure rate limiting rules for a specific user group.": "配置特定用户分组的速率限制规则。",
"Configure routes": "配置路由",
"Configure the ratio for this group.": "配置此分组的比例。", "Configure the ratio for this group.": "配置此分组的比例。",
"Configure upstream providers and routing.": "配置上游提供者和路由。", "Configure upstream providers and routing.": "配置上游提供者和路由。",
"Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "配置 Waffo Pancake 托管结账,用于美元计价的充值", "Configure Waffo Pancake hosted checkout integration for USD-priced top-ups": "配置 Waffo Pancake 托管结账,用于美元计价的充值",
...@@ -962,6 +978,9 @@ ...@@ -962,6 +978,9 @@
"Convert reasoning_content to <think> tag in content": "将 reasoning_content 转换为 content 中的 <think> 标签", "Convert reasoning_content to <think> tag in content": "将 reasoning_content 转换为 content 中的 <think> 标签",
"Convert string to lowercase": "把字符串转成小写", "Convert string to lowercase": "把字符串转成小写",
"Convert string to uppercase": "把字符串转成大写", "Convert string to uppercase": "把字符串转成大写",
"Converter": "转换器",
"Converter does not match incoming path": "转换器与入口路径不匹配",
"Converter is not registered": "转换器未注册",
"Copied": "已复制", "Copied": "已复制",
"Copied {{count}} key(s)": "已复制 {{count}} 个密钥", "Copied {{count}} key(s)": "已复制 {{count}} 个密钥",
"Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "复制渠道(源 ID: {{sourceId}})为 {{name}}(新 ID: {{id}})", "Copied channel (source ID: {{sourceId}}) to {{name}} (new ID: {{id}})": "复制渠道(源 ID: {{sourceId}})为 {{name}}(新 ID: {{id}})",
...@@ -1145,6 +1164,7 @@ ...@@ -1145,6 +1164,7 @@
"Default / range": "默认值 / 范围", "Default / range": "默认值 / 范围",
"Default API Version *": "默认 API 版本 *", "Default API Version *": "默认 API 版本 *",
"Default API version for this channel": "此渠道的默认 API 版本", "Default API version for this channel": "此渠道的默认 API 版本",
"Default Bearer": "默认 Bearer",
"Default Collapse Sidebar": "默认折叠侧边栏", "Default Collapse Sidebar": "默认折叠侧边栏",
"Default consumption chart": "默认消耗分布图", "Default consumption chart": "默认消耗分布图",
"Default Max Tokens": "默认最大 Token 数", "Default Max Tokens": "默认最大 Token 数",
...@@ -1754,6 +1774,9 @@ ...@@ -1754,6 +1774,9 @@
"Failed to update user": "更新用户失败", "Failed to update user": "更新用户失败",
"Failure keywords": "失败关键词", "Failure keywords": "失败关键词",
"Fair": "公平", "Fair": "公平",
"Fallback": "兜底",
"Fallback base URL": "兜底 Base URL",
"Fallback routing": "兜底路由",
"Fallback tier": "兜底阶梯", "Fallback tier": "兜底阶梯",
"FAQ": "常见问答", "FAQ": "常见问答",
"FAQ added. Click \"Save Settings\" to apply.": "FAQ 已添加。点击 \"保存设置\" 以应用。", "FAQ added. Click \"Save Settings\" to apply.": "FAQ 已添加。点击 \"保存设置\" 以应用。",
...@@ -1893,6 +1916,10 @@ ...@@ -1893,6 +1916,10 @@
"GC executed": "GC 已执行", "GC executed": "GC 已执行",
"GC execution failed": "GC 执行失败", "GC execution failed": "GC 执行失败",
"Gemini": "Gemini", "Gemini": "Gemini",
"Gemini Batch Embed Contents": "Gemini 批量嵌入内容",
"Gemini Embed Content": "Gemini 嵌入内容",
"Gemini Generate Content": "Gemini 内容生成",
"Gemini Generate Content to OpenAI Chat": "Gemini Generate Content 到 OpenAI Chat",
"Gemini Image 4K": "Gemini 图片 4K", "Gemini Image 4K": "Gemini 图片 4K",
"Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "即使禁用适配器,Gemini 也会继续自动检测思维模式。仅当您需要对定价和预算进行更精细的控制时才启用此选项。", "Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.": "即使禁用适配器,Gemini 也会继续自动检测思维模式。仅当您需要对定价和预算进行更精细的控制时才启用此选项。",
"General": "常规", "General": "常规",
...@@ -2095,6 +2122,12 @@ ...@@ -2095,6 +2122,12 @@
"Include Rule Name": "包含规则名", "Include Rule Name": "包含规则名",
"Includes request rules": "包含请求规则", "Includes request rules": "包含请求规则",
"Including failed requests, 0 = unlimited": "包括失败的请求,0 = 无限制", "Including failed requests, 0 = unlimited": "包括失败的请求,0 = 无限制",
"Incoming path": "入口路径",
"Incoming path is required": "入口路径不能为空",
"Incoming path must be unique": "入口路径必须唯一",
"Incoming path must not include query": "入口路径不能包含 query",
"Incoming path must start with /": "入口路径必须以 / 开头",
"Incomplete": "未完成",
"Increased user quota by {{quota}}": "增加用户额度 {{quota}}", "Increased user quota by {{quota}}": "增加用户额度 {{quota}}",
"Index": "索引", "Index": "索引",
"Initial quota given to new users": "授予新用户的初始配额", "Initial quota given to new users": "授予新用户的初始配额",
...@@ -2408,6 +2441,7 @@ ...@@ -2408,6 +2441,7 @@
"Mode": "模式", "Mode": "模式",
"model": "模型", "model": "模型",
"Model": "模型", "Model": "模型",
"Model {{model}}": "模型 {{model}}",
"Model Access": "模型访问", "Model Access": "模型访问",
"Model Analytics": "模型数据分析", "Model Analytics": "模型数据分析",
"Model Analytics Defaults": "模型分析默认设置", "Model Analytics Defaults": "模型分析默认设置",
...@@ -2432,7 +2466,6 @@ ...@@ -2432,7 +2466,6 @@
"Model mapping must be a JSON object with string values": "模型映射必须是值为字符串的 JSON 对象", "Model mapping must be a JSON object with string values": "模型映射必须是值为字符串的 JSON 对象",
"Model mapping must be valid JSON": "模型映射必须是有效的 JSON", "Model mapping must be valid JSON": "模型映射必须是有效的 JSON",
"Model mapping values must be strings": "模型映射的值必须是字符串", "Model mapping values must be strings": "模型映射的值必须是字符串",
"Model {{model}}": "模型 {{model}}",
"Model name": "模型名称", "Model name": "模型名称",
"Model Name": "模型名称", "Model Name": "模型名称",
"Model Name *": "模型名称 *", "Model Name *": "模型名称 *",
...@@ -2471,6 +2504,7 @@ ...@@ -2471,6 +2504,7 @@
"Models climbing the leaderboard fastest": "排行榜上升最快的模型", "Models climbing the leaderboard fastest": "排行榜上升最快的模型",
"Models directory": "模型目录", "Models directory": "模型目录",
"Models Directory": "模型目录", "Models Directory": "模型目录",
"Models exposed by this channel": "此渠道暴露的模型",
"Models fetched successfully": "模型获取成功", "Models fetched successfully": "模型获取成功",
"Models filled to form": "模型已填充到表单", "Models filled to form": "模型已填充到表单",
"Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "此处列出的模型不会自动添加或移除 -thinking / -nothinking 后缀。", "Models listed here will not automatically append or remove -thinking / -nothinking suffixes.": "此处列出的模型不会自动添加或移除 -thinking / -nothinking 后缀。",
...@@ -2551,6 +2585,7 @@ ...@@ -2551,6 +2585,7 @@
"Name, provider type, and availability.": "名称、供应商类型和可用状态。", "Name, provider type, and availability.": "名称、供应商类型和可用状态。",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"Native format": "原生格式", "Native format": "原生格式",
"Native forwarding": "原生转发",
"Need a redemption code?": "需要兑换码?", "Need a redemption code?": "需要兑换码?",
"Needs API key": "需要 API 密钥", "Needs API key": "需要 API 密钥",
"Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "嵌套 JSON,定义按分组添加(+:)、移除(-:)或追加可用分组的规则。", "Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.": "嵌套 JSON,定义按分组添加(+:)、移除(-:)或追加可用分组的规则。",
...@@ -2593,6 +2628,7 @@ ...@@ -2593,6 +2628,7 @@
"No API routes configured": "未配置 API 路由", "No API routes configured": "未配置 API 路由",
"No app usage data available for this model.": "该模型暂无应用使用数据。", "No app usage data available for this model.": "该模型暂无应用使用数据。",
"No apps match the selected filters": "没有匹配筛选条件的应用", "No apps match the selected filters": "没有匹配筛选条件的应用",
"No Auth": "无认证",
"No available models": "没有可用模型", "No available models": "没有可用模型",
"No available Web chat links": "没有可用的 Web 聊天链接", "No available Web chat links": "没有可用的 Web 聊天链接",
"No backup": "无备份", "No backup": "无备份",
...@@ -2767,7 +2803,14 @@ ...@@ -2767,7 +2803,14 @@
"of 3:": "共 3 个:", "of 3:": "共 3 个:",
"off": "关闭", "off": "关闭",
"Official": "官方", "Official": "官方",
"Official Claude Messages": "官方 Claude Messages",
"Official documentation": "官方说明", "Official documentation": "官方说明",
"Official Gemini from OpenAI Chat": "官方 Gemini(OpenAI Chat 入口)",
"Official Gemini Native": "官方 Gemini 原生",
"Official OpenAI Chat": "官方 OpenAI Chat",
"Official OpenAI Embeddings": "官方 OpenAI Embeddings",
"Official OpenAI Images": "官方 OpenAI Images",
"Official OpenAI Responses": "官方 OpenAI Responses",
"Official Repository": "官方仓库", "Official Repository": "官方仓库",
"Official Sync": "官方同步", "Official Sync": "官方同步",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -2814,9 +2857,24 @@ ...@@ -2814,9 +2857,24 @@
"Open theme settings": "打开主题设置", "Open theme settings": "打开主题设置",
"Open weights": "开放权重", "Open weights": "开放权重",
"OpenAI": "OpenAI", "OpenAI": "OpenAI",
"OpenAI Audio Speech": "OpenAI 语音生成",
"OpenAI Audio Transcriptions": "OpenAI 音频转录",
"OpenAI Audio Translations": "OpenAI 音频翻译",
"OpenAI Chat Completions": "OpenAI 聊天补全",
"OpenAI Chat to Anthropic Messages": "OpenAI Chat 到 Anthropic Messages",
"OpenAI Chat to Gemini Generate Content": "OpenAI Chat 到 Gemini Generate Content",
"OpenAI Chat to OpenAI Responses": "OpenAI Chat 到 OpenAI Responses",
"OpenAI Completions": "OpenAI 文本补全",
"OpenAI Compatible": "兼容 OpenAI", "OpenAI Compatible": "兼容 OpenAI",
"OpenAI Embeddings": "OpenAI 嵌入",
"OpenAI Image Edits": "OpenAI 图像编辑",
"OpenAI Image Generations": "OpenAI 图像生成",
"OpenAI Organization": "OpenAI 组织", "OpenAI Organization": "OpenAI 组织",
"OpenAI Organization ID (optional)": "OpenAI 组织 ID(可选)", "OpenAI Organization ID (optional)": "OpenAI 组织 ID(可选)",
"OpenAI Realtime": "OpenAI 实时",
"OpenAI Rerank": "OpenAI 重排序",
"OpenAI Responses": "OpenAI 响应",
"OpenAI Responses Compact": "OpenAI 响应压缩",
"OpenAI, Anthropic, etc.": "OpenAI、Anthropic 等", "OpenAI, Anthropic, etc.": "OpenAI、Anthropic 等",
"OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Google 等", "OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Google 等",
"OpenAIMax": "OpenAIMax", "OpenAIMax": "OpenAIMax",
...@@ -3217,6 +3275,7 @@ ...@@ -3217,6 +3275,7 @@
"QR code is not configured. Please contact support.": "二维码未配置。请联系支持人员。", "QR code is not configured. Please contact support.": "二维码未配置。请联系支持人员。",
"Quantity": "数量", "Quantity": "数量",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Query": "Query",
"Query Balance": "查询余额", "Query Balance": "查询余额",
"Query Param": "请求参数", "Query Param": "请求参数",
"Querying...": "正在查询...", "Querying...": "正在查询...",
...@@ -3454,8 +3513,8 @@ ...@@ -3454,8 +3513,8 @@
"Resolve Conflicts": "解决冲突", "Resolve Conflicts": "解决冲突",
"Resource Configuration": "资源配置", "Resource Configuration": "资源配置",
"Response": "响应", "Response": "响应",
"Response time: {{duration}}": "响应时间:{{duration}}",
"Response Time": "响应时间", "Response Time": "响应时间",
"Response time: {{duration}}": "响应时间:{{duration}}",
"Responses API Version": "响应 API 版本", "Responses API Version": "响应 API 版本",
"Restore defaults": "恢复默认", "Restore defaults": "恢复默认",
"Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)", "Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)",
...@@ -3493,6 +3552,7 @@ ...@@ -3493,6 +3552,7 @@
"Route Description": "路由描述", "Route Description": "路由描述",
"Route is required": "路由为必填项", "Route is required": "路由为必填项",
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示", "Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
"Routes": "路由",
"Routing & Overrides": "路由与覆盖", "Routing & Overrides": "路由与覆盖",
"Routing Strategy": "路由策略", "Routing Strategy": "路由策略",
"Rows per page": "每页行数", "Rows per page": "每页行数",
...@@ -4349,12 +4409,17 @@ ...@@ -4349,12 +4409,17 @@
"Upstream Model Updates": "上游模型更新", "Upstream Model Updates": "上游模型更新",
"Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,本次忽略 {{ignored}} 个,当前已忽略模型 {{totalIgnored}} 个", "Upstream model updates applied: {{added}} added, {{removed}} removed, {{ignored}} ignored this time, {{totalIgnored}} total ignored models": "已处理上游模型更新:加入 {{added}} 个,删除 {{removed}} 个,本次忽略 {{ignored}} 个,当前已忽略模型 {{totalIgnored}} 个",
"Upstream price sync": "上游价格同步", "Upstream price sync": "上游价格同步",
"Upstream path": "上游路径",
"Upstream path is required": "上游路径不能为空",
"Upstream path must be a full URL or a path starting with /": "上游路径必须是完整 URL,或以 / 开头的路径",
"Upstream prices fetched successfully": "已成功获取上游价格", "Upstream prices fetched successfully": "已成功获取上游价格",
"Upstream ratios fetched successfully": "上游比率获取成功", "Upstream ratios fetched successfully": "上游比率获取成功",
"Upstream Request ID": "上游请求 ID", "Upstream Request ID": "上游请求 ID",
"Upstream Response": "上游返回", "Upstream Response": "上游返回",
"upstream services integrated": "上游服务适配", "upstream services integrated": "上游服务适配",
"Upstream Updates": "上游更新", "Upstream Updates": "上游更新",
"Upstream URL": "上游 URL",
"Upstream URL must be a full URL": "上游 URL 必须是完整 URL",
"uptime": "正常运行时间", "uptime": "正常运行时间",
"Uptime": "运行时间", "Uptime": "运行时间",
"Uptime (30d)": "可用率 (30 天)", "Uptime (30d)": "可用率 (30 天)",
...@@ -4378,6 +4443,7 @@ ...@@ -4378,6 +4443,7 @@
"USD price per 1M input tokens.": "每 100 万输入 token 的美元价格。", "USD price per 1M input tokens.": "每 100 万输入 token 的美元价格。",
"USD price per 1M tokens.": "每 100 万 token 的美元价格。", "USD price per 1M tokens.": "每 100 万 token 的美元价格。",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "使用 +: 添加分组,使用 -: 移除默认可选分组,不加前缀则追加分组。", "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "使用 +: 添加分组,使用 -: 移除默认可选分组,不加前缀则追加分组。",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "填写以 / 开头的路径时会自动拼接渠道 Base URL;填写完整 URL 时,此路由会直接使用该 URL。",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "请使用支持生物识别认证或安全密钥的兼容浏览器或设备来注册通行密钥。", "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "请使用支持生物识别认证或安全密钥的兼容浏览器或设备来注册通行密钥。",
"Use authenticator code": "使用验证器代码", "Use authenticator code": "使用验证器代码",
"Use backup code": "使用备用代码", "Use backup code": "使用备用代码",
...@@ -4396,6 +4462,7 @@ ...@@ -4396,6 +4462,7 @@
"Used": "已使用", "Used": "已使用",
"Used / Remaining": "已使用 / 剩余", "Used / Remaining": "已使用 / 剩余",
"Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "用作新产品的 SuccessURL。若留空,系统会要求你确认。", "Used as SuccessURL on the new product. You'll be prompted to confirm if left blank.": "用作新产品的 SuccessURL。若留空,系统会要求你确认。",
"Used by route auth templates": "用于路由认证模板",
"Used for load balancing. Higher weight = more requests": "用于负载均衡。权重越高 = 请求越多", "Used for load balancing. Higher weight = more requests": "用于负载均衡。权重越高 = 请求越多",
"Used in URLs and API routes": "用于URL和API路由", "Used in URLs and API routes": "用于URL和API路由",
"Used Quota": "消耗额度", "Used Quota": "消耗额度",
...@@ -4590,6 +4657,7 @@ ...@@ -4590,6 +4657,7 @@
"When enabled, Midjourney callbacks are accepted (reveals server IP).": "启用时,接受 Midjourney 回调 (会泄露服务器 IP)。", "When enabled, Midjourney callbacks are accepted (reveals server IP).": "启用时,接受 Midjourney 回调 (会泄露服务器 IP)。",
"When enabled, newly created tokens start in the first auto group.": "启用后,新创建的令牌将从第一个自动分组开始。", "When enabled, newly created tokens start in the first auto group.": "启用后,新创建的令牌将从第一个自动分组开始。",
"When enabled, prompts are scanned before reaching upstream models.": "启用后,提示将在到达上游模型之前被扫描。", "When enabled, prompts are scanned before reaching upstream models.": "启用后,提示将在到达上游模型之前被扫描。",
"When enabled, requests that do not match any advanced route are forwarded to the channel base URL. When disabled, unmatched requests return an error.": "开启后,未命中任何高级路由的请求会转发到该渠道的 Base URL;关闭时,未匹配请求会返回错误。",
"When enabled, the store field will be blocked": "开启后将阻止 store 字段透传", "When enabled, the store field will be blocked": "开启后将阻止 store 字段透传",
"When enabled, users can pick this group when creating tokens.": "启用后,用户创建令牌时可以选择该分组。", "When enabled, users can pick this group when creating tokens.": "启用后,用户创建令牌时可以选择该分组。",
"When enabled, violation requests will incur additional charges.": "开启后,违规请求将额外扣费。", "When enabled, violation requests will incur additional charges.": "开启后,违规请求将额外扣费。",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment