@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 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -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 {};
@@ -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"}
@@ -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"}
@@ -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);
@@ -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
@@ -0,0 +1,6 @@
1
+ /**
2
+ * DeepSeek AI provider configuration.
3
+ *
4
+ * @module
5
+ */
6
+ export {};
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
+ }