✨ feat: WP-05 模型网关核心——逻辑模型、流式适配与 Run 真实执行

- 迁移 7+8:logical_model(能力目录)、provider_endpoint(密钥只存环境变量名,永不落库)、turn、model_call(每 attempt 单独持久化:usage/TTFT/错误,Provider 未返回保持 null 不估算——ADR-003);message.run_id 关联
- OpenAI-compatible 流式适配器:httpx SSE 解析、标准化 StreamChunk(delta/usage/done)、transport 可注入(测试与故障注入);密钥经环境变量读取不进日志
- Run 执行器:后台执行完整生命周期——running → content.block.started → 真实 content.block.delta*(流式产生,delta_index 单调)→ content.block.completed → message.completed → 终态;assistant 消息为权威快照;ModelCall 记录 usage 与 TTFT;无可用端点时 Run 显式失败并留 error 事件(fail-closed)
- post_message:响应提交后由框架 BackgroundTasks 执行 Run(显式预提交保证 Run 落库);SSE 改为增量轮询(重放 + 等待新事件至终态,30s 上限)
- 测试 54 项全绿(+2):端到端 mock 流验证 ModelCall usage/TTFT/assistant 权威消息、无端点 Run failed + content.block.failed 事件
This commit is contained in:
chenxin committed 2026-09-11 09:44:54 +08:00
1 parent 40221170ae
commit b4242f3844
10 files changed
+789 -56

No files matched your search

+98
View File
@@ -207,6 +207,9 @@ class Message(Base):
ForeignKey("message.id", ondelete="set null"), nullable=True
)
variant_kind: Mapped[str] = mapped_column(String, nullable=False, default="origin")
run_id: Mapped[str | None] = mapped_column(
ForeignKey("run.id", ondelete="set null"), nullable=True, index=True
)
content: Mapped[list[dict[str, Any]]] = mapped_column(JSONB, nullable=False)
parser_version: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
@@ -374,3 +377,98 @@ class Character(Base):
name="origin_values",
),
)
class LogicalModel(Base):
"""逻辑模型:业务只引用能力与逻辑名,不感知 Provider(契约第 5 节分面)。"""
__tablename__ = "logical_model"
id: Mapped[str] = mapped_column(String, primary_key=True) # kebab-case,如 default-chat
capabilities: Mapped[list[str]] = mapped_column(JSONB, nullable=False, default=list)
description: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class ProviderEndpoint(Base):
"""Provider 端点:密钥只存环境变量名,永不落库明文。"""
__tablename__ = "provider_endpoint"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("endpoint_")
)
logical_model_id: Mapped[str] = mapped_column(
ForeignKey("logical_model.id", ondelete="cascade"), nullable=False, index=True
)
provider: Mapped[str] = mapped_column(String, nullable=False)
name: Mapped[str] = mapped_column(String, nullable=False)
base_url: Mapped[str] = mapped_column(String, nullable=False)
model_id: Mapped[str] = mapped_column(String, nullable=False)
api_key_env: Mapped[str] = mapped_column(String, nullable=False)
priority: Mapped[int] = mapped_column(nullable=False, default=100)
enabled: Mapped[bool] = mapped_column(nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
CheckConstraint(
"provider IN ('openai_compatible', 'anthropic')", name="provider_values"
),
)
class Turn(Base):
"""Turn:Run 内一次模型决策及其工具交互阶段。"""
__tablename__ = "turn"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: new_id("turn_"))
run_id: Mapped[str] = mapped_column(
ForeignKey("run.id", ondelete="cascade"), nullable=False, index=True
)
index: Mapped[int] = mapped_column(nullable=False, default=0)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
Index("uq_turn_run_index", "run_id", "index", unique=True),
)
class ModelCall(Base):
"""每次上游 attempt 单独持久化:重试/故障转移不覆盖(ADR-003)。
Provider 未返回的 usage 字段保持 null,不估算伪装。
"""
__tablename__ = "model_call"
id: Mapped[str] = mapped_column(
String, primary_key=True, default=lambda: new_id("model_call_")
)
turn_id: Mapped[str] = mapped_column(
ForeignKey("turn.id", ondelete="cascade"), nullable=False, index=True
)
endpoint_id: Mapped[str | None] = mapped_column(
ForeignKey("provider_endpoint.id", ondelete="set null"), nullable=True
)
attempt: Mapped[int] = mapped_column(nullable=False, default=1)
status: Mapped[str] = mapped_column(String, nullable=False, default="pending")
request_summary: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
usage: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
ttft_ms: Mapped[int | None] = mapped_column(nullable=True)
error: Mapped[str | None] = mapped_column(String, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
__table_args__ = (
CheckConstraint(
"status IN ('pending', 'streaming', 'succeeded', 'failed')", name="status_values"
),
Index("uq_model_call_turn_attempt", "turn_id", "attempt", unique=True),
)