Skip to content

fix(openai): replay DeepSeek tool reasoning_content#71473

Closed
Sanjays2402 wants to merge 2 commits into
openclaw:mainfrom
Sanjays2402:fix/71455-deepseek-reasoning-content
Closed

fix(openai): replay DeepSeek tool reasoning_content#71473
Sanjays2402 wants to merge 2 commits into
openclaw:mainfrom
Sanjays2402:fix/71455-deepseek-reasoning-content

Conversation

@Sanjays2402

Copy link
Copy Markdown
Contributor

Fixes #71455.

Summary

  • detect DeepSeek-family OpenAI-compatible models by model id, so Ollama/custom hosted deepseek-* models get DeepSeek thinking compat
  • inject blank reasoning_content: "" on assistant messages with replayed tool_calls when DeepSeek thinking mode is active
  • preserve the disabled-thinking path without adding reasoning_content

Test plan

  • pnpm test src/agents/openai-transport-stream.test.ts extensions/deepseek/index.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8b1b6281a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/openai-completions-compat.ts Outdated
Comment on lines +75 to +77
const providerOrFamilyIsDeepSeek =
isDefaultRouteProvider(input.provider, "deepseek") || knownProviderFamily === "deepseek";
const isDeepSeek = endpointClass === "deepseek-native" || providerOrFamilyIsDeepSeek;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Keep OpenRouter models on openrouter thinking format

