diff --git a/config.yaml.example b/config.yaml.example index ef8a520..3f49848 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -22,6 +22,17 @@ endpoints: enabled: true priority: 2 + # OpenAI Compatible 端点示例 + # - name: openai-compatible-example + # url: https://api.example.com + # endpoint_type: openai + # path_prefix: /v1/chat/completions # OpenAI 兼容端点的 API 路径 + # auth_type: auth_token + # auth_value: your-bearer-token-here + # enabled: true + # priority: 3 + # reorder_system_messages_first: false # 仅对 OpenAI Compatible 端点生效,将 system 消息重排到 messages 最前面(默认:false) + logging: level: info # debug | info | warn | error log_request_types: failed # failed | success | all diff --git a/internal/config/types.go b/internal/config/types.go index d027127..f00b9e5 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -1,13 +1,13 @@ package config type Config struct { - Server ServerConfig `yaml:"server"` - Endpoints []EndpointConfig `yaml:"endpoints"` - Logging LoggingConfig `yaml:"logging"` - Validation ValidationConfig `yaml:"validation"` - Tagging TaggingConfig `yaml:"tagging"` // 标签系统配置(永远启用) - Timeouts TimeoutConfig `yaml:"timeouts"` // 超时配置 - I18n I18nConfig `yaml:"i18n"` // 国际化配置 + Server ServerConfig `yaml:"server"` + Endpoints []EndpointConfig `yaml:"endpoints"` + Logging LoggingConfig `yaml:"logging"` + Validation ValidationConfig `yaml:"validation"` + Tagging TaggingConfig `yaml:"tagging"` // 标签系统配置(永远启用) + Timeouts TimeoutConfig `yaml:"timeouts"` // 超时配置 + I18n I18nConfig `yaml:"i18n"` // 国际化配置 } // I18nConfig 国际化配置 @@ -23,43 +23,44 @@ type ServerConfig struct { } type EndpointConfig struct { - Name string `yaml:"name"` - URL string `yaml:"url"` - EndpointType string `yaml:"endpoint_type"` // "anthropic" | "openai" 等 - PathPrefix string `yaml:"path_prefix,omitempty"` // OpenAI端点的路径前缀,如 "/v1/chat/completions" - AuthType string `yaml:"auth_type"` - AuthValue string `yaml:"auth_value"` - Enabled bool `yaml:"enabled"` - Priority int `yaml:"priority"` - Tags []string `yaml:"tags"` // 新增:支持的tag列表 - ModelRewrite *ModelRewriteConfig `yaml:"model_rewrite,omitempty"` // 新增:模型重写配置 - Proxy *ProxyConfig `yaml:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *OAuthConfig `yaml:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `yaml:"header_overrides,omitempty" json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `yaml:"parameter_overrides,omitempty" json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 - MaxTokensFieldName string `yaml:"max_tokens_field_name,omitempty" json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 - RateLimitReset *int64 `yaml:"rate_limit_reset,omitempty" json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset - RateLimitStatus *string `yaml:"rate_limit_status,omitempty" json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status - EnhancedProtection bool `yaml:"enhanced_protection,omitempty" json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 + Name string `yaml:"name"` + URL string `yaml:"url"` + EndpointType string `yaml:"endpoint_type"` // "anthropic" | "openai" 等 + PathPrefix string `yaml:"path_prefix,omitempty"` // OpenAI端点的路径前缀,如 "/v1/chat/completions" + AuthType string `yaml:"auth_type"` + AuthValue string `yaml:"auth_value"` + Enabled bool `yaml:"enabled"` + Priority int `yaml:"priority"` + Tags []string `yaml:"tags"` // 新增:支持的tag列表 + ModelRewrite *ModelRewriteConfig `yaml:"model_rewrite,omitempty"` // 新增:模型重写配置 + Proxy *ProxyConfig `yaml:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *OAuthConfig `yaml:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `yaml:"header_overrides,omitempty" json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `yaml:"parameter_overrides,omitempty" json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 + MaxTokensFieldName string `yaml:"max_tokens_field_name,omitempty" json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 + RateLimitReset *int64 `yaml:"rate_limit_reset,omitempty" json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset + RateLimitStatus *string `yaml:"rate_limit_status,omitempty" json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status + EnhancedProtection bool `yaml:"enhanced_protection,omitempty" json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 + ReorderSystemMessagesFirst *bool `yaml:"reorder_system_messages_first,omitempty" json:"reorder_system_messages_first,omitempty"` // 仅对OpenAI端点生效:将system消息重排到messages最前面 } // 新增:代理配置结构 type ProxyConfig struct { - Type string `yaml:"type" json:"type"` // "http" | "socks5" - Address string `yaml:"address" json:"address"` // 代理服务器地址,如 "127.0.0.1:1080" + Type string `yaml:"type" json:"type"` // "http" | "socks5" + Address string `yaml:"address" json:"address"` // 代理服务器地址,如 "127.0.0.1:1080" Username string `yaml:"username,omitempty" json:"username,omitempty"` // 代理认证用户名(可选) Password string `yaml:"password,omitempty" json:"password,omitempty"` // 代理认证密码(可选) } // 新增:OAuth 配置结构 type OAuthConfig struct { - AccessToken string `yaml:"access_token" json:"access_token"` // 访问令牌 - RefreshToken string `yaml:"refresh_token" json:"refresh_token"` // 刷新令牌 - ExpiresAt int64 `yaml:"expires_at" json:"expires_at"` // 过期时间戳(毫秒) - TokenURL string `yaml:"token_url" json:"token_url"` // Token刷新URL(必填) - ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"` // 客户端ID - Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` // 权限范围 - AutoRefresh bool `yaml:"auto_refresh" json:"auto_refresh"` // 是否自动刷新 + AccessToken string `yaml:"access_token" json:"access_token"` // 访问令牌 + RefreshToken string `yaml:"refresh_token" json:"refresh_token"` // 刷新令牌 + ExpiresAt int64 `yaml:"expires_at" json:"expires_at"` // 过期时间戳(毫秒) + TokenURL string `yaml:"token_url" json:"token_url"` // Token刷新URL(必填) + ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"` // 客户端ID + Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` // 权限范围 + AutoRefresh bool `yaml:"auto_refresh" json:"auto_refresh"` // 是否自动刷新 } // 新增:模型重写配置结构 @@ -83,23 +84,23 @@ type LoggingConfig struct { } type ValidationConfig struct { - PythonJSONFixing PythonJSONFixingConfig `yaml:"python_json_fixing"` + PythonJSONFixing PythonJSONFixingConfig `yaml:"python_json_fixing"` } // PythonJSONFixing 配置结构 type PythonJSONFixingConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用 Python JSON 修复 - TargetTools []string `yaml:"target_tools" json:"target_tools"` // 需要修复的工具列表 - DebugLogging bool `yaml:"debug_logging" json:"debug_logging"` // 是否启用调试日志 - MaxAttempts int `yaml:"max_attempts" json:"max_attempts"` // 最大修复尝试次数 + Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用 Python JSON 修复 + TargetTools []string `yaml:"target_tools" json:"target_tools"` // 需要修复的工具列表 + DebugLogging bool `yaml:"debug_logging" json:"debug_logging"` // 是否启用调试日志 + MaxAttempts int `yaml:"max_attempts" json:"max_attempts"` // 最大修复尝试次数 } // 新增:超时配置结构 type TimeoutConfig struct { // 网络超时设置(代理和健康检查共用) - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` // TLS握手超时,默认10s - ResponseHeader string `yaml:"response_header" json:"response_header"` // 响应头超时,默认60s - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` // 空闲连接超时,默认90s + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` // TLS握手超时,默认10s + ResponseHeader string `yaml:"response_header" json:"response_header"` // 响应头超时,默认60s + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` // 空闲连接超时,默认90s // 健康检查特有配置 HealthCheckTimeout string `yaml:"health_check_timeout" json:"health_check_timeout"` // 健康检查整体响应超时,默认30s CheckInterval string `yaml:"check_interval" json:"check_interval"` // 健康检查间隔,默认30s @@ -108,19 +109,19 @@ type TimeoutConfig struct { // 代理客户端超时配置(内部使用,从TimeoutConfig转换) type ProxyTimeoutConfig struct { - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` - ResponseHeader string `yaml:"response_header" json:"response_header"` - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` - OverallRequest string `yaml:"overall_request" json:"overall_request"` // 保持为空,无限制 + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` + ResponseHeader string `yaml:"response_header" json:"response_header"` + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` + OverallRequest string `yaml:"overall_request" json:"overall_request"` // 保持为空,无限制 } // 健康检查超时配置(内部使用,从TimeoutConfig转换) type HealthCheckTimeoutConfig struct { - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` - ResponseHeader string `yaml:"response_header" json:"response_header"` - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` - OverallRequest string `yaml:"overall_request" json:"overall_request"` - CheckInterval string `yaml:"check_interval" json:"check_interval"` + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` + ResponseHeader string `yaml:"response_header" json:"response_header"` + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` + OverallRequest string `yaml:"overall_request" json:"overall_request"` + CheckInterval string `yaml:"check_interval" json:"check_interval"` RecoveryThreshold int `yaml:"recovery_threshold" json:"recovery_threshold"` } @@ -148,8 +149,8 @@ func (tc *TimeoutConfig) ToHealthCheckTimeoutConfig() HealthCheckTimeoutConfig { // Tag系统配置结构 (永远启用) type TaggingConfig struct { - PipelineTimeout string `yaml:"pipeline_timeout"` - Taggers []TaggerConfig `yaml:"taggers"` + PipelineTimeout string `yaml:"pipeline_timeout"` + Taggers []TaggerConfig `yaml:"taggers"` } type TaggerConfig struct { @@ -158,6 +159,6 @@ type TaggerConfig struct { BuiltinType string `yaml:"builtin_type"` // 内置类型: "path" | "header" | "body-json" | "method" | "query" Tag string `yaml:"tag"` // 标记的tag名称 Enabled bool `yaml:"enabled"` - Priority int `yaml:"priority"` // 执行优先级(未使用,因为并发执行) - Config map[string]interface{} `yaml:"config"` // tagger特定配置 -} \ No newline at end of file + Priority int `yaml:"priority"` // 执行优先级(未使用,因为并发执行) + Config map[string]interface{} `yaml:"config"` // tagger特定配置 +} diff --git a/internal/conversion/request_converter.go b/internal/conversion/request_converter.go index f20466e..ac3c215 100644 --- a/internal/conversion/request_converter.go +++ b/internal/conversion/request_converter.go @@ -30,7 +30,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn return nil, nil, NewConversionError("parse_error", "Failed to parse Anthropic request", err) } - // 创建转换上下文 + // 创建转换上下文 ctx := &ConversionContext{ ToolCallIDMap: make(map[string]string), IsStreaming: anthReq.Stream != nil && *anthReq.Stream, @@ -46,7 +46,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn // 温控映射 out.Temperature = anthReq.Temperature out.TopP = anthReq.TopP - + // 根据端点配置处理 max_tokens 字段名转换 if endpointInfo != nil && endpointInfo.MaxTokensFieldName != "" { // 根据配置的字段名设置对应字段 @@ -133,7 +133,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn // 用户消息可以包含 text / image / tool_result // 其中 tool_result 需转成 role:"tool" // 其他(text/image)转为 role:"user" - // + // // 重要:为了确保相同 ID 的 assistant 和 tool 消息紧挨着, // 我们需要先输出所有 tool_result,然后再输出 user 消息 // 使用新的 GetContentBlocks 方法获取内容块 @@ -147,7 +147,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn userBlocks = append(userBlocks, bl) } } - + // 先处理 tool_result -> role:"tool" // 这样确保 assistant 和 tool 消息紧挨着 for _, tr := range toolResults { @@ -157,7 +157,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn // 因上下文不可靠,这里严格要求 tool_use_id 存在: return nil, nil, errors.New("user.tool_result is missing tool_use_id") } - + // 提取 tool_result 的内容 var content string switch v := tr.Content.(type) { @@ -189,14 +189,14 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn default: content = "" } - + out.Messages = append(out.Messages, OpenAIMessage{ Role: "tool", ToolCallID: tr.ToolUseID, Content: strings.TrimSpace(content), }) } - + // 然后处理 user 内容(text/image) if len(userBlocks) > 0 { om := OpenAIMessage{Role: "user"} @@ -302,7 +302,7 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn // 根据 budget_tokens 映射推理强度 if anthReq.Thinking.BudgetTokens > 0 { out.MaxReasoningTokens = &anthReq.Thinking.BudgetTokens - + // 根据 budget_tokens 的大小设置推理强度 if anthReq.Thinking.BudgetTokens <= 5000 { out.ReasoningEffort = stringPtr("low") @@ -315,10 +315,10 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn // 如果没有指定 budget_tokens,使用默认的 medium 强度 out.ReasoningEffort = stringPtr("medium") } - + if c.logger != nil { c.logger.Debug("Converted thinking mode to OpenAI reasoning mode", map[string]interface{}{ - "budget_tokens": anthReq.Thinking.BudgetTokens, + "budget_tokens": anthReq.Thinking.BudgetTokens, "reasoning_effort": *out.ReasoningEffort, }) } @@ -331,6 +331,14 @@ func (c *RequestConverter) Convert(anthropicReq []byte, endpointInfo *EndpointIn } } + // 如果需要重排 system 消息到最前面 + if endpointInfo != nil && endpointInfo.ReorderSystemMessagesFirst { + out.Messages = reorderSystemMessagesFirst(out.Messages) + if c.logger != nil { + c.logger.Debug("Reordered system messages to the front of messages array") + } + } + // 序列化结果 result, err := json.Marshal(out) if err != nil { @@ -401,4 +409,36 @@ func (c *RequestConverter) anthropicSystemToText(sys interface{}) string { } return "" } -} \ No newline at end of file +} + +// reorderSystemMessagesFirst 将所有 system 消息重排到 messages 数组的最前面 +// 保持 system 消息之间的原有顺序,以及非 system 消息之间的原有顺序 +func reorderSystemMessagesFirst(messages []OpenAIMessage) []OpenAIMessage { + if len(messages) == 0 { + return messages + } + + var systemMessages []OpenAIMessage + var nonSystemMessages []OpenAIMessage + + // 分离 system 消息和非 system 消息 + for _, msg := range messages { + if msg.Role == "system" { + systemMessages = append(systemMessages, msg) + } else { + nonSystemMessages = append(nonSystemMessages, msg) + } + } + + // 如果没有 system 消息,直接返回原数组 + if len(systemMessages) == 0 { + return messages + } + + // 将 system 消息放在最前面,然后是非 system 消息 + result := make([]OpenAIMessage, 0, len(messages)) + result = append(result, systemMessages...) + result = append(result, nonSystemMessages...) + + return result +} diff --git a/internal/conversion/types.go b/internal/conversion/types.go index 85cdc83..b408624 100644 --- a/internal/conversion/types.go +++ b/internal/conversion/types.go @@ -2,33 +2,33 @@ package conversion // EndpointInfo 包含转换器需要的端点信息 type EndpointInfo struct { - Type string - MaxTokensFieldName string + Type string + MaxTokensFieldName string + ReorderSystemMessagesFirst bool } // Converter 定义转换器接口 type Converter interface { // 转换请求 ConvertRequest(anthropicReq []byte, endpointInfo *EndpointInfo) ([]byte, *ConversionContext, error) - + // 转换响应 ConvertResponse(openaiResp []byte, ctx *ConversionContext, isStreaming bool) ([]byte, error) - + // 检查是否需要转换 ShouldConvert(endpointType string) bool } // ConversionContext 转换上下文 type ConversionContext struct { - EndpointType string // "anthropic" | "openai" - ToolCallIDMap map[string]string // 工具调用ID映射 (Anthropic ID -> OpenAI ID) - IsStreaming bool // 是否为流式请求 - RequestHeaders map[string]string // 原始请求头 - StopSequences []string // 请求中的停止序列,用于响应时检测 + EndpointType string // "anthropic" | "openai" + ToolCallIDMap map[string]string // 工具调用ID映射 (Anthropic ID -> OpenAI ID) + IsStreaming bool // 是否为流式请求 + RequestHeaders map[string]string // 原始请求头 + StopSequences []string // 请求中的停止序列,用于响应时检测 // 注意:不包含模型映射,因为转换发生在模型重写之后 } - // ConversionError 转换错误 type ConversionError struct { Type string // "parse_error", "unsupported_feature", "tool_conversion_error" @@ -54,4 +54,4 @@ func NewConversionError(errorType, message string, err error) *ConversionError { Message: message, Err: err, } -} \ No newline at end of file +} diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go index d88105f..2c67fe4 100644 --- a/internal/endpoint/endpoint.go +++ b/internal/endpoint/endpoint.go @@ -27,10 +27,10 @@ const ( type BlacklistReason struct { // 导致失效的请求ID列表 CausingRequestIDs []string `json:"causing_request_ids"` - + // 失效时间 BlacklistedAt time.Time `json:"blacklisted_at"` - + // 失效时的错误信息摘要 ErrorSummary string `json:"error_summary"` } @@ -38,73 +38,75 @@ type BlacklistReason struct { // 删除不再需要的 RequestRecord 定义,因为已经移到 utils 包 type Endpoint struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" 等 - PathPrefix string `json:"path_prefix,omitempty"` // OpenAI端点的路径前缀 - AuthType string `json:"auth_type"` - AuthValue string `json:"auth_value"` - Enabled bool `json:"enabled"` - Priority int `json:"priority"` - Tags []string `json:"tags"` // 新增:支持的tag列表 - ModelRewrite *config.ModelRewriteConfig `json:"model_rewrite,omitempty"` // 新增:模型重写配置 - Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 - MaxTokensFieldName string `json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 - RateLimitReset *int64 `json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset - RateLimitStatus *string `json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status - EnhancedProtection bool `json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 - Status Status `json:"status"` - LastCheck time.Time `json:"last_check"` - FailureCount int `json:"failure_count"` - TotalRequests int `json:"total_requests"` - SuccessRequests int `json:"success_requests"` - LastFailure time.Time `json:"last_failure"` - SuccessiveSuccesses int `json:"successive_successes"` // 连续成功次数 - RequestHistory *utils.CircularBuffer `json:"-"` // 使用环形缓冲区,不导出到JSON - + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" 等 + PathPrefix string `json:"path_prefix,omitempty"` // OpenAI端点的路径前缀 + AuthType string `json:"auth_type"` + AuthValue string `json:"auth_value"` + Enabled bool `json:"enabled"` + Priority int `json:"priority"` + Tags []string `json:"tags"` // 新增:支持的tag列表 + ModelRewrite *config.ModelRewriteConfig `json:"model_rewrite,omitempty"` // 新增:模型重写配置 + Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 + MaxTokensFieldName string `json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 + RateLimitReset *int64 `json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset + RateLimitStatus *string `json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status + EnhancedProtection bool `json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 + ReorderSystemMessagesFirst *bool `json:"reorder_system_messages_first,omitempty"` // 仅对OpenAI端点生效:将system消息重排到messages最前面 + Status Status `json:"status"` + LastCheck time.Time `json:"last_check"` + FailureCount int `json:"failure_count"` + TotalRequests int `json:"total_requests"` + SuccessRequests int `json:"success_requests"` + LastFailure time.Time `json:"last_failure"` + SuccessiveSuccesses int `json:"successive_successes"` // 连续成功次数 + RequestHistory *utils.CircularBuffer `json:"-"` // 使用环形缓冲区,不导出到JSON + // 新增:被拉黑的原因(内存中,不持久化) BlacklistReason *BlacklistReason `json:"-"` - + // 新增:保护 BlacklistReason 的互斥锁 blacklistMutex sync.RWMutex - + // 新增:上次记录跳过健康检查日志的时间(用于减少日志频率) lastSkipLogTime time.Time `json:"-"` - - mutex sync.RWMutex + + mutex sync.RWMutex } func NewEndpoint(cfg config.EndpointConfig) *Endpoint { // 如果没有指定 endpoint_type,使用统一默认值 endpointType := config.GetStringWithDefault(cfg.EndpointType, config.Default.Endpoint.Type) - + return &Endpoint{ - ID: generateID(cfg.Name), - Name: cfg.Name, - URL: cfg.URL, - EndpointType: endpointType, - PathPrefix: cfg.PathPrefix, // 新增:复制PathPrefix - AuthType: cfg.AuthType, - AuthValue: cfg.AuthValue, - Enabled: config.GetBoolWithDefault(cfg.Enabled, true, config.Default.Endpoint.Enabled), - Priority: config.GetIntWithDefault(cfg.Priority, config.Default.Endpoint.Priority), - Tags: cfg.Tags, // 新增:从配置中复制tags - ModelRewrite: cfg.ModelRewrite, // 新增:从配置中复制模型重写配置 - Proxy: cfg.Proxy, // 新增:从配置中复制代理配置 - OAuthConfig: cfg.OAuthConfig, // 新增:从配置中复制OAuth配置 - HeaderOverrides: cfg.HeaderOverrides, // 新增:从配置中复制HTTP Header覆盖配置 - ParameterOverrides: cfg.ParameterOverrides, // 新增:从配置中复制Request Parameters覆盖配置 - MaxTokensFieldName: cfg.MaxTokensFieldName, // 新增:从配置中复制max_tokens参数名转换选项 - RateLimitReset: cfg.RateLimitReset, // 新增:从配置加载rate limit reset状态 - RateLimitStatus: cfg.RateLimitStatus, // 新增:从配置加载rate limit status状态 - EnhancedProtection: cfg.EnhancedProtection, // 新增:从配置加载官方帐号增强保护设置 - Status: StatusActive, - LastCheck: time.Now(), - RequestHistory: utils.NewCircularBuffer(100, 140*time.Second), // 100个记录,140秒窗口 + ID: generateID(cfg.Name), + Name: cfg.Name, + URL: cfg.URL, + EndpointType: endpointType, + PathPrefix: cfg.PathPrefix, // 新增:复制PathPrefix + AuthType: cfg.AuthType, + AuthValue: cfg.AuthValue, + Enabled: config.GetBoolWithDefault(cfg.Enabled, true, config.Default.Endpoint.Enabled), + Priority: config.GetIntWithDefault(cfg.Priority, config.Default.Endpoint.Priority), + Tags: cfg.Tags, // 新增:从配置中复制tags + ModelRewrite: cfg.ModelRewrite, // 新增:从配置中复制模型重写配置 + Proxy: cfg.Proxy, // 新增:从配置中复制代理配置 + OAuthConfig: cfg.OAuthConfig, // 新增:从配置中复制OAuth配置 + HeaderOverrides: cfg.HeaderOverrides, // 新增:从配置中复制HTTP Header覆盖配置 + ParameterOverrides: cfg.ParameterOverrides, // 新增:从配置中复制Request Parameters覆盖配置 + MaxTokensFieldName: cfg.MaxTokensFieldName, // 新增:从配置中复制max_tokens参数名转换选项 + RateLimitReset: cfg.RateLimitReset, // 新增:从配置加载rate limit reset状态 + RateLimitStatus: cfg.RateLimitStatus, // 新增:从配置加载rate limit status状态 + EnhancedProtection: cfg.EnhancedProtection, // 新增:从配置加载官方帐号增强保护设置 + ReorderSystemMessagesFirst: cfg.ReorderSystemMessagesFirst, // 新增:从配置加载system消息重排设置 + Status: StatusActive, + LastCheck: time.Now(), + RequestHistory: utils.NewCircularBuffer(100, 140*time.Second), // 100个记录,140秒窗口 } } @@ -134,12 +136,12 @@ func (e *Endpoint) GetAuthHeader() (string, error) { if e.OAuthConfig == nil { return "", fmt.Errorf("oauth config is required for oauth auth_type") } - + // 检查 token 是否需要刷新 if oauth.IsTokenExpired(e.OAuthConfig) { return "", fmt.Errorf("oauth token expired, refresh required") } - + return oauth.GetAuthorizationHeader(e.OAuthConfig), nil default: return e.AuthValue, nil @@ -149,7 +151,7 @@ func (e *Endpoint) GetAuthHeader() (string, error) { func (e *Endpoint) GetTags() []string { e.mutex.RLock() defer e.mutex.RUnlock() - + // 返回tags的副本以避免并发修改 tags := make([]string, len(e.Tags)) copy(tags, e.Tags) @@ -160,11 +162,11 @@ func (e *Endpoint) GetTags() []string { func (e *Endpoint) GetHeaderOverrides() map[string]string { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.HeaderOverrides == nil { return nil } - + // 返回HeaderOverrides的副本以避免并发修改 overrides := make(map[string]string, len(e.HeaderOverrides)) for k, v := range e.HeaderOverrides { @@ -177,11 +179,11 @@ func (e *Endpoint) GetHeaderOverrides() map[string]string { func (e *Endpoint) GetParameterOverrides() map[string]string { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.ParameterOverrides == nil { return nil } - + // 返回ParameterOverrides的副本以避免并发修改 overrides := make(map[string]string, len(e.ParameterOverrides)) for k, v := range e.ParameterOverrides { @@ -194,10 +196,10 @@ func (e *Endpoint) GetParameterOverrides() map[string]string { func (e *Endpoint) ToTaggedEndpoint() interfaces.TaggedEndpoint { e.mutex.RLock() defer e.mutex.RUnlock() - + tags := make([]string, len(e.Tags)) copy(tags, e.Tags) - + return interfaces.TaggedEndpoint{ Name: e.Name, URL: e.URL, @@ -210,10 +212,10 @@ func (e *Endpoint) ToTaggedEndpoint() interfaces.TaggedEndpoint { func (e *Endpoint) GetFullURL(path string) string { e.mutex.RLock() defer e.mutex.RUnlock() - + // 直接使用端点的URL作为基础URL baseURL := e.URL - + // 根据端点类型自动添加正确的路径前缀 switch e.EndpointType { case "anthropic": @@ -234,7 +236,7 @@ func (e *Endpoint) IsAvailable() bool { enabled := e.Enabled status := e.Status e.mutex.RUnlock() - + return enabled && status == StatusActive } @@ -243,7 +245,7 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { defer e.mutex.Unlock() now := time.Now() - + // 添加到环形缓冲区(包含请求ID) record := utils.RequestRecord{ Timestamp: now, @@ -251,11 +253,11 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { RequestID: requestID, } e.RequestHistory.Add(record) - + e.TotalRequests++ if success { e.SuccessRequests++ - e.FailureCount = 0 // 重置失败计数 + e.FailureCount = 0 // 重置失败计数 e.SuccessiveSuccesses++ // 增加连续成功次数 // 如果成功且之前是不可用状态,恢复为可用 if e.Status == StatusInactive { @@ -268,7 +270,7 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { e.FailureCount++ e.LastFailure = now e.SuccessiveSuccesses = 0 // 重置连续成功次数 - + // 使用环形缓冲区检查是否应该标记为不可用 if e.Status == StatusActive && e.RequestHistory.ShouldMarkInactive(now) { // 释放 mutex 以避免死锁,因为 MarkInactiveWithReason 需要获取 mutex @@ -289,13 +291,13 @@ func (e *Endpoint) MarkInactive() { func (e *Endpoint) MarkInactiveWithReason() { e.mutex.Lock() defer e.mutex.Unlock() - + if e.Status == StatusActive { e.Status = StatusInactive - + // 从循环缓冲区获取导致失效的请求ID failedRequestIDs := e.RequestHistory.GetRecentFailureRequestIDs(time.Now()) - + // 构建失效原因记录 e.blacklistMutex.Lock() e.BlacklistReason = &BlacklistReason{ @@ -313,15 +315,15 @@ func (e *Endpoint) MarkActive() { e.Status = StatusActive e.FailureCount = 0 e.SuccessiveSuccesses = 0 // 重置连续成功次数 - + // 清除失效原因记录 e.blacklistMutex.Lock() e.BlacklistReason = nil e.blacklistMutex.Unlock() - + // 重置跳过健康检查日志时间,确保下次rate limit时能立即记录 e.lastSkipLogTime = time.Time{} - + // 清理历史记录 e.RequestHistory.Clear() } @@ -332,7 +334,6 @@ func (e *Endpoint) GetSuccessiveSuccesses() int { return e.SuccessiveSuccesses } - func generateID(name string) string { // Use stable ID based on endpoint name hash for statistics persistence return statistics.GenerateEndpointID(name) @@ -354,7 +355,7 @@ func (e *Endpoint) CreateProxyClient(timeoutConfig config.ProxyTimeoutConfig) (* e.mutex.RLock() proxyConfig := e.Proxy e.mutex.RUnlock() - + factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ Type: httpclient.ClientTypeEndpoint, @@ -366,7 +367,7 @@ func (e *Endpoint) CreateProxyClient(timeoutConfig config.ProxyTimeoutConfig) (* }, ProxyConfig: proxyConfig, } - + return factory.CreateClient(clientConfig) } @@ -375,7 +376,7 @@ func (e *Endpoint) CreateHealthClient(timeoutConfig config.HealthCheckTimeoutCon e.mutex.RLock() proxyConfig := e.Proxy e.mutex.RUnlock() - + factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ Type: httpclient.ClientTypeHealth, @@ -387,7 +388,7 @@ func (e *Endpoint) CreateHealthClient(timeoutConfig config.HealthCheckTimeoutCon }, ProxyConfig: proxyConfig, } - + return factory.CreateClient(clientConfig) } @@ -400,15 +401,15 @@ func (e *Endpoint) RefreshOAuthToken(timeoutConfig config.ProxyTimeoutConfig) er func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeoutConfig, onTokenRefreshed func(*Endpoint) error) error { e.mutex.Lock() defer e.mutex.Unlock() - + if e.AuthType != "oauth" { return fmt.Errorf("endpoint is not configured for oauth authentication") } - + if e.OAuthConfig == nil { return fmt.Errorf("oauth config is nil") } - + // 创建HTTP客户端用于刷新请求 factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ @@ -421,21 +422,21 @@ func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeo }, ProxyConfig: e.Proxy, } - + client, err := factory.CreateClient(clientConfig) if err != nil { return fmt.Errorf("failed to create http client for token refresh: %v", err) } - + // 刷新token newOAuthConfig, err := oauth.RefreshToken(e.OAuthConfig, client) if err != nil { return fmt.Errorf("failed to refresh oauth token: %v", err) } - + // 更新配置 e.OAuthConfig = newOAuthConfig - + // 如果提供了回调函数,调用它来处理配置持久化 if onTokenRefreshed != nil { if err := onTokenRefreshed(e); err != nil { @@ -443,7 +444,7 @@ func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeo return fmt.Errorf("oauth token refreshed successfully but failed to persist to config file: %v", err) } } - + return nil } @@ -456,7 +457,7 @@ func (e *Endpoint) GetAuthHeaderWithRefresh(timeoutConfig config.ProxyTimeoutCon func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTimeoutConfig, onTokenRefreshed func(*Endpoint) error) (string, error) { // 首先尝试获取认证头部 authHeader, err := e.GetAuthHeader() - + if e.AuthType == "oauth" { if err != nil { // 如果获取失败且token确实过期,尝试刷新 @@ -470,7 +471,7 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi // 如果不是因为过期导致的错误,直接返回错误 return "", err } - + // 即使获取成功,也检查是否应该主动刷新 if oauth.ShouldRefreshToken(e.OAuthConfig) { // 主动刷新,但如果失败不影响当前请求 @@ -485,7 +486,7 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi } } } - + return authHeader, err } @@ -493,11 +494,11 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi func (e *Endpoint) GetBlacklistReason() *BlacklistReason { e.blacklistMutex.RLock() defer e.blacklistMutex.RUnlock() - + if e.BlacklistReason == nil { return nil } - + // 返回深度拷贝以避免并发修改 return &BlacklistReason{ CausingRequestIDs: append([]string{}, e.BlacklistReason.CausingRequestIDs...), @@ -510,30 +511,30 @@ func (e *Endpoint) GetBlacklistReason() *BlacklistReason { func (e *Endpoint) UpdateRateLimitState(reset *int64, status *string) (bool, error) { e.mutex.Lock() defer e.mutex.Unlock() - + // 检查是否有变化 changed := false - + // 比较reset值 if (e.RateLimitReset == nil) != (reset == nil) { changed = true } else if e.RateLimitReset != nil && reset != nil && *e.RateLimitReset != *reset { changed = true } - + // 比较status值 if (e.RateLimitStatus == nil) != (status == nil) { changed = true } else if e.RateLimitStatus != nil && status != nil && *e.RateLimitStatus != *status { changed = true } - + // 如果有变化,更新状态 if changed { e.RateLimitReset = reset e.RateLimitStatus = status } - + return changed, nil } @@ -541,20 +542,20 @@ func (e *Endpoint) UpdateRateLimitState(reset *int64, status *string) (bool, err func (e *Endpoint) GetRateLimitState() (*int64, *string) { e.mutex.RLock() defer e.mutex.RUnlock() - + var reset *int64 var status *string - + if e.RateLimitReset != nil { resetCopy := *e.RateLimitReset reset = &resetCopy } - + if e.RateLimitStatus != nil { statusCopy := *e.RateLimitStatus status = &statusCopy } - + return reset, status } @@ -574,17 +575,17 @@ func (e *Endpoint) ShouldMonitorRateLimit() bool { func (e *Endpoint) ShouldSkipHealthCheckUntilReset() bool { e.mutex.RLock() defer e.mutex.RUnlock() - + // 1. 必须是Anthropic官方端点 if !strings.Contains(strings.ToLower(e.URL), "api.anthropic.com") { return false } - + // 2. 必须有rate limit reset信息 if e.RateLimitReset == nil { return false } - + // 3. 当前时间必须小于reset时间 currentTime := time.Now().Unix() return currentTime < *e.RateLimitReset @@ -594,11 +595,11 @@ func (e *Endpoint) ShouldSkipHealthCheckUntilReset() bool { func (e *Endpoint) GetRateLimitResetTimeRemaining() int64 { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.RateLimitReset == nil { return 0 } - + currentTime := time.Now().Unix() remaining := *e.RateLimitReset - currentTime if remaining < 0 { @@ -612,7 +613,7 @@ func (e *Endpoint) GetRateLimitResetTimeRemaining() int64 { func (e *Endpoint) ShouldLogSkipHealthCheck() bool { e.mutex.Lock() defer e.mutex.Unlock() - + now := time.Now() // 如果从未记录过,或者距离上次记录超过5分钟,则应该记录 if e.lastSkipLogTime.IsZero() || now.Sub(e.lastSkipLogTime) >= 5*time.Minute { @@ -630,21 +631,21 @@ func (e *Endpoint) ShouldLogSkipHealthCheck() bool { func (e *Endpoint) ShouldDisableOnAllowedWarning() bool { e.mutex.RLock() defer e.mutex.RUnlock() - + // 必须启用增强保护 if !e.EnhancedProtection { return false } - + // 必须是Anthropic官方端点 if !strings.Contains(strings.ToLower(e.URL), "api.anthropic.com") { return false } - + // 必须有rate limit status信息且为allowed_warning if e.RateLimitStatus == nil || *e.RateLimitStatus != "allowed_warning" { return false } - + return true -} \ No newline at end of file +} diff --git a/internal/proxy/proxy_logic.go b/internal/proxy/proxy_logic.go index f12d64f..cc6731e 100644 --- a/internal/proxy/proxy_logic.go +++ b/internal/proxy/proxy_logic.go @@ -97,8 +97,9 @@ func (s *Server) proxyToEndpoint(c *gin.Context, ep *endpoint.Endpoint, path str // 创建端点信息 endpointInfo := &conversion.EndpointInfo{ - Type: ep.EndpointType, - MaxTokensFieldName: ep.MaxTokensFieldName, + Type: ep.EndpointType, + MaxTokensFieldName: ep.MaxTokensFieldName, + ReorderSystemMessagesFirst: ep.ReorderSystemMessagesFirst != nil && *ep.ReorderSystemMessagesFirst, } convertedBody, ctx, err := s.converter.ConvertRequest(finalRequestBody, endpointInfo) diff --git a/web/locales/de.json b/web/locales/de.json index 9453fbf..8466b47 100644 --- a/web/locales/de.json +++ b/web/locales/de.json @@ -670,6 +670,12 @@ "export_failed_error": "Export fehlgeschlagen", "http_header_content": "HTTP-Header-Inhalt", "fallback_support": "Umgebungsvariablen-Fallback-Unterstützung bei Fehlern", - "original_request_headers": "Ursprüngliche Request-Header" + "original_request_headers": "Ursprüngliche Request-Header", + "reorder_system_messages_first_configuration": "System-Nachrichten Neuordnungskonfiguration", + "enable_reorder_system_messages_first": "System-Nachrichten Neuordnung aktivieren", + "reorder_system_messages_first_openai_only": "Nur für OpenAI-kompatible Endpunkte wirksam", + "reorder_system_messages_first_move_to_front": "Wenn aktiviert, werden alle System-Nachrichten an den Anfang des Nachrichten-Arrays verschoben", + "reorder_system_messages_first_keep_order": "Bewahrt die ursprüngliche Reihenfolge mehrerer System-Nachrichten", + "reorder_system_messages_first_use_case": "Geeignet für Endpunkte, die strikt verlangen, dass System-Prompts zuerst erscheinen" } } \ No newline at end of file diff --git a/web/locales/en.json b/web/locales/en.json index df92499..4b6c7f6 100644 --- a/web/locales/en.json +++ b/web/locales/en.json @@ -689,6 +689,12 @@ "reverse_order": "Reverse Order", "exporting": "Exporting...", "version_found": "Version Found", - "click_to_view_github": "Click to View GitHub" + "click_to_view_github": "Click to View GitHub", + "reorder_system_messages_first_configuration": "System Messages Reorder Configuration", + "enable_reorder_system_messages_first": "Enable System Messages Reorder", + "reorder_system_messages_first_openai_only": "Only effective for OpenAI Compatible endpoints", + "reorder_system_messages_first_move_to_front": "When enabled, all system messages will be reordered to the front of the messages array", + "reorder_system_messages_first_keep_order": "Preserves the original order of multiple system messages", + "reorder_system_messages_first_use_case": "Suitable for endpoints that strictly require system prompt to be first" } } \ No newline at end of file diff --git a/web/locales/es.json b/web/locales/es.json index e5747a6..35dc453 100644 --- a/web/locales/es.json +++ b/web/locales/es.json @@ -670,6 +670,12 @@ "export_failed_error": "La exportación falló", "http_header_content": "Contenido de Encabezado HTTP", "fallback_support": "Soporte de respaldo de variables de entorno en caso de fallo", - "original_request_headers": "Encabezados de Solicitud Originales" + "original_request_headers": "Encabezados de Solicitud Originales", + "reorder_system_messages_first_configuration": "Configuración de Reordenamiento de Mensajes del Sistema", + "enable_reorder_system_messages_first": "Habilitar Reordenamiento de Mensajes del Sistema", + "reorder_system_messages_first_openai_only": "Solo efectivo para endpoints compatibles con OpenAI", + "reorder_system_messages_first_move_to_front": "Cuando está habilitado, todos los mensajes del sistema se reordenarán al frente del array de mensajes", + "reorder_system_messages_first_keep_order": "Preserva el orden original de múltiples mensajes del sistema", + "reorder_system_messages_first_use_case": "Adecuado para endpoints que requieren estrictamente que el system prompt esté primero" } } \ No newline at end of file diff --git a/web/locales/it.json b/web/locales/it.json index a0e4640..63ec990 100644 --- a/web/locales/it.json +++ b/web/locales/it.json @@ -670,6 +670,12 @@ "export_failed_error": "Esportazione fallita", "http_header_content": "Contenuto Header HTTP", "fallback_support": "Supporto fallback variabile di ambiente in caso di errore", - "original_request_headers": "Header Richiesta Originali" + "original_request_headers": "Header Richiesta Originali", + "reorder_system_messages_first_configuration": "Configurazione Riordinamento Messaggi di Sistema", + "enable_reorder_system_messages_first": "Abilita Riordinamento Messaggi di Sistema", + "reorder_system_messages_first_openai_only": "Efficace solo per endpoint compatibili con OpenAI", + "reorder_system_messages_first_move_to_front": "Quando abilitato, tutti i messaggi di sistema verranno riordinati all'inizio dell'array dei messaggi", + "reorder_system_messages_first_keep_order": "Preserva l'ordine originale di più messaggi di sistema", + "reorder_system_messages_first_use_case": "Adatto per endpoint che richiedono rigorosamente il system prompt per primo" } } \ No newline at end of file diff --git a/web/locales/ja.json b/web/locales/ja.json index d580fde..40b4144 100644 --- a/web/locales/ja.json +++ b/web/locales/ja.json @@ -670,6 +670,12 @@ "export_failed_error": "エクスポートに失敗", "http_header_content": "HTTPヘッダーコンテンツ", "fallback_support": "失敗時の環境変数フォールバックサポート", - "original_request_headers": "元のリクエストヘッダー" + "original_request_headers": "元のリクエストヘッダー", + "reorder_system_messages_first_configuration": "System Messages 重排配置", + "enable_reorder_system_messages_first": "System Messages 重排を有効化", + "reorder_system_messages_first_openai_only": "OpenAI Compatibleタイプのエンドポイントのみ有効", + "reorder_system_messages_first_move_to_front": "有効にすると、すべてのsystemメッセージが配列の先頭に並び替えられます", + "reorder_system_messages_first_keep_order": "複数のsystemメッセージの元の順序を保持", + "reorder_system_messages_first_use_case": "systemプロンプトを最初に配置する必要があるエンドポイントに適用" } } \ No newline at end of file diff --git a/web/locales/ko.json b/web/locales/ko.json index 158a674..3c9528b 100644 --- a/web/locales/ko.json +++ b/web/locales/ko.json @@ -670,6 +670,12 @@ "export_failed_error": "내보내기 실패", "http_header_content": "HTTP 헤더 콘텐츠", "fallback_support": "장애 시 환경 변수 폴백 지원", - "original_request_headers": "원본 요청 헤더" + "original_request_headers": "원본 요청 헤더", + "reorder_system_messages_first_configuration": "시스템 메시지 재정렬 구성", + "enable_reorder_system_messages_first": "시스템 메시지 재정렬 활성화", + "reorder_system_messages_first_openai_only": "OpenAI 호환 엔드포인트에만 적용됩니다", + "reorder_system_messages_first_move_to_front": "활성화하면 모든 시스템 메시지가 메시지 배열의 맨 앞으로 재정렬됩니다", + "reorder_system_messages_first_keep_order": "여러 시스템 메시지의 원래 순서를 유지합니다", + "reorder_system_messages_first_use_case": "시스템 프롬프트가 첫 번째에 있어야 하는 엔드포인트에 적합합니다" } } \ No newline at end of file diff --git a/web/locales/pt.json b/web/locales/pt.json index 94a375b..30abb88 100644 --- a/web/locales/pt.json +++ b/web/locales/pt.json @@ -670,6 +670,12 @@ "export_failed_error": "Exportação falhou", "http_header_content": "Conteúdo do Cabeçalho HTTP", "fallback_support": "Suporte de fallback de variável de ambiente em caso de falha", - "original_request_headers": "Cabeçalhos de Solicitação Originais" + "original_request_headers": "Cabeçalhos de Solicitação Originais", + "reorder_system_messages_first_configuration": "Configuração de Reordenação de Mensagens do Sistema", + "enable_reorder_system_messages_first": "Ativar Reordenação de Mensagens do Sistema", + "reorder_system_messages_first_openai_only": "Efetivo apenas para endpoints compatíveis com OpenAI", + "reorder_system_messages_first_move_to_front": "Quando ativado, todas as mensagens do sistema serão reordenadas para o início do array de mensagens", + "reorder_system_messages_first_keep_order": "Preserva a ordem original de múltiplas mensagens do sistema", + "reorder_system_messages_first_use_case": "Adequado para endpoints que exigem estritamente que o system prompt esteja primeiro" } } \ No newline at end of file diff --git a/web/locales/ru.json b/web/locales/ru.json index 59edd61..b9603d5 100644 --- a/web/locales/ru.json +++ b/web/locales/ru.json @@ -670,6 +670,12 @@ "export_failed_error": "Экспорт не удался", "http_header_content": "Содержимое HTTP-заголовка", "fallback_support": "Поддержка резервных переменных окружения при сбоях", - "original_request_headers": "Оригинальные заголовки запроса" + "original_request_headers": "Оригинальные заголовки запроса", + "reorder_system_messages_first_configuration": "Конфигурация переупорядочивания системных сообщений", + "enable_reorder_system_messages_first": "Включить переупорядочивание системных сообщений", + "reorder_system_messages_first_openai_only": "Действует только для конечных точек, совместимых с OpenAI", + "reorder_system_messages_first_move_to_front": "При включении все системные сообщения будут переупорядочены в начало массива сообщений", + "reorder_system_messages_first_keep_order": "Сохраняет исходный порядок нескольких системных сообщений", + "reorder_system_messages_first_use_case": "Подходит для конечных точек, которые строго требуют, чтобы системный промпт был первым" } } \ No newline at end of file diff --git a/web/locales/zh-cn.json b/web/locales/zh-cn.json index 5459658..31b8ffd 100644 --- a/web/locales/zh-cn.json +++ b/web/locales/zh-cn.json @@ -689,6 +689,12 @@ "reverse_order": "逆向排列", "exporting": "导出中...", "version_found": "发现版本", - "click_to_view_github": "点击查看 GitHub" + "click_to_view_github": "点击查看 GitHub", + "reorder_system_messages_first_configuration": "System Messages 重排配置", + "enable_reorder_system_messages_first": "启用 System Messages 重排", + "reorder_system_messages_first_openai_only": "仅对 OpenAI Compatible 类型端点生效", + "reorder_system_messages_first_move_to_front": "启用后将所有 system 消息重排到 messages 数组最前面", + "reorder_system_messages_first_keep_order": "保持多条 system 消息的原有顺序", + "reorder_system_messages_first_use_case": "适用于严格要求 system prompt 在首位的端点" } } \ No newline at end of file diff --git a/web/static/endpoints-advanced.js b/web/static/endpoints-advanced.js index ebee457..ebc5dbd 100644 --- a/web/static/endpoints-advanced.js +++ b/web/static/endpoints-advanced.js @@ -557,7 +557,7 @@ document.addEventListener('DOMContentLoaded', function() { this.checked ? StyleUtils.show(configDiv) : StyleUtils.hide(configDiv); }); } - + // Add header override rule button event listener const addHeaderRuleBtn = document.querySelector('[data-action="add-header-override-rule"]'); if (addHeaderRuleBtn) { @@ -565,7 +565,7 @@ document.addEventListener('DOMContentLoaded', function() { addHeaderOverrideRule(); }); } - + // Add parameter override rule button event listener const addParameterRuleBtn = document.querySelector('[data-action="add-parameter-override-rule"]'); if (addParameterRuleBtn) { @@ -573,4 +573,17 @@ document.addEventListener('DOMContentLoaded', function() { addParameterOverrideRule(); }); } -}); \ No newline at end of file +}); + +// ===== Reorder System Messages First Functions ===== + +// Collect reorder system messages first configuration +function collectReorderSystemMessagesFirstData() { + return document.getElementById('reorder-system-messages-first-enabled').checked; +} + +// Load reorder system messages first configuration to form +function loadReorderSystemMessagesFirstConfig(enabled) { + const checkbox = document.getElementById('reorder-system-messages-first-enabled'); + checkbox.checked = enabled === true; +} \ No newline at end of file diff --git a/web/static/endpoints-modal.js b/web/static/endpoints-modal.js index 62d3cfb..61551e8 100644 --- a/web/static/endpoints-modal.js +++ b/web/static/endpoints-modal.js @@ -116,7 +116,10 @@ function showEditEndpointModal(endpointName) { // Load enhanced protection configuration const enhancedProtection = endpoint.enhanced_protection || false; document.getElementById('enhanced-protection-enabled').checked = enhancedProtection; - + + // Load reorder system messages first configuration + loadReorderSystemMessagesFirstConfig(endpoint.reorder_system_messages_first); + // Check enhanced protection availability based on URL checkEnhancedProtectionAvailability(); @@ -210,7 +213,8 @@ function saveEndpoint() { proxy: collectProxyData(), // New: collect proxy configuration header_overrides: collectHeaderOverrideData(), // New: collect header override configuration parameter_overrides: collectParameterOverrideData(), // New: collect parameter override configuration - enhanced_protection: document.getElementById('enhanced-protection-enabled').checked // New: enhanced protection for official accounts + enhanced_protection: document.getElementById('enhanced-protection-enabled').checked, // New: enhanced protection for official accounts + reorder_system_messages_first: collectReorderSystemMessagesFirstData() // New: reorder system messages first }; // Add OAuth config if present diff --git a/web/templates/endpoint-modal.html b/web/templates/endpoint-modal.html index 646fe1a..c85328b 100644 --- a/web/templates/endpoint-modal.html +++ b/web/templates/endpoint-modal.html @@ -358,7 +358,7 @@