adaptive-memory-multi-model-router 2.15.3 → 2.16.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README_ja.md +2 -2
  3. package/README_zh.md +1 -1
  4. package/apps/cost-calculator/README.md +72 -0
  5. package/apps/cost-calculator/calculator.css +280 -0
  6. package/apps/cost-calculator/calculator.js +150 -0
  7. package/apps/cost-calculator/index.html +321 -0
  8. package/apps/cost-calculator/package.json +13 -0
  9. package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
  10. package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
  11. package/articles/ANNOUNCEMENT_vercel.md +85 -0
  12. package/dist/providers/providerConfig.d.ts +5 -1
  13. package/dist/providers/providerConfig.js +1006 -1
  14. package/dist/providers/providerConfig.js.map +1 -1
  15. package/docs/llms.txt +3 -3
  16. package/package.json +2 -2
  17. package/packages/a3m-vercel-ai/README.md +161 -0
  18. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
  19. package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
  20. package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
  21. package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
  22. package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
  23. package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
  24. package/packages/a3m-vercel-ai/dist/index.js +79 -0
  25. package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
  26. package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
  27. package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
  28. package/packages/a3m-vercel-ai/dist/types.js +5 -0
  29. package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
  30. package/packages/a3m-vercel-ai/package-lock.json +969 -0
  31. package/packages/a3m-vercel-ai/package.json +46 -0
  32. package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
  33. package/packages/a3m-vercel-ai/src/index.ts +104 -0
  34. package/packages/a3m-vercel-ai/src/types.ts +116 -0
  35. package/packages/a3m-vercel-ai/tsconfig.json +20 -0
  36. package/src/providers/providerConfig.ts +1053 -1
  37. package/summary.txt +38 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "a3m-vercel-ai",
