@namzu/deepseek 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # @namzu/deepseek
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7050dd4: Add `@namzu/deepseek`, and stop dropping reasoning when a stream is collected.
8
+
9
+ **A new driver, and a separate package on purpose.** DeepSeek's endpoint is OpenAI's Chat Completions shape, so pointing `@namzu/openai` at it with a `baseURL` looks like it should work. It does not, and the reason is thinking mode: it is **on by default**, the chain of thought comes back in a `reasoning_content` field that wire has no concept of, and the vendor requires that field replayed on every later turn once tool calls are in play. A driver that does not know about it drops the model's reasoning on every call.
10
+
11
+ `@namzu/deepseek` maps `ThinkingConfig` one-to-one onto the vendor's own `adaptive | enabled | disabled`, streams reasoning through `delta.reasoning` — the same channel `@namzu/anthropic` uses, so a host that renders one renders the other — and replays it automatically. Callers pass the assistant message back and the field goes with it.
12
+
13
+ It **refuses** two things the vendor accepts and applies to nothing: `effort` (this wire validates `thinking.type` and ignores any effort beside it) and the sampling parameters while thinking is on. Both were measured against the live API rather than read off the documentation. `samplingInThinkingMode: 'ignore'` opts out of the second.
14
+
15
+ It carries no price rows, deliberately: the vendor charges twice as much during peak UTC hours, and a static table has no hour in it.
16
+
17
+ **`collectChatCompletion` dropped reasoning blocks** (`@namzu/sdk`). `delta.reasoning` existed, `AssistantMessage.reasoning` is documented as replayed verbatim, and the run loop assembled it correctly — but this helper, which every non-streaming caller goes through, threw it away. So the same stream produced a message with reasoning through one route and without it through the other, and a vendor that needs the blocks back was sent a message that had lost them. It now buckets them by index exactly as the run loop does. This affects `@namzu/anthropic` users too.
18
+
19
+ **The CLI ships the driver** (`@namzu/cli`), so `namzu --provider deepseek` works on a fresh install with `DEEPSEEK_API_KEY` set. That is a fifth bundled driver and a slightly larger install.
20
+
21
+ Models are `deepseek-v4-flash` and `deepseek-v4-pro`. `deepseek-chat` and `deepseek-reasoner` were discontinued on 2026-07-24 and resolve to nothing.
package/LICENSE.md ADDED
@@ -0,0 +1,110 @@
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-MIT
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Cogitave
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the MIT license that is effective on the second anniversary of the date we make
91
+ the Software available. On or after that date, you may use the Software under
92
+ the MIT license, in which case the following will apply:
93
+
94
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
95
+ this software and associated documentation files (the "Software"), to deal in
96
+ the Software without restriction, including without limitation the rights to
97
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
98
+ of the Software, and to permit persons to whom the Software is furnished to do
99
+ so, subject to the following conditions:
100
+
101
+ The above copyright notice and this permission notice shall be included in all
102
+ copies or substantial portions of the Software.
103
+
104
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
105
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
106
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
107
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
108
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
109
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ <!-- okf
2
+ type: Reference
3
+ title: "@namzu/deepseek"
4
+ description: >-
5
+ The DeepSeek model driver for the Namzu agent kernel. Chat Completions with
6
+ streaming and tool use, and thinking mode mapped onto the kernel's reasoning
7
+ blocks in both directions.
8
+ tags: [readme, package, provider, deepseek]
9
+ timestamp: 2026-08-17T00:00:00Z
10
+ status: active
11
+ diataxis: reference
12
+ -->
13
+
14
+ <div align="center">
15
+
16
+ <h1>@namzu/deepseek</h1>
17
+
18
+ **The DeepSeek model driver for [Namzu](https://github.com/cogitave/namzu).**
19
+
20
+ [![npm](https://img.shields.io/npm/v/@namzu/deepseek.svg)](https://www.npmjs.com/package/@namzu/deepseek)
21
+ [![build](https://github.com/cogitave/namzu/actions/workflows/ci.yml/badge.svg)](https://github.com/cogitave/namzu/actions/workflows/ci.yml)
22
+ [![license](https://img.shields.io/badge/license-FSL--1.1--MIT-blue.svg)](https://github.com/cogitave/namzu/blob/main/LICENSE.md)
23
+
24
+ [Install](#install) · [Usage](#usage) · [Thinking mode](#thinking-mode) · [Documentation](#documentation)
25
+
26
+ </div>
27
+
28
+ ---
29
+
30
+ Implements the kernel's `LLMProvider` over DeepSeek's Chat Completions
31
+ endpoint, which is OpenAI-shaped — and then diverges where it counts, which is
32
+ why this is its own package rather than a `baseURL` on `@namzu/openai`.
33
+ Installed only if you use it; the kernel has no preferred vendor.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pnpm add @namzu/sdk @namzu/deepseek
39
+ ```
40
+
41
+ `@namzu/sdk` is a peer dependency. Install both. Requires Node.js 20+.
42
+
43
+ ## Usage
44
+
45
+ ```ts
46
+ import { ProviderRegistry } from '@namzu/sdk'
47
+ import { registerDeepSeek } from '@namzu/deepseek'
48
+
49
+ registerDeepSeek() // once, at startup
50
+
51
+ const { provider } = ProviderRegistry.create({
52
+ type: 'deepseek',
53
+ apiKey: process.env.DEEPSEEK_API_KEY ?? '',
54
+ model: 'deepseek-v4-flash',
55
+ })
56
+
57
+ for await (const chunk of provider.chatStream({
58
+ model: 'deepseek-v4-flash',
59
+ messages: [{ role: 'user', content: 'Is 91 prime?' }],
60
+ })) {
61
+ if (chunk.delta.reasoning?.text) process.stderr.write(chunk.delta.reasoning.text)
62
+ if (chunk.delta.content) process.stdout.write(chunk.delta.content)
63
+ }
64
+ ```
65
+
66
+ Models are `deepseek-v4-flash` and `deepseek-v4-pro`. `deepseek-chat` and
67
+ `deepseek-reasoner` were discontinued on 2026-07-24 and are not aliases.
68
+
69
+ ## Thinking mode
70
+
71
+ **Thinking is on by default** — the vendor's default, not this driver's. The
72
+ chain of thought arrives as `delta.reasoning` fragments, the same channel
73
+ `@namzu/anthropic` uses, and is replayed back to the vendor automatically on
74
+ later turns, which its tool-calling flow requires.
75
+
76
+ Turn it off per call with `thinking: { type: 'disabled' }`.
77
+
78
+ Two parameters are **refused** rather than sent, because the vendor accepts
79
+ them and applies neither: `effort`, and the sampling parameters while thinking
80
+ is on. Both refusals, and how to opt out, are in the documentation below.
81
+
82
+ ## Documentation
83
+
84
+ - [The DeepSeek driver](https://github.com/cogitave/namzu/blob/main/docs/providers/deepseek.md) — thinking mode, reasoning replay, what this wire refuses, and why it carries no price rows
85
+ - [Namzu docs](https://github.com/cogitave/namzu/tree/main/docs)
86
+
87
+ ## License
88
+
89
+ FSL-1.1-MIT, converting to MIT two years after each release.
@@ -0,0 +1,68 @@
1
+ import type { ChatCompletionParams, LLMProvider, ModelInfo, ProviderCapabilities, StreamChunk, ThinkingConfig } from '@namzu/sdk';
2
+ import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions';
3
+ import type { DeepSeekConfig } from './types.js';
4
+ /**
5
+ * No vision, no documents.
6
+ *
7
+ * The wire is OpenAI's Chat Completions, which has `image_url` content parts —
8
+ * but the models behind this endpoint are text-only, and a capability set is a
9
+ * claim about what a call will actually do, not about what the request format
10
+ * can express. Claiming vision here would route an image-bearing run to a
11
+ * driver that sends the image and gets prose about nothing.
12
+ */
13
+ export declare const DEEPSEEK_CAPABILITIES: ProviderCapabilities;
14
+ /**
15
+ * Is thinking on for this call?
16
+ *
17
+ * **Absent means ON.** That is the vendor's default, not a guess: a request
18
+ * with no `thinking` key comes back carrying `reasoning_content`. Every rule
19
+ * below that turns on "thinking is enabled" has to read it this way, or it
20
+ * applies to explicit callers only and misses the common case entirely.
21
+ */
22
+ export declare function thinkingEnabled(thinking: ThinkingConfig | undefined): boolean;
23
+ /**
24
+ * Refuse a sampling parameter that thinking mode will discard.
25
+ *
26
+ * Measured against the live API on 2026-08-17 rather than read off the
27
+ * documentation, which says only that these are "not supported": all four are
28
+ * accepted with HTTP 200 and applied to nothing. So a caller who pinned
29
+ * `temperature: 0` for reproducibility gets sampling, no error, and no way to
30
+ * find out — `refuse-do-not-degrade`, exactly.
31
+ *
32
+ * `samplingInThinkingMode: 'ignore'` opts out, for a host that keeps one
33
+ * parameter set across several providers and would rather this one drop them
34
+ * quietly than make it special.
35
+ */
36
+ export declare function assertSamplingUsable(params: ChatCompletionParams, mode: 'refuse' | 'ignore'): void;
37
+ /**
38
+ * Refuse a reasoning effort this wire cannot carry.
39
+ *
40
+ * `thinking.effort` is accepted here and validated against nothing — measured:
41
+ * `effort: 'bogus'` returns 200, and `effort: 'none'` still produces reasoning
42
+ * tokens. Only `thinking.type` is validated (the vendor's own error names its
43
+ * variants: `adaptive`, `enabled`, `disabled`). So an effort sent on this wire
44
+ * is a parameter accepted and discarded, which is the one thing a driver must
45
+ * not do quietly.
46
+ *
47
+ * The vendor's Anthropic-format endpoint does take `reasoning.effort`. A
48
+ * driver built on that wire could honour this; this one is built on Chat
49
+ * Completions and says so.
50
+ */
51
+ export declare function assertEffortUnsupported(params: ChatCompletionParams): void;
52
+ export declare function toDeepSeekMessages(messages: ChatCompletionParams['messages']): ChatCompletionMessageParam[];
53
+ export declare function toDeepSeekTools(params: ChatCompletionParams): ChatCompletionTool[] | undefined;
54
+ export declare class DeepSeekProvider implements LLMProvider {
55
+ readonly id = "deepseek";
56
+ readonly name = "DeepSeek";
57
+ readonly capabilities: ProviderCapabilities;
58
+ private client;
59
+ private defaultModel?;
60
+ private samplingMode;
61
+ constructor(config: DeepSeekConfig);
62
+ private resolveModel;
63
+ chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk>;
64
+ probeCredential(): Promise<void>;
65
+ listModels(): Promise<ModelInfo[]>;
66
+ healthCheck(): Promise<boolean>;
67
+ }
68
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EAEpB,WAAW,EACX,SAAS,EACT,oBAAoB,EAEpB,WAAW,EACX,cAAc,EAGd,MAAM,YAAY,CAAA;AAUnB,OAAO,KAAK,EACX,0BAA0B,EAC1B,kBAAkB,EAElB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,oBAMnC,CAAA;AAuFD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,QAAQ,GAAG,QAAQ,GACvB,IAAI,CAQN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAK1E;AA0BD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,oBAAoB,CAAC,UAAU,CAAC,GACxC,0BAA0B,EAAE,CAoC9B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,oBAAoB,GAAG,kBAAkB,EAAE,GAAG,SAAS,CAe9F;AAED,qBAAa,gBAAiB,YAAW,WAAW;IACnD,QAAQ,CAAC,EAAE,cAAa;IACxB,QAAQ,CAAC,IAAI,cAAa;IAC1B,QAAQ,CAAC,YAAY,uBAAwB;IAE7C,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,YAAY,CAAqB;gBAE7B,MAAM,EAAE,cAAc;IAgBlC,OAAO,CAAC,YAAY;IAUb,UAAU,CAAC,MAAM,EAAE,oBAAoB,GAAG,aAAa,CAAC,WAAW,CAAC;IAiJrE,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhC,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAYlC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAQrC"}
package/dist/client.js ADDED
@@ -0,0 +1,380 @@
1
+ import { ProviderRequestError, attributionHeaders, isCallerAbortError, isProviderRequestError, providerVendorError, toolResultToText, } from '@namzu/sdk';
2
+ import OpenAI from 'openai';
3
+ /**
4
+ * No vision, no documents.
5
+ *
6
+ * The wire is OpenAI's Chat Completions, which has `image_url` content parts —
7
+ * but the models behind this endpoint are text-only, and a capability set is a
8
+ * claim about what a call will actually do, not about what the request format
9
+ * can express. Claiming vision here would route an image-bearing run to a
10
+ * driver that sends the image and gets prose about nothing.
11
+ */
12
+ export const DEEPSEEK_CAPABILITIES = {
13
+ supportsTools: true,
14
+ supportsStreaming: true,
15
+ supportsFunctionCalling: true,
16
+ supportsVision: false,
17
+ supportsDocuments: false,
18
+ };
19
+ const DEFAULT_BASE_URL = 'https://api.deepseek.com';
20
+ function mapFinishReason(reason) {
21
+ switch (reason) {
22
+ case 'length':
23
+ return 'length';
24
+ case 'tool_calls':
25
+ return 'tool_calls';
26
+ case 'content_filter':
27
+ return 'content_filter';
28
+ default:
29
+ return 'stop';
30
+ }
31
+ }
32
+ function parseUsage(raw) {
33
+ if (!raw) {
34
+ return {
35
+ promptTokens: 0,
36
+ completionTokens: 0,
37
+ totalTokens: 0,
38
+ cachedTokens: 0,
39
+ cacheWriteTokens: 0,
40
+ };
41
+ }
42
+ const promptTokens = raw.prompt_tokens ?? 0;
43
+ const completionTokens = raw.completion_tokens ?? 0;
44
+ const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
45
+ return {
46
+ promptTokens,
47
+ completionTokens,
48
+ totalTokens: raw.total_tokens ?? promptTokens + completionTokens,
49
+ cachedTokens: raw.prompt_tokens_details?.cached_tokens ?? raw.prompt_cache_hit_tokens ?? 0,
50
+ // The vendor has no write-side cache charge: a cache entry is created as
51
+ // a side effect of a miss, at the ordinary input rate. Reporting a
52
+ // fabricated number here would be worse than the zero.
53
+ cacheWriteTokens: 0,
54
+ ...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
55
+ };
56
+ }
57
+ function formatToolChoice(tc) {
58
+ if (tc === undefined)
59
+ return undefined;
60
+ if (tc === 'auto' || tc === 'none' || tc === 'required')
61
+ return tc;
62
+ if (typeof tc === 'object' && tc.type === 'function') {
63
+ return { type: 'function', function: { name: tc.function.name } };
64
+ }
65
+ return undefined;
66
+ }
67
+ /** The four the vendor accepts in thinking mode and applies none of. */
68
+ const SAMPLING_FIELDS = ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty'];
69
+ /**
70
+ * Is thinking on for this call?
71
+ *
72
+ * **Absent means ON.** That is the vendor's default, not a guess: a request
73
+ * with no `thinking` key comes back carrying `reasoning_content`. Every rule
74
+ * below that turns on "thinking is enabled" has to read it this way, or it
75
+ * applies to explicit callers only and misses the common case entirely.
76
+ */
77
+ export function thinkingEnabled(thinking) {
78
+ return thinking?.type !== 'disabled';
79
+ }
80
+ /**
81
+ * Refuse a sampling parameter that thinking mode will discard.
82
+ *
83
+ * Measured against the live API on 2026-08-17 rather than read off the
84
+ * documentation, which says only that these are "not supported": all four are
85
+ * accepted with HTTP 200 and applied to nothing. So a caller who pinned
86
+ * `temperature: 0` for reproducibility gets sampling, no error, and no way to
87
+ * find out — `refuse-do-not-degrade`, exactly.
88
+ *
89
+ * `samplingInThinkingMode: 'ignore'` opts out, for a host that keeps one
90
+ * parameter set across several providers and would rather this one drop them
91
+ * quietly than make it special.
92
+ */
93
+ export function assertSamplingUsable(params, mode) {
94
+ if (mode === 'ignore')
95
+ return;
96
+ if (!thinkingEnabled(params.thinking))
97
+ return;
98
+ const set = SAMPLING_FIELDS.filter((f) => params[f] !== undefined);
99
+ if (set.length === 0)
100
+ return;
101
+ throw new Error(`DeepSeekProvider: ${set.join(', ')} ${set.length === 1 ? 'is' : 'are'} ignored in thinking mode, which is on for this call — the vendor accepts them and applies none of them, so the call would silently sample when you asked it not to. Disable thinking for this call (\`thinking: { type: 'disabled' }\`) to have them honoured, drop them, or set \`samplingInThinkingMode: 'ignore'\` on the provider to send them anyway.`);
102
+ }
103
+ /**
104
+ * Refuse a reasoning effort this wire cannot carry.
105
+ *
106
+ * `thinking.effort` is accepted here and validated against nothing — measured:
107
+ * `effort: 'bogus'` returns 200, and `effort: 'none'` still produces reasoning
108
+ * tokens. Only `thinking.type` is validated (the vendor's own error names its
109
+ * variants: `adaptive`, `enabled`, `disabled`). So an effort sent on this wire
110
+ * is a parameter accepted and discarded, which is the one thing a driver must
111
+ * not do quietly.
112
+ *
113
+ * The vendor's Anthropic-format endpoint does take `reasoning.effort`. A
114
+ * driver built on that wire could honour this; this one is built on Chat
115
+ * Completions and says so.
116
+ */
117
+ export function assertEffortUnsupported(params) {
118
+ if (params.effort === undefined)
119
+ return;
120
+ throw new Error(`DeepSeekProvider: \`effort\` is not carried by this endpoint. The vendor's Chat Completions wire validates \`thinking.type\` and ignores any effort sent beside it, so passing one through would change nothing and report success. Use \`thinking: { type: 'adaptive' | 'enabled' | 'disabled' }\` to steer this driver.`);
121
+ }
122
+ /**
123
+ * The reasoning blocks an assistant message carries, in wire form.
124
+ *
125
+ * The vendor's rule: with tool calls in play, an assistant turn's
126
+ * `reasoning_content` **must** be replayed in every later turn of the same
127
+ * flow; without tool calls it is ignored if sent. Replayed unconditionally
128
+ * here, which satisfies both halves and matches what `AssistantMessage.reasoning`
129
+ * already promises — "replayed verbatim and ahead of the text/tool blocks".
130
+ *
131
+ * Measured note, because it matters to anyone debugging this: as of
132
+ * 2026-08-17 omitting the replay does NOT produce the documented 400 on either
133
+ * `deepseek-v4-flash` or `deepseek-v4-pro`. The rule is still followed, because
134
+ * a contract the vendor states and does not currently enforce is a contract
135
+ * that can start being enforced in any release.
136
+ */
137
+ function replayReasoning(reasoning) {
138
+ if (!reasoning || reasoning.length === 0)
139
+ return undefined;
140
+ const text = reasoning
141
+ .filter((b) => b.type === 'thinking' && typeof b.text === 'string')
142
+ .map((b) => b.text)
143
+ .join('');
144
+ return text.length > 0 ? text : undefined;
145
+ }
146
+ export function toDeepSeekMessages(messages) {
147
+ return messages.map((msg) => {
148
+ if (msg.role === 'system')
149
+ return { role: 'system', content: msg.content };
150
+ if (msg.role === 'user') {
151
+ // No attachment branch, deliberately. `DEEPSEEK_CAPABILITIES` says
152
+ // this driver has neither vision nor documents, and a driver that
153
+ // quietly encoded an image the model cannot read would answer about
154
+ // a picture it never saw.
155
+ if (msg.attachments && msg.attachments.length > 0) {
156
+ throw new Error('DeepSeekProvider: this driver has no vision or document support, and a message carried attachments. Route multimodal turns to a provider whose capabilities include them.');
157
+ }
158
+ return { role: 'user', content: msg.content };
159
+ }
160
+ if (msg.role === 'tool') {
161
+ return {
162
+ role: 'tool',
163
+ content: toolResultToText(msg.content),
164
+ tool_call_id: msg.toolCallId,
165
+ };
166
+ }
167
+ const assistant = { role: 'assistant', content: msg.content };
168
+ if ('toolCalls' in msg && msg.toolCalls && msg.toolCalls.length > 0) {
169
+ ;
170
+ assistant.tool_calls = msg.toolCalls.map((tc) => ({
171
+ id: tc.id,
172
+ type: 'function',
173
+ function: { name: tc.function.name, arguments: tc.function.arguments },
174
+ }));
175
+ }
176
+ const replayed = replayReasoning(msg.reasoning);
177
+ if (replayed !== undefined) {
178
+ ;
179
+ assistant.reasoning_content = replayed;
180
+ }
181
+ return assistant;
182
+ });
183
+ }
184
+ export function toDeepSeekTools(params) {
185
+ if (!params.tools || params.tools.length === 0)
186
+ return undefined;
187
+ // No `strict` branch: `enforceToolInputSchema` names tools whose schema
188
+ // should be enforced rather than suggested, and this wire has no field for
189
+ // it. Setting `strict: true` here would be accepted and ignored — the same
190
+ // defect `assertEffortUnsupported` refuses above — so the flag is left to
191
+ // the kernel's own post-call Zod validation, which runs either way.
192
+ return params.tools.map((t) => ({
193
+ type: 'function',
194
+ function: {
195
+ name: t.function.name,
196
+ description: t.function.description ?? '',
197
+ parameters: (t.function.parameters ?? {}),
198
+ },
199
+ }));
200
+ }
201
+ export class DeepSeekProvider {
202
+ id = 'deepseek';
203
+ name = 'DeepSeek';
204
+ capabilities = DEEPSEEK_CAPABILITIES;
205
+ client;
206
+ defaultModel;
207
+ samplingMode;
208
+ constructor(config) {
209
+ if (!config.apiKey) {
210
+ throw new Error('DeepSeek API key is required. Set DEEPSEEK_API_KEY env variable.');
211
+ }
212
+ const clientOptions = {
213
+ apiKey: config.apiKey,
214
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
215
+ };
216
+ if (config.timeout !== undefined)
217
+ clientOptions.timeout = config.timeout;
218
+ clientOptions.defaultHeaders = { ...attributionHeaders(), ...(config.defaultHeaders ?? {}) };
219
+ this.client = new OpenAI(clientOptions);
220
+ this.defaultModel = config.model;
221
+ this.samplingMode = config.samplingInThinkingMode ?? 'refuse';
222
+ }
223
+ resolveModel(params) {
224
+ const model = params.model || this.defaultModel;
225
+ if (!model) {
226
+ throw new Error('DeepSeekProvider: model is required. Pass `model` in config or on the chat call.');
227
+ }
228
+ return model;
229
+ }
230
+ async *chatStream(params) {
231
+ const model = this.resolveModel(params);
232
+ assertEffortUnsupported(params);
233
+ assertSamplingUsable(params, this.samplingMode);
234
+ const sampling = thinkingEnabled(params.thinking)
235
+ ? {}
236
+ : {
237
+ temperature: params.temperature,
238
+ top_p: params.topP,
239
+ frequency_penalty: params.frequencyPenalty,
240
+ presence_penalty: params.presencePenalty,
241
+ };
242
+ // Built as its own object so `stream: true` keeps its literal type and the
243
+ // SDK's streaming overload is the one selected. Casting the whole
244
+ // argument — the obvious way to smuggle `thinking` past a type that does
245
+ // not know about it — widens `stream` to `boolean` and the call resolves
246
+ // to the non-streaming overload, whose result has no async iterator.
247
+ const body = {
248
+ model,
249
+ messages: toDeepSeekMessages(params.messages),
250
+ stream: true,
251
+ stream_options: { include_usage: true },
252
+ tools: toDeepSeekTools(params),
253
+ tool_choice: formatToolChoice(params.toolChoice),
254
+ max_tokens: params.maxTokens,
255
+ stop: params.stop,
256
+ response_format: params.responseFormat,
257
+ ...sampling,
258
+ };
259
+ // Sent only when the caller said something. Absent means the vendor's own
260
+ // default, which is thinking ON — see `thinkingEnabled`. The three
261
+ // variants are the vendor's, and they are the same three the SDK's
262
+ // `ThinkingConfig` declares, so this is a rename and not a mapping.
263
+ //
264
+ // Cast back to `typeof body` rather than to the SDK's parameter type: the
265
+ // extra key rides along at runtime, and the literal `stream: true`
266
+ // survives, which is what keeps the streaming overload selected.
267
+ const withThinking = (params.thinking ? { ...body, thinking: { type: params.thinking.type } } : body);
268
+ let stream;
269
+ try {
270
+ stream = await this.client.chat.completions.create(withThinking, {
271
+ signal: params.signal,
272
+ });
273
+ }
274
+ catch (err) {
275
+ if (isCallerAbortError(err, params.signal))
276
+ throw params.signal?.reason ?? err;
277
+ if (isProviderRequestError(err))
278
+ throw err;
279
+ throw providerVendorError({ providerId: 'deepseek', error: err });
280
+ }
281
+ // One reasoning block per turn on this wire: the vendor streams
282
+ // `reasoning_content` as a flat run of deltas with no index and no
283
+ // block boundaries, so index 0 is the whole of it rather than a guess.
284
+ let reasoningOpen = false;
285
+ try {
286
+ for await (const chunk of stream) {
287
+ params.signal?.throwIfAborted();
288
+ try {
289
+ const choice = chunk.choices[0];
290
+ const delta = choice?.delta;
291
+ const toolCalls = delta?.tool_calls?.map((tc) => ({
292
+ index: tc.index,
293
+ id: tc.id,
294
+ type: tc.type,
295
+ function: tc.function
296
+ ? { name: tc.function.name, arguments: tc.function.arguments }
297
+ : undefined,
298
+ }));
299
+ const reasoningText = delta?.reasoning_content;
300
+ const finishReason = choice?.finish_reason
301
+ ? mapFinishReason(choice.finish_reason)
302
+ : undefined;
303
+ const usage = chunk.usage ? parseUsage(chunk.usage) : undefined;
304
+ if (reasoningText) {
305
+ reasoningOpen = true;
306
+ yield {
307
+ id: chunk.id,
308
+ delta: { reasoning: { index: 0, type: 'thinking', text: reasoningText } },
309
+ };
310
+ }
311
+ // The block closes when content or a tool call starts, which is
312
+ // the only boundary this wire gives. Without it a consumer's
313
+ // reasoning pane never receives `done` and stays open for the
314
+ // life of the run.
315
+ const contentStarting = (delta?.content !== undefined && delta.content !== null) ||
316
+ (toolCalls !== undefined && toolCalls.length > 0);
317
+ if (reasoningOpen && contentStarting) {
318
+ reasoningOpen = false;
319
+ yield { id: chunk.id, delta: { reasoning: { index: 0, done: true } } };
320
+ }
321
+ const hasDelta = (delta?.content !== undefined && delta.content !== null) ||
322
+ (toolCalls !== undefined && toolCalls.length > 0);
323
+ if (!hasDelta && !finishReason && !usage)
324
+ continue;
325
+ yield {
326
+ id: chunk.id,
327
+ delta: { content: delta?.content ?? undefined, toolCalls },
328
+ finishReason,
329
+ usage,
330
+ };
331
+ }
332
+ catch (parseErr) {
333
+ if (isProviderRequestError(parseErr))
334
+ throw parseErr;
335
+ throw new ProviderRequestError({
336
+ kind: 'server',
337
+ providerId: 'deepseek',
338
+ detail: 'the provider stream returned malformed data',
339
+ });
340
+ }
341
+ }
342
+ // A turn that was all reasoning and no content — the model thought
343
+ // and then stopped — still has to close its block.
344
+ if (reasoningOpen) {
345
+ yield { id: 'deepseek-reasoning-close', delta: { reasoning: { index: 0, done: true } } };
346
+ }
347
+ }
348
+ catch (err) {
349
+ if (isCallerAbortError(err, params.signal))
350
+ throw params.signal?.reason ?? err;
351
+ if (isProviderRequestError(err))
352
+ throw err;
353
+ throw providerVendorError({ providerId: 'deepseek', error: err });
354
+ }
355
+ }
356
+ async probeCredential() {
357
+ await this.client.models.list();
358
+ }
359
+ async listModels() {
360
+ const page = await this.client.models.list();
361
+ return page.data.map((m) => ({
362
+ id: m.id,
363
+ name: m.id,
364
+ inputPrice: 0,
365
+ outputPrice: 0,
366
+ supportsToolUse: true,
367
+ supportsStreaming: true,
368
+ }));
369
+ }
370
+ async healthCheck() {
371
+ try {
372
+ await this.client.models.list();
373
+ return true;
374
+ }
375
+ catch {
376
+ return false;
377
+ }
378
+ }
379
+ }
380
+ //# sourceMappingURL=client.js.map