fix(core): strip reasoning_content from Cerebras requests - #11049
Conversation
Cerebras' OpenAI-compatible endpoint rejects the non-standard `messages[].reasoning_content` field on input with HTTP 400 (`wrong_api_format`). `determineProvider()` has no Cerebras branch, so `api.cerebras.ai` falls through to the default provider, which ships the field verbatim — every multi-turn request that replays a thinking turn fails with `400 status code (no body)`. Add a hostname-detected Cerebras provider subclass that removes `reasoning_content` at the outbound request boundary only, matching the existing Mistral handling. Session history stays intact, so DeepSeek/Qwen thinking-mode replay (which requires the field) is unaffected. The strip helper moves from `mistral.ts` to the shared `provider/utils.ts` unchanged. Detection is hostname-only: Cerebras serves third-party model names (qwen-3.8-27b, gpt-oss-120b, llama-*), so a model-name fallback would misroute other providers' models. Deleting the field rather than renaming it to Cerebras' accepted `reasoning` input field matches the proven Mistral pattern; the trade-off (no cross-turn thinking replay) is the same already-accepted behavior Mistral has. Fixes #11045 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Patrol-Run: qwen-issue-patrol/jmtnduz2xjf
|
|
|
Thanks for the PR — this one is easy to say yes to at the gate level. Template looks good ✓ — every required heading is filled in, including the Risk & Scope tradeoff and the Chinese translation, and the prose is unwrapped the way the template asks. Problem: observed, not theoretical. #11045 is a P1 report with a two- Direction: aligned, and the CHANGELOG backs it. Size: core paths, so counting per the gate rules — 96 production lines ( Approach: scope feels right and I could not find a smaller version of it. The one edit that looks like drive-by refactoring — lifting I also checked the thing that would actually break this rather than the thing that looks risky: whether an earlier Risk: Moving on to code review. 🔍 中文说明感谢贡献!这个 PR 在准入层面很容易通过。 模板完整 ✓ —— 所有必需小节都填写了,包括 Risk & Scope 的权衡说明和中文翻译,正文也没有硬折行。 问题:是已观测到的 bug,不是理论性加固。#11045 是一个 P1 报告,带两条 方向:对齐,而且 CHANGELOG 支持这个判断。 规模:触及核心路径,按门禁规则统计 —— 生产代码 96 行( 方案:范围合理,我找不到更小的实现版本。唯一看起来像顺手重构的改动 —— 把 我还检查了真正会让这个改动失效的地方,而不是看起来危险的地方: 风险: 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewWhat I would have done from the title and the "Why it's needed" alone, before opening the diff: the converter writes a non-standard field into shared history, one endpoint rejects it, and the codebase already solved exactly this for Mistral. So — add a The diff is that, and both judgement calls are right. Hostname-only, because Cerebras serves other vendors' model names, so a model fallback would misroute real DeepSeek/GLM/MiMo configs. Delete rather than rename, because that is the proven Mistral shape and #11045 itself recommends it as provider-safe. No blocking findings. Three things I verified independently, because they are the ways a boundary fix like this silently half-works: The strip covers both wire paths. History really is untouched. Nothing upstream can steal the route. I walked every earlier branch in Two non-blocking observations: 1. For 2. The "Before" output cannot be a plain For completeness rather than as a request: the The test is stronger than it needed to be, which is the main reason I am comfortable here. The multi-turn case stands up a real HTTP endpoint that 400s on any body containing sequenceDiagram
participant P1 as Session history
participant P2 as OpenAIContentConverter
participant P3 as ContentGenerationPipeline
participant P4 as Cerebras provider buildRequest
participant P5 as api.cerebras.ai endpoint
P1->>P2: contents with thought parts
P2->>P3: assistant message carrying reasoning_content
P3->>P4: buildRequest(baseRequest, userPromptId)
Note over P4: super mirrors to reasoning for qwen3 names, then strip runs
P4->>P4: messages.map(stripReasoningContent)
P4->>P5: wire body without reasoning_content
P5-->>P3: 200 instead of 400 wrong_api_format
Files changed (6)
TestingWhich evidence this carries: the PR's own CI, read through the API. This is an unattended CI run, so I did not build, run, or execute anything from this branch — no test output below is mine. Real check names and conclusions for Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Nothing is red. The three checks that would actually exercise this change — the ubuntu unit suite, lint/static, and the no-AK integration run — were all still in flight at the fetch, so there is no test conclusion to report yet and I am not guessing one. Pre-existing base failure, so a red head check is not automatically this PR's. Base Those two legs are skipped on PR runs in this repo and only execute post-merge, which is why they show Not verified, with reasons:
Sandboxed verification would settle the first gap: 中文说明代码审查只看标题和「为什么需要」时我会怎么做(在看 diff 之前): 转换器把一个非标准字段写进了共享历史,某个端点拒绝它,而代码库里已经为 Mistral 解决过完全相同的问题。所以——新增一个继承 diff 正是如此,且两个判断都对。只按 hostname,是因为 Cerebras 托管其他厂商的模型名,加模型名兜底会误路由真正的 DeepSeek/GLM/MiMo 配置。删除而非改名,因为这是 Mistral 已验证过的形态,#11045 本身也推荐这种做法作为 provider-safe 方案。 没有阻塞性问题。有三点我独立验证过,因为这类边界修复正是容易在这几处悄悄失效: strip 覆盖了全部出站路径。 会话历史确实没有被改写。 上游分支不会抢走路由。 我逐个看了 两条非阻塞的观察: 1. 对 2. "Before" 的输出不可能来自纯 另外仅作说明、并非要求:加到 测试本身写得比需要的更扎实,这也是我在这里放心的主要原因:多轮用例启动了一个真实 HTTP 端点,凡请求体含 测试证据本评论携带的证据类型:PR 自身的 CI,通过 API 读取。 这是无人值守的 CI 运行,我没有构建、运行或执行本分支的任何代码——下面没有任何测试出自我手。以上是 没有任何 check 是红的。但真正能检验这个改动的三项——ubuntu 单元测试、lint/static、no-AK 集成测试——在抓取时都还在运行,所以目前没有测试结论可报,我也不会去猜。 base 上已有既存失败,所以 head 变红不必然归因于本 PR。 base 未验证项及原因:
沙箱验证可以补齐第一个缺口: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — solid, minimal, and it fixes a P1 that makes an entire provider unusable past turn one; the two nits are about the description's precision, not the code. My independent proposal, written from the title and the motivation before I opened the diff, was the same shape the PR took: a provider subclass that strips after The thing I would want a maintainer to know is that I did not take the framing on trust. I confirmed the converter writes Two reservations, both non-blocking, both in the description rather than the diff: The The before/after evidence is the load-bearing part of the description and, as printed, was not captured against On volume: you have 41 open PRs right now, all from the last two days. I am not holding that against this one — it is judged on its own diff, and it is one of the cleaner things I reviewed today. But it is why I checked the unskimmable parts instead of pattern-matching the shape. Related and useful: base If I had to maintain this in six months I would thank the author: one more provider in a documented pattern, a shared helper instead of a second copy, and a test that asserts on real bytes rather than on a mock. The only thing I did not get to see is CI land, and I am not going to approve against a result that does not exist yet — the ubuntu unit suite, lint, and the integration run were all still in flight at my single fetch. So approval is deferred until CI lands green on 中文说明Confidence: 4/5 —— 扎实、改动最小,修掉的是一个让整个 provider 在第二轮之后完全不可用的 P1;两条保留意见都在描述的准确性上,不在代码上。 在看 diff 之前,我只根据标题和动机写下的独立方案,与这个 PR 采取的形态相同:一个在 我最希望 maintainer 知道的是:我没有采信这套叙述。我确认了转换器在 reasoning 轮次上无条件写入 两点保留意见,都不阻塞,都在描述而非 diff 里:
before/after 证据是描述里承重的部分,而按贴出的内容,它不是在 关于数量:你现在有 41 个 open PR,全部来自最近两天。我没有因此对这个 PR 有偏见——它是按自己的 diff 评判的,而且是我今天看过的比较干净的改动之一。但这正是我去核查那些无法略读的部分、而不是按形态做模式匹配的原因。相关且有用的一点:base 如果六个月后由我来维护这份代码,我会感谢作者:多了一个遵循既定模式的 provider、一份共享 helper 而不是第二份副本、以及一个对真实字节而非 mock 做断言的测试。我唯一没能看到的是 CI 落地,而我不会对着一个尚不存在的结果去 approve——在我那次单次抓取时,ubuntu 单元测试、lint 和集成测试都还在运行。因此批准推迟到 CI 在 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Test Plan (not a blocker): src/core/openaiContentGenerator/provider/cerebras.test.ts — no such file or directory; Tests 5 passed — this review observed 1945, 298 passed; 2 passed — this review observed 1945, 298 passed.
中文说明
已审查。 建议见行内评论。
Test Plan(非阻断):src/core/openaiContentGenerator/provider/cerebras.test.ts — no such file or directory; Tests 5 passed — this review observed 1945, 298 passed; 2 passed — this review observed 1945, 298 passed。
— qwen3.8-max via Qwen Code /review (v0.23.0)
| const hostname = new URL(baseUrl).hostname.toLowerCase(); | ||
| return ( | ||
| hostname === CEREBRAS_API_HOST || | ||
| hostname.endsWith(`.${CEREBRAS_API_HOST}`) | ||
| ); |
There was a problem hiding this comment.
[Suggestion] R1-3: isCerebrasProvider is a near-verbatim copy of isMistralHostname — same URL parse, lowercasing, exact-or-dot-suffix match, same try/catch — differing only in the host constant, and the buildRequest override this PR adds is byte-for-byte identical to Mistral's. The same matcher shape is hand-copied across roughly eight provider files. This PR already consolidated stripReasoningContent into provider/utils.ts; the hostname matcher belongs in that same shared home. The matcher encodes security-relevant semantics — exact host or dot-prefixed subdomain, rejecting lookalikes like api.cerebras.ai.evil.example, which this PR's own hostile-hostname test pins — so with ~8 hand-copies, any hardening of host matching (trailing-dot hostnames, IPv6 literals, IDN) must be applied to every copy in lockstep, and missing one silently misroutes or over-strips for that provider.
Witness:
cerebras buildRequest == mistral buildRequest: True
matcher sweep — new URL(...).hostname.toLowerCase() + exact-or-dot-suffix + try/catch:
8 hand-copies total: mistral.ts, cerebras.ts, deepseek.ts, zai.ts, mimo.ts,
openrouter.ts (regex sweep) + modelscope.ts, minimax.ts (direct read)
Extract a shared helper next to stripReasoningContent in provider/utils.ts and use it from cerebras.ts and mistral.ts — the two call sites this diff touches:
export function isProviderApiHost(
config: ContentGeneratorConfig,
host: string,
): boolean {
const baseUrl = config.baseUrl ?? '';
if (!baseUrl) return false;
try {
const hostname = new URL(baseUrl).hostname.toLowerCase();
return hostname === host || hostname.endsWith(`.${host}`);
} catch {
return false;
}
}The rewiring must preserve Mistral's hostname-OR-model-marker routing — if (isMistralHostname(config)) return true; followed by the MISTRAL_MODEL_MARKERS check (mistral.ts:37-41). If applied, please confirm the extraction keeps the matching semantics: cerebras.test.ts's "does not treat hostile hostnames containing api.cerebras.ai as Cerebras" and "strips reasoning_content for Cerebras subdomains" pin the exact-or-dot-suffix semantics, and either goes red if the shared helper regresses to hostname.includes(host).
中文说明
R1-3:isCerebrasProvider 与 isMistralHostname 几乎逐字相同——同样的 URL 解析、小写化、精确或点前缀子域名匹配、同样的 try/catch——只有 host 常量不同;本 PR 新增的 buildRequest 重写也与 Mistral 的逐字节一致。同一匹配逻辑在约 8 个 provider 文件中被手工复制。本 PR 已经把 stripReasoningContent 收敛到 provider/utils.ts;hostname 匹配器也应该放到同一个共享位置。该匹配器承载安全相关语义——精确匹配或点前缀子域名、拒绝形似域名(如 api.cerebras.ai.evil.example,本 PR 自己的恶意域名测试固定了这一点)——因此在约 8 份手工副本的情况下,任何对主机匹配的加固(尾点域名、IPv6 字面量、IDN)都必须同步应用到每一份,漏掉任何一份都会让对应 provider 悄悄误路由或过度 strip。
建议:在 provider/utils.ts 中、stripReasoningContent 旁边抽取共享辅助函数,并在 cerebras.ts 与 mistral.ts——即本 diff 触及的两个调用点——中使用(代码见英文部分)。
修复约束:重接线必须保留 Mistral 的「hostname 或模型标记」路由——if (isMistralHostname(config)) return true; 之后是 MISTRAL_MODEL_MARKERS 检查(mistral.ts:37-41)。修复见证:如果应用此改动,请确认抽取没有改变匹配语义——cerebras.test.ts 的「does not treat hostile hostnames containing api.cerebras.ai as Cerebras」与「strips reasoning_content for Cerebras subdomains」固定了精确或点前缀后缀语义;若共享 helper 退化为 hostname.includes(host),这两个用例会变红。
— qwen3.8-max via Qwen Code /review (v0.23.0)
Maintainer verification — real bundled CLI against a live
|
yiliang114
left a comment
There was a problem hiding this comment.
Review pass on head 368194b — no blockers found. (Attempted to approve, but GitHub does not allow approving your own PR, so this stands as the review record.)
Verified locally at head 368194b (worktree at that exact SHA): npm run typecheck in packages/core clean; vitest run src/core/openaiContentGenerator/ 22 files / 901 tests all pass, including the 5 new tests in cerebras.test.ts; eslint clean on all six changed files.
What I checked in the code:
- Hostname detection is exact-or-dot-suffix on
api.cerebras.ai;api.cerebras.ai.evil.examplefalls through to the default provider (pinned by test), and the strip never mutates session history — only the outbound request. - The Cerebras branch sits after the model-name-based detections (DeepSeek/Zai/MiMo/Mistral markers) in
determineProvider(); a collision would need a Cerebras-hosted model carrying another provider's marker, which the current Cerebras catalog does not have. That edge class is already tracked in the existing non-blocking inline note. stripReasoningContentmoved from mistral.ts to provider/utils.ts byte-for-byte; Mistral now shares it, and DeepSeek/Qwen endpoints that requirereasoning_contentreplay are untouched.
Non-blocking follow-ups already open inline: shared host-matcher extraction (bot R1-3) and the model-name routing edge. CI note: ubuntu Test and Lint & Static were still in flight on the slow runner pool at review time; integration and desktop-shell checks are green.
|
Review result: no blockers. Verified at head One platform boundary: |
|
That's just the late completion notice for the |
|
Permission required to run a tool Tool: ask_user_question Reply with: |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Test Plan (not a blocker): src/core/openaiContentGenerator/provider/cerebras.test.ts — no such file or directory; Tests 5 passed — this review observed 23394, 1945, 28579, 298, 1815, 504, 5995, 94 passed; 2 passed — this review observed 23394, 1945, 28579, 298, 1815, 504, 5995, 94 passed.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/core/openaiContentGenerator/provider/cerebras.ts:21 — [probe] Both negative branches of isCerebrasProvider (empty baseUrl, unparseable baseUrl) have no test; a catch->true mutant survives the full 904-test suite, which wou…packages/core/src/telemetry/gen-ai-provider.ts:66 — [probe] This PR makes Cerebras a named provider, but the gen_ai telemetry registry has no cerebras hostname entry, so Cerebras traffic is attributed to 'openai'; every other recognized ven…
中文说明
已审查。 建议见行内评论。
Test Plan(非阻断):src/core/openaiContentGenerator/provider/cerebras.test.ts — no such file or directory; Tests 5 passed — this review observed 23394, 1945, 28579, 298, 1815, 504, 5995, 94 passed; 2 passed — this review observed 23394, 1945, 28579, 298, 1815, 504, 5995, 94 passed。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.23.0)
| return ( | ||
| hostname === CEREBRAS_API_HOST || | ||
| hostname.endsWith(`.${CEREBRAS_API_HOST}`) | ||
| ); |
There was a problem hiding this comment.
[Suggestion] R1-3: isCerebrasProvider is a near-verbatim copy of isMistralHostname — same URL parse, lowercasing, exact-or-dot-suffix match, same try/catch — differing only in the host constant, and the buildRequest override this PR adds is byte-for-byte identical to Mistral's. The same matcher shape is hand-copied across roughly eight provider files. This PR already consolidated stripReasoningContent into provider/utils.ts; the hostname matcher belongs in that same shared home. The matcher encodes security-relevant semantics — exact host or dot-prefixed subdomain, rejecting lookalikes like api.cerebras.ai.evil.example, which this PR's own hostile-hostname test pins — so with ~8 hand-copies, any hardening of host matching (trailing-dot hostnames, IPv6 literals, IDN) must be applied to every copy in lockstep, and missing one silently misroutes or over-strips for that provider.
Witness:
cerebras buildRequest == mistral buildRequest: True
matcher sweep — new URL(...).hostname.toLowerCase() + exact-or-dot-suffix + try/catch:
8 hand-copies total: mistral.ts, cerebras.ts, deepseek.ts, zai.ts, mimo.ts,
openrouter.ts (regex sweep) + modelscope.ts, minimax.ts (direct read)
Extract a shared helper next to stripReasoningContent in provider/utils.ts and use it from cerebras.ts and mistral.ts — the two call sites this diff touches:
export function isProviderApiHost(
config: ContentGeneratorConfig,
host: string,
): boolean {
const baseUrl = config.baseUrl ?? '';
if (!baseUrl) return false;
try {
const hostname = new URL(baseUrl).hostname.toLowerCase();
return hostname === host || hostname.endsWith(`.${host}`);
} catch {
return false;
}
}The rewiring must preserve Mistral's hostname-OR-model-marker routing — if (isMistralHostname(config)) return true; followed by the MISTRAL_MODEL_MARKERS check (mistral.ts:37-41). If applied, please confirm the extraction keeps the matching semantics: cerebras.test.ts's "does not treat hostile hostnames containing api.cerebras.ai as Cerebras" and "strips reasoning_content for Cerebras subdomains" pin the exact-or-dot-suffix semantics, and either goes red if the shared helper regresses to hostname.includes(host).
中文说明
R1-3:isCerebrasProvider 与 isMistralHostname 几乎逐字相同——同样的 URL 解析、小写化、精确或点前缀子域名匹配、同样的 try/catch——只有 host 常量不同;本 PR 新增的 buildRequest 重写也与 Mistral 的逐字节一致。同一匹配逻辑在约 8 个 provider 文件中被手工复制。本 PR 已经把 stripReasoningContent 收敛到 provider/utils.ts;hostname 匹配器也应该放到同一个共享位置。该匹配器承载安全相关语义——精确匹配或点前缀子域名、拒绝形似域名(如 api.cerebras.ai.evil.example,本 PR 自己的恶意域名测试固定了这一点)——因此在约 8 份手工副本的情况下,任何对主机匹配的加固(尾点域名、IPv6 字面量、IDN)都必须同步应用到每一份,漏掉任何一份都会让对应 provider 悄悄误路由或过度 strip。
建议:在 provider/utils.ts 中、stripReasoningContent 旁边抽取共享辅助函数,并在 cerebras.ts 与 mistral.ts——即本 diff 触及的两个调用点——中使用(代码见英文部分)。
修复约束:重接线必须保留 Mistral 的「hostname 或模型标记」路由——if (isMistralHostname(config)) return true; 之后是 MISTRAL_MODEL_MARKERS 检查(mistral.ts:37-41)。修复见证:如果应用此改动,请确认抽取没有改变匹配语义——cerebras.test.ts 的「does not treat hostile hostnames containing api.cerebras.ai as Cerebras」与「strips reasoning_content for Cerebras subdomains」固定了精确或点前缀后缀语义;若共享 helper 退化为 hostname.includes(host),这两个用例会变红。
— qwen3.8-max via Qwen Code /review (v0.23.0)
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings. Approved.
What I checked
Cross-file context:
mistral.tsat head — confirmed the refactoring is clean:stripReasoningContentbody is identical to the removed version;MistralOpenAICompatibleProvidercontinues to import and use it correctly.default.tsat head — confirmedDefaultOpenAICompatibleProvider.buildRequestdoes not addreasoning_content; the CerebrasbuildRequestoverride (strip after super) is in the right order.determineProviderfull function — Cerebras check at line 115 sits after DeepSeek (line 75) and Mistral (line 107), before default; the ordering concern is correctly resolved.
Diff audit:
isCerebrasProvider: URL parse in try/catch, exact-or-dot-suffix hostname match, no mutation of input. Pattern mirrorsisMistralHostnameexactly.buildRequestoverride chainssuper.buildRequest→map(stripReasoningContent); the history-non-mutation guarantee holds (shallow copy, delete).- Export surface: class re-exported from
provider/index.tsandindex.ts; standaloneisCerebrasProviderfunction is not re-exported from either barrel.
Test audit:
- Unit cases cover: strip fires for apex + subdomain; hostile suffix (
api.cerebras.ai.evil.example) correctly falls through to default; non-Cerebras providers passreasoning_contentthrough; source history not mutated. - Integration test uses a real local HTTP server that 400s on any body containing
reasoning_content, wiring the full session-history → converter → provider boundary → wire path. Not vacuous: with fix reverted, the follow-up turn receives a 400 and theresponse2assertion fails.
Cross-check against existing reviews:
- qwen-code-ci-bot R1-3 (
cerebras.ts:28):isCerebrasProvideris a near-verbatim copy ofisMistralHostname— extracting a sharedmatchesApiHost(config, host)helper would eliminate the duplication. Confirmed minor (Class 9 duplication). Already in the record; no new thread. - wenshao maintainer approval: live-CLI verification consistent with my findings.
- qwen-code-review-bot: typecheck + 901 tests all passing at head. Consistent.
Scope
All 6 changed files reviewed. Not run: local build/test (no working tree); rung 3 not applicable (no platform-specific behavior changed).
Reviewed with AI assistance.


