Skip to content

fix(core): stop forcing DeepSeek temperature - #9879

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
AaronZ345:fix-deepseek-reasoning-temperature
Aug 29, 2026
Merged

fix(core): stop forcing DeepSeek temperature#9879
wenshao merged 1 commit into
QwenLM:mainfrom
AaronZ345:fix-deepseek-reasoning-temperature

Conversation

@AaronZ345

Copy link
Copy Markdown
Contributor

What this PR does

Removes the DeepSeek OpenAI-compatible provider's hard-coded default temperature: 0 so DeepSeek models can use their model/server default sampling behavior unless the user explicitly configures a temperature.

Why it's needed

Issue #9765 reports that forcing greedy sampling by default can make DeepSeek reasoning models such as deepseek-v4-flash fall into repetitive thinking loops. The provider should not override the model default for all DeepSeek requests; explicit user overrides via generation config remain supported by the normal request-building path.

Reviewer Test Plan

How to verify

Run the targeted DeepSeek provider tests, Prettier on the touched files, and the core package typecheck.

Evidence (Before & After)

Before: DeepSeekOpenAICompatibleProvider.getDefaultGenerationConfig() returned { temperature: 0 } and a regression test expecting no forced temperature failed.

After: the provider returns {}, leaving default sampling to the model/server unless the user explicitly sets a temperature.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

Local Node/npm workspace on macOS.

Risk & Scope

  • Main risk or tradeoff: Low; this changes only the DeepSeek provider default generation config and preserves explicit user temperature settings.
  • Not validated / out of scope: Live DeepSeek API behavior was not exercised locally.
  • Breaking changes / migration notes: Users who relied on implicit deterministic DeepSeek temperature can still set generationConfig.samplingParams.temperature explicitly.

Linked Issues

Fixes #9765.

中文说明

What this PR does

移除 DeepSeek OpenAI-compatible provider 中硬编码的默认 temperature: 0,让 DeepSeek 模型在用户没有显式配置 temperature 时使用模型/服务端默认采样行为。

Why it's needed

#9765 报告了 DeepSeek reasoning 模型(例如 deepseek-v4-flash)在默认贪心采样下容易进入重复 thinking loop。provider 不应该对所有 DeepSeek 请求强制覆盖模型默认值;用户显式配置的 temperature 仍会通过现有请求构造路径生效。

Reviewer Test Plan

How to verify

运行 DeepSeek provider 目标测试、改动文件 Prettier 检查、core 包 typecheck。

Evidence (Before & After)

Before:DeepSeekOpenAICompatibleProvider.getDefaultGenerationConfig() 返回 { temperature: 0 },期望不强制 temperature 的回归测试失败。

After:provider 返回 {},默认采样交给模型/服务端,除非用户显式设置 temperature。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

macOS 本地 Node/npm 工作区。

Risk & Scope

  • Main risk or tradeoff: 低;只改变 DeepSeek provider 的默认 generation config,并保留用户显式 temperature 设置。
  • Not validated / out of scope: 未在本地请求真实 DeepSeek API。
  • Breaking changes / migration notes: 依赖隐式 deterministic DeepSeek temperature 的用户仍可显式配置 generationConfig.samplingParams.temperature

Linked Issues

Fixes #9765.

