adaptive-memory-multi-model-router 1.9.4 → 2.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.
Files changed (130) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +11 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
  5. package/.github/workflows/npm-stats-validation.yml +152 -0
  6. package/.github/workflows/pages.yml +37 -0
  7. package/CHANGELOG.md +122 -0
  8. package/CODE_OF_CONDUCT.md +128 -0
  9. package/CONTRIBUTING.md +110 -0
  10. package/LAUNCH-PAIN-DRIVEN.md +339 -0
  11. package/LAUNCH.md +575 -0
  12. package/POPULARITY_BOOSTERS.md +285 -0
  13. package/README.md +231 -300
  14. package/SECURITY.md +69 -0
  15. package/articles/CONTENT_STRUCTURE.md +292 -0
  16. package/articles/DEVTO_COST_GUIDE.md +473 -0
  17. package/articles/DEVTO_FINAL.md +416 -0
  18. package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
  19. package/articles/HN_10X_BETTER.md +430 -0
  20. package/articles/HN_CHINESE_STYLE.md +308 -0
  21. package/articles/HN_FINAL.md +199 -0
  22. package/articles/HN_POSTED_VERSION.md +56 -0
  23. package/articles/HN_RESEARCH.md +364 -0
  24. package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
  25. package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
  26. package/articles/PAIN-DRIVEN-devto.md +242 -0
  27. package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
  28. package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
  29. package/articles/PAIN-DRIVEN-hackernews.md +131 -0
  30. package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
  31. package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
  32. package/articles/PAIN-DRIVEN-reddit.md +218 -0
  33. package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
  34. package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
  35. package/articles/PAIN-DRIVEN-twitter.md +120 -0
  36. package/articles/PORTKEY_VS_A3M.md +147 -0
  37. package/articles/REDDIT_FINAL.md +232 -0
  38. package/articles/TWITTER_FINAL.md +167 -0
  39. package/articles/WHY_10X_BETTER.md +261 -0
  40. package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
  41. package/articles/ai-discoverability-llm-routing.md +210 -0
  42. package/articles/devto-llm-routing.md +109 -0
  43. package/articles/hackernews-show-hn.md +65 -0
  44. package/articles/hashnode-llm-cost-optimization.md +125 -0
  45. package/articles/medium-building-llm-router.md +205 -0
  46. package/articles/reddit-ml.md +86 -0
  47. package/articles/twitter-thread-cost-savings.md +98 -0
  48. package/articles/youtube-tutorial-script.md +262 -0
  49. package/assets/banner.svg +109 -0
  50. package/assets/logo.svg +68 -0
  51. package/assets/social-preview.svg +64 -0
  52. package/demo/demo-script.md +53 -0
  53. package/dist/analytics/costAnalytics.d.ts +77 -0
  54. package/dist/analytics/costAnalytics.d.ts.map +1 -0
  55. package/dist/analytics/costAnalytics.js +219 -0
  56. package/dist/analytics/costAnalytics.js.map +1 -0
  57. package/dist/cache/semanticCache.d.ts +62 -0
  58. package/dist/cache/semanticCache.d.ts.map +1 -0
  59. package/dist/cache/semanticCache.js +176 -0
  60. package/dist/cache/semanticCache.js.map +1 -0
  61. package/dist/cli.js +35 -0
  62. package/dist/geo/generativeEngineOptimization.js +321 -0
  63. package/dist/geo/geoRouter.js +387 -0
  64. package/dist/index.d.ts +4 -723
  65. package/dist/index.js +11 -344
  66. package/dist/index.js.map +1 -1
  67. package/dist/integrations/langchainAdapter.d.ts +146 -0
  68. package/dist/integrations/langchainAdapter.d.ts.map +1 -0
  69. package/dist/integrations/langchainAdapter.js +731 -0
  70. package/dist/integrations/langchainAdapter.js.map +1 -0
  71. package/dist/integrations/oauth.d.ts +69 -0
  72. package/dist/integrations/oauth.d.ts.map +1 -0
  73. package/dist/integrations/oauth.js +225 -21
  74. package/dist/integrations/oauth.js.map +1 -0
  75. package/dist/memory/autoFetch.d.ts +39 -0
  76. package/dist/memory/autoFetch.d.ts.map +1 -0
  77. package/dist/memory/autoFetch.js +80 -88
  78. package/dist/memory/autoFetch.js.map +1 -0
  79. package/dist/memory/memoryTree.d.ts +76 -0
  80. package/dist/memory/memoryTree.d.ts.map +1 -0
  81. package/dist/memory/memoryTree.js +185 -130
  82. package/dist/memory/memoryTree.js.map +1 -0
  83. package/dist/memory/obsidianVault.d.ts +71 -0
  84. package/dist/memory/obsidianVault.d.ts.map +1 -0
  85. package/dist/memory/obsidianVault.js +207 -22
  86. package/dist/memory/obsidianVault.js.map +1 -0
  87. package/dist/providers/providerConfig.d.ts +49 -0
  88. package/dist/providers/providerConfig.d.ts.map +1 -0
  89. package/dist/providers/providerConfig.js +806 -401
  90. package/dist/providers/providerConfig.js.map +1 -0
  91. package/dist/security/guardrails.d.ts +76 -0
  92. package/dist/security/guardrails.d.ts.map +1 -0
  93. package/dist/security/guardrails.js +479 -0
  94. package/dist/security/guardrails.js.map +1 -0
  95. package/dist/security/inputValidation.js +351 -0
  96. package/dist/server/dashboard.d.ts +58 -0
  97. package/dist/server/dashboard.d.ts.map +1 -0
  98. package/dist/server/dashboard.js +553 -0
  99. package/dist/server/dashboard.js.map +1 -0
  100. package/dist/server/modelMapper.d.ts +43 -0
  101. package/dist/server/modelMapper.d.ts.map +1 -0
  102. package/dist/server/modelMapper.js +154 -0
  103. package/dist/server/modelMapper.js.map +1 -0
  104. package/dist/server/proxyServer.d.ts +41 -0
  105. package/dist/server/proxyServer.d.ts.map +1 -0
  106. package/dist/server/proxyServer.js +932 -0
  107. package/dist/server/proxyServer.js.map +1 -0
  108. package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
  109. package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
  110. package/dist/skills/__tests__/skill_manager.test.js +268 -0
  111. package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
  112. package/docs/geo/GENERATIVE_ENGINE_OPTIMIZATION.md +232 -0
  113. package/docs-site/index.html +347 -0
  114. package/llms.txt +138 -0
  115. package/package.json +72 -7
  116. package/playground/README.md +51 -0
  117. package/playground/codesandbox.json +12 -0
  118. package/playground/index.js +39 -0
  119. package/scripts/update-npm-badges.js +158 -0
  120. package/src/analytics/costAnalytics.ts +304 -0
  121. package/src/cache/semanticCache.ts +221 -0
  122. package/src/index.ts +6 -0
  123. package/src/integrations/langchainAdapter.ts +955 -0
  124. package/src/providers/providerConfig.ts +923 -0
  125. package/src/security/guardrails.ts +585 -0
  126. package/src/server/dashboard.ts +610 -0
  127. package/src/server/modelMapper.ts +182 -0
  128. package/src/server/proxyServer.ts +1105 -0
  129. package/src/types/langchain.d.ts +83 -0
  130. package/tsconfig.build.json +20 -0
