fix: prevent DeepSeek v4 reasoning chain from leaking as analysis result#193
Conversation
…sis result - Only fall back to reasoning_content for deepseek-reasoner model, not deepseek-v4-flash/v4-pro which also return this field - Extend isDeepSeekReasonerModel() to cover openai-compatible API type - Strip <think> tags in parseAIResponse() to handle models that embed thinking in the content field - Skip reasoning parameter injection in proxy for deepseek-reasoner Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds DeepSeek and MiMo AI provider support across types, DB schema, config routes, sync, settings UI, proxy reasoning injection rules, and AIService model detection, request shaping, response parsing (think-tag stripping), and analysis token limit. ChangesDeepSeek / MiMo Integration
Sequence DiagramsequenceDiagram
participant Client
participant ProxyRoute
participant ExternalAPI
participant AIService
Client->>ProxyRoute: Send AI proxy request (apiType, model, reasoningEffort)
ProxyRoute->>ProxyRoute: Resolve targetUrl and Authorization for openai/deepseek/mimo
alt model is deepseek-reasoner
ProxyRoute->>ProxyRoute: Skip injecting reasoning field
else other model
ProxyRoute->>ProxyRoute: Inject reasoning derived from reasoningEffort
end
ProxyRoute->>ExternalAPI: Forward proxied request
ExternalAPI-->>ProxyRoute: Return response payload
ProxyRoute->>AIService: Forward response for parsing
AIService->>AIService: Strip <think> blocks and extract final content/reasoning
AIService-->>Client: Return parsed content
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
DeepSeek v4-pro/v4-flash models have thinking enabled by default. With
max_tokens=700, all tokens are consumed by reasoning (700 reasoning_tokens),
leaving 0 tokens for content output. This causes empty content and the
thinking chain leaks as the analysis result.
- Add isDeepSeekThinkingModel() to detect DeepSeek models with default thinking
- Explicitly disable thinking with { thinking: { type: 'disabled' } } for these models
- Increase maxTokens from 700 to 1000 for analysis requests
Ref: https://api-docs.deepseek.com/zh-cn/guides/thinking_mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/services/aiService.ts (1)
611-617:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winHandle truncated
<think>blocks too.This only removes balanced
<think>...</think>pairs. The failure mode here was token exhaustion, so a cut-off payload like<think>...is still plausible; in that casecleanedkeeps the reasoning text and the fallback summary path leaks it again. Strip a dangling opening tag through end-of-string as well.🩹 Minimal fix
const cleaned = content .trim() .replace(/<think>[\s\S]*?<\/think>/gi, '') + .replace(/<think>[\s\S]*$/gi, '') .replace(/^```(?:json)?\s*/i, '') .replace(/\s*```$/i, '') .trim();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/aiService.ts` around lines 611 - 617, The current cleaning logic building the variable cleaned only strips balanced <think>...</think> pairs, so a truncated dangling "<think>" remains; update the regex used in the .replace that targets think blocks (the one currently using /<think>[\s\S]*?<\/think>/gi) so it also matches an opening <think> through end-of-string (e.g. make it accept either </think> or end-of-input) and keep the rest of the chaining (.replace(...).replace(...).trim()) unchanged to ensure dangling think blocks are removed before further processing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/services/aiService.ts`:
- Around line 132-136: The current isDeepSeekThinkingModel() marks any model
starting with "deepseek-" (except "deepseek-reasoner") as DeepSeek for both
'openai' and 'openai-compatible' APIs; restrict this by making an explicit
allowlist of DeepSeek models (e.g., ['deepseek-xyz', 'deepseek-abc', ...]) and
only enable the thinking injection when getApiType() === 'openai' OR
(getApiType() === 'openai-compatible' AND model is in that allowlist) so unknown
OpenAI-compatible backends won't receive the unknown thinking field; also update
parseAIResponse() to defensively strip any unbalanced or truncated <think> and
</think> tags (not just balanced pairs) from the raw response before attempting
JSON parse or falling back to summary to prevent leaked tags in fallback text.
---
Outside diff comments:
In `@src/services/aiService.ts`:
- Around line 611-617: The current cleaning logic building the variable cleaned
only strips balanced <think>...</think> pairs, so a truncated dangling "<think>"
remains; update the regex used in the .replace that targets think blocks (the
one currently using /<think>[\s\S]*?<\/think>/gi) so it also matches an opening
<think> through end-of-string (e.g. make it accept either </think> or
end-of-input) and keep the rest of the chaining
(.replace(...).replace(...).trim()) unchanged to ensure dangling think blocks
are removed before further processing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
…nnels Separate DeepSeek official API from generic openai-compatible to avoid sending DeepSeek-specific parameters (thinking, reasoning_content) to third-party proxies. - Add 'deepseek' to AIApiType union - Add DeepSeek option in API Format dropdown with default endpoint - Refactor isDeepSeek*Model() to use apiType === 'deepseek' - Include 'deepseek' in proxy routing and reasoning injection - Fix truncated <think> tag stripping in parseAIResponse() - openai-compatible remains generic, no DeepSeek-specific behavior Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/settings/AIConfigPanel.tsx (2)
422-441: 💤 Low valueConsider extracting help text logic to improve readability.
Similar to the placeholder, the nested ternary for help text is getting complex. While functionally correct, extracting this to a helper would improve maintainability.
♻️ Example refactor approach
const getApiEndpointHelp = (apiType: AIApiType, t: (zh: string, en: string) => string) => { switch (apiType) { case 'openai-compatible': return t('填写完整的API调用地址,包含完整路径', 'Enter the full API endpoint URL including the complete path'); case 'gemini': return t('只填到 v1beta 即可,路径会自动生成', 'Only include the version prefix v1beta, the path will be generated automatically'); case 'deepseek': return t('填写到域名即可,路径会自动生成', 'Only include the domain, the path will be generated automatically'); default: return t('只填到版本号即可(如 .../v1 或 .../v1beta),不要包含 /chat/completions、/responses、/messages', 'Only include the version prefix (e.g. .../v1 or .../v1beta). Do not include /chat/completions, /responses, or /messages.'); } }; // Then in the JSX: <p className="text-xs text-gray-500 dark:text-text-tertiary mt-1"> {getApiEndpointHelp(form.apiType, t)} </p>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/settings/AIConfigPanel.tsx` around lines 422 - 441, The nested ternary used to render the API endpoint help text in AIConfigPanel (based on form.apiType and t) should be extracted into a small helper (e.g., getApiEndpointHelp(apiType: AIApiType, t)) that returns the localized string for each case ('openai-compatible', 'gemini', 'deepseek', default). Add that helper near the top of the component (or export from a nearby util) and replace the ternary in the <p> with a single call to getApiEndpointHelp(form.apiType, t) to improve readability and maintainability.
410-420: 💤 Low valueConsider extracting placeholder logic to improve readability.
The nested ternary now has multiple branches and is becoming harder to maintain. Consider extracting this to a helper function or object lookup.
♻️ Example refactor using object lookup
const PLACEHOLDER_BY_API_TYPE: Record<AIApiType, string> = { 'openai': 'https://api.openai.com/v1', 'openai-responses': 'https://api.openai.com/v1', 'claude': 'https://api.anthropic.com/v1', 'gemini': 'https://generativelanguage.googleapis.com/v1beta', 'deepseek': 'https://api.deepseek.com', 'openai-compatible': 'https://integrate.api.nvidia.com/v1/chat/completions', }; // Then in the JSX: placeholder={PLACEHOLDER_BY_API_TYPE[form.apiType]}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/settings/AIConfigPanel.tsx` around lines 410 - 420, Extract the nested ternary that computes the input placeholder (currently based on form.apiType) into a simple lookup or helper to improve readability and maintainability: create a constant like PLACEHOLDER_BY_API_TYPE: Record<AIApiType, string> (or a function getPlaceholderForApiType(apiType: AIApiType): string) and replace the inline ternary in AIConfigPanel.tsx with placeholder={PLACEHOLDER_BY_API_TYPE[form.apiType]} (or placeholder={getPlaceholderForApiType(form.apiType)}), making sure to include all handled api types (openai, openai-responses, claude, deepseek, openai-compatible, gemini) and a sensible default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 422-441: The nested ternary used to render the API endpoint help
text in AIConfigPanel (based on form.apiType and t) should be extracted into a
small helper (e.g., getApiEndpointHelp(apiType: AIApiType, t)) that returns the
localized string for each case ('openai-compatible', 'gemini', 'deepseek',
default). Add that helper near the top of the component (or export from a nearby
util) and replace the ternary in the <p> with a single call to
getApiEndpointHelp(form.apiType, t) to improve readability and maintainability.
- Around line 410-420: Extract the nested ternary that computes the input
placeholder (currently based on form.apiType) into a simple lookup or helper to
improve readability and maintainability: create a constant like
PLACEHOLDER_BY_API_TYPE: Record<AIApiType, string> (or a function
getPlaceholderForApiType(apiType: AIApiType): string) and replace the inline
ternary in AIConfigPanel.tsx with
placeholder={PLACEHOLDER_BY_API_TYPE[form.apiType]} (or
placeholder={getPlaceholderForApiType(form.apiType)}), making sure to include
all handled api types (openai, openai-responses, claude, deepseek,
openai-compatible, gemini) and a sensible default.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: afc08939-ed67-43b9-84e9-f86bd7ac8a7f
📒 Files selected for processing (5)
server/src/routes/proxy.tssrc/components/settings/AIConfigPanel.tsxsrc/services/aiService.tssrc/types/index.tssrc/utils/apiUrlBuilder.ts
✅ Files skipped from review due to trivial changes (1)
- src/utils/apiUrlBuilder.ts
为了节约token,设置了最大回复token的值。deepseek思考的太用力,把max token耗光了,造成了实际content里没有字了。 |
MiMo models have two official channels with different base URLs: - API (按量付费): api.xiaomimimo.com, key prefix sk- - Token Plan (订阅制): token-plan-cn.xiaomimimo.com, key prefix tp- Add 'mimo' to AIApiType with a mimoPlan sub-option so users can select their billing channel. The thinking:disabled parameter is now only sent for the official mimo API type, not for openai-compatible with 'mimo' in the model name. - Add MiMoPlan type and mimoPlan field to AIConfig - Add MiMo option in API Format dropdown with plan sub-selector - Auto-switch baseUrl based on selected plan - Add mimo_plan column to ai_configs DB table with migration - Map mimo_plan in all CRUD and sync routes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/components/settings/AIConfigPanel.tsx (1)
123-141: 💤 Low valueReset
prevMimoPlanRefalongsideprevApiTypeRefinresetForm.The function resets
prevApiTypeRefat line 140 but doesn't resetprevMimoPlanRef. After editing a MiMo config withtoken-plan, then canceling, the ref retains'token-plan'while the form resets to'api'. This state inconsistency breaks the invariant that refs track previous form values.🔧 Proposed fix
prevApiTypeRef.current = 'openai'; + prevMimoPlanRef.current = 'api'; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/settings/AIConfigPanel.tsx` around lines 123 - 141, The resetForm function resets the form state but only resets prevApiTypeRef.current; also reset prevMimoPlanRef.current to match the cleared form's mimoPlan ('api') to avoid stale ref state. Update resetForm (same function name) to set prevMimoPlanRef.current = 'api' (or to the new form.mimoPlan value) alongside prevApiTypeRef.current so refs remain consistent with the reset form.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 123-141: The resetForm function resets the form state but only
resets prevApiTypeRef.current; also reset prevMimoPlanRef.current to match the
cleared form's mimoPlan ('api') to avoid stale ref state. Update resetForm (same
function name) to set prevMimoPlanRef.current = 'api' (or to the new
form.mimoPlan value) alongside prevApiTypeRef.current so refs remain consistent
with the reset form.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 326711be-d328-4b9f-9a94-efccbe274238
📒 Files selected for processing (8)
server/src/db/schema.tsserver/src/routes/configs.tsserver/src/routes/proxy.tsserver/src/routes/sync.tsserver/src/types/api.tssrc/components/settings/AIConfigPanel.tsxsrc/services/aiService.tssrc/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- server/src/routes/proxy.ts
- src/services/aiService.ts
- Reset prevMimoPlanRef in resetForm to avoid stale ref state - Extract endpoint placeholder and help text from nested ternaries into getEndpointPlaceholder() and getEndpointHelpText() helpers - Truncated <think> tag fix was already in place from prior commit Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/settings/AIConfigPanel.tsx`:
- Around line 66-68: The help text for the 'deepseek' and 'mimo' cases in
AIConfigPanel.tsx is incorrect: it currently says to provide only the domain but
the code/defaults expect a path (e.g., "/v1"). Update the return string for the
switch cases 'deepseek' and 'mimo' (the case handling in AIConfigPanel.tsx) to
instruct users to include the domain and the path (for example: include "/v1" or
the full base path), so the displayed guidance matches the actual endpoint
format used elsewhere.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a026b2e2-946e-447c-a411-af50779deb75
📒 Files selected for processing (1)
src/components/settings/AIConfigPanel.tsx
DeepSeek default has no /v1 (auto-appended), MiMo default includes /v1. Help text now reflects the expected format for each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
server/src/types/api.ts (1)
119-133: 💤 Low valueConsider adding
mimoPlantoSyncAIConfigsRequestfor type completeness.The
AIConfigRownow includesmimo_plan, butSyncAIConfigsRequest(used for the bulk sync API contract) does not includemimoPlan. While the actual route handler inconfigs.tsdoes acceptmimoPlan, the request type definition here is incomplete, which could cause type-checking gaps if this interface is used for validation.Proposed fix
export interface SyncAIConfigsRequest { configs: Array<{ id: string; name: string; apiType?: string; baseUrl: string; apiKey: string; model: string; isActive: boolean; customPrompt?: string; useCustomPrompt?: boolean; concurrency?: number; reasoningEffort?: string; + mimoPlan?: string; }>; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/types/api.ts` around lines 119 - 133, SyncAIConfigsRequest is missing the mimoPlan field present on AIConfigRow and accepted by the configs.ts handler; update the SyncAIConfigsRequest interface to include an optional mimoPlan?: string (matching the server's naming convention mapping from mimo_plan) on each config object so the bulk sync contract aligns with AIConfigRow and the route handler.src/services/aiService.ts (1)
188-196: 💤 Low valueConsider excluding
reasoningfor DeepSeek/MiMo API types.The
reasoning: { effort }parameter on line 195 is OpenAI-specific (for o1/o3 models). ForapiType === 'deepseek'orapiType === 'mimo', this parameter will be ignored by the API but is semantically misplaced. DeepSeek uses thethinkingparameter which you correctly handle on line 196.The current logic is harmless since DeepSeek/MiMo APIs ignore unknown parameters, but you could refine the condition to exclude reasoning for these API types:
-...(!isDeepSeekReasoner && reasoning && apiType !== 'openai-compatible' ? { reasoning } : {}), +...(!isDeepSeekReasoner && reasoning && apiType !== 'openai-compatible' && apiType !== 'deepseek' && apiType !== 'mimo' ? { reasoning } : {}),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/services/aiService.ts` around lines 188 - 196, The request builder is adding the OpenAI-specific reasoning parameter even for DeepSeek/MiMo API types; update the spread that adds reasoning (currently gated by !isDeepSeekReasoner && reasoning && apiType !== 'openai-compatible') to also exclude apiType values for 'deepseek' and 'mimo' (or use the existing flags isDeepSeekReasoner/isMiMoModel) so that reasoning is only sent for true OpenAI-style endpoints; adjust the conditional around the reasoning spread in the function that constructs the request (look for usages of reasoning, isDeepSeekReasoner, isMiMoModel, apiType, thinking) so DeepSeek/MiMo requests receive only thinking and not reasoning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@server/src/types/api.ts`:
- Around line 119-133: SyncAIConfigsRequest is missing the mimoPlan field
present on AIConfigRow and accepted by the configs.ts handler; update the
SyncAIConfigsRequest interface to include an optional mimoPlan?: string
(matching the server's naming convention mapping from mimo_plan) on each config
object so the bulk sync contract aligns with AIConfigRow and the route handler.
In `@src/services/aiService.ts`:
- Around line 188-196: The request builder is adding the OpenAI-specific
reasoning parameter even for DeepSeek/MiMo API types; update the spread that
adds reasoning (currently gated by !isDeepSeekReasoner && reasoning && apiType
!== 'openai-compatible') to also exclude apiType values for 'deepseek' and
'mimo' (or use the existing flags isDeepSeekReasoner/isMiMoModel) so that
reasoning is only sent for true OpenAI-style endpoints; adjust the conditional
around the reasoning spread in the function that constructs the request (look
for usages of reasoning, isDeepSeekReasoner, isMiMoModel, apiType, thinking) so
DeepSeek/MiMo requests receive only thinking and not reasoning.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 060adc94-44bf-4c1f-88ac-bf3ceb074564
📒 Files selected for processing (9)
server/src/db/schema.tsserver/src/routes/configs.tsserver/src/routes/proxy.tsserver/src/routes/sync.tsserver/src/types/api.tssrc/components/settings/AIConfigPanel.tsxsrc/services/aiService.tssrc/types/index.tssrc/utils/apiUrlBuilder.ts
…ncAIConfigsRequest
- reasoning: { effort } is OpenAI-specific, should not be sent to
DeepSeek or MiMo APIs (they use thinking parameter instead)
- Add mimoPlan to SyncAIConfigsRequest for type completeness
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@cyb233 佬友测下,我这边测试都过了。用新的deepseek独立渠道。 |
|
使用了 https://github.com/AmintaCCCP/GithubStarsManager/actions/runs/27006772870 的编译 正好看到有人新提了个 #194 ,或许可以一并考虑看看 |

问题
用户反馈使用
deepseek-v4-flash和deepseek-v4-pro模型分析仓库时,模型的思维链(reasoning chain)被当作分析结果展示,而非实际的 JSON 分析内容。相关讨论:#192
根因分析
问题1:token 耗尽
DeepSeek v4 模型默认开启 thinking mode。
max_tokens包含reasoning_tokens,当设置max_tokens: 700时,所有 700 个 token 被思维链消耗,content为空。问题2:reasoning_content 回退
requestText()的响应解析逻辑无条件回退到reasoning_content,将思维链当作最终结果返回。问题3:特殊参数混在通用路径中
DeepSeek/MiMo 特有参数(
thinking、reasoning_content)散落在openai/openai-compatible代码路径中,通过模型名字符串匹配,可能误伤第三方中转站。修复内容
1. 独立 DeepSeek 官方渠道(
deepseekAPI 类型)apiType = 'deepseek',自动处理thinking/reasoning_contentapiType = 'openai-compatible',通用行为https://api.deepseek.com2. 独立 MiMo 官方渠道(
mimoAPI 类型)MiMo 有两种官方渠道,接入点不同:
api.xiaomimimo.com/v1sk-token-plan-cn.xiaomimimo.com/v1tp-用户选择 MiMo 后,需进一步选择渠道,baseUrl 自动切换。
thinking:disabled仅对官方渠道注入。3. 禁用 DeepSeek/MiMo 的 thinking
对非 reasoner 的 DeepSeek 模型和 MiMo 模型显式发送
{ thinking: { type: 'disabled' } }。4. reasoning_content 回退限制
仅对
deepseek-reasoner模型使用reasoning_content回退。5. 增大 maxTokens
分析请求的 maxTokens 从 700 增至 1000。
6. 修复截断
<think>标签parseAIResponse()增加对截断<think>标签的处理。影响范围
参考
Summary by CodeRabbit
New Features
Bug Fixes
Improvements