feat: 添加格式转换追踪、模型过滤规则和 Provider 超时配置

1. Usage 格式转换追踪
   - 新增 endpoint_api_format 和 has_format_conversion 字段
   - 在用量记录中显示格式转换信息(请求格式 → 端点格式)
   - 兼容历史数据的回填逻辑

2. Provider API Key 模型过滤规则
   - 新增 model_include_patterns 和 model_exclude_patterns 字段
   - 支持 * 和 ? 通配符,不区分大小写
   - 自动获取模型时应用过滤规则

3. Provider 超时配置
   - 新增 stream_first_byte_timeout 和 request_timeout 字段
   - 支持每个 Provider 单独配置超时时间
   - 优先使用 Provider 配置,否则回退到全局配置

Close #122, Close #123

Co-Authored-By: AAEE86 <33052466+AAEE86@users.noreply.github.com>
This commit is contained in:
fawney19
2026-01-28 00:04:47 +08:00
parent 3c0bf5fdae
commit c732a03263
23 changed files with 652 additions and 97 deletions

View File

@@ -7,13 +7,15 @@
- 扫描所有启用了 auto_fetch_models 的 ProviderAPIKey
- 调用 Adapter.fetch_models() 获取模型列表
- 更新 Key 的 allowed_models保留 locked_models 中的模型)
- 支持包含/排除规则过滤模型
- 记录获取结果和错误信息
"""
import asyncio
import fnmatch
import os
from datetime import datetime, timezone
from typing import Optional
from typing import List, Optional, Set
from sqlalchemy.orm import Session, joinedload
@@ -42,6 +44,68 @@ KEY_FETCH_TIMEOUT_SECONDS = 120
UPSTREAM_MODELS_CACHE_TTL_SECONDS = MODEL_FETCH_INTERVAL_MINUTES * 60
def _match_pattern(model_id: str, pattern: str) -> bool:
"""
检查模型 ID 是否匹配模式
支持的通配符:
- * 匹配任意字符(包括空)
- ? 匹配单个字符
Args:
model_id: 模型 ID
pattern: 匹配模式
Returns:
是否匹配
"""
return fnmatch.fnmatch(model_id.lower(), pattern.lower())
def _filter_models_by_patterns(
model_ids: Set[str],
include_patterns: Optional[List[str]],
exclude_patterns: Optional[List[str]],
) -> Set[str]:
"""
根据包含/排除规则过滤模型列表
规则优先级:
1. 如果 include_patterns 为空或 None则包含所有模型
2. 如果 include_patterns 不为空,则只包含匹配的模型
3. exclude_patterns 总是会排除匹配的模型(优先级高于 include
Args:
model_ids: 原始模型 ID 集合
include_patterns: 包含规则列表(支持 * 和 ? 通配符)
exclude_patterns: 排除规则列表(支持 * 和 ? 通配符)
Returns:
过滤后的模型 ID 集合
"""
result = set()
for model_id in model_ids:
# 步骤1: 检查是否应该包含
should_include = True
if include_patterns:
# 有包含规则时,必须匹配至少一个规则
should_include = any(_match_pattern(model_id, p) for p in include_patterns)
if not should_include:
continue
# 步骤2: 检查是否应该排除
should_exclude = False
if exclude_patterns:
should_exclude = any(_match_pattern(model_id, p) for p in exclude_patterns)
if not should_exclude:
result.add(model_id)
return result
def _get_upstream_models_cache_key(provider_id: str, api_key_id: str) -> str:
"""生成上游模型缓存的 key"""
return f"upstream_models:{provider_id}:{api_key_id}"
@@ -341,7 +405,7 @@ class ModelFetchScheduler:
def _update_key_allowed_models(self, key: ProviderAPIKey, fetched_model_ids: set[str]) -> bool:
"""
更新 Key 的 allowed_models保留 locked_models
更新 Key 的 allowed_models保留 locked_models,应用过滤规则
Returns:
bool: 是否有变化
@@ -349,9 +413,26 @@ class ModelFetchScheduler:
# 获取当前锁定的模型
locked_models = set(key.locked_models or [])
# 新的 allowed_models = 获取到的模型 + 锁定的模型
# 应用包含/排除过滤规则
include_patterns = key.model_include_patterns
exclude_patterns = key.model_exclude_patterns
filtered_model_ids = _filter_models_by_patterns(
fetched_model_ids, include_patterns, exclude_patterns
)
# 记录过滤结果
if include_patterns or exclude_patterns:
filtered_count = len(fetched_model_ids) - len(filtered_model_ids)
if filtered_count > 0:
logger.info(
f"Key {key.id} 过滤规则生效: 原始 {len(fetched_model_ids)} 个模型, "
f"过滤后 {len(filtered_model_ids)} 个 (排除 {filtered_count} 个)"
)
# 新的 allowed_models = 过滤后的模型 + 锁定的模型
# 锁定模型无论上游是否返回都会保留
new_allowed_models = list(fetched_model_ids | locked_models)
new_allowed_models = list(filtered_model_ids | locked_models)
new_allowed_models.sort() # 保持顺序稳定
# 检查是否有变化

View File

@@ -30,6 +30,8 @@ class UsageRecordParams:
cache_read_input_tokens: int
request_type: str
api_format: Optional[str]
endpoint_api_format: Optional[str] # 端点原生 API 格式
has_format_conversion: bool # 是否发生了格式转换
is_stream: bool
response_time_ms: Optional[int]
first_byte_time_ms: Optional[int]
@@ -214,6 +216,8 @@ class UsageService:
cache_read_input_tokens: int,
request_type: str,
api_format: Optional[str],
endpoint_api_format: Optional[str],
has_format_conversion: bool,
is_stream: bool,
response_time_ms: Optional[int],
first_byte_time_ms: Optional[int],
@@ -349,6 +353,8 @@ class UsageService:
"price_per_request": request_price,
"request_type": request_type,
"api_format": api_format,
"endpoint_api_format": endpoint_api_format,
"has_format_conversion": has_format_conversion,
"is_stream": is_stream,
"status_code": status_code,
"error_message": error_message,
@@ -706,6 +712,8 @@ class UsageService:
cache_read_input_tokens=params.cache_read_input_tokens,
request_type=params.request_type,
api_format=params.api_format,
endpoint_api_format=params.endpoint_api_format,
has_format_conversion=params.has_format_conversion,
is_stream=params.is_stream,
response_time_ms=params.response_time_ms,
first_byte_time_ms=params.first_byte_time_ms,
@@ -756,6 +764,8 @@ class UsageService:
cache_read_input_tokens: int = 0,
request_type: str = "chat",
api_format: Optional[str] = None,
endpoint_api_format: Optional[str] = None,
has_format_conversion: bool = False,
is_stream: bool = False,
response_time_ms: Optional[int] = None,
first_byte_time_ms: Optional[int] = None,
@@ -794,7 +804,9 @@ class UsageService:
input_tokens=input_tokens, output_tokens=output_tokens,
cache_creation_input_tokens=cache_creation_input_tokens,
cache_read_input_tokens=cache_read_input_tokens,
request_type=request_type, api_format=api_format, is_stream=is_stream,
request_type=request_type, api_format=api_format,
endpoint_api_format=endpoint_api_format, has_format_conversion=has_format_conversion,
is_stream=is_stream,
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
status_code=status_code, error_message=error_message, metadata=metadata,
request_headers=request_headers, request_body=request_body,
@@ -849,6 +861,8 @@ class UsageService:
cache_read_input_tokens: int = 0,
request_type: str = "chat",
api_format: Optional[str] = None,
endpoint_api_format: Optional[str] = None,
has_format_conversion: bool = False,
is_stream: bool = False,
response_time_ms: Optional[int] = None,
first_byte_time_ms: Optional[int] = None,
@@ -889,7 +903,9 @@ class UsageService:
input_tokens=input_tokens, output_tokens=output_tokens,
cache_creation_input_tokens=cache_creation_input_tokens,
cache_read_input_tokens=cache_read_input_tokens,
request_type=request_type, api_format=api_format, is_stream=is_stream,
request_type=request_type, api_format=api_format,
endpoint_api_format=endpoint_api_format, has_format_conversion=has_format_conversion,
is_stream=is_stream,
response_time_ms=response_time_ms, first_byte_time_ms=first_byte_time_ms,
status_code=status_code, error_message=error_message, metadata=metadata,
request_headers=request_headers, request_body=request_body,
@@ -1486,6 +1502,8 @@ class UsageService:
provider_endpoint_id: Optional[str] = None,
provider_api_key_id: Optional[str] = None,
api_format: Optional[str] = None,
endpoint_api_format: Optional[str] = None,
has_format_conversion: Optional[bool] = None,
) -> Optional[Usage]:
"""
快速更新使用记录状态
@@ -1502,6 +1520,8 @@ class UsageService:
provider_endpoint_id: Endpoint ID可选streaming 状态时更新)
provider_api_key_id: Provider API Key ID可选streaming 状态时更新)
api_format: API 格式(可选,用于获取按格式配置的倍率)
endpoint_api_format: 端点原生 API 格式(可选)
has_format_conversion: 是否发生了格式转换(可选)
Returns:
更新后的 Usage 记录,如果未找到则返回 None
@@ -1540,6 +1560,10 @@ class UsageService:
)
if rate_multiplier is not None:
usage.rate_multiplier = rate_multiplier
if endpoint_api_format is not None:
usage.endpoint_api_format = endpoint_api_format
if has_format_conversion is not None:
usage.has_format_conversion = has_format_conversion
db.commit()

View File

@@ -41,6 +41,9 @@ class StreamUsageTracker:
provider_api_key_id: Optional[str] = None,
# API 格式(用于选择正确的响应解析器)
api_format: Optional[str] = None,
# 格式转换信息
endpoint_api_format: Optional[str] = None,
has_format_conversion: bool = False,
):
"""
初始化流式用量跟踪器
@@ -60,6 +63,8 @@ class StreamUsageTracker:
provider_endpoint_id: Endpoint ID用于记录真实成本
provider_api_key_id: API Key ID用于记录真实成本
api_format: API 格式CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI
endpoint_api_format: 端点原生 API 格式
has_format_conversion: 是否发生了格式转换
"""
self.db = db
# 只存储ID避免会话绑定问题
@@ -79,6 +84,8 @@ class StreamUsageTracker:
# API 格式和响应解析器
self.api_format = api_format or "CLAUDE"
self.endpoint_api_format = endpoint_api_format
self.has_format_conversion = has_format_conversion
self.response_parser = get_parser_for_format(self.api_format)
self.stream_stats = StreamStats() # 解析器统计信息
@@ -488,6 +495,8 @@ class StreamUsageTracker:
provider_endpoint_id=self.provider_endpoint_id,
provider_api_key_id=self.provider_api_key_id,
api_format=self.api_format,
endpoint_api_format=self.endpoint_api_format,
has_format_conversion=self.has_format_conversion,
)
except Exception as e:
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
@@ -710,6 +719,8 @@ class StreamUsageTracker:
cache_read_input_tokens=self.cache_read_input_tokens,
request_type="chat",
api_format=self.api_format,
endpoint_api_format=self.endpoint_api_format,
has_format_conversion=self.has_format_conversion,
is_stream=True,
response_time_ms=response_time_ms,
status_code=self.status_code, # 使用实际的状态码
@@ -801,6 +812,9 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
provider_api_key_id: Optional[str] = None,
# API 格式(用于选择正确的响应解析器)
api_format: Optional[str] = None,
# 格式转换信息
endpoint_api_format: Optional[str] = None,
has_format_conversion: bool = False,
):
super().__init__(
db,
@@ -817,6 +831,8 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
provider_endpoint_id,
provider_api_key_id,
api_format,
endpoint_api_format,
has_format_conversion,
)
# 用于更准确的token计算
self._init_tokenizer()
@@ -950,6 +966,8 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
provider_endpoint_id=self.provider_endpoint_id,
provider_api_key_id=self.provider_api_key_id,
api_format=self.api_format,
endpoint_api_format=self.endpoint_api_format,
has_format_conversion=self.has_format_conversion,
)
except Exception as e:
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
@@ -1054,6 +1072,9 @@ def create_stream_tracker(
provider_api_key_id: Optional[str] = None,
# API 格式(用于选择正确的响应解析器)
api_format: Optional[str] = None,
# 格式转换信息
endpoint_api_format: Optional[str] = None,
has_format_conversion: bool = False,
) -> StreamUsageTracker:
"""
创建流式用量跟踪器
@@ -1074,6 +1095,8 @@ def create_stream_tracker(
provider_endpoint_id: Endpoint ID用于记录真实成本
provider_api_key_id: API Key ID用于记录真实成本
api_format: API 格式CLAUDE, CLAUDE_CLI, OPENAI, OPENAI_CLI
endpoint_api_format: 端点原生 API 格式
has_format_conversion: 是否发生了格式转换
Returns:
流式用量跟踪器实例
@@ -1094,6 +1117,8 @@ def create_stream_tracker(
provider_endpoint_id,
provider_api_key_id,
api_format,
endpoint_api_format,
has_format_conversion,
)
else:
return StreamUsageTracker(
@@ -1111,4 +1136,6 @@ def create_stream_tracker(
provider_endpoint_id,
provider_api_key_id,
api_format,
endpoint_api_format,
has_format_conversion,
)