Skip to content

Commit 5f2a317

Browse files
committed
fix: CodeBuddy body 归一 developer 角色(PI 11128 的最终根因)
新增请求体 dump 诊断开关(DUMP_REQUEST_BODIES=1)后拿到 PI 真实 请求体,逐字段二分定位: - PI 对 reasoning 模型把 system 消息转成 OpenAI 的 role: "developer"(convertMessages: useDeveloperRole) - 腾讯 CB 后端不认 developer 角色,直接判 11128 渠道风控 - 实测:原样 body=11128;仅把 developer 改回 system=200; 去掉 reasoning_effort/store/max_completion_tokens/stream_options/ tools.strict 均仍 11128(全部排除) 修复:CB stream_chat 归一 messages 中 developer → system。 端到端验证:PI 原始 body(含 developer、tool 历史、7 个工具) 经服务请求 → 200 正常流式回复。后端 640 测试 / 覆盖 100%。
1 parent 0143e79 commit 5f2a317

5 files changed

Lines changed: 72 additions & 0 deletions

File tree

‎src/config.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ class Settings(BaseSettings):
4343
# CodeBuddy 聊天最小间隔:腾讯频率风控(11128)在连续快速请求时触发,
4444
# 实测 ≥5s 间隔稳定避开;0 关闭节流
4545
codebuddy_chat_min_interval: float = 5
46+
# 诊断:把 /v1 入口的原始请求体落到 data/dumps/(排查客户端差异用)
47+
dump_request_bodies: bool = False
4648

4749
@cached_property
4850
def admin_set(self) -> frozenset[str]:

‎src/main.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import json
67
import logging
78
import time
89
from contextlib import asynccontextmanager
@@ -286,6 +287,11 @@ async def health():
286287
@app.post("/v1/chat/completions")
287288
async def chat_completions(request: Request, user: str = Depends(api_key_user)):
288289
body = await request.json()
290+
if config.dump_request_bodies:
291+
dump_dir = Path(config.data_dir) / "dumps"
292+
dump_dir.mkdir(parents=True, exist_ok=True)
293+
(dump_dir / f"{int(time.time() * 1000)}.json").write_text(
294+
json.dumps(body, ensure_ascii=False, indent=1), encoding="utf-8")
289295
chat_request = parse_chat_request(body)
290296
if chat_request.stream:
291297
return StreamingResponse(executor.stream(chat_request, username=user),

‎src/provider/codebuddy/client.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ async def stream_chat(self, credential: CodeBuddyCredential, payload: dict[str,
8787
body = dict(payload)
8888
body["model"] = model
8989
body["stream"] = True
90+
# PI 等客户端对 reasoning 模型会把 system 转成 OpenAI 的 developer
91+
# 角色;腾讯后端不认 developer,实测直接判 11128 渠道风控 → 归一为 system
92+
messages = body.get("messages")
93+
if isinstance(messages, list):
94+
for message in messages:
95+
if isinstance(message, dict) and message.get("role") == "developer":
96+
message["role"] = "system"
9097
# 官方 CLI 请求的标准特征字段;缺失会被渠道风控判定非官方调用(11128)
9198
body.setdefault("enable_thinking", True)
9299
stream_options = body.get("stream_options")

‎tests/test_m15_operations.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2474,3 +2474,40 @@ def test_authorize_without_pending_login_still_rejected(tmp_path):
24742474
response = client.get("/authorize?refreshToken=RT")
24752475
assert response.status_code == 400
24762476
assert response.json()["error"]["code"] == "invalid_request"
2477+
2478+
2479+
def test_dump_request_bodies_writes_file(tmp_path):
2480+
"""诊断开关:dump_request_bodies=True 时 /v1 请求体落盘(main 291-293)。"""
2481+
import json as _json
2482+
2483+
from fastapi.testclient import TestClient
2484+
2485+
from src.config import Settings
2486+
from src.main import build_app
2487+
2488+
settings = Settings(_env_file=None, APP_SECRET="s", DATA_DIR=str(tmp_path),
2489+
dump_request_bodies=True)
2490+
2491+
class P:
2492+
id = "p1"
2493+
2494+
async def list_models(self, _data):
2495+
from src.provider.base import Model
2496+
2497+
return [Model(id="m")]
2498+
2499+
def import_credential(self, raw): # pragma: no cover
2500+
return raw
2501+
2502+
app = build_app(settings, providers={"p1": P()})
2503+
key = app.state.api_keys.create("root")["api_key"]
2504+
with TestClient(app) as client:
2505+
r = client.post("/v1/chat/completions",
2506+
headers={"Authorization": f"Bearer {key}"},
2507+
json={"model": "m", "stream": False,
2508+
"messages": [{"role": "user", "content": "hi"}]})
2509+
assert r.status_code in (200, 400, 503) # body 是否合法不影响 dump
2510+
dumps = list((tmp_path / "dumps").glob("*.json"))
2511+
assert dumps, "应产生 dump 文件"
2512+
body = _json.loads(dumps[0].read_text(encoding="utf-8"))
2513+
assert body["model"] == "m"

‎tests/test_m1b_codebuddy.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,3 +1230,23 @@ async def stream_chat(self, _cred, _payload, _model):
12301230
rows = [tuple(r) for r in db.connect().execute(
12311231
"SELECT err_count, cooling_until FROM credentials ORDER BY provider")]
12321232
assert rows == [(0, None), (0, None)] # TRAE 4001 不冷却
1233+
1234+
1235+
async def test_stream_chat_normalizes_developer_role():
1236+
"""PI 的 developer 角色归一为 system(腾讯实测 developer → 11128)。"""
1237+
import json as _json
1238+
1239+
captured: dict = {}
1240+
1241+
def capture_handler(request: httpx.Request) -> httpx.Response:
1242+
captured["body"] = _json.loads(request.read())
1243+
return httpx.Response(200, text=fixture("chat-basic.sse"))
1244+
1245+
provider = CodeBuddyProvider(client=_client(capture_handler))
1246+
payload = {"messages": [
1247+
{"role": "developer", "content": "You are PI."},
1248+
{"role": "user", "content": "hi"},
1249+
]}
1250+
_ = [e async for e in provider.stream_chat({"bearer_token": "t"}, payload, "m")]
1251+
roles = [m["role"] for m in captured["body"]["messages"]]
1252+
assert roles == ["system", "user"]

0 commit comments

Comments
 (0)