Prevent DeepSeek disabled-thinking effort conflicts

DeepSeek-compatible Anthropic endpoints reject requests that combine an explicit disabled-thinking block with an effort value. The desktop UI can create that shape when users disable thinking and keep medium effort selected, so the request builder now omits effort whenever it will send explicit disabled thinking.

Constraint: DeepSeek-compatible Anthropic endpoints reject disabled thinking with effort

Rejected: Only patch Desktop provider settings | hidden title and side request paths still share the request builder

Confidence: high

Scope-risk: narrow

Directive: Keep explicit disabled-thinking providers from sending effort unless the upstream contract changes

Tested: UI repro on pre-fix DeepSeek provider path reproduced the 400

Tested: desktop/scripts/e2e-deepseek-thinking-ui-agent-browser.sh

Tested: bun test src/utils/__tests__/thinking.test.ts src/server/__tests__/provider-presets.test.ts src/server/__tests__/title-service.test.ts

Tested: git diff --check

Not-tested: Real DeepSeek API key live call
This commit is contained in:
程序员阿江(Relakkes) 2026-05-02 12:00:22 +08:00
parent c13fa75c3b
commit a5986eca7e
2 changed files with 345 additions and 12 deletions

View File

@ -0,0 +1,325 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v agent-browser >/dev/null 2>&1; then
echo "agent-browser is required for this UI E2E" >&2
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ARTIFACT_DIR="${ARTIFACT_DIR:-/tmp/cc-haha-e2e-deepseek-thinking-$$}"
CONFIG_DIR="${ARTIFACT_DIR}/config"
UPSTREAM_PORT="${UPSTREAM_PORT:-49391}"
API_PORT="${API_PORT:-49392}"
WEB_PORT="${WEB_PORT:-49393}"
SESSION_NAME="${SESSION_NAME:-cc-haha-deepseek-thinking-ui-$$}"
mkdir -p "${ARTIFACT_DIR}"
UPSTREAM_PID=""
SERVER_PID=""
WEB_PID=""
cleanup() {
agent-browser --session "${SESSION_NAME}" close >/dev/null 2>&1 || true
if [[ -n "${WEB_PID}" ]]; then kill "${WEB_PID}" >/dev/null 2>&1 || true; fi
if [[ -n "${SERVER_PID}" ]]; then kill "${SERVER_PID}" >/dev/null 2>&1 || true; fi
if [[ -n "${UPSTREAM_PID}" ]]; then kill "${UPSTREAM_PID}" >/dev/null 2>&1 || true; fi
wait "${WEB_PID}" >/dev/null 2>&1 || true
wait "${SERVER_PID}" >/dev/null 2>&1 || true
wait "${UPSTREAM_PID}" >/dev/null 2>&1 || true
}
trap cleanup EXIT
ARTIFACT_DIR="${ARTIFACT_DIR}" UPSTREAM_PORT="${UPSTREAM_PORT}" node --input-type=module <<'NODE' &
import http from 'node:http'
import { mkdirSync, writeFileSync } from 'node:fs'
const artifactDir = process.env.ARTIFACT_DIR
const port = Number(process.env.UPSTREAM_PORT)
mkdirSync(artifactDir, { recursive: true })
const captures = []
function record(req, body) {
captures.push({
ts: new Date().toISOString(),
method: req.method,
url: req.url,
headers: req.headers,
body,
})
writeFileSync(`${artifactDir}/captures.json`, JSON.stringify(captures, null, 2))
}
function anthropicSse() {
return [
'event: message_start',
'data: {"type":"message_start","message":{"id":"msg_ui_test","type":"message","role":"assistant","model":"deepseek-v4-pro","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"output_tokens":0}}}',
'',
'event: content_block_start',
'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}',
'',
'event: content_block_delta',
'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}',
'',
'event: content_block_stop',
'data: {"type":"content_block_stop","index":0}',
'',
'event: message_delta',
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":1}}',
'',
'event: message_stop',
'data: {"type":"message_stop"}',
'',
].join('\n')
}
function openaiChatSse() {
return [
'data: {"id":"chatcmpl-ui","object":"chat.completion.chunk","created":1,"model":"deepseek-v4-pro","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}',
'',
'data: {"id":"chatcmpl-ui","object":"chat.completion.chunk","created":1,"model":"deepseek-v4-pro","choices":[{"index":0,"delta":{"content":"ok"},"finish_reason":null}]}',
'',
'data: {"id":"chatcmpl-ui","object":"chat.completion.chunk","created":1,"model":"deepseek-v4-pro","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}',
'',
'data: [DONE]',
'',
].join('\n')
}
http.createServer((req, res) => {
let raw = ''
req.on('data', chunk => { raw += chunk })
req.on('end', () => {
let body = {}
try {
body = raw ? JSON.parse(raw) : {}
} catch {
body = { raw }
}
record(req, body)
const hasConflict =
body?.thinking?.type === 'disabled' &&
body?.output_config?.effort !== undefined
if (hasConflict) {
res.writeHead(400, { 'content-type': 'application/json' })
res.end(JSON.stringify({
error: {
message: 'thinking options type cannot be disabled when reasoning_effort is set',
type: 'invalid_request_error',
param: null,
code: 'invalid_request_error',
},
}))
return
}
if (req.url?.includes('/v1/chat/completions')) {
res.writeHead(200, { 'content-type': 'text/event-stream' })
res.end(openaiChatSse())
return
}
res.writeHead(200, { 'content-type': 'text/event-stream' })
res.end(anthropicSse())
})
}).listen(port, '127.0.0.1', () => {
console.log(`fake upstream listening on ${port}`)
})
NODE
UPSTREAM_PID=$!
(
cd "${ROOT_DIR}"
CLAUDE_CONFIG_DIR="${CONFIG_DIR}" SERVER_PORT="${API_PORT}" \
bun run src/server/index.ts --host 127.0.0.1 --port "${API_PORT}"
) &
SERVER_PID=$!
(
cd "${ROOT_DIR}/desktop"
VITE_DESKTOP_SERVER_URL="http://127.0.0.1:${API_PORT}" \
bun run dev -- --host 127.0.0.1 --port "${WEB_PORT}"
) &
WEB_PID=$!
wait_for_url() {
local url="$1"
for _ in {1..80}; do
if curl -fsS "${url}" >/dev/null 2>&1; then
return 0
fi
sleep 0.25
done
echo "Timed out waiting for ${url}" >&2
exit 1
}
wait_for_url "http://127.0.0.1:${API_PORT}/health"
wait_for_url "http://127.0.0.1:${WEB_PORT}"
AB=(agent-browser --session "${SESSION_NAME}")
APP_URL="http://127.0.0.1:${WEB_PORT}"
UPSTREAM_URL="http://127.0.0.1:${UPSTREAM_PORT}"
"${AB[@]}" open "${APP_URL}" >/dev/null
ui_eval() {
"${AB[@]}" eval "$1" >/dev/null
}
ui_eval "
const buttonByName = (name) => [...document.querySelectorAll('button')]
.find((node) => (node.getAttribute('aria-label') || node.textContent || '').trim() === name);
buttonByName('设置')?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').includes('添加服务商'))
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
const setValue = (el, value) => {
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
const byPlaceholder = (placeholder) => [...document.querySelectorAll('input,textarea')]
.find((node) => node.getAttribute('placeholder') === placeholder);
setValue(byPlaceholder('https://api.example.com/anthropic'), '${UPSTREAM_URL}/anthropic');
setValue(byPlaceholder('sk-...'), 'ui-test-key');
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').trim() === '添加' && !node.disabled)
?.click();
"
"${AB[@]}" wait 700 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').includes('设为默认'))
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.getAttribute('aria-label') || node.textContent || '').includes('通用'))
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').trim() === '中')
?.click();
const thinking = [...document.querySelectorAll('input[type=\"checkbox\"]')]
.find((node) => node.getAttribute('aria-label') === '启用思考模式');
if (thinking?.checked) thinking.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "document.querySelector('button[aria-label=\"Close 设置\"]')?.click();"
"${AB[@]}" wait 300 >/dev/null
"${AB[@]}" fill textarea "Say ok" >/dev/null
"${AB[@]}" eval "[...document.querySelectorAll('button')].find((node)=>node.textContent?.includes('运行') && !node.disabled)?.click();" >/dev/null
"${AB[@]}" wait 5000 >/dev/null
node --input-type=module <<NODE
import { readFileSync } from 'node:fs'
const captures = JSON.parse(readFileSync('${ARTIFACT_DIR}/captures.json', 'utf8'))
const main = captures.find((capture) => capture.url.includes('/anthropic/v1/messages?beta=true'))
if (!main) throw new Error('No DeepSeek Anthropic UI request captured')
if (main.body?.thinking?.type !== 'disabled') throw new Error('DeepSeek request did not disable thinking')
if (main.body?.output_config?.effort !== undefined) {
throw new Error('DeepSeek request still sends output_config.effort with disabled thinking')
}
NODE
ui_eval "
const buttonByName = (name) => [...document.querySelectorAll('button')]
.find((node) => (node.getAttribute('aria-label') || node.textContent || '').trim() === name);
buttonByName('设置')?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').includes('添加服务商'))
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').trim() === 'Custom')
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').includes('Anthropic Messages'))
?.click();
"
"${AB[@]}" wait 100 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').includes('OpenAI Chat Completions'))
?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
const setValue = (el, value) => {
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
const byPlaceholder = (placeholder) => [...document.querySelectorAll('input,textarea')]
.find((node) => node.getAttribute('placeholder') === placeholder);
setValue(byPlaceholder('服务商名称'), 'Custom OpenAI DeepSeek');
setValue(byPlaceholder('https://api.example.com/anthropic'), '${UPSTREAM_URL}');
setValue(byPlaceholder('sk-...'), 'ui-test-key');
setValue(byPlaceholder('Model ID'), 'deepseek-v4-pro');
const sameAsMain = [...document.querySelectorAll('input')]
.filter((node) => node.getAttribute('placeholder') === '与主模型相同');
setValue(sameAsMain[0], 'deepseek-v4-flash');
setValue(sameAsMain[1], 'deepseek-v4-pro');
setValue(sameAsMain[2], 'deepseek-v4-pro');
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.textContent || '').trim() === '添加' && !node.disabled)
?.click();
"
"${AB[@]}" wait 700 >/dev/null
ui_eval "
const buttons = [...document.querySelectorAll('button')]
.filter((node) => (node.textContent || '').includes('设为默认'));
buttons.at(-1)?.click();
"
"${AB[@]}" wait 300 >/dev/null
ui_eval "document.querySelector('button[aria-label=\"Close 设置\"]')?.click();"
"${AB[@]}" wait 300 >/dev/null
ui_eval "
[...document.querySelectorAll('button')]
.find((node) => (node.getAttribute('aria-label') || node.textContent || '').trim() === '新建会话')
?.click();
"
"${AB[@]}" wait 300 >/dev/null
"${AB[@]}" fill textarea "Say ok through custom openai" >/dev/null
"${AB[@]}" eval "[...document.querySelectorAll('button')].find((node)=>node.textContent?.includes('运行') && !node.disabled)?.click();" >/dev/null
"${AB[@]}" wait 5000 >/dev/null
node --input-type=module <<NODE
import { readFileSync } from 'node:fs'
const captures = JSON.parse(readFileSync('${ARTIFACT_DIR}/captures.json', 'utf8'))
const openai = captures.find((capture) => capture.url.includes('/v1/chat/completions'))
if (!openai) throw new Error('No Custom OpenAI Chat UI request captured')
if (openai.body?.thinking !== undefined) throw new Error('OpenAI Chat request leaked Anthropic thinking')
if (openai.body?.output_config !== undefined) throw new Error('OpenAI Chat request leaked Anthropic output_config')
if (openai.body?.reasoning_effort !== undefined) throw new Error('OpenAI Chat request unexpectedly sent reasoning_effort')
NODE
echo "UI E2E passed. Artifacts: ${ARTIFACT_DIR}"

