@molecule/api-ai-deepseek 1.0.10 → 1.0.12
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/dist/provider.d.ts +12 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +98 -32
- package/package.json +2 -2
package/dist/provider.d.ts
CHANGED
|
@@ -62,8 +62,20 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
62
62
|
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
63
63
|
* System prompt is prepended as a system role message.
|
|
64
64
|
*
|
|
65
|
+
* Thinking mode has one wire obligation that non-thinking mode does not:
|
|
66
|
+
* DeepSeek's thinking-mode guide requires the `reasoning_content` of every
|
|
67
|
+
* retained assistant turn to be sent back on a later request carrying `tools`,
|
|
68
|
+
* including turns where the model made no tool call — omitting it is a 400.
|
|
69
|
+
* So when `replayReasoning` is set (the caller passed a `thinking` block), each
|
|
70
|
+
* assistant turn carries `ChatMessage.reasoning` verbatim. That reasoning is
|
|
71
|
+
* never synthesized: a turn with no recorded reasoning sends `''`, which is
|
|
72
|
+
* the only representation of "this turn ran without thinking" the wire has.
|
|
73
|
+
* A non-thinking request keeps the historical shape (no `reasoning_content`);
|
|
74
|
+
* the guide says a non-thinking request ignores the field anyway.
|
|
75
|
+
*
|
|
65
76
|
* @param messages - The chat messages to format.
|
|
66
77
|
* @param system - Optional system prompt to prepend.
|
|
78
|
+
* @param replayReasoning - Whether thinking mode is on for this request.
|
|
67
79
|
* @returns The formatted messages array for the DeepSeek API.
|
|
68
80
|
*/
|
|
69
81
|
private formatMessages;
|
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;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;
|
|
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;AAwGhD,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;IAsRzD;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,cAAc;IA8FtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IAuDlC;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CA0LzB;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
|
@@ -17,9 +17,11 @@ const logger = getLogger();
|
|
|
17
17
|
// (not through the package barrel).
|
|
18
18
|
import './secrets.js';
|
|
19
19
|
/**
|
|
20
|
-
* Map thinking budget tokens to a DeepSeek reasoning_effort level
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Map thinking budget tokens to a DeepSeek reasoning_effort level, used ONLY as
|
|
21
|
+
* a fallback when the caller supplies a budget without a resolved native level.
|
|
22
|
+
* The native API accepts `low`/`high`/`max`; a caller that resolved one of them
|
|
23
|
+
* (the catalog's `supportedEffortLevels`) sends it verbatim and never reaches
|
|
24
|
+
* this function, so the fallback stays the two coarse buckets.
|
|
23
25
|
*
|
|
24
26
|
* @param budgetTokens - Requested thinking budget in tokens.
|
|
25
27
|
* @returns `'high'` or `'max'` for the upstream effort hint.
|
|
@@ -149,7 +151,7 @@ class DeepseekAIProviderImpl {
|
|
|
149
151
|
const canonicalModel = params.model ?? this.defaultModel;
|
|
150
152
|
const model = this.modelMap[canonicalModel] ?? canonicalModel;
|
|
151
153
|
const maxTokens = params.maxTokens ?? this.maxTokens;
|
|
152
|
-
const messages = this.formatMessages(params.messages, params.system);
|
|
154
|
+
const messages = this.formatMessages(params.messages, params.system, Boolean(params.thinking));
|
|
153
155
|
// Stop sequences, capped at the API's own limit. Verified honoured by the
|
|
154
156
|
// native endpoint on 2026-10-17: with `stop: ['<analysis>']` a reply that
|
|
155
157
|
// would have opened with that tag streamed nothing at all, and one that
|
|
@@ -188,10 +190,11 @@ class DeepseekAIProviderImpl {
|
|
|
188
190
|
// weaker nudge still produces a turn, and the agent loop has its own
|
|
189
191
|
// prose-only backstops, whereas a 400 produces nothing at all.
|
|
190
192
|
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// is
|
|
193
|
+
// LIVE for an entry that exposes opt-in reasoning (deepseek-flash, whose
|
|
194
|
+
// effort levels include the local `off` default): a project that picks
|
|
195
|
+
// low/high/max gets thinking mode on a discovery turn, and the forced
|
|
196
|
+
// nudge is degraded here instead of 400ing the first turn of the
|
|
197
|
+
// conversation.
|
|
195
198
|
const forcedChoice = params.toolChoice === 'required' ||
|
|
196
199
|
(typeof params.toolChoice === 'object' && params.toolChoice?.type === 'tool');
|
|
197
200
|
if (forcedChoice && params.thinking) {
|
|
@@ -206,15 +209,19 @@ class DeepseekAIProviderImpl {
|
|
|
206
209
|
body.tool_choice = { type: 'function', function: { name: params.toolChoice.name } };
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
|
-
// DeepSeek V4 defaults `thinking` to ENABLED.
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
212
|
+
// DeepSeek V4 defaults `thinking` to ENABLED. Per DeepSeek's thinking-mode
|
|
213
|
+
// guide (https://api-docs.deepseek.com/guides/thinking_mode/), a
|
|
214
|
+
// thinking-mode request that carries tools must send every retained
|
|
215
|
+
// assistant turn's `reasoning_content` back — including a turn where the
|
|
216
|
+
// model made no tool call — or the API answers 400. `formatMessages` replays
|
|
217
|
+
// `ChatMessage.reasoning` for exactly that reason. The catalog decides
|
|
218
|
+
// whether a turn carries a thinking block: deepseek-flash defaults to the
|
|
219
|
+
// local `off` level (no block → reasoning explicitly disabled below), and
|
|
220
|
+
// only a project that opts into low/high/max sends one.
|
|
214
221
|
if (params.thinking) {
|
|
215
222
|
body.thinking = { type: 'enabled' };
|
|
216
|
-
// Prefer a caller-resolved native value (
|
|
217
|
-
// catalog; fall back to the budget threshold.
|
|
223
|
+
// Prefer a caller-resolved native value (`low` | `high` | `max`) from the
|
|
224
|
+
// model catalog; fall back to the budget threshold.
|
|
218
225
|
body.reasoning_effort = params.thinking.effort ?? budgetToEffort(params.thinking.budgetTokens);
|
|
219
226
|
}
|
|
220
227
|
else {
|
|
@@ -298,18 +305,44 @@ class DeepseekAIProviderImpl {
|
|
|
298
305
|
detail = errorBody;
|
|
299
306
|
}
|
|
300
307
|
logger.error('DeepSeek API error', { status: response.status, detail, errorBody });
|
|
308
|
+
// The status ladder decides what the CALLER can do, so every branch
|
|
309
|
+
// must stay distinguishable downstream: the turn classifier keys on
|
|
310
|
+
// these sentences, and an exhausted account must never read as
|
|
311
|
+
// "try again" (2026-10-20: a −$0.09 DeepSeek account answered 402 on
|
|
312
|
+
// every request and each one surfaced as the generic error — sixteen
|
|
313
|
+
// benchmark runs died at their first request, indistinguishable from
|
|
314
|
+
// an outage and each given a retry that could never succeed).
|
|
301
315
|
const clientMessage = response.status === 429
|
|
302
316
|
? 'AI rate limit exceeded. Please try again shortly.'
|
|
303
317
|
: response.status === 401
|
|
304
|
-
?
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
318
|
+
? // The status rides in the text: the turn classifier is
|
|
319
|
+
// text-only, and an auth failure must classify `auth`
|
|
320
|
+
// (never retried) — not the unknown bucket's pity retry.
|
|
321
|
+
'AI service configuration error (HTTP 401) — the API key was rejected.'
|
|
322
|
+
: response.status === 402 ||
|
|
323
|
+
/insufficient balance|credit balance|payment required/i.test(detail)
|
|
324
|
+
? // An exhausted account is a platform billing condition, not a
|
|
325
|
+
// request problem and not a transient outage — no retry can
|
|
326
|
+
// restore a negative balance.
|
|
327
|
+
'AI service is unavailable — provider credit balance too low.'
|
|
328
|
+
: response.status === 400 &&
|
|
329
|
+
/prompt is too long|too many tokens|token.*limit|context.*length/i.test(detail)
|
|
330
|
+
? "Conversation too long for the model's context window. Use /compact to free space, or start a new conversation."
|
|
331
|
+
: response.status === 400
|
|
332
|
+
? 'AI request was invalid — check the model and request parameters.'
|
|
333
|
+
: response.status === 500 ||
|
|
334
|
+
response.status === 502 ||
|
|
335
|
+
response.status === 503 ||
|
|
336
|
+
response.status === 504 ||
|
|
337
|
+
response.status === 529
|
|
338
|
+
? // The outage statuses the turn classifier reads as transient —
|
|
339
|
+
// one shared sentence, so a provider shed rides the retry
|
|
340
|
+
// ladder instead of killing the turn (was: 503 only).
|
|
341
|
+
'AI service is temporarily overloaded. Please try again in a moment.'
|
|
342
|
+
: // Anything else still names its status, so a never-before-seen
|
|
343
|
+
// code is classifiable (and 5xx-shaped ones ride `http 5xx`
|
|
344
|
+
// into the transient class) instead of a bare "try again".
|
|
345
|
+
`AI service error (HTTP ${response.status}). Please try again.`;
|
|
313
346
|
yield { type: 'error', message: clientMessage, errorKey: 'ai.error.apiError' };
|
|
314
347
|
return;
|
|
315
348
|
}
|
|
@@ -492,11 +525,23 @@ class DeepseekAIProviderImpl {
|
|
|
492
525
|
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
493
526
|
* System prompt is prepended as a system role message.
|
|
494
527
|
*
|
|
528
|
+
* Thinking mode has one wire obligation that non-thinking mode does not:
|
|
529
|
+
* DeepSeek's thinking-mode guide requires the `reasoning_content` of every
|
|
530
|
+
* retained assistant turn to be sent back on a later request carrying `tools`,
|
|
531
|
+
* including turns where the model made no tool call — omitting it is a 400.
|
|
532
|
+
* So when `replayReasoning` is set (the caller passed a `thinking` block), each
|
|
533
|
+
* assistant turn carries `ChatMessage.reasoning` verbatim. That reasoning is
|
|
534
|
+
* never synthesized: a turn with no recorded reasoning sends `''`, which is
|
|
535
|
+
* the only representation of "this turn ran without thinking" the wire has.
|
|
536
|
+
* A non-thinking request keeps the historical shape (no `reasoning_content`);
|
|
537
|
+
* the guide says a non-thinking request ignores the field anyway.
|
|
538
|
+
*
|
|
495
539
|
* @param messages - The chat messages to format.
|
|
496
540
|
* @param system - Optional system prompt to prepend.
|
|
541
|
+
* @param replayReasoning - Whether thinking mode is on for this request.
|
|
497
542
|
* @returns The formatted messages array for the DeepSeek API.
|
|
498
543
|
*/
|
|
499
|
-
formatMessages(messages, system) {
|
|
544
|
+
formatMessages(messages, system, replayReasoning = false) {
|
|
500
545
|
const formatted = [];
|
|
501
546
|
if (system) {
|
|
502
547
|
formatted.push({ role: 'system', content: system });
|
|
@@ -504,8 +549,14 @@ class DeepseekAIProviderImpl {
|
|
|
504
549
|
for (const m of messages) {
|
|
505
550
|
if (m.role === 'system')
|
|
506
551
|
continue; // handled above
|
|
552
|
+
// Only assistant turns have a reasoning channel. `reasoning_content: ''`
|
|
553
|
+
// is deliberate for a retained turn with no recorded reasoning (a
|
|
554
|
+
// non-thinking turn inside a thinking-mode history): the vendor requires
|
|
555
|
+
// the field on every retained assistant turn, and an empty string is the
|
|
556
|
+
// only way to say "nothing was recorded" without inventing content.
|
|
557
|
+
const reasoningField = replayReasoning && m.role === 'assistant' ? { reasoning_content: m.reasoning ?? '' } : {};
|
|
507
558
|
if (typeof m.content === 'string') {
|
|
508
|
-
formatted.push({ role: m.role, content: m.content });
|
|
559
|
+
formatted.push({ role: m.role, content: m.content, ...reasoningField });
|
|
509
560
|
continue;
|
|
510
561
|
}
|
|
511
562
|
// Content blocks — convert to OpenAI format
|
|
@@ -552,13 +603,14 @@ class DeepseekAIProviderImpl {
|
|
|
552
603
|
// screenshot tool) must not be dropped: Chat Completions accepts a user
|
|
553
604
|
// message after the tool messages, so emit them instead of losing them.
|
|
554
605
|
if (parts.length > 0)
|
|
555
|
-
formatted.push({ role: m.role, content: parts });
|
|
606
|
+
formatted.push({ role: m.role, content: parts, ...reasoningField });
|
|
556
607
|
}
|
|
557
608
|
else if (toolCalls.length > 0) {
|
|
558
609
|
// Assistant message with tool calls
|
|
559
610
|
formatted.push({
|
|
560
611
|
role: 'assistant',
|
|
561
612
|
content: parts.length > 0 ? parts : null,
|
|
613
|
+
...reasoningField,
|
|
562
614
|
tool_calls: toolCalls,
|
|
563
615
|
});
|
|
564
616
|
}
|
|
@@ -566,6 +618,7 @@ class DeepseekAIProviderImpl {
|
|
|
566
618
|
formatted.push({
|
|
567
619
|
role: m.role,
|
|
568
620
|
content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts,
|
|
621
|
+
...reasoningField,
|
|
569
622
|
});
|
|
570
623
|
}
|
|
571
624
|
}
|
|
@@ -821,11 +874,24 @@ class DeepseekAIProviderImpl {
|
|
|
821
874
|
code: streamError.code,
|
|
822
875
|
message: streamError.message,
|
|
823
876
|
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
877
|
+
// Same distinctions as the pre-flight ladder, in the same precedence
|
|
878
|
+
// the turn classifier applies (rate → auth → context → balance →
|
|
879
|
+
// transient): an exhausted account is a billing state (never
|
|
880
|
+
// transient, never "try again"), an auth failure stays `auth`, a
|
|
881
|
+
// context overflow stays recoverable. There is no HTTP status here —
|
|
882
|
+
// the stream answered 200 — so each class keys on the provider's own
|
|
883
|
+
// wording, plus numeric codes some hosts send as `code`.
|
|
884
|
+
const clientMessage = /rate.?limit|\b429\b|quota/i.test(detail)
|
|
885
|
+
? 'AI rate limit exceeded. Please try again shortly.'
|
|
886
|
+
: /\b401\b|authentication|unauthorized/i.test(detail)
|
|
887
|
+
? 'AI service authentication/configuration error — the API key was rejected.'
|
|
888
|
+
: /context.?length|context.?window|too long|too many tokens|token.?limit/i.test(detail)
|
|
889
|
+
? "Conversation too long for the model's context window. Use /compact to free space, or start a new conversation."
|
|
890
|
+
: /insufficient balance|credit balance|payment required|\b402\b/i.test(detail)
|
|
891
|
+
? 'AI service is unavailable — provider credit balance too low.'
|
|
892
|
+
: /overload|capacity|\b(?:500|502|503|504|529)\b/i.test(detail)
|
|
893
|
+
? 'AI service is temporarily overloaded. Please try again in a moment.'
|
|
894
|
+
: 'AI service error. Please try again.';
|
|
829
895
|
yield { type: 'error', message: clientMessage, errorKey: 'ai.error.apiError' };
|
|
830
896
|
continue;
|
|
831
897
|
}
|
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.12",
|
|
4
4
|
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@molecule/api-bond": "1.0.2",
|
|
38
38
|
"@types/node": "26.1.2",
|
|
39
39
|
"typescript": "6.0.3",
|
|
40
|
-
"vitest": "4.1.
|
|
40
|
+
"vitest": "4.1.12"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@molecule/api-ai": "^1.0.1",
|