Files
writer-work-flow/packages/llm_gateway/tests/test_capability_degradation.py
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

110 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""T5.4 能力协商 / 降级ARCH §4.4)。
结构化输出原生不支持时降级instructor JSON-提示路径仍由适配器自处理;网关
层负责在链内**优先选支持结构化输出的 provider**,无则降级到首个可用、对上层透明)。
降级时 `served_by.degraded=True` 标注,记账/日志可见,正确性不受影响。
"""
from __future__ import annotations
import uuid
from fakes_resilience import FakeLedger, ScriptedAdapter, chain, chain_resolver
from pydantic import BaseModel
from ww_llm_gateway.adapters.base import Capabilities
from ww_llm_gateway.gateway import Gateway
from ww_llm_gateway.types import Block, LlmRequest, Scope
class Tiny(BaseModel):
x: str
def _structured_req() -> LlmRequest:
return LlmRequest(
tier="analyst",
input="给我结构化",
output_schema=Tiny,
scope=Scope(user_id=uuid.UUID(int=1)),
)
async def test_prefers_structured_capable_provider_in_chain() -> None:
# 主 provider 不支持结构化输出,回退 provider 支持 → 网关优先选支持者服务结构化请求。
no_struct = ScriptedAdapter(
"weakprov",
capabilities_=Capabilities(structured_output=False, prefix_cache=False),
)
struct = ScriptedAdapter(
"strongprov",
capabilities_=Capabilities(structured_output=True, prefix_cache=True),
)
ledger = FakeLedger()
gw = Gateway(
{"weakprov": no_struct, "strongprov": struct},
ledger,
chain_resolver=chain_resolver(chain(("weakprov", "w"), ("strongprov", "s"))),
)
resp = await gw.run(_structured_req())
assert resp.served_by.provider == "strongprov"
assert resp.parsed is not None
assert no_struct.complete_calls == 0
async def test_degrades_when_no_structured_capable_provider() -> None:
# 链上无 provider 支持结构化输出 → 降级用首个可用(适配器自走 instructor JSON 提示)
# 并标 served_by.degraded=True不硬失败。
weak = ScriptedAdapter(
"weakprov",
capabilities_=Capabilities(structured_output=False),
)
gw = Gateway(
{"weakprov": weak},
FakeLedger(),
chain_resolver=chain_resolver(chain(("weakprov", "w"))),
)
resp = await gw.run(_structured_req())
assert resp.served_by.provider == "weakprov"
assert resp.served_by.degraded is True
assert weak.complete_calls == 1
async def test_no_degradation_flag_for_plain_text() -> None:
# 纯文本请求对任何 provider 都不算降级。
weak = ScriptedAdapter("weakprov", capabilities_=Capabilities(structured_output=False))
gw = Gateway(
{"weakprov": weak},
FakeLedger(),
chain_resolver=chain_resolver(chain(("weakprov", "w"))),
)
req = LlmRequest(tier="writer", input="正文", scope=Scope(user_id=uuid.UUID(int=1)))
resp = await gw.run(req)
assert resp.served_by.degraded is False
async def test_cache_blocks_passed_through_regardless_of_capability() -> None:
# 前缀缓存不支持时只是跳过,不改正确性、不算降级。
weak = ScriptedAdapter("weakprov", capabilities_=Capabilities(prefix_cache=False))
gw = Gateway(
{"weakprov": weak},
FakeLedger(),
chain_resolver=chain_resolver(chain(("weakprov", "w"))),
)
req = LlmRequest(
tier="writer",
input="正文",
system=[Block(text="世界观硬规则", cache=True)],
scope=Scope(user_id=uuid.UUID(int=1)),
)
resp = await gw.run(req)
assert resp.text
assert resp.served_by.degraded is False