@mathew-macharia/black-mamba 1.0.3 → 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/docs/install.md DELETED
@@ -1,68 +0,0 @@
1
- # Install
2
-
3
- Requires Node.js 18 or newer.
4
-
5
- ```
6
- npm install -g @mathew-macharia/black-mamba
7
- ```
8
-
9
- The command is `bm`. Run it in the directory that holds your data. That working directory (`process.cwd()`) is where `bm` reads `accounts/` and `.env`. The proof-of-work file `sha3_wasm_bg.wasm` is installed with the package.
10
-
11
- ```
12
- <data-directory>/
13
- .env optional
14
- accounts/
15
- <name>/
16
- api-key.txt
17
- curl.txt
18
- ```
19
-
20
- `<name>` is the account folder name. It is the name printed in the terminal lines below. One folder is one DeepSeek login. Every account folder must contain both files. See [accounts.md](accounts.md) for the other files the process creates, and [config.md](config.md) for `.env`.
21
-
22
- `accounts/<name>/api-key.txt` is one line: the bearer key your app sends as `Authorization: Bearer <key>`. The line is trimmed. An empty file, or the same key in two account folders, stops startup.
23
-
24
- `accounts/<name>/curl.txt` is a Chrome DevTools copy of a `chat.deepseek.com` request: "Copy as cURL", "Copy as PowerShell", or a `fetch()` snippet. It must contain an `Authorization: Bearer <deepseek-bearer>` header or a `Cookie` header (or both). A placeholder shape:
25
-
26
- ```
27
- curl "<deepseek-url>" -H "authorization: Bearer <deepseek-bearer>" -H "cookie: <cookie>"
28
- ```
29
-
30
- Do not commit `accounts/` or `.env`.
31
-
32
- From the data directory:
33
-
34
- ```
35
- bm
36
- ```
37
-
38
- When the server is up, stdout has:
39
-
40
- ```
41
- [Server] 🚀 Black Mamba listening on http://localhost:3000
42
- ```
43
-
44
- The port is `PORT` (default 3000). Each account then prints one line. Stdout on success:
45
-
46
- ```
47
- [Server] <name> logged in successfully
48
- ```
49
-
50
- Stderr when the DeepSeek check failed:
51
-
52
- ```
53
- [Server] <name> log in failed
54
- ```
55
-
56
- The failure reason is appended to `accounts/<name>/logs.txt`, not to that terminal line.
57
-
58
- If a login that had succeeded later fails (the 30-minute check, or a DeepSeek auth error on a request), stderr prints:
59
-
60
- ```
61
- [Server] <name> logged out
62
- ```
63
-
64
- The reason is written to that account's `logs.txt` before the terminal line.
65
-
66
- If `accounts/` is missing or has no account folders, stderr is `No accounts found!` and the process exits with status 1. A missing `api-key.txt` or `curl.txt`, or a `curl.txt` that has neither a bearer token nor a cookie, prints `[Startup] Fatal error:` and exits with status 1.
67
-
68
- `SIGINT` and `SIGTERM` stop the process. The terminal prints `Received <signal>. Shutting down gracefully...` and the process exits with status 0.
package/docs/mapping.md DELETED
@@ -1,11 +0,0 @@
1
- # Mapping
2
-
3
- `stateless_seed`: A request with no `x-session-id` sends the full history into a new DeepSeek chat and records nothing.
4
-
5
- `root_seed`: A sessioned request with no stored parent node (first turn, unknown session, or unknown parent entry) sends the full history into a new DeepSeek chat and records the node.
6
-
7
- `delta`: The parent node exists and this request's history is that node's context plus its reply plus new messages, so only those new messages are sent on the stored DeepSeek chat and parent message.
8
-
9
- `reseed_context_diverged`: The parent node exists, but the history no longer extends it (shorter context, a changed prefix, a missing or edited assistant reply, or a compaction summary), so the full history is sent into a new DeepSeek chat.
10
-
11
- `reseed_upstream_rejected`: DeepSeek rejected the chat or parent message a `delta` turn referenced, so the full history is sent into a new DeepSeek chat and that result is the one returned.
package/docs/overview.md DELETED
@@ -1,25 +0,0 @@
1
- # Overview
2
-
3
- Black Mamba is an HTTP server. It accepts OpenAI-style chat completion requests and sends each turn to DeepSeek web chat. A client chooses an account with `Authorization: Bearer <key>`, where `<key>` is the text in that account's `api-key.txt`.
4
-
5
- The process keeps a local map from Pi session entries to DeepSeek chat ids and message ids. A later turn on the same branch continues that DeepSeek chat and sends only the new messages. A request with no `x-session-id` is stateless: the full history goes to a new DeepSeek chat and nothing is stored.
6
-
7
- Credentials come from `accounts/<name>/curl.txt` (DeepSeek bearer token and cookies). They stay in memory. `Set-Cookie` on DeepSeek responses updates the cookie jar. The files are not rewritten.
8
-
9
- At startup each account is checked with `GET <DEEPSEEK_BASE_URL>/api/v0/users/current`. A verified account is checked again every 30 minutes.
10
-
11
- ## Request path
12
-
13
- 1. The app sends `POST /v1/chat/completions` with the account bearer key and a JSON body whose `messages` array is the conversation so far.
14
- 2. The key selects the account. The body is validated. If `x-session-id` is set, `x-entry-id` is required, and the session graph under `accounts/<name>/.session/` is loaded.
15
- 3. The planner chooses one strategy and renders a single text prompt. A seed strategy sends the full history. `delta` sends only the messages after the stored parent reply. See [mapping.md](mapping.md).
16
- 4. The account waits, when a previous turn has just finished, until the gap since that finish is a random time between `MIN_TURN_DELAY_MS` and `MAX_TURN_DELAY_MS`. The first turn of a process does not wait.
17
- 5. If the plan has no DeepSeek chat id, Black Mamba creates one with `POST <DEEPSEEK_BASE_URL>/api/v0/chat_session/create`.
18
- 6. It requests a proof-of-work challenge from `POST <DEEPSEEK_BASE_URL>/api/v0/chat/create_pow_challenge`, solves it with `sha3_wasm_bg.wasm`, and posts the prompt to `POST <DEEPSEEK_BASE_URL>/api/v0/chat/completion`. The JSON body includes the chat id, the parent message id (`null` for a new chat), `thinking_enabled`, and `search_enabled`. The proof-of-work answer is the `x-ds-pow-response` header.
19
- 7. DeepSeek replies with a server-sent event stream. Black Mamba reads the reply text, any thinking text, `response_message_id`, and the change in accumulated token usage. Citation markers of the form `[reference:N]` and `[citation:N]` are removed from the text.
20
- 8. If the request included `tools`, DSML tool-call blocks in the reply are converted to OpenAI `tool_calls` and removed from the assistant text. Image parts in the request are not forwarded; they are replaced with the text `[image omitted: Black Mamba forwards text only]`.
21
- 9. For a request that carried `x-session-id`, the Pi entry id, the DeepSeek chat id, the DeepSeek message id, and a fingerprint of the context are written under `accounts/<name>/.session/`. A stateless request is not written.
22
- 10. The app receives one JSON chat completion, or an SSE stream when `stream` is true. The response header `x-black-mamba-strategy` names the strategy that was used.
23
- 11. If DeepSeek rejects the stored chat or parent message on a `delta` turn, Black Mamba opens a new chat, sends the full history (`reseed_upstream_rejected`), and answers from that second stream. If that second call fails, or if any other upstream call fails, the error is returned to the app. Upstream failures use HTTP 502.
24
-
25
- If the app closes the connection before the response is finished, the DeepSeek request is aborted and the turn is not recorded.
@@ -1,119 +0,0 @@
1
- // Terminal interaction for loading DeepSeek credentials and keeping them healthy.
2
-
3
- import readline from "node:readline";
4
- import { CONFIG } from "../config.js";
5
- import { createLogger } from "../logger.js";
6
- import { parseCurlCommand } from "./curl-parser.js";
7
-
8
- const log = createLogger("Auth");
9
-
10
- export function readCurlFromTerminal(promptLabel, { input = process.stdin, output = process.stdout } = {}) {
11
- return new Promise((resolve, reject) => {
12
- const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY) });
13
- console.log(`\n${promptLabel}`);
14
- console.log(" (Chrome DevTools -> Network -> right-click 'completion' -> Copy as cURL or Copy as PowerShell, paste below, then press Enter)\n");
15
-
16
- const lines = [];
17
- let resolved = false;
18
- const finish = () => {
19
- if (resolved) return;
20
- resolved = true;
21
- rl.close();
22
- resolve(lines.join("\n"));
23
- };
24
-
25
- rl.on("line", (line) => {
26
- const trimmed = line.trim();
27
- if (trimmed === "" && lines.length > 0) {
28
- const prev = lines[lines.length - 1].trim();
29
- if (!prev.endsWith("^") && !prev.endsWith("\\") && !prev.endsWith("`")) {
30
- finish();
31
- return;
32
- }
33
- }
34
- if (trimmed !== "" || lines.length > 0) lines.push(line);
35
- });
36
-
37
- rl.on("close", () => {
38
- if (resolved) return;
39
- resolved = true;
40
- if (lines.length === 0) reject(new Error("Input stream closed before receiving cURL command"));
41
- else resolve(lines.join("\n"));
42
- });
43
- });
44
- }
45
-
46
- /**
47
- * Owns the interactive credential lifecycle: initial paste, re-prompt on expiry, periodic health checks.
48
- * @param {{ auth: import("./auth.js").DeepSeekAuth, fetchImpl?: typeof fetch, sanitize: (err: unknown) => string, port?: number }} deps
49
- */
50
- export function createAuthSupervisor({ auth, fetchImpl = fetch, sanitize, port = CONFIG.port, config = CONFIG.deepseek }) {
51
- let promptInProgress = false;
52
- let healthTimer = null;
53
-
54
- function printSummary() {
55
- const summary = auth.statusSummary();
56
- log.info(`Session active | Bearer token: ${auth.token ? "yes" : "no"} | Cookies in RAM: ${auth.cookies.size}`);
57
- if (summary.next_due_expiry) {
58
- log.info(`Earliest cookie due: "${summary.next_due_expiry.cookie}" in ${summary.next_due_expiry.remaining}`);
59
- }
60
- log.info(`Live expiry details available anytime at GET http://localhost:${port}/`);
61
- }
62
-
63
- async function promptAndLoad(promptLabel = "📋 Paste your DeepSeek cURL command:") {
64
- if (promptInProgress) return;
65
- promptInProgress = true;
66
- try {
67
- while (true) {
68
- const rawCurl = await readCurlFromTerminal(promptLabel);
69
- try {
70
- auth.applyParsedSession(parseCurlCommand(rawCurl));
71
- await auth.verify(fetchImpl);
72
- printSummary();
73
- return;
74
- } catch (err) {
75
- log.error(`❌ ${sanitize(err)}`);
76
- if (!process.stdin.isTTY) throw err;
77
- promptLabel = "📋 Please paste a fresh, valid DeepSeek cURL command:";
78
- }
79
- }
80
- } finally {
81
- promptInProgress = false;
82
- }
83
- }
84
-
85
- function reprompt(reason) {
86
- auth.markInvalid();
87
- log.warn(`⚠️ ${reason}`);
88
- if (process.stdin.isTTY && !promptInProgress) {
89
- promptAndLoad("🔄 Session expired! Paste a fresh DeepSeek cURL command to resume:").catch((err) => {
90
- log.error("Failed to reload session:", sanitize(err));
91
- });
92
- }
93
- }
94
-
95
- function startHealthMonitor() {
96
- stopHealthMonitor();
97
- healthTimer = setInterval(async () => {
98
- if (!auth.verified || promptInProgress) return;
99
- try {
100
- await auth.verify(fetchImpl);
101
- const summary = auth.statusSummary();
102
- const expiryInfo = summary.next_due_expiry ? `${summary.next_due_expiry.cookie} due in ${summary.next_due_expiry.remaining}` : "active";
103
- log.info(`Health check OK (age: ${summary.session_age}, ${expiryInfo})`);
104
- } catch (err) {
105
- reprompt(sanitize(err));
106
- }
107
- }, config.healthCheckIntervalMs);
108
- healthTimer.unref?.();
109
- }
110
-
111
- function stopHealthMonitor() {
112
- if (healthTimer) {
113
- clearInterval(healthTimer);
114
- healthTimer = null;
115
- }
116
- }
117
-
118
- return Object.freeze({ promptAndLoad, reprompt, startHealthMonitor, stopHealthMonitor });
119
- }
@@ -1,130 +0,0 @@
1
- // Canonical view of OpenAI-format messages and the fingerprints Black Mamba uses to verify that the context pi sends
2
- // matches the context DeepSeek already holds. Hashes are compared, never used as keys.
3
-
4
- import { BadRequestError } from "../errors.js";
5
- import { canonicalJson, sha256Hex, shortHash } from "../utils/hash.js";
6
-
7
- /** Exact prefix pi uses when it turns a compaction entry into a user message (pi-coding-agent core/messages.js). */
8
- export const PI_COMPACTION_SUMMARY_PREFIX =
9
- "The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
10
- /** Exact prefix pi uses for branch summaries after /tree navigation. */
11
- export const PI_BRANCH_SUMMARY_PREFIX = "The following is a summary of a branch that this conversation came back from:\n\n<summary>\n";
12
-
13
- export function normalizeRole(role) {
14
- return typeof role === "string" ? role.trim().toLowerCase() : "";
15
- }
16
-
17
- /**
18
- * Renders message content to text. Non-text parts are made visible as explicit placeholders instead of being dropped.
19
- */
20
- export function contentToText(content) {
21
- if (content === null || content === undefined) return "";
22
- if (typeof content === "string") return content;
23
- if (Array.isArray(content)) {
24
- return content
25
- .map((part) => {
26
- if (!part || typeof part !== "object") return "[unsupported content part]";
27
- if (part.type === "text") return String(part.text ?? "");
28
- if (part.type === "image_url" || part.type === "image") return "[image omitted: Black Mamba forwards text only]";
29
- return `[unsupported content part: ${String(part.type)}]`;
30
- })
31
- .join("\n");
32
- }
33
- throw new BadRequestError("Message 'content' must be a string or an array of content parts", "invalid_message");
34
- }
35
-
36
- /** Parses tool-call arguments; returns { value, raw } where raw is kept only when the string is not valid JSON. */
37
- export function parseToolArguments(args) {
38
- if (args === undefined || args === null) return { value: {} };
39
- if (typeof args === "object") return { value: args };
40
- if (typeof args === "string") {
41
- try {
42
- return { value: JSON.parse(args) };
43
- } catch {
44
- return { value: null, raw: args };
45
- }
46
- }
47
- return { value: null, raw: String(args) };
48
- }
49
-
50
- export function toolCallName(tc) {
51
- return tc?.function?.name ?? tc?.name ?? "";
52
- }
53
-
54
- export function toolCallArguments(tc) {
55
- return tc?.function?.arguments ?? tc?.arguments;
56
- }
57
-
58
- /** Structural view of a message that ignores presentation-only fields (reasoning_content, name, ids). */
59
- export function canonicalizeMessage(msg) {
60
- const role = normalizeRole(msg.role);
61
- const canonical = { role, content: msg.content === null || msg.content === undefined ? null : contentToText(msg.content) };
62
- if (role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
63
- canonical.tool_calls = msg.tool_calls.map((tc) => {
64
- const parsed = parseToolArguments(toolCallArguments(tc));
65
- return { name: toolCallName(tc), arguments: parsed.raw !== undefined ? parsed.raw : canonicalJson(parsed.value) };
66
- });
67
- }
68
- if (role === "tool") canonical.tool_call_id = msg.tool_call_id ?? null;
69
- return canonical;
70
- }
71
-
72
- export function messageHash(msg) {
73
- return shortHash(canonicalJson(canonicalizeMessage(msg)));
74
- }
75
-
76
- export function hasLeadingSystem(messages) {
77
- const role = normalizeRole(messages[0]?.role);
78
- return role === "system" || role === "developer";
79
- }
80
-
81
- /** Hash of the leading system message (or "none"). The system prompt is verified separately from the conversation body. */
82
- export function systemHash(messages) {
83
- return hasLeadingSystem(messages) ? messageHash(messages[0]) : "none";
84
- }
85
-
86
- /**
87
- * Chain hash over the conversation body (every message except a leading system message) of the first `count` messages.
88
- * Order-sensitive, so insertions, omissions and edits anywhere in the prefix are detected.
89
- */
90
- export function prefixHash(messages, count = messages.length) {
91
- const start = hasLeadingSystem(messages) ? 1 : 0;
92
- let acc = "bm-v2";
93
- for (let i = start; i < count; i++) {
94
- acc = sha256Hex(`${acc}:${messageHash(messages[i])}`);
95
- }
96
- return acc.slice(0, 32);
97
- }
98
-
99
- /** Fingerprint of an assistant reply as Black Mamba produced it (streamed text + emitted tool calls). */
100
- export function assistantFingerprint({ text, toolCalls = [] }) {
101
- return {
102
- textHash: shortHash((text ?? "").trim()),
103
- toolCalls: toolCalls.map((tc) => {
104
- const parsed = parseToolArguments(tc.argumentsString ?? tc.arguments);
105
- return { name: tc.name, argsHash: shortHash(parsed.raw !== undefined ? parsed.raw : canonicalJson(parsed.value)) };
106
- }),
107
- };
108
- }
109
-
110
- /** Fingerprint of an assistant message as pi replays it in a later request. */
111
- export function assistantFingerprintFromMessage(msg) {
112
- return assistantFingerprint({
113
- text: contentToText(msg.content),
114
- toolCalls: Array.isArray(msg.tool_calls) ? msg.tool_calls.map((tc) => ({ name: toolCallName(tc), arguments: toolCallArguments(tc) })) : [],
115
- });
116
- }
117
-
118
- export function fingerprintsEqual(a, b) {
119
- if (!a || !b) return false;
120
- if (a.textHash !== b.textHash || a.toolCalls.length !== b.toolCalls.length) return false;
121
- return a.toolCalls.every((tc, i) => tc.name === b.toolCalls[i].name && tc.argsHash === b.toolCalls[i].argsHash);
122
- }
123
-
124
- export function isCompactionSummaryMessage(msg) {
125
- return normalizeRole(msg?.role) === "user" && contentToText(msg.content).startsWith(PI_COMPACTION_SUMMARY_PREFIX);
126
- }
127
-
128
- export function isBranchSummaryMessage(msg) {
129
- return normalizeRole(msg?.role) === "user" && contentToText(msg.content).startsWith(PI_BRANCH_SUMMARY_PREFIX);
130
- }
@@ -1,214 +0,0 @@
1
- // POST /v1/chat/completions orchestration: parse -> plan -> DeepSeek turn -> stream back -> record node.
2
-
3
- import { CONFIG } from "../config.js";
4
- import { UpstreamContextRejectedError, UpstreamError } from "../errors.js";
5
- import { createLogger } from "../logger.js";
6
- import { consumeDeepSeekSseStream } from "../deepseek/sse.js";
7
- import { assistantFingerprint } from "../pi/messages.js";
8
- import { parseChatRequest } from "../pi/request.js";
9
- import {
10
- buildMappingInfo,
11
- buildUsageStats,
12
- createCompletionMetadata,
13
- formatCompletionResponse,
14
- formatStreamDeltaEvent,
15
- formatStreamFinishEvent,
16
- formatStreamReasoningEvent,
17
- formatStreamRoleEvent,
18
- formatStreamToolCallArgsEvent,
19
- formatStreamToolCallStartEvent,
20
- formatStreamUsageEvent,
21
- } from "../pi/response.js";
22
- import { createToolCallStreamProcessor, parseToolCallsFromText } from "../pi/tool-calls.js";
23
- import { STRATEGY, planCompletion, reseedAfterUpstreamRejection } from "../session/planner.js";
24
- import { estimateTokenCount } from "../utils/tokens.js";
25
- import { readJsonBody, sendJson, startSse, writeSse } from "./http.js";
26
-
27
- const STRATEGY_HEADER = "x-black-mamba-strategy";
28
-
29
- function describePlan(plan, routing) {
30
- const pi = routing ? `${routing.sessionId}/${routing.entryId}` : "stateless";
31
- const parent = plan.parentNode ? plan.parentNode.piEntryId : "-";
32
- const ds = plan.deepseek.sessionId ? `${plan.deepseek.sessionId}:${plan.deepseek.parentMessageId ?? "root"}` : "new-chat";
33
- return `pi=${pi} parent=${parent} strategy=${plan.strategy}${plan.reason ? ` reason=${plan.reason}` : ""} ds=${ds} delta=${plan.deltaMessageCount} prompt_chars=${plan.prompt.length}`;
34
- }
35
-
36
- /**
37
- * @param {object} deps
38
- * @param {ReturnType<import("../deepseek/client.js").createDeepSeekClient>} deps.deepseek
39
- * @param {import("../session/graph.js").SessionGraph} deps.graph
40
- * @param {ReturnType<import("./pacing.js").createPacer>} deps.pacer
41
- */
42
- export function createCompletionHandler({ deepseek, graph, pacer, logger = createLogger("Mapping"), keepAliveIntervalMs = CONFIG.server.keepAliveIntervalMs }) {
43
- async function openStream(plan, request, signal) {
44
- const dsSessionId = plan.deepseek.sessionId ?? (await deepseek.createChatSession());
45
- const { streamBody } = await deepseek.openCompletionStream({
46
- chatSessionId: dsSessionId,
47
- parentMessageId: plan.deepseek.parentMessageId,
48
- prompt: plan.prompt,
49
- thinkingEnabled: request.thinkingEnabled,
50
- searchEnabled: request.searchEnabled,
51
- signal,
52
- });
53
- return { dsSessionId, streamBody };
54
- }
55
-
56
- async function openWithExplicitRecovery(plan, request, signal) {
57
- try {
58
- return { plan, ...(await openStream(plan, request, signal)) };
59
- } catch (err) {
60
- if (!(err instanceof UpstreamContextRejectedError) || plan.strategy !== STRATEGY.DELTA) throw err;
61
- logger.warn(`DeepSeek rejected chat ${plan.deepseek.sessionId} parent ${plan.deepseek.parentMessageId}: ${err.message}. Re-seeding this branch into a fresh chat.`);
62
- const reseeded = reseedAfterUpstreamRejection(request, plan, err.details);
63
- logger.info(describePlan(reseeded, request.routing));
64
- return { plan: reseeded, ...(await openStream(reseeded, request, signal)) };
65
- }
66
- }
67
-
68
- function resolveTurnTokens(result, plan, replyText) {
69
- if (result.turnTokens > 0) return { turnTokens: result.turnTokens, source: "deepseek" };
70
- return { turnTokens: estimateTokenCount(plan.prompt) + estimateTokenCount(replyText), source: "estimated" };
71
- }
72
-
73
- async function recordNode({ plan, dsSessionId, result, text, toolCalls, turnTokens }) {
74
- if (!plan.record) {
75
- if (result.messageId === null || result.messageId === undefined) {
76
- logger.warn("DeepSeek stream ended without response_message_id (stateless request; nothing to record).");
77
- }
78
- return null;
79
- }
80
- if (result.messageId === null || result.messageId === undefined) {
81
- throw new UpstreamError("DeepSeek did not return a response_message_id; this turn cannot be mapped onto the session tree and was not recorded.", {
82
- code: "deepseek_missing_message_id",
83
- details: { piEntryId: plan.record.piEntryId, deepseekSessionId: dsSessionId },
84
- });
85
- }
86
- const node = {
87
- piEntryId: plan.record.piEntryId,
88
- piParentEntryId: plan.record.piParentEntryId,
89
- strategy: plan.strategy,
90
- reason: plan.reason,
91
- deepseek: { sessionId: dsSessionId, messageId: result.messageId, parentMessageId: plan.deepseek.parentMessageId },
92
- context: { ...plan.record.context, assistant: assistantFingerprint({ text, toolCalls }) },
93
- usage: { turnTokens, cachedTokens: plan.cachedTokens },
94
- createdAt: Date.now(),
95
- };
96
- await graph.putNode(plan.session, node);
97
- logger.info(`recorded pi=${plan.session.piSessionId}/${node.piEntryId} -> ds=${dsSessionId}:${result.messageId} (parent ${node.deepseek.parentMessageId ?? "root"}, ${turnTokens} turn tokens)`);
98
- return node;
99
- }
100
-
101
- function mappingInfo({ request, plan, dsSessionId, messageId, usageSource }) {
102
- const info = buildMappingInfo({
103
- sessionId: request.routing?.sessionId ?? dsSessionId,
104
- deepseekSessionId: dsSessionId,
105
- messageId,
106
- parentMessageId: plan.deepseek.parentMessageId,
107
- strategy: plan.strategy,
108
- reason: plan.reason,
109
- deltaMessageCount: plan.deltaMessageCount,
110
- });
111
- info.black_mamba.usage_source = usageSource;
112
- return info;
113
- }
114
-
115
- async function respondJson({ res, request, plan, dsSessionId, streamBody, meta }) {
116
- const result = await consumeDeepSeekSseStream(streamBody);
117
- let text = result.reply;
118
- let toolCalls = [];
119
- if (request.tools.length > 0) {
120
- const parsed = parseToolCallsFromText(result.reply);
121
- text = parsed.cleanText;
122
- toolCalls = parsed.toolCalls;
123
- }
124
- const { turnTokens, source } = resolveTurnTokens(result, plan, result.reply);
125
- const fingerprintCalls = toolCalls.map((tc) => ({ name: tc.function.name, argumentsString: tc.function.arguments }));
126
- await recordNode({ plan, dsSessionId, result, text, toolCalls: fingerprintCalls, turnTokens });
127
-
128
- const usage = buildUsageStats({ prompt: plan.prompt, reply: result.reply, turnTokens, cachedTokens: plan.cachedTokens });
129
- const payload = formatCompletionResponse({
130
- ...meta,
131
- model: request.model,
132
- content: text,
133
- toolCalls,
134
- usage,
135
- mapping: mappingInfo({ request, plan, dsSessionId, messageId: result.messageId, usageSource: source }),
136
- });
137
- sendJson(res, 200, payload, { [STRATEGY_HEADER]: plan.strategy });
138
- }
139
-
140
- async function respondStream({ req, res, request, plan, dsSessionId, streamBody, meta }) {
141
- startSse(res, req, { [STRATEGY_HEADER]: plan.strategy });
142
- writeSse(res, formatStreamRoleEvent(meta));
143
- const keepAlive = setInterval(() => writeSse(res, ": keep-alive\n\n"), keepAliveIntervalMs);
144
- keepAlive.unref?.();
145
-
146
- let emittedText = "";
147
- const emittedToolCalls = [];
148
- const emitText = (content) => {
149
- emittedText += content;
150
- writeSse(res, formatStreamDeltaEvent(meta, content));
151
- };
152
- const emitToolCall = ({ index, id, name, argumentsString }) => {
153
- emittedToolCalls.push({ name, argumentsString });
154
- writeSse(res, formatStreamToolCallStartEvent(meta, { toolCallIndex: index, id, name }));
155
- writeSse(res, formatStreamToolCallArgsEvent(meta, { toolCallIndex: index, argumentsChunk: argumentsString }));
156
- };
157
- const processor = request.tools.length > 0 ? createToolCallStreamProcessor({ onTextDelta: emitText, onToolCall: emitToolCall }) : null;
158
-
159
- let result;
160
- try {
161
- result = await consumeDeepSeekSseStream(streamBody, (chunk) => {
162
- if (chunk.isThinking) writeSse(res, formatStreamReasoningEvent(meta, chunk.text));
163
- else if (processor) processor.processChunk(chunk.text);
164
- else emitText(chunk.text);
165
- });
166
- processor?.flush();
167
- } finally {
168
- clearInterval(keepAlive);
169
- }
170
-
171
- const { turnTokens, source } = resolveTurnTokens(result, plan, result.reply);
172
- await recordNode({ plan, dsSessionId, result, text: emittedText, toolCalls: emittedToolCalls, turnTokens });
173
-
174
- writeSse(
175
- res,
176
- formatStreamFinishEvent(meta, {
177
- finishReason: emittedToolCalls.length > 0 ? "tool_calls" : "stop",
178
- mapping: mappingInfo({ request, plan, dsSessionId, messageId: result.messageId, usageSource: source }),
179
- })
180
- );
181
- if (request.includeUsage) {
182
- writeSse(res, formatStreamUsageEvent(meta, buildUsageStats({ prompt: plan.prompt, reply: result.reply, turnTokens, cachedTokens: plan.cachedTokens })));
183
- }
184
- writeSse(res, "data: [DONE]\n\n");
185
- res.end();
186
- }
187
-
188
- return async function handleChatCompletions(req, res) {
189
- const body = await readJsonBody(req);
190
- const request = parseChatRequest(body, req.headers);
191
- const initialPlan = await planCompletion({ request, graph, logger });
192
- logger.info(describePlan(initialPlan, request.routing));
193
-
194
- // Abort the DeepSeek turn if pi disconnects mid-stream (pi marks its assistant message as aborted and drops it).
195
- const abortController = new AbortController();
196
- let finished = false;
197
- if (typeof res.on === "function") {
198
- res.on("close", () => {
199
- if (!finished && !res.writableFinished) abortController.abort();
200
- });
201
- }
202
-
203
- await pacer.apply();
204
- try {
205
- const { plan, dsSessionId, streamBody } = await openWithExplicitRecovery(initialPlan, request, abortController.signal);
206
- const meta = { ...createCompletionMetadata(), model: request.model };
207
- if (request.stream) await respondStream({ req, res, request, plan, dsSessionId, streamBody, meta });
208
- else await respondJson({ res, request, plan, dsSessionId, streamBody, meta });
209
- } finally {
210
- finished = true;
211
- pacer.recordCompletionFinished();
212
- }
213
- };
214
- }