diff --git a/core/types.py b/core/types.py index 28ca3cb..fb1cf61 100644 --- a/core/types.py +++ b/core/types.py @@ -1 +1,24 @@ """跨模块共享类型。""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class LLMResponse: + """LLM/VLM 调用的统一返回值。 + + 由 adapters 层生成,core 层消费。frozen=True 确保响应不可变。 + """ + + content: str + thinking: str + model: str + provider: str + prompt_tokens: int + completion_tokens: int + latency_ms: int + ttft_ms: float | None + max_inter_token_ms: float | None + cache_hit: bool + call_id: str diff --git a/tests/unit/test_core_types.py b/tests/unit/test_core_types.py new file mode 100644 index 0000000..45947e2 --- /dev/null +++ b/tests/unit/test_core_types.py @@ -0,0 +1,51 @@ +"""core/types.py 单元测试。""" +from __future__ import annotations + +import pytest + +from core.types import LLMResponse + + +class TestLLMResponse: + @pytest.fixture() + def sample_response(self) -> LLMResponse: + return LLMResponse( + content="回答内容", + thinking="思考过程", + model="deepseek-v4-pro", + provider="deepseek", + prompt_tokens=100, + completion_tokens=50, + latency_ms=1200, + ttft_ms=350.0, + max_inter_token_ms=45.0, + cache_hit=False, + call_id="test-uuid-001", + ) + + def test_frozen_prevents_mutation(self, sample_response: LLMResponse) -> None: + with pytest.raises(AttributeError): + sample_response.content = "篡改" + + def test_all_fields_accessible(self, sample_response: LLMResponse) -> None: + assert sample_response.content == "回答内容" + assert sample_response.thinking == "思考过程" + assert sample_response.model == "deepseek-v4-pro" + assert sample_response.provider == "deepseek" + assert sample_response.prompt_tokens == 100 + assert sample_response.completion_tokens == 50 + assert sample_response.latency_ms == 1200 + assert sample_response.ttft_ms == 350.0 + assert sample_response.max_inter_token_ms == 45.0 + assert sample_response.cache_hit is False + assert sample_response.call_id == "test-uuid-001" + + def test_cache_hit_response_has_none_ttft(self) -> None: + resp = LLMResponse( + content="cached", thinking="", model="m", provider="p", + prompt_tokens=0, completion_tokens=0, latency_ms=1, + ttft_ms=None, max_inter_token_ms=None, cache_hit=True, call_id="c", + ) + assert resp.ttft_ms is None + assert resp.max_inter_token_ms is None + assert resp.cache_hit is True