Skip to content
7 changes: 5 additions & 2 deletions server/src/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ router.post('/api/proxy/ai', async (req, res) => {
'Accept': 'application/json',
};

if (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible') {
if (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible' || apiType === 'deepseek') {
// openai-compatible 类型直接使用 baseUrl 作为完整地址
targetUrl = apiType === 'openai-compatible'
? baseUrl.replace(/\/$/, '')
Expand All @@ -197,11 +197,14 @@ router.post('/api/proxy/ai', async (req, res) => {
targetUrl = urlObj.toString();
}

// DeepSeek Reasoner does not support the reasoning parameter
const isDeepSeekReasoner = model.trim() === 'deepseek-reasoner';
const effectiveRequestBody = (
reasoningEffort
&& !isDeepSeekReasoner
&& typeof requestBody === 'object'
&& requestBody !== null
&& (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible')
&& (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible' || apiType === 'deepseek')
&& !('reasoning' in requestBody)
)
? { ...requestBody, reasoning: { effort: reasoningEffort } }
Expand Down
23 changes: 16 additions & 7 deletions src/components/settings/AIConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const DEFAULT_API_ENDPOINTS: Record<AIApiType, string> = {
'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': '',
};

Expand Down Expand Up @@ -392,6 +393,7 @@ Repository information:
<option value="openai-responses">OpenAI (Responses)</option>
<option value="claude">Claude</option>
<option value="gemini">Gemini</option>
<option value="deepseek">DeepSeek</option>
<option value="openai-compatible">OpenAI Compatible (Custom Endpoint)</option>
</select>
</div>
Expand All @@ -410,9 +412,11 @@ Repository information:
? 'https://api.openai.com/v1'
: form.apiType === 'claude'
? 'https://api.anthropic.com/v1'
: form.apiType === 'openai-compatible'
? 'https://integrate.api.nvidia.com/v1/chat/completions'
: 'https://generativelanguage.googleapis.com/v1beta'
: form.apiType === 'deepseek'
? 'https://api.deepseek.com'
: form.apiType === 'openai-compatible'
? 'https://integrate.api.nvidia.com/v1/chat/completions'
: 'https://generativelanguage.googleapis.com/v1beta'
}
/>
<p className="text-xs text-gray-500 dark:text-text-tertiary mt-1">
Expand All @@ -426,10 +430,15 @@ Repository information:
'只填到 v1beta 即可,路径会自动生成',
'Only include the version prefix v1beta, the path will be generated automatically'
)
: 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.'
)}
: form.apiType === 'deepseek'
? t(
'填写到域名即可,路径会自动生成',
'Only include the domain, the path will be generated automatically'
)
: 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.'
)}
</p>
{form.baseUrl && (
<p className="text-xs text-gray-500 dark:text-text-tertiary mt-1">
Expand Down
37 changes: 31 additions & 6 deletions src/services/aiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,21 @@ export class AIService {
return effort ? { effort } : undefined;
}

private isDeepSeekModel(): boolean {
return this.getApiType() === 'deepseek';
}

private isDeepSeekReasonerModel(): boolean {
return this.getApiType() === 'openai' && this.config.model.trim() === 'deepseek-reasoner';
return this.isDeepSeekModel() && this.config.model.trim() === 'deepseek-reasoner';
}

/**
* Check if the model is a DeepSeek model with default thinking enabled (e.g. deepseek-v4-pro, deepseek-v4-flash).
* These models consume max_tokens for reasoning, leaving 0 tokens for content if max_tokens is too low.
* We need to explicitly disable thinking for these models.
*/
private isDeepSeekThinkingModel(): boolean {
return this.isDeepSeekModel() && this.config.model.trim() !== 'deepseek-reasoner';
}

private isMiMoModel(): boolean {
Expand Down Expand Up @@ -154,14 +167,15 @@ export class AIService {
const configId = this.config.id;
const reasoning = this.getOpenAIReasoningPayload();

if (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible') {
if (apiType === 'openai' || apiType === 'openai-responses' || apiType === 'openai-compatible' || apiType === 'deepseek') {
const messages = [
...(options.system.trim()
? [{ role: 'system', content: options.system }]
: []),
{ role: 'user', content: options.user },
];
const isDeepSeekReasoner = this.isDeepSeekReasonerModel();
const isDeepSeekThinking = this.isDeepSeekThinkingModel();
const isMiMoModel = this.isMiMoModel();

const requestBody = apiType === 'openai-responses'
Expand All @@ -171,15 +185,15 @@ export class AIService {
temperature: options.temperature,
max_output_tokens: options.maxTokens,
...(reasoning ? { reasoning } : {}),
...(isMiMoModel ? { thinking: { type: 'disabled' } } : {}),
...(isMiMoModel || isDeepSeekThinking ? { thinking: { type: 'disabled' } } : {}),
}
: {
model: this.config.model,
messages,
max_tokens: options.maxTokens,
...(!isDeepSeekReasoner ? { temperature: options.temperature } : {}),
...(!isDeepSeekReasoner && reasoning && apiType !== 'openai-compatible' ? { reasoning } : {}),
...(isMiMoModel ? { thinking: { type: 'disabled' } } : {}),
...(isMiMoModel || isDeepSeekThinking ? { thinking: { type: 'disabled' } } : {}),
};

let data: Record<string, unknown>;
Expand Down Expand Up @@ -264,11 +278,18 @@ export class AIService {
return content;
}

// Only fall back to reasoning_content for the dedicated deepseek-reasoner model.
// Other DeepSeek models (e.g. deepseek-v4-flash, deepseek-v4-pro) may also return
// reasoning_content (the thinking chain), but we must not use it as the final answer.
const reasoningContent = message?.reasoning_content;
if (reasoningContent) {
if (reasoningContent && isDeepSeekReasoner) {
this.logAIRequestDebug(startTime, { apiType, model, configId }, { responseLength: reasoningContent.length }, httpDetails);
return reasoningContent;
}

if (!content && reasoningContent) {
logger.warn('ai', 'Model returned reasoning_content but empty content', { model, configId });
}
}

this.logAIRequestDebug(startTime, { apiType, model, configId }, { error: 'request failed' }, httpDetails);
Expand Down Expand Up @@ -479,7 +500,7 @@ ${options.user}` : options.user;
system,
user: prompt,
temperature: 0.3,
maxTokens: 700,
maxTokens: 1000,
signal,
});

Expand Down Expand Up @@ -587,8 +608,12 @@ ${repoInfo}

private parseAIResponse(content: string): { summary: string; tags: string[]; platforms: string[] } {
try {
// Strip thinking tags that some models embed in the content field (e.g. <think>...</think>)
// Also handle truncated tags (dangling <think> without </think>) from token exhaustion
const cleaned = content
.trim()
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<think>[\s\S]*$/gi, '')
.replace(/^```(?:json)?\s*/i, '')
.replace(/\s*```$/i, '')
.trim();
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export interface GitHubUser {
email: string | null;
}

export type AIApiType = 'openai' | 'openai-responses' | 'claude' | 'gemini' | 'openai-compatible';
export type AIApiType = 'openai' | 'openai-responses' | 'claude' | 'gemini' | 'deepseek' | 'openai-compatible';
export type AIReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh';

export type SecretStatus = 'ok' | 'empty' | 'decrypt_failed';
Expand Down
1 change: 1 addition & 0 deletions src/utils/apiUrlBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export function buildFinalApiUrl(baseUrl: string, apiType: AIApiType): string {
return buildApiUrl(baseUrl, 'v1beta/models/{model}:generateContent');
}

// deepseek uses the same Chat Completions endpoint as openai
const pathWithVersion = apiType === 'openai-responses' ? 'v1/responses' : 'v1/chat/completions';
return buildApiUrl(baseUrl, pathWithVersion);
}
Loading