@mathew-macharia/black-mamba 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -8
- package/index.js +38 -19
- package/package.json +1 -1
- package/src/accounts.js +83 -24
- package/src/completions.js +149 -0
- package/src/config.js +1 -9
- package/src/deepseek/classify.js +13 -13
- package/src/deepseek/client.js +24 -23
- package/src/errors.js +0 -11
- package/src/{server/http.js → http.js} +16 -50
- package/src/{server/pacing.js → pacing.js} +1 -1
- package/src/{pi/prompt.js → prompt.js} +67 -46
- package/src/{pi/request.js → request.js} +4 -42
- package/src/{pi/response.js → response.js} +4 -21
- package/docs/accounts.md +0 -37
- package/docs/config.md +0 -40
- package/docs/http.md +0 -171
- package/docs/install.md +0 -68
- package/docs/mapping.md +0 -11
- package/docs/overview.md +0 -25
- package/src/deepseek/auth-cli.js +0 -119
- package/src/pi/messages.js +0 -130
- package/src/server/completions.js +0 -214
- package/src/session/graph.js +0 -182
- package/src/session/planner.js +0 -177
- package/src/session/store.js +0 -100
- package/src/utils/hash.js +0 -27
- /package/src/{utils/sanitize.js → sanitize.js} +0 -0
- /package/src/{utils/tokens.js → tokens.js} +0 -0
- /package/src/{pi/tool-calls.js → tool-calls.js} +0 -0
package/.env.example
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
# Port for Black Mamba central hub server
|
|
1
2
|
PORT=3000
|
|
2
3
|
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
# MAX_TURN_DELAY_MS=2000
|
|
4
|
+
# Single API key to authenticate all requests from Pi and other apps
|
|
5
|
+
API_KEY=bm-secret-key-12345
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# DEEPSEEK_STALE_CONTEXT_BIZ_CODES=40401,40002
|
|
7
|
+
# Human-like pacing delay between turns per account (800ms - 2000ms with jitter to mimic human pace and avoid Cloudflare challenges)
|
|
8
|
+
MIN_TURN_DELAY_MS=800
|
|
9
|
+
MAX_TURN_DELAY_MS=2000
|
|
11
10
|
|
|
12
|
-
# Verbose
|
|
11
|
+
# Verbose debug logging (optional)
|
|
13
12
|
# BM_DEBUG=1
|
package/index.js
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Black Mamba —
|
|
3
|
-
//
|
|
2
|
+
// Black Mamba — Central rotational proxy hub for DeepSeek.
|
|
3
|
+
// Converges all added accounts into a single OpenAI-compatible API key.
|
|
4
4
|
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import { realpathSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { createAccountPool, listAccountDirs, loadAccounts } from "./src/accounts.js";
|
|
10
10
|
import { CONFIG } from "./src/config.js";
|
|
11
|
-
import {
|
|
11
|
+
import { createCompletionHandler } from "./src/completions.js";
|
|
12
12
|
import { createPowSolver, loadWasm } from "./src/deepseek/pow.js";
|
|
13
|
-
import { createRequestHandler } from "./src/
|
|
14
|
-
import {
|
|
13
|
+
import { createRequestHandler } from "./src/http.js";
|
|
14
|
+
import { createLogger } from "./src/logger.js";
|
|
15
|
+
import { createSanitizer } from "./src/sanitize.js";
|
|
16
|
+
|
|
17
|
+
export function createHubApp({ pool, apiKey = CONFIG.apiKey, logger = createLogger("Server") }) {
|
|
18
|
+
const sanitize = createSanitizer(() => [
|
|
19
|
+
apiKey,
|
|
20
|
+
...pool.slots.flatMap((slot) => slot.auth.secrets()),
|
|
21
|
+
]);
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
const sanitize = createSanitizer(() => slots.flatMap((slot) => [slot.apiKey, ...slot.auth.secrets()]));
|
|
23
|
+
const handleChatCompletions = createCompletionHandler({ pool, logger });
|
|
18
24
|
const handleRequest = createRequestHandler({
|
|
19
|
-
|
|
25
|
+
handleChatCompletions,
|
|
26
|
+
getStatus: () => pool.statusSummary(),
|
|
20
27
|
sanitize,
|
|
28
|
+
apiKey,
|
|
21
29
|
logger,
|
|
22
30
|
});
|
|
23
|
-
|
|
31
|
+
|
|
32
|
+
return { pool, handleRequest, sanitize, log: logger };
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
function stopApp(app) {
|
|
27
|
-
for (const slot of app.slots) {
|
|
28
|
-
slot.graph.stopPeriodicPrune();
|
|
36
|
+
for (const slot of app.pool.slots) {
|
|
29
37
|
slot.stopHealthMonitor();
|
|
30
38
|
}
|
|
31
39
|
}
|
|
@@ -35,27 +43,38 @@ async function main() {
|
|
|
35
43
|
try {
|
|
36
44
|
const accountNames = await listAccountDirs();
|
|
37
45
|
if (accountNames.length === 0) {
|
|
38
|
-
console.error("No accounts found!");
|
|
46
|
+
console.error("❌ No accounts found! Add at least one account in accounts/<name>/curl.txt");
|
|
39
47
|
process.exit(1);
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
const log = createLogger("Server");
|
|
43
51
|
const wasm = await loadWasm({ logger: createLogger("WASM") });
|
|
44
|
-
const slots = await loadAccounts({
|
|
45
|
-
|
|
52
|
+
const slots = await loadAccounts({
|
|
53
|
+
powSolver: createPowSolver(wasm),
|
|
54
|
+
onLoggedOut: (name) => log.error(`Account [${name}] logged out`),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const pool = createAccountPool(slots, { logger: createLogger("Router") });
|
|
58
|
+
app = createHubApp({ pool, apiKey: CONFIG.apiKey, logger: log });
|
|
59
|
+
|
|
46
60
|
for (const slot of slots) {
|
|
47
|
-
slot.graph.startPeriodicPrune();
|
|
48
61
|
slot.startHealthMonitor();
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
const server = http.createServer((req, res) => {
|
|
52
65
|
app.handleRequest(req, res);
|
|
53
66
|
});
|
|
67
|
+
|
|
54
68
|
server.listen(CONFIG.port, () => {
|
|
55
|
-
app.log.info(`🚀 Black Mamba listening on http://localhost:${CONFIG.port}`);
|
|
69
|
+
app.log.info(`🚀 Black Mamba Hub listening on http://localhost:${CONFIG.port}`);
|
|
70
|
+
if (CONFIG.apiKey) {
|
|
71
|
+
app.log.info(`🔑 API Key protection enabled (configured via environment)`);
|
|
72
|
+
} else {
|
|
73
|
+
app.log.warn(`⚠️ No API_KEY configured in environment; proxy will accept unauthenticated requests`);
|
|
74
|
+
}
|
|
56
75
|
for (const slot of slots) {
|
|
57
|
-
if (slot.auth.verified) app.log.info(
|
|
58
|
-
else app.log.error(
|
|
76
|
+
if (slot.auth.verified) app.log.info(`✅ Account [${slot.name}] verified and ready`);
|
|
77
|
+
else app.log.error(`❌ Account [${slot.name}] verification failed`);
|
|
59
78
|
}
|
|
60
79
|
});
|
|
61
80
|
|
package/package.json
CHANGED
package/src/accounts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Multi-account management and rotational routing pool ("Wi-Fi router" hub).
|
|
2
2
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -9,11 +9,8 @@ import { createDeepSeekClient } from "./deepseek/client.js";
|
|
|
9
9
|
import { DeepSeekAuth } from "./deepseek/auth.js";
|
|
10
10
|
import { parseCurlCommand } from "./deepseek/curl-parser.js";
|
|
11
11
|
import { createLogger, createFileSink } from "./logger.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { validateApiKey } from "./server/http.js";
|
|
15
|
-
import { SessionGraph } from "./session/graph.js";
|
|
16
|
-
import { createSanitizer } from "./utils/sanitize.js";
|
|
12
|
+
import { createPacer } from "./pacing.js";
|
|
13
|
+
import { createSanitizer } from "./sanitize.js";
|
|
17
14
|
|
|
18
15
|
function accountsDir(rootDir) {
|
|
19
16
|
return path.join(rootDir, "accounts");
|
|
@@ -26,31 +23,33 @@ export async function listAccountDirs(rootDir = CONFIG.rootDir) {
|
|
|
26
23
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Loads all account slots from accounts/<name>/curl.txt
|
|
28
|
+
*/
|
|
29
|
+
export async function loadAccounts({
|
|
30
|
+
rootDir = CONFIG.rootDir,
|
|
31
|
+
powSolver,
|
|
32
|
+
fetchImpl = fetch,
|
|
33
|
+
minDelayMs = CONFIG.pacing.minTurnDelayMs,
|
|
34
|
+
maxDelayMs = CONFIG.pacing.maxTurnDelayMs,
|
|
35
|
+
onLoggedOut = () => {},
|
|
36
|
+
} = {}) {
|
|
35
37
|
const names = await listAccountDirs(rootDir);
|
|
36
38
|
if (names.length === 0) return [];
|
|
37
39
|
|
|
38
40
|
const specs = [];
|
|
39
|
-
const seen = new Map();
|
|
40
41
|
for (const name of names) {
|
|
41
42
|
const dir = path.join(accountsDir(rootDir), name);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (seen.has(apiKey)) throw new Error(`Duplicate api key in accounts/${seen.get(apiKey)} and accounts/${name}`);
|
|
46
|
-
seen.set(apiKey, name);
|
|
43
|
+
const curlPath = path.join(dir, "curl.txt");
|
|
44
|
+
if (!existsSync(curlPath)) continue;
|
|
45
|
+
const curl = await readFile(curlPath, "utf8");
|
|
47
46
|
let parsed;
|
|
48
47
|
try {
|
|
49
48
|
parsed = parseCurlCommand(curl);
|
|
50
49
|
} catch (err) {
|
|
51
50
|
throw new Error(`accounts/${name}/curl.txt: ${err.message}`);
|
|
52
51
|
}
|
|
53
|
-
specs.push({ name, dir,
|
|
52
|
+
specs.push({ name, dir, parsed });
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
const slots = [];
|
|
@@ -58,10 +57,9 @@ export async function loadAccounts({ rootDir = CONFIG.rootDir, powSolver, fetchI
|
|
|
58
57
|
const logger = createLogger(spec.name, { sink: createFileSink(path.join(spec.dir, "logs.txt")) });
|
|
59
58
|
const auth = new DeepSeekAuth();
|
|
60
59
|
auth.applyParsedSession(spec.parsed);
|
|
61
|
-
const sanitize = createSanitizer(() =>
|
|
62
|
-
const graph = new SessionGraph({ sessionsDir: path.join(spec.dir, ".session"), logger });
|
|
63
|
-
await graph.init();
|
|
60
|
+
const sanitize = createSanitizer(() => auth.secrets());
|
|
64
61
|
const pacer = createPacer({ logger, minDelayMs, maxDelayMs });
|
|
62
|
+
|
|
65
63
|
let connected = false;
|
|
66
64
|
const noteDisconnect = (reason) => {
|
|
67
65
|
logger.error(sanitize(reason));
|
|
@@ -69,8 +67,8 @@ export async function loadAccounts({ rootDir = CONFIG.rootDir, powSolver, fetchI
|
|
|
69
67
|
connected = false;
|
|
70
68
|
onLoggedOut(spec.name);
|
|
71
69
|
};
|
|
70
|
+
|
|
72
71
|
const deepseek = createDeepSeekClient({ auth, powSolver, fetchImpl, onAuthFailure: noteDisconnect });
|
|
73
|
-
const handleChatCompletions = createCompletionHandler({ deepseek, graph, pacer, logger });
|
|
74
72
|
|
|
75
73
|
let healthTimer = null;
|
|
76
74
|
const stopHealthMonitor = () => {
|
|
@@ -101,7 +99,68 @@ export async function loadAccounts({ rootDir = CONFIG.rootDir, powSolver, fetchI
|
|
|
101
99
|
logger.error(sanitize(err));
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
slots.push(
|
|
102
|
+
slots.push(
|
|
103
|
+
Object.freeze({
|
|
104
|
+
name: spec.name,
|
|
105
|
+
dir: spec.dir,
|
|
106
|
+
auth,
|
|
107
|
+
deepseek,
|
|
108
|
+
pacer,
|
|
109
|
+
logger,
|
|
110
|
+
startHealthMonitor,
|
|
111
|
+
stopHealthMonitor,
|
|
112
|
+
})
|
|
113
|
+
);
|
|
105
114
|
}
|
|
106
115
|
return slots;
|
|
107
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a round-robin rotational account pool.
|
|
120
|
+
* Acts like a Wi-Fi router / hub: converges all accounts into a pool and rotates requests across them.
|
|
121
|
+
*/
|
|
122
|
+
export function createAccountPool(slots, { logger = createLogger("Router") } = {}) {
|
|
123
|
+
let currentIndex = 0;
|
|
124
|
+
|
|
125
|
+
function nextAccount() {
|
|
126
|
+
if (slots.length === 0) {
|
|
127
|
+
throw new Error("No DeepSeek accounts available in pool. Please add an account in accounts/<name>/curl.txt");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Try finding the next verified account starting at currentIndex
|
|
131
|
+
for (let i = 0; i < slots.length; i++) {
|
|
132
|
+
const idx = (currentIndex + i) % slots.length;
|
|
133
|
+
const candidate = slots[idx];
|
|
134
|
+
if (candidate.auth.verified) {
|
|
135
|
+
currentIndex = (idx + 1) % slots.length;
|
|
136
|
+
logger.info(`🔄 Routed request to account [${candidate.name}] (index ${idx + 1}/${slots.length})`);
|
|
137
|
+
return candidate;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If none verified, return next slot anyway
|
|
142
|
+
const fallback = slots[currentIndex % slots.length];
|
|
143
|
+
currentIndex = (currentIndex + 1) % slots.length;
|
|
144
|
+
logger.warn(`⚠️ Routed request to unverified account [${fallback.name}]`);
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function statusSummary() {
|
|
149
|
+
return {
|
|
150
|
+
status: "ok",
|
|
151
|
+
total_accounts: slots.length,
|
|
152
|
+
verified_accounts: slots.filter((s) => s.auth.verified).length,
|
|
153
|
+
accounts: slots.map((s) => ({
|
|
154
|
+
name: s.name,
|
|
155
|
+
verified: s.auth.verified,
|
|
156
|
+
...s.auth.statusSummary(),
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
slots,
|
|
163
|
+
nextAccount,
|
|
164
|
+
statusSummary,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// POST /v1/chat/completions handler:
|
|
2
|
+
// Stateless prompt dump -> rotational account selection -> human pacing -> DeepSeek turn -> OpenAI stream/JSON.
|
|
3
|
+
|
|
4
|
+
import { CONFIG } from "./config.js";
|
|
5
|
+
import { createLogger } from "./logger.js";
|
|
6
|
+
import { consumeDeepSeekSseStream } from "./deepseek/sse.js";
|
|
7
|
+
import { renderSeedPrompt } from "./prompt.js";
|
|
8
|
+
import { parseChatRequest } from "./request.js";
|
|
9
|
+
import {
|
|
10
|
+
buildUsageStats,
|
|
11
|
+
createCompletionMetadata,
|
|
12
|
+
formatCompletionResponse,
|
|
13
|
+
formatStreamDeltaEvent,
|
|
14
|
+
formatStreamFinishEvent,
|
|
15
|
+
formatStreamReasoningEvent,
|
|
16
|
+
formatStreamRoleEvent,
|
|
17
|
+
formatStreamToolCallArgsEvent,
|
|
18
|
+
formatStreamToolCallStartEvent,
|
|
19
|
+
formatStreamUsageEvent,
|
|
20
|
+
} from "./response.js";
|
|
21
|
+
import { createToolCallStreamProcessor, parseToolCallsFromText } from "./tool-calls.js";
|
|
22
|
+
import { estimateTokenCount } from "./tokens.js";
|
|
23
|
+
import { readJsonBody, sendJson, startSse, writeSse } from "./http.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} deps
|
|
27
|
+
* @param {ReturnType<import("./accounts.js").createAccountPool>} deps.pool
|
|
28
|
+
* @param {object} [deps.logger]
|
|
29
|
+
*/
|
|
30
|
+
export function createCompletionHandler({ pool, logger = createLogger("Completions"), keepAliveIntervalMs = CONFIG.server.keepAliveIntervalMs }) {
|
|
31
|
+
async function respondJson({ res, request, account, streamBody, prompt, meta }) {
|
|
32
|
+
const result = await consumeDeepSeekSseStream(streamBody);
|
|
33
|
+
let text = result.reply;
|
|
34
|
+
let toolCalls = [];
|
|
35
|
+
|
|
36
|
+
if (request.tools.length > 0) {
|
|
37
|
+
const parsed = parseToolCallsFromText(result.reply);
|
|
38
|
+
text = parsed.cleanText;
|
|
39
|
+
toolCalls = parsed.toolCalls;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const turnTokens = result.turnTokens > 0
|
|
43
|
+
? result.turnTokens
|
|
44
|
+
: estimateTokenCount(prompt) + estimateTokenCount(result.reply);
|
|
45
|
+
|
|
46
|
+
const usage = buildUsageStats({ prompt, reply: result.reply, turnTokens });
|
|
47
|
+
const payload = formatCompletionResponse({
|
|
48
|
+
...meta,
|
|
49
|
+
model: request.model,
|
|
50
|
+
content: text,
|
|
51
|
+
toolCalls,
|
|
52
|
+
usage,
|
|
53
|
+
});
|
|
54
|
+
sendJson(res, 200, payload);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function respondStream({ req, res, request, account, streamBody, prompt, meta }) {
|
|
58
|
+
startSse(res, req);
|
|
59
|
+
writeSse(res, formatStreamRoleEvent(meta));
|
|
60
|
+
const keepAlive = setInterval(() => writeSse(res, ": keep-alive\n\n"), keepAliveIntervalMs);
|
|
61
|
+
keepAlive.unref?.();
|
|
62
|
+
|
|
63
|
+
let emittedText = "";
|
|
64
|
+
const emittedToolCalls = [];
|
|
65
|
+
const emitText = (content) => {
|
|
66
|
+
emittedText += content;
|
|
67
|
+
writeSse(res, formatStreamDeltaEvent(meta, content));
|
|
68
|
+
};
|
|
69
|
+
const emitToolCall = ({ index, id, name, argumentsString }) => {
|
|
70
|
+
emittedToolCalls.push({ name, argumentsString });
|
|
71
|
+
writeSse(res, formatStreamToolCallStartEvent(meta, { toolCallIndex: index, id, name }));
|
|
72
|
+
writeSse(res, formatStreamToolCallArgsEvent(meta, { toolCallIndex: index, argumentsChunk: argumentsString }));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const processor = request.tools.length > 0
|
|
76
|
+
? createToolCallStreamProcessor({ onTextDelta: emitText, onToolCall: emitToolCall })
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await consumeDeepSeekSseStream(streamBody, (chunk) => {
|
|
82
|
+
if (chunk.isThinking) writeSse(res, formatStreamReasoningEvent(meta, chunk.text));
|
|
83
|
+
else if (processor) processor.processChunk(chunk.text);
|
|
84
|
+
else emitText(chunk.text);
|
|
85
|
+
});
|
|
86
|
+
processor?.flush();
|
|
87
|
+
} finally {
|
|
88
|
+
clearInterval(keepAlive);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const turnTokens = result.turnTokens > 0
|
|
92
|
+
? result.turnTokens
|
|
93
|
+
: estimateTokenCount(prompt) + estimateTokenCount(result.reply);
|
|
94
|
+
|
|
95
|
+
writeSse(
|
|
96
|
+
res,
|
|
97
|
+
formatStreamFinishEvent(meta, {
|
|
98
|
+
finishReason: emittedToolCalls.length > 0 ? "tool_calls" : "stop",
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (request.includeUsage) {
|
|
103
|
+
writeSse(res, formatStreamUsageEvent(meta, buildUsageStats({ prompt, reply: result.reply, turnTokens })));
|
|
104
|
+
}
|
|
105
|
+
writeSse(res, "data: [DONE]\n\n");
|
|
106
|
+
res.end();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return async function handleChatCompletions(req, res) {
|
|
110
|
+
const body = await readJsonBody(req);
|
|
111
|
+
const request = parseChatRequest(body);
|
|
112
|
+
const prompt = renderSeedPrompt(request.messages, request.tools);
|
|
113
|
+
|
|
114
|
+
// Rotational account selection ("Wi-Fi router" hub)
|
|
115
|
+
const account = pool.nextAccount();
|
|
116
|
+
logger.info(`Request [${request.model}] -> [${account.name}] (prompt: ${prompt.length} chars, tools: ${request.tools.length})`);
|
|
117
|
+
|
|
118
|
+
const abortController = new AbortController();
|
|
119
|
+
let finished = false;
|
|
120
|
+
if (typeof res.on === "function") {
|
|
121
|
+
res.on("close", () => {
|
|
122
|
+
if (!finished && !res.writableFinished) abortController.abort();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Apply per-account human pacing
|
|
127
|
+
await account.pacer.apply();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const { streamBody } = await account.deepseek.openCompletionStream({
|
|
131
|
+
prompt,
|
|
132
|
+
thinkingEnabled: request.thinkingEnabled,
|
|
133
|
+
searchEnabled: request.searchEnabled,
|
|
134
|
+
signal: abortController.signal,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const meta = { ...createCompletionMetadata(), model: request.model };
|
|
138
|
+
if (request.stream) {
|
|
139
|
+
await respondStream({ req, res, request, account, streamBody, prompt, meta });
|
|
140
|
+
} else {
|
|
141
|
+
await respondJson({ res, request, account, streamBody, prompt, meta });
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
finished = true;
|
|
145
|
+
account.pacer.recordCompletionFinished();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
package/src/config.js
CHANGED
|
@@ -30,6 +30,7 @@ export function loadConfig(env = process.env, rootDir = ROOT_DIR) {
|
|
|
30
30
|
return Object.freeze({
|
|
31
31
|
rootDir,
|
|
32
32
|
port: readNumber(env.PORT, 3000),
|
|
33
|
+
apiKey: (env.API_KEY || env.BM_API_KEY || "").trim(),
|
|
33
34
|
debug: env.BM_DEBUG === "1" || env.BM_DEBUG === "true",
|
|
34
35
|
|
|
35
36
|
deepseek: Object.freeze({
|
|
@@ -44,9 +45,6 @@ export function loadConfig(env = process.env, rootDir = ROOT_DIR) {
|
|
|
44
45
|
healthCheckIntervalMs: 30 * 60 * 1000,
|
|
45
46
|
estimatedWafTtlMs: 4 * 24 * 60 * 60 * 1000,
|
|
46
47
|
estimatedSessionTtlMs: 30 * 24 * 60 * 60 * 1000,
|
|
47
|
-
// DeepSeek biz codes that mean "the chat session / parent message you referenced no longer exists".
|
|
48
|
-
// Only these (plus a narrow message pattern) trigger an explicit re-seed of a stateful branch.
|
|
49
|
-
staleContextBizCodes: readList(env.DEEPSEEK_STALE_CONTEXT_BIZ_CODES).map(Number),
|
|
50
48
|
}),
|
|
51
49
|
|
|
52
50
|
server: Object.freeze({
|
|
@@ -54,12 +52,6 @@ export function loadConfig(env = process.env, rootDir = ROOT_DIR) {
|
|
|
54
52
|
keepAliveIntervalMs: 10_000,
|
|
55
53
|
}),
|
|
56
54
|
|
|
57
|
-
sessions: Object.freeze({
|
|
58
|
-
dir: path.join(rootDir, ".sessions"),
|
|
59
|
-
ttlMs: 14 * 24 * 60 * 60 * 1000,
|
|
60
|
-
maxEntries: 1000,
|
|
61
|
-
}),
|
|
62
|
-
|
|
63
55
|
pacing: Object.freeze({
|
|
64
56
|
minTurnDelayMs: readNumber(env.MIN_TURN_DELAY_MS, 800),
|
|
65
57
|
maxTurnDelayMs: readNumber(env.MAX_TURN_DELAY_MS, 2000),
|
package/src/deepseek/classify.js
CHANGED
|
@@ -21,17 +21,17 @@ export function upstreamReason(jsonBody, rawText, status) {
|
|
|
21
21
|
return jsonBody?.data?.biz_msg || jsonBody?.msg || (rawText ? rawText.slice(0, 200) : "") || `HTTP ${status}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
24
|
+
/** Safe fields from an auth-failure response for account logs. */
|
|
25
|
+
export function deepSeekAuthFacts(res, jsonBody = null) {
|
|
26
|
+
const facts = { http: res?.status ?? null };
|
|
27
|
+
const waf = res?.headers?.get?.("x-amzn-waf-action");
|
|
28
|
+
if (waf) facts.waf = waf;
|
|
29
|
+
if (jsonBody && typeof jsonBody === "object") {
|
|
30
|
+
if (jsonBody.code != null) facts.code = jsonBody.code;
|
|
31
|
+
if (jsonBody.msg != null) facts.msg = String(jsonBody.msg).slice(0, 300);
|
|
32
|
+
const data = jsonBody.data;
|
|
33
|
+
if (data?.biz_code != null) facts.biz_code = data.biz_code;
|
|
34
|
+
if (data?.biz_msg != null) facts.biz_msg = String(data.biz_msg).slice(0, 300);
|
|
35
|
+
}
|
|
36
|
+
return facts;
|
|
37
37
|
}
|
package/src/deepseek/client.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// HTTP client for the DeepSeek web API. Every failure is classified and thrown; nothing is papered over.
|
|
2
2
|
|
|
3
3
|
import { CONFIG } from "../config.js";
|
|
4
|
-
import { DeepSeekAuthError,
|
|
4
|
+
import { DeepSeekAuthError, UpstreamError } from "../errors.js";
|
|
5
5
|
import {
|
|
6
|
+
deepSeekAuthFacts,
|
|
6
7
|
isDeepSeekAuthFailure,
|
|
7
8
|
isHumanChallengeRequired,
|
|
8
|
-
isStaleContextRejection,
|
|
9
9
|
isWafChallengeResponse,
|
|
10
10
|
upstreamReason,
|
|
11
11
|
} from "./classify.js";
|
|
@@ -22,10 +22,12 @@ const SESSION_EXPIRED_NOTICE = "DeepSeek session expired. Paste a fresh cURL in
|
|
|
22
22
|
* @param {(reason: string) => void} [deps.onAuthFailure] Called once per detected credential failure.
|
|
23
23
|
*/
|
|
24
24
|
export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, config = CONFIG.deepseek, onAuthFailure = () => {} }) {
|
|
25
|
-
function authFailure(notice) {
|
|
25
|
+
function authFailure(notice, res, jsonBody = null) {
|
|
26
26
|
auth.markInvalid();
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const facts = res ? deepSeekAuthFacts(res, jsonBody) : null;
|
|
28
|
+
if (facts) onAuthFailure(`${notice} ${JSON.stringify(facts)}`);
|
|
29
|
+
else onAuthFailure(notice);
|
|
30
|
+
return new DeepSeekAuthError(notice, facts ?? undefined);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function ensureCredentials() {
|
|
@@ -51,7 +53,7 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
51
53
|
const json = await res.json().catch(() => null);
|
|
52
54
|
|
|
53
55
|
if (isDeepSeekAuthFailure(res, json)) {
|
|
54
|
-
throw authFailure("DeepSeek session expired while creating chat session. " + SESSION_EXPIRED_NOTICE);
|
|
56
|
+
throw authFailure("DeepSeek session expired while creating chat session. " + SESSION_EXPIRED_NOTICE, res, json);
|
|
55
57
|
}
|
|
56
58
|
const remoteId = json?.data?.biz_data?.id || json?.data?.biz_data?.chat_session?.id;
|
|
57
59
|
if (!res.ok || typeof remoteId !== "string" || !remoteId) {
|
|
@@ -67,7 +69,7 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
67
69
|
const res = await postJson(config.powChallengeUrl, { target_path: config.powTargetPath });
|
|
68
70
|
|
|
69
71
|
if (isWafChallengeResponse(res)) {
|
|
70
|
-
throw authFailure("AWS WAF cookie expired during PoW challenge. " + SESSION_EXPIRED_NOTICE);
|
|
72
|
+
throw authFailure("AWS WAF cookie expired during PoW challenge. " + SESSION_EXPIRED_NOTICE, res);
|
|
71
73
|
}
|
|
72
74
|
let json;
|
|
73
75
|
try {
|
|
@@ -76,7 +78,11 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
76
78
|
throw new UpstreamError(`Invalid PoW challenge response (HTTP ${res.status})`, { code: "pow_challenge_invalid" });
|
|
77
79
|
}
|
|
78
80
|
if (isDeepSeekAuthFailure(res, json)) {
|
|
79
|
-
throw authFailure(
|
|
81
|
+
throw authFailure(
|
|
82
|
+
isHumanChallengeRequired(json) ? HUMAN_CHALLENGE_NOTICE : "DeepSeek session expired during PoW challenge. " + SESSION_EXPIRED_NOTICE,
|
|
83
|
+
res,
|
|
84
|
+
json
|
|
85
|
+
);
|
|
80
86
|
}
|
|
81
87
|
const challenge = json?.data?.biz_data?.challenge;
|
|
82
88
|
if (!res.ok || !challenge) {
|
|
@@ -89,21 +95,19 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
/**
|
|
92
|
-
* Opens a completion stream. Returns the raw SSE body; decoding lives in sse.js.
|
|
93
|
-
* @param {{
|
|
98
|
+
* Opens a completion stream on a fresh DeepSeek chat session. Returns the raw SSE body; decoding lives in sse.js.
|
|
99
|
+
* @param {{ prompt: string, thinkingEnabled: boolean, searchEnabled: boolean, chatSessionId?: string, signal?: AbortSignal }} params
|
|
94
100
|
*/
|
|
95
|
-
async function openCompletionStream({
|
|
101
|
+
async function openCompletionStream({ prompt, thinkingEnabled, searchEnabled, chatSessionId, signal }) {
|
|
96
102
|
ensureCredentials();
|
|
97
|
-
|
|
98
|
-
throw new Error("openCompletionStream requires a DeepSeek chat session id");
|
|
99
|
-
}
|
|
103
|
+
const sessionId = chatSessionId || (await createChatSession());
|
|
100
104
|
|
|
101
105
|
const powHeader = powSolver.solve(await fetchPowChallenge());
|
|
102
106
|
const res = await postJson(
|
|
103
107
|
config.chatCompletionUrl,
|
|
104
108
|
{
|
|
105
|
-
chat_session_id:
|
|
106
|
-
parent_message_id:
|
|
109
|
+
chat_session_id: sessionId,
|
|
110
|
+
parent_message_id: null,
|
|
107
111
|
model_type: "default",
|
|
108
112
|
prompt,
|
|
109
113
|
ref_file_ids: [],
|
|
@@ -117,7 +121,7 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
117
121
|
);
|
|
118
122
|
|
|
119
123
|
if (isWafChallengeResponse(res) || res.status === 401) {
|
|
120
|
-
throw authFailure("DeepSeek returned unauthorized / WAF challenge on chat completion. " + SESSION_EXPIRED_NOTICE);
|
|
124
|
+
throw authFailure("DeepSeek returned unauthorized / WAF challenge on chat completion. " + SESSION_EXPIRED_NOTICE, res);
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
const contentType = res.headers.get("content-type") || "";
|
|
@@ -130,12 +134,9 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
130
134
|
// non-JSON body
|
|
131
135
|
}
|
|
132
136
|
if (isDeepSeekAuthFailure(res, errJson)) {
|
|
133
|
-
throw authFailure(isHumanChallengeRequired(errJson) ? HUMAN_CHALLENGE_NOTICE : SESSION_EXPIRED_NOTICE);
|
|
134
|
-
}
|
|
135
|
-
const details = { status: res.status, body: errJson ?? rawText.slice(0, 500), chat_session_id: chatSessionId, parent_message_id: parentMessageId };
|
|
136
|
-
if (isStaleContextRejection(res, errJson, config.staleContextBizCodes)) {
|
|
137
|
-
throw new UpstreamContextRejectedError(`DeepSeek rejected the referenced chat context: ${upstreamReason(errJson, rawText, res.status)}`, details);
|
|
137
|
+
throw authFailure(isHumanChallengeRequired(errJson) ? HUMAN_CHALLENGE_NOTICE : SESSION_EXPIRED_NOTICE, res, errJson);
|
|
138
138
|
}
|
|
139
|
+
const details = { status: res.status, body: errJson ?? rawText.slice(0, 500), chat_session_id: sessionId };
|
|
139
140
|
throw new UpstreamError(`DeepSeek completion failed: ${upstreamReason(errJson, rawText, res.status)}`, { details });
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -144,7 +145,7 @@ export function createDeepSeekClient({ auth, powSolver, fetchImpl = fetch, confi
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
auth.markVerified();
|
|
147
|
-
return { streamBody: res.body };
|
|
148
|
+
return { streamBody: res.body, chatSessionId: sessionId };
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
return Object.freeze({ createChatSession, openCompletionStream, fetchPowChallenge });
|
package/src/errors.js
CHANGED
|
@@ -45,14 +45,3 @@ export class UpstreamError extends HttpError {
|
|
|
45
45
|
this.name = "UpstreamError";
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Raised when DeepSeek explicitly rejected the chat session / parent message we referenced.
|
|
51
|
-
* The completion handler converts this into an explicit, logged re-seed for stateful branches.
|
|
52
|
-
*/
|
|
53
|
-
export class UpstreamContextRejectedError extends UpstreamError {
|
|
54
|
-
constructor(message, details) {
|
|
55
|
-
super(message, { statusCode: 502, code: "deepseek_context_rejected", details });
|
|
56
|
-
this.name = "UpstreamContextRejectedError";
|
|
57
|
-
}
|
|
58
|
-
}
|