@@ -0,0 +1,955 @@
1
+ /**
2
+ * A3M Router - LangChain Adapter
3
+ *
4
+ * Drop-in replacement for ChatOpenAI from @langchain/openai.
5
+ * Routes all LLM calls through the A3M Router for cost optimization,
6
+ * load balancing, and intelligent provider selection.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { A3MChatModel } from 'adaptive-memory-multi-model-router/langchain';
11
+ * import { HumanMessage } from '@langchain/core/messages';
12
+ *
13
+ * const model = new A3MChatModel({
14
+ * modelName: 'auto', // or 'groq/llama-3.3-70b-versatile'
15
+ * temperature: 0.7,
16
+ * });
17
+ *
18
+ * const response = await model.invoke([
19
+ * new HumanMessage("What is 2+2?")
20
+ * ]);
21
+ *
22
+ * // Streaming
23
+ * const stream = await model.stream([
24
+ * new HumanMessage("Tell me a story")
25
+ * ]);
26
+ * for await (const chunk of stream) {
27
+ * process.stdout.write(chunk.content as string);
28
+ * }
29
+ * ```
30
+ *
31
+ * LangChain is a PEER DEPENDENCY. Install it separately:
32
+ * npm install @langchain/core @langchain/openai
33
+ */
34
+
35
+ // ============================================================
36
+ // TYPE-ONLY IMPORTS (no runtime dependency on langchain)
37
+ // ============================================================
38
+
39
+ import type {
40
+ BaseChatModelParams,
41
+ BaseChatModelCallOptions,
42
+ } from '@langchain/core/language_models/chat_models';
43
+
44
+ import type {
45
+ BaseMessage,
46
+ AIMessage,
47
+ AIMessageChunk,
48
+ MessageContent,
49
+ } from '@langchain/core/messages';
50
+
51
+ import type {
52
+ ChatGeneration,
53
+ ChatGenerationChunk,
54
+ ChatResult,
55
+ } from '@langchain/core/outputs';
56
+
57
+ import type {
58
+ ToolDefinition,
59
+ } from '@langchain/core/language_models/base';
60
+
61
+ import type {
62
+ StructuredOutputMethodParams,
63
+ } from '@langchain/core/language_models/structured_output';
64
+
65
+ // ============================================================
66
+ // A3M INTERNAL IMPORTS
67
+ // ============================================================
68
+
69
+ import {
70
+ type ProviderDefinition,
71
+ getAvailableProviders,
72
+ loadConfig,
73
+ healthCheck,
74
+ registerProvider,
75
+ } from '../providers/providerConfig.js';
76
+
77
+ import { ProviderRegistry } from '../providers/registry.js';
78
+
79
+ // ============================================================
80
+ // TYPES
81
+ // ============================================================
82
+
83
+ export interface A3MChatModelOptions {
84
+ /** A3M router instance (creates default if not provided) */
85
+ router?: any;
86
+ /** Model to use: 'auto' for router selection, or 'provider/model' */
87
+ modelName?: string;
88
+ /** Temperature (0-2) */
89
+ temperature?: number;
90
+ /** Max output tokens */
91
+ maxTokens?: number;
92
+ /** Top-p sampling */
93
+ topP?: number;
94
+ /** Frequency penalty (-2 to 2) */
95
+ frequencyPenalty?: number;
96
+ /** Presence penalty (-2 to 2) */
97
+ presencePenalty?: number;
98
+ /** Stop sequences */
99
+ stop?: string[];
100
+ /** Timeout in ms */
101
+ timeout?: number;
102
+ /** API key override (uses env vars if not set) */
103
+ apiKey?: string;
104
+ /** Base URL override */
105
+ baseUrl?: string;
106
+ /** Provider format hint */
107
+ format?: 'openai' | 'anthropic' | 'google';
108
+ /** Extra headers to send with requests */
109
+ headers?: Record<string, string>;
110
+ /** Tags for tracing */
111
+ tags?: string[];
112
+ /** Metadata for tracing */
113
+ metadata?: Record<string, any>;
114
+ /** Verbose logging */
115
+ verbose?: boolean;
116
+ }
117
+
118
+ export interface A3MToolCall {
119
+ id: string;
120
+ type: 'function';
121
+ function: {
122
+ name: string;
123
+ arguments: string;
124
+ };
125
+ }
126
+
127
+ // ============================================================
128
+ // HELPER: Convert LangChain messages to OpenAI format
129
+ // ============================================================
130
+
131
+ function langchainMessagesToOpenAI(messages: BaseMessage[]): Array<{
132
+ role: 'system' | 'user' | 'assistant' | 'tool';
133
+ content: string | Array<{ type: string; text?: string; image_url?: any }>;
134
+ name?: string;
135
+ tool_call_id?: string;
136
+ tool_calls?: A3MToolCall[];
137
+ }> {
138
+ return messages.map((msg) => {
139
+ const role = msg._getType() as 'system' | 'user' | 'assistant' | 'tool';
140
+
141
+ let content: string | Array<{ type: string; text?: string; image_url?: any }>;
142
+ if (typeof msg.content === 'string') {
143
+ content = msg.content;
144
+ } else if (Array.isArray(msg.content)) {
145
+ content = (msg.content as Array<any>).map((part) => {
146
+ if (typeof part === 'string') return { type: 'text', text: part };
147
+ if (part.type === 'text') return { type: 'text', text: part.text };
148
+ if (part.type === 'image_url') return { type: 'image_url', image_url: part.image_url };
149
+ return part;
150
+ });
151
+ } else {
152
+ content = String(msg.content);
153
+ }
154
+
155
+ const result: any = { role, content };
156
+
157
+ // Tool calls (from AIMessage with tool_calls)
158
+ const aiMsg = msg as any;
159
+ if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
160
+ result.tool_calls = aiMsg.tool_calls.map((tc: any) => ({
161
+ id: tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
162
+ type: 'function' as const,
163
+ function: {
164
+ name: tc.name,
165
+ arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),
166
+ },
167
+ }));
168
+ }
169
+
170
+ // Tool call ID (from ToolMessage)
171
+ if (aiMsg.tool_call_id) {
172
+ result.tool_call_id = aiMsg.tool_call_id;
173
+ }
174
+
175
+ // Name
176
+ if (aiMsg.name) {
177
+ result.name = aiMsg.name;
178
+ }
179
+
180
+ return result;
181
+ });
182
+ }
183
+
184
+ // ============================================================
185
+ // HELPER: HTTP request with streaming support
186
+ // ============================================================
187
+
188
+ async function makeRequest(
189
+ url: string,
190
+ headers: Record<string, string>,
191
+ body: any,
192
+ timeout: number = 60000,
193
+ ): Promise<any> {
194
+ const controller = new AbortController();
195
+ const timer = setTimeout(() => controller.abort(), timeout);
196
+
197
+ try {
198
+ const response = await fetch(url, {
199
+ method: 'POST',
200
+ headers,
201
+ body: JSON.stringify(body),
202
+ signal: controller.signal,
203
+ });
204
+
205
+ if (!response.ok) {
206
+ const text = await response.text();
207
+ throw new Error(`HTTP ${response.status}: ${text}`);
208
+ }
209
+
210
+ return await response.json();
211
+ } finally {
212
+ clearTimeout(timer);
213
+ }
214
+ }
215
+
216
+ async function* makeStreamingRequest(
217
+ url: string,
218
+ headers: Record<string, string>,
219
+ body: any,
220
+ timeout: number = 120000,
221
+ ): AsyncGenerator<string> {
222
+ const controller = new AbortController();
223
+ const timer = setTimeout(() => controller.abort(), timeout);
224
+
225
+ try {
226
+ const response = await fetch(url, {
227
+ method: 'POST',
228
+ headers,
229
+ body: JSON.stringify(body),
230
+ signal: controller.signal,
231
+ });
232
+
233
+ if (!response.ok) {
234
+ const text = await response.text();
235
+ throw new Error(`HTTP ${response.status}: ${text}`);
236
+ }
237
+
238
+ const reader = response.body?.getReader();
239
+ if (!reader) throw new Error('No response body');
240
+
241
+ const decoder = new TextDecoder();
242
+ let buffer = '';
243
+
244
+ while (true) {
245
+ const { done, value } = await reader.read();
246
+ if (done) break;
247
+
248
+ buffer += decoder.decode(value, { stream: true });
249
+ const lines = buffer.split('\n');
250
+ buffer = lines.pop() || '';
251
+
252
+ for (const line of lines) {
253
+ const trimmed = line.trim();
254
+ if (trimmed.startsWith('data: ')) {
255
+ const data = trimmed.slice(6);
256
+ if (data === '[DONE]') return;
257
+ yield data;
258
+ }
259
+ }
260
+ }
261
+ } finally {
262
+ clearTimeout(timer);
263
+ }
264
+ }
265
+
266
+ // ============================================================
267
+ // HELPER: Parse provider/model from modelName
268
+ // ============================================================
269
+
270
+ function parseModel(modelName: string): { provider: string; model: string } {
271
+ if (modelName.includes('/')) {
272
+ const idx = modelName.indexOf('/');
273
+ return {
274
+ provider: modelName.slice(0, idx),
275
+ model: modelName.slice(idx + 1),
276
+ };
277
+ }
278
+ return { provider: modelName, model: '' };
279
+ }
280
+
281
+ // ============================================================
282
+ // HELPER: Resolve provider config from registry
283
+ // ============================================================
284
+
285
+ function resolveProvider(
286
+ modelName: string,
287
+ registry: ProviderRegistry,
288
+ ): { providerDef: ProviderDefinition; model: string; providerName: string } | null {
289
+ loadConfig();
290
+
291
+ if (modelName === 'auto' || modelName === '') {
292
+ // Use registry's priority-based selection
293
+ const selected = registry.selectModel();
294
+ if (!selected) return null;
295
+ const parsed = parseModel(selected);
296
+ const available = getAvailableProviders();
297
+ const prov = available[parsed.provider];
298
+ if (!prov) return null;
299
+ return { providerDef: prov, model: parsed.model || prov.models[0], providerName: parsed.provider };
300
+ }
301
+
302
+ const parsed = parseModel(modelName);
303
+ const available = getAvailableProviders();
304
+ const prov = available[parsed.provider];
305
+ if (prov) {
306
+ return { providerDef: prov, model: parsed.model || prov.models[0], providerName: parsed.provider };
307
+ }
308
+
309
+ // Try matching by model name across all providers
310
+ for (const [id, providerDef] of Object.entries(available)) {
311
+ if (providerDef.models.includes(modelName)) {
312
+ return { providerDef, model: modelName, providerName: id };
313
+ }
314
+ }
315
+
316
+ return null;
317
+ }
318
+
319
+ // ============================================================
320
+ // HELPER: Build request URL and headers for a provider
321
+ // ============================================================
322
+
323
+ function buildRequestConfig(
324
+ providerDef: ProviderDefinition,
325
+ apiKeyOverride?: string,
326
+ extraHeaders?: Record<string, string>,
327
+ ): { url: string; headers: Record<string, string> } {
328
+ const apiKey = apiKeyOverride || providerDef.apiKey || '';
329
+
330
+ switch (providerDef.format) {
331
+ case 'anthropic':
332
+ return {
333
+ url: providerDef.baseUrl,
334
+ headers: {
335
+ 'Content-Type': 'application/json',
336
+ 'x-api-key': apiKey,
337
+ 'anthropic-version': '2023-06-01',
338
+ ...extraHeaders,
339
+ },
340
+ };
341
+
342
+ case 'google':
343
+ return {
344
+ url: providerDef.baseUrl,
345
+ headers: {
346
+ 'Content-Type': 'application/json',
347
+ ...extraHeaders,
348
+ },
349
+ };
350
+
351
+ case 'cohere':
352
+ return {
353
+ url: providerDef.baseUrl,
354
+ headers: {
355
+ 'Content-Type': 'application/json',
356
+ 'Authorization': `Bearer ${apiKey}`,
357
+ ...extraHeaders,
358
+ },
359
+ };
360
+
361
+ case 'openai':
362
+ default:
363
+ return {
364
+ url: providerDef.baseUrl,
365
+ headers: {
366
+ 'Content-Type': 'application/json',
367
+ 'Authorization': `Bearer ${apiKey}`,
368
+ ...extraHeaders,
369
+ },
370
+ };
371
+ }
372
+ }
373
+
374
+ // ============================================================
375
+ // HELPER: Build request body per provider format
376
+ // ============================================================
377
+
378
+ function buildRequestBody(
379
+ providerDef: ProviderDefinition,
380
+ model: string,
381
+ openaiMessages: any[],
382
+ options: {
383
+ temperature?: number;
384
+ maxTokens?: number;
385
+ topP?: number;
386
+ frequencyPenalty?: number;
387
+ presencePenalty?: number;
388
+ stop?: string[];
389
+ tools?: any[];
390
+ stream?: boolean;
391
+ },
392
+ ): any {
393
+ switch (providerDef.format) {
394
+ case 'anthropic':
395
+ // Extract system message
396
+ let systemPrompt = '';
397
+ const nonSystemMessages = openaiMessages.filter((m: any) => {
398
+ if (m.role === 'system') {
399
+ systemPrompt += (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)) + '\n';
400
+ return false;
401
+ }
402
+ return true;
403
+ });
404
+ return {
405
+ model,
406
+ max_tokens: options.maxTokens || 4096,
407
+ system: systemPrompt.trim() || undefined,
408
+ messages: nonSystemMessages.map((m: any) => ({
409
+ role: m.role === 'tool' ? 'user' : m.role,
410
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
411
+ })),
412
+ temperature: options.temperature,
413
+ top_p: options.topP,
414
+ stop_sequences: options.stop,
415
+ stream: options.stream || false,
416
+ };
417
+
418
+ case 'google': {
419
+ const systemMsg = openaiMessages.find((m: any) => m.role === 'system');
420
+ const contents = openaiMessages
421
+ .filter((m: any) => m.role !== 'system')
422
+ .map((m: any) => ({
423
+ role: m.role === 'assistant' ? 'model' : 'user',
424
+ parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
425
+ }));
426
+ return {
427
+ contents,
428
+ systemInstruction: systemMsg ? { parts: [{ text: systemMsg.content }] } : undefined,
429
+ generationConfig: {
430
+ maxOutputTokens: options.maxTokens || 4096,
431
+ temperature: options.temperature,
432
+ topP: options.topP,
433
+ stopSequences: options.stop,
434
+ },
435
+ };
436
+ }
437
+
438
+ case 'openai':
439
+ case 'cohere':
440
+ default:
441
+ return {
442
+ model,
443
+ messages: openaiMessages,
444
+ temperature: options.temperature,
445
+ max_tokens: options.maxTokens,
446
+ top_p: options.topP,
447
+ frequency_penalty: options.frequencyPenalty,
448
+ presence_penalty: options.presencePenalty,
449
+ stop: options.stop,
450
+ tools: options.tools,
451
+ stream: options.stream || false,
452
+ };
453
+ }
454
+ }
455
+
456
+ // ============================================================
457
+ // HELPER: Parse response per provider format
458
+ // ============================================================
459
+
460
+ function parseResponse(providerDef: ProviderDefinition, data: any): {
461
+ content: string;
462
+ toolCalls?: A3MToolCall[];
463
+ usage?: { input_tokens: number; output_tokens: number };
464
+ model: string;
465
+ } {
466
+ switch (providerDef.format) {
467
+ case 'anthropic':
468
+ return {
469
+ content: data.content?.[0]?.text || '',
470
+ toolCalls: data.content?.filter((c: any) => c.type === 'tool_use').map((tc: any) => ({
471
+ id: tc.id,
472
+ type: 'function' as const,
473
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) },
474
+ })),
475
+ usage: {
476
+ input_tokens: data.usage?.input_tokens || 0,
477
+ output_tokens: data.usage?.output_tokens || 0,
478
+ },
479
+ model: data.model || '',
480
+ };
481
+
482
+ case 'google':
483
+ return {
484
+ content: data.candidates?.[0]?.content?.parts?.[0]?.text || '',
485
+ usage: {
486
+ input_tokens: data.usageMetadata?.promptTokenCount || 0,
487
+ output_tokens: data.usageMetadata?.candidatesTokenCount || 0,
488
+ },
489
+ model: data.modelVersion || '',
490
+ };
491
+
492
+ case 'openai':
493
+ case 'cohere':
494
+ default:
495
+ return {
496
+ content: data.choices?.[0]?.message?.content || '',
497
+ toolCalls: data.choices?.[0]?.message?.tool_calls,
498
+ usage: {
499
+ input_tokens: data.usage?.prompt_tokens || 0,
500
+ output_tokens: data.usage?.completion_tokens || 0,
501
+ },
502
+ model: data.model || '',
503
+ };
504
+ }
505
+ }
506
+
507
+ // ============================================================
508
+ // MAIN: A3MChatModel
509
+ // ============================================================
510
+
511
+ export class A3MChatModel {
512
+ // LangChain BaseChatModel compatibility fields
513
+ lc_namespace = ['adaptive_memory_multi_model_router', 'langchain'];
514
+ lc_sequential = true;
515
+ lc_runnable = true;
516
+
517
+ // Parsed options
518
+ private modelName: string;
519
+ private temperature: number;
520
+ private maxTokens: number;
521
+ private topP?: number;
522
+ private frequencyPenalty?: number;
523
+ private presencePenalty?: number;
524
+ private stop?: string[];
525
+ private timeout: number;
526
+ private apiKey?: string;
527
+ private baseUrl?: string;
528
+ private format?: 'openai' | 'anthropic' | 'google';
529
+ private extraHeaders?: Record<string, string>;
530
+ private verbose: boolean;
531
+ private boundTools: any[] = [];
532
+
533
+ // A3M Router
534
+ private registry: ProviderRegistry;
535
+
536
+ constructor(options: A3MChatModelOptions & Record<string, any> = {}) {
537
+ this.modelName = options.modelName || options.model || 'auto';
538
+ this.temperature = options.temperature ?? 0.7;
539
+ this.maxTokens = options.maxTokens ?? 4096;
540
+ this.topP = options.topP;
541
+ this.frequencyPenalty = options.frequencyPenalty;
542
+ this.presencePenalty = options.presencePenalty;
543
+ this.stop = options.stop;
544
+ this.timeout = options.timeout ?? 60000;
545
+ this.apiKey = options.apiKey;
546
+ this.baseUrl = options.baseUrl;
547
+ this.format = options.format;
548
+ this.extraHeaders = options.headers;
549
+ this.verbose = options.verbose ?? false;
550
+
551
+ // Initialize A3M Router
552
+ if (options.router) {
553
+ this.registry = options.router;
554
+ } else {
555
+ this.registry = new ProviderRegistry();
556
+ }
557
+ }
558
+
559
+ // ========================================================================
560
+ // LangChain compatibility: property getters
561
+ // ========================================================================
562
+
563
+ get model(): string {
564
+ return this.modelName;
565
+ }
566
+
567
+ get identifyingParams(): Record<string, any> {
568
+ return {
569
+ modelName: this.modelName,
570
+ temperature: this.temperature,
571
+ maxTokens: this.maxTokens,
572
+ };
573
+ }
574
+
575
+ get lc_aliases(): Record<string, string> {
576
+ return {
577
+ model: 'modelName',
578
+ };
579
+ }
580
+
581
+ // ========================================================================
582
+ // LangChain compatibility: serialize/deserialize
583
+ // ========================================================================
584
+
585
+ toJSON(): Record<string, any> {
586
+ return {
587
+ type: 'A3MChatModel',
588
+ kwargs: {
589
+ modelName: this.modelName,
590
+ temperature: this.temperature,
591
+ maxTokens: this.maxTokens,
592
+ },
593
+ };
594
+ }
595
+
596
+ static async deserialize(data: Record<string, any>): Promise<A3MChatModel> {
597
+ return new A3MChatModel(data.kwargs || {});
598
+ }
599
+
600
+ // ========================================================================
601
+ // Core: _generate (non-streaming)
602
+ // ========================================================================
603
+
604
+ async _generate(
605
+ messages: BaseMessage[],
606
+ options?: BaseChatModelCallOptions & { tools?: any[] },
607
+ ): Promise<ChatResult> {
608
+ const resolved = resolveProvider(this.modelName, this.registry);
609
+ if (!resolved) {
610
+ throw new Error(`A3M Router: No provider available for model "${this.modelName}". ` +
611
+ `Set API keys via environment variables (e.g., GROQ_API_KEY, OPENAI_API_KEY).`);
612
+ }
613
+
614
+ const { providerDef, model, providerName } = resolved;
615
+ const requestConfig = buildRequestConfig(providerDef, this.apiKey, this.extraHeaders);
616
+ const openaiMessages = langchainMessagesToOpenAI(messages);
617
+ const tools = options?.tools || this.boundTools;
618
+
619
+ const body = buildRequestBody(providerDef, model, openaiMessages, {
620
+ temperature: this.temperature,
621
+ maxTokens: this.maxTokens,
622
+ topP: this.topP,
623
+ frequencyPenalty: this.frequencyPenalty,
624
+ presencePenalty: this.presencePenalty,
625
+ stop: this.stop,
626
+ tools: tools.length > 0 ? tools : undefined,
627
+ stream: false,
628
+ });
629
+
630
+ if (this.verbose) {
631
+ console.log(`[A3M Router] ${providerName}/${model} -> ${requestConfig.url}`);
632
+ }
633
+
634
+ const data = await makeRequest(requestConfig.url, requestConfig.headers, body, this.timeout);
635
+ const parsed = parseResponse(providerDef, data);
636
+
637
+ // Build AIMessage-compatible response
638
+ const generationInfo: Record<string, any> = {
639
+ model: parsed.model,
640
+ provider: providerName,
641
+ };
642
+
643
+ if (parsed.usage) {
644
+ generationInfo.tokenUsage = {
645
+ promptTokens: parsed.usage.input_tokens,
646
+ completionTokens: parsed.usage.output_tokens,
647
+ totalTokens: parsed.usage.input_tokens + parsed.usage.output_tokens,
648
+ };
649
+ }
650
+
651
+ const aiMessage: Record<string, any> = {
652
+ content: parsed.content,
653
+ additional_kwargs: {},
654
+ response_metadata: generationInfo,
655
+ };
656
+
657
+ if (parsed.toolCalls && parsed.toolCalls.length > 0) {
658
+ aiMessage.tool_calls = parsed.toolCalls.map((tc: A3MToolCall) => ({
659
+ id: tc.id,
660
+ name: tc.function.name,
661
+ args: JSON.parse(tc.function.arguments),
662
+ }));
663
+ aiMessage.additional_kwargs.tool_calls = parsed.toolCalls;
664
+ }
665
+
666
+ return {
667
+ generations: [{
668
+ text: parsed.content,
669
+ message: aiMessage as any as AIMessage,
670
+ generationInfo,
671
+ }] as ChatGeneration[],
672
+ llmOutput: generationInfo,
673
+ };
674
+ }
675
+
676
+ // ========================================================================
677
+ // Core: invoke (high-level)
678
+ // ========================================================================
679
+
680
+ async invoke(
681
+ input: BaseMessage[] | string,
682
+ options?: BaseChatModelCallOptions & { tools?: any[] },
683
+ ): Promise<any> {
684
+ const messages: BaseMessage[] = typeof input === 'string'
685
+ ? [{ _getType: () => 'human', content: input } as any]
686
+ : input;
687
+
688
+ const result = await this._generate(messages, options);
689
+ return result.generations[0]?.message;
690
+ }
691
+
692
+ // ========================================================================
693
+ // Core: _streamResponseChunks (streaming)
694
+ // ========================================================================
695
+
696
+ async *_streamResponseChunks(
697
+ messages: BaseMessage[],
698
+ options?: BaseChatModelCallOptions & { tools?: any[] },
699
+ ): AsyncGenerator<ChatGenerationChunk> {
700
+ const resolved = resolveProvider(this.modelName, this.registry);
701
+ if (!resolved) {
702
+ throw new Error(`A3M Router: No provider available for model "${this.modelName}".`);
703
+ }
704
+
705
+ const { providerDef, model, providerName } = resolved;
706
+ const requestConfig = buildRequestConfig(providerDef, this.apiKey, this.extraHeaders);
707
+ const openaiMessages = langchainMessagesToOpenAI(messages);
708
+ const tools = options?.tools || this.boundTools;
709
+
710
+ const body = buildRequestBody(providerDef, model, openaiMessages, {
711
+ temperature: this.temperature,
712
+ maxTokens: this.maxTokens,
713
+ topP: this.topP,
714
+ frequencyPenalty: this.frequencyPenalty,
715
+ presencePenalty: this.presencePenalty,
716
+ stop: this.stop,
717
+ tools: tools.length > 0 ? tools : undefined,
718
+ stream: true,
719
+ });
720
+
721
+ if (this.verbose) {
722
+ console.log(`[A3M Router] streaming ${providerName}/${model} -> ${requestConfig.url}`);
723
+ }
724
+
725
+ const stream = makeStreamingRequest(requestConfig.url, requestConfig.headers, body, this.timeout * 2);
726
+
727
+ for await (const chunk of stream) {
728
+ try {
729
+ const parsed = JSON.parse(chunk);
730
+
731
+ // OpenAI streaming format
732
+ const delta = parsed.choices?.[0]?.delta;
733
+ if (delta) {
734
+ const content = delta.content || '';
735
+ const toolCalls = delta.tool_calls;
736
+
737
+ const messageChunk: Record<string, any> = {
738
+ content,
739
+ additional_kwargs: {},
740
+ };
741
+
742
+ if (toolCalls) {
743
+ messageChunk.tool_call_chunks = toolCalls.map((tc: any) => ({
744
+ id: tc.id || '',
745
+ name: tc.function?.name || '',
746
+ args: tc.function?.arguments || '',
747
+ }));
748
+ }
749
+
750
+ yield {
751
+ text: content,
752
+ message: messageChunk as any as AIMessageChunk,
753
+ generationInfo: {
754
+ model: parsed.model || model,
755
+ provider: providerName,
756
+ },
757
+ } as ChatGenerationChunk;
758
+ }
759
+ } catch {
760
+ // Skip malformed SSE chunks
761
+ }
762
+ }
763
+ }
764
+
765
+ // ========================================================================
766
+ // Core: stream (high-level)
767
+ // ========================================================================
768
+
769
+ async *stream(
770
+ input: BaseMessage[] | string,
771
+ options?: BaseChatModelCallOptions & { tools?: any[] },
772
+ ): AsyncGenerator<any> {
773
+ const messages: BaseMessage[] = typeof input === 'string'
774
+ ? [{ _getType: () => 'human', content: input } as any]
775
+ : input;
776
+
777
+ yield* this._streamResponseChunks(messages, options);
778
+ }
779
+
780
+ // ========================================================================
781
+ // Tool calling support
782
+ // ========================================================================
783
+
784
+ bindTools(tools: any[]): A3MChatModel {
785
+ const clone = new A3MChatModel({
786
+ router: this.registry,
787
+ modelName: this.modelName,
788
+ temperature: this.temperature,
789
+ maxTokens: this.maxTokens,
790
+ topP: this.topP,
791
+ frequencyPenalty: this.frequencyPenalty,
792
+ presencePenalty: this.presencePenalty,
793
+ stop: this.stop,
794
+ timeout: this.timeout,
795
+ apiKey: this.apiKey,
796
+ baseUrl: this.baseUrl,
797
+ format: this.format,
798
+ headers: this.extraHeaders,
799
+ verbose: this.verbose,
800
+ });
801
+ clone.boundTools = tools;
802
+ return clone;
803
+ }
804
+
805
+ // ========================================================================
806
+ // Structured output support
807
+ // ========================================================================
808
+
809
+ withStructuredOutput<T extends Record<string, any>>(
810
+ schema: any,
811
+ config?: StructuredOutputMethodParams | boolean,
812
+ ): any {
813
+ // Build a wrapper that parses the response as structured JSON
814
+ const self = this;
815
+
816
+ const schemaDef = typeof schema === 'object' && 'schema' in (schema as any)
817
+ ? (schema as any).schema
818
+ : schema;
819
+
820
+ const toolDef = {
821
+ type: 'function' as const,
822
+ function: {
823
+ name: 'structured_output',
824
+ description: 'Structured output',
825
+ parameters: schemaDef,
826
+ },
827
+ };
828
+
829
+ const bound = this.bindTools([toolDef]);
830
+
831
+ return {
832
+ async invoke(input: BaseMessage[] | string): Promise<T> {
833
+ const result = await bound.invoke(input);
834
+ // Try to parse tool call args, fall back to content JSON parse
835
+ if (result?.tool_calls?.[0]?.args) {
836
+ return result.tool_calls[0].args as T;
837
+ }
838
+ if (result?.additional_kwargs?.tool_calls?.[0]?.function?.arguments) {
839
+ return JSON.parse(result.additional_kwargs.tool_calls[0].function.arguments) as T;
840
+ }
841
+ if (typeof result?.content === 'string') {
842
+ try {
843
+ const jsonMatch = result.content.match(/```json\n([\s\S]*?)\n```/) ||
844
+ result.content.match(/\{[\s\S]*\}/);
845
+ if (jsonMatch) {
846
+ return JSON.parse(jsonMatch[0].replace(/```json\n?/g, '').replace(/\n?```/g, '')) as T;
847
+ }
848
+ } catch {
849
+ // Fall through
850
+ }
851
+ }
852
+ throw new Error('A3M Router: Failed to parse structured output from response');
853
+ },
854
+ };
855
+ }
856
+
857
+ // ========================================================================
858
+ // Provider management
859
+ // ========================================================================
860
+
861
+ /**
862
+ * Get the list of available providers
863
+ */
864
+ getAvailableProviders(): Record<string, ProviderDefinition> {
865
+ return getAvailableProviders();
866
+ }
867
+
868
+ /**
869
+ * Get provider status from the registry
870
+ */
871
+ getProviderStatus(): any {
872
+ return this.registry.getStatus();
873
+ }
874
+
875
+ /**
876
+ * Switch the model at runtime
877
+ */
878
+ withModel(modelName: string): A3MChatModel {
879
+ const clone = new A3MChatModel({
880
+ router: this.registry,
881
+ modelName,
882
+ temperature: this.temperature,
883
+ maxTokens: this.maxTokens,
884
+ topP: this.topP,
885
+ frequencyPenalty: this.frequencyPenalty,
886
+ presencePenalty: this.presencePenalty,
887
+ stop: this.stop,
888
+ timeout: this.timeout,
889
+ apiKey: this.apiKey,
890
+ baseUrl: this.baseUrl,
891
+ format: this.format,
892
+ headers: this.extraHeaders,
893
+ verbose: this.verbose,
894
+ });
895
+ clone.boundTools = [...this.boundTools];
896
+ return clone;
897
+ }
898
+
899
+ /**
900
+ * Set temperature
901
+ */
902
+ withTemperature(temperature: number): A3MChatModel {
903
+ const clone = new A3MChatModel({
904
+ router: this.registry,
905
+ modelName: this.modelName,
906
+ temperature,
907
+ maxTokens: this.maxTokens,
908
+ topP: this.topP,
909
+ frequencyPenalty: this.frequencyPenalty,
910
+ presencePenalty: this.presencePenalty,
911
+ stop: this.stop,
912
+ timeout: this.timeout,
913
+ apiKey: this.apiKey,
914
+ baseUrl: this.baseUrl,
915
+ format: this.format,
916
+ headers: this.extraHeaders,
917
+ verbose: this.verbose,
918
+ });
919
+ clone.boundTools = [...this.boundTools];
920
+ return clone;
921
+ }
922
+
923
+ // ========================================================================
924
+ // LangChain event system compatibility
925
+ // ========================================================================
926
+
927
+ /** LLM type identifier for LangChain compatibility */
928
+ _llmType(): string {
929
+ return 'a3m-router';
930
+ }
931
+ }
932
+
933
+ // ============================================================
934
+ // FACTORY HELPERS
935
+ // ============================================================
936
+
937
+ /**
938
+ * Create an A3MChatModel pre-configured for a specific provider
939
+ */
940
+ export function createA3MChatModel(provider: string, options: Omit<A3MChatModelOptions, 'modelName'> = {}): A3MChatModel {
941
+ return new A3MChatModel({
942
+ ...options,
943
+ modelName: provider,
944
+ });
945
+ }
946
+
947
+ /**
948
+ * Create an A3MChatModel with automatic routing
949
+ */
950
+ export function createAutoRoutingChatModel(options: Omit<A3MChatModelOptions, 'modelName'> = {}): A3MChatModel {
951
+ return new A3MChatModel({
952
+ ...options,
953
+ modelName: 'auto',
954
+ });
955
+ }