@molecule/api-ai-deepseek 1.0.7 → 1.0.9

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.
@@ -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;IA2MzD;;;;;;;;;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;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;IAyOzD;;;;;;;;;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();
@@ -205,9 +206,24 @@ class DeepseekAIProviderImpl {
205
206
  body.temperature = params.temperature;
206
207
  }
207
208
  }
209
+ // Correlation for THIS call (see ChatParams.requestId). It rides a header
210
+ // rather than the body's `user`: `user` is the stable per-user
211
+ // abuse-attribution value the provider scopes enforcement by, and varying it
212
+ // per request would make one account look like thousands. The header is
213
+ // ignored by every OpenAI-compatible server and is here so the vendor can
214
+ // find our request when a reply turns out not to belong to it.
215
+ const requestId = params.requestId ?? randomUUID();
216
+ // What the provider tells us about the call, reported on `done` (or logged
217
+ // when the reply has to be dropped as foreign). `responseId` and `model`
218
+ // come off the stream itself; the trace id off the response headers.
219
+ const call = {
220
+ meta: { requestId, endpoint: `${this.baseUrl}${this.completionsPath}` },
221
+ endUserId: params.endUserId,
222
+ };
208
223
  const headers = {
209
224
  'Content-Type': 'application/json',
210
225
  Authorization: `Bearer ${this.apiKey}`,
226
+ 'X-Mol-Request-Id': requestId,
211
227
  };
212
228
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
213
229
  const firstTokenTimeoutMs = this.firstTokenTimeoutMs;
