@molecule/api-ai-deepseek 1.0.11 → 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;IAiRzD;;;;;;;;;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;CA0LzB;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 {
@@ -518,11 +525,23 @@ class DeepseekAIProviderImpl {
518
525
  * Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
519
526
  * System prompt is prepended as a system role message.
520
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
+ *
521
539
  * @param messages - The chat messages to format.
522
540
  * @param system - Optional system prompt to prepend.
541
+ * @param replayReasoning - Whether thinking mode is on for this request.
523
542
  * @returns The formatted messages array for the DeepSeek API.
524
543
  */
525
- formatMessages(messages, system) {
544
+ formatMessages(messages, system, replayReasoning = false) {
526
545
  const formatted = [];
527
546
  if (system) {
528
547
  formatted.push({ role: 'system', content: system });
@@ -530,8 +549,14 @@ class DeepseekAIProviderImpl {
530
549
  for (const m of messages) {
531
550
  if (m.role === 'system')
532
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 ?? '' } : {};
533
558
  if (typeof m.content === 'string') {
534
- formatted.push({ role: m.role, content: m.content });
559
+ formatted.push({ role: m.role, content: m.content, ...reasoningField });
535
560
  continue;
536
561
  }
537
562
  // Content blocks — convert to OpenAI format
@@ -578,13 +603,14 @@ class DeepseekAIProviderImpl {
578
603
  // screenshot tool) must not be dropped: Chat Completions accepts a user
579
604
  // message after the tool messages, so emit them instead of losing them.
580
605
  if (parts.length > 0)
581
- formatted.push({ role: m.role, content: parts });
606
+ formatted.push({ role: m.role, content: parts, ...reasoningField });
582
607
  }
583
608
  else if (toolCalls.length > 0) {
584
609
  // Assistant message with tool calls
585
610
  formatted.push({
586
611
  role: 'assistant',
587
612
  content: parts.length > 0 ? parts : null,
613
+ ...reasoningField,
588
614
  tool_calls: toolCalls,
589
615
  });
590
616
  }
@@ -592,6 +618,7 @@ class DeepseekAIProviderImpl {
592
618
  formatted.push({
593
619
  role: m.role,
594
620
  content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts,
621
+ ...reasoningField,
595
622
  });
596
623
  }
597
624
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-deepseek",
3
- "version": "1.0.11",
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",