LibreChat/api/server/controllers
Danny Avila 6d9c01927d
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
🧠 refactor: Replay DeepSeek reasoning_content via OpenRouter (#13368)
* 🧠 fix: Replay DeepSeek `reasoning_content` via OpenRouter

DeepSeek's thinking-mode API rejects multi-turn tool-calling requests
unless `reasoning_content` from each tool-bearing assistant message is
replayed verbatim, returning HTTP 400 "The `reasoning_content` in the
thinking mode must be passed back to the API." The agents SDK already
handles this for direct `Providers.DEEPSEEK`, but DeepSeek models routed
via OpenRouter use `Providers.OPENROUTER` — `formatAgentMessages` skipped
the reasoning-preservation branch, and `ChatOpenRouter` left
`includeReasoningContent` unset, so the field silently dropped on every
subsequent turn.

Add `isDeepSeekReasoningProvider(provider, model)` and use it in two
places: (1) `getOpenAILLMConfig` flips `includeReasoningContent: true`
when OpenRouter is dispatching a `deepseek/*` model so the LangChain
client emits the field on assistant turns that have non-empty
`additional_kwargs.reasoning_content`, and (2) `AgentClient` spoofs the
provider hint to `Providers.DEEPSEEK` when calling
`formatAgentMessages`, triggering the SDK's existing
`preserveReasoningContent` path that re-attaches the field to
reconstructed tool-bearing AIMessages. The downstream
`_convertMessagesToOpenAIParams` is already gated on non-empty
`reasoning_content`, so the flag is a no-op outside thinking mode.

Resolves #13366.

* fix: Harden DeepSeek detection against OpenRouter routing edges

Address three Codex review findings on #13368:

1. Strip OpenRouter's `~` latest-routing prefix before applying the
   DeepSeek model regex. `~deepseek-chat` and `~deepseek/r1` were
   previously left unmatched because the regex's start/`/` boundary
   only saw the `~`. Mirror the SDK's `normalizeOpenRouterModel()`
   here and in `getOpenAILLMConfig`.

2. Add a custom-endpoint fallback: when the model id carries the
   unambiguous `deepseek/...` OpenRouter namespace, accept it
   regardless of the resolved provider. Covers the case where a user
   configures OpenRouter under a non-standard endpoint name and
   `initializeAgent` normalizes the unknown provider to `openai`,
   stranding the spoof. Bare `deepseek-*` ids still require an
   explicit DeepSeek/OpenRouter provider so unrelated endpoints
   labelling a model `deepseek-r1` don't trigger.

3. Inspect every agent in `this.agentConfigs` when deciding whether
   to spoof the format provider. Multi-agent handoff runs feed all
   agents' messages through one `formatAgentMessages` call, so a
   DeepSeek handoff under a non-DeepSeek primary previously lost its
   persisted reasoning_content too.

Also addresses Copilot's review note: only pass the options object
to `formatAgentMessages` when the DeepSeek spoof is actually needed,
preserving the pre-fix behavior for everyone else.

* fix: Extend DeepSeek reasoning_content fix to OpenAI-compat agent paths

Address two more Codex P2 findings on #13368:

1. `getOpenAILLMConfig` no longer gates `includeReasoningContent` on
   `useOpenRouter`. Any DeepSeek-style model id (with `~` latest-routing
   prefix stripped) is sufficient. This re-aligns the LLM gate with
   `AgentClient`'s formatter spoof, which already treats a `deepseek/*`
   id as authoritative — so a custom-named OpenRouter endpoint or a
   DeepSeek-compatible proxy gets the field both attached to history AND
   serialized to the wire. Direct `ChatDeepSeek` ignores the flag (its
   own conversion path hardcodes `includeReasoningContent: true`), so
   this is a harmless no-op there.

2. Thread the same `Providers.DEEPSEEK` formatter hint through
   `api/server/controllers/agents/openai.js` and `responses.js` (the
   OpenAI-/Responses-compatible serving paths). Without it those paths
   restored `additional_kwargs.reasoning_content` only in `AgentClient`
   while the LLM config flipped `includeReasoningContent` on for them
   too — so DeepSeek tool turns served from those endpoints would still
   ship requests with the flag set but no field present, hitting the
   same second-turn 400. The `needsDeepSeekFormatHint` helper in
   `openai.js` mirrors `AgentClient`'s per-agent check.

* fix: Tighten DeepSeek detection and cover handoff sub-agents

Address four more Codex P2 findings on #13368:

- Tighten the DeepSeek model regex to `^deepseek(?:[-/]|$)/i` (anchored
  to start). Rejects cloned/distilled slugs like
  `mistral/deepseek-distilled-foo` and `community/deepseek-r1` that
  previously matched via the `(?:^|/)` alternation, which could attach
  the DeepSeek-only `reasoning_content` field on proxies that don't
  accept it.

- Anchoring also collapses the namespace-only fallback into the same
  pattern, so bare `deepseek-chat` / `deepseek-reasoner` on a
  custom OpenAI-compatible DeepSeek proxy are now recognized — fixing
  the asymmetry where `getOpenAILLMConfig` would flip
  `includeReasoningContent` for those bare ids but `AgentClient`
  wouldn't pass the formatter hint.

- Extend `needsDeepSeekFormatHint` in `openai.js` (and the inline
  check in `responses.js`) to walk `handoffAgentConfigs` too. In
  multi-agent runs where the primary isn't DeepSeek but a connected
  handoff agent is, the SDK's `formatAgentMessages` previously dropped
  the handoff's persisted reasoning_content before the next tool turn,
  preserving the 400 the PR was meant to prevent.

- Mirror the regex change in `getOpenAILLMConfig`.

Out of scope: the OpenAI-compatible serving paths still don't
preserve incoming `reasoning_content`/`reasoning` fields in
`convertMessages`, nor does the Responses API persist reasoning in
`saveResponseOutput`. Those are deeper persistence/conversion fixes
worth a separate PR.

* test: Allow includeReasoningContent for Azure-serverless DeepSeek

CI surfaced a backward-compat expectation that snapshotted the
pre-fix behavior. Azure-serverless DeepSeek deployments (e.g.
`DeepSeek-R1`) forward to the same DeepSeek thinking-mode tool-call
contract, so the LLM gate now correctly flips
`includeReasoningContent: true` for them too. The downstream
gate on a non-empty `additional_kwargs.reasoning_content` keeps
this a no-op outside thinking mode.

* chore: Trim noisy comments

Per CLAUDE.md ("self-documenting code; no inline comments narrating
what code does"), strip the multi-paragraph rationale that crept into
the DeepSeek reasoning_content fix. The commit history and PR
description carry the why; the code says the what.

Keeps one single-line JSDoc on `isDeepSeekReasoningProvider` (linking
to the DeepSeek docs) and a `(#13366)` tag on each opt-in site so
future readers can find the context.

* revert: Drop non-functional DeepSeek hint from OpenAI-compat serving paths

Codex's later review passes correctly flagged that threading the
DeepSeek formatter hint through openai.js (`/v1/chat/completions`) and
responses.js (`/v1/responses`) doesn't actually fix the second-turn
400 in those paths. Empirical check against the real SDK confirmed the
gap is deeper and pre-existing:

  formatAgentMessages(payload, ..., { provider: DEEPSEEK })

where payload is the `convertMessages`/`convertInputToMessages` output
shape (string content + TOP-LEVEL `tool_calls`) produces NO tool-bearing
AIMessage at all — `formatAssistantMessage` only reconstructs tool calls
from `tool_call`-typed *content parts*, never a top-level `tool_calls`
field. So those serving paths don't reconstruct tool-call history (let
alone reasoning) regardless of the hint. The Responses persistence layer
likewise stores only output text, not tool calls or reasoning.

Making those paths work requires reworking the wire->internal message
conversion (and Responses persistence) to emit content-part arrays — a
broad, pre-existing concern beyond this issue and risky to land here.
Rather than ship a hint that looks like a fix but is inert, revert the
serving-path changes and scope this PR to the validated AgentClient
chat path (the actual surface in #13366).

Reverts the openai.js/responses.js threading and their spec mocks to
main. Keeps the AgentClient fix, `isDeepSeekReasoningProvider`, the
`getOpenAILLMConfig` flag, and the type.
2026-05-28 22:10:49 -07:00
..
__tests__ 🧯 fix: Harden Data Retention Semantics (#13049) 2026-05-19 21:58:42 -04:00
agents 🧠 refactor: Replay DeepSeek reasoning_content via OpenRouter (#13368) 2026-05-28 22:10:49 -07:00
assistants 🔐 feat: Add Signed CloudFront File Downloads (#12970) 2026-05-06 19:48:30 -04:00
auth 🤝 fix: Honor OPENID_REUSE_TOKENS in Admin OAuth Exchange (#13154) 2026-05-18 09:34:58 -04:00
AuthController.js 🛡️ fix: Harden OpenID Session Token Reuse (#13086) 2026-05-11 23:29:01 -04:00
AuthController.spec.js 🛡️ fix: Harden OpenID Session Token Reuse (#13086) 2026-05-11 23:29:01 -04:00
Balance.js
EndpointController.js
FavoritesController.js 📌 feat: Add Pin Support for Model Specs (#11219) 2026-04-09 18:37:25 -04:00
FavoritesController.spec.js 📌 feat: Add Pin Support for Model Specs (#11219) 2026-04-09 18:37:25 -04:00
mcp.js 🪪 fix: Prevent MCP Server Name Collisions (#13256) 2026-05-22 20:46:14 -04:00
ModelController.js 🏗️ refactor: Remove Redundant Caching, Migrate Config Services to TypeScript (#12466) 2026-03-30 16:49:48 -04:00
PermissionsController.js 🛡️ fix: Escape People Picker Search Regex (#13169) 2026-05-18 09:04:31 -04:00
PluginController.js 🪪 fix: Resolve Group-Scoped Config Overrides (#13176) 2026-05-18 10:16:20 -04:00
PluginController.spec.js 🪪 fix: Resolve Group-Scoped Config Overrides (#13176) 2026-05-18 10:16:20 -04:00
SkillStatesController.js 🎚️ feat: Per-User Skill Active/Inactive Toggle with Ownership-Aware Defaults (#12692) 2026-04-25 04:02:00 -04:00
tools.js 🧯 fix: Harden Data Retention Semantics (#13049) 2026-05-19 21:58:42 -04:00
TwoFactorController.js
UserController.js 🪪 fix: Resolve Group-Scoped Config Overrides (#13176) 2026-05-18 10:16:20 -04:00
UserController.spec.js 🧯 fix: Harden Data Retention Semantics (#13049) 2026-05-19 21:58:42 -04:00