"""思考模式必须显式声明(deepseek-v4 默认开启)。 保护的是两件事:漏传 ``thinking`` 会让对话延迟从约 2s 涨到 13s 以上(实测同一个单句 问答),并且思考 token 按输出价计费 —— 一个用户看不到的字段同时打破响应时间目标 与成本模型。 """ from __future__ import annotations import json from typing import Any import httpx import pytest from packages.llm.providers.deepseek import DeepSeekProvider from packages.llm.routing import build_scene_configs from packages.llm.types import Message, ProviderRequest, Scene def _request(*, thinking: bool) -> ProviderRequest: return ProviderRequest( model="deepseek-v4-flash", messages=(Message(role="user", content="面试穿什么"),), temperature=0.7, max_tokens=800, timeout_seconds=30.0, thinking=thinking, ) def _provider(captured: dict[str, Any]) -> DeepSeekProvider: def handler(request: httpx.Request) -> httpx.Response: captured.update(json.loads(request.content)) return httpx.Response( 200, json={ "choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}], "usage": { "prompt_tokens": 12, "completion_tokens": 34, "completion_tokens_details": {"reasoning_tokens": 20}, }, }, ) return DeepSeekProvider( api_key="test-key", client=httpx.AsyncClient(transport=httpx.MockTransport(handler)), ) @pytest.mark.parametrize(("thinking", "expected"), [(False, "disabled"), (True, "enabled")]) async def test_thinking_is_always_sent_explicitly(thinking: bool, expected: str) -> None: captured: dict[str, Any] = {} provider = _provider(captured) await provider.complete(_request(thinking=thinking)) # 关键是这个字段一定存在:省略它就等于接受供应商的"开启"默认值 assert captured["thinking"] == {"type": expected} await provider.aclose() async def test_reasoning_tokens_are_recorded_for_cost_visibility() -> None: provider = _provider({}) response = await provider.complete(_request(thinking=True)) # 思考 token 已计入 tokens_out,单独留一份用于回答"输出成本有多少花在推理上" assert response.usage.tokens_out == 34 assert response.usage.tokens_reasoning == 20 await provider.aclose() def test_no_v1_scene_enables_thinking_by_default() -> None: configs = build_scene_configs(primary_provider="deepseek") assert [s for s, c in configs.items() if c.thinking] == [] # 且每个场景都显式给了值,不存在"忘了配"的场景 assert set(configs) == set(Scene)