@molecule/api-ai-deepseek 1.0.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/LICENSE +115 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/provider.d.ts +90 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +590 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +24 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the
|
|
45
|
+
original version of the Work and any modifications or additions
|
|
46
|
+
to that Work, that is intentionally submitted to the Licensor for
|
|
47
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
48
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
49
|
+
|
|
50
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
51
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
52
|
+
subsequently incorporated within the Work.
|
|
53
|
+
|
|
54
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
55
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
56
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
57
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
58
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
59
|
+
Work and such Derivative Works in Source or Object form.
|
|
60
|
+
|
|
61
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
62
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
63
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
64
|
+
patent license to make, have made, use, offer to sell, sell, import,
|
|
65
|
+
and otherwise transfer the Work.
|
|
66
|
+
|
|
67
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
68
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
69
|
+
modifications, and in Source or Object form, provided that You
|
|
70
|
+
meet the following conditions:
|
|
71
|
+
|
|
72
|
+
(a) You must give any other recipients of the Work or
|
|
73
|
+
Derivative Works a copy of this License; and
|
|
74
|
+
|
|
75
|
+
(b) You must cause any modified files to carry prominent notices
|
|
76
|
+
stating that You changed the files; and
|
|
77
|
+
|
|
78
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
79
|
+
that You distribute, all copyright, patent, trademark, and
|
|
80
|
+
attribution notices from the Source form of the Work,
|
|
81
|
+
excluding those notices that do not pertain to any part of
|
|
82
|
+
the Derivative Works; and
|
|
83
|
+
|
|
84
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
85
|
+
distribution, then any Derivative Works that You distribute must
|
|
86
|
+
include a readable copy of the attribution notices contained
|
|
87
|
+
within such NOTICE file.
|
|
88
|
+
|
|
89
|
+
5. Submission of Contributions.
|
|
90
|
+
|
|
91
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
92
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
93
|
+
|
|
94
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
95
|
+
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
96
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
97
|
+
|
|
98
|
+
8. Limitation of Liability. In no event and under no legal theory shall
|
|
99
|
+
any Contributor be liable to You for damages.
|
|
100
|
+
|
|
101
|
+
9. Accepting Warranty or Additional Liability.
|
|
102
|
+
|
|
103
|
+
Copyright 2026 Molecule Dev, Inc.
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
106
|
+
you may not use this file except in compliance with the License.
|
|
107
|
+
You may obtain a copy of the License at
|
|
108
|
+
|
|
109
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
110
|
+
|
|
111
|
+
Unless required by applicable law or agreed to in writing, software
|
|
112
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
113
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
See the License for the specific language governing permissions and
|
|
115
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser guard — `@molecule/api-ai-deepseek` is SERVER-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
|
|
5
|
+
* Evaluating a server package in a browser bundle is always an import-graph mistake
|
|
6
|
+
* (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
|
|
7
|
+
* ("Buffer is not defined") far from the culprit. Throwing here names the package and
|
|
8
|
+
* the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
|
|
9
|
+
* unaffected: the throw requires browser globals AND the absence of a node runtime.
|
|
10
|
+
*/
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
|
|
13
|
+
throw new Error('@molecule/api-ai-deepseek is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
|
|
14
|
+
'from server code (a server route/function or your API), or dynamic-import it inside ' +
|
|
15
|
+
'the server handler — never from components or shared client modules, and never ' +
|
|
16
|
+
'polyfill Buffer/process to silence this.');
|
|
17
|
+
}
|
|
18
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek AI provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Config: `DEEPSEEK_API_KEY` (SERVER-side only) plus an optional default model id/base URL.
|
|
6
|
+
*
|
|
7
|
+
* **Missing `DEEPSEEK_API_KEY` fails fast**: the provider throws naming the exact env var on
|
|
8
|
+
* first use (the exported `provider` is a lazy proxy, so this fires on the first `chat()` call,
|
|
9
|
+
* not at bond/module-load time) — it never silently sends an empty key.
|
|
10
|
+
*
|
|
11
|
+
* **Error message disambiguation**: a plain 400 that ISN'T a context-length error (bad param,
|
|
12
|
+
* malformed tool schema) gets its own non-retryable message distinct from the generic
|
|
13
|
+
* "AI service error. Please try again." used for retryable failures.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
export * from './browser-guard.js';
|
|
18
|
+
export * from './provider.js';
|
|
19
|
+
export * from './secrets.js';
|
|
20
|
+
export * from './types.js';
|
|
21
|
+
import type { AIProvider } from '@molecule/api-ai';
|
|
22
|
+
/**
|
|
23
|
+
* The provider implementation.
|
|
24
|
+
*/
|
|
25
|
+
export declare const provider: AIProvider;
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAE1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAMlD;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UAUrB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek AI provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Config: `DEEPSEEK_API_KEY` (SERVER-side only) plus an optional default model id/base URL.
|
|
6
|
+
*
|
|
7
|
+
* **Missing `DEEPSEEK_API_KEY` fails fast**: the provider throws naming the exact env var on
|
|
8
|
+
* first use (the exported `provider` is a lazy proxy, so this fires on the first `chat()` call,
|
|
9
|
+
* not at bond/module-load time) — it never silently sends an empty key.
|
|
10
|
+
*
|
|
11
|
+
* **Error message disambiguation**: a plain 400 that ISN'T a context-length error (bad param,
|
|
12
|
+
* malformed tool schema) gets its own non-retryable message distinct from the generic
|
|
13
|
+
* "AI service error. Please try again." used for retryable failures.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
export * from './browser-guard.js';
|
|
18
|
+
export * from './provider.js';
|
|
19
|
+
export * from './secrets.js';
|
|
20
|
+
export * from './types.js';
|
|
21
|
+
import { createProvider } from './provider.js';
|
|
22
|
+
/** Lazily-initialized provider singleton. Defers creation until first use so that env vars / secrets are resolved. */
|
|
23
|
+
let _provider = null;
|
|
24
|
+
/**
|
|
25
|
+
* The provider implementation.
|
|
26
|
+
*/
|
|
27
|
+
export const provider = new Proxy({}, {
|
|
28
|
+
get(_, prop, receiver) {
|
|
29
|
+
if (!_provider)
|
|
30
|
+
_provider = createProvider();
|
|
31
|
+
return Reflect.get(_provider, prop, receiver);
|
|
32
|
+
},
|
|
33
|
+
// set trap: methods run with `this` bound to the proxy — without it, instance-state writes land on the dummy target and are lost (see api-push-notifications-web-push)
|
|
34
|
+
set(_, prop, value) {
|
|
35
|
+
if (!_provider)
|
|
36
|
+
_provider = createProvider();
|
|
37
|
+
return Reflect.set(_provider, prop, value);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek implementation of AIProvider.
|
|
3
|
+
*
|
|
4
|
+
* Uses the DeepSeek API (OpenAI-compatible) for streaming chat completions with
|
|
5
|
+
* tool use and reasoning. DeepSeek's Chat Completions endpoint mirrors OpenAI's
|
|
6
|
+
* wire format (tool_calls, reasoning_content, prompt_tokens_details.cached_tokens),
|
|
7
|
+
* so this provider shares its shape with the xAI provider.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { AIProvider, ChatEvent, ChatParams } from '@molecule/api-ai';
|
|
12
|
+
import './secrets.js';
|
|
13
|
+
import type { DeepseekConfig } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* DeepSeek AI provider that implements the `AIProvider` interface using
|
|
16
|
+
* DeepSeek's OpenAI-compatible Chat Completions API with support for streaming,
|
|
17
|
+
* tool use, and reasoning.
|
|
18
|
+
*/
|
|
19
|
+
declare class DeepseekAIProviderImpl implements AIProvider {
|
|
20
|
+
readonly name = "deepseek";
|
|
21
|
+
private apiKey;
|
|
22
|
+
private defaultModel;
|
|
23
|
+
private maxTokens;
|
|
24
|
+
private baseUrl;
|
|
25
|
+
private completionsPath;
|
|
26
|
+
private modelMap;
|
|
27
|
+
private onRateLimit?;
|
|
28
|
+
constructor(config?: DeepseekConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Sends a chat request and yields streamed events (text chunks, tool use, done/error).
|
|
31
|
+
*
|
|
32
|
+
* @param params - Chat parameters including messages, model, system prompt, and tools.
|
|
33
|
+
* @yields {ChatEvent} Chat events including text chunks, tool use requests, done signals, and errors.
|
|
34
|
+
*/
|
|
35
|
+
chat(params: ChatParams): AsyncIterable<ChatEvent>;
|
|
36
|
+
/**
|
|
37
|
+
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
38
|
+
* System prompt is prepended as a system role message.
|
|
39
|
+
*
|
|
40
|
+
* @param messages - The chat messages to format.
|
|
41
|
+
* @param system - Optional system prompt to prepend.
|
|
42
|
+
* @returns The formatted messages array for the DeepSeek API.
|
|
43
|
+
*/
|
|
44
|
+
private formatMessages;
|
|
45
|
+
/**
|
|
46
|
+
* Converts internal `AITool` definitions to the OpenAI function calling format.
|
|
47
|
+
*
|
|
48
|
+
* @param tools - The AI tool definitions to format.
|
|
49
|
+
* @returns The formatted tools array for the DeepSeek API.
|
|
50
|
+
*/
|
|
51
|
+
private formatTools;
|
|
52
|
+
/**
|
|
53
|
+
* Parses a non-streaming OpenAI-compatible response into `ChatEvent` objects.
|
|
54
|
+
*
|
|
55
|
+
* @param data - The raw API response data to parse.
|
|
56
|
+
* @yields {ChatEvent} Chat events parsed from the response.
|
|
57
|
+
*/
|
|
58
|
+
private parseNonStreamingResponse;
|
|
59
|
+
/**
|
|
60
|
+
* Parses a streaming SSE response from the DeepSeek API (OpenAI-compatible
|
|
61
|
+
* format), yielding `ChatEvent` objects as they arrive.
|
|
62
|
+
*
|
|
63
|
+
* @param response - The fetch Response with an SSE body stream.
|
|
64
|
+
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
65
|
+
*/
|
|
66
|
+
private parseStreamingResponse;
|
|
67
|
+
/**
|
|
68
|
+
* Processes an array of SSE lines from the DeepSeek streaming response, yielding
|
|
69
|
+
* ChatEvent objects for text, thinking, tool use, and tool completion.
|
|
70
|
+
*
|
|
71
|
+
* @param lines - Raw SSE lines to process.
|
|
72
|
+
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
73
|
+
* @param state - Mutable token usage counters.
|
|
74
|
+
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
75
|
+
*/
|
|
76
|
+
private processSSELines;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Back-compat alias for the provider class (the scaffold exported this name).
|
|
80
|
+
*/
|
|
81
|
+
export declare const DeepseekAIProvider: typeof DeepseekAIProviderImpl;
|
|
82
|
+
/**
|
|
83
|
+
* Creates a DeepSeek AI provider instance.
|
|
84
|
+
*
|
|
85
|
+
* @param config - DeepSeek-specific configuration (API key, model, max tokens, base URL).
|
|
86
|
+
* @returns An `AIProvider` backed by the DeepSeek Chat Completions API.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createProvider(config?: DeepseekConfig): AIProvider;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,UAAU,EAGV,SAAS,EAET,UAAU,EAEX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAuBhD;;;;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;gBAE7B,MAAM,GAAE,cAAmB;IAqBvC;;;;;OAKG;IACI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC;IAoKzD;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IA4EtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IAuDlC;;;;;;OAMG;YACY,sBAAsB;IAoFrC;;;;;;;;OAQG;IACH,OAAO,CAAE,eAAe;CAkIzB;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
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek implementation of AIProvider.
|
|
3
|
+
*
|
|
4
|
+
* Uses the DeepSeek API (OpenAI-compatible) for streaming chat completions with
|
|
5
|
+
* tool use and reasoning. DeepSeek's Chat Completions endpoint mirrors OpenAI's
|
|
6
|
+
* wire format (tool_calls, reasoning_content, prompt_tokens_details.cached_tokens),
|
|
7
|
+
* so this provider shares its shape with the xAI provider.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { getLogger } from '@molecule/api-bond';
|
|
12
|
+
import { t } from '@molecule/api-i18n';
|
|
13
|
+
const logger = getLogger();
|
|
14
|
+
// Side-effect import: registers this bond's secret definitions so the
|
|
15
|
+
// runtime registry is populated even when provider.js is imported directly
|
|
16
|
+
// (not through the package barrel).
|
|
17
|
+
import './secrets.js';
|
|
18
|
+
/**
|
|
19
|
+
* Map thinking budget tokens to a DeepSeek reasoning_effort level. DeepSeek V4
|
|
20
|
+
* accepts only `'high'` or `'max'` for `reasoning_effort`, so larger budgets map
|
|
21
|
+
* to `'max'` and everything else to `'high'`.
|
|
22
|
+
*
|
|
23
|
+
* @param budgetTokens - Requested thinking budget in tokens.
|
|
24
|
+
* @returns `'high'` or `'max'` for the upstream effort hint.
|
|
25
|
+
*/
|
|
26
|
+
function budgetToEffort(budgetTokens) {
|
|
27
|
+
return budgetTokens >= 12_000 ? 'max' : 'high';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* DeepSeek AI provider that implements the `AIProvider` interface using
|
|
31
|
+
* DeepSeek's OpenAI-compatible Chat Completions API with support for streaming,
|
|
32
|
+
* tool use, and reasoning.
|
|
33
|
+
*/
|
|
34
|
+
class DeepseekAIProviderImpl {
|
|
35
|
+
name = 'deepseek';
|
|
36
|
+
apiKey;
|
|
37
|
+
defaultModel;
|
|
38
|
+
maxTokens;
|
|
39
|
+
baseUrl;
|
|
40
|
+
completionsPath;
|
|
41
|
+
modelMap;
|
|
42
|
+
onRateLimit;
|
|
43
|
+
constructor(config = {}) {
|
|
44
|
+
this.apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY ?? '';
|
|
45
|
+
this.defaultModel = config.defaultModel ?? 'deepseek-v4-flash';
|
|
46
|
+
this.maxTokens = config.maxTokens ?? 4096;
|
|
47
|
+
this.baseUrl = config.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com';
|
|
48
|
+
this.completionsPath =
|
|
49
|
+
config.completionsPath ?? process.env.DEEPSEEK_COMPLETIONS_PATH ?? '/v1/chat/completions';
|
|
50
|
+
this.modelMap = config.modelMap ?? {};
|
|
51
|
+
this.onRateLimit = config.onRateLimit;
|
|
52
|
+
// Fail fast with an actionable local error rather than a cryptic 401 on the
|
|
53
|
+
// first request. The default `provider` export constructs lazily on first
|
|
54
|
+
// use (see index.ts), so this surfaces the moment the provider is actually
|
|
55
|
+
// used, not at bond/module-load time.
|
|
56
|
+
if (!this.apiKey) {
|
|
57
|
+
throw new Error('DEEPSEEK_API_KEY is not set. Add it to your environment to use the DeepSeek AI provider.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Sends a chat request and yields streamed events (text chunks, tool use, done/error).
|
|
62
|
+
*
|
|
63
|
+
* @param params - Chat parameters including messages, model, system prompt, and tools.
|
|
64
|
+
* @yields {ChatEvent} Chat events including text chunks, tool use requests, done signals, and errors.
|
|
65
|
+
*/
|
|
66
|
+
async *chat(params) {
|
|
67
|
+
// Canonical catalog id (used everywhere else); translate to the upstream's
|
|
68
|
+
// id only for the outbound request body via `modelMap`.
|
|
69
|
+
const canonicalModel = params.model ?? this.defaultModel;
|
|
70
|
+
const model = this.modelMap[canonicalModel] ?? canonicalModel;
|
|
71
|
+
const maxTokens = params.maxTokens ?? this.maxTokens;
|
|
72
|
+
const messages = this.formatMessages(params.messages, params.system);
|
|
73
|
+
const body = {
|
|
74
|
+
// Abuse attribution (see ChatParams.endUserId): OpenAI-compatible APIs
|
|
75
|
+
// scope enforcement by `user`, so an abusive tenant can be actioned
|
|
76
|
+
// individually instead of the org key that serves everyone. The value is
|
|
77
|
+
// the caller's opaque hash — never derived or logged here.
|
|
78
|
+
...(params.endUserId ? { user: params.endUserId } : {}),
|
|
79
|
+
model,
|
|
80
|
+
// DeepSeek uses `max_tokens` (not OpenAI's newer `max_completion_tokens`,
|
|
81
|
+
// which it silently ignores).
|
|
82
|
+
max_tokens: maxTokens,
|
|
83
|
+
messages,
|
|
84
|
+
stream: params.stream !== false,
|
|
85
|
+
...(params.stream !== false ? { stream_options: { include_usage: true } } : {}),
|
|
86
|
+
};
|
|
87
|
+
// Tools: merge custom tools (function format) and server tools
|
|
88
|
+
const functions = params.tools?.length ? this.formatTools(params.tools) : [];
|
|
89
|
+
const serverTools = (params.serverTools ?? []).map((st) => ({ ...st }));
|
|
90
|
+
const allTools = [...functions, ...serverTools];
|
|
91
|
+
if (allTools.length > 0) {
|
|
92
|
+
body.tools = allTools;
|
|
93
|
+
if (params.toolChoice === 'required') {
|
|
94
|
+
body.tool_choice = 'required';
|
|
95
|
+
}
|
|
96
|
+
else if (typeof params.toolChoice === 'object' && params.toolChoice?.type === 'tool') {
|
|
97
|
+
body.tool_choice = { type: 'function', function: { name: params.toolChoice.name } };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// DeepSeek V4 defaults `thinking` to ENABLED. Thinking mode requires the
|
|
101
|
+
// assistant's reasoning_content to be echoed back on every subsequent turn
|
|
102
|
+
// (a 400 otherwise) and is awkward to combine with the tool-calling loop, so
|
|
103
|
+
// we run non-thinking unless a thinking budget is explicitly requested. The
|
|
104
|
+
// chat handler gates that on the model's `supportsThinking` flag.
|
|
105
|
+
if (params.thinking) {
|
|
106
|
+
body.thinking = { type: 'enabled' };
|
|
107
|
+
// Prefer a caller-resolved native value ('high' | 'max') from the model
|
|
108
|
+
// catalog; fall back to the budget threshold.
|
|
109
|
+
body.reasoning_effort = params.thinking.effort ?? budgetToEffort(params.thinking.budgetTokens);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
body.thinking = { type: 'disabled' };
|
|
113
|
+
if (params.temperature !== undefined) {
|
|
114
|
+
body.temperature = params.temperature;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const headers = {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
120
|
+
};
|
|
121
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
122
|
+
const signal = params.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
123
|
+
// Retry with exponential backoff for rate limits (429) and overloaded (503)
|
|
124
|
+
const MAX_RETRIES = 3;
|
|
125
|
+
let response = null;
|
|
126
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
127
|
+
response = await fetch(`${this.baseUrl}${this.completionsPath}`, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers,
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
signal,
|
|
132
|
+
});
|
|
133
|
+
if (response.status === 429 || response.status === 503) {
|
|
134
|
+
const willRetry = attempt < MAX_RETRIES;
|
|
135
|
+
// Retry-After is equally-valid as delta-seconds or an HTTP-date;
|
|
136
|
+
// parseInt on the date form yields NaN, degrading the backoff to a
|
|
137
|
+
// ~0ms retry against an already rate-limiting API. Guard it.
|
|
138
|
+
const retryAfter = response.headers.get('retry-after');
|
|
139
|
+
const parsedRetryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : NaN;
|
|
140
|
+
const hasRetryAfter = Number.isFinite(parsedRetryAfterSeconds) && parsedRetryAfterSeconds >= 0;
|
|
141
|
+
// Additive jitter (<500ms): concurrent callers rejected together must
|
|
142
|
+
// not sleep identical delays and re-arrive as the same burst.
|
|
143
|
+
const delayMs = willRetry
|
|
144
|
+
? (hasRetryAfter
|
|
145
|
+
? Math.min(parsedRetryAfterSeconds * 1000, 60_000)
|
|
146
|
+
: Math.min(1000 * 2 ** attempt, 30_000)) + Math.floor(Math.random() * 500)
|
|
147
|
+
: 0;
|
|
148
|
+
try {
|
|
149
|
+
this.onRateLimit?.({
|
|
150
|
+
provider: this.name,
|
|
151
|
+
model,
|
|
152
|
+
status: response.status,
|
|
153
|
+
attempt: attempt + 1,
|
|
154
|
+
willRetry,
|
|
155
|
+
retryInMs: delayMs,
|
|
156
|
+
...(hasRetryAfter ? { retryAfterSeconds: parsedRetryAfterSeconds } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
// Host-app telemetry; its failure must never break the request.
|
|
161
|
+
logger.warn('onRateLimit callback threw', { error });
|
|
162
|
+
}
|
|
163
|
+
if (willRetry) {
|
|
164
|
+
logger.warn('DeepSeek API rate limited, retrying', {
|
|
165
|
+
status: response.status,
|
|
166
|
+
attempt: attempt + 1,
|
|
167
|
+
delayMs,
|
|
168
|
+
});
|
|
169
|
+
await new Promise((resolve) => {
|
|
170
|
+
const timer = setTimeout(resolve, delayMs);
|
|
171
|
+
signal.addEventListener('abort', () => {
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
resolve();
|
|
174
|
+
}, { once: true });
|
|
175
|
+
});
|
|
176
|
+
if (signal.aborted)
|
|
177
|
+
break;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const errorBody = await response.text();
|
|
185
|
+
let detail = `HTTP ${response.status}`;
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(errorBody);
|
|
188
|
+
if (parsed.error?.message)
|
|
189
|
+
detail = parsed.error.message;
|
|
190
|
+
}
|
|
191
|
+
catch (_error) {
|
|
192
|
+
// JSON.parse failed — raw text is already in `errorBody`; use it as-is.
|
|
193
|
+
if (errorBody.length > 0 && errorBody.length < 200)
|
|
194
|
+
detail = errorBody;
|
|
195
|
+
}
|
|
196
|
+
logger.error('DeepSeek API error', { status: response.status, detail, errorBody });
|
|
197
|
+
const clientMessage = response.status === 429
|
|
198
|
+
? 'AI rate limit exceeded. Please try again shortly.'
|
|
199
|
+
: response.status === 401
|
|
200
|
+
? 'AI service configuration error.'
|
|
201
|
+
: response.status === 400 &&
|
|
202
|
+
/prompt is too long|too many tokens|token.*limit|context.*length/i.test(detail)
|
|
203
|
+
? "Conversation too long for the model's context window. Use /compact to free space, or start a new conversation."
|
|
204
|
+
: response.status === 400
|
|
205
|
+
? 'AI request was invalid — check the model and request parameters.'
|
|
206
|
+
: response.status === 503
|
|
207
|
+
? 'AI service is temporarily overloaded. Please try again in a moment.'
|
|
208
|
+
: 'AI service error. Please try again.';
|
|
209
|
+
yield { type: 'error', message: clientMessage, errorKey: 'ai.error.apiError' };
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (params.stream === false) {
|
|
213
|
+
const data = (await response.json());
|
|
214
|
+
yield* this.parseNonStreamingResponse(data);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
yield* this.parseStreamingResponse(response);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
221
|
+
* System prompt is prepended as a system role message.
|
|
222
|
+
*
|
|
223
|
+
* @param messages - The chat messages to format.
|
|
224
|
+
* @param system - Optional system prompt to prepend.
|
|
225
|
+
* @returns The formatted messages array for the DeepSeek API.
|
|
226
|
+
*/
|
|
227
|
+
formatMessages(messages, system) {
|
|
228
|
+
const formatted = [];
|
|
229
|
+
if (system) {
|
|
230
|
+
formatted.push({ role: 'system', content: system });
|
|
231
|
+
}
|
|
232
|
+
for (const m of messages) {
|
|
233
|
+
if (m.role === 'system')
|
|
234
|
+
continue; // handled above
|
|
235
|
+
if (typeof m.content === 'string') {
|
|
236
|
+
formatted.push({ role: m.role, content: m.content });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// Content blocks — convert to OpenAI format
|
|
240
|
+
const blocks = m.content;
|
|
241
|
+
const parts = [];
|
|
242
|
+
const toolCalls = [];
|
|
243
|
+
const toolResults = [];
|
|
244
|
+
for (const block of blocks) {
|
|
245
|
+
switch (block.type) {
|
|
246
|
+
case 'text':
|
|
247
|
+
parts.push({ type: 'text', text: block.text });
|
|
248
|
+
break;
|
|
249
|
+
case 'image':
|
|
250
|
+
parts.push({
|
|
251
|
+
type: 'image_url',
|
|
252
|
+
image_url: { url: `data:${block.mediaType};base64,${block.data}` },
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
case 'tool_use':
|
|
256
|
+
toolCalls.push({
|
|
257
|
+
id: block.id,
|
|
258
|
+
type: 'function',
|
|
259
|
+
function: { name: block.name, arguments: JSON.stringify(block.input) },
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
case 'tool_result':
|
|
263
|
+
toolResults.push({
|
|
264
|
+
role: 'tool',
|
|
265
|
+
tool_call_id: block.tool_use_id,
|
|
266
|
+
content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
269
|
+
default:
|
|
270
|
+
// document, audio, video — unsupported, skip
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (toolResults.length > 0) {
|
|
275
|
+
// Tool results are separate messages in OpenAI format
|
|
276
|
+
for (const tr of toolResults) {
|
|
277
|
+
formatted.push(tr);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (toolCalls.length > 0) {
|
|
281
|
+
// Assistant message with tool calls
|
|
282
|
+
formatted.push({
|
|
283
|
+
role: 'assistant',
|
|
284
|
+
content: parts.length > 0 ? parts : null,
|
|
285
|
+
tool_calls: toolCalls,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
formatted.push({
|
|
290
|
+
role: m.role,
|
|
291
|
+
content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return formatted;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Converts internal `AITool` definitions to the OpenAI function calling format.
|
|
299
|
+
*
|
|
300
|
+
* @param tools - The AI tool definitions to format.
|
|
301
|
+
* @returns The formatted tools array for the DeepSeek API.
|
|
302
|
+
*/
|
|
303
|
+
formatTools(tools) {
|
|
304
|
+
return tools.map((tool) => ({
|
|
305
|
+
type: 'function',
|
|
306
|
+
function: {
|
|
307
|
+
name: tool.name,
|
|
308
|
+
description: tool.description,
|
|
309
|
+
parameters: tool.parameters,
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Parses a non-streaming OpenAI-compatible response into `ChatEvent` objects.
|
|
315
|
+
*
|
|
316
|
+
* @param data - The raw API response data to parse.
|
|
317
|
+
* @yields {ChatEvent} Chat events parsed from the response.
|
|
318
|
+
*/
|
|
319
|
+
*parseNonStreamingResponse(data) {
|
|
320
|
+
const choices = data.choices;
|
|
321
|
+
if (choices?.length) {
|
|
322
|
+
const message = choices[0].message;
|
|
323
|
+
if (message) {
|
|
324
|
+
// Reasoning/thinking content
|
|
325
|
+
if (message.reasoning_content && typeof message.reasoning_content === 'string') {
|
|
326
|
+
yield { type: 'thinking', content: message.reasoning_content };
|
|
327
|
+
}
|
|
328
|
+
if (message.content && typeof message.content === 'string') {
|
|
329
|
+
yield { type: 'text', content: message.content };
|
|
330
|
+
}
|
|
331
|
+
const toolCalls = message.tool_calls;
|
|
332
|
+
if (toolCalls) {
|
|
333
|
+
for (const tc of toolCalls) {
|
|
334
|
+
const fn = tc.function;
|
|
335
|
+
let input = {};
|
|
336
|
+
try {
|
|
337
|
+
input = JSON.parse(fn.arguments);
|
|
338
|
+
}
|
|
339
|
+
catch (_error) {
|
|
340
|
+
// JSON.parse failed — fall back to empty object; malformed args are non-fatal.
|
|
341
|
+
}
|
|
342
|
+
yield {
|
|
343
|
+
type: 'tool_use',
|
|
344
|
+
id: tc.id,
|
|
345
|
+
name: fn.name,
|
|
346
|
+
input,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const usage = data.usage;
|
|
353
|
+
const cached = usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
354
|
+
yield {
|
|
355
|
+
type: 'done',
|
|
356
|
+
usage: {
|
|
357
|
+
// Report uncached input + cached separately (Anthropic semantics) so the
|
|
358
|
+
// consumer's context/cost math doesn't double-count the cached subset.
|
|
359
|
+
inputTokens: Math.max(0, (usage?.prompt_tokens ?? 0) - cached),
|
|
360
|
+
outputTokens: usage?.completion_tokens ?? 0,
|
|
361
|
+
cacheReadInputTokens: cached,
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Parses a streaming SSE response from the DeepSeek API (OpenAI-compatible
|
|
367
|
+
* format), yielding `ChatEvent` objects as they arrive.
|
|
368
|
+
*
|
|
369
|
+
* @param response - The fetch Response with an SSE body stream.
|
|
370
|
+
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
371
|
+
*/
|
|
372
|
+
async *parseStreamingResponse(response) {
|
|
373
|
+
const reader = response.body?.getReader();
|
|
374
|
+
if (!reader) {
|
|
375
|
+
yield {
|
|
376
|
+
type: 'error',
|
|
377
|
+
message: t('ai.error.noResponseBody'),
|
|
378
|
+
errorKey: 'ai.error.noResponseBody',
|
|
379
|
+
};
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const decoder = new TextDecoder();
|
|
383
|
+
let buffer = '';
|
|
384
|
+
const state = { inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
385
|
+
// Track pending tool calls (accumulated across deltas). `started` guards the
|
|
386
|
+
// one-shot tool_use_start emission once the id + name are first known.
|
|
387
|
+
const pendingTools = new Map();
|
|
388
|
+
try {
|
|
389
|
+
while (true) {
|
|
390
|
+
const { done, value } = await reader.read();
|
|
391
|
+
if (done)
|
|
392
|
+
break;
|
|
393
|
+
buffer += decoder.decode(value, { stream: true });
|
|
394
|
+
const lines = buffer.split('\n');
|
|
395
|
+
buffer = lines.pop() ?? '';
|
|
396
|
+
let yieldedContent = false;
|
|
397
|
+
for (const event of this.processSSELines(lines, pendingTools, state)) {
|
|
398
|
+
yieldedContent = true;
|
|
399
|
+
yield event;
|
|
400
|
+
// A mid-stream error (rate-limit/overload during high load, sent as a
|
|
401
|
+
// `data: {"error": {...}}` chunk) means the turn was truncated. Stop
|
|
402
|
+
// here and do NOT fall through to the trailing `done`, which would
|
|
403
|
+
// report the truncated turn as a successful completion and skip the
|
|
404
|
+
// consumer's overload/retry recovery.
|
|
405
|
+
if (event.type === 'error')
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Signal liveness when the API sends data but no content events were produced
|
|
409
|
+
// (e.g., during extended reasoning with empty chunks or keepalive comments).
|
|
410
|
+
// This resets the consumer's stream timeout without forwarding to the client.
|
|
411
|
+
if (!yieldedContent) {
|
|
412
|
+
yield { type: 'keep_alive' };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Flush any remaining data in the buffer after EOF
|
|
416
|
+
if (buffer.trim()) {
|
|
417
|
+
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
|
|
418
|
+
yield event;
|
|
419
|
+
if (event.type === 'error')
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Emit any tool calls still pending after stream ends (safety net for missing finish_reason)
|
|
424
|
+
if (pendingTools.size > 0) {
|
|
425
|
+
for (const [, pending] of pendingTools) {
|
|
426
|
+
let input = {};
|
|
427
|
+
try {
|
|
428
|
+
input = JSON.parse(pending.args);
|
|
429
|
+
}
|
|
430
|
+
catch (_error) {
|
|
431
|
+
// JSON.parse failed — fall back to empty object; malformed args are non-fatal.
|
|
432
|
+
}
|
|
433
|
+
yield { type: 'tool_use', id: pending.id, name: pending.name, input };
|
|
434
|
+
}
|
|
435
|
+
pendingTools.clear();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
reader.releaseLock();
|
|
440
|
+
}
|
|
441
|
+
yield {
|
|
442
|
+
type: 'done',
|
|
443
|
+
usage: {
|
|
444
|
+
inputTokens: Math.max(0, state.inputTokens - state.cachedTokens),
|
|
445
|
+
outputTokens: state.outputTokens,
|
|
446
|
+
cacheReadInputTokens: state.cachedTokens,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Processes an array of SSE lines from the DeepSeek streaming response, yielding
|
|
452
|
+
* ChatEvent objects for text, thinking, tool use, and tool completion.
|
|
453
|
+
*
|
|
454
|
+
* @param lines - Raw SSE lines to process.
|
|
455
|
+
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
456
|
+
* @param state - Mutable token usage counters.
|
|
457
|
+
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
458
|
+
*/
|
|
459
|
+
*processSSELines(lines, pendingTools, state) {
|
|
460
|
+
for (const line of lines) {
|
|
461
|
+
if (!line.startsWith('data: '))
|
|
462
|
+
continue;
|
|
463
|
+
const json = line.slice(6).trim();
|
|
464
|
+
if (json === '[DONE]')
|
|
465
|
+
continue;
|
|
466
|
+
try {
|
|
467
|
+
const event = JSON.parse(json);
|
|
468
|
+
// A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
|
|
469
|
+
// `choices`) — most importantly rate-limit/overload during high load.
|
|
470
|
+
// Without this it matches no branch below and is silently dropped, so the
|
|
471
|
+
// stream falls through to a misleading `done` and the truncated turn reads
|
|
472
|
+
// as a successful completion. Surface it as a real error event (also
|
|
473
|
+
// satisfies the no-silent-swallow rule).
|
|
474
|
+
const streamError = event.error;
|
|
475
|
+
if (streamError) {
|
|
476
|
+
const detail = `${streamError.code ?? streamError.type ?? ''} ${streamError.message ?? ''}`;
|
|
477
|
+
logger.error('DeepSeek streaming error event', {
|
|
478
|
+
type: streamError.type,
|
|
479
|
+
code: streamError.code,
|
|
480
|
+
message: streamError.message,
|
|
481
|
+
});
|
|
482
|
+
const clientMessage = /overload|capacity|503|529/i.test(detail)
|
|
483
|
+
? 'AI service is temporarily overloaded. Please try again in a moment.'
|
|
484
|
+
: /rate.?limit|429|quota/i.test(detail)
|
|
485
|
+
? 'AI rate limit exceeded. Please try again shortly.'
|
|
486
|
+
: 'AI service error. Please try again.';
|
|
487
|
+
yield { type: 'error', message: clientMessage, errorKey: 'ai.error.apiError' };
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
// Usage info (may appear in the final chunk)
|
|
491
|
+
const usage = event.usage;
|
|
492
|
+
if (usage) {
|
|
493
|
+
if (usage.prompt_tokens)
|
|
494
|
+
state.inputTokens = usage.prompt_tokens;
|
|
495
|
+
if (usage.completion_tokens)
|
|
496
|
+
state.outputTokens = usage.completion_tokens;
|
|
497
|
+
if (usage.prompt_tokens_details?.cached_tokens)
|
|
498
|
+
state.cachedTokens = usage.prompt_tokens_details.cached_tokens;
|
|
499
|
+
}
|
|
500
|
+
const choices = event.choices;
|
|
501
|
+
if (!choices?.length)
|
|
502
|
+
continue;
|
|
503
|
+
const choice = choices[0];
|
|
504
|
+
const delta = choice.delta;
|
|
505
|
+
if (!delta)
|
|
506
|
+
continue;
|
|
507
|
+
// Reasoning/thinking content
|
|
508
|
+
if (delta.reasoning_content && typeof delta.reasoning_content === 'string') {
|
|
509
|
+
yield { type: 'thinking', content: delta.reasoning_content };
|
|
510
|
+
}
|
|
511
|
+
// Text content
|
|
512
|
+
if (delta.content && typeof delta.content === 'string') {
|
|
513
|
+
yield { type: 'text', content: delta.content };
|
|
514
|
+
}
|
|
515
|
+
// Tool calls (accumulated across deltas)
|
|
516
|
+
const toolCalls = delta.tool_calls;
|
|
517
|
+
if (toolCalls) {
|
|
518
|
+
for (const tc of toolCalls) {
|
|
519
|
+
const idx = tc.index;
|
|
520
|
+
const fn = tc.function;
|
|
521
|
+
if (!pendingTools.has(idx)) {
|
|
522
|
+
pendingTools.set(idx, {
|
|
523
|
+
id: tc.id ?? '',
|
|
524
|
+
name: fn?.name ? fn.name : '',
|
|
525
|
+
args: '',
|
|
526
|
+
started: false,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
const pending = pendingTools.get(idx);
|
|
530
|
+
if (tc.id)
|
|
531
|
+
pending.id = tc.id;
|
|
532
|
+
if (fn?.name)
|
|
533
|
+
pending.name = fn.name;
|
|
534
|
+
// Surface the tool start once id + name are known, before the
|
|
535
|
+
// arguments finish streaming, so the client shows activity immediately.
|
|
536
|
+
if (!pending.started && pending.id && pending.name) {
|
|
537
|
+
pending.started = true;
|
|
538
|
+
yield { type: 'tool_use_start', id: pending.id, name: pending.name };
|
|
539
|
+
}
|
|
540
|
+
if (fn?.arguments) {
|
|
541
|
+
const chunk = fn.arguments;
|
|
542
|
+
pending.args += chunk;
|
|
543
|
+
// Emit the chunk's char count as real progress (drives the live
|
|
544
|
+
// token counter; re-arms the stream timeout) rather than a silent
|
|
545
|
+
// keep_alive — just the count, not the content.
|
|
546
|
+
yield { type: 'tool_input_delta', id: pending.id, chars: chunk.length, text: chunk };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// finish_reason signals completion
|
|
551
|
+
const finishReason = choice.finish_reason;
|
|
552
|
+
if (finishReason === 'tool_calls' || finishReason === 'stop') {
|
|
553
|
+
// Emit any accumulated tool calls
|
|
554
|
+
for (const [, pending] of pendingTools) {
|
|
555
|
+
let input = {};
|
|
556
|
+
try {
|
|
557
|
+
input = JSON.parse(pending.args);
|
|
558
|
+
}
|
|
559
|
+
catch (_error) {
|
|
560
|
+
// JSON.parse failed — fall back to empty object; malformed args are non-fatal.
|
|
561
|
+
}
|
|
562
|
+
yield {
|
|
563
|
+
type: 'tool_use',
|
|
564
|
+
id: pending.id,
|
|
565
|
+
name: pending.name,
|
|
566
|
+
input,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
pendingTools.clear();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
logger.debug('Skipping malformed SSE JSON line', { json, error });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Back-compat alias for the provider class (the scaffold exported this name).
|
|
580
|
+
*/
|
|
581
|
+
export const DeepseekAIProvider = DeepseekAIProviderImpl;
|
|
582
|
+
/**
|
|
583
|
+
* Creates a DeepSeek AI provider instance.
|
|
584
|
+
*
|
|
585
|
+
* @param config - DeepSeek-specific configuration (API key, model, max tokens, base URL).
|
|
586
|
+
* @returns An `AIProvider` backed by the DeepSeek Chat Completions API.
|
|
587
|
+
*/
|
|
588
|
+
export function createProvider(config) {
|
|
589
|
+
return new DeepseekAIProviderImpl(config);
|
|
590
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { SecretDefinition } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the DeepSeek AI bond. */
|
|
15
|
+
export declare const aiDeepseekSecretDefinitions: SecretDefinition[];
|
|
16
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,2DAA2D;AAC3D,eAAO,MAAM,2BAA2B,EAAE,gBAAgB,EAQzD,CAAA"}
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { registerSecrets } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the DeepSeek AI bond. */
|
|
15
|
+
export const aiDeepseekSecretDefinitions = [
|
|
16
|
+
{
|
|
17
|
+
key: 'DEEPSEEK_API_KEY',
|
|
18
|
+
description: 'DeepSeek API key — Create an API key on the DeepSeek open platform.',
|
|
19
|
+
helpUrl: 'https://platform.deepseek.com/api_keys',
|
|
20
|
+
required: true,
|
|
21
|
+
example: 'sk-...',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
registerSecrets(aiDeepseekSecretDefinitions);
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek AI provider configuration.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { AiRateLimitCallback } from '@molecule/api-ai';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for DeepSeek.
|
|
9
|
+
*/
|
|
10
|
+
export interface DeepseekConfig {
|
|
11
|
+
/** API key. Defaults to DEEPSEEK_API_KEY env var. */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Default model. Defaults to 'deepseek-v4-flash'. */
|
|
14
|
+
defaultModel?: string;
|
|
15
|
+
/** Maximum tokens for completions. */
|
|
16
|
+
maxTokens?: number;
|
|
17
|
+
/** Base URL override (for proxies). Defaults to 'https://api.deepseek.com'. */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Chat-completions path appended to {@link baseUrl}. Defaults to
|
|
21
|
+
* `/v1/chat/completions` (DeepSeek-direct puts the version in the path). Set
|
|
22
|
+
* this when the same open-weight model is served by a US OpenAI-compatible
|
|
23
|
+
* host whose version prefix lives in the base URL instead — e.g. DeepInfra:
|
|
24
|
+
* `baseUrl='https://api.deepinfra.com/v1/openai'` + `completionsPath='/chat/completions'`.
|
|
25
|
+
*/
|
|
26
|
+
completionsPath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional catalog-id → upstream-model-id map, applied to the outbound request
|
|
29
|
+
* ONLY. Lets a US OpenAI-compatible host (DeepInfra) receive its namespaced id
|
|
30
|
+
* (`deepseek-ai/DeepSeek-V4-Flash`) while the rest of the platform — pricing,
|
|
31
|
+
* cost ceilings, display — keeps using the canonical catalog id
|
|
32
|
+
* (`deepseek-v4-flash`). An id not in the map passes through unchanged.
|
|
33
|
+
*/
|
|
34
|
+
modelMap?: Record<string, string>;
|
|
35
|
+
/** Called on each rate-limited/overloaded upstream response, before any retry sleep. */
|
|
36
|
+
onRateLimit?: AiRateLimitCallback;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Process Env interface.
|
|
40
|
+
*/
|
|
41
|
+
export interface ProcessEnv {
|
|
42
|
+
DEEPSEEK_API_KEY: string;
|
|
43
|
+
/** Base URL override (for credential brokers / gateways / US OpenAI-compatible hosts). */
|
|
44
|
+
DEEPSEEK_BASE_URL?: string;
|
|
45
|
+
/** Chat-completions path override (see {@link DeepseekConfig.completionsPath}). */
|
|
46
|
+
DEEPSEEK_COMPLETIONS_PATH?: string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACjC,wFAAwF;IACxF,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,gBAAgB,EAAE,MAAM,CAAA;IACxB,0FAA0F;IAC1F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,mFAAmF;IACnF,yBAAyB,CAAC,EAAE,MAAM,CAAA;CACnC"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@molecule/api-ai-deepseek",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"molecule",
|
|
24
|
+
"ai",
|
|
25
|
+
"deepseek"
|
|
26
|
+
],
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@molecule/api-ai": "1.0.0",
|
|
30
|
+
"@molecule/api-bond": "1.0.0",
|
|
31
|
+
"@types/node": "26.1.2",
|
|
32
|
+
"typescript": "6.0.3",
|
|
33
|
+
"vitest": "4.1.10"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@molecule/api-ai": "^1.0.0",
|
|
37
|
+
"@molecule/api-bond": "^1.0.0",
|
|
38
|
+
"@molecule/api-i18n": "^1.0.0",
|
|
39
|
+
"@molecule/api-secrets": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@molecule/api-ai": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|