79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""开发档 Mock 模型服务:OpenAI 兼容 /chat/completions SSE 流式回声。
|
||
|
||
仅供本地联调 Run 流水线(事件序列、芽体渲染、对账),不模拟任何真实模型能力。
|
||
生产部署不启动本服务(与 /v1/webhooks/v1/payments/mock 同一开发档惯例)。
|
||
|
||
启动:uv run python scripts/mock_model.py (127.0.0.1:8001)
|
||
鉴权:任意 Bearer 均接受(联调端点,只监听回环地址)。
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.responses import StreamingResponse
|
||
|
||
app = FastAPI(title="Hunjing Dev Mock Model", docs_url=None)
|
||
|
||
REPLY = (
|
||
"收到了。按当前镜域的状态,我先把这件事拆成两步:先核对读书会的偏好记忆,"
|
||
"再给出一份可归真的结果。这个回复由开发档 Mock 模型逐字流出,"
|
||
"用于验证 content.block.delta 事件序列、临时芽体渲染与 message.completed 对账。"
|
||
)
|
||
|
||
|
||
@app.get("/healthz")
|
||
async def healthz() -> dict[str, str]:
|
||
return {"status": "ok", "env": "development", "mock": "true"}
|
||
|
||
|
||
@app.post("/v1/chat/completions")
|
||
async def chat_completions(request: Request) -> StreamingResponse:
|
||
body = await request.json()
|
||
user_text = ""
|
||
for m in reversed(body.get("messages", [])):
|
||
if m.get("role") == "user" and m.get("content"):
|
||
user_text = str(m["content"])[-60:]
|
||
break
|
||
|
||
text = f"(mock 回应 · 你说:「{user_text}」)\n{REPLY}"
|
||
|
||
async def gen():
|
||
# 逐字流出,模拟真实 TTFT 与节奏
|
||
for ch in text:
|
||
await asyncio.sleep(0.02)
|
||
yield (
|
||
"data: "
|
||
+ json.dumps(
|
||
{"choices": [{"index": 0, "delta": {"content": ch}, "finish_reason": None}]},
|
||
ensure_ascii=False,
|
||
)
|
||
+ "\n\n"
|
||
)
|
||
yield (
|
||
"data: "
|
||
+ json.dumps(
|
||
{"choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
|
||
ensure_ascii=False,
|
||
)
|
||
+ "\n\n"
|
||
)
|
||
yield (
|
||
"data: "
|
||
+ json.dumps(
|
||
{
|
||
"choices": [],
|
||
"usage": {
|
||
"prompt_tokens": 32,
|
||
"completion_tokens": len(text),
|
||
"total_tokens": 32 + len(text),
|
||
},
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
+ "\n\n"
|
||
)
|
||
yield "data: [DONE]\n\n"
|
||
|
||
return StreamingResponse(gen(), media_type="text/event-stream")
|