feat(gateway): 实现网关核心模块
实现内容: - internal/adapter: Provider Adapter抽象层和OpenAI实现 - internal/router: 多Provider路由(支持latency/weighted/availability策略) - internal/handler: OpenAI兼容API端点(/v1/chat/completions, /v1/completions) - internal/ratelimit: Token Bucket和Sliding Window限流器 - internal/alert: 告警系统(支持邮件/钉钉/飞书) - internal/config: 配置管理 - pkg/error: 完整错误码体系 - pkg/model: API请求/响应模型 PRD对齐: - P0-1: 统一API接入 ✅ (OpenAI兼容) - P0-2: 基础路由与稳定性 ✅ (多Provider路由+Fallback) - P0-4: 预算与限流 ✅ (Token Bucket限流) 注意:需要供应链模块支持后再完善成本归因和账单导出
This commit is contained in:
246
gateway/pkg/error/error.go
Normal file
246
gateway/pkg/error/error.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package error
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrorCode 错误码枚举
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
// 认证授权 (AUTH_*)
|
||||
AUTH_INVALID_TOKEN ErrorCode = "AUTH_001"
|
||||
AUTH_INSUFFICIENT_PERMISSION ErrorCode = "AUTH_002"
|
||||
AUTH_MFA_REQUIRED ErrorCode = "AUTH_003"
|
||||
|
||||
// 计费 (BILLING_*)
|
||||
BILLING_INSUFFICIENT_BALANCE ErrorCode = "BILLING_001"
|
||||
BILLING_CHARGE_FAILED ErrorCode = "BILLING_002"
|
||||
BILLING_REFUND_FAILED ErrorCode = "BILLING_003"
|
||||
BILLING_DISCREPANCY ErrorCode = "BILLING_004"
|
||||
|
||||
// 路由 (ROUTER_*)
|
||||
ROUTER_NO_PROVIDER_AVAILABLE ErrorCode = "ROUTER_001"
|
||||
ROUTER_ALL_PROVIDERS_FAILED ErrorCode = "ROUTER_002"
|
||||
ROUTER_TIMEOUT ErrorCode = "ROUTER_003"
|
||||
|
||||
// 供应商 (PROVIDER_*)
|
||||
PROVIDER_INVALID_KEY ErrorCode = "PROVIDER_001"
|
||||
PROVIDER_RATE_LIMIT ErrorCode = "PROVIDER_002"
|
||||
PROVIDER_QUOTA_EXCEEDED ErrorCode = "PROVIDER_003"
|
||||
PROVIDER_MODEL_NOT_FOUND ErrorCode = "PROVIDER_004"
|
||||
PROVIDER_ERROR ErrorCode = "PROVIDER_005"
|
||||
|
||||
// 限流 (RATE_LIMIT_*)
|
||||
RATE_LIMIT_EXCEEDED ErrorCode = "RATE_LIMIT_001"
|
||||
RATE_LIMIT_TOKEN_EXCEEDED ErrorCode = "RATE_LIMIT_002"
|
||||
RATE_LIMIT_BURST_EXCEEDED ErrorCode = "RATE_LIMIT_003"
|
||||
|
||||
// 通用 (COMMON_*)
|
||||
COMMON_INVALID_REQUEST ErrorCode = "COMMON_001"
|
||||
COMMON_RESOURCE_NOT_FOUND ErrorCode = "COMMON_002"
|
||||
COMMON_INTERNAL_ERROR ErrorCode = "COMMON_003"
|
||||
COMMON_SERVICE_UNAVAILABLE ErrorCode = "COMMON_004"
|
||||
)
|
||||
|
||||
// ErrorInfo 错误信息
|
||||
type ErrorInfo struct {
|
||||
Code ErrorCode
|
||||
Message string
|
||||
HTTPStatus int
|
||||
Retryable bool
|
||||
}
|
||||
|
||||
// GatewayError 网关错误
|
||||
type GatewayError struct {
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Details map[string]interface{}
|
||||
RequestID string
|
||||
Internal error
|
||||
}
|
||||
|
||||
func (e *GatewayError) Error() string {
|
||||
if e.Internal != nil {
|
||||
return fmt.Sprintf("%s: %s (caused by: %v)", e.Code, e.Message, e.Internal)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *GatewayError) Unwrap() error {
|
||||
return e.Internal
|
||||
}
|
||||
|
||||
// ErrorDefinitions 错误码定义
|
||||
var ErrorDefinitions = map[ErrorCode]ErrorInfo{
|
||||
AUTH_INVALID_TOKEN: {
|
||||
Code: AUTH_INVALID_TOKEN,
|
||||
Message: "Invalid or expired token",
|
||||
HTTPStatus: 401,
|
||||
Retryable: false,
|
||||
},
|
||||
AUTH_INSUFFICIENT_PERMISSION: {
|
||||
Code: AUTH_INSUFFICIENT_PERMISSION,
|
||||
Message: "Insufficient permissions",
|
||||
HTTPStatus: 403,
|
||||
Retryable: false,
|
||||
},
|
||||
AUTH_MFA_REQUIRED: {
|
||||
Code: AUTH_MFA_REQUIRED,
|
||||
Message: "MFA verification required",
|
||||
HTTPStatus: 403,
|
||||
Retryable: false,
|
||||
},
|
||||
BILLING_INSUFFICIENT_BALANCE: {
|
||||
Code: BILLING_INSUFFICIENT_BALANCE,
|
||||
Message: "Insufficient balance",
|
||||
HTTPStatus: 402,
|
||||
Retryable: false,
|
||||
},
|
||||
BILLING_CHARGE_FAILED: {
|
||||
Code: BILLING_CHARGE_FAILED,
|
||||
Message: "Charge failed",
|
||||
HTTPStatus: 500,
|
||||
Retryable: true,
|
||||
},
|
||||
BILLING_REFUND_FAILED: {
|
||||
Code: BILLING_REFUND_FAILED,
|
||||
Message: "Refund failed",
|
||||
HTTPStatus: 500,
|
||||
Retryable: true,
|
||||
},
|
||||
BILLING_DISCREPANCY: {
|
||||
Code: BILLING_DISCREPANCY,
|
||||
Message: "Billing discrepancy detected",
|
||||
HTTPStatus: 500,
|
||||
Retryable: true,
|
||||
},
|
||||
ROUTER_NO_PROVIDER_AVAILABLE: {
|
||||
Code: ROUTER_NO_PROVIDER_AVAILABLE,
|
||||
Message: "No provider available",
|
||||
HTTPStatus: 503,
|
||||
Retryable: true,
|
||||
},
|
||||
ROUTER_ALL_PROVIDERS_FAILED: {
|
||||
Code: ROUTER_ALL_PROVIDERS_FAILED,
|
||||
Message: "All providers failed",
|
||||
HTTPStatus: 503,
|
||||
Retryable: true,
|
||||
},
|
||||
ROUTER_TIMEOUT: {
|
||||
Code: ROUTER_TIMEOUT,
|
||||
Message: "Request timeout",
|
||||
HTTPStatus: 504,
|
||||
Retryable: true,
|
||||
},
|
||||
PROVIDER_INVALID_KEY: {
|
||||
Code: PROVIDER_INVALID_KEY,
|
||||
Message: "Invalid API key",
|
||||
HTTPStatus: 401,
|
||||
Retryable: false,
|
||||
},
|
||||
PROVIDER_RATE_LIMIT: {
|
||||
Code: PROVIDER_RATE_LIMIT,
|
||||
Message: "Rate limit exceeded",
|
||||
HTTPStatus: 429,
|
||||
Retryable: true,
|
||||
},
|
||||
PROVIDER_QUOTA_EXCEEDED: {
|
||||
Code: PROVIDER_QUOTA_EXCEEDED,
|
||||
Message: "Quota exceeded",
|
||||
HTTPStatus: 402,
|
||||
Retryable: false,
|
||||
},
|
||||
PROVIDER_MODEL_NOT_FOUND: {
|
||||
Code: PROVIDER_MODEL_NOT_FOUND,
|
||||
Message: "Model not found",
|
||||
HTTPStatus: 404,
|
||||
Retryable: false,
|
||||
},
|
||||
PROVIDER_ERROR: {
|
||||
Code: PROVIDER_ERROR,
|
||||
Message: "Provider error",
|
||||
HTTPStatus: 502,
|
||||
Retryable: true,
|
||||
},
|
||||
RATE_LIMIT_EXCEEDED: {
|
||||
Code: RATE_LIMIT_EXCEEDED,
|
||||
Message: "Rate limit exceeded",
|
||||
HTTPStatus: 429,
|
||||
Retryable: false,
|
||||
},
|
||||
RATE_LIMIT_TOKEN_EXCEEDED: {
|
||||
Code: RATE_LIMIT_TOKEN_EXCEEDED,
|
||||
Message: "Token limit exceeded",
|
||||
HTTPStatus: 429,
|
||||
Retryable: false,
|
||||
},
|
||||
RATE_LIMIT_BURST_EXCEEDED: {
|
||||
Code: RATE_LIMIT_BURST_EXCEEDED,
|
||||
Message: "Burst limit exceeded",
|
||||
HTTPStatus: 429,
|
||||
Retryable: false,
|
||||
},
|
||||
COMMON_INVALID_REQUEST: {
|
||||
Code: COMMON_INVALID_REQUEST,
|
||||
Message: "Invalid request",
|
||||
HTTPStatus: 400,
|
||||
Retryable: false,
|
||||
},
|
||||
COMMON_RESOURCE_NOT_FOUND: {
|
||||
Code: COMMON_RESOURCE_NOT_FOUND,
|
||||
Message: "Resource not found",
|
||||
HTTPStatus: 404,
|
||||
Retryable: false,
|
||||
},
|
||||
COMMON_INTERNAL_ERROR: {
|
||||
Code: COMMON_INTERNAL_ERROR,
|
||||
Message: "Internal error",
|
||||
HTTPStatus: 500,
|
||||
Retryable: true,
|
||||
},
|
||||
COMMON_SERVICE_UNAVAILABLE: {
|
||||
Code: COMMON_SERVICE_UNAVAILABLE,
|
||||
Message: "Service unavailable",
|
||||
HTTPStatus: 503,
|
||||
Retryable: true,
|
||||
},
|
||||
}
|
||||
|
||||
// NewGatewayError 创建网关错误
|
||||
func NewGatewayError(code ErrorCode, message string) *GatewayError {
|
||||
return &GatewayError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequestID 设置请求ID
|
||||
func (e *GatewayError) WithRequestID(requestID string) *GatewayError {
|
||||
e.RequestID = requestID
|
||||
return e
|
||||
}
|
||||
|
||||
// WithDetail 设置详情
|
||||
func (e *GatewayError) WithDetail(key string, value interface{}) *GatewayError {
|
||||
e.Details[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// WithInternal 设置内部错误
|
||||
func (e *GatewayError) WithInternal(err error) *GatewayError {
|
||||
e.Internal = err
|
||||
return e
|
||||
}
|
||||
|
||||
// GetErrorInfo 获取错误信息
|
||||
func (e *GatewayError) GetErrorInfo() ErrorInfo {
|
||||
if info, ok := ErrorDefinitions[e.Code]; ok {
|
||||
return info
|
||||
}
|
||||
return ErrorInfo{
|
||||
Code: COMMON_INTERNAL_ERROR,
|
||||
Message: e.Message,
|
||||
HTTPStatus: 500,
|
||||
Retryable: true,
|
||||
}
|
||||
}
|
||||
144
gateway/pkg/model/model.go
Normal file
144
gateway/pkg/model/model.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ChatCompletionRequest 聊天完成请求
|
||||
type ChatCompletionRequest struct {
|
||||
Model string `json:"model" binding:"required"`
|
||||
Messages []ChatMessage `json:"messages" binding:"required"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Stop []string `json:"stop,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
PresencePenalty float64 `json:"presence_penalty,omitempty"`
|
||||
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
// ChatMessage 聊天消息
|
||||
type ChatMessage struct {
|
||||
Role string `json:"role" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// ChatCompletionResponse 聊天完成响应
|
||||
type ChatCompletionResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Choice `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type Choice struct {
|
||||
Index int `json:"index"`
|
||||
Message ChatMessage `json:"message"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
// CompletionRequest 完成请求
|
||||
type CompletionRequest struct {
|
||||
Model string `json:"model" binding:"required"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Stop []string `json:"stop,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
}
|
||||
|
||||
// CompletionResponse 完成响应
|
||||
type CompletionResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Choice1 `json:"choices"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
type Choice1 struct {
|
||||
Text string `json:"text"`
|
||||
Index int `json:"index"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
}
|
||||
|
||||
// StreamResponse 流式响应
|
||||
type StreamResponse struct {
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []Delta `json:"choices"`
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
Delta struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
} `json:"delta"`
|
||||
Index int `json:"index"`
|
||||
FinishReason string `json:"finish_reason,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
type ErrorResponse struct {
|
||||
Error ErrorDetail `json:"error"`
|
||||
}
|
||||
|
||||
type ErrorDetail struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Param string `json:"param,omitempty"`
|
||||
}
|
||||
|
||||
// HealthStatus 健康状态
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Services map[string]bool `json:"services"`
|
||||
}
|
||||
|
||||
// Tenant 租户
|
||||
type Tenant struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Plan string `json:"plan"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Budget 预算
|
||||
type Budget struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
MonthlyLimit float64 `json:"monthly_limit"`
|
||||
AlertThreshold float64 `json:"alert_threshold"`
|
||||
CurrentUsage float64 `json:"current_usage"`
|
||||
}
|
||||
|
||||
// RouteRequest 路由请求
|
||||
type RouteRequest struct {
|
||||
Model string
|
||||
TenantID int64
|
||||
RouteType string // "primary", "fallback"
|
||||
}
|
||||
|
||||
// RouteResult 路由结果
|
||||
type RouteResult struct {
|
||||
Provider string
|
||||
Model string
|
||||
LatencyMs int64
|
||||
Success bool
|
||||
Error error
|
||||
}
|
||||
Reference in New Issue
Block a user