mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 将格式转换全局开关迁移到环境变量配置
- 新增 FORMAT_CONVERSION_ENABLED 环境变量(默认开启) - 移除数据库中的 format_conversion_enabled 系统配置 - 移除前端系统设置页面的格式转换开关 UI - 统一同族格式转换(OPENAI/OPENAI_CLI)逻辑,也需检查全局开关和端点配置 - 流生成器使用 ctx.needs_conversion 替代 _needs_format_conversion 方法调用 - 更新测试用例适配新逻辑 - 清理 .env.example 中过时的超时和连接池配置注释
This commit is contained in:
48
.env.example
48
.env.example
@@ -43,51 +43,3 @@ ADMIN_PASSWORD=admin123456
|
|||||||
# 默认: * (允许所有源)
|
# 默认: * (允许所有源)
|
||||||
# CORS_ORIGINS=*
|
# CORS_ORIGINS=*
|
||||||
|
|
||||||
# ==================== 超时配置 ====================
|
|
||||||
# 以下配置控制各种超时行为,影响故障转移和请求处理
|
|
||||||
|
|
||||||
# --- HTTP 连接层超时(httpx 底层) ---
|
|
||||||
# 这些是网络层的超时,控制 TCP 连接和数据传输
|
|
||||||
|
|
||||||
# TCP 连接建立超时(默认 10 秒)
|
|
||||||
# 无法在此时间内建立 TCP 连接则触发故障转移
|
|
||||||
# HTTP_CONNECT_TIMEOUT=10.0
|
|
||||||
|
|
||||||
# 读取数据超时(默认 60 秒)
|
|
||||||
# 两次数据包之间的最大等待时间,超时触发故障转移
|
|
||||||
# 注意:如果上游持续发送数据(哪怕很慢),此计时器会不断重置
|
|
||||||
# HTTP_READ_TIMEOUT=60.0
|
|
||||||
|
|
||||||
# 流式响应首字节超时(默认 30 秒,范围 10-120 秒)
|
|
||||||
# 从发起请求到收到第一个字节的最大等待时间
|
|
||||||
# 仅对流式请求生效,超时触发故障转移
|
|
||||||
# STREAM_FIRST_BYTE_TIMEOUT=30.0
|
|
||||||
|
|
||||||
# ==================== 数据库连接池配置 ====================
|
|
||||||
# 连接池大小直接影响并发能力,特别是流式请求场景
|
|
||||||
# 每个流式请求会占用一个连接直到响应完成
|
|
||||||
|
|
||||||
# PostgreSQL 最大连接数(默认 100,需与 postgresql.conf 中 max_connections 匹配)
|
|
||||||
# PG_MAX_CONNECTIONS=100
|
|
||||||
|
|
||||||
# 预留给管理工具的连接数(默认 10)
|
|
||||||
# PG_RESERVED_CONNECTIONS=10
|
|
||||||
|
|
||||||
# 连接池大小(默认自动计算:(PG_MAX - RESERVED) / WORKERS / 2)
|
|
||||||
# 高并发场景建议手动设置较大值
|
|
||||||
# DB_POOL_SIZE=20
|
|
||||||
|
|
||||||
# 最大溢出连接数(默认等于 DB_POOL_SIZE)
|
|
||||||
# 高峰期可临时创建的额外连接
|
|
||||||
# DB_MAX_OVERFLOW=20
|
|
||||||
|
|
||||||
# 连接获取超时(默认 60 秒)
|
|
||||||
# 超时后抛出 TimeoutError
|
|
||||||
# DB_POOL_TIMEOUT=60
|
|
||||||
|
|
||||||
# 连接池使用率警告阈值(默认 70%)
|
|
||||||
# DB_POOL_WARN_THRESHOLD=70
|
|
||||||
|
|
||||||
# 批量余额查询并发限制(默认自动计算:连接池的 40%)
|
|
||||||
# 控制 Provider 余额查询的并发数,避免耗尽连接池
|
|
||||||
# BATCH_BALANCE_CONCURRENCY=8
|
|
||||||
|
|||||||
@@ -188,25 +188,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center h-full">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="format-conversion-enabled"
|
|
||||||
v-model:checked="systemConfig.format_conversion_enabled"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Label
|
|
||||||
for="format-conversion-enabled"
|
|
||||||
class="cursor-pointer"
|
|
||||||
>
|
|
||||||
启用格式自动转换
|
|
||||||
</Label>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
|
||||||
允许网关在 OpenAI/Claude/Gemini 格式间自动转换
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
|
|
||||||
@@ -854,8 +835,6 @@ interface SystemConfig {
|
|||||||
enable_registration: boolean
|
enable_registration: boolean
|
||||||
// 独立余额 Key 过期管理
|
// 独立余额 Key 过期管理
|
||||||
auto_delete_expired_keys: boolean
|
auto_delete_expired_keys: boolean
|
||||||
// 格式转换
|
|
||||||
format_conversion_enabled: boolean
|
|
||||||
// 日志记录
|
// 日志记录
|
||||||
request_log_level: string
|
request_log_level: string
|
||||||
max_request_body_size: number
|
max_request_body_size: number
|
||||||
@@ -908,8 +887,6 @@ const systemConfig = ref<SystemConfig>({
|
|||||||
enable_registration: false,
|
enable_registration: false,
|
||||||
// 独立余额 Key 过期管理
|
// 独立余额 Key 过期管理
|
||||||
auto_delete_expired_keys: false,
|
auto_delete_expired_keys: false,
|
||||||
// 格式转换
|
|
||||||
format_conversion_enabled: false,
|
|
||||||
// 日志记录
|
// 日志记录
|
||||||
request_log_level: 'basic',
|
request_log_level: 'basic',
|
||||||
max_request_body_size: 1048576,
|
max_request_body_size: 1048576,
|
||||||
@@ -935,8 +912,7 @@ const hasBasicConfigChanges = computed(() => {
|
|||||||
systemConfig.value.default_user_quota_usd !== originalConfig.value.default_user_quota_usd ||
|
systemConfig.value.default_user_quota_usd !== originalConfig.value.default_user_quota_usd ||
|
||||||
systemConfig.value.rate_limit_per_minute !== originalConfig.value.rate_limit_per_minute ||
|
systemConfig.value.rate_limit_per_minute !== originalConfig.value.rate_limit_per_minute ||
|
||||||
systemConfig.value.enable_registration !== originalConfig.value.enable_registration ||
|
systemConfig.value.enable_registration !== originalConfig.value.enable_registration ||
|
||||||
systemConfig.value.auto_delete_expired_keys !== originalConfig.value.auto_delete_expired_keys ||
|
systemConfig.value.auto_delete_expired_keys !== originalConfig.value.auto_delete_expired_keys
|
||||||
systemConfig.value.format_conversion_enabled !== originalConfig.value.format_conversion_enabled
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1013,8 +989,6 @@ async function loadSystemConfig() {
|
|||||||
'enable_registration',
|
'enable_registration',
|
||||||
// 独立余额 Key 过期管理
|
// 独立余额 Key 过期管理
|
||||||
'auto_delete_expired_keys',
|
'auto_delete_expired_keys',
|
||||||
// 格式转换
|
|
||||||
'format_conversion_enabled',
|
|
||||||
// 日志记录
|
// 日志记录
|
||||||
'request_log_level',
|
'request_log_level',
|
||||||
'max_request_body_size',
|
'max_request_body_size',
|
||||||
@@ -1072,11 +1046,6 @@ async function saveBasicConfig() {
|
|||||||
value: systemConfig.value.auto_delete_expired_keys,
|
value: systemConfig.value.auto_delete_expired_keys,
|
||||||
description: '是否自动删除过期的API Key'
|
description: '是否自动删除过期的API Key'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'format_conversion_enabled',
|
|
||||||
value: systemConfig.value.format_conversion_enabled,
|
|
||||||
description: '是否启用全局格式自动转换'
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -1090,7 +1059,6 @@ async function saveBasicConfig() {
|
|||||||
originalConfig.value.rate_limit_per_minute = systemConfig.value.rate_limit_per_minute
|
originalConfig.value.rate_limit_per_minute = systemConfig.value.rate_limit_per_minute
|
||||||
originalConfig.value.enable_registration = systemConfig.value.enable_registration
|
originalConfig.value.enable_registration = systemConfig.value.enable_registration
|
||||||
originalConfig.value.auto_delete_expired_keys = systemConfig.value.auto_delete_expired_keys
|
originalConfig.value.auto_delete_expired_keys = systemConfig.value.auto_delete_expired_keys
|
||||||
originalConfig.value.format_conversion_enabled = systemConfig.value.format_conversion_enabled
|
|
||||||
}
|
}
|
||||||
success('基础配置已保存')
|
success('基础配置已保存')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -862,8 +862,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
|
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
|
||||||
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
||||||
|
|
||||||
# 检查是否需要格式转换
|
# 使用已设置的 ctx.needs_conversion(由候选筛选阶段根据端点配置判断)
|
||||||
needs_conversion = self._needs_format_conversion(ctx)
|
# 不再调用 _needs_format_conversion,它只检查格式差异,不检查端点配置
|
||||||
|
needs_conversion = ctx.needs_conversion
|
||||||
|
|
||||||
async for chunk in stream_response.aiter_bytes():
|
async for chunk in stream_response.aiter_bytes():
|
||||||
buffer += chunk
|
buffer += chunk
|
||||||
@@ -1224,8 +1225,9 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
|
# 使用增量解码器处理跨 chunk 的 UTF-8 字符
|
||||||
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
||||||
|
|
||||||
# 检查是否需要格式转换
|
# 使用已设置的 ctx.needs_conversion(由候选筛选阶段根据端点配置判断)
|
||||||
needs_conversion = self._needs_format_conversion(ctx)
|
# 不再调用 _needs_format_conversion,它只检查格式差异,不检查端点配置
|
||||||
|
needs_conversion = ctx.needs_conversion
|
||||||
|
|
||||||
# 先处理预读的字节块
|
# 先处理预读的字节块
|
||||||
for chunk in prefetched_chunks:
|
for chunk in prefetched_chunks:
|
||||||
@@ -2394,7 +2396,13 @@ class CliMessageHandlerBase(BaseMessageHandler):
|
|||||||
|
|
||||||
def _needs_format_conversion(self, ctx: StreamContext) -> bool:
|
def _needs_format_conversion(self, ctx: StreamContext) -> bool:
|
||||||
"""
|
"""
|
||||||
检查是否需要进行格式转换
|
[已废弃] 仅根据格式差异判断是否需要转换
|
||||||
|
|
||||||
|
警告:此方法只检查格式是否不同,不检查端点的 format_acceptance_config 配置!
|
||||||
|
正确的判断应使用候选筛选阶段的结果(ctx.needs_conversion),该结果由
|
||||||
|
is_format_compatible() 函数根据全局开关和端点配置计算得出。
|
||||||
|
|
||||||
|
此方法保留仅供调试和日志输出使用,流生成器中不应调用此方法。
|
||||||
|
|
||||||
当 Provider 的 API 格式与客户端请求的 API 格式不同时,需要转换响应。
|
当 Provider 的 API 格式与客户端请求的 API 格式不同时,需要转换响应。
|
||||||
例如:客户端请求 Claude 格式,但 Provider 返回 OpenAI 格式。
|
例如:客户端请求 Claude 格式,但 Provider 返回 OpenAI 格式。
|
||||||
|
|||||||
@@ -99,9 +99,10 @@ def _get_formats_for_api(api_format: str) -> list[str]:
|
|||||||
return _OPENAI_FORMATS
|
return _OPENAI_FORMATS
|
||||||
|
|
||||||
|
|
||||||
def _is_format_conversion_enabled(db: Session) -> bool:
|
def _is_format_conversion_enabled() -> bool:
|
||||||
"""检查全局格式转换开关"""
|
"""检查全局格式转换开关(从环境变量读取,默认开启)"""
|
||||||
return bool(SystemConfigService.get_config(db, "format_conversion_enabled", False))
|
from src.config.settings import config
|
||||||
|
return config.format_conversion_enabled
|
||||||
|
|
||||||
|
|
||||||
def _get_convertible_formats(client_format: str, global_conversion_enabled: bool) -> list[str]:
|
def _get_convertible_formats(client_format: str, global_conversion_enabled: bool) -> list[str]:
|
||||||
@@ -511,7 +512,7 @@ async def list_models(
|
|||||||
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
||||||
|
|
||||||
# 获取可用格式(包括可转换的格式)
|
# 获取可用格式(包括可转换的格式)
|
||||||
global_conversion_enabled = _is_format_conversion_enabled(db)
|
global_conversion_enabled = _is_format_conversion_enabled()
|
||||||
candidate_formats = _get_convertible_formats(api_format, global_conversion_enabled)
|
candidate_formats = _get_convertible_formats(api_format, global_conversion_enabled)
|
||||||
candidate_formats, empty_response = _filter_formats_by_restrictions(
|
candidate_formats, empty_response = _filter_formats_by_restrictions(
|
||||||
candidate_formats, restrictions, api_format
|
candidate_formats, restrictions, api_format
|
||||||
@@ -615,7 +616,7 @@ async def retrieve_model(
|
|||||||
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
||||||
|
|
||||||
# 获取可用格式(包括可转换的格式)
|
# 获取可用格式(包括可转换的格式)
|
||||||
global_conversion_enabled = _is_format_conversion_enabled(db)
|
global_conversion_enabled = _is_format_conversion_enabled()
|
||||||
candidate_formats = _get_convertible_formats(api_format, global_conversion_enabled)
|
candidate_formats = _get_convertible_formats(api_format, global_conversion_enabled)
|
||||||
candidate_formats, _ = _filter_formats_by_restrictions(
|
candidate_formats, _ = _filter_formats_by_restrictions(
|
||||||
candidate_formats, restrictions, api_format
|
candidate_formats, restrictions, api_format
|
||||||
@@ -700,7 +701,7 @@ async def list_models_gemini(
|
|||||||
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
||||||
|
|
||||||
# 获取可用格式(包括可转换的格式)
|
# 获取可用格式(包括可转换的格式)
|
||||||
global_conversion_enabled = _is_format_conversion_enabled(db)
|
global_conversion_enabled = _is_format_conversion_enabled()
|
||||||
candidate_formats = _get_convertible_formats("gemini", global_conversion_enabled)
|
candidate_formats = _get_convertible_formats("gemini", global_conversion_enabled)
|
||||||
candidate_formats, empty_response = _filter_formats_by_restrictions(
|
candidate_formats, empty_response = _filter_formats_by_restrictions(
|
||||||
candidate_formats, restrictions, "gemini"
|
candidate_formats, restrictions, "gemini"
|
||||||
@@ -781,7 +782,7 @@ async def get_model_gemini(
|
|||||||
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
restrictions = AccessRestrictions.from_api_key_and_user(key_record, user)
|
||||||
|
|
||||||
# 获取可用格式(包括可转换的格式)
|
# 获取可用格式(包括可转换的格式)
|
||||||
global_conversion_enabled = _is_format_conversion_enabled(db)
|
global_conversion_enabled = _is_format_conversion_enabled()
|
||||||
candidate_formats = _get_convertible_formats("gemini", global_conversion_enabled)
|
candidate_formats = _get_convertible_formats("gemini", global_conversion_enabled)
|
||||||
candidate_formats, _ = _filter_formats_by_restrictions(
|
candidate_formats, _ = _filter_formats_by_restrictions(
|
||||||
candidate_formats, restrictions, "gemini"
|
candidate_formats, restrictions, "gemini"
|
||||||
|
|||||||
@@ -147,6 +147,13 @@ class Config:
|
|||||||
# HTTP_REQUEST_TIMEOUT: 非流式请求整体超时(秒),默认 300 秒
|
# HTTP_REQUEST_TIMEOUT: 非流式请求整体超时(秒),默认 300 秒
|
||||||
self.http_request_timeout = float(os.getenv("HTTP_REQUEST_TIMEOUT", "300.0"))
|
self.http_request_timeout = float(os.getenv("HTTP_REQUEST_TIMEOUT", "300.0"))
|
||||||
|
|
||||||
|
# 格式转换配置
|
||||||
|
# FORMAT_CONVERSION_ENABLED: 全局格式转换总开关,默认开启
|
||||||
|
# 注意:即使开启,也需要端点配置 format_acceptance_config.enabled=true 才能生效
|
||||||
|
self.format_conversion_enabled = os.getenv(
|
||||||
|
"FORMAT_CONVERSION_ENABLED", "true"
|
||||||
|
).lower() == "true"
|
||||||
|
|
||||||
# HTTP 连接池配置
|
# HTTP 连接池配置
|
||||||
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
|
# HTTP_MAX_CONNECTIONS: 最大连接数,影响并发能力
|
||||||
# - 每个连接占用一个 socket,过多会耗尽系统资源
|
# - 每个连接占用一个 socket,过多会耗尽系统资源
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
格式兼容性检查
|
格式兼容性检查
|
||||||
|
|
||||||
用于候选筛选时判断端点是否可以处理客户端请式。
|
用于候选筛选时判断端点是否可以处理客户端请求格式。
|
||||||
|
|
||||||
|
转换逻辑:
|
||||||
|
1. 格式完全匹配 -> 透传(无需转换)
|
||||||
|
2. 同族格式透传(CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 格式相同,只是认证不同)
|
||||||
|
3. 需要转换的情况 -> 检查全局开关 + 端点配置 + 转换器能力
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -33,7 +38,7 @@ def is_format_compatible(
|
|||||||
endpoint_api_format: 端点的 API 格式
|
endpoint_api_format: 端点的 API 格式
|
||||||
endpoint_format_acceptance_config: 端点的格式接受配置
|
endpoint_format_acceptance_config: 端点的格式接受配置
|
||||||
is_stream: 是否是流式请求
|
is_stream: 是否是流式请求
|
||||||
global_conversion_enabled: 全局格式转换开关
|
global_conversion_enabled: 全局格式转换开关(来自环境变量 FORMAT_CONVERSION_ENABLED,默认 True)
|
||||||
registry: 转换器注册表(可选,默认使用全局单例)
|
registry: 转换器注册表(可选,默认使用全局单例)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -55,7 +60,7 @@ def is_format_compatible(
|
|||||||
provider_format = endpoint_api_format.upper()
|
provider_format = endpoint_api_format.upper()
|
||||||
client_format_upper = client_format.upper()
|
client_format_upper = client_format.upper()
|
||||||
|
|
||||||
# 1. 格式完全匹配 -> 兼容,无需转换
|
# 1. 格式完全匹配 -> 透传(无需转换)
|
||||||
if provider_format == client_format_upper:
|
if provider_format == client_format_upper:
|
||||||
return True, False, None
|
return True, False, None
|
||||||
|
|
||||||
@@ -63,32 +68,18 @@ def is_format_compatible(
|
|||||||
provider_base = get_base_format(provider_format)
|
provider_base = get_base_format(provider_format)
|
||||||
client_base = get_base_format(client_format_upper)
|
client_base = get_base_format(client_format_upper)
|
||||||
|
|
||||||
if provider_base == client_base:
|
is_same_family = provider_base == client_base
|
||||||
# 同族格式:检查是否需要转换
|
if is_same_family and provider_base != "OPENAI":
|
||||||
# - OPENAI 和 OPENAI_CLI 的请求/响应格式不同(Chat Completions vs Responses API),需要转换
|
# CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 等:格式相同,只是认证不同,可透传
|
||||||
# - CLAUDE 和 CLAUDE_CLI、GEMINI 和 GEMINI_CLI 格式相同,只是认证不同,可透传
|
# 注意:这些格式之间没有定义转换器,因为数据格式完全相同
|
||||||
if provider_base == "OPENAI":
|
return True, False, None
|
||||||
# OPENAI/OPENAI_CLI 同族转换:兼容但需要转换
|
|
||||||
# 检查转换器能力(同族转换不需要全局开关和端点配置)
|
|
||||||
if registry.can_convert_full(
|
|
||||||
client_format_upper, provider_format, require_stream=is_stream
|
|
||||||
):
|
|
||||||
return True, True, None # 兼容,需要转换
|
|
||||||
else:
|
|
||||||
return (
|
|
||||||
False,
|
|
||||||
False,
|
|
||||||
f"不存在 {client_format} <-> {provider_format} 的完整转换器",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# CLAUDE/CLAUDE_CLI、GEMINI/GEMINI_CLI 等:格式相同,可透传
|
|
||||||
return True, False, None
|
|
||||||
|
|
||||||
# 3. 检查全局开关
|
# 3. 需要转换的情况(OPENAI/OPENAI_CLI 同族转换 或 跨格式转换)
|
||||||
|
# 检查全局开关(来自环境变量,默认开启)
|
||||||
if not global_conversion_enabled:
|
if not global_conversion_enabled:
|
||||||
return False, False, "全局格式转换未启用"
|
return False, False, "全局格式转换未启用(环境变量 FORMAT_CONVERSION_ENABLED=false)"
|
||||||
|
|
||||||
# 4. 检查端点配置
|
# 4. 检查端点配置(核心控制)
|
||||||
if endpoint_format_acceptance_config is None:
|
if endpoint_format_acceptance_config is None:
|
||||||
return False, False, "端点未配置格式转换"
|
return False, False, "端点未配置格式转换"
|
||||||
|
|
||||||
|
|||||||
5
src/services/cache/aware_scheduler.py
vendored
5
src/services/cache/aware_scheduler.py
vendored
@@ -659,9 +659,8 @@ class CacheAwareScheduler:
|
|||||||
return [], global_model_id
|
return [], global_model_id
|
||||||
|
|
||||||
# 2. 构建候选列表(传入 is_stream 和 capability_requirements 用于过滤)
|
# 2. 构建候选列表(传入 is_stream 和 capability_requirements 用于过滤)
|
||||||
global_conversion_enabled = bool(
|
from src.config.settings import config
|
||||||
SystemConfigService.get_config(db, "format_conversion_enabled", False)
|
global_conversion_enabled = config.format_conversion_enabled
|
||||||
)
|
|
||||||
candidates = await self._build_candidates(
|
candidates = await self._build_candidates(
|
||||||
db=db,
|
db=db,
|
||||||
providers=providers,
|
providers=providers,
|
||||||
|
|||||||
@@ -118,10 +118,6 @@ class SystemConfigService:
|
|||||||
"value": "cache_affinity",
|
"value": "cache_affinity",
|
||||||
"description": "调度模式:fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式,优先使用已缓存的Provider)",
|
"description": "调度模式:fixed_order(固定顺序模式,严格按优先级顺序) 或 cache_affinity(缓存亲和模式,优先使用已缓存的Provider)",
|
||||||
},
|
},
|
||||||
"format_conversion_enabled": {
|
|
||||||
"value": False,
|
|
||||||
"description": "是否启用全局格式自动转换(需要端点配置 format_acceptance_config 才能生效)",
|
|
||||||
},
|
|
||||||
"auto_delete_expired_keys": {
|
"auto_delete_expired_keys": {
|
||||||
"value": False,
|
"value": False,
|
||||||
"description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",
|
"description": "是否自动删除过期的API Key(True=物理删除,False=仅禁用),仅管理员可配置",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ def test_cli_format_convertible_when_converter_supports_full() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_global_switch_disabled_blocks_conversion() -> None:
|
def test_global_switch_disabled_blocks_conversion() -> None:
|
||||||
|
"""全局开关关闭时(环境变量 FORMAT_CONVERSION_ENABLED=false)阻止转换"""
|
||||||
ok, needs_conv, reason = is_format_compatible(
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
"CLAUDE",
|
"CLAUDE",
|
||||||
"OPENAI",
|
"OPENAI",
|
||||||
@@ -56,7 +57,7 @@ def test_global_switch_disabled_blocks_conversion() -> None:
|
|||||||
)
|
)
|
||||||
assert ok is False
|
assert ok is False
|
||||||
assert needs_conv is False
|
assert needs_conv is False
|
||||||
assert reason and "全局" in reason
|
assert reason and ("全局" in reason or "FORMAT_CONVERSION_ENABLED" in reason)
|
||||||
|
|
||||||
|
|
||||||
def test_endpoint_config_none_blocks_conversion() -> None:
|
def test_endpoint_config_none_blocks_conversion() -> None:
|
||||||
@@ -219,9 +220,9 @@ def test_openai_cli_to_openai_needs_conversion() -> None:
|
|||||||
ok, needs_conv, reason = is_format_compatible(
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
"OPENAI_CLI",
|
"OPENAI_CLI",
|
||||||
"OPENAI",
|
"OPENAI",
|
||||||
endpoint_format_acceptance_config=None, # 同族转换不需要端点配置
|
endpoint_format_acceptance_config={"enabled": True}, # 同族转换也需要端点配置
|
||||||
is_stream=False,
|
is_stream=False,
|
||||||
global_conversion_enabled=False, # 同族转换不需要全局开关
|
global_conversion_enabled=True, # 同族转换也需要全局开关
|
||||||
registry=registry,
|
registry=registry,
|
||||||
)
|
)
|
||||||
assert ok is True
|
assert ok is True
|
||||||
@@ -237,9 +238,9 @@ def test_openai_to_openai_cli_needs_conversion() -> None:
|
|||||||
ok, needs_conv, reason = is_format_compatible(
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
"OPENAI",
|
"OPENAI",
|
||||||
"OPENAI_CLI",
|
"OPENAI_CLI",
|
||||||
endpoint_format_acceptance_config=None, # 同族转换不需要端点配置
|
endpoint_format_acceptance_config={"enabled": True}, # 同族转换也需要端点配置
|
||||||
is_stream=False,
|
is_stream=False,
|
||||||
global_conversion_enabled=False, # 同族转换不需要全局开关
|
global_conversion_enabled=True, # 同族转换也需要全局开关
|
||||||
registry=registry,
|
registry=registry,
|
||||||
)
|
)
|
||||||
assert ok is True
|
assert ok is True
|
||||||
@@ -255,9 +256,9 @@ def test_openai_cli_to_openai_stream_needs_conversion() -> None:
|
|||||||
ok, needs_conv, reason = is_format_compatible(
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
"OPENAI_CLI",
|
"OPENAI_CLI",
|
||||||
"OPENAI",
|
"OPENAI",
|
||||||
endpoint_format_acceptance_config=None,
|
endpoint_format_acceptance_config={"enabled": True}, # 同族转换也需要端点配置
|
||||||
is_stream=True,
|
is_stream=True,
|
||||||
global_conversion_enabled=False,
|
global_conversion_enabled=True, # 同族转换也需要全局开关
|
||||||
registry=registry,
|
registry=registry,
|
||||||
)
|
)
|
||||||
assert ok is True
|
assert ok is True
|
||||||
@@ -273,11 +274,65 @@ def test_openai_cli_to_openai_fails_without_converter() -> None:
|
|||||||
ok, needs_conv, reason = is_format_compatible(
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
"OPENAI_CLI",
|
"OPENAI_CLI",
|
||||||
"OPENAI",
|
"OPENAI",
|
||||||
endpoint_format_acceptance_config=None,
|
endpoint_format_acceptance_config={"enabled": True},
|
||||||
is_stream=False,
|
is_stream=False,
|
||||||
global_conversion_enabled=False,
|
global_conversion_enabled=True,
|
||||||
registry=registry,
|
registry=registry,
|
||||||
)
|
)
|
||||||
assert ok is False
|
assert ok is False
|
||||||
assert needs_conv is False
|
assert needs_conv is False
|
||||||
assert reason and "转换器" in reason
|
assert reason and "转换器" in reason
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_blocked_when_global_switch_disabled() -> None:
|
||||||
|
"""同族转换(OPENAI/OPENAI_CLI)也受全局开关限制(环境变量 FORMAT_CONVERSION_ENABLED=false)"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config={"enabled": True},
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=False, # 全局开关关闭
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is False
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason and ("全局" in reason or "FORMAT_CONVERSION_ENABLED" in reason)
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_blocked_when_endpoint_disabled() -> None:
|
||||||
|
"""同族转换(OPENAI/OPENAI_CLI)也受端点开关限制"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config={"enabled": False}, # 端点开关关闭
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=True,
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is False
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason and "未启用" in reason
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_cli_to_openai_blocked_when_endpoint_not_configured() -> None:
|
||||||
|
"""同族转换(OPENAI/OPENAI_CLI)也需要端点配置"""
|
||||||
|
registry = MagicMock()
|
||||||
|
registry.can_convert_full.return_value = True
|
||||||
|
|
||||||
|
ok, needs_conv, reason = is_format_compatible(
|
||||||
|
"OPENAI_CLI",
|
||||||
|
"OPENAI",
|
||||||
|
endpoint_format_acceptance_config=None, # 无端点配置
|
||||||
|
is_stream=False,
|
||||||
|
global_conversion_enabled=True,
|
||||||
|
registry=registry,
|
||||||
|
)
|
||||||
|
assert ok is False
|
||||||
|
assert needs_conv is False
|
||||||
|
assert reason and "未配置" in reason
|
||||||
|
|||||||
Reference in New Issue
Block a user