Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 57 additions & 56 deletions internal/config/types.go
Original file line number Diff line number Diff line change
@@ -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 国际化配置
Expand All @@ -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"` // 是否自动刷新
}

// 新增:模型重写配置结构
Expand All @@ -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
Expand All @@ -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"`
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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特定配置
}
Priority int `yaml:"priority"` // 执行优先级(未使用,因为并发执行)
Config map[string]interface{} `yaml:"config"` // tagger特定配置
}
62 changes: 51 additions & 11 deletions internal/conversion/request_converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 != "" {
// 根据配置的字段名设置对应字段
Expand Down Expand Up @@ -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 方法获取内容块
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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"}
Expand Down Expand Up @@ -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")
Expand All @@ -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,
})
}
Expand All @@ -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 {
Expand Down Expand Up @@ -401,4 +409,36 @@ func (c *RequestConverter) anthropicSystemToText(sys interface{}) string {
}
return ""
}
}
}

// 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
}
22 changes: 11 additions & 11 deletions internal/conversion/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -54,4 +54,4 @@ func NewConversionError(errorType, message string, err error) *ConversionError {
Message: message,
Err: err,
}
}
}
Loading