View File

@ -1557,17 +1557,28 @@ async function* queryModel(
: []
const extraBodyParams = getExtraBodyParams(bedrockBetas)
const hasThinking =
thinkingConfig.type !== 'disabled' &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING)
const modelCanThink = modelSupportsThinking(options.model)
const sendsExplicitDisabledThinking =
!(hasThinking && modelCanThink) && shouldSendExplicitDisabledThinking()
const outputConfig: BetaOutputConfig = {
...((extraBodyParams.output_config as BetaOutputConfig) ?? {}),
}
configureEffortParams(
effort,
outputConfig,
extraBodyParams,
betasParams,
options.model,
)
if (sendsExplicitDisabledThinking) {
delete outputConfig.effort
} else {
configureEffortParams(
effort,
outputConfig,
extraBodyParams,
betasParams,
options.model,
)
}
configureTaskBudgetParams(
options.taskBudget,
@ -1594,15 +1605,12 @@ async function* queryModel(
options.maxOutputTokensOverride ||
getMaxOutputTokensForModel(options.model)
const hasThinking =
thinkingConfig.type !== 'disabled' &&
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING)
let thinking: BetaMessageStreamParams['thinking'] | undefined = undefined
// IMPORTANT: Do not change the adaptive-vs-budget thinking selection below
// without notifying the model launch DRI and research. This is a sensitive
// setting that can greatly affect model quality and bashing.
if (hasThinking && modelSupportsThinking(options.model)) {
if (hasThinking && modelCanThink) {
if (
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING) &&
modelSupportsAdaptiveThinking(options.model)
@ -1628,7 +1636,7 @@ async function* queryModel(
type: 'enabled',
} satisfies BetaMessageStreamParams['thinking']
}
} else if (shouldSendExplicitDisabledThinking()) {
} else if (sendsExplicitDisabledThinking) {
thinking = {
type: 'disabled',
} as unknown as BetaMessageStreamParams['thinking']