feat: 添加提供商格式转换优先级保持配置

- 新增 Provider.keep_priority_on_conversion 字段,控制格式转换时是否保持优先级
- 新增全局配置 KEEP_PRIORITY_ON_CONVERSION,可全局启用优先级保持
- 调度器根据配置决定是否降级需要格式转换的候选
- 前端 Provider 表单添加"保持优先级"开关
- 调整 HTTP 读写超时默认值为 3600 秒
This commit is contained in:
fawney19
2026-01-28 15:53:33 +08:00
parent 5e81a0b581
commit bae278a0c1
8 changed files with 212 additions and 27 deletions
+1
View File
@@ -301,6 +301,7 @@ def _build_provider_summary(db: Session, provider: Provider) -> ProviderWithEndp
description=provider.description,
website=provider.website,
provider_priority=provider.provider_priority,
keep_priority_on_conversion=provider.keep_priority_on_conversion,
is_active=provider.is_active,
billing_type=provider.billing_type.value if provider.billing_type else None,
monthly_quota_usd=provider.monthly_quota_usd,
+10 -2
View File
@@ -141,8 +141,8 @@ class Config:
# HTTP 请求超时配置(秒)
self.http_connect_timeout = float(os.getenv("HTTP_CONNECT_TIMEOUT", "10.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_read_timeout = float(os.getenv("HTTP_READ_TIMEOUT", "3600.0"))
self.http_write_timeout = float(os.getenv("HTTP_WRITE_TIMEOUT", "3600.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"))
@@ -154,6 +154,14 @@ class Config:
"FORMAT_CONVERSION_ENABLED", "true"
).lower() == "true"
# KEEP_PRIORITY_ON_CONVERSION: 格式转换时是否保持提供商原优先级,默认关闭
# - false(默认): 需要格式转换的候选整体降级到不需要转换的候选之后
# - true: 所有提供商保持原优先级,不因格式转换降级
# 注意:即使全局关闭,单个提供商也可以通过 keep_priority_on_conversion 字段保持自己的优先级
self.keep_priority_on_conversion = os.getenv(
"KEEP_PRIORITY_ON_CONVERSION", "false"
).lower() == "true"
# HTTP 连接池配置
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
# - 每个连接占用一个 socket,过多会耗尽系统资源
+6
View File
@@ -645,6 +645,12 @@ class Provider(Base):
# 101+: 备用(高成本或限制严格的)
provider_priority = Column(Integer, default=100)
# 格式转换时是否保持优先级(默认 False)
# - False: 需要格式转换时,该提供商的候选会被降级到不需要转换的候选之后
# - True: 即使需要格式转换,也保持原优先级排名
# 注意:如果全局配置 KEEP_PRIORITY_ON_CONVERSION=true,此字段被忽略(所有提供商都保持优先级)
keep_priority_on_conversion = Column(Boolean, default=False, nullable=False)
# 状态
is_active = Column(Boolean, default=True, nullable=False)
+8
View File
@@ -613,6 +613,10 @@ class ProviderUpdateRequest(BaseModel):
description: Optional[str] = None
website: Optional[str] = Field(None, max_length=500, description="主站网站")
provider_priority: Optional[int] = Field(None, description="提供商优先级(数字越小越优先)")
keep_priority_on_conversion: Optional[bool] = Field(
None,
description="格式转换时是否保持优先级(True=保持原优先级,False=需要转换时降级)",
)
is_active: Optional[bool] = None
billing_type: Optional[str] = Field(
None, description="计费类型:monthly_quota/pay_as_you_go/free_tier"
@@ -637,6 +641,10 @@ class ProviderWithEndpointsSummary(BaseModel):
description: Optional[str] = None
website: Optional[str] = None
provider_priority: int = Field(default=100, description="提供商优先级(数字越小越优先)")
keep_priority_on_conversion: bool = Field(
default=False,
description="格式转换时是否保持优先级(True=保持原优先级,False=需要转换时降级)",
)
is_active: bool
# 计费相关字段
+105 -25
View File
@@ -1162,9 +1162,22 @@ class CacheAwareScheduler:
candidate.is_cached = False
return candidates
# 按是否匹配缓存亲和性分类候选
cached_candidates: List[ProviderCandidate] = []
other_candidates: List[ProviderCandidate] = []
# 判断候选是否应该被降级(用于分组)
from src.config.settings import config
global_keep_priority = config.keep_priority_on_conversion
def should_demote(c: ProviderCandidate) -> bool:
"""判断候选是否应该被降级"""
if global_keep_priority:
return False # 全局开启时,所有候选都不降级
if not c.needs_conversion:
return False # exact 候选不降级
if getattr(c.provider, "keep_priority_on_conversion", False):
return False # 提供商配置了保持优先级
return True # 需要降级
# 按是否匹配缓存亲和性分类候选,同时记录是否降级
matched_candidate: Optional[ProviderCandidate] = None
matched = False
for candidate in candidates:
@@ -1178,7 +1191,7 @@ class CacheAwareScheduler:
and key.id == affinity.key_id
):
candidate.is_cached = True
cached_candidates.append(candidate)
matched_candidate = candidate
matched = True
logger.debug(
f"检测到缓存亲和性: affinity_key={affinity_key[:8]}..., "
@@ -1189,18 +1202,57 @@ class CacheAwareScheduler:
)
else:
candidate.is_cached = False
other_candidates.append(candidate)
if not matched:
logger.debug(f"API格式 {api_format_str} 的缓存亲和性存在但组合不可用")
return candidates
# 重新排序:缓存候选优先
if cached_candidates:
result = cached_candidates + other_candidates
logger.debug(f"{len(cached_candidates)} 个缓存组合已提升至优先级")
# 缓存亲和性命中且该候选可用(未被跳过)时,无条件优先使用
# 理由:1) 它之前成功过;2) 它有 prompt cache 优势
# 只有当缓存亲和性的候选被跳过(健康度太低/熔断)时,才按 exact 优先排序
assert matched_candidate is not None # guaranteed by matched=True
if not matched_candidate.is_skipped:
# 缓存命中且健康,无条件提升到最前面
other_candidates = [c for c in candidates if c is not matched_candidate]
result = [matched_candidate] + other_candidates
logger.debug(
f"缓存亲和性命中且健康,无条件优先使用 "
f"(needs_conversion={matched_candidate.needs_conversion})"
)
return result
return candidates
# 缓存命中但被跳过(不健康),按 exact 优先排序
# 缓存候选在其所属类别内提升到最前面
logger.debug(
f"缓存亲和性命中但不健康 (skip_reason={matched_candidate.skip_reason}),"
f"按 exact 优先排序"
)
matched_should_demote = should_demote(matched_candidate)
# 分组:非降级类 和 降级类
keep_priority_candidates: List[ProviderCandidate] = []
demote_candidates: List[ProviderCandidate] = []
for c in candidates:
if c is matched_candidate:
continue # 先跳过缓存命中的候选
if should_demote(c):
demote_candidates.append(c)
else:
keep_priority_candidates.append(c)
# 将缓存命中的候选插入到其所属类别的最前面
if matched_should_demote:
# 缓存命中的是降级类,插入到降级类最前面
demote_candidates.insert(0, matched_candidate)
else:
# 缓存命中的是非降级类,插入到非降级类最前面
keep_priority_candidates.insert(0, matched_candidate)
result = keep_priority_candidates + demote_candidates
logger.debug(f"缓存组合已提升至其类别内优先级 (demote={matched_should_demote})")
return result
except Exception as e:
logger.warning(f"检查缓存亲和性失败: {e},继续使用默认排序")
@@ -1249,31 +1301,59 @@ class CacheAwareScheduler:
"""
根据优先级模式对候选列表排序(数字越小越优先)
排序规则:
1. exact 候选(needs_conversion=False)优先于 convertible 候选
2. 在同一类型内,按优先级模式排序:
- provider: 提供商优先模式,按 Provider.provider_priority -> Key.internal_priority 排序
- global_key: 全局 Key 优先模式,按 Key.global_priority_by_format 排序
排序规则(受 KEEP_PRIORITY_ON_CONVERSION 配置影响):
1. 如果全局配置 keep_priority_on_conversion=True,所有候选保持原优先级
2. 否则,按 needs_conversion 和 provider.keep_priority_on_conversion 分组:
- 保持优先级的候选(exact 或 provider.keep_priority_on_conversion=True)按原优先级排序
- 需要降级的候选(convertible 且 provider.keep_priority_on_conversion=False)整体排在后面
3. 在同一组内,按优先级模式排序:
- provider: 按 Provider.provider_priority -> Key.internal_priority 排序
- global_key: 按 Key.global_priority_by_format 排序
"""
if not candidates:
return candidates
# 按 needs_conversion 分组:exact 优先
exact_candidates = [c for c in candidates if not c.needs_conversion]
convertible_candidates = [c for c in candidates if c.needs_conversion]
from src.config.settings import config
# 全局配置:如果开启,所有候选保持原优先级
global_keep_priority = config.keep_priority_on_conversion
if global_keep_priority:
# 全局开启:不分组,直接按优先级模式排序
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
return self._sort_by_global_priority_with_hash(candidates, affinity_key, api_format)
# 提供商优先模式:保持构建时的顺序(已按 provider_priority 排序)
return candidates
# 全局未开启:按是否需要降级分组
# - 不需要降级:exact 候选 或 provider.keep_priority_on_conversion=True 的 convertible 候选
# - 需要降级:convertible 且 provider.keep_priority_on_conversion=False
keep_priority_candidates: List[ProviderCandidate] = []
demote_candidates: List[ProviderCandidate] = []
for c in candidates:
if not c.needs_conversion:
# exact 候选:不需要降级
keep_priority_candidates.append(c)
elif getattr(c.provider, "keep_priority_on_conversion", False):
# convertible 但提供商配置了保持优先级
keep_priority_candidates.append(c)
else:
# convertible 且未配置保持优先级:降级
demote_candidates.append(c)
if self.priority_mode == self.PRIORITY_MODE_GLOBAL_KEY:
# 全局 Key 优先模式:分别对两组排序后合并
sorted_exact = self._sort_by_global_priority_with_hash(
exact_candidates, affinity_key, api_format
sorted_keep = self._sort_by_global_priority_with_hash(
keep_priority_candidates, affinity_key, api_format
)
sorted_convertible = self._sort_by_global_priority_with_hash(
convertible_candidates, affinity_key, api_format
sorted_demote = self._sort_by_global_priority_with_hash(
demote_candidates, affinity_key, api_format
)
return sorted_exact + sorted_convertible
return sorted_keep + sorted_demote
# 提供商优先模式:exact 在前,convertible 在后(各组内部顺序已由构建时保证)
return exact_candidates + convertible_candidates
# 提供商优先模式:保持优先级的在前,降级的在后(各组内部顺序已由构建时保证)
return keep_priority_candidates + demote_candidates
def _sort_by_global_priority_with_hash(
self,