@@ -217,10 +233,11 @@ class DeepseekAIProviderImpl {
217
233
  // wrong, and DeepSeek keeps a stalled prefill's connection alive with
218
234
  // `: keep-alive` comments — so a byte-level idle timeout stays quiet too.
219
235
  // Left alone, the whole first-token window is spent before anything notices.
220
- // Measured in production (X0 xr-molecule-flash-m71, 2026-10-16): the chat
221
- // handler's 180 s progress watchdog was the ONLY bound, and a 3 min 15 s hole
222
- // opened in the middle of a live turn. Bound it here instead, where the same
223
- // request can simply be asked again.
236
+ // Measured in production: on 2026-10-01 nine turns died this way in one
237
+ // project, each spending 540-582 s — three 180 s windows of the chat
238
+ // handler's progress watchdog, which was the ONLY bound on the stream, and
239
+ // their own record named no provider time at all. Bound it here instead,
240
+ // where the same request can simply be asked again.
224
241
  for (let stalledAttempt = 0;; stalledAttempt++) {
225
242
  const controller = new AbortController();
226
243
  const forwardAbort = () => controller.abort();
@@ -240,6 +257,16 @@ class DeepseekAIProviderImpl {
240
257
  model,
241
258
  signal,
242
259
  });
260
+ // Whatever the transport exposes about this call. DeepSeek's edge
261
+ // answers with `x-ds-trace-id`; other OpenAI-compatible hosts use
262
+ // `x-request-id`. Recorded rather than read for control flow: it is the
263
+ // handle a vendor investigation needs.
264
+ const traceId = response.headers.get('x-ds-trace-id') ??
265
+ response.headers.get('x-request-id') ??
266
+ response.headers.get('request-id') ??
267
+ undefined;
268
+ if (traceId)
269
+ call.meta.traceId = traceId;
243
270
  if (!response.ok) {
244
271
  const errorBody = await response.text();
245
272
  let detail = `HTTP ${response.status}`;
@@ -298,10 +325,7 @@ class DeepseekAIProviderImpl {
298
325
  };
299
326
  armBound(firstTokenTimeoutMs, 'before_content');
300
327
  try {
301
- yield* this.parseStreamingResponse(response, state, {
302
- silenceMs: this.streamSilenceTimeoutMs,
303
- arm: armBound,
304
- });
328
+ yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
305
329
  }
306
330
  catch (error) {
307
331
  // Only a bound expiry is this provider's to absorb — anything else
@@ -607,9 +631,11 @@ class DeepseekAIProviderImpl {
607
631
  * and streamed content. Owned by the caller so a retry can read what the
608
632
  * abandoned attempt produced.
609
633
  * @param bounds - How to re-arm the stream bound after the reply starts.
634
+ * @param call - The request's own identity, filled in from the response —
635
+ * see {@link DeepseekCallContext}.
610
636
  * @yields {ChatEvent} Chat events as they arrive from the SSE stream.
611
637
  */
612
- async *parseStreamingResponse(response, state, bounds) {
638
+ async *parseStreamingResponse(response, state, bounds, call) {
613
639
  const reader = response.body?.getReader();
614
640
  if (!reader) {
615
641
  yield {
@@ -633,7 +659,7 @@ class DeepseekAIProviderImpl {
633
659
  const lines = buffer.split('\n');
634
660
  buffer = lines.pop() ?? '';
635
661
  let yieldedContent = false;
636
- for (const event of this.processSSELines(lines, pendingTools, state)) {
662
+ for (const event of this.processSSELines(lines, pendingTools, state, call)) {
637
663
  yieldedContent = true;
638
664
  // The reply has started. Counting this from the EVENTS (rather than
639
665
  // from the byte stream) is what lets the first-token bound see a
@@ -672,7 +698,7 @@ class DeepseekAIProviderImpl {
672
698
  }
673
699
  // Flush any remaining data in the buffer after EOF
674
700
  if (buffer.trim()) {
675
- for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
701
+ for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state, call)) {
676
702
  yield event;
677
703
  if (event.type === 'error')
678
704
  return;
@@ -703,6 +729,7 @@ class DeepseekAIProviderImpl {
703
729
  outputTokens: state.outputTokens,
704
730
  cacheReadInputTokens: state.cachedTokens,
705
731
  },
732
+ provider: call.meta,
706
733
  };
707
734
  }
708
735
  /**
@@ -712,9 +739,11 @@ class DeepseekAIProviderImpl {
712
739
  * @param lines - Raw SSE lines to process.
713
740
  * @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
714
741
  * @param state - Mutable token usage counters.
742
+ * @param call - The request's identity: the answer's own `id` and `model` are
743
+ * recorded on `call.meta`, and any echo of the request is verified.
715
744
  * @yields {ChatEvent} Chat events extracted from the SSE lines.
716
745
  */
717
- *processSSELines(lines, pendingTools, state) {
746
+ *processSSELines(lines, pendingTools, state, call) {
718
747
  for (const line of lines) {
719
748
  if (!line.startsWith('data: '))
720
749
  continue;
@@ -723,6 +752,44 @@ class DeepseekAIProviderImpl {
723
752
  continue;
724
753
  try {
725
754
  const event = JSON.parse(json);
755
+ // The reply's OWN identity, recorded for the caller: the completion id
756
+ // (one per answer, repeated on every delta) and the model the provider
757
+ // says answered. Recorded rather than asserted — a re-host legitimately
758
+ // answers under its own dated snapshot id.
759
+ if (typeof event.id === 'string')
760
+ call.meta.responseId = event.id;
761
+ if (typeof event.model === 'string')
762
+ call.meta.model = event.model;
763
+ // If this host echoes an identity back, verify it. A reply carrying an
764
+ // identity that is not the one we sent is a reply that was produced for
765
+ // somebody else's request, and it must not be streamed on as ours —
766
+ // 2026-10-16 a conversation received a complete foreign completion on a
767
+ // stream whose usage matched its own context, and nothing could tell.
768
+ // DeepSeek's own API echoes nothing, so this arm is inert there; it is
769
+ // live for any OpenAI-compatible host that does, and it is the only
770
+ // check that runs BEFORE the text reaches the caller.
771
+ const echoedUser = typeof event.user === 'string' ? event.user : undefined;
772
+ const echoedRequest = typeof event.request_id === 'string'
773
+ ? event.request_id
774
+ : typeof event.requestId === 'string'
775
+ ? event.requestId
776
+ : undefined;
777
+ const wrongUser = echoedUser !== undefined && call.endUserId !== undefined && echoedUser !== call.endUserId;
778
+ const wrongRequest = echoedRequest !== undefined && echoedRequest !== call.meta.requestId;
779
+ if (wrongUser || wrongRequest) {
780
+ logger.error('Provider echoed a different identity — this reply is not ours', {
781
+ requestId: call.meta.requestId,
782
+ echoedUser: wrongUser ? echoedUser : undefined,
783
+ echoedRequestId: wrongRequest ? echoedRequest : undefined,
784
+ responseId: call.meta.responseId,
785
+ traceId: call.meta.traceId,
786
+ endpoint: call.meta.endpoint,
787
+ });
788
+ yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
789
+ // The whole reply is somebody else's: stop reading it. The caller's
790
+ // error ladder retries the request once.
791
+ return;
792
+ }
726
793
  // A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
727
794
  // `choices`) — most importantly rate-limit/overload during high load.
728
795
  // 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.7",
3
+ "version": "1.0.9",
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.1",
36
+ "@molecule/api-ai": "1.3.3",
37
37
  "@molecule/api-bond": "1.0.2",
38
38
  "@types/node": "26.1.2",
39
39
  "typescript": "6.0.3",