✨ 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

+3 -35
View File
@@ -24,7 +24,6 @@ from app.models import (
Message,
OutboxEvent,
Run,
RunEvent,
)
MESSAGE_ROUTE = "/v1/conversations/{conversation_id}/messages"
@@ -157,20 +156,19 @@ async def replay_or_execute_message(
run = None
if body.start_run:
# 垂直切片空状态闭环:不调用模型,Run 原子创建并直接进入终态 completed。
# Run 创建为 queued,由网关执行器在后台推进至终态(WP-05)
run = Run(
personal_domain_id=domain_id,
conversation_id=conversation.id,
status="completed",
completed_at=datetime.now(UTC),
status="queued",
)
db.add(run)
await db.flush()
message.run_id = run.id
resource_ids: dict[str, Any] = {"message_id": message.id}
if run is not None:
resource_ids["run_id"] = run.id
_record_run_events(db, run, message, blocks)
db.add(
AuditEvent(
actor_type="user",
@@ -203,36 +201,6 @@ async def replay_or_execute_message(
return message, run, False
def _record_run_events(
db: AsyncSession, run: Run, message: Message, blocks: list[dict[str, Any]]
) -> None:
"""空状态闭环的事件流:状态事件 + 每 block completed + message.completed。
sequence 在 Run 内从 0 单调;事件与权威状态同事务提交(契约第 5 节)。
delta/progress 事件在模型流式接入(WP-05)后由真实流产生,此处不伪造。
"""
seq = 0
events: list[tuple[str, dict[str, Any]]] = [
("run.status.changed", {"run_id": run.id, "status": run.status})
]
for index, block in enumerate(blocks):
events.append(
(
"content.block.completed",
{"run_id": run.id, "message_id": message.id, "index": index,
"block": {"id": block["id"], "type": block["type"], "status": "completed"}},
)
)
events.append(
(
"message.completed",
{"run_id": run.id, "message_ids": [message.id], "final_status": run.status},
)
)
for event_type, data in events:
db.add(RunEvent(run_id=run.id, sequence=seq, type=event_type, data=data))
seq += 1
async def list_messages(
db: AsyncSession,