"""真实推理矩阵:独立请求/身份资格,逐轮留证,UNKNOWN 不证明关闭。 不新增成功 SSE 捕获器,不把整类异常跳过;历史长度/prompt 锚点仅作 指定样本的形态回归,不提升能力覆盖。T10 不可关闭与预期拒绝是独立命题。 """ import asyncio import dataclasses import os from collections.abc import Mapping from pathlib import Path from types import MappingProxyType from uuid import uuid4 import pytest from dotenv import dotenv_values from polygateway import GatewaySettings, ThinkingObservation from polygateway.thinking import DEFAULT_CAPABILITIES, ThinkingCapability from polygateway.types import EFFORT_ORDER, Effort from tests.e2e.conftest import ( LiveCapture, captured_chat_round, chat_expectations, declared_control, enforce_verdict, observed_client, source_controls, ) from tests.live_evidence import ( LiveVerdict, assess_thinking_coverage, combine_live_verdicts, qualify_live_rounds, summarize_verdicts, write_live_round, ) _ENV = {k: v for k, v in {**dotenv_values(".env"), **os.environ}.items() if v is not None} _HAS_SOURCE = any(k.startswith("LLM__") and k.endswith("__API_KEY") for k in _ENV) pytestmark = [ pytest.mark.slow, pytest.mark.skipif(not _HAS_SOURCE, reason="缺少矩阵必需凭据,未覆盖"), ] _OUT_DIR = Path("tests/outputs/134/live") _ROUNDS = int(os.environ.get("PGW_E2E_THINKING_ROUNDS", "10")) _TIER_ROUNDS = int(os.environ.get("PGW_E2E_TIER_ROUNDS", "5")) _TIER_LONG_ROUNDS = int(os.environ.get("PGW_E2E_TIER_LONG_ROUNDS", "3")) _TIER_CONCURRENCY = int(os.environ.get("PGW_E2E_TIER_CONCURRENCY", "3")) _PROMPT = "一个笼子里有若干鸡和兔,共 35 个头、94 只脚。鸡和兔各有多少只?只输出两个数字。" _TIER_PROMPT = "23 乘以 47 等于多少?只回答一个数字,不要解释。" _TIER_LONG_PROMPT = ( "\n".join( f"{i:04d}. 这是一段与题目无关的填充文字,仅用于把上下文撑到数千 token," "以复核短提示词下得到的关闭结论在长上下文下是否依然成立。" for i in range(120) ) + "\n\n" + _TIER_PROMPT ) _ALL_EFFORTS = (*EFFORT_ORDER, Effort.AUTO) _MODEL_PROVIDER = { "MiniMax-M3": "minimax", "MiniMax-M2.7": "minimax", "MiniMax-M2.5": "minimax", "qwen3.7-plus": "qwen", "qwen3.7-max": "qwen", "qwen3.6-plus": "qwen", "qwen3.5-flash": "qwen", "qwen-plus-latest": "qwen", "deepseek-v4-pro": "deepseek", "deepseek-v4-flash": "deepseek", "deepseek-v4-flash-vision-exp": "deepseek", "glm-5.3": "zhipu", "glm-5.3-flash": "zhipu", "glm-5.2": "zhipu", "glm-5.1": "zhipu", "glm-5": "zhipu", "glm-4.6v": "zhipu", "kimi-k3": "moonshot", "kimi-for-coding": "moonshot", "gpt-5.4": "openai", "gpt-5.5": "openai", "claude-opus-5": "anthropic", "claude-sonnet-5": "anthropic", "claude-haiku-5": "anthropic", "gemini-3.1-pro": "google", "gemini-3-flash": "google", } _MODEL_REPORTED_ALIASES: Mapping[str, frozenset[str]] = MappingProxyType( { "kimi-k3": frozenset({"k3"}), "kimi-for-coding": frozenset({"k3"}), "gemini-3-flash": frozenset({"gemini-3-flash-preview"}), "gemini-3.1-pro": frozenset({"gemini-3.1-pro-preview"}), } ) def _settings(**source_overrides): """强制关闭缓存,清除 inherited 受管意图后应用本矩阵配置。""" base = GatewaySettings.from_env("LLM", env={**_ENV, "PGW_CACHE_BACKEND": "none"}) source = dataclasses.replace( base.sources[0], enable_thinking=None, reasoning_effort=None, extra_body={} ) source = dataclasses.replace(source, **source_overrides) return dataclasses.replace(base, sources=(source,)) def _tier_settings(model): """保留既有一次 retry 探测预算;不压缩生产 stall 或单次 timeout。""" base = GatewaySettings.from_env( "LLM", env={**_ENV, "PGW_CACHE_BACKEND": "none", "LLM_MAX_RETRIES": "1"} ) source = dataclasses.replace( base.sources[0], model=model, provider=_MODEL_PROVIDER[model], enable_thinking=None, reasoning_effort=None, extra_body={}, ) return dataclasses.replace(base, sources=(source,)) async def _collect_rounds( settings, *, rounds, stream, prompt, matrix_id, effort=None, capabilities=None, concurrency=1 ): """保留所有失败轮,不把可用轮集合偷偷当新分母。""" if rounds < 1 or concurrency < 1: raise ValueError("轮次/并发必须为正数") messages = [{"role": "user", "content": prompt}] controls = ( source_controls(settings) if effort is None else {s.name: declared_control(s.provider, effort) for s in settings.sources} ) capture = LiveCapture( expectations=chat_expectations( settings, messages=messages, stream=stream, controls=controls ) ) run_id = uuid4().hex semaphore = asyncio.Semaphore(concurrency) async with observed_client(settings, capture, capabilities=capabilities) as client: async def one(index): """每轮已落盘后才回到汇总,断言失败也有记录。""" def validate(response): assert response.content.strip() if effort is not None: assert response.applied_effort is effort async with semaphore: response, verdict = await captured_chat_round( client, capture, run_id=run_id, matrix_id=matrix_id, round_index=index + 1, output_dir=_OUT_DIR, messages=messages, models={s.name: s.model for s in settings.sources}, providers={s.name: s.provider for s in settings.sources}, source_efforts={ s.name: s.reasoning_effort if s.reasoning_effort is not None else (Effort.AUTO if s.enable_thinking else Effort.NONE) if s.enable_thinking is not None else None for s in settings.sources }, aliases=_MODEL_REPORTED_ALIASES, stream=stream, reasoning_effort=effort, cache_salt=f"{run_id}-{index}", validate=validate, ) return {"round": index + 1, "verdict": verdict, "response": response} # return_exceptions 保证一个取证写失败不使其他任务越过资源关闭边界。 results = await asyncio.gather(*(one(i) for i in range(rounds)), return_exceptions=True) values = [] for value in results: if isinstance(value, BaseException): raise value values.append(value) counts = summarize_verdicts([value["verdict"] for value in values], planned_rounds=rounds) write_live_round( _OUT_DIR, run_id=run_id, matrix_id=matrix_id + "-rounds", round_index=0, safe_fields={"counts": counts, "completed_rounds": len(values), "planned_rounds": rounds}, ) return values async def _run_rounds(rounds, *, stream=True, matrix_id="thinking", **source_overrides): """L1–L8 的资格证据出口,不作整类 skip。""" return await _collect_rounds( _settings(**source_overrides), rounds=rounds, stream=stream, prompt=_PROMPT, matrix_id=matrix_id, ) def _qualified(rows, *, planned_rounds): """汇总资格先失败后未覆盖;部分失败不能被成功轮掩盖。""" return qualify_live_rounds([row["verdict"] for row in rows], planned_rounds=planned_rounds) def _coverage(rows, *, planned_rounds, proposition): """只有全轮资格通过才进入推理观测命题。""" verdict = _qualified(rows, planned_rounds=planned_rounds) if verdict.status == "PASS": verdict = assess_thinking_coverage( [row["response"].thinking_observation for row in rows], planned_rounds=planned_rounds, proposition=proposition, ) return verdict def _conclude(matrix, verdict, *, proposition=None): """命题汇总先落盘再交给 pytest,不覆盖逐轮原件。""" write_live_round( _OUT_DIR, run_id=uuid4().hex, matrix_id=matrix, round_index=0, safe_fields={ "status": verdict.status, "reason": verdict.reason, "proposition": proposition, }, ) enforce_verdict(verdict) class TestMiniMaxM3: """AUTO 拒绝已移至离线契约;真实开启明确请求 medium。""" async def test_l1_disable_actually_disables(self): rows = await _run_rounds( _ROUNDS, matrix_id="L1", provider="minimax", model="MiniMax-M3", enable_thinking=False ) _conclude( "L1", _coverage(rows, planned_rounds=_ROUNDS, proposition="disabled"), proposition="disabled", ) async def test_l2_enable_actually_enables(self): rows = await _run_rounds( _ROUNDS, matrix_id="L2", provider="minimax", model="MiniMax-M3", reasoning_effort=Effort.MEDIUM, ) _conclude( "L2", _coverage(rows, planned_rounds=_ROUNDS, proposition="enabled"), proposition="enabled", ) async def test_l2b_off_and_on_are_distinguishable_without_magic_numbers(self): """指定历史 prompt 锚点回归,不宣称关闭能力已覆盖。""" rounds = max(3, _ROUNDS // 3) off = await _run_rounds( rounds, matrix_id="L2b-off", provider="minimax", model="MiniMax-M3", enable_thinking=False, ) on = await _run_rounds( rounds, matrix_id="L2b-on", provider="minimax", model="MiniMax-M3", reasoning_effort=Effort.MEDIUM, ) verdict = _qualified(off + on, planned_rounds=rounds * 2) if verdict.status == "PASS": distinct = max(r["response"].prompt_tokens for r in off) < min( r["response"].prompt_tokens for r in on ) verdict = LiveVerdict( "PASS" if distinct else "FAIL", "指定历史 prompt 锚点比较;不是关闭证明" ) _conclude("L2b", verdict, proposition="historical-prompt-anchor") async def test_l3_no_opinion_is_the_model_default(self): rows = await _run_rounds(_ROUNDS, matrix_id="L3", provider="minimax", model="MiniMax-M3") verdict = _qualified(rows, planned_rounds=_ROUNDS) if verdict.status == "PASS" and any( row["response"].applied_effort is not None for row in rows ): verdict = LiveVerdict("FAIL", "不表态路径擅自记录档位") _conclude("L3", verdict, proposition="no-opinion-not-capability") async def test_l3b_none_is_recognised_not_silently_dropped(self): """保留原非法 raw 值对照预算,但不提升 UNKNOWN。""" rounds = max(3, _ROUNDS // 3) bogus = await _run_rounds( rounds, matrix_id="L3b-bogus", provider="minimax", model="MiniMax-M3", extra_body={"reasoning_effort": "definitely-not-a-real-level"}, ) off = await _run_rounds( rounds, matrix_id="L3b-off", provider="minimax", model="MiniMax-M3", enable_thinking=False, ) verdicts = [ _coverage(bogus, planned_rounds=rounds, proposition="enabled"), _coverage(off, planned_rounds=rounds, proposition="disabled"), ] _conclude("L3b", _combine(verdicts), proposition="raw-counterexample") async def test_l4_raw_only_explicit_high(self): """退出受管意图后才保留 raw high;双来源拒绝在 unit 守卫。""" rounds = max(3, _ROUNDS // 2) rows = await _run_rounds( rounds, matrix_id="L4", provider="minimax", model="MiniMax-M3", extra_body={"reasoning_effort": "high"}, ) _conclude( "L4", _coverage(rows, planned_rounds=rounds, proposition="enabled"), proposition="enabled", ) async def test_l5_non_stream_path_is_distinguishable_and_honestly_unknown(self): """保留流/非流预算;UNKNOWN 是明确未覆盖而非长度锚点成功。""" rounds = max(3, _ROUNDS // 2) off = await _run_rounds( rounds, matrix_id="L5-off", stream=False, provider="minimax", model="MiniMax-M3", enable_thinking=False, ) on = await _run_rounds( rounds, matrix_id="L5-on", stream=False, provider="minimax", model="MiniMax-M3", reasoning_effort=Effort.MEDIUM, ) _conclude( "L5", _combine( [ _coverage(off, planned_rounds=rounds, proposition="disabled"), _coverage(on, planned_rounds=rounds, proposition="enabled"), ] ), proposition="nonstream-enabled-disabled", ) def _combine(verdicts): """任一失败优先,部分未覆盖不得汇总全 PASS。""" return combine_live_verdicts(verdicts) class TestOtherProviders: """既有供应商开启/关闭真实矩阵。""" @pytest.mark.parametrize( ("matrix", "provider", "model"), [("L6", "qwen", "qwen3.7-plus"), ("L7", "deepseek", "deepseek-v4-pro")], ) async def test_existing_profiles_still_disable(self, matrix, provider, model): rows = await _run_rounds( _ROUNDS, matrix_id=matrix, provider=provider, model=model, enable_thinking=False ) _conclude( matrix, _coverage(rows, planned_rounds=_ROUNDS, proposition="disabled"), proposition="disabled", ) async def test_qwen_enabled_is_observed(self): rows = await _run_rounds( _ROUNDS, matrix_id="L6b", provider="qwen", model="qwen3.7-plus", enable_thinking=True ) _conclude( "L6b", _coverage(rows, planned_rounds=_ROUNDS, proposition="enabled"), proposition="enabled", ) class TestCapabilityDrift: """只运行可关闭声明的真实验证;不可关闭装配拒绝另在 unit。""" @pytest.mark.parametrize( "model", sorted( model for model, capability in DEFAULT_CAPABILITIES.items() if capability.can_disable ), ) async def test_declared_capability_matches_reality(self, model): rounds = max(3, _ROUNDS // 2) rows = await _run_rounds( rounds, matrix_id="L8", provider=_MODEL_PROVIDER[model], model=model, enable_thinking=False, ) _conclude( "L8", _coverage(rows, planned_rounds=rounds, proposition="disabled"), proposition="disabled", ) async def _probe_effort(model, effort, *, rounds, prompt, prompt_kind): """临时全档表仅用于 T10 探测,不写回 DEFAULT,也不生成预期 wire。""" return await _collect_rounds( _tier_settings(model), rounds=rounds, stream=True, prompt=prompt, matrix_id="T10-" + prompt_kind, effort=effort, capabilities={model: ThinkingCapability(_ALL_EFFORTS, evidence="T10 临时探测声明")}, concurrency=_TIER_CONCURRENCY, ) class TestTierProbe: """逐型号能力命题,不把拒绝、不可关闭和 UNKNOWN 混在一起。""" @pytest.mark.parametrize("model", sorted(_MODEL_PROVIDER)) async def test_t10_none_direction_matches_declaration(self, model): capability = DEFAULT_CAPABILITIES.get(model) proposition = "disabled" if capability and capability.can_disable else "cannot_disable" short = await _probe_effort( model, Effort.NONE, rounds=_TIER_ROUNDS, prompt=_TIER_PROMPT, prompt_kind="none-short" ) verdict = _coverage(short, planned_rounds=_TIER_ROUNDS, proposition=proposition) # 沿既有矩阵:短档没有 OBSERVED 才做长上下文复核;不新增锚点调用。 if _qualified(short, planned_rounds=_TIER_ROUNDS).status == "PASS" and not any( r["response"].thinking_observation is ThinkingObservation.OBSERVED for r in short ): long_rows = await _probe_effort( model, Effort.NONE, rounds=_TIER_LONG_ROUNDS, prompt=_TIER_LONG_PROMPT, prompt_kind="none-long", ) verdict = _coverage( short + long_rows, planned_rounds=_TIER_ROUNDS + _TIER_LONG_ROUNDS, proposition=proposition, ) if capability is None and verdict.status != "FAIL": verdict = LiveVerdict("UNCOVERED", "未登记候选只保留观测,不自动登记能力") _conclude("T10-none", verdict, proposition=proposition) @pytest.mark.parametrize("model", sorted(DEFAULT_CAPABILITIES)) async def test_t10_declared_tiers_actually_reason(self, model): tiers = [e for e in DEFAULT_CAPABILITIES[model].supported_efforts if e is not Effort.NONE] verdicts = [] for tier in tiers: rows = await _probe_effort( model, tier, rounds=_TIER_ROUNDS, prompt=_TIER_PROMPT, prompt_kind="tier-" + tier.value, ) verdict = _coverage(rows, planned_rounds=_TIER_ROUNDS, proposition="enabled") verdicts.append(verdict) write_live_round( _OUT_DIR, run_id=uuid4().hex, matrix_id="T10-tier", round_index=0, safe_fields={ "requested_model": model, "requested_effort": tier.value, "status": verdict.status, "reason": verdict.reason, }, ) _conclude("T10-tiers", _combine(verdicts), proposition="enabled-all-declared-tiers") @pytest.mark.parametrize("model", ["gemini-3.1-pro", "gpt-5.5", "glm-5.3"]) async def test_t10_no_opinion_stays_no_opinion(self, model): rows = await _collect_rounds( _tier_settings(model), rounds=_TIER_ROUNDS, stream=True, prompt=_TIER_PROMPT, matrix_id="T10-default", capabilities={model: ThinkingCapability(_ALL_EFFORTS, evidence="T10 临时探测声明")}, ) verdict = _qualified(rows, planned_rounds=_TIER_ROUNDS) if verdict.status == "PASS" and any( row["response"].applied_effort is not None for row in rows ): verdict = LiveVerdict("FAIL", "默认基线擅自推定档位") _conclude("T10-default", verdict, proposition="no-opinion-not-capability")