What this PR does
Adds a Cerebras provider that removes the non-standard
messages[].reasoning_contentfield at the outbound request boundary — the same treatment Mistral already receives — and detects it by hostname (api.cerebras.aiand subdomains) indetermineProvider(). The existingstripReasoningContenthelper moves frommistral.tsto the sharedprovider/utils.tsunchanged, so both providers strip through one implementation. Session history itself is never mutated.Why it's needed
Cerebras' OpenAI-compatible endpoint rejects
reasoning_contenton input with HTTP 400 (wrong_api_format:messages.N.assistant.reasoning_content ... is unsupported). The history converter writes that field on every assistant turn that carried reasoning parts, anddetermineProvider()has no Cerebras branch, soapi.cerebras.aifalls through toDefaultOpenAICompatibleProvider, which ships the field verbatim. Result: the first turn succeeds, then every multi-turn request — including background subagents replaying session history — fails with400 status code (no body), exactly as reported in #11045. The strip stays at the outbound request boundary, so DeepSeek/Qwen thinking-mode endpoints that requirereasoning_contentreplayed are untouched.Reviewer Test Plan
How to verify
packages/core/src/core/openaiContentGenerator/provider/cerebras.test.tsis red onmainand green with this change. The multi-turn case starts a local OpenAI-compatible endpoint that 400s on any request body containingreasoning_content(returning the same validation payload asapi.cerebras.ai): before the fix the follow-up turn fails with exactly400 status code (no body); after the fix both turns succeed and the assistant message on the wire carries noreasoning_content. Unit cases additionally cover subdomain hosts, hostile hostnames (api.cerebras.ai.evil.examplestays on the default provider), non-mutation of source history, and unchanged pass-through for non-Cerebras endpoints.Commands:
cd packages/core && npx vitest run src/core/openaiContentGenerator/(22 files, 901 tests, all pass),npm run typecheckin packages/core (clean),npx eslinton the changed files (clean).Evidence (Before & After)
Before (
main@74fe3a65):After (this branch):
Tested on
Environment (optional)
vitest unit tests plus a local HTTP stand-in endpoint; npm workspaces install on Linux x64, Node v24.
Risk & Scope
reasoninginput field. This matches the proven Mistral pattern and is strictly better than every multi-turn request failing; renaming can be a follow-up if maintainers prefer it. Detection is hostname-only because Cerebras serves third-party model names (qwen-3.8-27b,gpt-oss-120b,llama-*), so a model-name fallback would misroute other providers' models.400 status code (no body)error-message surfacing belongs in its own ticket per the triage thread.provider/utils.tsunchanged.Linked Issues
Fixes #11045
中文说明
这个 PR 做了什么
新增一个 Cerebras provider,在出站请求边界移除非标准的
messages[].reasoning_content字段——与 Mistral 已有的处理方式一致——并在determineProvider()中按 hostname(api.cerebras.ai及其子域名)识别。原有的stripReasoningContent辅助函数从mistral.ts原样移到共享的provider/utils.ts,两个 provider 共用同一份实现。会话历史本身不会被改动。为什么需要
Cerebras 的 OpenAI 兼容端点对输入中的
reasoning_content一律返回 HTTP 400(wrong_api_format:messages.N.assistant.reasoning_content ... is unsupported)。历史转换器会在每个带 reasoning part 的 assistant 轮次上写入该字段,而determineProvider()没有 Cerebras 分支,所以api.cerebras.ai落到DefaultOpenAICompatibleProvider,原样把字段发出去。结果就是:第一轮成功,之后每一轮多轮请求(包括重放会话历史的后台 subagent)都报400 status code (no body),与 #11045 描述完全一致。strip 只发生在出站请求边界,因此需要回传reasoning_content的 DeepSeek/Qwen 思考模式端点不受影响。评审验证计划
如何验证
packages/core/src/core/openaiContentGenerator/provider/cerebras.test.ts在main上是红的,打上本补丁后变绿。多轮用例会启动一个本地 OpenAI 兼容端点,凡是请求体含reasoning_content就返回 400(返回与api.cerebras.ai相同的校验报错):修复前第二轮恰好报400 status code (no body);修复后两轮都成功,且发到线上的 assistant 消息不带reasoning_content。单元用例还覆盖了子域名、恶意域名(api.cerebras.ai.evil.example仍走默认 provider)、不改写源历史、以及非 Cerebras 端点行为不变。命令:
cd packages/core && npx vitest run src/core/openaiContentGenerator/(22 个文件、901 个测试全部通过),在 packages/core 下npm run typecheck(干净),对改动文件跑npx eslint(干净)。前后证据
修复前(
main@74fe3a65):修复后(本分支):
测试环境
环境(可选)
vitest 单元测试 + 本地 HTTP 模拟端点;Linux x64 上 npm workspaces 安装,Node v24。
风险与范围
reasoning输入字段。这与 Mistral 已验证的模式一致,且严格好于每一轮多轮请求都失败;如果 maintainer 倾向于改名,可以作为后续改进。识别只按 hostname,因为 Cerebras 托管的是第三方模型名(qwen-3.8-27b、gpt-oss-120b、llama-*),按模型名兜底会误路由其他 provider 的模型。400 status code (no body)错误信息呈现)按分诊线程的结论另开 issue。provider/utils.ts。关联 Issue
Fixes #11045