@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.
@@ -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;
@@ -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;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"}
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. DeepSeek V4
21
- * accepts only `'high'` or `'max'` for `reasoning_effort`, so larger budgets map
22
- * to `'max'` and everything else to `'high'`.
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
- // Latent rather than live at the time of writing — no DeepSeek model in
192
- // the catalog sets supportsThinking — but the model-health cron adds
193
- // models to that catalog unattended, and the one place forceToolUse fires
194
- // is discovery mode, i.e. the FIRST turn of every new conversation.
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. Thinking mode requires the
210
- // assistant's reasoning_content to be echoed back on every subsequent turn
211
- // (a 400 otherwise) and is awkward to combine with the tool-calling loop, so
212
- // we run non-thinking unless a thinking budget is explicitly requested. The
213
- // chat handler gates that on the model's `supportsThinking` flag.
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 ('high' | 'max') from the model
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
- ? 'AI service configuration error.'
305
- : response.status === 400 &&
306
- /prompt is too long|too many tokens|token.*limit|context.*length/i.test(detail)
307
- ? "Conversation too long for the model's context window. Use /compact to free space, or start a new conversation."
308
- : response.status === 400
309
- ? 'AI request was invalid — check the model and request parameters.'
310
- : response.status === 503
311
- ? 'AI service is temporarily overloaded. Please try again in a moment.'
312
- : 'AI service error. Please try again.';
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
- const clientMessage = /overload|capacity|503|529/i.test(detail)
825
- ? 'AI service is temporarily overloaded. Please try again in a moment.'
826
- : /rate.?limit|429|quota/i.test(detail)
827
- ? 'AI rate limit exceeded. Please try again shortly.'
828
- : 'AI service error. Please try again.';
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.10",
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.11"
40
+ "vitest": "4.1.12"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@molecule/api-ai": "^1.0.1",