@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 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-16T10:23:43.043Z
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
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAEX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAoFhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAyFpD;;;;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;IA4MzD;;;;;;;;;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;;;;;;;;;;OAUG;YACY,sBAAsB;IAyGrC;;;;;;;;OAQG;IACH,OAAO,CAAE,eAAe;CAkIzB;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,+BAAyB,CAAA;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAElE"}
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.8",
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.2",
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",