feat: M1 — 立项→写章草稿(SSE)→自动保存;连一家 provider
- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由 - 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本) - LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error) - API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接) - 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页 - M1 E2E:真实 DB + mock 网关零 token 走通闭环
This commit is contained in:
56
packages/llm_gateway/tests/fakes.py
Normal file
56
packages/llm_gateway/tests/fakes.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""网关单测替身(mock provider + 内存 ledger)——不碰真实 API / DB。
|
||||
|
||||
放在独立模块(非 conftest)以便测试用绝对导入 `from fakes import ...`;
|
||||
本目录无 __init__.py(避免与顶层 tests 包同名冲突),故走 pytest 路径注入。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from ww_llm_gateway.adapters.base import (
|
||||
Capabilities,
|
||||
ProviderResult,
|
||||
ProviderUsage,
|
||||
StreamChunk,
|
||||
)
|
||||
from ww_llm_gateway.routing import Route
|
||||
from ww_llm_gateway.types import LlmRequest, Scope, Tier, Usage
|
||||
|
||||
|
||||
class FakeAdapter:
|
||||
provider = "deepseek"
|
||||
|
||||
def __init__(self, text: str = "hello world", deltas: list[str] | None = None) -> None:
|
||||
self.text = text
|
||||
self.deltas = deltas if deltas is not None else ["hel", "lo ", "world"]
|
||||
self.complete_calls = 0
|
||||
self.stream_calls = 0
|
||||
|
||||
def capabilities(self) -> Capabilities:
|
||||
return Capabilities(structured_output=True, prefix_cache=True)
|
||||
|
||||
async def complete(self, req: LlmRequest, model: str) -> ProviderResult:
|
||||
self.complete_calls += 1
|
||||
return ProviderResult(
|
||||
text=self.text,
|
||||
usage=ProviderUsage(input_tokens=100, output_tokens=50),
|
||||
)
|
||||
|
||||
async def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]:
|
||||
self.stream_calls += 1
|
||||
for d in self.deltas:
|
||||
yield StreamChunk(text=d)
|
||||
yield StreamChunk(usage=ProviderUsage(input_tokens=100, output_tokens=50))
|
||||
|
||||
|
||||
class FakeLedger:
|
||||
def __init__(self) -> None:
|
||||
self.records: list[Usage] = []
|
||||
|
||||
async def record(self, scope: Scope, usage: Usage) -> None:
|
||||
self.records.append(usage)
|
||||
|
||||
|
||||
def fake_route(tier: Tier) -> Route:
|
||||
return Route(provider="deepseek", model="deepseek-chat")
|
||||
Reference in New Issue
Block a user