Let DeepSeek reasoning models use their model default temperature so deterministic sampling does not amplify repeated thinking loops.

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on c4224ef and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— c4224ef 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template: complete ✓
  • Problem: observed, not theoretical. bug(core): DeepSeek provider forces temperature 0, causing reasoning models (deepseek-v4-flash) to loop in thinking #9765 reports repetitive thinking loops on deepseek-v4-flash with default settings, and the same model under another client (opencode) with defaults does not loop — pointing squarely at a qwen-code default difference. Root cause is identified: getDefaultGenerationConfig() forcing temperature: 0 (greedy decoding), which the issue argues is the worst case for reasoning-model looping.
  • Direction: aligned. The forced temperature: 0 is an exception — the base OpenAI-compatible provider returns {} and DashScope does too, leaving sampling to the server default. The repo's own reasoning-effort design doc also notes DeepSeek/GLM should not carry sampling params in thinking mode. CHANGELOG (claude-code): no direct reference to this change, but a recent temperatureOverride fix there confirms temperature-default handling is a live user-facing area.
  • Size: core paths touched (packages/core/src/**): 4 production lines (deepseek.ts: +1/−3), 6 test lines (deepseek.test.ts: +2/−4), 0 generated/schema. Far below any escalation threshold.
  • Approach: minimal — one behavioral line plus the matching test update. The issue listed gating temperature on thinking-mode as an alternative; plain removal is the simplest of the suggested options and matches what sibling providers already do. Note the intentional behavior change: DeepSeek output is no longer deterministic by default (server default applies); the PR documents the explicit-override migration path, and samplingParams.temperature precedence is preserved by the pipeline's config > request > default merge.
  • Risk: Stage 1e matched openaiContentGenerator — a path correlated with post-merge reverts in this repo's history. That doesn't block anything here, but it means full review depth and CI evidence before approval. The diff itself is small and its single production consumer is deterministic.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板:完整 ✓
  • 问题:已观测到的 bug,不是理论问题。bug(core): DeepSeek provider forces temperature 0, causing reasoning models (deepseek-v4-flash) to loop in thinking #9765 报告 deepseek-v4-flash 在默认设置下出现重复 thinking loop,而同一模型在另一个客户端(opencode)默认设置下不循环——明确指向 qwen-code 的默认值差异。根因已定位:getDefaultGenerationConfig() 强制 temperature: 0(贪心采样),issue 认为这对 reasoning 模型的循环是最坏情况。
  • 方向:对齐。强制 temperature: 0 是例外——OpenAI-compatible 基类 provider 返回 {},DashScope 也是,采样交给服务端默认值。仓库自己的 reasoning-effort 设计文档也指出 DeepSeek/GLM 在 thinking 模式下不应携带采样参数。CHANGELOG(claude-code):无直接对应,但近期有一个 temperatureOverride 修复,说明 temperature 默认值处理是活跃的用户关注点。
  • 规模:触及核心路径(packages/core/src/**):生产代码 4 行(deepseek.ts:+1/−3),测试 6 行(deepseek.test.ts:+2/−4),生成/schema 0 行。远低于任何升级阈值。
  • 方案:最小化——一行行为改动加对应测试更新。issue 中列过按 thinking 模式门控 temperature 的替代方案;直接移除是所提选项中最简单的,也与兄弟 provider 的现有行为一致。注意这是有意的行为变更:DeepSeek 输出默认不再确定(采用服务端默认值);PR 已说明显式配置的迁移路径,pipeline 的 config > request > default 合并保证了 samplingParams.temperature 优先生效。
  • 风险:Stage 1e 命中 openaiContentGenerator——该路径与本仓库历史中合并后回滚相关。这不构成阻碍,但意味着需要完整 review 深度和 CI 证据才能批准。diff 本身很小,唯一的生产消费点是确定性的。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at c4224ef8367534a92776c3764ffaecb76191f6c7 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Code review

My independent take before reading the diff: make the DeepSeek provider stop forcing a temperature — return the same empty default config the base provider and DashScope already return — and update the one test that pins the old value. That is exactly what this PR does, and there's nothing extra in it.

What I verified in the code:

  • buildGenerateContentConfig in the OpenAI pipeline is the only production consumer of getDefaultGenerationConfig(). It resolves each sampling param with priority user config, then per-request config, then provider default. With the default now empty, temperature is simply omitted from the request body when nobody sets it, so the server default applies — and an explicit generationConfig.samplingParams.temperature still wins, unchanged.
  • No other test pins DeepSeek at temperature: 0 — the pipeline and generator tests mock provider defaults — so the updated unit test (toEqual({})) is the right regression pin and nothing else needs touching.
  • No docs describe the old forced default, so no doc drift.

No blockers, no convention violations. The diff is the minimal change for the stated goal.

The load-bearing part of this change is where the temperature value comes from at request-build time:

sequenceDiagram
    participant P1 as User samplingParams config
    participant P2 as Per-request config
    participant P3 as DeepSeek provider default
    participant P4 as buildGenerateContentConfig
    participant P5 as Wire request body
    P4->>P1: read temperature
    P4->>P2: read temperature
    P4->>P3: read temperature
    Note over P4: first defined wins - user config, then request, then default
    P4->>P5: emit temperature only when one is defined
Loading
Files changed (2)
File What changed
packages/core/src/core/openaiContentGenerator/provider/deepseek.ts getDefaultGenerationConfig returns an empty config instead of forcing temperature 0
packages/core/src/core/openaiContentGenerator/provider/deepseek.test.ts regression test updated to expect an empty default config

Testing evidence

This is an unattended CI run — I did not build or execute any PR code. Evidence below is the PR's own CI, fetched via the API at the reviewed commit.

Currently green: Security Checks (Secret scan, Dependency CVE audit), Desktop Shell on ubuntu and windows, precheck-pr / precheck, Classify PR, label. The unit suite Test (ubuntu-latest, Node 22.x) is still in progress — the repo's unit suite takes a while, so I'm not polling it; the finalize job updates the table below once CI settles. Test (macos-latest), Test (windows-latest), and Integration Tests (CLI, No Sandbox) show as skipped at job level while the parent Qwen Code CI run is still in flight. No failing checks at fetch time, so no log excerpt to quote.

Final CI results for c4224ef (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Static review settles the merge priority (deterministic code path), and the updated unit test pins the new default, but the wire-level consequence — DeepSeek request bodies no longer carrying temperature: 0 when the user has not configured one — is a behavioural claim the suite alone doesn't pin. Sandboxed verification would settle it: @qwen-code /verify — A/B against the base build would show the old request emitting temperature: 0 and the new one omitting the key. The author lacks write access, so this would be a sponsored run: a maintainer's @qwen-code /verify comment approves the head it was written against, and the run carries a pre-execution risk screen plus a full workspace wipe — read the resulting report with the same skepticism as the fork's own CI logs. The second half of the claim (that the reasoning loops in #9765 stop) needs live DeepSeek API behavior, which no sandbox lane can exercise; that part rests on the reporter's cross-client comparison in the issue.

中文说明

代码审查:我在看 diff 之前的独立想法就是——让 DeepSeek provider 不再强制 temperature,返回与基类和 DashScope 相同的空默认配置,并更新锁定旧值的测试。PR 正是这么做的,没有任何多余改动。

已在代码中核实:

  • OpenAI pipeline 的 buildGenerateContentConfiggetDefaultGenerationConfig() 唯一的生产消费点,按"用户配置 > 请求配置 > provider 默认"的优先级解析采样参数。默认变为空后,未设置时 temperature 直接从请求体中省略,交由服务端默认值;显式的 samplingParams.temperature 仍然优先生效,行为不变。
  • 没有其他测试把 DeepSeek 锁定在 temperature: 0(pipeline/generator 测试均 mock provider 默认值),因此更新后的单测(toEqual({}))就是正确的回归锚点,无需其他改动。
  • 没有文档描述旧的强制默认值,不存在文档漂移。

无阻塞项,无规范违规。diff 是达成目标的最小改动。

测试证据:这是无人值守 CI 运行,未构建或执行任何 PR 代码,以上证据来自 API 拉取的 PR 自身 CI。目前安全类检查(Secret scan、依赖 CVE 审计)、Desktop Shell(ubuntu/windows)、precheck 均绿;单测套件 Test (ubuntu-latest, Node 22.x) 仍在运行(本仓库单测耗时较长,不做轮询,finalize 任务会在 CI 结束后更新上方表格);macOS/Windows 测试与 CLI 集成测试在父运行仍在进行时显示为 job 级 skipped。抓取时无失败检查,故无日志摘录。

沙箱验证可坐实 wire 层结论:@qwen-code /verify 可与 base 构建做 A/B,证明旧请求携带 temperature: 0 而新请求省略该键。作者无 write 权限,需由 maintainer 发起赞助运行(含执行前风险筛查与工作区清理),报告应像对待 fork 的 CI 日志一样保持审慎。关于"#9765 中的重复 thinking loop 消失"的另一半结论,需要真实 DeepSeek API 行为,任何沙箱通道都无法演练,该部分依据 issue 中报告者的跨客户端对比。

Qwen Code · qwen3.8-max

Reviewed at c4224ef8367534a92776c3764ffaecb76191f6c7 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal fix for an observed bug with the right test pin; holding back the last point only because the unit suite hasn't landed green on this commit yet and the loop-fix half of the claim rests on the reporter's account.

Stepping back: this PR does exactly what I would have done — stop overriding the model's sampling default, match what the base provider and DashScope already do, and fix the one test that pinned the old value. Nothing extra, nothing missing. The problem is real and observed (#9765's cross-client comparison is decent evidence, and greedy decoding on reasoning models looping is a well-known failure mode), and the direction matches where the repo is already heading — the reasoning-effort design doc explicitly wants sampling params off DeepSeek thinking requests.

One thing a maintainer should see before merge, stated plainly: the author has ~20 open PRs, nearly all created in a single burst today. That doesn't taint this PR — judged on its own merit it's a legitimate, well-scoped fix — but it's exactly the situation where each PR needs to stand on its own evidence rather than momentum, so I'm keeping the bar where it belongs.

Verdict: approve — but the unit suite Test (ubuntu-latest, Node 22.x) is still in flight on this commit, so approval is deferred until CI lands green on c4224ef8367534a92776c3764ffaecb76191f6c7. The finalize job will post the commit-pinned approval once every check completes green, and withhold it if anything lands red.

中文说明

置信度:4/5 —— 对一个已观测到的 bug 的干净、最小修复,测试锚点正确;保留最后 1 分仅因为该 commit 的单测套件尚未跑绿,且"循环消失"这半结论依赖报告者的陈述。

整体来看:这个 PR 的做法与我的独立方案完全一致——不再覆盖模型采样默认值,与基类 provider 和 DashScope 保持一致,并修正唯一锁定旧值的测试。没有多余改动,也没有遗漏。问题是真实且已观测的(#9765 的跨客户端对比是不错的证据,贪心采样导致 reasoning 模型循环也是已知的失效模式),方向也与仓库现有路线一致——reasoning-effort 设计文档明确希望 DeepSeek thinking 请求不携带采样参数。

一点需要 maintainer 在合并前看到的情况,直说:该作者约有 20 个 open PR,几乎全部在今天的同一波提交中创建。这并不影响本 PR——按自身价值评判,它是一个合理、范围清晰的修复——但这正是每个 PR 都必须靠自身证据站住脚、而不是靠数量势能过关的场景,因此评审标准保持在应有高度。

结论:批准——但该 commit 的单测 Test (ubuntu-latest, Node 22.x) 仍在运行,批准推迟到 CI 在该 commit 上全绿之后。finalize 任务会在所有检查绿完成后发布绑定该 commit 的批准;若有检查变红则不会批准。

Qwen Code · qwen3.8-max

Reviewed at c4224ef8367534a92776c3764ffaecb76191f6c7 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: reverse audit — stopped before round 1 by the review time budget.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:反向审计——评审时间预算不足,未能开始第 1 轮。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines 138 to 140
override getDefaultGenerationConfig(): GenerateContentConfig {
return {
temperature: 0,
};
return {};
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This override now returns {} — byte-for-byte identical to what the base class DefaultOpenAICompatibleProvider.getDefaultGenerationConfig() already returns (provider/default.ts:219), so inheritance yields identical behavior and the override — along with its dedicated test, which now asserts the base behavior as if it were DeepSeek-specific — is redundant; five of the seven subclasses already inherit {} without overriding. The cost is maintenance, not runtime: the override and test read as if DeepSeek carried a distinct default generation config, and if the base default ever gains a real value, DeepSeek silently opts out here and diverges from every other subclass with no visible diff — re-introducing the exact bug class this PR fixes. Either delete the override, its test block, and the now-unused GenerateContentConfig import, or — if the override deliberately pins {} as a regression guard for #9765 — say that why in a one-line comment, which is currently invisible:

override getDefaultGenerationConfig(): GenerateContentConfig {
  // Deliberately empty: forcing temperature 0 caused repetitive thinking
  // loops on DeepSeek reasoning models (#9765).
  return {};
}
中文说明

[Suggestion] 这个 override 现在返回 {}——与基类 DefaultOpenAICompatibleProvider.getDefaultGenerationConfig() 已经返回的内容(provider/default.ts:219)逐字节完全相同,因此继承即可得到完全一致的行为;该 override 连同它的专属测试(现在把基类行为当作 DeepSeek 特有行为来断言)都是冗余的——七个子类中已有五个不做 override、直接继承 {}。代价在维护层面而非运行时:这个 override 和测试读起来像是 DeepSeek 拥有独立的默认 generation config;如果基类默认值将来加入了真实的值,DeepSeek 会通过这里悄悄退出继承、在没有任何可见 diff 的情况下与其他所有子类产生差异——重新引入本 PR 所修复的那类 bug。可以删除该 override、对应的测试代码块以及因此不再使用的 GenerateContentConfig import;或者——如果这个 override 是有意将 {} 固定下来、作为 #9765 的回归防护——请用一行注释说明这个原因,目前这一点完全看不出来。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship — CI landed green after the review. ✅

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — built and run locally, real stack

I built the merge tree (origin/main f8bcdaae8f + PR head c4224ef836, merges clean) and drove the bundled dist/cli.js — headless and interactive — against a local OpenAI-compatible endpoint that records the raw request bytes it receives. So the evidence below is the wire payload itself, not a client-side assertion or a mocked provider.

The A/B is a single-variable swap inside one tree: flip the four-line hunk in deepseek.ts, re-bundle, re-run. Re-bundling the head source afterwards reproduced the head bundle byte-for-byte (d0d638af…), so nothing else moved between the two runs.

Verdict: the PR's claim holds exactly as written, and I'd merge it. One consequence is bigger than the PR body suggests, and it's a conscious call rather than a defect — see Blast radius below.


1. Core claim — verified at the wire

Diff of the two actual POST /v1/chat/completions bodies (messages and tool schemas elided) is exactly one line:

   "stream_options": {
     "include_usage": true
   },
-  "temperature": 0,
   "tools": "<27 tool schemas elided>"
 }

raw request body diff

bundle model endpoint temperature on wire
BASE (temperature: 0 kept) deepseek-v4-flash 127.0.0.1 (self-hosted style) 0, 0
BASE qwen3-coder-plus 127.0.0.1 (control) absent, absent
HEAD (this PR) deepseek-v4-flash 127.0.0.1 (self-hosted style) absent, absent
HEAD qwen3-coder-plus 127.0.0.1 (control) absent, absent
HEAD deepseek-v4-flash api.deepseek.com (linux/arm64 container) absent, absent

wire capture matrix

Three things this pins that the unit test alone cannot:

  • Scoped. The control model proves temperature: 0 was DeepSeek-only before and that nothing leaked into the shared path after.
  • Both selection routes. isDeepSeekProvider matches on hostname or model name. I exercised both — a self-hosted-style 127.0.0.1 endpoint with a deepseek-* model name, and a genuine api.deepseek.com hostname (resolved to a local capture server inside a Debian container via --add-host). Same result on both, so no branch was left untested.
  • Second OS. The PR body marks Linux as "not tested locally". The api.deepseek.com run above is linux/arm64, Node 24.19 — same outcome.

2. The migration note in the PR body is true

An explicit user temperature still reaches the wire under HEAD, including the deterministic 0 that users relying on the old behaviour would restore:

modelProviders[].generationConfig.samplingParams.temperature wire
0.6 0.6, 0.6
0 0, 0

3. Tests, mutation, lint

  • packages/core provider + pipeline suite on HEAD: 876 passed / 19 files.
  • Mutation check: restoring temperature: 0 in deepseek.ts turns the new test red (expected { temperature: +0 } to deeply equal {}). The test pin is load-bearing, not decorative.
  • prettier --check on both touched files: clean. npm run typecheck -w packages/core: clean.

tests and mutation

4. Blast radius — worth one conscious decision before merge

getDefaultGenerationConfig() is the floor for every DeepSeek request that does not carry a temperature of its own — not just the main agent turn. Captured live during a /compress run:

request BASE HEAD
main agent turn 0 absent
memory-extraction subagent 0 absent
main agent turn 0 absent
chat-compression side query 0 absent

I walked every runSideQuery call site to draw the line exactly. Seven inherit the provider default and therefore move with this PR: followup/suggestionGenerator, tools/web-fetch, utils/nextSpeakerChecker, utils/subagentGenerator, agents/arena/ArenaManager, services/chatCompressionService, services/visionBridge. Nine set their own and are untouched, since request config outranks the provider default: memory/relevanceSelector (0), memory/forget (0), goals/goal-verifier and goal-checkpoint-verifier (0), permissions/classifier (0, both stages), services/sessionTitle (0.2), sessionRecap (0.3), toolUseSummary (0.3).

What that means in numbers: DeepSeek documents its API default as temperature: 1.0, and recommends 0.0 for coding/math. So this PR moves DeepSeek code generation, chat compression, and memory extraction from 0.0 — DeepSeek's own coding recommendation — to 1.0, in exchange for fixing the reasoning loop in #9765.

I think that trade is still worth taking (it restores parity with every other provider, and a looping model is worse than a warmer one), but it's the kind of thing that should be merged knowingly rather than discovered later. If a maintainer disagrees, #9765's option 2 — keep 0 when thinking is off, drop it when thinking is on — is the narrower fix, at the cost of a conditional in a place that currently has none.

5. What I could not verify

  • That the loops actually stop. That needs the live DeepSeek API; my endpoint is a local recorder. This half of the claim still rests on the reporter's cross-client comparison in bug(core): DeepSeek provider forces temperature 0, causing reasoning models (deepseek-v4-flash) to loop in thinking #9765.
  • Thinking mode × sampling params. docs/design/2026-06-30-unified-reasoning-effort-cli.md states DeepSeek rejects temperature/top_p/… in thinking mode and that translators "must strip those sampling params" — no such stripping exists in the code today. I could not exercise it: with model.reasoningEffort: "high" in settings, no reasoning_effort reached the wire for either deepseek-v4-flash or deepseek-reasoner in my harness, so I have no observation to offer. Worth noting only because it means this PR removes the default temperature but a user-configured one would still ship alongside a thinking request — out of scope here, possibly a follow-up.

6. Real session, end to end

An interactive TUI turn against the deepseek-v4-flash endpoint on the HEAD bundle, for what it's worth as a sanity check that nothing else broke:

interactive session

Environment & how to reproduce
  • Host: macOS 26.6.2 arm64, Node v24.18.1, npm 11.16.0. Container leg: node:24-bookworm, Node v24.19.0, aarch64.
  • Tree: origin/main f8bcdaae8f + c4224ef836, merged clean (9e173d6c8a). origin/main has moved one commit since, touching nothing under packages/core/src/core/openaiContentGenerator/.
  • Bundle digests (md5 of dist/cli.js + dist/chunks/*.js): BASE b3f892c2a0b5c92d717f389a82bf606c, HEAD d0d638af9cd53fccb7102f32fbcabe2e.
# 1. capture server: records every request body verbatim, answers with a valid SSE stream
node wire-capture-server.mjs            # 127.0.0.1:8879, writes capture.jsonl

# 2. drive the bundled CLI at it — DeepSeek provider is selected on the model name
env -i PATH="$PATH" HOME="$ISOLATED_HOME" QWEN_HOME="$ISOLATED_HOME/.qwen" \
  OPENAI_API_KEY=sk-test OPENAI_BASE_URL=http://127.0.0.1:8879/v1 \
  OPENAI_MODEL=deepseek-v4-flash \
  node dist/cli.js --yolo -p "Say the word ready and nothing else."

# 3. read the temperature off the recorded bytes
python3 -c "import json;[print(json.loads(json.loads(l)['raw']).get('temperature','ABSENT')) for l in open('capture.jsonl')]"

# 4. genuine hostname leg
docker run --rm --add-host api.deepseek.com:127.0.0.1 -v "$PWD/dist:/app/dist:ro" ... node:24-bookworm
#    with OPENAI_BASE_URL=http://api.deepseek.com/v1
中文说明

维护者验证 —— 本地真实环境构建并运行

我构建了合并树(origin/main f8bcdaae8f + PR head c4224ef836,干净合并),用打包后的 dist/cli.js(headless 与交互式两种)打向一个本地 OpenAI 兼容端点,该端点把收到的原始请求字节逐条落盘。所以下面的证据是链路上的报文本身,不是客户端断言,也不是 mock provider。

A/B 是同一棵树内的单变量切换:翻转 deepseek.ts 里那四行,重新 bundle,重新跑。之后再把 head 源码 bundle 回来,产物与之前逐字节相同d0d638af…),说明两次运行之间没有第二个变量在动。

结论:PR 的论断完全成立,我支持合入。 有一处影响面比 PR 描述里写的要大,那属于需要明确拍板的取舍,不是缺陷 —— 见第 4 节。

1. 核心论断 —— 链路层已证实

两次真实 POST /v1/chat/completions 报文体的 diff(messages 与 tool schema 已省略)恰好只有一行:- "temperature": 0,

bundle 模型 端点 链路上的 temperature
BASE(保留 temperature: 0 deepseek-v4-flash 127.0.0.1(自建部署形态) 0, 0
BASE qwen3-coder-plus 127.0.0.1(对照组) 无, 无
HEAD(本 PR) deepseek-v4-flash 127.0.0.1(自建部署形态) 无, 无
HEAD qwen3-coder-plus 127.0.0.1(对照组) 无, 无
HEAD deepseek-v4-flash api.deepseek.com(linux/arm64 容器) 无, 无

这三点是单测本身锁不住的:

  • 范围收敛。 对照模型证明改动前 temperature: 0 只出现在 DeepSeek 上,改动后也没有渗到公共路径。
  • 两条选中路径都走到了。 isDeepSeekProvider 按主机名模型名匹配,两条我都跑了:127.0.0.1 + deepseek-* 模型名,以及真正的 api.deepseek.com 主机名(在 Debian 容器里用 --add-host 把它解析到本地抓包服务)。结果一致,没有分支被漏掉。
  • 第二个操作系统。 PR 描述里 Linux 标注为「未本地验证」,上面这条 api.deepseek.com 就跑在 linux/arm64 + Node 24.19 上,结论相同。

2. PR 描述里的迁移说明属实

HEAD 下用户显式配置的 temperature 仍然会上链路,包括老用户想恢复确定性时会设的 00.6 → 链路 0.60 → 链路 0

3. 测试、变异、lint

  • packages/core 的 provider + pipeline 套件在 HEAD 上:876 通过 / 19 个文件
  • 变异验证:temperature: 0 塞回 deepseek.ts,新测试立刻变红(expected { temperature: +0 } to deeply equal {})。这个测试锚点是承重的,不是摆设。
  • 两个改动文件 prettier --check 通过;npm run typecheck -w packages/core 通过。

4. 影响面 —— 合并前值得明确拍一次板

getDefaultGenerationConfig()所有自身不带 temperature 的 DeepSeek 请求的兜底,不只是主对话轮。/compress 实跑抓到的:

请求 BASE HEAD
主 agent 轮次 0
memory 抽取子 agent 0
主 agent 轮次 0
chat-compression 侧查询 0

我把所有 runSideQuery 调用点逐个走了一遍,边界如下。继承 provider 默认值、因而随本 PR 一起变化的有七处:followup/suggestionGeneratortools/web-fetchutils/nextSpeakerCheckerutils/subagentGeneratoragents/arena/ArenaManagerservices/chatCompressionServiceservices/visionBridge自带 temperature、因而不受影响的有九处(请求级配置优先级高于 provider 默认值):memory/relevanceSelector(0)、memory/forget(0)、goals/goal-verifiergoal-checkpoint-verifier(0)、permissions/classifier(0,两个阶段)、services/sessionTitle(0.2)、sessionRecap(0.3)、toolUseSummary(0.3)。

换算成具体数字:DeepSeek 官方文档写明 API 默认 temperature: 1.0,并推荐代码/数学场景用 0.0。也就是说本 PR 把 DeepSeek 上的代码生成、上下文压缩、记忆抽取从 0.0(DeepSeek 自己给编码场景的推荐值)挪到了 1.0,换来的是修掉 #9765 的 reasoning 循环。

我认为这个交换仍然值得(它让 DeepSeek 与其它所有 provider 对齐,而且模型死循环比采样偏热更糟),但这属于应当知情合入、而不是事后才发现的事。若维护者不认同,#9765 的方案二 —— thinking 关闭时保留 0、开启时去掉 —— 是更窄的修法,代价是在一处目前没有条件分支的地方引入条件。

5. 我没有验证到的部分

  • 循环是否真的消失。 这需要真实 DeepSeek API,我的端点只是本地记录器。这半个论断仍然依赖 bug(core): DeepSeek provider forces temperature 0, causing reasoning models (deepseek-v4-flash) to loop in thinking #9765 里报告者的跨客户端对比。
  • thinking 模式 × 采样参数。 docs/design/2026-06-30-unified-reasoning-effort-cli.md 写明 DeepSeek 在 thinking 模式下拒绝 temperature/top_p 等,并要求 translator「必须剥离这些采样参数」—— 但代码里目前并没有这个剥离。我也没能跑到这条路径:settings 里设 model.reasoningEffort: "high" 后,deepseek-v4-flashdeepseek-reasoner 的链路上都没有出现 reasoning_effort,因此我给不出观测。之所以提一句,是因为这意味着本 PR 去掉的是默认值,而用户显式配置的 temperature 仍会随 thinking 请求一起发出 —— 超出本 PR 范围,或可作为后续项。

6. 端到端真实会话

HEAD bundle 下针对 deepseek-v4-flash 端点的一次交互式 TUI 轮次,作为「其它部分没被搞坏」的 sanity check(见上方第四张截图)。

环境: macOS 26.6.2 arm64 / Node v24.18.1 / npm 11.16.0;容器侧 node:24-bookworm / Node v24.19.0 / aarch64。树:f8bcdaae8f + c4224ef836 合并于 9e173d6c8a;此后 origin/main 前进了一个 commit,未触及 packages/core/src/core/openaiContentGenerator/。Bundle 摘要:BASE b3f892c2a0b5c92d717f389a82bf606c,HEAD d0d638af9cd53fccb7102f32fbcabe2e。复现命令见上方英文折叠块。

@wenshao
wenshao added this pull request to the merge queue Aug 29, 2026
Merged via the queue into QwenLM:main with commit 02f2101 Aug 29, 2026
131 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(core): DeepSeek provider forces temperature 0, causing reasoning models (deepseek-v4-flash) to loop in thinking

3 participants