mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(test,export,headers): 模型测试复用统一运行时、实时进度展示、导出增强与请求头大小写保留
- 模型测试 failover 从手动 FailoverEngine 改为 TaskService.execute_sync_candidates 统一运行时 - 前端新增实时 trace 轮询进度展示(候选状态、测试账号、进度条) - 用户导出/导入支持明文 Key 优先(版本升至 1.2),新增 email_verified 字段 - SENSITIVE_CREDENTIAL_FIELDS 统一到 provider_ops/types.py,补充 refresh_token - 请求头大小写保留机制(resolve_header_name_case + HeaderBuilder.add 语义修改) - Codex envelope 移除合成头部,保留客户端原始请求头 - endpoint_checker 支持自定义超时透传 - 新增 x-forwarded-scheme 到上游丢弃头部列表
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
|
||||
from src.api.handlers.base.request_builder import PassthroughRequestBuilder
|
||||
from src.services.provider.adapters.codex.request_patching import (
|
||||
maybe_patch_request_for_codex,
|
||||
patch_openai_cli_request_for_codex,
|
||||
@@ -110,42 +113,49 @@ def test_openai_cli_normalizer_request_from_internal_codex_variant_defaults_stor
|
||||
assert out["store"] is False
|
||||
|
||||
|
||||
def test_codex_envelope_extra_headers_includes_sse_accept_and_session() -> None:
|
||||
def test_codex_envelope_extra_headers_does_not_inject_synthetic_headers() -> None:
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
headers = codex_oauth_envelope.extra_headers() or {}
|
||||
assert headers.get("Accept") == "text/event-stream"
|
||||
assert headers.get("Originator") == "codex_cli_rs"
|
||||
assert headers.get("Version") == "0.101.0"
|
||||
assert headers.get("Connection") == "Keep-Alive"
|
||||
assert isinstance(headers.get("Session_id"), str)
|
||||
assert headers.get("Session_id")
|
||||
assert codex_oauth_envelope.extra_headers() is None
|
||||
|
||||
|
||||
def test_codex_envelope_extra_headers_compact_uses_json_accept() -> None:
|
||||
from src.services.provider.adapters.codex.context import (
|
||||
CodexRequestContext,
|
||||
set_codex_request_context,
|
||||
def test_codex_passthrough_builder_preserves_real_codex_headers() -> None:
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
builder = PassthroughRequestBuilder()
|
||||
endpoint = SimpleNamespace(api_family="openai", endpoint_kind="cli", header_rules=None)
|
||||
key = SimpleNamespace(api_key="unused")
|
||||
|
||||
headers = builder.build_headers(
|
||||
original_headers={
|
||||
"accept": "text/event-stream",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "Codex Desktop/0.108.0-alpha.12",
|
||||
"originator": "Codex Desktop",
|
||||
"x-codex-turn-metadata": '{"turn_id":"abc"}',
|
||||
"x-forwarded-scheme": "https",
|
||||
"host": "aether.hetunai.cn",
|
||||
"content-length": "123",
|
||||
},
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
pre_computed_auth=("Authorization", "Bearer upstream-token"),
|
||||
envelope=codex_oauth_envelope,
|
||||
)
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
set_codex_request_context(CodexRequestContext(is_compact=True))
|
||||
headers = codex_oauth_envelope.extra_headers() or {}
|
||||
assert headers.get("Accept") == "application/json"
|
||||
set_codex_request_context(None)
|
||||
|
||||
|
||||
def test_codex_envelope_extra_headers_uses_account_id_header() -> None:
|
||||
from src.services.provider.adapters.codex.context import (
|
||||
CodexRequestContext,
|
||||
set_codex_request_context,
|
||||
)
|
||||
from src.services.provider.adapters.codex.envelope import codex_oauth_envelope
|
||||
|
||||
set_codex_request_context(CodexRequestContext(account_id="acc_123"))
|
||||
headers = codex_oauth_envelope.extra_headers() or {}
|
||||
assert headers.get("Chatgpt-Account-Id") == "acc_123"
|
||||
set_codex_request_context(None)
|
||||
assert headers["accept"] == "text/event-stream"
|
||||
assert headers["content-type"] == "application/json"
|
||||
assert headers["user-agent"] == "Codex Desktop/0.108.0-alpha.12"
|
||||
assert headers["originator"] == "Codex Desktop"
|
||||
assert headers["x-codex-turn-metadata"] == '{"turn_id":"abc"}'
|
||||
assert headers["Authorization"] == "Bearer upstream-token"
|
||||
assert "Version" not in headers
|
||||
assert "Session_id" not in headers
|
||||
assert "Connection" not in headers
|
||||
assert "Chatgpt-Account-Id" not in headers
|
||||
assert "host" not in headers
|
||||
assert "content-length" not in headers
|
||||
assert "x-forwarded-scheme" not in headers
|
||||
|
||||
|
||||
def _encode_unsigned_jwt(payload: dict[str, object]) -> str:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from src.api.admin.system import AdminExportConfigAdapter, AdminImportConfigAdapter
|
||||
from src.services.provider_ops.types import SENSITIVE_CREDENTIAL_FIELDS
|
||||
|
||||
|
||||
def test_export_key_api_formats_falls_back_to_provider_endpoints_when_none() -> None:
|
||||
@@ -59,3 +60,57 @@ def test_import_key_api_formats_keeps_explicit_empty_list() -> None:
|
||||
)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
class _FakeCrypto:
|
||||
def encrypt(self, value: str) -> str:
|
||||
return f"enc:{value}"
|
||||
|
||||
def decrypt(self, value: str) -> str:
|
||||
return value.removeprefix("enc:")
|
||||
|
||||
|
||||
def test_provider_ops_sensitive_fields_include_refresh_token() -> None:
|
||||
assert "refresh_token" in SENSITIVE_CREDENTIAL_FIELDS
|
||||
|
||||
|
||||
def test_export_provider_config_decrypts_refresh_token() -> None:
|
||||
adapter = AdminExportConfigAdapter()
|
||||
|
||||
config = {
|
||||
"provider_ops": {
|
||||
"connector": {
|
||||
"credentials": {
|
||||
"refresh_token": "enc:rt-1",
|
||||
"api_key": "enc:key-1",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = adapter._decrypt_provider_config(config, _FakeCrypto())
|
||||
|
||||
assert result["provider_ops"]["connector"]["credentials"]["refresh_token"] == "rt-1"
|
||||
assert result["provider_ops"]["connector"]["credentials"]["api_key"] == "key-1"
|
||||
assert config["provider_ops"]["connector"]["credentials"]["refresh_token"] == "enc:rt-1"
|
||||
|
||||
|
||||
def test_import_provider_config_encrypts_refresh_token() -> None:
|
||||
adapter = AdminImportConfigAdapter()
|
||||
|
||||
config = {
|
||||
"provider_ops": {
|
||||
"connector": {
|
||||
"credentials": {
|
||||
"refresh_token": "rt-1",
|
||||
"api_key": "key-1",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = adapter._encrypt_provider_config(config, _FakeCrypto())
|
||||
|
||||
assert result["provider_ops"]["connector"]["credentials"]["refresh_token"] == "enc:rt-1"
|
||||
assert result["provider_ops"]["connector"]["credentials"]["api_key"] == "enc:key-1"
|
||||
assert config["provider_ops"]["connector"]["credentials"]["refresh_token"] == "rt-1"
|
||||
|
||||
57
tests/unit/test_admin_system_users_export_import.py
Normal file
57
tests/unit/test_admin_system_users_export_import.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from src.api.admin.system import AdminExportUsersAdapter, AdminImportUsersAdapter
|
||||
from src.core.crypto import crypto_service
|
||||
from src.models.database import ApiKey
|
||||
|
||||
|
||||
def test_export_user_api_key_prefers_plaintext_key() -> None:
|
||||
plaintext_key = "ak-user-plain-1"
|
||||
key = ApiKey(
|
||||
id="key-1",
|
||||
user_id="user-1",
|
||||
key_hash=ApiKey.hash_key(plaintext_key),
|
||||
key_encrypted=crypto_service.encrypt(plaintext_key),
|
||||
name="Demo Key",
|
||||
is_standalone=False,
|
||||
balance_used_usd=1.5,
|
||||
current_balance_usd=8.5,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
data = AdminExportUsersAdapter._serialize_api_key(key, include_is_standalone=True)
|
||||
|
||||
assert data["key"] == plaintext_key
|
||||
assert "key_encrypted" not in data
|
||||
assert data["key_hash"] == ApiKey.hash_key(plaintext_key)
|
||||
assert data["is_standalone"] is False
|
||||
|
||||
|
||||
def test_import_user_api_key_material_reencrypts_plaintext_key() -> None:
|
||||
plaintext_key = "ak-user-plain-2"
|
||||
|
||||
key_hash, key_encrypted = AdminImportUsersAdapter._resolve_api_key_material(
|
||||
{
|
||||
"key": plaintext_key,
|
||||
"key_hash": "stale-hash",
|
||||
"key_encrypted": "stale-ciphertext",
|
||||
}
|
||||
)
|
||||
|
||||
assert key_hash == ApiKey.hash_key(plaintext_key)
|
||||
assert key_encrypted is not None
|
||||
assert crypto_service.decrypt(key_encrypted) == plaintext_key
|
||||
|
||||
|
||||
def test_import_user_api_key_material_keeps_legacy_encrypted_payload() -> None:
|
||||
legacy_plaintext = "ak-user-legacy-1"
|
||||
legacy_encrypted = crypto_service.encrypt(legacy_plaintext)
|
||||
legacy_hash = ApiKey.hash_key(legacy_plaintext)
|
||||
|
||||
key_hash, key_encrypted = AdminImportUsersAdapter._resolve_api_key_material(
|
||||
{
|
||||
"key_hash": legacy_hash,
|
||||
"key_encrypted": legacy_encrypted,
|
||||
}
|
||||
)
|
||||
|
||||
assert key_hash == legacy_hash
|
||||
assert key_encrypted == legacy_encrypted
|
||||
@@ -65,7 +65,8 @@ class TestHeaderBuilder:
|
||||
builder.add("authorization", "b")
|
||||
built = builder.build()
|
||||
assert len(built) == 1
|
||||
assert list(built.values()) == ["b"]
|
||||
assert built["Authorization"] == "b"
|
||||
assert "authorization" not in built
|
||||
|
||||
def test_add_protected_does_not_override(self) -> None:
|
||||
builder = HeaderBuilder()
|
||||
@@ -135,7 +136,8 @@ class TestBuildUpstreamHeaders:
|
||||
extra_headers={"User-Agent": "b"},
|
||||
)
|
||||
assert len([k for k in result if k.lower() == "user-agent"]) == 1
|
||||
assert result["User-Agent"] == "b"
|
||||
assert result["user-agent"] == "b"
|
||||
assert "User-Agent" not in result
|
||||
|
||||
def test_default_content_type(self) -> None:
|
||||
result = build_upstream_headers_for_endpoint({}, "openai:chat", "provider")
|
||||
@@ -169,3 +171,35 @@ class TestCapabilityResolverHeaderParsing:
|
||||
request_headers={"x-require-capability": "context_1m"}
|
||||
)
|
||||
assert reqs == {"context_1m": True}
|
||||
|
||||
|
||||
class TestAuthHeaderCasePreservation:
|
||||
def test_build_upstream_headers_preserves_lowercase_authorization_key(self) -> None:
|
||||
result = build_upstream_headers_for_endpoint(
|
||||
{"authorization": "Bearer client-token", "X-Test": "1"},
|
||||
"openai:chat",
|
||||
"provider",
|
||||
)
|
||||
assert "authorization" in result
|
||||
assert "Authorization" not in result
|
||||
assert result["authorization"] == "Bearer provider"
|
||||
|
||||
def test_passthrough_request_builder_preserves_lowercase_authorization_key(self) -> None:
|
||||
from types import SimpleNamespace
|
||||
|
||||
from src.api.handlers.base.request_builder import PassthroughRequestBuilder
|
||||
|
||||
builder = PassthroughRequestBuilder()
|
||||
endpoint = SimpleNamespace(api_family="openai", endpoint_kind="cli", header_rules=None)
|
||||
key = SimpleNamespace(api_key="unused")
|
||||
|
||||
headers = builder.build_headers(
|
||||
original_headers={"authorization": "Bearer client-token"},
|
||||
endpoint=endpoint,
|
||||
key=key,
|
||||
pre_computed_auth=("Authorization", "Bearer provider-token"),
|
||||
)
|
||||
|
||||
assert "authorization" in headers
|
||||
assert "Authorization" not in headers
|
||||
assert headers["authorization"] == "Bearer provider-token"
|
||||
|
||||
@@ -2,7 +2,9 @@ from types import SimpleNamespace
|
||||
|
||||
from src.api.admin.provider_query import (
|
||||
_build_direct_test_candidates,
|
||||
_build_test_attempts_from_candidate_keys,
|
||||
_filter_test_candidates_by_endpoint,
|
||||
_resolve_test_effective_model,
|
||||
)
|
||||
|
||||
|
||||
@@ -36,3 +38,54 @@ def test_filter_test_candidates_by_endpoint_keeps_matching_candidates() -> None:
|
||||
|
||||
assert {candidate.endpoint.id for candidate in filtered} == {endpoint_a.id}
|
||||
assert all(candidate.endpoint.id != endpoint_b.id for candidate in filtered)
|
||||
|
||||
|
||||
def test_resolve_test_effective_model_prefers_pool_key_mapping() -> None:
|
||||
provider, endpoint_a, _endpoint_b = _build_provider()
|
||||
candidate = _build_direct_test_candidates(provider, endpoint_id=endpoint_a.id)[0] # type: ignore[arg-type]
|
||||
pool_key = SimpleNamespace(id="pool-key", _pool_mapping_matched_model="mapped-model")
|
||||
request = SimpleNamespace(mode="global", model_name="gpt-4")
|
||||
|
||||
effective = _resolve_test_effective_model(
|
||||
provider=provider, # type: ignore[arg-type]
|
||||
candidate=candidate,
|
||||
request=request, # type: ignore[arg-type]
|
||||
gm_obj=None,
|
||||
key=pool_key,
|
||||
)
|
||||
|
||||
assert effective == "mapped-model"
|
||||
|
||||
|
||||
def test_build_test_attempts_from_candidate_keys_includes_retry_index() -> None:
|
||||
candidate_keys = [
|
||||
SimpleNamespace(
|
||||
candidate_index=2,
|
||||
retry_index=1,
|
||||
key_id="key-b",
|
||||
key_name="Key B",
|
||||
auth_type="api_key",
|
||||
status="failed",
|
||||
skip_reason=None,
|
||||
error_message="timeout",
|
||||
status_code=504,
|
||||
latency_ms=1200,
|
||||
)
|
||||
]
|
||||
|
||||
attempts = _build_test_attempts_from_candidate_keys(
|
||||
candidate_keys=candidate_keys,
|
||||
candidate_meta_by_pair={
|
||||
(2, "key-b"): {
|
||||
"endpoint_api_format": "openai:chat",
|
||||
"endpoint_base_url": "https://example.test/v1",
|
||||
"effective_model": "mapped-model",
|
||||
}
|
||||
},
|
||||
candidate_meta_by_index={},
|
||||
)
|
||||
|
||||
assert len(attempts) == 1
|
||||
assert attempts[0].retry_index == 1
|
||||
assert attempts[0].effective_model == "mapped-model"
|
||||
assert attempts[0].endpoint_api_format == "openai:chat"
|
||||
|
||||
Reference in New Issue
Block a user