mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
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:
@@ -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() # 保持顺序稳定
|
||||
|
||||
# 检查是否有变化
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user