This change makes any model whose ID contains deepseek evaluate as isDeepSeek, which takes precedence over isOpenRouterLike in resolveOpenAICompletionsCompatDefaults. For OpenRouter DeepSeek models (for example openrouter/deepseek/... used in this repo), thinkingFormat now flips from openrouter to deepseek, so buildOpenAICompletionsParams stops sending OpenRouter's reasoning object and instead sends reasoning_effort plus injected reasoning_content; that can break reasoning controls or request compatibility on OpenRouter routes.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds DeepSeek thinking-mode compatibility for Ollama/custom-hosted deepseek-* models by detecting them via model ID and injecting a blank reasoning_content: "" on replayed tool-call assistant messages.

  • OpenRouter regression: resolveKnownProviderFamily now returns "deepseek" for any model ID containing "deepseek" before the provider switch executes. Because thinkingFormat resolves isDeepSeek before isOpenRouterLike, OpenRouter-hosted DeepSeek models (e.g. deepseek-r1 via provider: "openrouter") silently switch from thinkingFormat: "openrouter" to thinkingFormat: "deepseek", causing params.reasoning (OpenRouter's required thinking field) to be omitted and the DeepSeek reasoning_content injection to run instead — breaking OpenRouter thinking for those models.

Confidence Score: 3/5

Not safe to merge as-is — the model-ID heuristic placed before the provider switch breaks thinkingFormat for OpenRouter-hosted DeepSeek models.

One P1 regression: OpenRouter DeepSeek models lose proper params.reasoning setup and get an incorrect thinkingFormat, silently disabling thinking on that provider. The core fix (Ollama DeepSeek tool-call injection) is correct, but the detection scope is too broad.

src/agents/provider-attribution.ts — the early-return model-ID check in resolveKnownProviderFamily needs to be moved to the default branch to avoid overriding known non-DeepSeek providers like openrouter.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/provider-attribution.ts
Line: 344-348

Comment:
**Model-ID check shadows provider for OpenRouter-hosted DeepSeek models**

The early-return on `normalizedModelId?.includes("deepseek")` runs before the `provider` switch, so an OpenRouter model like `deepseek-r1` gets `knownProviderFamily = "deepseek"` instead of `"openrouter"`. Back in `openai-completions-compat.ts`, `thinkingFormat` resolves `isDeepSeek` before `isOpenRouterLike`, so those models end up with `thinkingFormat = "deepseek"` rather than `"openrouter"`. As a result, `params.reasoning = { effort: ... }` (OpenRouter's thinking field) is never populated, `params.reasoning_effort` is set instead (which OpenRouter ignores for reasoning), and the DeepSeek `reasoning_content` injection runs unnecessarily. Thinking stops working for any OpenRouter-hosted DeepSeek model.

A safer fix is to move the model-ID heuristic into the `default` branch so it only fires for unknown/generic providers:

```typescript
default:
  if (normalizedModelId?.includes("deepseek")) return "deepseek";
  return provider || "unknown";
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/openai-completions-compat.ts
Line: 75-77

Comment:
**`isDefaultRoute` guard silently dropped from `isDefaultRouteProvider` check**

The original condition was `isDefaultRoute && isDefaultRouteProvider(input.provider, "deepseek")`, so a user with `provider: "deepseek"` and a custom `baseUrl` (making `isDefaultRoute = false`) was not treated as `isDeepSeek`. The new line removes that guard, meaning any custom-baseUrl configuration that still carries `provider: "deepseek"` now gets `isDeepSeek = true`. This could apply DeepSeek-specific compat settings (e.g. `thinkingFormat: "deepseek"`, the new `reasoning_content` injection) to proxies or mirrors of the official DeepSeek API that may not support those DeepSeek extensions.

If the intent is to support the full `provider="deepseek"` + custom baseUrl combo, a comment explaining why the guard was intentionally removed would help future readers avoid restoring it accidentally.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(openai): replay deepseek tool reason..." | Re-trigger Greptile

Comment thread src/agents/provider-attribution.ts Outdated
Comment on lines +344 to +348
function resolveKnownProviderFamily(provider: string | undefined, modelId?: string | null): string {
const normalizedModelId = normalizeOptionalLowercaseString(modelId);
if (normalizedModelId?.includes("deepseek")) {
return "deepseek";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Model-ID check shadows provider for OpenRouter-hosted DeepSeek models

The early-return on normalizedModelId?.includes("deepseek") runs before the provider switch, so an OpenRouter model like deepseek-r1 gets knownProviderFamily = "deepseek" instead of "openrouter". Back in openai-completions-compat.ts, thinkingFormat resolves isDeepSeek before isOpenRouterLike, so those models end up with thinkingFormat = "deepseek" rather than "openrouter". As a result, params.reasoning = { effort: ... } (OpenRouter's thinking field) is never populated, params.reasoning_effort is set instead (which OpenRouter ignores for reasoning), and the DeepSeek reasoning_content injection runs unnecessarily. Thinking stops working for any OpenRouter-hosted DeepSeek model.

A safer fix is to move the model-ID heuristic into the default branch so it only fires for unknown/generic providers:

default:
  if (normalizedModelId?.includes("deepseek")) return "deepseek";
  return provider || "unknown";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/provider-attribution.ts
Line: 344-348

Comment:
**Model-ID check shadows provider for OpenRouter-hosted DeepSeek models**

The early-return on `normalizedModelId?.includes("deepseek")` runs before the `provider` switch, so an OpenRouter model like `deepseek-r1` gets `knownProviderFamily = "deepseek"` instead of `"openrouter"`. Back in `openai-completions-compat.ts`, `thinkingFormat` resolves `isDeepSeek` before `isOpenRouterLike`, so those models end up with `thinkingFormat = "deepseek"` rather than `"openrouter"`. As a result, `params.reasoning = { effort: ... }` (OpenRouter's thinking field) is never populated, `params.reasoning_effort` is set instead (which OpenRouter ignores for reasoning), and the DeepSeek `reasoning_content` injection runs unnecessarily. Thinking stops working for any OpenRouter-hosted DeepSeek model.

A safer fix is to move the model-ID heuristic into the `default` branch so it only fires for unknown/generic providers:

```typescript
default:
  if (normalizedModelId?.includes("deepseek")) return "deepseek";
  return provider || "unknown";
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/agents/openai-completions-compat.ts Outdated
Comment on lines +75 to +77
const providerOrFamilyIsDeepSeek =
isDefaultRouteProvider(input.provider, "deepseek") || knownProviderFamily === "deepseek";
const isDeepSeek = endpointClass === "deepseek-native" || providerOrFamilyIsDeepSeek;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 isDefaultRoute guard silently dropped from isDefaultRouteProvider check

The original condition was isDefaultRoute && isDefaultRouteProvider(input.provider, "deepseek"), so a user with provider: "deepseek" and a custom baseUrl (making isDefaultRoute = false) was not treated as isDeepSeek. The new line removes that guard, meaning any custom-baseUrl configuration that still carries provider: "deepseek" now gets isDeepSeek = true. This could apply DeepSeek-specific compat settings (e.g. thinkingFormat: "deepseek", the new reasoning_content injection) to proxies or mirrors of the official DeepSeek API that may not support those DeepSeek extensions.

If the intent is to support the full provider="deepseek" + custom baseUrl combo, a comment explaining why the guard was intentionally removed would help future readers avoid restoring it accidentally.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/openai-completions-compat.ts
Line: 75-77

Comment:
**`isDefaultRoute` guard silently dropped from `isDefaultRouteProvider` check**

The original condition was `isDefaultRoute && isDefaultRouteProvider(input.provider, "deepseek")`, so a user with `provider: "deepseek"` and a custom `baseUrl` (making `isDefaultRoute = false`) was not treated as `isDeepSeek`. The new line removes that guard, meaning any custom-baseUrl configuration that still carries `provider: "deepseek"` now gets `isDeepSeek = true`. This could apply DeepSeek-specific compat settings (e.g. `thinkingFormat: "deepseek"`, the new `reasoning_content` injection) to proxies or mirrors of the official DeepSeek API that may not support those DeepSeek extensions.

If the intent is to support the full `provider="deepseek"` + custom baseUrl combo, a comment explaining why the guard was intentionally removed would help future readers avoid restoring it accidentally.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Apr 25, 2026
@Sanjays2402

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 57ade55403:

  • moved the deepseek model-id heuristic so it only applies for unknown/custom provider IDs
  • restored the default-route guard for provider: "deepseek" while still supporting local/custom DeepSeek-family model IDs via knownProviderFamily
  • added a regression test proving OpenRouter-hosted DeepSeek models keep params.reasoning = { effort }, do not send reasoning_effort, and do not receive DeepSeek reasoning_content injection

Validation: pnpm test src/agents/openai-transport-stream.test.ts extensions/deepseek/index.test.ts → 93/93 passing.

@steipete

Copy link
Copy Markdown
Contributor

Codex maintainer review: closing this as not ready / not proven against a current failing path.

I tested the PR head with the advertised focused suite:

pnpm test src/agents/openai-transport-stream.test.ts extensions/deepseek/index.test.ts

Result: passed (openai-transport-stream.test.ts: 84 passed, extensions/deepseek/index.test.ts: 8 passed).

I also tested the available local Ollama paths:

  • Local Ollama is available (0.21.2).
  • deepseek-r1:8b-0528-qwen3-fp16 exists locally, but Ollama rejects the request before this bug shape because that model does not support tools: does not support tools.
  • deepseek-v4-flash-cn-think is not available locally (model not found).
  • Ollama Cloud Kimi is available and tool/thinking capable here:
    • kimi-k2.5:cloud accepted replay with reasoning_content: "".
    • kimi-k2.5:cloud also accepted the same replay without reasoning_content.
    • kimi-k2.6:cloud also accepted replay without reasoning_content.

So I cannot reproduce the linked #71455 failure on current available Ollama paths, and the Kimi/Ollama part of the claim does not match live behavior here.

Implementation concern: this patch promotes any model id containing deepseek into the DeepSeek provider family inside generic provider attribution / OpenAI-compatible transport handling. That is too broad for a maintainer merge: it can affect unrelated routed providers and aggregators based only on model-name substring, instead of keeping the behavior owned by a provider/plugin path with a real live regression.

If this still fails for a specific Ollama-hosted DeepSeek V4 model, please reopen with the exact model id that is available through Ollama, the request/response error, and ideally a live repro we can run. A safer fix would target that provider/model path explicitly and include a regression that proves the failing replay shape.

@steipete steipete closed this Apr 25, 2026
@steipete

Copy link
Copy Markdown
Contributor

Follow-up Mac Studio live check after maintainer request:

  • Installed Ollama on peters-mac-studio-1 and started the local service.
  • The machine already had deepseek-r1:671b installed, but that model reports only completion + thinking capabilities and rejects tool calls, so it cannot validate this issue shape.
  • Tested the modern Ollama DeepSeek cloud route that does have tools + thinking capability: deepseek-v3.2:cloud.

Replay results through Ollama /v1/chat/completions:

  • deepseek-v3.2:cloud with prior assistant tool_calls and reasoning_content: "": accepted, returned ok.
  • deepseek-v3.2:cloud with the same prior assistant tool_calls and no reasoning_content: accepted, returned ok.
  • deepseek-r1:671b with tools: rejected before replay semantics with does not support tools.

So even on the beefy Mac Studio / Ollama DeepSeek path, I still cannot reproduce a current Ollama-side requirement for reasoning_content on replayed tool-call assistant messages. This reinforces the close: the PR's broad model-id substring attribution change is not justified by live behavior.

@steipete

Copy link
Copy Markdown
Contributor

Detailed closeout / live-test matrix:

Local machine (clawdbot6):

  • Ollama was already installed and serving: 0.21.2.

  • .profile had DEEPSEEK_API_KEY and MOONSHOT_API_KEY, but no OLLAMA_API_KEY env var. The local Ollama CLI was still signed in enough to access Ollama cloud models.

  • PR-head focused tests passed:

    pnpm test src/agents/openai-transport-stream.test.ts -t "DeepSeek thinking tool-call replay|thinking is disabled"
    pnpm test src/agents/openai-transport-stream.test.ts extensions/deepseek/index.test.ts

    Results: 2 focused tests passed; full advertised focused suite passed (openai-transport-stream.test.ts: 84 passed, extensions/deepseek/index.test.ts: 8 passed).

  • Local deepseek-r1:8b-0528-qwen3-fp16 existed, but could not validate the issue because Ollama rejected tool usage before replay semantics:

    registry.ollama.ai/library/deepseek-r1:8b-0528-qwen3-fp16 does not support tools
    
  • deepseek-v4-flash-cn-think was not available locally:

    model not found
    
  • Tested available Ollama Cloud Kimi paths because [Bug]: DeepSeek V4 thinking mode causes HTTP 400 "reasoning_content must be passed back" on tool calls — sessions freeze unrecoverably #71455 amended the report to Kimi/Ollama:

    • kimi-k2.5:cloud with replayed assistant tool_calls + reasoning_content: "": accepted, returned ok.
    • kimi-k2.5:cloud with replayed assistant tool_calls and no reasoning_content: accepted, returned ok.
    • kimi-k2.6:cloud with replayed assistant tool_calls and no reasoning_content: accepted, returned ok.

Mac Studio (peters-mac-studio-1):

  • Installed Ollama via Homebrew and started the service.

  • Verified the host is the right heavy box: 512 GB RAM, plenty of free disk.

  • The machine already had deepseek-r1:671b installed, but that model reports only completion + thinking capabilities and rejects tools, so it cannot validate a tool-call replay bug:

    registry.ollama.ai/library/deepseek-r1:671b does not support tools
    
  • Tested deepseek-v3.2:cloud, which Ollama reports as completion + tools + thinking and is the current Ollama DeepSeek cloud route matching the required capability shape:

    • prior assistant tool_calls + reasoning_content: "": accepted, returned ok.
    • prior assistant tool_calls with no reasoning_content: accepted, returned ok.

Conclusion:

I could not reproduce the claimed current Ollama failure on any available tool-capable Ollama route. The PR's production change promotes any model id containing deepseek into the DeepSeek provider family inside generic provider attribution / OpenAI-compatible transport handling. That is too broad without a live failing provider/model path: it can affect unrelated routed providers and aggregators by substring rather than keeping the behavior provider-owned.

Closing remains correct. If a specific Ollama-hosted DeepSeek model still fails, we need the exact Ollama model id, the failing request/response error, and a live repro against that route before accepting a core transport change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DeepSeek V4 thinking mode causes HTTP 400 "reasoning_content must be passed back" on tool calls — sessions freeze unrecoverably

2 participants