mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +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 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']
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user