@molecule/api-ai-deepseek 1.1.0 → 1.1.1
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 +5 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/provider.d.ts +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +96 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ 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-27T11:22:37.877Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-deepseek
|
|
@@ -225,8 +225,10 @@ malformed tool schema) gets its own non-retryable message distinct from the gene
|
|
|
225
225
|
**A stream that opens and then produces nothing is bounded, not waited on.** DeepSeek answers
|
|
226
226
|
200 and then keeps a stalled prefill alive with `: keep-alive` comments, so no HTTP status is
|
|
227
227
|
ever wrong and a byte-level idle timeout never sees the stall. `chat()` therefore bounds the
|
|
228
|
-
wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`)
|
|
229
|
-
|
|
228
|
+
wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`) — a window that
|
|
229
|
+
starts when the request is sent, so it also bounds an attempt whose response HEADERS never
|
|
230
|
+
arrive — and asks once more with a fresh stream; after the reply has started it bounds the
|
|
231
|
+
silence between chunks
|
|
230
232
|
instead (60 s, `MOL_DEEPSEEK_STREAM_SILENCE_TIMEOUT_MS`) and surfaces an error event, keeping
|
|
231
233
|
the partial reply. Hosts observe both through the `onStreamStall` callback.
|
|
232
234
|
|
package/dist/index.d.ts
CHANGED
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
* **A stream that opens and then produces nothing is bounded, not waited on.** DeepSeek answers
|
|
35
35
|
* 200 and then keeps a stalled prefill alive with `: keep-alive` comments, so no HTTP status is
|
|
36
36
|
* ever wrong and a byte-level idle timeout never sees the stall. `chat()` therefore bounds the
|
|
37
|
-
* wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`)
|
|
38
|
-
*
|
|
37
|
+
* wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`) — a window that
|
|
38
|
+
* starts when the request is sent, so it also bounds an attempt whose response HEADERS never
|
|
39
|
+
* arrive — and asks once more with a fresh stream; after the reply has started it bounds the
|
|
40
|
+
* silence between chunks
|
|
39
41
|
* instead (60 s, `MOL_DEEPSEEK_STREAM_SILENCE_TIMEOUT_MS`) and surfaces an error event, keeping
|
|
40
42
|
* the partial reply. Hosts observe both through the `onStreamStall` callback.
|
|
41
43
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAE1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAMlD;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UAUrB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
* **A stream that opens and then produces nothing is bounded, not waited on.** DeepSeek answers
|
|
35
35
|
* 200 and then keeps a stalled prefill alive with `: keep-alive` comments, so no HTTP status is
|
|
36
36
|
* ever wrong and a byte-level idle timeout never sees the stall. `chat()` therefore bounds the
|
|
37
|
-
* wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`)
|
|
38
|
-
*
|
|
37
|
+
* wait for the first CONTENT event (45 s, `MOL_DEEPSEEK_FIRST_TOKEN_TIMEOUT_MS`) — a window that
|
|
38
|
+
* starts when the request is sent, so it also bounds an attempt whose response HEADERS never
|
|
39
|
+
* arrive — and asks once more with a fresh stream; after the reply has started it bounds the
|
|
40
|
+
* silence between chunks
|
|
39
41
|
* instead (60 s, `MOL_DEEPSEEK_STREAM_SILENCE_TIMEOUT_MS`) and surfaces an error event, keeping
|
|
40
42
|
* the partial reply. Hosts observe both through the `onStreamStall` callback.
|
|
41
43
|
*
|
package/dist/provider.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
66
66
|
* them in one loop made each one's attempt count depend on the other's.
|
|
67
67
|
*
|
|
68
68
|
* @param args - The request body, headers, upstream model and abort signal.
|
|
69
|
-
* @returns The
|
|
69
|
+
* @returns The response and the final dispatch send time (deliberate backoff excluded).
|
|
70
70
|
*/
|
|
71
71
|
private requestWithRateLimitRetries;
|
|
72
72
|
/**
|
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;AAKH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAEV,cAAc,EACd,uBAAuB,EAGxB,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgHhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAEV,cAAc,EACd,uBAAuB,EAGxB,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgHhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AA8MpD;;;;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;IAsTzD;;;;;;;;;;;;;;;;;;OAkBG;IACH,wBAAwB,CAAC,KAAK,EAAE,cAAc,GAAG,uBAAuB;IAQxE;;;;;;;;;OASG;YACW,2BAA2B;IA4GzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,cAAc;IA8FtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IA+ElC;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CAwOzB;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
|
@@ -198,6 +198,22 @@ function isContentEvent(event) {
|
|
|
198
198
|
event.type === 'tool_use_start' ||
|
|
199
199
|
event.type === 'tool_input_delta');
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* The first-token window expired before response HEADERS arrived.
|
|
203
|
+
*
|
|
204
|
+
* Module-private: the only consumer is this provider's own stall loop, which
|
|
205
|
+
* reports it in the existing `before_content` shape and asks once more. It is
|
|
206
|
+
* never thrown for a caller cancellation — a caller abort (or the whole-attempt
|
|
207
|
+
* ceiling) outranks the window and is re-thrown untouched.
|
|
208
|
+
*/
|
|
209
|
+
class FirstTokenHeaderTimeoutError extends Error {
|
|
210
|
+
boundMs;
|
|
211
|
+
constructor(boundMs) {
|
|
212
|
+
super(`DeepSeek sent no response headers within ${boundMs} ms`);
|
|
213
|
+
this.boundMs = boundMs;
|
|
214
|
+
this.name = 'FirstTokenHeaderTimeoutError';
|
|
215
|
+
}
|
|
216
|
+
}
|
|
201
217
|
/**
|
|
202
218
|
* DeepSeek AI provider that implements the `AIProvider` interface using
|
|
203
219
|
* DeepSeek's OpenAI-compatible Chat Completions API with support for streaming,
|
|
@@ -368,14 +384,47 @@ class DeepseekAIProviderImpl {
|
|
|
368
384
|
// present, still wins over it (both abort this one controller).
|
|
369
385
|
const ceiling = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
370
386
|
ceiling.unref?.();
|
|
371
|
-
const signal = controller.signal;
|
|
372
387
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
388
|
+
let response;
|
|
389
|
+
let requestSentAt = 0;
|
|
390
|
+
try {
|
|
391
|
+
const dispatched = await this.requestWithRateLimitRetries({
|
|
392
|
+
body,
|
|
393
|
+
headers,
|
|
394
|
+
model,
|
|
395
|
+
controller,
|
|
396
|
+
// The first-token window covers an attempt that never receives
|
|
397
|
+
// response HEADERS too, not only one that receives them and then
|
|
398
|
+
// sends no content: a window that started at the headers left the
|
|
399
|
+
// whole pre-header phase to the five-minute attempt ceiling alone.
|
|
400
|
+
// Passing it only while streaming keeps the non-streaming path —
|
|
401
|
+
// which has no first token to wait for — on that ceiling.
|
|
402
|
+
...(params.stream === false ? {} : { firstTokenTimeoutMs }),
|
|
403
|
+
});
|
|
404
|
+
response = dispatched.response;
|
|
405
|
+
requestSentAt = dispatched.sentAt;
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (!(error instanceof FirstTokenHeaderTimeoutError) || params.signal?.aborted)
|
|
409
|
+
throw error;
|
|
410
|
+
// Nothing reached the consumer, so the same once-only re-ask a
|
|
411
|
+
// post-header stall gets is safe here too — reported in the
|
|
412
|
+
// established `before_content` shape, so hosts and analytics see one
|
|
413
|
+
// kind of stall either way.
|
|
414
|
+
const willRetry = stalledAttempt < MAX_STALL_RETRIES;
|
|
415
|
+
this.reportStreamStall(model, {
|
|
416
|
+
stallMs: error.boundMs,
|
|
417
|
+
attempt: stalledAttempt + 1,
|
|
418
|
+
willRetry,
|
|
419
|
+
streamedChars: 0,
|
|
420
|
+
keepAlives: 0,
|
|
421
|
+
phase: 'before_content',
|
|
422
|
+
});
|
|
423
|
+
if (willRetry)
|
|
424
|
+
continue;
|
|
425
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
379
428
|
// Whatever the transport exposes about this call. DeepSeek's edge
|
|
380
429
|
// answers with `x-ds-trace-id`; other OpenAI-compatible hosts use
|
|
381
430
|
// `x-request-id`. Recorded rather than read for control flow: it is the
|
|
@@ -467,14 +516,17 @@ class DeepseekAIProviderImpl {
|
|
|
467
516
|
controller.abort();
|
|
468
517
|
}, ms);
|
|
469
518
|
};
|
|
470
|
-
|
|
519
|
+
// Headers and content share this dispatch's existing first-token window.
|
|
520
|
+
// A 429/503 retry has its own send time; deliberate backoff is excluded.
|
|
521
|
+
armBound(Math.max(0, firstTokenTimeoutMs - (Date.now() - requestSentAt)), 'before_content');
|
|
522
|
+
bounds.windowStartedAt = requestSentAt;
|
|
471
523
|
try {
|
|
472
524
|
yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
|
|
473
525
|
}
|
|
474
526
|
catch (error) {
|
|
475
527
|
// Only a bound expiry is this provider's to absorb — anything else
|
|
476
528
|
// (the caller aborting, a socket failure) belongs to the caller.
|
|
477
|
-
if (!bounds.expired)
|
|
529
|
+
if (!bounds.expired || params.signal?.aborted)
|
|
478
530
|
throw error;
|
|
479
531
|
const stall = {
|
|
480
532
|
stallMs: Date.now() - bounds.windowStartedAt,
|
|
@@ -543,20 +595,38 @@ class DeepseekAIProviderImpl {
|
|
|
543
595
|
* them in one loop made each one's attempt count depend on the other's.
|
|
544
596
|
*
|
|
545
597
|
* @param args - The request body, headers, upstream model and abort signal.
|
|
546
|
-
* @returns The
|
|
598
|
+
* @returns The response and the final dispatch send time (deliberate backoff excluded).
|
|
547
599
|
*/
|
|
548
600
|
async requestWithRateLimitRetries(args) {
|
|
549
|
-
const { body, headers, model,
|
|
601
|
+
const { body, headers, model, controller, firstTokenTimeoutMs } = args;
|
|
602
|
+
const signal = controller.signal;
|
|
550
603
|
// Retry with exponential backoff for rate limits (429) and overloaded (503)
|
|
551
604
|
const MAX_RETRIES = 3;
|
|
552
605
|
let response = null;
|
|
606
|
+
let sentAt = 0;
|
|
553
607
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
608
|
+
// Bound THIS dispatch, not the retry loop: a deliberate backoff sleep is
|
|
609
|
+
// this provider's own wait, so no window is running while it is spent.
|
|
610
|
+
let headerDeadline = null;
|
|
611
|
+
sentAt = Date.now();
|
|
612
|
+
if (firstTokenTimeoutMs !== undefined) {
|
|
613
|
+
headerDeadline = setTimeout(() => controller.abort(new FirstTokenHeaderTimeoutError(firstTokenTimeoutMs)), firstTokenTimeoutMs);
|
|
614
|
+
headerDeadline.unref?.();
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
response = await fetch(`${this.baseUrl}${this.completionsPath}`, {
|
|
618
|
+
method: 'POST',
|
|
619
|
+
headers,
|
|
620
|
+
body: JSON.stringify(body),
|
|
621
|
+
signal,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
finally {
|
|
625
|
+
// Release the header timer when fetch settles. The successful SSE reader
|
|
626
|
+
// uses the remaining window; a rate-limit retry starts a new dispatch.
|
|
627
|
+
if (headerDeadline)
|
|
628
|
+
clearTimeout(headerDeadline);
|
|
629
|
+
}
|
|
560
630
|
if (response.status === 429 || response.status === 503) {
|
|
561
631
|
const willRetry = attempt < MAX_RETRIES;
|
|
562
632
|
// Retry-After is equally-valid as delta-seconds or an HTTP-date;
|
|
@@ -593,21 +663,24 @@ class DeepseekAIProviderImpl {
|
|
|
593
663
|
attempt: attempt + 1,
|
|
594
664
|
delayMs,
|
|
595
665
|
});
|
|
666
|
+
// A host may cancel inside onRateLimit, before a backoff listener exists.
|
|
667
|
+
signal.throwIfAborted();
|
|
596
668
|
await new Promise((resolve) => {
|
|
597
|
-
const
|
|
598
|
-
signal.addEventListener('abort', () => {
|
|
669
|
+
const finish = () => {
|
|
599
670
|
clearTimeout(timer);
|
|
671
|
+
signal.removeEventListener('abort', finish);
|
|
600
672
|
resolve();
|
|
601
|
-
}
|
|
673
|
+
};
|
|
674
|
+
const timer = setTimeout(finish, delayMs);
|
|
675
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
602
676
|
});
|
|
603
|
-
|
|
604
|
-
break;
|
|
677
|
+
signal.throwIfAborted();
|
|
605
678
|
continue;
|
|
606
679
|
}
|
|
607
680
|
}
|
|
608
681
|
break;
|
|
609
682
|
}
|
|
610
|
-
return response;
|
|
683
|
+
return { response: response, sentAt };
|
|
611
684
|
}
|
|
612
685
|
/**
|
|
613
686
|
* Record one stalled attempt: a warn line always, plus the host's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-deepseek",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@molecule/api-ai": "1.4.0",
|
|
37
|
-
"@molecule/api-bond": "1.0.
|
|
37
|
+
"@molecule/api-bond": "1.0.4",
|
|
38
38
|
"@types/node": "26.6.5",
|
|
39
39
|
"typescript": "6.0.3",
|
|
40
40
|
"vitest": "4.1.12"
|