mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
Merge branch 'fawney19:master' into master
This commit is contained in:
@@ -76,7 +76,6 @@ export interface ProviderExport {
|
|||||||
provider_priority?: number
|
provider_priority?: number
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
concurrent_limit?: number | null
|
concurrent_limit?: number | null
|
||||||
timeout?: number | null
|
|
||||||
max_retries?: number | null
|
max_retries?: number | null
|
||||||
proxy?: any
|
proxy?: any
|
||||||
config?: any
|
config?: any
|
||||||
@@ -89,7 +88,6 @@ export interface EndpointExport {
|
|||||||
api_format: string
|
api_format: string
|
||||||
base_url: string
|
base_url: string
|
||||||
headers?: any
|
headers?: any
|
||||||
timeout?: number
|
|
||||||
max_retries?: number
|
max_retries?: number
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
custom_path?: string | null
|
custom_path?: string | null
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export async function createEndpoint(
|
|||||||
base_url: string
|
base_url: string
|
||||||
custom_path?: string
|
custom_path?: string
|
||||||
header_rules?: HeaderRule[]
|
header_rules?: HeaderRule[]
|
||||||
timeout?: number
|
|
||||||
max_retries?: number
|
max_retries?: number
|
||||||
is_active?: boolean
|
is_active?: boolean
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
@@ -48,7 +47,6 @@ export async function updateEndpoint(
|
|||||||
base_url: string
|
base_url: string
|
||||||
custom_path: string | null
|
custom_path: string | null
|
||||||
header_rules: HeaderRule[]
|
header_rules: HeaderRule[]
|
||||||
timeout: number
|
|
||||||
max_retries: number
|
max_retries: number
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
config: Record<string, any>
|
config: Record<string, any>
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export async function updateProvider(
|
|||||||
quota_expires_at: string
|
quota_expires_at: string
|
||||||
rpm_limit: number | null
|
rpm_limit: number | null
|
||||||
// 请求配置(从 Endpoint 迁移)
|
// 请求配置(从 Endpoint 迁移)
|
||||||
timeout: number
|
|
||||||
max_retries: number
|
max_retries: number
|
||||||
proxy: ProxyConfig | null
|
proxy: ProxyConfig | null
|
||||||
cache_ttl_minutes: number // 0表示不支持缓存,>0表示支持缓存并设置TTL(分钟)
|
cache_ttl_minutes: number // 0表示不支持缓存,>0表示支持缓存并设置TTL(分钟)
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export interface ProviderEndpoint {
|
|||||||
custom_path?: string // 自定义请求路径(可选,为空则使用 API 格式默认路径)
|
custom_path?: string // 自定义请求路径(可选,为空则使用 API 格式默认路径)
|
||||||
// 请求头配置
|
// 请求头配置
|
||||||
header_rules?: HeaderRule[] // 请求头规则列表,支持 set/drop/rename 操作
|
header_rules?: HeaderRule[] // 请求头规则列表,支持 set/drop/rename 操作
|
||||||
timeout: number
|
|
||||||
max_retries: number
|
max_retries: number
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
@@ -302,7 +301,6 @@ export interface ProviderWithEndpointsSummary {
|
|||||||
quota_last_reset_at?: string // 当前周期开始时间
|
quota_last_reset_at?: string // 当前周期开始时间
|
||||||
quota_expires_at?: string
|
quota_expires_at?: string
|
||||||
// 请求配置(从 Endpoint 迁移)
|
// 请求配置(从 Endpoint 迁移)
|
||||||
timeout?: number // 请求超时(秒)
|
|
||||||
max_retries?: number // 最大重试次数
|
max_retries?: number // 最大重试次数
|
||||||
proxy?: ProxyConfig | null // 代理配置
|
proxy?: ProxyConfig | null // 代理配置
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
|
|||||||
@@ -80,29 +80,16 @@
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="space-y-1.5">
|
||||||
<div class="space-y-1.5">
|
<Label>最大重试次数</Label>
|
||||||
<Label>超时时间 (秒)</Label>
|
<Input
|
||||||
<Input
|
:model-value="form.max_retries ?? ''"
|
||||||
:model-value="form.timeout ?? ''"
|
type="number"
|
||||||
type="number"
|
min="0"
|
||||||
min="1"
|
max="10"
|
||||||
max="600"
|
placeholder="默认 2"
|
||||||
placeholder="默认 300"
|
@update:model-value="(v) => form.max_retries = parseNumberInput(v)"
|
||||||
@update:model-value="(v) => form.timeout = parseNumberInput(v)"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<Label>最大重试次数</Label>
|
|
||||||
<Input
|
|
||||||
:model-value="form.max_retries ?? ''"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="10"
|
|
||||||
placeholder="默认 2"
|
|
||||||
@update:model-value="(v) => form.max_retries = parseNumberInput(v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -279,7 +266,6 @@ const form = ref({
|
|||||||
rate_limit: undefined as number | undefined,
|
rate_limit: undefined as number | undefined,
|
||||||
concurrent_limit: undefined as number | undefined,
|
concurrent_limit: undefined as number | undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
timeout: undefined as number | undefined,
|
|
||||||
max_retries: undefined as number | undefined,
|
max_retries: undefined as number | undefined,
|
||||||
// 代理配置(扁平化便于表单绑定)
|
// 代理配置(扁平化便于表单绑定)
|
||||||
proxy_enabled: false,
|
proxy_enabled: false,
|
||||||
@@ -304,7 +290,6 @@ function resetForm() {
|
|||||||
rate_limit: undefined,
|
rate_limit: undefined,
|
||||||
concurrent_limit: undefined,
|
concurrent_limit: undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
timeout: undefined,
|
|
||||||
max_retries: undefined,
|
max_retries: undefined,
|
||||||
// 代理配置
|
// 代理配置
|
||||||
proxy_enabled: false,
|
proxy_enabled: false,
|
||||||
@@ -335,7 +320,6 @@ function loadProviderData() {
|
|||||||
rate_limit: undefined,
|
rate_limit: undefined,
|
||||||
concurrent_limit: undefined,
|
concurrent_limit: undefined,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
timeout: props.provider.timeout ?? undefined,
|
|
||||||
max_retries: props.provider.max_retries ?? undefined,
|
max_retries: props.provider.max_retries ?? undefined,
|
||||||
// 代理配置
|
// 代理配置
|
||||||
proxy_enabled: proxy?.enabled ?? false,
|
proxy_enabled: proxy?.enabled ?? false,
|
||||||
@@ -391,7 +375,6 @@ const handleSubmit = async () => {
|
|||||||
provider_priority: form.value.provider_priority,
|
provider_priority: form.value.provider_priority,
|
||||||
is_active: form.value.is_active,
|
is_active: form.value.is_active,
|
||||||
// 请求配置
|
// 请求配置
|
||||||
timeout: form.value.timeout ?? undefined,
|
|
||||||
max_retries: form.value.max_retries ?? undefined,
|
max_retries: form.value.max_retries ?? undefined,
|
||||||
proxy,
|
proxy,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,9 +425,9 @@ const MOCK_ENDPOINT_KEYS = [
|
|||||||
|
|
||||||
// Mock Endpoints
|
// Mock Endpoints
|
||||||
const MOCK_ENDPOINTS = [
|
const MOCK_ENDPOINTS = [
|
||||||
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'CLAUDE', base_url: 'https://api.anthropic.com', timeout: 300, max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
{ id: 'ep-001', provider_id: 'provider-001', provider_name: 'anthropic', api_format: 'CLAUDE', base_url: 'https://api.anthropic.com', max_retries: 2, is_active: true, total_keys: 2, active_keys: 2, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||||
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'OPENAI', base_url: 'https://api.openai.com', timeout: 60, max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
{ id: 'ep-002', provider_id: 'provider-002', provider_name: 'openai', api_format: 'OPENAI', base_url: 'https://api.openai.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-01T00:00:00Z', updated_at: new Date().toISOString() },
|
||||||
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'GEMINI', base_url: 'https://generativelanguage.googleapis.com', timeout: 60, max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
{ id: 'ep-003', provider_id: 'provider-003', provider_name: 'google', api_format: 'GEMINI', base_url: 'https://generativelanguage.googleapis.com', max_retries: 2, is_active: true, total_keys: 1, active_keys: 1, created_at: '2024-01-15T00:00:00Z', updated_at: new Date().toISOString() }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Mock 能力定义
|
// Mock 能力定义
|
||||||
@@ -1221,7 +1221,6 @@ function generateMockEndpointsForProvider(providerId: string) {
|
|||||||
base_url: format.includes('CLAUDE') ? 'https://api.anthropic.com' :
|
base_url: format.includes('CLAUDE') ? 'https://api.anthropic.com' :
|
||||||
format.includes('OPENAI') ? 'https://api.openai.com' :
|
format.includes('OPENAI') ? 'https://api.openai.com' :
|
||||||
'https://generativelanguage.googleapis.com',
|
'https://generativelanguage.googleapis.com',
|
||||||
timeout: 300,
|
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
is_active: healthDetail?.is_active ?? true,
|
is_active: healthDetail?.is_active ?? true,
|
||||||
total_keys: Math.ceil(Math.random() * 3) + 1,
|
total_keys: Math.ceil(Math.random() * 3) + 1,
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ async def list_provider_endpoints(
|
|||||||
- `api_format`: API 格式
|
- `api_format`: API 格式
|
||||||
- `base_url`: 基础 URL
|
- `base_url`: 基础 URL
|
||||||
- `custom_path`: 自定义路径
|
- `custom_path`: 自定义路径
|
||||||
- `timeout`: 超时时间(秒)
|
|
||||||
- `max_retries`: 最大重试次数
|
- `max_retries`: 最大重试次数
|
||||||
- `is_active`: 是否活跃
|
- `is_active`: 是否活跃
|
||||||
- `total_keys`: Key 总数
|
- `total_keys`: Key 总数
|
||||||
@@ -103,7 +102,6 @@ async def create_provider_endpoint(
|
|||||||
- `base_url`: 基础 URL
|
- `base_url`: 基础 URL
|
||||||
- `custom_path`: 自定义路径(可选)
|
- `custom_path`: 自定义路径(可选)
|
||||||
- `header_rules`: 请求头规则列表(可选,支持 set/drop/rename 操作)
|
- `header_rules`: 请求头规则列表(可选,支持 set/drop/rename 操作)
|
||||||
- `timeout`: 超时时间(秒,默认 300)
|
|
||||||
- `max_retries`: 最大重试次数(默认 2)
|
- `max_retries`: 最大重试次数(默认 2)
|
||||||
- `config`: 额外配置(可选)
|
- `config`: 额外配置(可选)
|
||||||
- `proxy`: 代理配置(可选)
|
- `proxy`: 代理配置(可选)
|
||||||
@@ -139,7 +137,6 @@ async def get_endpoint(
|
|||||||
- `api_format`: API 格式
|
- `api_format`: API 格式
|
||||||
- `base_url`: 基础 URL
|
- `base_url`: 基础 URL
|
||||||
- `custom_path`: 自定义路径
|
- `custom_path`: 自定义路径
|
||||||
- `timeout`: 超时时间(秒)
|
|
||||||
- `max_retries`: 最大重试次数
|
- `max_retries`: 最大重试次数
|
||||||
- `is_active`: 是否活跃
|
- `is_active`: 是否活跃
|
||||||
- `total_keys`: Key 总数
|
- `total_keys`: Key 总数
|
||||||
@@ -170,7 +167,6 @@ async def update_endpoint(
|
|||||||
- `base_url`: 基础 URL
|
- `base_url`: 基础 URL
|
||||||
- `custom_path`: 自定义路径
|
- `custom_path`: 自定义路径
|
||||||
- `header_rules`: 请求头规则列表
|
- `header_rules`: 请求头规则列表
|
||||||
- `timeout`: 超时时间(秒)
|
|
||||||
- `max_retries`: 最大重试次数
|
- `max_retries`: 最大重试次数
|
||||||
- `is_active`: 是否活跃
|
- `is_active`: 是否活跃
|
||||||
- `config`: 额外配置
|
- `config`: 额外配置
|
||||||
@@ -306,7 +302,6 @@ class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
|
|||||||
base_url=self.endpoint_data.base_url,
|
base_url=self.endpoint_data.base_url,
|
||||||
custom_path=self.endpoint_data.custom_path,
|
custom_path=self.endpoint_data.custom_path,
|
||||||
header_rules=self.endpoint_data.header_rules,
|
header_rules=self.endpoint_data.header_rules,
|
||||||
timeout=self.endpoint_data.timeout,
|
|
||||||
max_retries=self.endpoint_data.max_retries,
|
max_retries=self.endpoint_data.max_retries,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
config=self.endpoint_data.config,
|
config=self.endpoint_data.config,
|
||||||
|
|||||||
@@ -317,14 +317,14 @@ async def test_model(
|
|||||||
logger.error(f"[test-model] Failed to decrypt API key: {e}")
|
logger.error(f"[test-model] Failed to decrypt API key: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to decrypt API key")
|
raise HTTPException(status_code=500, detail="Failed to decrypt API key")
|
||||||
|
|
||||||
# 构建请求配置(timeout 从 Provider 读取)
|
# 构建请求配置
|
||||||
endpoint_config = {
|
endpoint_config = {
|
||||||
"api_key": api_key_value,
|
"api_key": api_key_value,
|
||||||
"api_key_id": api_key.id, # 添加API Key ID用于用量记录
|
"api_key_id": api_key.id, # 添加API Key ID用于用量记录
|
||||||
"base_url": endpoint.base_url,
|
"base_url": endpoint.base_url,
|
||||||
"api_format": endpoint.api_format,
|
"api_format": endpoint.api_format,
|
||||||
"extra_headers": get_extra_headers_from_endpoint(endpoint),
|
"extra_headers": get_extra_headers_from_endpoint(endpoint),
|
||||||
"timeout": provider.timeout or TimeoutDefaults.HTTP_REQUEST,
|
"timeout": TimeoutDefaults.HTTP_REQUEST,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ async def create_provider(request: Request, db: Session = Depends(get_db)):
|
|||||||
- `provider_priority`: 提供商优先级(数字越小优先级越高,默认 100)
|
- `provider_priority`: 提供商优先级(数字越小优先级越高,默认 100)
|
||||||
- `is_active`: 是否启用(默认 true)
|
- `is_active`: 是否启用(默认 true)
|
||||||
- `concurrent_limit`: 并发限制(可选)
|
- `concurrent_limit`: 并发限制(可选)
|
||||||
- `timeout`: 请求超时(秒,可选)
|
|
||||||
- `max_retries`: 最大重试次数(可选)
|
- `max_retries`: 最大重试次数(可选)
|
||||||
- `proxy`: 代理配置(可选)
|
- `proxy`: 代理配置(可选)
|
||||||
- `config`: 额外配置信息(JSON,可选)
|
- `config`: 额外配置信息(JSON,可选)
|
||||||
@@ -176,7 +175,6 @@ async def update_provider(provider_id: str, request: Request, db: Session = Depe
|
|||||||
- `provider_priority`: 提供商优先级
|
- `provider_priority`: 提供商优先级
|
||||||
- `is_active`: 是否启用
|
- `is_active`: 是否启用
|
||||||
- `concurrent_limit`: 并发限制
|
- `concurrent_limit`: 并发限制
|
||||||
- `timeout`: 请求超时(秒)
|
|
||||||
- `max_retries`: 最大重试次数
|
- `max_retries`: 最大重试次数
|
||||||
- `proxy`: 代理配置
|
- `proxy`: 代理配置
|
||||||
- `config`: 额外配置信息(JSON)
|
- `config`: 额外配置信息(JSON)
|
||||||
@@ -293,7 +291,6 @@ class AdminCreateProviderAdapter(AdminApiAdapter):
|
|||||||
provider_priority=validated_data.provider_priority,
|
provider_priority=validated_data.provider_priority,
|
||||||
is_active=validated_data.is_active,
|
is_active=validated_data.is_active,
|
||||||
concurrent_limit=validated_data.concurrent_limit,
|
concurrent_limit=validated_data.concurrent_limit,
|
||||||
timeout=validated_data.timeout,
|
|
||||||
max_retries=validated_data.max_retries,
|
max_retries=validated_data.max_retries,
|
||||||
proxy=validated_data.proxy.model_dump() if validated_data.proxy else None,
|
proxy=validated_data.proxy.model_dump() if validated_data.proxy else None,
|
||||||
config=validated_data.config,
|
config=validated_data.config,
|
||||||
|
|||||||
@@ -302,7 +302,6 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
|
|||||||
quota_reset_day=provider.quota_reset_day,
|
quota_reset_day=provider.quota_reset_day,
|
||||||
quota_last_reset_at=provider.quota_last_reset_at,
|
quota_last_reset_at=provider.quota_last_reset_at,
|
||||||
quota_expires_at=provider.quota_expires_at,
|
quota_expires_at=provider.quota_expires_at,
|
||||||
timeout=provider.timeout,
|
|
||||||
max_retries=provider.max_retries,
|
max_retries=provider.max_retries,
|
||||||
proxy=provider.proxy,
|
proxy=provider.proxy,
|
||||||
total_endpoints=total_endpoints,
|
total_endpoints=total_endpoints,
|
||||||
|
|||||||
@@ -774,7 +774,6 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
"api_format": ep.api_format,
|
"api_format": ep.api_format,
|
||||||
"base_url": ep.base_url,
|
"base_url": ep.base_url,
|
||||||
"header_rules": ep.header_rules,
|
"header_rules": ep.header_rules,
|
||||||
"timeout": ep.timeout,
|
|
||||||
"max_retries": ep.max_retries,
|
"max_retries": ep.max_retries,
|
||||||
"is_active": ep.is_active,
|
"is_active": ep.is_active,
|
||||||
"custom_path": ep.custom_path,
|
"custom_path": ep.custom_path,
|
||||||
@@ -855,7 +854,6 @@ class AdminExportConfigAdapter(AdminApiAdapter):
|
|||||||
"provider_priority": provider.provider_priority,
|
"provider_priority": provider.provider_priority,
|
||||||
"is_active": provider.is_active,
|
"is_active": provider.is_active,
|
||||||
"concurrent_limit": provider.concurrent_limit,
|
"concurrent_limit": provider.concurrent_limit,
|
||||||
"timeout": provider.timeout,
|
|
||||||
"max_retries": provider.max_retries,
|
"max_retries": provider.max_retries,
|
||||||
"proxy": provider.proxy,
|
"proxy": provider.proxy,
|
||||||
"config": provider.config,
|
"config": provider.config,
|
||||||
@@ -1006,7 +1004,6 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
existing_provider.concurrent_limit = prov_data.get(
|
existing_provider.concurrent_limit = prov_data.get(
|
||||||
"concurrent_limit"
|
"concurrent_limit"
|
||||||
)
|
)
|
||||||
existing_provider.timeout = prov_data.get("timeout", existing_provider.timeout)
|
|
||||||
existing_provider.max_retries = prov_data.get(
|
existing_provider.max_retries = prov_data.get(
|
||||||
"max_retries", existing_provider.max_retries
|
"max_retries", existing_provider.max_retries
|
||||||
)
|
)
|
||||||
@@ -1031,7 +1028,6 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
provider_priority=prov_data.get("provider_priority", 100),
|
provider_priority=prov_data.get("provider_priority", 100),
|
||||||
is_active=prov_data.get("is_active", True),
|
is_active=prov_data.get("is_active", True),
|
||||||
concurrent_limit=prov_data.get("concurrent_limit"),
|
concurrent_limit=prov_data.get("concurrent_limit"),
|
||||||
timeout=prov_data.get("timeout"),
|
|
||||||
max_retries=prov_data.get("max_retries"),
|
max_retries=prov_data.get("max_retries"),
|
||||||
proxy=prov_data.get("proxy"),
|
proxy=prov_data.get("proxy"),
|
||||||
config=prov_data.get("config"),
|
config=prov_data.get("config"),
|
||||||
@@ -1064,7 +1060,6 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
"base_url", existing_ep.base_url
|
"base_url", existing_ep.base_url
|
||||||
)
|
)
|
||||||
existing_ep.header_rules = ep_data.get("header_rules")
|
existing_ep.header_rules = ep_data.get("header_rules")
|
||||||
existing_ep.timeout = ep_data.get("timeout", 300)
|
|
||||||
existing_ep.max_retries = ep_data.get("max_retries", 2)
|
existing_ep.max_retries = ep_data.get("max_retries", 2)
|
||||||
existing_ep.is_active = ep_data.get("is_active", True)
|
existing_ep.is_active = ep_data.get("is_active", True)
|
||||||
existing_ep.custom_path = ep_data.get("custom_path")
|
existing_ep.custom_path = ep_data.get("custom_path")
|
||||||
@@ -1079,7 +1074,6 @@ class AdminImportConfigAdapter(AdminApiAdapter):
|
|||||||
api_format=ep_data["api_format"],
|
api_format=ep_data["api_format"],
|
||||||
base_url=ep_data["base_url"],
|
base_url=ep_data["base_url"],
|
||||||
header_rules=ep_data.get("header_rules"),
|
header_rules=ep_data.get("header_rules"),
|
||||||
timeout=ep_data.get("timeout", 300),
|
|
||||||
max_retries=ep_data.get("max_retries", 2),
|
max_retries=ep_data.get("max_retries", 2),
|
||||||
is_active=ep_data.get("is_active", True),
|
is_active=ep_data.get("is_active", True),
|
||||||
custom_path=ep_data.get("custom_path"),
|
custom_path=ep_data.get("custom_path"),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Chat Handler Base - Chat API 格式的通用基类
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, AsyncGenerator, Callable, Dict, Optional
|
from typing import Any, AsyncGenerator, Callable, Dict, Optional
|
||||||
|
|
||||||
@@ -476,7 +477,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
|
|
||||||
# 配置 HTTP 超时
|
# 配置 HTTP 超时
|
||||||
# 注意:read timeout 用于检测连接断开,不是整体请求超时
|
# 注意:read timeout 用于检测连接断开,不是整体请求超时
|
||||||
# 整体请求超时由 asyncio.wait_for 控制,使用 provider.timeout
|
# 整体请求超时由 asyncio.wait_for 控制,使用全局配置
|
||||||
timeout_config = httpx.Timeout(
|
timeout_config = httpx.Timeout(
|
||||||
connect=config.http_connect_timeout,
|
connect=config.http_connect_timeout,
|
||||||
read=config.http_read_timeout, # 使用全局配置,用于检测连接断开
|
read=config.http_read_timeout, # 使用全局配置,用于检测连接断开
|
||||||
@@ -484,8 +485,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
pool=config.http_pool_timeout,
|
pool=config.http_pool_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
# provider.timeout 作为整体请求超时(建立连接 + 获取首字节)
|
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
||||||
request_timeout = float(provider.timeout or 300)
|
request_timeout = config.stream_first_byte_timeout
|
||||||
|
|
||||||
# 创建 HTTP 客户端(支持代理配置,从 Provider 读取)
|
# 创建 HTTP 客户端(支持代理配置,从 Provider 读取)
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
@@ -527,7 +528,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用 asyncio.wait_for 包裹整个"建立连接 + 获取首字节"阶段
|
# 使用 asyncio.wait_for 包裹整个"建立连接 + 获取首字节"阶段
|
||||||
# provider.timeout 控制整体超时,避免上游长时间无响应
|
# stream_first_byte_timeout 控制首字节超时,避免上游长时间无响应
|
||||||
await asyncio.wait_for(_connect_and_prefetch(), timeout=request_timeout)
|
await asyncio.wait_for(_connect_and_prefetch(), timeout=request_timeout)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -716,7 +717,8 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
|
|
||||||
request_timeout = float(provider.timeout or 300)
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
|
request_timeout = config.http_request_timeout
|
||||||
http_client = await HTTPClientPool.get_proxy_client(
|
http_client = await HTTPClientPool.get_proxy_client(
|
||||||
proxy_config=provider.proxy,
|
proxy_config=provider.proxy,
|
||||||
)
|
)
|
||||||
@@ -803,6 +805,26 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
|
|||||||
upstream_status=resp.status_code,
|
upstream_status=resp.status_code,
|
||||||
upstream_response=raw_content,
|
upstream_response=raw_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 检查响应体中的嵌套错误(HTTP 200 但响应体包含错误)
|
||||||
|
if isinstance(response_json, dict):
|
||||||
|
parser = get_parser_for_format(api_format)
|
||||||
|
if parser.is_error_response(response_json):
|
||||||
|
parsed = parser.parse_response(response_json, 200)
|
||||||
|
logger.warning(
|
||||||
|
f" [{self.request_id}] 非流式检测到嵌套错误: "
|
||||||
|
f"Provider={provider.name}, "
|
||||||
|
f"error_type={parsed.error_type}, "
|
||||||
|
f"embedded_status={parsed.embedded_status_code}, "
|
||||||
|
f"message={parsed.error_message}"
|
||||||
|
)
|
||||||
|
raise EmbeddedErrorException(
|
||||||
|
provider_name=str(provider.name),
|
||||||
|
error_code=parsed.embedded_status_code,
|
||||||
|
error_message=parsed.error_message,
|
||||||
|
error_status=parsed.error_type,
|
||||||
|
)
|
||||||
|
|
||||||
return response_json if isinstance(response_json, dict) else {}
|
return response_json if isinstance(response_json, dict) else {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -490,8 +490,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
pool=config.http_pool_timeout,
|
pool=config.http_pool_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
# provider.timeout 作为整体请求超时(建立连接 + 获取首字节)
|
# 流式请求使用 stream_first_byte_timeout 作为首字节超时
|
||||||
request_timeout = float(provider.timeout or 300)
|
request_timeout = config.stream_first_byte_timeout
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f" └─ [{self.request_id}] 发送流式请求: "
|
f" └─ [{self.request_id}] 发送流式请求: "
|
||||||
@@ -539,7 +539,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用 asyncio.wait_for 包裹整个"建立连接 + 获取首字节"阶段
|
# 使用 asyncio.wait_for 包裹整个"建立连接 + 获取首字节"阶段
|
||||||
# provider.timeout 控制整体超时,避免上游长时间无响应
|
# stream_first_byte_timeout 控制首字节超时,避免上游长时间无响应
|
||||||
await asyncio.wait_for(_connect_and_prefetch(), timeout=request_timeout)
|
await asyncio.wait_for(_connect_and_prefetch(), timeout=request_timeout)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
@@ -1633,7 +1633,8 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
# 注意:使用 get_proxy_client 复用连接池,不再每次创建新客户端
|
||||||
from src.clients.http_client import HTTPClientPool
|
from src.clients.http_client import HTTPClientPool
|
||||||
|
|
||||||
request_timeout = float(provider.timeout or 300)
|
# 非流式请求使用 http_request_timeout 作为整体超时
|
||||||
|
request_timeout = config.http_request_timeout
|
||||||
http_client = await HTTPClientPool.get_proxy_client(
|
http_client = await HTTPClientPool.get_proxy_client(
|
||||||
proxy_config=provider.proxy,
|
proxy_config=provider.proxy,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
不再经过 Protocol 抽象层。
|
不再经过 Protocol 抽象层。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
from typing import Any, Dict, Optional, Tuple, Type
|
from typing import Any, Dict, Optional, Tuple, Type
|
||||||
|
|
||||||
from src.api.handlers.base.response_parser import (
|
from src.api.handlers.base.response_parser import (
|
||||||
@@ -58,6 +59,71 @@ def _check_nested_error(response: Dict[str, Any]) -> Tuple[bool, Optional[Dict[s
|
|||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_embedded_status_code(error_info: Optional[Dict[str, Any]]) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
从错误信息中提取嵌套的状态码
|
||||||
|
|
||||||
|
支持多种格式:
|
||||||
|
1. 直接的 code 字段: {"code": 400}
|
||||||
|
2. status 字段: {"status": 400}
|
||||||
|
3. 从 message 中正则提取: "Request failed with status code 400"
|
||||||
|
4. 从 type 字段映射: "invalid_request_error" -> 400
|
||||||
|
|
||||||
|
Args:
|
||||||
|
error_info: 错误信息字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
提取的状态码,如果无法提取则返回 None
|
||||||
|
"""
|
||||||
|
if not error_info:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. 直接的 code 字段(Gemini 等)
|
||||||
|
code = error_info.get("code")
|
||||||
|
if isinstance(code, int) and 100 <= code < 600:
|
||||||
|
return code
|
||||||
|
if isinstance(code, str) and code.isdigit():
|
||||||
|
code_int = int(code)
|
||||||
|
if 100 <= code_int < 600:
|
||||||
|
return code_int
|
||||||
|
|
||||||
|
# 2. status 字段
|
||||||
|
status = error_info.get("status")
|
||||||
|
if isinstance(status, int) and 100 <= status < 600:
|
||||||
|
return status
|
||||||
|
if isinstance(status, str) and status.isdigit():
|
||||||
|
status_int = int(status)
|
||||||
|
if 100 <= status_int < 600:
|
||||||
|
return status_int
|
||||||
|
|
||||||
|
# 3. 从 message 中正则提取 (例如 "Request failed with status code 400")
|
||||||
|
message = error_info.get("message", "")
|
||||||
|
if message:
|
||||||
|
# 匹配 "status code XXX" 或 "status XXX" 或 "HTTP XXX"
|
||||||
|
match = re.search(r"(?:status\s*(?:code\s*)?|HTTP\s*)(\d{3})", message, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
code_int = int(match.group(1))
|
||||||
|
if 100 <= code_int < 600:
|
||||||
|
return code_int
|
||||||
|
|
||||||
|
# 4. 从 type 字段映射常见的错误类型
|
||||||
|
error_type = error_info.get("type", "")
|
||||||
|
type_to_status = {
|
||||||
|
"invalid_request_error": 400,
|
||||||
|
"authentication_error": 401,
|
||||||
|
"permission_error": 403,
|
||||||
|
"not_found_error": 404,
|
||||||
|
"rate_limit_error": 429,
|
||||||
|
"overloaded_error": 503,
|
||||||
|
"api_error": 500,
|
||||||
|
"internal_error": 500,
|
||||||
|
}
|
||||||
|
if error_type and error_type.lower() in type_to_status:
|
||||||
|
return type_to_status[error_type.lower()]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class OpenAIResponseParser(ResponseParser):
|
class OpenAIResponseParser(ResponseParser):
|
||||||
"""OpenAI 格式响应解析器"""
|
"""OpenAI 格式响应解析器"""
|
||||||
|
|
||||||
@@ -141,6 +207,7 @@ class OpenAIResponseParser(ResponseParser):
|
|||||||
result.is_error = True
|
result.is_error = True
|
||||||
result.error_type = error_info.get("type")
|
result.error_type = error_info.get("type")
|
||||||
result.error_message = error_info.get("message")
|
result.error_message = error_info.get("message")
|
||||||
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -273,6 +340,7 @@ class ClaudeResponseParser(ResponseParser):
|
|||||||
result.is_error = True
|
result.is_error = True
|
||||||
result.error_type = error_info.get("type")
|
result.error_type = error_info.get("type")
|
||||||
result.error_message = error_info.get("message")
|
result.error_message = error_info.get("message")
|
||||||
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -417,6 +485,7 @@ class GeminiResponseParser(ResponseParser):
|
|||||||
result.is_error = True
|
result.is_error = True
|
||||||
result.error_type = error_info.get("status")
|
result.error_type = error_info.get("status")
|
||||||
result.error_message = error_info.get("message")
|
result.error_message = error_info.get("message")
|
||||||
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ class ParsedResponse:
|
|||||||
is_error: bool = False
|
is_error: bool = False
|
||||||
error_type: Optional[str] = None
|
error_type: Optional[str] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
# 从响应体解析出的嵌套状态码(当 HTTP 200 但响应体含错误时使用)
|
||||||
|
embedded_status_code: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
class ResponseParser(ABC):
|
class ResponseParser(ABC):
|
||||||
|
|||||||
@@ -286,15 +286,12 @@ class StreamProcessor:
|
|||||||
f" [{self.request_id}] 检测到嵌套错误: "
|
f" [{self.request_id}] 检测到嵌套错误: "
|
||||||
f"Provider={provider.name}, "
|
f"Provider={provider.name}, "
|
||||||
f"error_type={parsed.error_type}, "
|
f"error_type={parsed.error_type}, "
|
||||||
|
f"embedded_status={parsed.embedded_status_code}, "
|
||||||
f"message={parsed.error_message}"
|
f"message={parsed.error_message}"
|
||||||
)
|
)
|
||||||
raise EmbeddedErrorException(
|
raise EmbeddedErrorException(
|
||||||
provider_name=str(provider.name),
|
provider_name=str(provider.name),
|
||||||
error_code=(
|
error_code=parsed.embedded_status_code,
|
||||||
int(parsed.error_type)
|
|
||||||
if parsed.error_type and parsed.error_type.isdigit()
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
error_message=parsed.error_message,
|
error_message=parsed.error_message,
|
||||||
error_status=parsed.error_type,
|
error_status=parsed.error_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -186,15 +186,12 @@ def check_prefetched_response_error(
|
|||||||
f" [{request_id}] 检测到 JSON 错误响应: "
|
f" [{request_id}] 检测到 JSON 错误响应: "
|
||||||
f"Provider={provider_name}, "
|
f"Provider={provider_name}, "
|
||||||
f"error_type={parsed.error_type}, "
|
f"error_type={parsed.error_type}, "
|
||||||
|
f"embedded_status={parsed.embedded_status_code}, "
|
||||||
f"message={parsed.error_message}"
|
f"message={parsed.error_message}"
|
||||||
)
|
)
|
||||||
raise EmbeddedErrorException(
|
raise EmbeddedErrorException(
|
||||||
provider_name=provider_name,
|
provider_name=provider_name,
|
||||||
error_code=(
|
error_code=parsed.embedded_status_code,
|
||||||
int(parsed.error_type)
|
|
||||||
if parsed.error_type and parsed.error_type.isdigit()
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
error_message=parsed.error_message,
|
error_message=parsed.error_message,
|
||||||
error_status=parsed.error_type,
|
error_status=parsed.error_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -214,16 +214,14 @@ class TimeoutDefaults:
|
|||||||
"""超时配置默认值(秒)
|
"""超时配置默认值(秒)
|
||||||
|
|
||||||
超时配置说明:
|
超时配置说明:
|
||||||
- 全局默认值和 Provider 默认值统一为 120 秒
|
- 非流式请求超时由环境变量 HTTP_REQUEST_TIMEOUT 控制(默认 300 秒)
|
||||||
- 120 秒是 LLM API 的合理默认值:
|
- 流式请求首字节超时由环境变量 STREAM_FIRST_BYTE_TIMEOUT 控制(默认 30 秒)
|
||||||
* 大多数请求在 30 秒内完成
|
- 此处的常量仅用于无法访问 config 的场景(如模型查询测试)
|
||||||
* 复杂推理(如 Claude extended thinking)可能需要 60-90 秒
|
|
||||||
* 120 秒足够覆盖大部分场景,同时避免线程池被长时间占用
|
|
||||||
- 如需更长超时,可在 Provider 级别单独配置
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# HTTP 请求默认超时(与 Provider 默认值保持一致)
|
# HTTP 请求默认超时(用于模型查询等测试场景)
|
||||||
HTTP_REQUEST = 120 # 2分钟
|
# 与 config.http_request_timeout 默认值保持一致
|
||||||
|
HTTP_REQUEST = 300 # 5分钟
|
||||||
|
|
||||||
# 数据库连接池获取超时
|
# 数据库连接池获取超时
|
||||||
DB_POOL = 30
|
DB_POOL = 30
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ class Config:
|
|||||||
self.http_read_timeout = float(os.getenv("HTTP_READ_TIMEOUT", "60.0"))
|
self.http_read_timeout = float(os.getenv("HTTP_READ_TIMEOUT", "60.0"))
|
||||||
self.http_write_timeout = float(os.getenv("HTTP_WRITE_TIMEOUT", "60.0"))
|
self.http_write_timeout = float(os.getenv("HTTP_WRITE_TIMEOUT", "60.0"))
|
||||||
self.http_pool_timeout = float(os.getenv("HTTP_POOL_TIMEOUT", "10.0"))
|
self.http_pool_timeout = float(os.getenv("HTTP_POOL_TIMEOUT", "10.0"))
|
||||||
|
# HTTP_REQUEST_TIMEOUT: 非流式请求整体超时(秒),默认 300 秒
|
||||||
|
self.http_request_timeout = float(os.getenv("HTTP_REQUEST_TIMEOUT", "300.0"))
|
||||||
|
|
||||||
# HTTP 连接池配置
|
# HTTP 连接池配置
|
||||||
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
|
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
|
||||||
@@ -167,10 +169,9 @@ class Config:
|
|||||||
# STREAM_PREFETCH_LINES: 预读行数,用于检测嵌套错误
|
# STREAM_PREFETCH_LINES: 预读行数,用于检测嵌套错误
|
||||||
# STREAM_STATS_DELAY: 统计记录延迟(秒),等待流完全关闭
|
# STREAM_STATS_DELAY: 统计记录延迟(秒),等待流完全关闭
|
||||||
# STREAM_FIRST_BYTE_TIMEOUT: 首字节超时(秒),等待首字节超过此时间触发故障转移
|
# STREAM_FIRST_BYTE_TIMEOUT: 首字节超时(秒),等待首字节超过此时间触发故障转移
|
||||||
# 范围: 10-120 秒,默认 30 秒(必须小于 http_write_timeout 避免竞态)
|
|
||||||
self.stream_prefetch_lines = int(os.getenv("STREAM_PREFETCH_LINES", "5"))
|
self.stream_prefetch_lines = int(os.getenv("STREAM_PREFETCH_LINES", "5"))
|
||||||
self.stream_stats_delay = float(os.getenv("STREAM_STATS_DELAY", "0.1"))
|
self.stream_stats_delay = float(os.getenv("STREAM_STATS_DELAY", "0.1"))
|
||||||
self.stream_first_byte_timeout = self._parse_ttfb_timeout()
|
self.stream_first_byte_timeout = float(os.getenv("STREAM_FIRST_BYTE_TIMEOUT", "30.0"))
|
||||||
|
|
||||||
# 请求体读取超时(秒)
|
# 请求体读取超时(秒)
|
||||||
# REQUEST_BODY_TIMEOUT: 等待客户端发送完整请求体的超时时间
|
# REQUEST_BODY_TIMEOUT: 等待客户端发送完整请求体的超时时间
|
||||||
@@ -289,39 +290,6 @@ class Config:
|
|||||||
# 最小 10 个保活连接,最大不超过 max_connections
|
# 最小 10 个保活连接,最大不超过 max_connections
|
||||||
return max(10, min(keepalive, self.http_max_connections))
|
return max(10, min(keepalive, self.http_max_connections))
|
||||||
|
|
||||||
def _parse_ttfb_timeout(self) -> float:
|
|
||||||
"""
|
|
||||||
解析 TTFB 超时配置,带错误处理和范围限制
|
|
||||||
|
|
||||||
TTFB (Time To First Byte) 用于检测慢响应的 Provider,超时触发故障转移。
|
|
||||||
此值必须小于 http_write_timeout,避免竞态条件。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
超时时间(秒),范围 10-120,默认 30
|
|
||||||
"""
|
|
||||||
default_timeout = 30.0
|
|
||||||
min_timeout = 10.0
|
|
||||||
max_timeout = 120.0 # 必须小于 http_write_timeout (默认 60s) 的 2 倍
|
|
||||||
|
|
||||||
raw_value = os.getenv("STREAM_FIRST_BYTE_TIMEOUT", str(default_timeout))
|
|
||||||
try:
|
|
||||||
timeout = float(raw_value)
|
|
||||||
except ValueError:
|
|
||||||
# 延迟导入,避免循环依赖(Config 初始化时 logger 可能未就绪)
|
|
||||||
self._ttfb_config_warning = (
|
|
||||||
f"无效的 STREAM_FIRST_BYTE_TIMEOUT 配置 '{raw_value}',使用默认值 {default_timeout}秒"
|
|
||||||
)
|
|
||||||
return default_timeout
|
|
||||||
|
|
||||||
# 范围限制
|
|
||||||
clamped = max(min_timeout, min(max_timeout, timeout))
|
|
||||||
if clamped != timeout:
|
|
||||||
self._ttfb_config_warning = (
|
|
||||||
f"STREAM_FIRST_BYTE_TIMEOUT={timeout}秒超出范围 [{min_timeout}-{max_timeout}],"
|
|
||||||
f"已调整为 {clamped}秒"
|
|
||||||
)
|
|
||||||
return clamped
|
|
||||||
|
|
||||||
def _validate_pool_config(self) -> None:
|
def _validate_pool_config(self) -> None:
|
||||||
"""验证连接池配置是否安全"""
|
"""验证连接池配置是否安全"""
|
||||||
total_per_worker = self.db_pool_size + self.db_max_overflow
|
total_per_worker = self.db_pool_size + self.db_max_overflow
|
||||||
@@ -369,10 +337,6 @@ class Config:
|
|||||||
if hasattr(self, "_pool_config_warning") and self._pool_config_warning:
|
if hasattr(self, "_pool_config_warning") and self._pool_config_warning:
|
||||||
logger.warning(self._pool_config_warning)
|
logger.warning(self._pool_config_warning)
|
||||||
|
|
||||||
# TTFB 超时配置警告
|
|
||||||
if hasattr(self, "_ttfb_config_warning") and self._ttfb_config_warning:
|
|
||||||
logger.warning(self._ttfb_config_warning)
|
|
||||||
|
|
||||||
# 管理员密码检查(必须在环境变量中设置)
|
# 管理员密码检查(必须在环境变量中设置)
|
||||||
if hasattr(self, "_missing_admin_password") and self._missing_admin_password:
|
if hasattr(self, "_missing_admin_password") and self._missing_admin_password:
|
||||||
logger.error("必须设置 ADMIN_PASSWORD 环境变量!")
|
logger.error("必须设置 ADMIN_PASSWORD 环境变量!")
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ class CreateProviderRequest(BaseModel):
|
|||||||
is_active: Optional[bool] = Field(True, description="是否启用")
|
is_active: Optional[bool] = Field(True, description="是否启用")
|
||||||
concurrent_limit: Optional[int] = Field(None, ge=0, description="并发限制")
|
concurrent_limit: Optional[int] = Field(None, ge=0, description="并发限制")
|
||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
timeout: Optional[int] = Field(300, ge=1, le=600, description="请求超时(秒)")
|
|
||||||
max_retries: Optional[int] = Field(2, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(2, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
||||||
config: Optional[Dict[str, Any]] = Field(None, description="其他配置")
|
config: Optional[Dict[str, Any]] = Field(None, description="其他配置")
|
||||||
@@ -159,7 +158,6 @@ class UpdateProviderRequest(BaseModel):
|
|||||||
is_active: Optional[bool] = None
|
is_active: Optional[bool] = None
|
||||||
concurrent_limit: Optional[int] = Field(None, ge=0)
|
concurrent_limit: Optional[int] = Field(None, ge=0)
|
||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
timeout: Optional[int] = Field(None, ge=1, le=600, description="请求超时(秒)")
|
|
||||||
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(None, ge=0, le=10, description="最大重试次数")
|
||||||
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
proxy: Optional[ProxyConfig] = Field(None, description="代理配置")
|
||||||
config: Optional[Dict[str, Any]] = None
|
config: Optional[Dict[str, Any]] = None
|
||||||
|
|||||||
@@ -403,7 +403,6 @@ class ProviderUpdate(BaseModel):
|
|||||||
api_format: Optional[str] = None
|
api_format: Optional[str] = None
|
||||||
base_url: Optional[str] = None
|
base_url: Optional[str] = None
|
||||||
headers: Optional[dict] = None
|
headers: Optional[dict] = None
|
||||||
timeout: Optional[int] = Field(None, ge=1, le=600)
|
|
||||||
max_retries: Optional[int] = Field(None, ge=0, le=10)
|
max_retries: Optional[int] = Field(None, ge=0, le=10)
|
||||||
priority: Optional[int] = None
|
priority: Optional[int] = None
|
||||||
weight: Optional[float] = Field(None, gt=0)
|
weight: Optional[float] = Field(None, gt=0)
|
||||||
@@ -423,7 +422,6 @@ class ProviderResponse(BaseModel):
|
|||||||
api_format: str
|
api_format: str
|
||||||
base_url: str
|
base_url: str
|
||||||
headers: Optional[dict]
|
headers: Optional[dict]
|
||||||
timeout: int
|
|
||||||
max_retries: int
|
max_retries: int
|
||||||
priority: int
|
priority: int
|
||||||
weight: float
|
weight: float
|
||||||
|
|||||||
@@ -552,11 +552,10 @@ class Provider(Base):
|
|||||||
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
|
concurrent_limit = Column(Integer, nullable=True) # 并发请求限制
|
||||||
|
|
||||||
# 请求配置(从 Endpoint 迁移,作为全局默认值)
|
# 请求配置(从 Endpoint 迁移,作为全局默认值)
|
||||||
# 超时 300 秒对于 LLM API 是合理的默认值:
|
# [已废弃] timeout 字段不再使用,超时由环境变量控制:
|
||||||
# - 大多数请求在 30 秒内完成
|
# - 非流式请求: HTTP_REQUEST_TIMEOUT(默认 300 秒)
|
||||||
# - 复杂推理(如 Claude thinking)可能需要 60-120 秒
|
# - 流式首字节: STREAM_FIRST_BYTE_TIMEOUT(默认 30 秒)
|
||||||
# - 300 秒足够覆盖极端场景(如超长上下文、复杂工具调用)
|
timeout = Column(Integer, default=300, nullable=True) # [已废弃] 请求超时(秒)
|
||||||
timeout = Column(Integer, default=300, nullable=True) # 请求超时(秒)
|
|
||||||
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
max_retries = Column(Integer, default=2, nullable=True) # 最大重试次数
|
||||||
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
proxy = Column(JSONB, nullable=True) # 代理配置: {url, username, password, enabled}
|
||||||
|
|
||||||
@@ -604,7 +603,7 @@ class ProviderEndpoint(Base):
|
|||||||
|
|
||||||
# 请求配置
|
# 请求配置
|
||||||
header_rules = Column(JSON, nullable=True) # 请求头规则 [{action, key, value, from, to}]
|
header_rules = Column(JSON, nullable=True) # 请求头规则 [{action, key, value, from, to}]
|
||||||
timeout = Column(Integer, default=300) # 超时(秒)
|
timeout = Column(Integer, default=300) # [已废弃] 超时(秒),由环境变量控制
|
||||||
max_retries = Column(Integer, default=2) # 最大重试次数
|
max_retries = Column(Integer, default=2) # 最大重试次数
|
||||||
|
|
||||||
# 状态
|
# 状态
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class ProviderEndpointCreate(BaseModel):
|
|||||||
description="请求头规则列表,支持 set/drop/rename 操作",
|
description="请求头规则列表,支持 set/drop/rename 操作",
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout: int = Field(default=300, ge=10, le=600, description="超时时间(秒)")
|
|
||||||
max_retries: int = Field(default=2, ge=0, le=10, description="最大重试次数")
|
max_retries: int = Field(default=2, ge=0, le=10, description="最大重试次数")
|
||||||
|
|
||||||
# 额外配置
|
# 额外配置
|
||||||
@@ -81,7 +80,6 @@ class ProviderEndpointUpdate(BaseModel):
|
|||||||
description="请求头规则列表,支持 set/drop/rename 操作",
|
description="请求头规则列表,支持 set/drop/rename 操作",
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout: Optional[int] = Field(default=None, ge=10, le=600, description="超时时间(秒)")
|
|
||||||
max_retries: Optional[int] = Field(default=None, ge=0, le=10, description="最大重试次数")
|
max_retries: Optional[int] = Field(default=None, ge=0, le=10, description="最大重试次数")
|
||||||
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
||||||
config: Optional[Dict[str, Any]] = Field(default=None, description="额外配置")
|
config: Optional[Dict[str, Any]] = Field(default=None, description="额外配置")
|
||||||
@@ -117,7 +115,6 @@ class ProviderEndpointResponse(BaseModel):
|
|||||||
default=None, description="请求头规则列表"
|
default=None, description="请求头规则列表"
|
||||||
)
|
)
|
||||||
|
|
||||||
timeout: int
|
|
||||||
max_retries: int
|
max_retries: int
|
||||||
|
|
||||||
# 状态
|
# 状态
|
||||||
@@ -619,7 +616,6 @@ class ProviderWithEndpointsSummary(BaseModel):
|
|||||||
quota_expires_at: Optional[datetime] = Field(default=None, description="配额过期时间")
|
quota_expires_at: Optional[datetime] = Field(default=None, description="配额过期时间")
|
||||||
|
|
||||||
# 请求配置(从 Endpoint 迁移)
|
# 请求配置(从 Endpoint 迁移)
|
||||||
timeout: Optional[int] = Field(default=300, description="请求超时(秒)")
|
|
||||||
max_retries: Optional[int] = Field(default=2, description="最大重试次数")
|
max_retries: Optional[int] = Field(default=2, description="最大重试次数")
|
||||||
proxy: Optional[Dict[str, Any]] = Field(default=None, description="代理配置")
|
proxy: Optional[Dict[str, Any]] = Field(default=None, description="代理配置")
|
||||||
|
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ class AuthService:
|
|||||||
|
|
||||||
if key_record.is_locked:
|
if key_record.is_locked:
|
||||||
logger.warning("API认证失败 - 密钥已被管理员锁定")
|
logger.warning("API认证失败 - 密钥已被管理员锁定")
|
||||||
raise ForbiddenException("该API密钥已被管理员锁定,请联系管理员")
|
raise ForbiddenException("该密钥已被管理员锁定,请联系管理员")
|
||||||
|
|
||||||
# 检查过期时间
|
# 检查过期时间
|
||||||
if key_record.expires_at:
|
if key_record.expires_at:
|
||||||
|
|||||||
@@ -358,6 +358,8 @@ class FallbackOrchestrator:
|
|||||||
# 需要检查错误消息是否为客户端错误(如 prompt is too long),这类错误不应重试
|
# 需要检查错误消息是否为客户端错误(如 prompt is too long),这类错误不应重试
|
||||||
if isinstance(cause, EmbeddedErrorException):
|
if isinstance(cause, EmbeddedErrorException):
|
||||||
error_message = cause.error_message or ""
|
error_message = cause.error_message or ""
|
||||||
|
# 使用嵌入式状态码(如果有),否则默认 200
|
||||||
|
embedded_status = cause.error_code or 200
|
||||||
if self._error_classifier.is_client_error(error_message):
|
if self._error_classifier.is_client_error(error_message):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f" [{request_id}] 嵌入式客户端错误,停止重试: {error_message[:200]}"
|
f" [{request_id}] 嵌入式客户端错误,停止重试: {error_message[:200]}"
|
||||||
@@ -366,7 +368,7 @@ class FallbackOrchestrator:
|
|||||||
client_error = UpstreamClientException(
|
client_error = UpstreamClientException(
|
||||||
message=error_message or "请求无效",
|
message=error_message or "请求无效",
|
||||||
provider_name=str(provider.name),
|
provider_name=str(provider.name),
|
||||||
status_code=200, # 嵌入式错误的 HTTP 状态码通常是 200
|
status_code=embedded_status,
|
||||||
upstream_error=error_message,
|
upstream_error=error_message,
|
||||||
)
|
)
|
||||||
RequestCandidateService.mark_candidate_failed(
|
RequestCandidateService.mark_candidate_failed(
|
||||||
@@ -374,7 +376,7 @@ class FallbackOrchestrator:
|
|||||||
candidate_id=candidate_record_id,
|
candidate_id=candidate_record_id,
|
||||||
error_type="UpstreamClientException",
|
error_type="UpstreamClientException",
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
status_code=200,
|
status_code=embedded_status,
|
||||||
latency_ms=elapsed_ms,
|
latency_ms=elapsed_ms,
|
||||||
concurrent_requests=captured_key_concurrent,
|
concurrent_requests=captured_key_concurrent,
|
||||||
)
|
)
|
||||||
@@ -396,7 +398,7 @@ class FallbackOrchestrator:
|
|||||||
candidate_id=candidate_record_id,
|
candidate_id=candidate_record_id,
|
||||||
error_type="EmbeddedErrorException",
|
error_type="EmbeddedErrorException",
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
status_code=200,
|
status_code=embedded_status,
|
||||||
latency_ms=elapsed_ms,
|
latency_ms=elapsed_ms,
|
||||||
concurrent_requests=captured_key_concurrent,
|
concurrent_requests=captured_key_concurrent,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1703,11 +1703,9 @@ class UsageService:
|
|||||||
Returns:
|
Returns:
|
||||||
请求状态列表
|
请求状态列表
|
||||||
"""
|
"""
|
||||||
from src.models.database import ProviderEndpoint
|
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# 构建基础查询,包含端点的 timeout 配置
|
# 构建基础查询
|
||||||
query = db.query(
|
query = db.query(
|
||||||
Usage.id,
|
Usage.id,
|
||||||
Usage.status,
|
Usage.status,
|
||||||
@@ -1722,8 +1720,7 @@ class UsageService:
|
|||||||
Usage.first_byte_time_ms, # 首字时间 (TTFB)
|
Usage.first_byte_time_ms, # 首字时间 (TTFB)
|
||||||
Usage.created_at,
|
Usage.created_at,
|
||||||
Usage.provider_endpoint_id,
|
Usage.provider_endpoint_id,
|
||||||
ProviderEndpoint.timeout.label("endpoint_timeout"),
|
)
|
||||||
).outerjoin(ProviderEndpoint, Usage.provider_endpoint_id == ProviderEndpoint.id)
|
|
||||||
|
|
||||||
# 管理员轮询:可附带 provider 与上游 key 名称(注意:不要在普通用户接口暴露上游 key 信息)
|
# 管理员轮询:可附带 provider 与上游 key 名称(注意:不要在普通用户接口暴露上游 key 信息)
|
||||||
if include_admin_fields:
|
if include_admin_fields:
|
||||||
@@ -1751,8 +1748,8 @@ class UsageService:
|
|||||||
timeout_ids = []
|
timeout_ids = []
|
||||||
for r in records:
|
for r in records:
|
||||||
if r.status in ("pending", "streaming") and r.created_at:
|
if r.status in ("pending", "streaming") and r.created_at:
|
||||||
# 使用端点配置的超时时间,若无则使用默认值
|
# 使用全局配置的超时时间
|
||||||
timeout_seconds = r.endpoint_timeout or default_timeout_seconds
|
timeout_seconds = default_timeout_seconds
|
||||||
|
|
||||||
# 处理时区:如果 created_at 没有时区信息,假定为 UTC
|
# 处理时区:如果 created_at 没有时区信息,假定为 UTC
|
||||||
created_at = r.created_at
|
created_at = r.created_at
|
||||||
|
|||||||
Reference in New Issue
Block a user