Files
meida_backend/tests/integration/test_vertical_slice.py
T
xsl f46ecc5eb0 fix(llm): price by call time and stop paying for thinking tokens
Two defects that only surface against the real DeepSeek API, both of which
break the cost model rather than the code:

deepseek-v4-flash enables thinking mode by default. The same single-turn
question took 13.5s with it on and 1.8s with it off, and the thinking tokens
are billed at the output rate. That misses NFR-PERF-02b (15s per turn) and
inflates every conversational call. SceneConfig now carries an explicit
`thinking` flag, all four V1 scenes set it to false, and the provider always
sends the parameter rather than relying on the upstream default. Reasoning
tokens are recorded separately in Usage.tokens_reasoning so the cost of ever
turning it back on is visible.

DeepSeek moves to peak/off-peak pricing on 2026-08-17, with peak (Beijing
09:00-12:00 and 14:00-18:00) at twice the off-peak rate. Reports are generated
mostly during peak hours, so a single blended rate would leave ob_ai_call --
the only input to V2 pricing -- off by nearly a factor of two.
PeakAwareRateTable resolves the rate at call time; cost is computed and stored
then, never recomputed later at whatever the current rate happens to be.

Also here:
- Replace the live API key in the redaction test with a synthetic one. The
  test that proves we redact secrets was itself a secret leak waiting for its
  first commit.
- Correct LLM_COST_RATES_JSON, which held placeholder rates.
- Make ids.SnowflakeGenerator take an injectable clock so rollover and
  clock-rollback are testable without patching a slots class.
- Rename exceptions to the -Error suffix (N818) and clear the remaining ruff,
  mypy and import-linter findings, including two dead lines: a duplicate
  pytestmark assignment and a no-op assert on a None return.

Gates: ruff, mypy (96 files incl. tests), 4/4 import contracts,
detect-secrets, 166 unit + 3 integration tests, migration up/down round-trip.
2026-08-14 00:54:40 +08:00

102 lines
3.3 KiB
Python
Executable File

"""End-to-end vertical slice against real PostgreSQL + Redis.
Run with ``make up && make migrate`` first, or let ``scripts/smoke.sh`` do it.
Skipped automatically when the infrastructure is not reachable, so ``make test``
stays fast and hermetic.
The LLM provider is faked here — ``scripts/smoke.sh`` is the one that proves the
real DeepSeek path.
"""
from __future__ import annotations
import os
import uuid
from decimal import Decimal
from typing import Any
import pytest
def _infra_available() -> bool:
return bool(os.getenv("MEIDA_RUN_INTEGRATION"))
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not _infra_available(),
reason="set MEIDA_RUN_INTEGRATION=1 with PostgreSQL and Redis running",
),
]
@pytest.fixture
async def client() -> Any:
import httpx
from apps.api.main import create_app
from packages.llm.types import ProviderResponse, Usage
from packages.shared.config import get_settings
app = create_app(get_settings())
class FakeProvider:
name = "deepseek"
default_model = "deepseek-v4-flash"
async def complete(self, request: Any) -> ProviderResponse:
return ProviderResponse(
text="建议白衬衫配西裤。",
model="deepseek-v4-flash",
usage=Usage(tokens_in=800, tokens_out=120, tokens_cached=600),
)
async def aclose(self) -> None:
return None
transport = httpx.ASGITransport(app=app)
async with (
httpx.AsyncClient(transport=transport, base_url="http://test") as http,
app.router.lifespan_context(app),
):
# 替换掉真实 provider:集成用例验证配额与计量链路,不该花钱也不该受网络影响
app.state.llm_gateway._providers["deepseek"] = FakeProvider()
yield http
async def test_chat_slice_reserves_commits_and_records_cost(client: Any) -> None:
token_resp = await client.post(
"/api/v1/auth/dev-token", json={"device_key": f"it-{uuid.uuid4().hex[:8]}"}
)
assert token_resp.json()["code"] == 0
headers = {"Authorization": f"Bearer {token_resp.json()['data']['access_token']}"}
before = (await client.get("/api/v1/quota", headers=headers)).json()["data"]
chat = await client.post("/api/v1/agent/chat", json={"message": "面试穿什么"}, headers=headers)
body = chat.json()
assert body["code"] == 0
assert body["data"]["reply"]
assert body["data"]["prompt_version"] == "v1"
assert Decimal(body["data"]["usage"]["cost_cents"]) > 0
after = (await client.get("/api/v1/quota", headers=headers)).json()["data"]
assert after["used"] == before["used"] + 1
ledger = (await client.get("/api/v1/quota/ledger", headers=headers)).json()["data"]
assert ledger[0]["tx_type"] == "consume"
assert ledger[0]["status"] == "committed"
async def test_alias_path_v1_matches_api_v1(client: Any) -> None:
assert (await client.get("/healthz")).status_code == 200
token = await client.post("/v1/auth/dev-token", json={"device_key": "alias-check"})
assert token.json()["code"] == 0
async def test_unauthenticated_requests_get_the_unified_envelope(client: Any) -> None:
resp = await client.post("/api/v1/agent/chat", json={"message": "hi"})
assert resp.status_code == 401
assert resp.json()["code"] == 40100