feat: advanced custom channel (#5590)
This commit is contained in:
+189
-17
@@ -1,5 +1,11 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ChannelSettings struct {
|
||||
ForceFormat bool `json:"force_format,omitempty"`
|
||||
ThinkingToContent bool `json:"thinking_to_content,omitempty"`
|
||||
@@ -24,23 +30,24 @@ const (
|
||||
)
|
||||
|
||||
type ChannelOtherSettings struct {
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
|
||||
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规
|
||||
AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
|
||||
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
|
||||
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
|
||||
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新
|
||||
UpstreamModelUpdateLastCheckTime int64 `json:"upstream_model_update_last_check_time,omitempty"` // 上次检测时间
|
||||
UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型
|
||||
UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型
|
||||
UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
|
||||
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规
|
||||
AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
|
||||
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
|
||||
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
|
||||
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新
|
||||
UpstreamModelUpdateLastCheckTime int64 `json:"upstream_model_update_last_check_time,omitempty"` // 上次检测时间
|
||||
UpstreamModelUpdateLastDetectedModels []string `json:"upstream_model_update_last_detected_models,omitempty"` // 上次检测到的可加入模型
|
||||
UpstreamModelUpdateLastRemovedModels []string `json:"upstream_model_update_last_removed_models,omitempty"` // 上次检测到的可删除模型
|
||||
UpstreamModelUpdateIgnoredModels []string `json:"upstream_model_update_ignored_models,omitempty"` // 手动忽略的模型
|
||||
AdvancedCustom *AdvancedCustomConfig `json:"advanced_custom,omitempty"`
|
||||
}
|
||||
|
||||
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
|
||||
@@ -49,3 +56,168 @@ func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user