feat(admin,pool,billing): 端点级模型测试、Provider 自动置顶、缓存 TTL 分级计费展示与账号状态增强

- 模型测试支持指定端点:新增 ModelTestDialog 组件,多端点时弹窗选择,单端点直接测试;
  后端 test-model-failover 接口新增 endpoint_id 参数,支持 global/direct 模式下按端点过滤候选
- 创建 Provider 时优先级自动置顶(provider_priority 默认 None,后端取 min-1),
  显式指定优先级时 shift 已有行;前端创建时不发送 priority,更新时保留
- 缓存计费 UI 增强:RequestDetailDrawer 支持 5min/1h 缓存创建 token 分级展示,
  含按 TTL 匹配单价和分行成本计算;ModelDetailDrawer/ModelsTab 标签区分 5min/1h 缓存创建
- Pool 批量操作额度筛选拆分为「无5H限额」和「无周限额」,按 | 分隔 segment 匹配
- KeyFormDialog 优化非 vertex_ai 时布局,API 密钥输入内联到 grid 右列
- Codex refresher 结构化错误标记:401/402/403 使用 [OAUTH_EXPIRED]/[ACCOUNT_BLOCK] 前缀,
  新增 deactivated_workspace 识别与分类
- 前后端 accountBlock 关键词同步:新增 token invalidated、deactivated_workspace 识别,
  OAuth 失效提示清理 block 前缀后展示
- PoolConfig 新增 batch_concurrency 配置(默认 8,上限 32)
- 预设模型新增 gpt-5.4;TestResultDialog 响应式布局与 key 脱敏优化
This commit is contained in:
fawney19
2026-03-06 13:13:01 +08:00
parent d17472f09e
commit d97ec3fde2
27 changed files with 1109 additions and 105 deletions

View File

@@ -4,6 +4,7 @@ import pytest
from src.api.admin.providers.routes import (
_merge_claude_code_advanced_config,
_resolve_new_provider_priority,
_should_enable_format_conversion_by_default,
)
from src.core.exceptions import InvalidRequestException
@@ -37,3 +38,27 @@ def test_merge_claude_code_advanced_rejects_non_claude_payload() -> None:
claude_code_advanced={"max_sessions": 9},
claude_advanced_in_payload=True,
)
def test_new_provider_priority_defaults_to_current_top() -> None:
priority, needs_shift = _resolve_new_provider_priority(
current_min_priority=3, requested_priority=None
)
assert priority == 2
assert needs_shift is False
def test_new_provider_priority_defaults_to_100_when_empty() -> None:
priority, needs_shift = _resolve_new_provider_priority(
current_min_priority=None, requested_priority=None
)
assert priority == 100
assert needs_shift is False
def test_new_provider_priority_keeps_explicit_value() -> None:
priority, needs_shift = _resolve_new_provider_priority(
current_min_priority=3, requested_priority=8
)
assert priority == 8
assert needs_shift is True

View File

@@ -115,3 +115,25 @@ def test_antigravity_oauth_reason_text_detected_as_disabled() -> None:
assert state.blocked is True
assert state.code == "account_disabled"
assert state.label == "账号停用"
def test_resolve_from_structured_oauth_reason_workspace_deactivated() -> None:
state = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)",
)
assert state.blocked is True
assert state.code == "workspace_deactivated"
assert state.label == "工作区停用"
def test_resolve_from_structured_oauth_reason_token_invalidated() -> None:
state = resolve_pool_account_state(
provider_type="codex",
upstream_metadata=None,
oauth_invalid_reason="[OAUTH_EXPIRED] Your authentication token has been invalidated. Please try signing in again.",
)
assert state.blocked is True
assert state.code == "oauth_expired"
assert state.label == "Token 失效"

View File

@@ -39,6 +39,7 @@ def test_parse_pool_config_returns_defaults_for_empty_advanced() -> None:
assert cfg.proactive_refresh_seconds == 180
assert cfg.health_policy_enabled is True
assert cfg.unschedulable_rules == []
assert cfg.batch_concurrency == 8
assert cfg.probing_enabled is False
assert cfg.probing_interval_minutes == 10
assert cfg.auto_remove_banned_keys is False
@@ -100,11 +101,24 @@ def test_parse_pool_config_overrides_values_legacy_string_list() -> None:
assert cfg.overload_cooldown_seconds == 60
assert cfg.proactive_refresh_seconds == 300
assert cfg.health_policy_enabled is False
assert cfg.batch_concurrency == 8
assert cfg.probing_enabled is True
assert cfg.probing_interval_minutes == 15
assert cfg.auto_remove_banned_keys is True
def test_parse_pool_config_parses_batch_concurrency() -> None:
cfg = parse_pool_config({"pool_advanced": {"batch_concurrency": 12}})
assert cfg is not None
assert cfg.batch_concurrency == 12
def test_parse_pool_config_clamps_batch_concurrency() -> None:
cfg = parse_pool_config({"pool_advanced": {"batch_concurrency": 99}})
assert cfg is not None
assert cfg.batch_concurrency == 32
def test_parse_pool_config_new_object_list_format() -> None:
"""New object-list format: [{preset, enabled, mode}]."""
cfg = parse_pool_config(

View File

@@ -196,7 +196,7 @@ async def test_codex_refresher_http_401_marks_auth_invalid_and_disables(
assert result["auto_disabled"] is True
assert metadata_updates == {}
assert state_updates["k1"]["is_active"] is False
assert "401" in str(state_updates["k1"]["oauth_invalid_reason"])
assert str(state_updates["k1"]["oauth_invalid_reason"]).startswith("[OAUTH_EXPIRED]")
@pytest.mark.asyncio
@@ -847,3 +847,66 @@ async def test_kiro_refresher_success_updates_metadata_and_auth_config(
assert state_updates["k1"]["oauth_invalid_at"] is None
assert state_updates["k1"]["oauth_invalid_reason"] is None
assert state_updates["k1"]["auth_config"].startswith("ENC:")
@pytest.mark.asyncio
async def test_codex_refresher_http_402_workspace_deactivated_marks_account_block(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.provider_keys.quota_refresh import codex_refresher as module
key = SimpleNamespace(
id="k1",
name="K1",
api_key="enc-key",
auth_type="oauth",
auth_config="enc-config",
proxy=None,
)
provider = SimpleNamespace(proxy=None)
endpoint = SimpleNamespace()
metadata_updates: dict[str, dict[str, Any]] = {}
state_updates: dict[str, dict[str, Any]] = {}
async def _fake_auth_info(_endpoint: Any, _key: Any) -> Any:
return None
_install_module(
monkeypatch,
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
monkeypatch.setattr(
module.crypto_service,
"decrypt",
lambda value: (
"sk-test"
if value == "enc-key"
else json.dumps({"plan_type": "team", "account_id": "acc-1"})
),
)
response = _FakeResponse(status_code=402, payload={"detail": {"code": "deactivated_workspace"}})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
provider=cast(Any, provider),
key=cast(Any, key),
endpoint=cast(Any, endpoint),
codex_wham_usage_url="https://example.test",
metadata_updates=metadata_updates,
state_updates=state_updates,
)
assert result["status"] == "workspace_deactivated"
assert result["status_code"] == 402
assert metadata_updates["k1"]["codex"]["account_disabled"] is True
assert metadata_updates["k1"]["codex"]["reason"] == "deactivated_workspace"
assert state_updates["k1"]["oauth_invalid_at"] is not None
assert str(state_updates["k1"]["oauth_invalid_reason"]).startswith("[ACCOUNT_BLOCK]")