mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 性能监控基础设施、解密缓存及计费简化
- 新增 PerfRecorder 性能记录工具,支持采样率与慢请求日志 - 在请求管道中埋点:auth、body_read、json_parse、context_build、authorize、handle - 流处理器增加 parse/conversion 耗时追踪与 perf_metrics 落库 - 解密服务添加 LRU 缓存,降低高频解密 CPU 开销 - 格式转换分层开关设计:全局 OFF 时回退到端点配置,而非一刀切拒绝 - 移除 shadow billing 模块,统一使用新计费引擎 - 新增 Codex 网关请求适配器(store=false、role 映射、include 补齐) - endpoint 创建接口支持 body_rules 参数
This commit is contained in:
@@ -45,19 +45,38 @@ def test_cli_format_convertible_when_converter_supports_full() -> None:
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_global_switch_disabled_blocks_conversion() -> None:
|
||||
"""全局开关关闭时阻止转换"""
|
||||
def test_global_switch_disabled_falls_back_to_endpoint() -> None:
|
||||
"""全局开关关闭时回退到端点配置(分层开关设计)"""
|
||||
registry = MagicMock()
|
||||
registry.can_convert_full.return_value = True
|
||||
|
||||
# 全局 OFF + 端点 enabled -> 允许(端点覆盖全局默认)
|
||||
ok, needs_conv, reason = is_format_compatible(
|
||||
"claude:chat",
|
||||
"openai:chat",
|
||||
endpoint_format_acceptance_config={"enabled": True},
|
||||
is_stream=False,
|
||||
effective_conversion_enabled=False,
|
||||
registry=registry,
|
||||
)
|
||||
assert ok is True
|
||||
assert needs_conv is True
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_global_switch_disabled_blocks_when_endpoint_not_configured() -> None:
|
||||
"""全局开关关闭 + 端点未配置 -> 阻止转换"""
|
||||
ok, needs_conv, reason = is_format_compatible(
|
||||
"claude:chat",
|
||||
"openai:chat",
|
||||
endpoint_format_acceptance_config=None,
|
||||
is_stream=False,
|
||||
effective_conversion_enabled=False,
|
||||
registry=MagicMock(),
|
||||
)
|
||||
assert ok is False
|
||||
assert needs_conv is False
|
||||
assert reason and "格式转换已禁用" in reason
|
||||
assert reason and "未配置" in reason
|
||||
|
||||
|
||||
def test_endpoint_config_none_blocks_conversion() -> None:
|
||||
@@ -212,8 +231,8 @@ def test_gemini_cli_to_gemini_no_conversion_needed() -> None:
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_claude_cli_to_claude_blocked_when_global_switch_disabled() -> None:
|
||||
"""透传格式(CLAUDE_CLI -> CLAUDE)也受全局开关限制"""
|
||||
def test_claude_cli_to_claude_allowed_when_endpoint_enabled() -> None:
|
||||
"""透传格式(CLAUDE_CLI -> CLAUDE)全局 OFF 时回退到端点配置"""
|
||||
ok, needs_conv, reason = is_format_compatible(
|
||||
"claude:cli",
|
||||
"claude:chat",
|
||||
@@ -222,8 +241,10 @@ def test_claude_cli_to_claude_blocked_when_global_switch_disabled() -> None:
|
||||
effective_conversion_enabled=False,
|
||||
registry=MagicMock(),
|
||||
)
|
||||
assert ok is False
|
||||
assert reason and "格式转换已禁用" in reason
|
||||
# 全局 OFF + 端点 enabled -> 允许(同族透传无需转换)
|
||||
assert ok is True
|
||||
assert needs_conv is False
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_claude_cli_to_claude_blocked_when_endpoint_not_configured() -> None:
|
||||
@@ -312,8 +333,8 @@ def test_openai_cli_to_openai_fails_without_converter() -> None:
|
||||
assert reason and "转换器" in reason
|
||||
|
||||
|
||||
def test_openai_cli_to_openai_blocked_when_global_switch_disabled() -> None:
|
||||
"""同族转换(OPENAI/OPENAI_CLI)也受全局开关限制"""
|
||||
def test_openai_cli_to_openai_allowed_when_endpoint_enabled() -> None:
|
||||
"""同族转换(OPENAI/OPENAI_CLI)全局 OFF 时回退到端点配置"""
|
||||
registry = MagicMock()
|
||||
registry.can_convert_full.return_value = True
|
||||
|
||||
@@ -325,9 +346,10 @@ def test_openai_cli_to_openai_blocked_when_global_switch_disabled() -> None:
|
||||
effective_conversion_enabled=False, # 全局开关关闭
|
||||
registry=registry,
|
||||
)
|
||||
assert ok is False
|
||||
assert needs_conv is False
|
||||
assert reason and "格式转换已禁用" in reason
|
||||
# 全局 OFF + 端点 enabled -> 允许(需要转换)
|
||||
assert ok is True
|
||||
assert needs_conv is True
|
||||
assert reason is None
|
||||
|
||||
|
||||
def test_openai_cli_to_openai_blocked_when_endpoint_disabled() -> None:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.settings import config
|
||||
from src.services.billing.schema import BillingSnapshot, CostResult
|
||||
from src.services.billing.shadow import CostBreakdown, ShadowBillingService
|
||||
|
||||
|
||||
class TestShadowBillingServiceModeResolution:
|
||||
def test_get_engine_mode_exact_and_wildcard(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(config, "billing_engine", "legacy", raising=False)
|
||||
monkeypatch.setattr(
|
||||
config,
|
||||
"billing_engine_overrides",
|
||||
'{"anthropic/*": "shadow", "openai/gpt-4o": "new"}',
|
||||
raising=False,
|
||||
)
|
||||
|
||||
svc = ShadowBillingService(MagicMock())
|
||||
assert svc.get_engine_mode("openai", "gpt-4o") == "new"
|
||||
assert svc.get_engine_mode("anthropic", "claude-3-5-sonnet") == "shadow"
|
||||
assert svc.get_engine_mode("other", "x") == "legacy"
|
||||
|
||||
|
||||
class TestShadowBillingServiceExecution:
|
||||
def test_legacy_mode_skips_new_engine(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(config, "billing_engine", "legacy", raising=False)
|
||||
monkeypatch.setattr(config, "billing_engine_overrides", "{}", raising=False)
|
||||
|
||||
svc = ShadowBillingService(MagicMock())
|
||||
# Guard: if new engine calculate gets called, fail.
|
||||
svc._new_billing = MagicMock()
|
||||
svc._new_billing.calculate.side_effect = AssertionError(
|
||||
"new engine should not run in legacy mode"
|
||||
)
|
||||
|
||||
legacy_truth = CostBreakdown(
|
||||
input_cost=0.1,
|
||||
output_cost=0.2,
|
||||
cache_creation_cost=0.0,
|
||||
cache_read_cost=0.0,
|
||||
request_cost=0.0,
|
||||
total_cost=0.3,
|
||||
)
|
||||
|
||||
res = svc.calculate_with_shadow(
|
||||
provider="openai",
|
||||
provider_id="p-1",
|
||||
model="gpt-4o",
|
||||
task_type="chat",
|
||||
api_format="openai:chat",
|
||||
input_tokens=1,
|
||||
output_tokens=1,
|
||||
legacy_truth=legacy_truth,
|
||||
is_failed_request=False,
|
||||
)
|
||||
|
||||
assert res.engine_mode == "legacy"
|
||||
assert res.truth_engine == "legacy"
|
||||
assert res.shadow_snapshot is None
|
||||
assert res.truth_breakdown.total_cost == 0.3
|
||||
|
||||
def test_shadow_mode_returns_snapshot_and_keeps_legacy_truth(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(config, "billing_engine", "shadow", raising=False)
|
||||
monkeypatch.setattr(config, "billing_engine_overrides", "{}", raising=False)
|
||||
monkeypatch.setattr(config, "billing_diff_threshold_usd", 0.0001, raising=False)
|
||||
|
||||
svc = ShadowBillingService(MagicMock())
|
||||
|
||||
# Stub new engine output
|
||||
snapshot = BillingSnapshot(
|
||||
resolved_dimensions={"input_tokens": 1},
|
||||
resolved_variables={"input_price_per_1m": "3.0"},
|
||||
cost_breakdown={"input_cost": 0.003},
|
||||
total_cost=0.003,
|
||||
status="complete",
|
||||
calculated_at="2026-02-02T00:00:00Z",
|
||||
)
|
||||
svc._new_billing = MagicMock()
|
||||
svc._new_billing.calculate.return_value = CostResult(
|
||||
cost=0.003, status="complete", snapshot=snapshot
|
||||
)
|
||||
|
||||
legacy_truth = CostBreakdown(
|
||||
input_cost=0.004,
|
||||
output_cost=0.0,
|
||||
cache_creation_cost=0.0,
|
||||
cache_read_cost=0.0,
|
||||
request_cost=0.0,
|
||||
total_cost=0.004,
|
||||
)
|
||||
|
||||
res = svc.calculate_with_shadow(
|
||||
provider="openai",
|
||||
provider_id="p-1",
|
||||
model="gpt-4o",
|
||||
task_type="chat",
|
||||
api_format="openai:chat",
|
||||
input_tokens=1,
|
||||
output_tokens=0,
|
||||
legacy_truth=legacy_truth,
|
||||
is_failed_request=False,
|
||||
)
|
||||
|
||||
assert res.engine_mode == "shadow"
|
||||
assert res.truth_engine == "legacy"
|
||||
assert res.shadow_snapshot is not None
|
||||
assert res.truth_breakdown.total_cost == 0.004
|
||||
assert "diff_usd" in res.comparison
|
||||
@@ -67,7 +67,11 @@ async def test_build_candidates_allows_cross_format_when_endpoint_accepts_and_ov
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_candidates_blocks_cross_format_when_master_switch_off() -> None:
|
||||
async def test_build_candidates_allows_cross_format_when_global_off_but_endpoint_enabled() -> None:
|
||||
"""
|
||||
分层开关设计:全局 OFF 时回退到端点配置
|
||||
- 全局 OFF + 端点 enabled=True -> 允许(端点覆盖全局默认)
|
||||
"""
|
||||
register_default_normalizers()
|
||||
|
||||
scheduler = CacheAwareScheduler()
|
||||
@@ -85,6 +89,40 @@ async def test_build_candidates_blocks_cross_format_when_master_switch_off() ->
|
||||
]
|
||||
provider.api_keys = [_mock_key("k1", ["openai:chat"])]
|
||||
|
||||
candidates = await scheduler._build_candidates(
|
||||
db=MagicMock(),
|
||||
providers=[provider],
|
||||
client_format="claude:chat",
|
||||
model_name="dummy-model",
|
||||
affinity_key=None,
|
||||
global_conversion_enabled=False, # 全局开关关闭,但端点配置允许
|
||||
)
|
||||
|
||||
# 新设计:全局 OFF 时回退到端点配置,端点 enabled=True 则允许
|
||||
assert len(candidates) == 1
|
||||
assert candidates[0].needs_conversion is True
|
||||
assert candidates[0].provider_api_format == "openai:chat"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_candidates_blocks_cross_format_when_global_off_and_endpoint_not_configured() -> (
|
||||
None
|
||||
):
|
||||
"""
|
||||
分层开关设计:全局 OFF + 端点未配置 -> 阻止
|
||||
"""
|
||||
register_default_normalizers()
|
||||
|
||||
scheduler = CacheAwareScheduler()
|
||||
scheduler._check_model_support = AsyncMock(return_value=(True, None, None, {"m"})) # type: ignore[method-assign]
|
||||
scheduler._check_key_availability = MagicMock(return_value=(True, None, None)) # type: ignore[method-assign]
|
||||
|
||||
provider = MagicMock()
|
||||
provider.name = "p1"
|
||||
provider.enable_format_conversion = False
|
||||
provider.endpoints = [_mock_endpoint("openai:chat", None)] # 端点未配置格式接受策略
|
||||
provider.api_keys = [_mock_key("k1", ["openai:chat"])]
|
||||
|
||||
candidates = await scheduler._build_candidates(
|
||||
db=MagicMock(),
|
||||
providers=[provider],
|
||||
@@ -94,6 +132,7 @@ async def test_build_candidates_blocks_cross_format_when_master_switch_off() ->
|
||||
global_conversion_enabled=False, # 全局开关关闭
|
||||
)
|
||||
|
||||
# 全局 OFF + 端点未配置 -> 阻止
|
||||
assert candidates == []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user