3
+ "version": "0.1.0",
4
+ "description": "A3M Router provider for Vercel AI SDK - intelligent cost-based routing with parallel execution",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "typecheck": "tsc --noEmit",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "dependencies": {},
25
+ "peerDependencies": {
26
+ "ai": "^3.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "ai": "^3.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "keywords": [
34
+ "vercel",
35
+ "ai",
36
+ "sdk",
37
+ "router",
38
+ "llm",
39
+ "cost-optimization",
40
+ "a3m"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Das-rebel/a3m-router"
45
+ }
46
+ }
@@ -0,0 +1,381 @@
1
+ /**
2
+ * A3M Router Language Model for Vercel AI SDK (v3)
3
+ *
4
+ * Implements the LanguageModelV1 interface from @ai-sdk/provider
5
+ */
6
+
7
+ import {
8
+ LanguageModelV1,
9
+ LanguageModelV1CallOptions,
10
+ LanguageModelV1CallWarning,
11
+ LanguageModelV1FinishReason,
12
+ LanguageModelV1ProviderMetadata,
13
+ LanguageModelV1StreamPart,
14
+ LanguageModelV1FunctionToolCall,
15
+ } from '@ai-sdk/provider';
16
+ import type { A3MRouterConfig } from './types';
17
+
18
+ /**
19
+ * Convert Vercel AI SDK prompt format to OpenAI format for A3M
20
+ */
21
+ function convertMessages(
22
+ prompt: LanguageModelV1CallOptions['prompt']
23
+ ): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> {
24
+ const result: Array<{ role: 'user' | 'assistant' | 'system'; content: string }> = [];
25
+
26
+ // Handle string prompts
27
+ if (typeof prompt === 'string') {
28
+ return [{ role: 'user', content: prompt }];
29
+ }
30
+
31
+ // messages is an array of prompt parts
32
+ for (const part of prompt) {
33
+ if (part.role === 'system') {
34
+ result.push({ role: 'system', content: part.content });
35
+ } else if (part.role === 'user') {
36
+ if (typeof part.content === 'string') {
37
+ result.push({ role: 'user', content: part.content });
38
+ } else if (Array.isArray(part.content)) {
39
+ const textContent = part.content
40
+ .filter((p): p is { type: 'text'; text: string } => p.type === 'text')
41
+ .map(p => p.text)
42
+ .join('\n');
43
+ result.push({ role: 'user', content: textContent });
44
+ }
45
+ } else if (part.role === 'assistant') {
46
+ if (typeof part.content === 'string') {
47
+ result.push({ role: 'assistant', content: part.content });
48
+ } else if (Array.isArray(part.content)) {
49
+ const textContent = part.content
50
+ .filter((p): p is { type: 'text'; text: string } => p.type === 'text')
51
+ .map(p => p.text)
52
+ .join('\n');
53
+ result.push({ role: 'assistant', content: textContent });
54
+ }
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * Create an A3M Router Language Model for Vercel AI SDK
63
+ */
64
+ export function createA3MLanguageModel(config: A3MRouterConfig = {}) {
65
+ const baseURL = config.baseURL || 'http://localhost:8787';
66
+ const apiKey = config.apiKey || 'not-needed';
67
+
68
+ const model: LanguageModelV1 = {
69
+ specificationVersion: 'v1',
70
+ provider: 'a3m',
71
+ modelId: 'auto',
72
+ defaultObjectGenerationMode: undefined,
73
+
74
+ async doGenerate(options: LanguageModelV1CallOptions): Promise<{
75
+ text?: string;
76
+ toolCalls?: LanguageModelV1FunctionToolCall[];
77
+ finishReason: LanguageModelV1FinishReason;
78
+ usage: {
79
+ promptTokens: number;
80
+ completionTokens: number;
81
+ totalTokens: number;
82
+ };
83
+ rawCall: {
84
+ rawPrompt: unknown;
85
+ rawSettings: Record<string, unknown>;
86
+ };
87
+ warnings?: LanguageModelV1CallWarning[];
88
+ providerMetadata?: LanguageModelV1ProviderMetadata;
89
+ }> {
90
+ const messages = convertMessages(options.prompt);
91
+
92
+ // Extract system prompt if present
93
+ const systemMessages = messages.filter(m => m.role === 'system');
94
+ const nonSystemMessages = messages.filter(m => m.role !== 'system');
95
+ const system = systemMessages.map(m => m.content).join('\n');
96
+
97
+ // Build the request body for A3M
98
+ const requestBody: Record<string, unknown> = {
99
+ model: 'auto',
100
+ messages: nonSystemMessages.length > 0 ? nonSystemMessages : [{ role: 'user', content: ' ' }],
101
+ };
102
+
103
+ if (system) {
104
+ requestBody.system = system;
105
+ }
106
+
107
+ if (options.temperature !== undefined) {
108
+ requestBody.temperature = options.temperature;
109
+ }
110
+
111
+ if (options.maxTokens !== undefined) {
112
+ requestBody.max_tokens = options.maxTokens;
113
+ }
114
+
115
+ if (options.topP !== undefined) {
116
+ requestBody.top_p = options.topP;
117
+ }
118
+
119
+ if (options.stopSequences !== undefined) {
120
+ requestBody.stop = options.stopSequences;
121
+ }
122
+
123
+ if (config.parallelEnsemble) {
124
+ requestBody.parallel = config.parallelCount || 3;
125
+ }
126
+
127
+ if (config.stealth) {
128
+ requestBody.stealth = true;
129
+ }
130
+
131
+ try {
132
+ // Call A3M Router
133
+ const response = await fetch(`${baseURL}/v1/chat/completions`, {
134
+ method: 'POST',
135
+ headers: {
136
+ 'Content-Type': 'application/json',
137
+ 'Authorization': `Bearer ${apiKey}`,
138
+ },
139
+ body: JSON.stringify(requestBody),
140
+ });
141
+
142
+ if (!response.ok) {
143
+ const error = await response.text();
144
+ throw new Error(`A3M Router error: ${response.status} ${error}`);
145
+ }
146
+
147
+ const data = await response.json() as {
148
+ choices: Array<{
149
+ message: {
150
+ content?: string;
151
+ tool_calls?: Array<{
152
+ id: string;
153
+ function: { name: string; arguments: string };
154
+ }>;
155
+ };
156
+ finish_reason: string;
157
+ }>;
158
+ usage?: {
159
+ prompt_tokens: number;
160
+ completion_tokens: number;
161
+ total_tokens: number;
162
+ };
163
+ _meta?: {
164
+ provider: string;
165
+ cost: number;
166
+ };
167
+ };
168
+
169
+ const choice = data.choices[0];
170
+
171
+ return {
172
+ text: choice.message.content || undefined,
173
+ toolCalls: choice.message.tool_calls?.map(tc => ({
174
+ toolCallType: 'function' as const,
175
+ toolCallId: tc.id,
176
+ toolName: tc.function.name,
177
+ args: tc.function.arguments,
178
+ })),
179
+ finishReason: (choice.finish_reason as LanguageModelV1FinishReason) || 'stop',
180
+ usage: {
181
+ promptTokens: data.usage?.prompt_tokens || 0,
182
+ completionTokens: data.usage?.completion_tokens || 0,
183
+ totalTokens: data.usage?.total_tokens || 0,
184
+ },
185
+ rawCall: {
186
+ rawPrompt: messages,
187
+ rawSettings: requestBody,
188
+ },
189
+ providerMetadata: data._meta ? {
190
+ a3m: {
191
+ provider: data._meta.provider,
192
+ cost: data._meta.cost,
193
+ },
194
+ } : undefined,
195
+ };
196
+ } catch (error) {
197
+ throw new Error(
198
+ `A3M Router generation failed: ${error instanceof Error ? error.message : String(error)}`
199
+ );
200
+ }
201
+ },
202
+
203
+ doStream(options: LanguageModelV1CallOptions) {
204
+ const messages = convertMessages(options.prompt);
205
+
206
+ // Extract system prompt if present
207
+ const systemMessages = messages.filter(m => m.role === 'system');
208
+ const nonSystemMessages = messages.filter(m => m.role !== 'system');
209
+ const system = systemMessages.map(m => m.content).join('\n');
210
+
211
+ const requestBody: Record<string, unknown> = {
212
+ model: 'auto',
213
+ messages: nonSystemMessages.length > 0 ? nonSystemMessages : [{ role: 'user', content: ' ' }],
214
+ stream: true,
215
+ };
216
+
217
+ if (system) {
218
+ requestBody.system = system;
219
+ }
220
+
221
+ if (options.temperature !== undefined) {
222
+ requestBody.temperature = options.temperature;
223
+ }
224
+
225
+ if (options.maxTokens !== undefined) {
226
+ requestBody.max_tokens = options.maxTokens;
227
+ }
228
+
229
+ if (config.parallelEnsemble) {
230
+ requestBody.parallel = config.parallelCount || 3;
231
+ }
232
+
233
+ if (config.stealth) {
234
+ requestBody.stealth = true;
235
+ }
236
+
237
+ let attemptCount = 0;
238
+ const maxAttempts = 3;
239
+
240
+ const doStream = async (): Promise<{
241
+ stream: ReadableStream<LanguageModelV1StreamPart>;
242
+ rawCall: { rawPrompt: unknown; rawSettings: Record<string, unknown> };
243
+ }> => {
244
+ attemptCount++;
245
+
246
+ try {
247
+ const response = await fetch(`${baseURL}/v1/chat/completions`, {
248
+ method: 'POST',
249
+ headers: {
250
+ 'Content-Type': 'application/json',
251
+ 'Authorization': `Bearer ${apiKey}`,
252
+ },
253
+ body: JSON.stringify(requestBody),
254
+ });
255
+
256
+ if (!response.ok) {
257
+ const error = await response.text();
258
+ throw new Error(`A3M Router streaming error: ${response.status} ${error}`);
259
+ }
260
+
261
+ if (!response.body) {
262
+ throw new Error('A3M Router: no response body');
263
+ }
264
+
265
+ const stream = new ReadableStream<LanguageModelV1StreamPart>({
266
+ async start(controller) {
267
+ const reader = response.body!.getReader();
268
+ const decoder = new TextDecoder();
269
+ let buffer = '';
270
+
271
+ try {
272
+ while (true) {
273
+ const { done, value } = await reader.read();
274
+
275
+ if (done) {
276
+ controller.close();
277
+ break;
278
+ }
279
+
280
+ buffer += decoder.decode(value, { stream: true });
281
+ const lines = buffer.split('\n');
282
+ buffer = lines.pop() || '';
283
+
284
+ for (const line of lines) {
285
+ if (line.startsWith('data: ')) {
286
+ const data = line.slice(6);
287
+
288
+ if (data === '[DONE]') {
289
+ controller.close();
290
+ return;
291
+ }
292
+
293
+ try {
294
+ const parsed = JSON.parse(data);
295
+
296
+ // Handle chat completion chunks
297
+ if (parsed.choices?.[0]?.delta?.content) {
298
+ const chunk = parsed.choices[0].delta.content;
299
+ controller.enqueue({
300
+ type: 'text-delta',
301
+ textDelta: chunk,
302
+ } as LanguageModelV1StreamPart);
303
+ }
304
+
305
+ // Handle tool call start
306
+ if (parsed.choices?.[0]?.delta?.tool_calls?.[0]) {
307
+ const toolCall = parsed.choices[0].delta.tool_calls[0];
308
+ if (toolCall.id) {
309
+ controller.enqueue({
310
+ type: 'tool-call',
311
+ toolCallType: 'function',
312
+ toolCallId: toolCall.id,
313
+ toolName: toolCall.function?.name || '',
314
+ args: toolCall.function?.arguments || '',
315
+ } as LanguageModelV1StreamPart);
316
+ }
317
+ }
318
+
319
+ // Handle tool call delta
320
+ if (parsed.choices?.[0]?.delta?.tool_calls?.[0]?.function?.arguments) {
321
+ const argsDelta = parsed.choices[0].delta.tool_calls[0].function.arguments;
322
+ if (argsDelta) {
323
+ controller.enqueue({
324
+ type: 'tool-call-delta',
325
+ toolCallType: 'function',
326
+ toolCallId: parsed.choices[0].delta.tool_calls[0].id,
327
+ toolName: parsed.choices[0].delta.tool_calls[0].function?.name || '',
328
+ argsTextDelta: argsDelta,
329
+ } as LanguageModelV1StreamPart);
330
+ }
331
+ }
332
+
333
+ // Handle finish
334
+ if (parsed.choices?.[0]?.finish_reason) {
335
+ controller.enqueue({
336
+ type: 'finish',
337
+ finishReason: parsed.choices[0].finish_reason as LanguageModelV1FinishReason,
338
+ usage: {
339
+ promptTokens: parsed.usage?.prompt_tokens || 0,
340
+ completionTokens: parsed.usage?.completion_tokens || 0,
341
+ },
342
+ } as LanguageModelV1StreamPart);
343
+ }
344
+ } catch {
345
+ // Skip malformed JSON
346
+ }
347
+ }
348
+ }
349
+ }
350
+ } catch (streamError) {
351
+ controller.error(streamError);
352
+ }
353
+ },
354
+ });
355
+
356
+ return {
357
+ stream,
358
+ rawCall: {
359
+ rawPrompt: messages,
360
+ rawSettings: requestBody,
361
+ },
362
+ };
363
+ } catch (error) {
364
+ if (attemptCount < maxAttempts) {
365
+ await new Promise(resolve => setTimeout(resolve, 1000 * attemptCount));
366
+ return doStream();
367
+ }
368
+ throw error;
369
+ }
370
+ };
371
+
372
+ return new Promise((resolve, reject) => {
373
+ doStream()
374
+ .then(resolve)
375
+ .catch(reject);
376
+ });
377
+ },
378
+ };
379
+
380
+ return model;
381
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @a3m/vercel-ai - A3M Router Provider for Vercel AI SDK
3
+ *
4
+ * Drop-in replacement that routes to the cheapest capable provider
5
+ * with parallel execution, caching, and automatic fallback.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createA3MProvider } from '@a3m/vercel-ai';
10
+ * import { generateText } from 'ai';
11
+ *
12
+ * const a3m = createA3MProvider();
13
+ *
14
+ * const { text } = await generateText({
15
+ * model: a3m('auto'),
16
+ * prompt: 'What is the capital of France?',
17
+ * });
18
+ * ```
19
+ */
20
+
21
+ export type { A3MRouterConfig, A3MRouteResponse, A3MStreamChunk } from './types';
22
+ export { createA3MLanguageModel } from './a3m-language-model';
23
+
24
+ import type { LanguageModelV1 } from '@ai-sdk/provider';
25
+ import type { A3MRouterConfig } from './types';
26
+ import { createA3MLanguageModel } from './a3m-language-model';
27
+
28
+ /**
29
+ * A3M Router provider for Vercel AI SDK
30
+ */
31
+ export interface A3MProvider {
32
+ name: 'a3m';
33
+ /**
34
+ * Returns an A3M language model.
35
+ *
36
+ * @param modelId - Use 'auto' for automatic routing, or specify a model like 'gpt-4', 'claude-3', etc.
37
+ */
38
+ (modelId: string): LanguageModelV1;
39
+ }
40
+
41
+ /**
42
+ * Create an A3M Router provider for Vercel AI SDK
43
+ */
44
+ export function createA3MProvider(config: A3MRouterConfig = {}): A3MProvider {
45
+ const languageModel = createA3MLanguageModel(config);
46
+
47
+ const provider = Object.assign(
48
+ (modelId: string) => {
49
+ // Return the language model - A3M handles model selection internally
50
+ return languageModel;
51
+ },
52
+ {
53
+ name: 'a3m' as const,
54
+ // Allow accessing config for introspection
55
+ __config: config,
56
+ }
57
+ );
58
+
59
+ return provider;
60
+ }
61
+
62
+ /**
63
+ * Example usage with Next.js App Router
64
+ *
65
+ * ```typescript
66
+ * // app/api/chat/route.ts
67
+ * import { createA3MProvider } from '@a3m/vercel-ai';
68
+ * import { generateText } from 'ai';
69
+ *
70
+ * const a3m = createA3MProvider({
71
+ * parallelEnsemble: true,
72
+ * });
73
+ *
74
+ * export async function POST(req: Request) {
75
+ * const { messages } = await req.json();
76
+ *
77
+ * const result = await generateText({
78
+ * model: a3m('auto'),
79
+ * messages,
80
+ * });
81
+ *
82
+ * return Response.json(result);
83
+ * }
84
+ * ```
85
+ */
86
+
87
+ /**
88
+ * Example usage with streaming
89
+ *
90
+ * ```typescript
91
+ * import { createA3MProvider } from '@a3m/vercel-ai';
92
+ * import { streamText } from 'ai';
93
+ *
94
+ * const a3m = createA3MProvider();
95
+ *
96
+ * const result = await streamText({
97
+ * model: a3m('auto'),
98
+ * prompt: 'Write a story about a robot...',
99
+ * });
100
+ *
101
+ * // Stream to response
102
+ * return result.toDataStreamResponse();
103
+ * ```
104
+ */
@@ -0,0 +1,116 @@
1
+ /**
2
+ * A3M Router configuration for Vercel AI SDK
3
+ */
4
+
5
+ export interface A3MRouterConfig {
6
+ /**
7
+ * A3M Router endpoint
8
+ * @default 'http://localhost:8787'
9
+ */
10
+ baseURL?: string;
11
+
12
+ /**
13
+ * API key for A3M Router
14
+ * @default 'not-needed' for local
15
+ */
16
+ apiKey?: string;
17
+
18
+ /**
19
+ * Enable parallel ensemble execution
20
+ * Runs multiple providers and picks the best result
21
+ * @default false
22
+ */
23
+ parallelEnsemble?: boolean;
24
+
25
+ /**
26
+ * Number of providers to run in parallel when parallelEnsemble is true
27
+ * @default 3
28
+ */
29
+ parallelCount?: number;
30
+
31
+ /**
32
+ * Enable stealth mode (anti-detection for browser automation)
33
+ * @default false
34
+ */
35
+ stealth?: boolean;
36
+
37
+ /**
38
+ * Cache configuration
39
+ */
40
+ cache?: {
41
+ enabled?: boolean;
42
+ ttl?: number; // seconds
43
+ };
44
+
45
+ /**
46
+ * Provider configuration
47
+ * Maps provider names to their API keys
48
+ */
49
+ providers?: Record<string, {
50
+ apiKey: string;
51
+ /** Override the base URL for this provider */
52
+ baseURL?: string;
53
+ }>;
54
+
55
+ /**
56
+ * Cost limits per provider
57
+ */
58
+ budgetLimits?: Record<string, {
59
+ maxCostPerRequest?: number;
60
+ maxRequestsPerMinute?: number;
61
+ }>;
62
+ }
63
+
64
+ /**
65
+ * A3M Router response from /route or /generate
66
+ */
67
+ export interface A3MRouteResponse {
68
+ /** The generated content */
69
+ content: string;
70
+
71
+ /** The provider that was selected */
72
+ provider: string;
73
+
74
+ /** The model that was used */
75
+ model: string;
76
+
77
+ /** Cost in USD */
78
+ cost: number;
79
+
80
+ /** Latency in ms */
81
+ latencyMs: number;
82
+
83
+ /** Token usage */
84
+ usage?: {
85
+ promptTokens: number;
86
+ completionTokens: number;
87
+ totalTokens: number;
88
+ };
89
+
90
+ /** Confidence score (0-1) */
91
+ confidence?: number;
92
+
93
+ /** Finish reason */
94
+ finishReason?: 'stop' | 'length' | 'error';
95
+ }
96
+
97
+ /**
98
+ * A3M Router streaming response chunk
99
+ */
100
+ export interface A3MStreamChunk {
101
+ /** The delta content */
102
+ delta: string;
103
+
104
+ /** Provider that responded */
105
+ provider: string;
106
+
107
+ /** Whether this is the final chunk */
108
+ done: boolean;
109
+
110
+ /** Token usage (only on final chunk) */
111
+ usage?: {
112
+ promptTokens: number;
113
+ completionTokens: number;
114
+ totalTokens: number;
115
+ };
116
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }