Skip to content

Commit 4e72cc4

Browse files
committed
feat(provider): 模型推理元数据透传 + 黑名单按实测补全(B1.6)
两边上游模型配置接口实测(2026-09-21)后按证据收窄: 元数据: - Model 增 supports_reasoning / default_effort,_META_FIELDS 一并透传 - CB: supportsReasoning -> supports_reasoning;reasoning.effort -> default_effort (实测取值 high/medium) - TRAE: display_config.model_capability(reasoning_model/chat_model)优先, 缺失回落 reasoning_effort_config.support_thinking;无档位字段 -> default_effort 恒 None - 计划里的 supported_efforts 不实现(上游只给默认档位、不给档位清单,枚举属编造); CB onlyReasoning/canDisableThinking 语义未核实,不透传 - 顺带修 TRAE _to_model 的 display_config 非 dict 崩溃(新测试暴露) 黑名单默认值(只影响列表展示,直连不受影响): - 补 default(CB,HTTP 200 但零内容)、hunyuan-image-*(CB,400 11103 backend not supported)、file_search_agent(TRAE,错误 3003 + 零内容, *sub*agent* 不含 sub 故漏网) - 刻意不加(推翻计划原拟名):*-volc(deepseek-v3-2-volc 实测正常)、 codewise-*/completion-*/*-lkeap(两边清单零命中,无从核实)、 aquila/sagitta/seed-code-pro-0430(TRAE 实测均可正常 chat) 测试:CB/TRAE 元数据解析各分支(含坏数据留空、非 dict 回落)、 /v1/models 透传与逐字段补缺、黑名单新增模式生效。1089 passed,行/分支 100%。
1 parent 398c6d8 commit 4e72cc4

10 files changed

Lines changed: 204 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ curl http://127.0.0.1:8000/v1/user/balance -H "Authorization: Bearer sk-你的ke
127127
| `GROWTH_IRREVERSIBLE_ACTIONS` | `true` | 是否允许成长中心的不可逆动作:抽奖、连登兑换、开 Buddy 盲盒、消耗补登卡。`false` 时仍会领取旅行礼物与任务奖励 |
128128
| `QUOTA_EXPIRY_WINDOW_SECONDS` | `129600` | 到期排序窗口:把距到期 ≤ 该秒数的积分加总,多的账号先用(避免积分过期浪费);`≤0` 关闭,退回纯健康度排序 |
129129
| `CONVERSATION_STICKY_SECONDS` | `3600` | 会话粘性 TTL:优先按请求体显式会话标识(`conversation_id`/`conversationId`/`prompt_cache_key`,metadata 或顶层),无则回落消息前缀指纹,多轮请求固定用同一凭证(手动 pin 的凭证优先,粘性让位);带 `user_id` 时不派生前缀兜底键(避免并行对话误钉同一号);凭证出错仍会轮换,成功后重新粘定;`≤0` 关闭 |
130-
| `MODEL_BLOCKLIST` | `custom_model_*,*sub*agent*,summary,browser_use_*` | 模型列表黑名单(仅影响列表展示) |
130+
| `MODEL_BLOCKLIST` | `custom_model_*,*sub*agent*,summary,browser_use_*,file_search_agent,default,hunyuan-image-*` | 模型列表黑名单(fnmatch,仅影响列表展示,直连指定不受影响);默认值按两边上游实测清单补入内部/不可用模型(`default` 零内容、`hunyuan-image-*` 400 11103),刻意不含 `*-volc` 与 `aquila`/`sagitta`/`seed-code-pro-0430`(实测可正常 chat)(见 TECHNICAL.md §3.5) |
131131
| `ALLOWED_HOSTS` | 空 | Host 白名单,防 DNS rebinding |
132132
| `CODEBUDDY_API_ENDPOINT` | `https://copilot.tencent.com` | CodeBuddy 上游地址;改动时必须同时把它加入 `CODEBUDDY_ALLOWED_ENDPOINTS` |
133133
| `CODEBUDDY_ALLOWED_ENDPOINTS` | 官方两站(见 compose) | 上游端点白名单,真实 Token 只发往白名单内地址 |

‎TECHNICAL.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,46 @@ reasoning)+ 续写指令」重发,并把输出上限两键归零(否则在
251251
参考实现(IceeAn/codebuddy2api)只**统计** `finish_reason`,**不实现**续写,
252252
故本项无照搬蓝本,全部依据上述直连实测。
253253

254+
### 3.5 会话粘性键(B1.5)与模型元数据/黑名单(B1.6)
255+
256+
**会话粘性键**(`src/engine/affinity.py`,详见 [PROPOSAL.md 会话粘性](PROPOSAL.md)):
257+
键优先级 `conversation_id` > `conversationId` > `prompt_cache_key`(同键名先
258+
`metadata` 对象再请求体顶层)→ 无显式标识时回落「用户名 + 消息增量前缀指纹」;
259+
请求体带 `user_id`(顶层或 `metadata` 内)时不派生兜底键。键名核实状态:
260+
`prompt_cache_key` 是 OpenAI 官方顶层参数、`metadata.user_id` 是 Anthropic
261+
Messages API 官方字段(均已核实);`conversation_id`/`conversationId`/顶层
262+
`user_id` 非两家标准键,本机 71 份真实请求 dump(PI 客户端,顶层键仅
263+
model/messages/tools/stream/stream_options/store/reasoning_effort)**未观测到**,
264+
作为网关兼容探测接受(命中即用、未命中无害)。
265+
266+
**模型元数据**(B1.6,2026-09-21 两边上游模型配置接口实测):
267+
两边上游都直接给出推理元数据,网关透传为 `/v1/models` 的 OpenAI 额外字段:
268+
269+
| 字段 | CB 来源 | TRAE 来源 | 实测取值 |
270+
|---|---|---|---|
271+
| `supports_reasoning` | `supportsReasoning` | `display_config.model_capability`(`reasoning_model`→true、`chat_model`→false),缺失回落 `reasoning_effort_config.support_thinking` | true / false / null |
272+
| `default_effort` | `reasoning.effort` | 无对应字段(`reasoning_effort_config` 只有 `support_thinking` 布尔) | CB: `high` / `medium`,TRAE 恒为 null |
273+
274+
已批准计划里另一字段 `supported_efforts`(可接受档位集合)**不实现**:上游只给
275+
「该模型的默认档位」,不给档位清单,凭空枚举 ChatGPT 三档属编造(与 §3.4 同一
276+
取舍原则)。CB 的 `onlyReasoning`/`canDisableThinking` 同样**不透传**——语义未经
277+
核实。逐字段补缺语义不变(双上游同名模型先到先填、后到只补 None)。
278+
279+
**黑名单默认值**(`MODEL_BLOCKLIST`,fnmatch glob,只影响列表展示、直连不受影响):
280+
在原有 `custom_model_*` / `*sub*agent*` / `summary` / `browser_use_*` 之外,按实测
281+
补入三类**确认不可用**的上游噪音模型:
282+
283+
| 模式 | 命中实例 | 实测结论 |
284+
|---|---|---|
285+
| `default` | CB `default` | HTTP 200 但**零内容**(不可用于 chat) |
286+
| `hunyuan-image-*` | CB `hunyuan-image-alpha`、`-edit` | HTTP 400 `11103`「Backend [hunyuan-stream] is not supported」 |
287+
| `file_search_agent` | TRAE `file_search_agent` | HTTP 200 但错误 `3003`「model service is unavailable」+ 零内容(`*sub*agent*` 不含 "sub" 故漏网) |
288+
289+
**刻意不加**(推翻了计划原拟的噪音名,全部直连实测):
290+
`*-volc`(`deepseek-v3-2-volc` 实测正常 chat)、`codewise-*` / `completion-*` /
291+
`*-lkeap`(两边清单零命中,无从核实)、`aquila` / `sagitta` /
292+
`seed-code-pro-0430`(TRAE 实测均正常 chat,虽名字可疑但可用)。
293+
254294
---
255295

256296
## 4. Provider 协议(Q16=A 细接口)

‎docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ services:
1414
CODEBUDDY_API_ENDPOINT: ${CODEBUDDY_API_ENDPOINT:-https://copilot.tencent.com}
1515
DEFAULT_MODEL: ${DEFAULT_MODEL:-glm-5.2}
1616
# 模型列表黑名单(fnmatch glob,逗号分隔,完全替换语义)
17-
MODEL_BLOCKLIST: ${MODEL_BLOCKLIST:-custom_model_*,*sub*agent*,summary,browser_use_*}
17+
MODEL_BLOCKLIST: ${MODEL_BLOCKLIST:-custom_model_*,*sub*agent*,summary,browser_use_*,file_search_agent,default,hunyuan-image-*}
1818
QUOTA_PROBE_MINUTES: ${QUOTA_PROBE_MINUTES:-60}
1919
# 成长中心(仅 CodeBuddy):一轮领取周期,以及是否允许不可逆动作
2020
# (抽奖/连登兑换/开 Buddy 盲盒/消耗补登卡)

‎src/api/models.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929

3030
# 响应透传的元数据字段(Model → OpenAI 额外字段)
3131
_META_FIELDS = ("credit_rate", "max_input_tokens", "max_output_tokens",
32-
"supports_images", "supports_tool_call")
32+
"supports_images", "supports_tool_call",
33+
"supports_reasoning", "default_effort")
3334

3435

3536
def _blocked(model_id: str, patterns: tuple[str, ...]) -> bool:

‎src/config.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,17 @@ class Settings(BaseSettings):
6666
# 模型列表黑名单(fnmatch glob,逗号分隔):滤掉非用户模型与老模型,
6767
# 只影响 /v1/models 与 playground 列表,直连指定不受影响。
6868
# 覆盖此值时为完全替换(含默认噪音规则),增删请重写全量。
69-
model_blocklist: str = "custom_model_*,*sub*agent*,summary,browser_use_*"
69+
# B1.6 实测(2026-09-21,两边上游真实清单)补入的内部/不可用模型:
70+
# custom_model_* / *sub*agent* / summary / browser_use_* / file_search_agent
71+
# 为上游内部或代理模型(chat 报 3003 / 非用户模型)
72+
# default 实测 HTTP 200 但零内容(不可用于 chat)
73+
# hunyuan-image-* 实测 HTTP 400 11103「backend is not supported」
74+
# 刻意不加的:*-volc(deepseek-v3-2-volc 实测正常 chat)、aquila/sagitta/
75+
# seed-code-pro-0430(TRAE 实测均正常 chat)
76+
model_blocklist: str = (
77+
"custom_model_*,*sub*agent*,summary,browser_use_*,file_search_agent,"
78+
"default,hunyuan-image-*"
79+
)
7080
# 诊断:把 /v1 入口的原始请求体落到 data/dumps/(排查客户端差异用)
7181
dump_request_bodies: bool = False
7282
# 截断续写(B1.4):上游以 finish_reason=length 截断时,同凭证自动续写,

‎src/provider/base.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ class Model:
241241
max_output_tokens: int | None = None
242242
supports_images: bool | None = None
243243
supports_tool_call: bool | None = None
244+
# 推理元数据(B1.6):两边上游的模型配置接口都直接给出,有则透传
245+
supports_reasoning: bool | None = None
246+
# 上游为该模型配置的默认档位(CB: reasoning.effort ∈ {high, medium};
247+
# TRAE 的 reasoning_effort_config 只有 support_thinking 布尔,无档位,为 None)
248+
default_effort: str | None = None
244249

245250

246251
@dataclass(slots=True)

‎src/provider/codebuddy/client.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ async def fetch_models(self, credential: CodeBuddyCredential) -> list[Model]:
349349
max_output_tokens=_int_field(item.get("maxOutputTokens")),
350350
supports_images=_bool_field(item.get("supportsImages")),
351351
supports_tool_call=_bool_field(item.get("supportsToolCall")),
352+
supports_reasoning=_bool_field(item.get("supportsReasoning")),
353+
default_effort=_effort_field(item.get("reasoning")),
352354
))
353355
if not models:
354356
raise UpstreamProtocolViolation("config returned no valid model ids")
@@ -394,6 +396,18 @@ def _bool_field(value: object) -> bool | None:
394396
return value if isinstance(value, bool) else None
395397

396398

399+
def _effort_field(value: object) -> str | None:
400+
"""CB 模型条目的 `reasoning` 对象 → 默认档位(实测只有 effort 字段)。
401+
402+
上游给 `{"effort": "high"|"medium", "summary": "auto"}`;缺失或类型不符
403+
返回 None(透传 None 比编造档位诚实)。
404+
"""
405+
if not isinstance(value, dict):
406+
return None
407+
effort = value.get("effort")
408+
return effort if isinstance(effort, str) and effort else None
409+
410+
397411
def _extract_accounts(body: dict[str, Any]) -> list[Any]:
398412
"""从上游响应里取出账户列表。
399413

‎src/provider/trae/client.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ def _to_model(config: dict) -> Model | None:
350350
倍率在 display_contact_config(JSON 字符串)里:
351351
consumption_rate.enable 且有 data.rate 时才可信。
352352
"""
353-
display = config.get("display_config") or {}
353+
display = config.get("display_config")
354+
if not isinstance(display, dict):
355+
display = {}
354356
credit_rate: float | None = None
355357
contact = config.get("display_contact_config")
356358
if isinstance(contact, str) and contact:
@@ -372,6 +374,10 @@ def _to_model(config: dict) -> Model | None:
372374
name=str(display.get("display_name") or ""),
373375
credit_rate=credit_rate,
374376
max_input_tokens=max_input,
377+
supports_reasoning=_trae_supports_reasoning(config),
378+
# TRAE 的 reasoning_effort_config 只有 support_thinking 布尔,
379+
# 无档位信息 → default_effort 保持 None(透传 None 优于编造)
380+
default_effort=None,
375381
)
376382

377383
async def fetch_quota(self, credential: TraeCredential) -> Quota:
@@ -479,6 +485,23 @@ async def _post_json(self, url: str, body: dict[str, Any],
479485
return data
480486

481487

488+
def _trae_supports_reasoning(config: dict[str, Any]) -> bool | None:
489+
"""TRAE 侧推理能力判定(B1.6,实测两处上游字段)。
490+
491+
优先 `display_config.model_capability == "reasoning_model"`;缺失时回落
492+
`reasoning_effort_config.support_thinking`。两者都不可信时返回 None。
493+
"""
494+
display = config.get("display_config")
495+
if isinstance(display, dict):
496+
capability = display.get("model_capability")
497+
if isinstance(capability, str) and capability:
498+
return capability == "reasoning_model"
499+
effort = config.get("reasoning_effort_config")
500+
if isinstance(effort, dict) and isinstance(effort.get("support_thinking"), bool):
501+
return effort["support_thinking"]
502+
return None
503+
504+
482505
def _normalize_epoch(value: int) -> int:
483506
"""上游可能返回毫秒;>1e12 视为毫秒。"""
484507
return value // 1000 if value > 1_000_000_000_000 else value

‎tests/test_m1a_trae.py‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -540,11 +540,12 @@ def handler(request: httpx.Request) -> httpx.Response:
540540

541541

542542
async def test_client_fetch_models_metadata():
543-
"""倍率(display_contact_config.consumption_rate)与上下文窗口透传。"""
543+
"""倍率 / 上下文窗口 / 推理能力透传。"""
544544
def handler(_request: httpx.Request) -> httpx.Response:
545545
return httpx.Response(200, json={"config_info_list": [
546546
{"config_name": "Doubao-Seed-Evolving",
547-
"display_config": {"display_name": "Seed-Evolving"},
547+
"display_config": {"display_name": "Seed-Evolving",
548+
"model_capability": "reasoning_model"},
548549
"context_window_tokens": {"dev": 256000},
549550
"display_contact_config": json.dumps({
550551
"consumption_rate": {"enable": True, "data": {"rate": 0.08}}})},
@@ -555,18 +556,50 @@ def handler(_request: httpx.Request) -> httpx.Response:
555556
{"config_name": "kimi-k3",
556557
"display_config": {"display_name": "K3"},
557558
"display_contact_config": json.dumps({
558-
"consumption_rate": {"enable": True, "data": {"rate": "oops"}}})},
559+
"consumption_rate": {"enable": True, "data": {"rate": "oops"}}}),
560+
"reasoning_effort_config": {"support_thinking": False}},
559561
]})
560562

561563
models = await _client(handler).fetch_models(TraeCredential(access_token="a"))
562564
assert models[0].credit_rate == 0.08
563565
assert models[0].max_input_tokens == 256000
566+
assert models[0].supports_reasoning is True
567+
# TRAE 无档位清单可核实 → 不编造默认档位
568+
assert models[0].default_effort is None
564569
# 坏数据 → 字段留空,不影响条目
565570
assert models[1].credit_rate is None and models[1].max_input_tokens is None
571+
# 能力字段缺失 → 回落 reasoning_effort_config.support_thinking
572+
assert models[1].supports_reasoning is None
573+
assert models[2].supports_reasoning is False
566574
# rate 非数值同样留空
567575
assert models[2].credit_rate is None
568576

569577

578+
async def test_trae_supports_reasoning_branches():
579+
"""能力判定的优先级与回落:capability 优先,其次 support_thinking。"""
580+
def handler(_request: httpx.Request) -> httpx.Response:
581+
return httpx.Response(200, json={"config_info_list": [
582+
# capability 非空字符串 → 不以 support_thinking 为准
583+
{"config_name": "c1", "display_config": {"model_capability": "chat_model"},
584+
"reasoning_effort_config": {"support_thinking": True}},
585+
# capability 为空串 → 掉到 support_thinking
586+
{"config_name": "c2", "display_config": {"model_capability": ""},
587+
"reasoning_effort_config": {"support_thinking": True}},
588+
# display_config 非 dict → 掉到 support_thinking
589+
{"config_name": "c3", "display_config": "oops",
590+
"reasoning_effort_config": {"support_thinking": True}},
591+
# support_thinking 非布尔 → None
592+
{"config_name": "c4", "reasoning_effort_config": {"support_thinking": "yes"}},
593+
# reasoning_effort_config 非 dict → None
594+
{"config_name": "c5", "reasoning_effort_config": ["oops"]},
595+
# 全部缺失 → None
596+
{"config_name": "c6"},
597+
]})
598+
599+
models = await _client(handler).fetch_models(TraeCredential(access_token="a"))
600+
assert [m.supports_reasoning for m in models] == [False, True, True, None, None, None]
601+
602+
570603
@pytest.mark.parametrize("payload", [
571604
{}, {"config_info_list": "no"}, {"config_info_list": []},
572605
])

0 commit comments

Comments
 (0)