@molecule/api-ai-deepseek 1.0.8 → 1.0.10
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/README.md +3 -3
- package/dist/provider.d.ts +4 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +91 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,17 +3,17 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-16T23:28:16.072Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-deepseek
|
|
10
10
|
|
|
11
|
+
DeepSeek AI provider for molecule.dev.
|
|
12
|
+
|
|
11
13
|
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
14
|
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
15
|
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
16
|
|
|
15
|
-
DeepSeek AI provider for molecule.dev.
|
|
16
|
-
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```typescript
|
package/dist/provider.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
90
90
|
* and streamed content. Owned by the caller so a retry can read what the
|
|
91
91
|
* abandoned attempt produced.
|
|
92
92
|
* @param bounds - How to re-arm the stream bound after the reply starts.
|
|
93
|
+
* @param call - The request's own identity, filled in from the response —
|
|
94
|
+
* see {@link DeepseekCallContext}.
|
|
93
95
|
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
94
96
|
*/
|
|
95
97
|
private parseStreamingResponse;
|
|
@@ -100,6 +102,8 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
100
102
|
* @param lines - Raw SSE lines to process.
|
|
101
103
|
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
102
104
|
* @param state - Mutable token usage counters.
|
|
105
|
+
* @param call - The request's identity: the answer's own `id` and `model` are
|
|
106
|
+
* recorded on `call.meta`, and any echo of the request is verified.
|
|
103
107
|
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
104
108
|
*/
|
|
105
109
|
private processSSELines;
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAGX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAsGhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAgGpD;;;;GAIG;AACH,cAAM,sBAAuB,YAAW,UAAU;IAChD,QAAQ,CAAC,IAAI,cAAa;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,OAAO,CAAC,aAAa,CAAC,CAAuB;IAC7C,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,sBAAsB,CAAQ;gBAE1B,MAAM,GAAE,cAAmB;IAwBvC;;;;;OAKG;IACI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC;IAqPzD;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAgFtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IAuDlC;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CA2KzB;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,+BAAyB,CAAA;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAElE"}
|
package/dist/provider.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
11
12
|
import { getLogger } from '@molecule/api-bond';
|
|
12
13
|
import { t } from '@molecule/api-i18n';
|
|
13
14
|
const logger = getLogger();
|
|
@@ -32,6 +33,12 @@ export const DEFAULT_FIRST_TOKEN_TIMEOUT_MS = 45_000;
|
|
|
32
33
|
const DEFAULT_STREAM_SILENCE_TIMEOUT_MS = 60_000;
|
|
33
34
|
/** Node's `setTimeout` ceiling: a larger delay is clamped to 1 ms, i.e. instant. */
|
|
34
35
|
const MAX_TIMER_MS = 2_147_483_647;
|
|
36
|
+
/**
|
|
37
|
+
* How many stop sequences DeepSeek accepts in one request ("Up to 16 sequences
|
|
38
|
+
* where the API will stop generating further tokens", create-chat-completion).
|
|
39
|
+
* A longer list is sent truncated rather than refused by the API.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_STOP_SEQUENCES = 16;
|
|
35
42
|
/**
|
|
36
43
|
* Resolve a stream bound from the config, then the environment, then the
|
|
37
44
|
* default. A value that is not a positive finite number is IGNORED rather than
|
|
@@ -143,6 +150,16 @@ class DeepseekAIProviderImpl {
|
|
|
143
150
|
const model = this.modelMap[canonicalModel] ?? canonicalModel;
|
|
144
151
|
const maxTokens = params.maxTokens ?? this.maxTokens;
|
|
145
152
|
const messages = this.formatMessages(params.messages, params.system);
|
|
153
|
+
// Stop sequences, capped at the API's own limit. Verified honoured by the
|
|
154
|
+
// native endpoint on 2026-10-17: with `stop: ['<analysis>']` a reply that
|
|
155
|
+
// would have opened with that tag streamed nothing at all, and one that
|
|
156
|
+
// reached it mid-answer ended there with the tag dropped from the text.
|
|
157
|
+
const stop = (params.stop ?? []).filter((s) => s.length > 0).slice(0, MAX_STOP_SEQUENCES);
|
|
158
|
+
if ((params.stop?.length ?? 0) > MAX_STOP_SEQUENCES) {
|
|
159
|
+
logger.warn('DeepSeek takes at most 16 stop sequences — the rest were dropped', {
|
|
160
|
+
requested: params.stop?.length,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
146
163
|
const body = {
|
|
147
164
|
// Abuse attribution (see ChatParams.endUserId): OpenAI-compatible APIs
|
|
148
165
|
// scope enforcement by `user`, so an abusive tenant can be actioned
|
|
@@ -156,6 +173,7 @@ class DeepseekAIProviderImpl {
|
|
|
156
173
|
messages,
|
|
157
174
|
stream: params.stream !== false,
|
|
158
175
|
...(params.stream !== false ? { stream_options: { include_usage: true } } : {}),
|
|
176
|
+
...(stop.length > 0 ? { stop } : {}),
|
|
159
177
|
};
|
|
160
178
|
// Tools: merge custom tools (function format) and server tools
|
|
161
179
|
const functions = params.tools?.length ? this.formatTools(params.tools) : [];
|
|
@@ -205,9 +223,24 @@ class DeepseekAIProviderImpl {
|
|
|
205
223
|
body.temperature = params.temperature;
|
|
206
224
|
}
|
|
207
225
|
}
|
|
226
|
+
// Correlation for THIS call (see ChatParams.requestId). It rides a header
|
|
227
|
+
// rather than the body's `user`: `user` is the stable per-user
|
|
228
|
+
// abuse-attribution value the provider scopes enforcement by, and varying it
|
|
229
|
+
// per request would make one account look like thousands. The header is
|
|
230
|
+
// ignored by every OpenAI-compatible server and is here so the vendor can
|
|
231
|
+
// find our request when a reply turns out not to belong to it.
|
|
232
|
+
const requestId = params.requestId ?? randomUUID();
|
|
233
|
+
// What the provider tells us about the call, reported on `done` (or logged
|
|
234
|
+
// when the reply has to be dropped as foreign). `responseId` and `model`
|
|
235
|
+
// come off the stream itself; the trace id off the response headers.
|
|
236
|
+
const call = {
|
|
237
|
+
meta: { requestId, endpoint: `${this.baseUrl}${this.completionsPath}` },
|
|
238
|
+
endUserId: params.endUserId,
|
|
239
|
+
};
|
|
208
240
|
const headers = {
|
|
209
241
|
'Content-Type': 'application/json',
|
|
210
242
|
Authorization: `Bearer ${this.apiKey}`,
|
|
243
|
+
'X-Mol-Request-Id': requestId,
|
|
211
244
|
};
|
|
212
245
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
213
246
|
const firstTokenTimeoutMs = this.firstTokenTimeoutMs;
|
|
@@ -241,6 +274,16 @@ class DeepseekAIProviderImpl {
|
|
|
241
274
|
model,
|
|
242
275
|
signal,
|
|
243
276
|
});
|
|
277
|
+
// Whatever the transport exposes about this call. DeepSeek's edge
|
|
278
|
+
// answers with `x-ds-trace-id`; other OpenAI-compatible hosts use
|
|
279
|
+
// `x-request-id`. Recorded rather than read for control flow: it is the
|
|
280
|
+
// handle a vendor investigation needs.
|
|
281
|
+
const traceId = response.headers.get('x-ds-trace-id') ??
|
|
282
|
+
response.headers.get('x-request-id') ??
|
|
283
|
+
response.headers.get('request-id') ??
|
|
284
|
+
undefined;
|
|
285
|
+
if (traceId)
|
|
286
|
+
call.meta.traceId = traceId;
|
|
244
287
|
if (!response.ok) {
|
|
245
288
|
const errorBody = await response.text();
|
|
246
289
|
let detail = `HTTP ${response.status}`;
|
|
@@ -299,10 +342,7 @@ class DeepseekAIProviderImpl {
|
|
|
299
342
|
};
|
|
300
343
|
armBound(firstTokenTimeoutMs, 'before_content');
|
|
301
344
|
try {
|
|
302
|
-
yield* this.parseStreamingResponse(response, state, {
|
|
303
|
-
silenceMs: this.streamSilenceTimeoutMs,
|
|
304
|
-
arm: armBound,
|
|
305
|
-
});
|
|
345
|
+
yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
|
|
306
346
|
}
|
|
307
347
|
catch (error) {
|
|
308
348
|
// Only a bound expiry is this provider's to absorb — anything else
|
|
@@ -608,9 +648,11 @@ class DeepseekAIProviderImpl {
|
|
|
608
648
|
* and streamed content. Owned by the caller so a retry can read what the
|
|
609
649
|
* abandoned attempt produced.
|
|
610
650
|
* @param bounds - How to re-arm the stream bound after the reply starts.
|
|
651
|
+
* @param call - The request's own identity, filled in from the response —
|
|
652
|
+
* see {@link DeepseekCallContext}.
|
|
611
653
|
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
612
654
|
*/
|
|
613
|
-
async *parseStreamingResponse(response, state, bounds) {
|
|
655
|
+
async *parseStreamingResponse(response, state, bounds, call) {
|
|
614
656
|
const reader = response.body?.getReader();
|
|
615
657
|
if (!reader) {
|
|
616
658
|
yield {
|
|
@@ -634,7 +676,7 @@ class DeepseekAIProviderImpl {
|
|
|
634
676
|
const lines = buffer.split('\n');
|
|
635
677
|
buffer = lines.pop() ?? '';
|
|
636
678
|
let yieldedContent = false;
|
|
637
|
-
for (const event of this.processSSELines(lines, pendingTools, state)) {
|
|
679
|
+
for (const event of this.processSSELines(lines, pendingTools, state, call)) {
|
|
638
680
|
yieldedContent = true;
|
|
639
681
|
// The reply has started. Counting this from the EVENTS (rather than
|
|
640
682
|
// from the byte stream) is what lets the first-token bound see a
|
|
@@ -673,7 +715,7 @@ class DeepseekAIProviderImpl {
|
|
|
673
715
|
}
|
|
674
716
|
// Flush any remaining data in the buffer after EOF
|
|
675
717
|
if (buffer.trim()) {
|
|
676
|
-
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
|
|
718
|
+
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state, call)) {
|
|
677
719
|
yield event;
|
|
678
720
|
if (event.type === 'error')
|
|
679
721
|
return;
|
|
@@ -704,6 +746,7 @@ class DeepseekAIProviderImpl {
|
|
|
704
746
|
outputTokens: state.outputTokens,
|
|
705
747
|
cacheReadInputTokens: state.cachedTokens,
|
|
706
748
|
},
|
|
749
|
+
provider: call.meta,
|
|
707
750
|
};
|
|
708
751
|
}
|
|
709
752
|
/**
|
|
@@ -713,9 +756,11 @@ class DeepseekAIProviderImpl {
|
|
|
713
756
|
* @param lines - Raw SSE lines to process.
|
|
714
757
|
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
715
758
|
* @param state - Mutable token usage counters.
|
|
759
|
+
* @param call - The request's identity: the answer's own `id` and `model` are
|
|
760
|
+
* recorded on `call.meta`, and any echo of the request is verified.
|
|
716
761
|
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
717
762
|
*/
|
|
718
|
-
*processSSELines(lines, pendingTools, state) {
|
|
763
|
+
*processSSELines(lines, pendingTools, state, call) {
|
|
719
764
|
for (const line of lines) {
|
|
720
765
|
if (!line.startsWith('data: '))
|
|
721
766
|
continue;
|
|
@@ -724,6 +769,44 @@ class DeepseekAIProviderImpl {
|
|
|
724
769
|
continue;
|
|
725
770
|
try {
|
|
726
771
|
const event = JSON.parse(json);
|
|
772
|
+
// The reply's OWN identity, recorded for the caller: the completion id
|
|
773
|
+
// (one per answer, repeated on every delta) and the model the provider
|
|
774
|
+
// says answered. Recorded rather than asserted — a re-host legitimately
|
|
775
|
+
// answers under its own dated snapshot id.
|
|
776
|
+
if (typeof event.id === 'string')
|
|
777
|
+
call.meta.responseId = event.id;
|
|
778
|
+
if (typeof event.model === 'string')
|
|
779
|
+
call.meta.model = event.model;
|
|
780
|
+
// If this host echoes an identity back, verify it. A reply carrying an
|
|
781
|
+
// identity that is not the one we sent is a reply that was produced for
|
|
782
|
+
// somebody else's request, and it must not be streamed on as ours —
|
|
783
|
+
// 2026-10-16 a conversation received a complete foreign completion on a
|
|
784
|
+
// stream whose usage matched its own context, and nothing could tell.
|
|
785
|
+
// DeepSeek's own API echoes nothing, so this arm is inert there; it is
|
|
786
|
+
// live for any OpenAI-compatible host that does, and it is the only
|
|
787
|
+
// check that runs BEFORE the text reaches the caller.
|
|
788
|
+
const echoedUser = typeof event.user === 'string' ? event.user : undefined;
|
|
789
|
+
const echoedRequest = typeof event.request_id === 'string'
|
|
790
|
+
? event.request_id
|
|
791
|
+
: typeof event.requestId === 'string'
|
|
792
|
+
? event.requestId
|
|
793
|
+
: undefined;
|
|
794
|
+
const wrongUser = echoedUser !== undefined && call.endUserId !== undefined && echoedUser !== call.endUserId;
|
|
795
|
+
const wrongRequest = echoedRequest !== undefined && echoedRequest !== call.meta.requestId;
|
|
796
|
+
if (wrongUser || wrongRequest) {
|
|
797
|
+
logger.error('Provider echoed a different identity — this reply is not ours', {
|
|
798
|
+
requestId: call.meta.requestId,
|
|
799
|
+
echoedUser: wrongUser ? echoedUser : undefined,
|
|
800
|
+
echoedRequestId: wrongRequest ? echoedRequest : undefined,
|
|
801
|
+
responseId: call.meta.responseId,
|
|
802
|
+
traceId: call.meta.traceId,
|
|
803
|
+
endpoint: call.meta.endpoint,
|
|
804
|
+
});
|
|
805
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
806
|
+
// The whole reply is somebody else's: stop reading it. The caller's
|
|
807
|
+
// error ladder retries the request once.
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
727
810
|
// A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
|
|
728
811
|
// `choices`) — most importantly rate-limit/overload during high load.
|
|
729
812
|
// Without this it matches no branch below and is silently dropped, so the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-deepseek",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"directory": "packages/api/bonds/ai/deepseek"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@molecule/api-ai": "1.3.
|
|
36
|
+
"@molecule/api-ai": "1.3.4",
|
|
37
37
|
"@molecule/api-bond": "1.0.2",
|
|
38
38
|
"@types/node": "26.1.2",
|
|
39
39
|
"typescript": "6.0.3",
|