@molecule/api-ai-deepseek 1.0.8 → 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;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;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;
@@ -241,6 +257,16 @@ class DeepseekAIProviderImpl {
241
257
  model,
242
258
  signal,
243
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;
244
270
  if (!response.ok) {
245
271
  const errorBody = await response.text();
246
272
  let detail = `HTTP ${response.status}`;
@@ -299,10 +325,7 @@ class DeepseekAIProviderImpl {
299
325
  };
300
326
  armBound(firstTokenTimeoutMs, 'before_content');
301
327
  try {
302
- yield* this.parseStreamingResponse(response, state, {
303
- silenceMs: this.streamSilenceTimeoutMs,
304
- arm: armBound,
305
- });
328
+ yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
306
329
  }
307
330
  catch (error) {
308
331
  // Only a bound expiry is this provider's to absorb — anything else
@@ -608,9 +631,11 @@ class DeepseekAIProviderImpl {
608
631
  * and streamed content. Owned by the caller so a retry can read what the
609
632
  * abandoned attempt produced.
610
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}.
611
636
  * @yields {ChatEvent} Chat events as they arrive from the SSE stream.
612
637
  */
613
- async *parseStreamingResponse(response, state, bounds) {
638
+ async *parseStreamingResponse(response, state, bounds, call) {
614
639
  const reader = response.body?.getReader();
615
640
  if (!reader) {
616
641
  yield {
@@ -634,7 +659,7 @@ class DeepseekAIProviderImpl {
634
659
  const lines = buffer.split('\n');
635
660
  buffer = lines.pop() ?? '';
636
661
  let yieldedContent = false;
637
- for (const event of this.processSSELines(lines, pendingTools, state)) {
662
+ for (const event of this.processSSELines(lines, pendingTools, state, call)) {
638
663
  yieldedContent = true;
639
664
  // The reply has started. Counting this from the EVENTS (rather than
640
665
  // from the byte stream) is what lets the first-token bound see a
@@ -673,7 +698,7 @@ class DeepseekAIProviderImpl {
673
698
  }
674
699
  // Flush any remaining data in the buffer after EOF
675
700
  if (buffer.trim()) {
676
- for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
701
+ for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state, call)) {
677
702
  yield event;
678
703
  if (event.type === 'error')
679
704
  return;
@@ -704,6 +729,7 @@ class DeepseekAIProviderImpl {
704
729
  outputTokens: state.outputTokens,
705
730
  cacheReadInputTokens: state.cachedTokens,
706
731
  },
732
+ provider: call.meta,
707
733
  };
708
734
  }
709
735
  /**
@@ -713,9 +739,11 @@ class DeepseekAIProviderImpl {
713
739
  * @param lines - Raw SSE lines to process.
714
740
  * @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
715
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.
716
744
  * @yields {ChatEvent} Chat events extracted from the SSE lines.
717
745
  */
718
- *processSSELines(lines, pendingTools, state) {
746
+ *processSSELines(lines, pendingTools, state, call) {
719
747
  for (const line of lines) {
720
748
  if (!line.startsWith('data: '))
721
749
  continue;
@@ -724,6 +752,44 @@ class DeepseekAIProviderImpl {
724
752
  continue;
725
753
  try {
726
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
+ }
727
793
  // A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
728
794
  // `choices`) — most importantly rate-limit/overload during high load.
729
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.8",
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.2",
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",