mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +08:00
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:
commit
b86c7f1ea2
325
desktop/scripts/e2e-deepseek-thinking-ui-agent-browser.sh
Executable file
325
desktop/scripts/e2e-deepseek-thinking-ui-agent-browser.sh
Executable 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}"
|
||||||
@ -1557,17 +1557,28 @@ async function* queryModel(
|
|||||||
: []
|
: []
|
||||||
const extraBodyParams = getExtraBodyParams(bedrockBetas)
|
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 = {
|
const outputConfig: BetaOutputConfig = {
|
||||||
...((extraBodyParams.output_config as BetaOutputConfig) ?? {}),
|
...((extraBodyParams.output_config as BetaOutputConfig) ?? {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
configureEffortParams(
|
if (sendsExplicitDisabledThinking) {
|
||||||
effort,
|
delete outputConfig.effort
|
||||||
outputConfig,
|
} else {
|
||||||
extraBodyParams,
|
configureEffortParams(
|
||||||
betasParams,
|
effort,
|
||||||
options.model,
|
outputConfig,
|
||||||
)
|
extraBodyParams,
|
||||||
|
betasParams,
|
||||||
|
options.model,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
configureTaskBudgetParams(
|
configureTaskBudgetParams(
|
||||||
options.taskBudget,
|
options.taskBudget,
|
||||||
@ -1594,15 +1605,12 @@ async function* queryModel(
|
|||||||
options.maxOutputTokensOverride ||
|
options.maxOutputTokensOverride ||
|
||||||
getMaxOutputTokensForModel(options.model)
|
getMaxOutputTokensForModel(options.model)
|
||||||
|
|
||||||
const hasThinking =
|
|
||||||
thinkingConfig.type !== 'disabled' &&
|
|
||||||
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_THINKING)
|
|
||||||
let thinking: BetaMessageStreamParams['thinking'] | undefined = undefined
|
let thinking: BetaMessageStreamParams['thinking'] | undefined = undefined
|
||||||
|
|
||||||
// IMPORTANT: Do not change the adaptive-vs-budget thinking selection below
|
// IMPORTANT: Do not change the adaptive-vs-budget thinking selection below
|
||||||
// without notifying the model launch DRI and research. This is a sensitive
|
// without notifying the model launch DRI and research. This is a sensitive
|
||||||
// setting that can greatly affect model quality and bashing.
|
// setting that can greatly affect model quality and bashing.
|
||||||
if (hasThinking && modelSupportsThinking(options.model)) {
|
if (hasThinking && modelCanThink) {
|
||||||
if (
|
if (
|
||||||
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING) &&
|
!isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING) &&
|
||||||
modelSupportsAdaptiveThinking(options.model)
|
modelSupportsAdaptiveThinking(options.model)
|
||||||
@ -1628,7 +1636,7 @@ async function* queryModel(
|
|||||||
type: 'enabled',
|
type: 'enabled',
|
||||||
} satisfies BetaMessageStreamParams['thinking']
|
} satisfies BetaMessageStreamParams['thinking']
|
||||||
}
|
}
|
||||||
} else if (shouldSendExplicitDisabledThinking()) {
|
} else if (sendsExplicitDisabledThinking) {
|
||||||
thinking = {
|
thinking = {
|
||||||
type: 'disabled',
|
type: 'disabled',
|
||||||
} as unknown as BetaMessageStreamParams['thinking']
|
} as unknown as BetaMessageStreamParams['thinking']
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user