Merge verified DeepSeek provider fix into main

The fix was developed and UI-verified in a detached Codex worktree, then merged locally so main carries the same tested request-shape behavior.

Constraint: User requested local main integration without pushing

Confidence: high

Scope-risk: narrow

Tested: git status before merge was clean

Tested: merged commit verification retained in history

Not-tested: Post-merge full suite
This commit is contained in:
程序员阿江(Relakkes) 2026-05-02 12:00:35 +08:00
commit b86c7f1ea2
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']