From df5bcd87b5024dfaa3c29380ebf095fafbdf160b Mon Sep 17 00:00:00 2001 From: john Date: Wed, 29 Jul 2026 09:19:08 +0800 Subject: [PATCH] fix: route stable production goosed through DeepSeek no-think proxy DeepSeek V4 tool rounds fail with reasoning_content errors when stable goosed hits api.deepseek.com directly. Default stable portal startup to the 18036 compat proxy and add a standalone recovery script for 103 ops. Co-authored-by: Cursor --- .../deepseek-production-parity.test.mjs | 4 + scripts/fix-deepseek-no-think-prod.mjs | 140 ++++++++++++++++++ scripts/run-memind-portal-prod.sh | 6 + 3 files changed, 150 insertions(+) create mode 100644 scripts/fix-deepseek-no-think-prod.mjs diff --git a/release-gate/deepseek-production-parity.test.mjs b/release-gate/deepseek-production-parity.test.mjs index d75271b..5bb6004 100644 --- a/release-gate/deepseek-production-parity.test.mjs +++ b/release-gate/deepseek-production-parity.test.mjs @@ -56,6 +56,7 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con const [ builder, localStack, + stableRunner, candidateRunner, compatRunner, canaryRelease, @@ -63,6 +64,7 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con ] = await Promise.all([ fs.readFile(path.join(ROOT, 'scripts', 'build-portal-runtime.mjs'), 'utf8'), fs.readFile(path.join(ROOT, 'release-gate', 'local-stack.mjs'), 'utf8'), + fs.readFile(path.join(ROOT, 'scripts', 'run-memind-portal-prod.sh'), 'utf8'), fs.readFile(path.join(ROOT, 'scripts', 'run-memind-portal-candidate.sh'), 'utf8'), fs.readFile( path.join(ROOT, 'scripts', 'run-deepseek-compat-proxy-candidate.sh'), @@ -77,6 +79,8 @@ test('Gate, artifact, candidate routing and rollback share one compatibility con localStack, /path\.join\(resolvedPortalRoot, 'deepseek-no-think-proxy\.mjs'\)/, ); + assert.match(stableRunner, /export MEMIND_DEEPSEEK_DISABLE_THINKING="\$\{MEMIND_DEEPSEEK_DISABLE_THINKING:-1\}"/); + assert.match(stableRunner, /export MEMIND_GOOSED_HOST_GATEWAY="\$\{MEMIND_GOOSED_HOST_GATEWAY:-host\.docker\.internal\}"/); assert.match(candidateRunner, /export MEMIND_DEEPSEEK_DISABLE_THINKING=1/); assert.match(candidateRunner, /export MEMIND_GOOSED_HOST_GATEWAY=host\.docker\.internal/); assert.match(compatRunner, /source "\$\{STABLE_ROOT\}\/\.env"/); diff --git a/scripts/fix-deepseek-no-think-prod.mjs b/scripts/fix-deepseek-no-think-prod.mjs new file mode 100644 index 0000000..1ac096c --- /dev/null +++ b/scripts/fix-deepseek-no-think-prod.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * Standalone recovery: point every stable goosed target at the 18036 no-think proxy. + * Works on bundled 103 runtime (no db.mjs / llm-providers.mjs required). + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Agent, fetch as undiciFetch } from 'undici'; + +const GOOSED_PROVIDER_ID = 'custom_memind_deepseek_no_think'; +const DEFAULT_MODEL = 'deepseek-v4-pro'; +const MODELS = ['deepseek-v4-pro', 'deepseek-v4-flash']; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const insecureDispatcher = new Agent({ connect: { rejectUnauthorized: false } }); + +function loadEnvFile(filePath) { + const env = {}; + if (!fs.existsSync(filePath)) return env; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq < 0) continue; + const key = trimmed.slice(0, eq).trim(); + let value = trimmed.slice(eq + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) + || (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + env[key] = value; + } + return env; +} + +const fileEnv = loadEnvFile(path.join(root, '.env')); +const env = { ...fileEnv, ...process.env }; +const apiSecret = String(env.TKMIND_SERVER__SECRET_KEY ?? '').trim(); +const apiKey = String(env.DEEPSEEK_API_KEY ?? '').trim(); +const apiTargets = String( + env.TKMIND_API_TARGETS + ?? env.TKMIND_API_TARGET + ?? 'https://127.0.0.1:18006', +) + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + +if (!apiSecret) { + console.error('missing TKMIND_SERVER__SECRET_KEY'); + process.exit(1); +} +if (!apiKey) { + console.error('missing DEEPSEEK_API_KEY'); + process.exit(1); +} + +const host = String(env.MEMIND_GOOSED_HOST_GATEWAY ?? 'host.docker.internal').trim() || 'host.docker.internal'; +const port = Number(env.MEMIND_DEEPSEEK_NO_THINK_PORT ?? 18036); +const proxyBaseUrl = `http://${host}:${port}/v1`; + +async function goosedFetch(apiTarget, pathname, init = {}) { + const url = new URL(pathname, apiTarget); + const headers = { + ...(init.headers ?? {}), + 'X-Secret-Key': apiSecret, + }; + if (init.body && !headers['Content-Type']) { + headers['Content-Type'] = 'application/json'; + } + const dispatcher = apiTarget.startsWith('https://') ? insecureDispatcher : undefined; + return undiciFetch(url, { ...init, headers, dispatcher }); +} + +async function upsertConfig(apiTarget, key, value, isSecret = false) { + const res = await goosedFetch(apiTarget, '/config/upsert', { + method: 'POST', + body: JSON.stringify({ key, value, is_secret: isSecret }), + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`upsert ${key} on ${apiTarget} failed: ${res.status} ${text}`); + } +} + +async function upsertProvider(apiTarget) { + const body = { + engine: 'openai', + display_name: 'memind_deepseek_no_think', + api_url: proxyBaseUrl, + api_key: apiKey, + models: MODELS, + supports_streaming: true, + requires_auth: true, + preserves_thinking: false, + }; + + let res = await goosedFetch( + apiTarget, + `/config/custom-providers/${encodeURIComponent(GOOSED_PROVIDER_ID)}`, + { method: 'PUT', body: JSON.stringify(body) }, + ); + if (!res.ok) { + const text = await res.text().catch(() => ''); + const missing = res.status === 404 || /provider not found/i.test(text); + if (!missing) { + throw new Error(`upsert provider on ${apiTarget} failed: ${res.status} ${text}`); + } + res = await goosedFetch(apiTarget, '/config/custom-providers', { + method: 'POST', + body: JSON.stringify(body), + }); + } + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`create provider on ${apiTarget} failed: ${res.status} ${text}`); + } + + await upsertConfig(apiTarget, 'DEEPSEEK_API_KEY', apiKey, true); + for (const [key, value] of [ + ['GOOSE_PROVIDER', GOOSED_PROVIDER_ID], + ['GOOSE_MODEL', DEFAULT_MODEL], + ['TKMIND_PROVIDER', GOOSED_PROVIDER_ID], + ['TKMIND_MODEL', DEFAULT_MODEL], + ['GOOSE_THINKING_EFFORT', 'off'], + ]) { + await upsertConfig(apiTarget, key, value, false); + } +} + +const results = []; +for (const target of apiTargets) { + await upsertProvider(target); + results.push({ target, provider: GOOSED_PROVIDER_ID, model: DEFAULT_MODEL, proxyBaseUrl }); +} + +console.log(JSON.stringify({ ok: true, results }, null, 2)); diff --git a/scripts/run-memind-portal-prod.sh b/scripts/run-memind-portal-prod.sh index 88d27d0..1908eab 100755 --- a/scripts/run-memind-portal-prod.sh +++ b/scripts/run-memind-portal-prod.sh @@ -26,6 +26,12 @@ fi export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-${NODE_BIN}}" export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}" +# DeepSeek V4 tool rounds require reasoning_content replay; stable goosed must use the +# host no-think compat proxy (see docs/发包必看.md §4.7). +export MEMIND_DEEPSEEK_DISABLE_THINKING="${MEMIND_DEEPSEEK_DISABLE_THINKING:-1}" +export MEMIND_DEEPSEEK_NO_THINK_PORT="${MEMIND_DEEPSEEK_NO_THINK_PORT:-18036}" +export MEMIND_GOOSED_HOST_GATEWAY="${MEMIND_GOOSED_HOST_GATEWAY:-host.docker.internal}" + free_port_if_stale_memind_listener() { local port="${H5_PORT:-8081}" local pids