@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 +21 -0
- package/LICENSE.md +110 -0
- package/README.md +89 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +380 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
- package/src/client.ts +475 -0
- package/src/index.ts +35 -0
- package/src/types.ts +52 -0
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
|
+
[](https://www.npmjs.com/package/@namzu/deepseek)
|
|
21
|
+
[](https://github.com/cogitave/namzu/actions/workflows/ci.yml)
|
|
22
|
+
[](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.
|
package/dist/client.d.ts
ADDED
|
@@ -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
|