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,731 @@
1
+ "use strict";
2
+ /**
3
+ * A3M Router - LangChain Adapter
4
+ *
5
+ * Drop-in replacement for ChatOpenAI from @langchain/openai.
6
+ * Routes all LLM calls through the A3M Router for cost optimization,
7
+ * load balancing, and intelligent provider selection.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { A3MChatModel } from 'adaptive-memory-multi-model-router/langchain';
12
+ * import { HumanMessage } from '@langchain/core/messages';
13
+ *
14
+ * const model = new A3MChatModel({
15
+ * modelName: 'auto', // or 'groq/llama-3.3-70b-versatile'
16
+ * temperature: 0.7,
17
+ * });
18
+ *
19
+ * const response = await model.invoke([
20
+ * new HumanMessage("What is 2+2?")
21
+ * ]);
22
+ *
23
+ * // Streaming
24
+ * const stream = await model.stream([
25
+ * new HumanMessage("Tell me a story")
26
+ * ]);
27
+ * for await (const chunk of stream) {
28
+ * process.stdout.write(chunk.content as string);
29
+ * }
30
+ * ```
31
+ *
32
+ * LangChain is a PEER DEPENDENCY. Install it separately:
33
+ * npm install @langchain/core @langchain/openai
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.A3MChatModel = void 0;
37
+ exports.createA3MChatModel = createA3MChatModel;
38
+ exports.createAutoRoutingChatModel = createAutoRoutingChatModel;
39
+ // ============================================================
40
+ // A3M INTERNAL IMPORTS
41
+ // ============================================================
42
+ const providerConfig_js_1 = require("../providers/providerConfig.js");
43
+ const registry_js_1 = require("../providers/registry.js");
44
+ // ============================================================
45
+ // HELPER: Convert LangChain messages to OpenAI format
46
+ // ============================================================
47
+ function langchainMessagesToOpenAI(messages) {
48
+ return messages.map((msg) => {
49
+ const role = msg._getType();
50
+ let content;
51
+ if (typeof msg.content === 'string') {
52
+ content = msg.content;
53
+ }
54
+ else if (Array.isArray(msg.content)) {
55
+ content = msg.content.map((part) => {
56
+ if (typeof part === 'string')
57
+ return { type: 'text', text: part };
58
+ if (part.type === 'text')
59
+ return { type: 'text', text: part.text };
60
+ if (part.type === 'image_url')
61
+ return { type: 'image_url', image_url: part.image_url };
62
+ return part;
63
+ });
64
+ }
65
+ else {
66
+ content = String(msg.content);
67
+ }
68
+ const result = { role, content };
69
+ // Tool calls (from AIMessage with tool_calls)
70
+ const aiMsg = msg;
71
+ if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
72
+ result.tool_calls = aiMsg.tool_calls.map((tc) => ({
73
+ id: tc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
74
+ type: 'function',
75
+ function: {
76
+ name: tc.name,
77
+ arguments: typeof tc.args === 'string' ? tc.args : JSON.stringify(tc.args),
78
+ },
79
+ }));
80
+ }
81
+ // Tool call ID (from ToolMessage)
82
+ if (aiMsg.tool_call_id) {
83
+ result.tool_call_id = aiMsg.tool_call_id;
84
+ }
85
+ // Name
86
+ if (aiMsg.name) {
87
+ result.name = aiMsg.name;
88
+ }
89
+ return result;
90
+ });
91
+ }
92
+ // ============================================================
93
+ // HELPER: HTTP request with streaming support
94
+ // ============================================================
95
+ async function makeRequest(url, headers, body, timeout = 60000) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), timeout);
98
+ try {
99
+ const response = await fetch(url, {
100
+ method: 'POST',
101
+ headers,
102
+ body: JSON.stringify(body),
103
+ signal: controller.signal,
104
+ });
105
+ if (!response.ok) {
106
+ const text = await response.text();
107
+ throw new Error(`HTTP ${response.status}: ${text}`);
108
+ }
109
+ return await response.json();
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ }
114
+ }
115
+ async function* makeStreamingRequest(url, headers, body, timeout = 120000) {
116
+ const controller = new AbortController();
117
+ const timer = setTimeout(() => controller.abort(), timeout);
118
+ try {
119
+ const response = await fetch(url, {
120
+ method: 'POST',
121
+ headers,
122
+ body: JSON.stringify(body),
123
+ signal: controller.signal,
124
+ });
125
+ if (!response.ok) {
126
+ const text = await response.text();
127
+ throw new Error(`HTTP ${response.status}: ${text}`);
128
+ }
129
+ const reader = response.body?.getReader();
130
+ if (!reader)
131
+ throw new Error('No response body');
132
+ const decoder = new TextDecoder();
133
+ let buffer = '';
134
+ while (true) {
135
+ const { done, value } = await reader.read();
136
+ if (done)
137
+ break;
138
+ buffer += decoder.decode(value, { stream: true });
139
+ const lines = buffer.split('\n');
140
+ buffer = lines.pop() || '';
141
+ for (const line of lines) {
142
+ const trimmed = line.trim();
143
+ if (trimmed.startsWith('data: ')) {
144
+ const data = trimmed.slice(6);
145
+ if (data === '[DONE]')
146
+ return;
147
+ yield data;
148
+ }
149
+ }
150
+ }
151
+ }
152
+ finally {
153
+ clearTimeout(timer);
154
+ }
155
+ }
156
+ // ============================================================
157
+ // HELPER: Parse provider/model from modelName
158
+ // ============================================================
159
+ function parseModel(modelName) {
160
+ if (modelName.includes('/')) {
161
+ const idx = modelName.indexOf('/');
162
+ return {
163
+ provider: modelName.slice(0, idx),
164
+ model: modelName.slice(idx + 1),
165
+ };
166
+ }
167
+ return { provider: modelName, model: '' };
168
+ }
169
+ // ============================================================
170
+ // HELPER: Resolve provider config from registry
171
+ // ============================================================
172
+ function resolveProvider(modelName, registry) {
173
+ (0, providerConfig_js_1.loadConfig)();
174
+ if (modelName === 'auto' || modelName === '') {
175
+ // Use registry's priority-based selection
176
+ const selected = registry.selectModel();
177
+ if (!selected)
178
+ return null;
179
+ const parsed = parseModel(selected);
180
+ const available = (0, providerConfig_js_1.getAvailableProviders)();
181
+ const prov = available[parsed.provider];
182
+ if (!prov)
183
+ return null;
184
+ return { providerDef: prov, model: parsed.model || prov.models[0], providerName: parsed.provider };
185
+ }
186
+ const parsed = parseModel(modelName);
187
+ const available = (0, providerConfig_js_1.getAvailableProviders)();
188
+ const prov = available[parsed.provider];
189
+ if (prov) {
190
+ return { providerDef: prov, model: parsed.model || prov.models[0], providerName: parsed.provider };
191
+ }
192
+ // Try matching by model name across all providers
193
+ for (const [id, providerDef] of Object.entries(available)) {
194
+ if (providerDef.models.includes(modelName)) {
195
+ return { providerDef, model: modelName, providerName: id };
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+ // ============================================================
201
+ // HELPER: Build request URL and headers for a provider
202
+ // ============================================================
203
+ function buildRequestConfig(providerDef, apiKeyOverride, extraHeaders) {
204
+ const apiKey = apiKeyOverride || providerDef.apiKey || '';
205
+ switch (providerDef.format) {
206
+ case 'anthropic':
207
+ return {
208
+ url: providerDef.baseUrl,
209
+ headers: {
210
+ 'Content-Type': 'application/json',
211
+ 'x-api-key': apiKey,
212
+ 'anthropic-version': '2023-06-01',
213
+ ...extraHeaders,
214
+ },
215
+ };
216
+ case 'google':
217
+ return {
218
+ url: providerDef.baseUrl,
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ ...extraHeaders,
222
+ },
223
+ };
224
+ case 'cohere':
225
+ return {
226
+ url: providerDef.baseUrl,
227
+ headers: {
228
+ 'Content-Type': 'application/json',
229
+ 'Authorization': `Bearer ${apiKey}`,
230
+ ...extraHeaders,
231
+ },
232
+ };
233
+ case 'openai':
234
+ default:
235
+ return {
236
+ url: providerDef.baseUrl,
237
+ headers: {
238
+ 'Content-Type': 'application/json',
239
+ 'Authorization': `Bearer ${apiKey}`,
240
+ ...extraHeaders,
241
+ },
242
+ };
243
+ }
244
+ }
245
+ // ============================================================
246
+ // HELPER: Build request body per provider format
247
+ // ============================================================
248
+ function buildRequestBody(providerDef, model, openaiMessages, options) {
249
+ switch (providerDef.format) {
250
+ case 'anthropic':
251
+ // Extract system message
252
+ let systemPrompt = '';
253
+ const nonSystemMessages = openaiMessages.filter((m) => {
254
+ if (m.role === 'system') {
255
+ systemPrompt += (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)) + '\n';
256
+ return false;
257
+ }
258
+ return true;
259
+ });
260
+ return {
261
+ model,
262
+ max_tokens: options.maxTokens || 4096,
263
+ system: systemPrompt.trim() || undefined,
264
+ messages: nonSystemMessages.map((m) => ({
265
+ role: m.role === 'tool' ? 'user' : m.role,
266
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
267
+ })),
268
+ temperature: options.temperature,
269
+ top_p: options.topP,
270
+ stop_sequences: options.stop,
271
+ stream: options.stream || false,
272
+ };
273
+ case 'google': {
274
+ const systemMsg = openaiMessages.find((m) => m.role === 'system');
275
+ const contents = openaiMessages
276
+ .filter((m) => m.role !== 'system')
277
+ .map((m) => ({
278
+ role: m.role === 'assistant' ? 'model' : 'user',
279
+ parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
280
+ }));
281
+ return {
282
+ contents,
283
+ systemInstruction: systemMsg ? { parts: [{ text: systemMsg.content }] } : undefined,
284
+ generationConfig: {
285
+ maxOutputTokens: options.maxTokens || 4096,
286
+ temperature: options.temperature,
287
+ topP: options.topP,
288
+ stopSequences: options.stop,
289
+ },
290
+ };
291
+ }
292
+ case 'openai':
293
+ case 'cohere':
294
+ default:
295
+ return {
296
+ model,
297
+ messages: openaiMessages,
298
+ temperature: options.temperature,
299
+ max_tokens: options.maxTokens,
300
+ top_p: options.topP,
301
+ frequency_penalty: options.frequencyPenalty,
302
+ presence_penalty: options.presencePenalty,
303
+ stop: options.stop,
304
+ tools: options.tools,
305
+ stream: options.stream || false,
306
+ };
307
+ }
308
+ }
309
+ // ============================================================
310
+ // HELPER: Parse response per provider format
311
+ // ============================================================
312
+ function parseResponse(providerDef, data) {
313
+ switch (providerDef.format) {
314
+ case 'anthropic':
315
+ return {
316
+ content: data.content?.[0]?.text || '',
317
+ toolCalls: data.content?.filter((c) => c.type === 'tool_use').map((tc) => ({
318
+ id: tc.id,
319
+ type: 'function',
320
+ function: { name: tc.name, arguments: JSON.stringify(tc.input) },
321
+ })),
322
+ usage: {
323
+ input_tokens: data.usage?.input_tokens || 0,
324
+ output_tokens: data.usage?.output_tokens || 0,
325
+ },
326
+ model: data.model || '',
327
+ };
328
+ case 'google':
329
+ return {
330
+ content: data.candidates?.[0]?.content?.parts?.[0]?.text || '',
331
+ usage: {
332
+ input_tokens: data.usageMetadata?.promptTokenCount || 0,
333
+ output_tokens: data.usageMetadata?.candidatesTokenCount || 0,
334
+ },
335
+ model: data.modelVersion || '',
336
+ };
337
+ case 'openai':
338
+ case 'cohere':
339
+ default:
340
+ return {
341
+ content: data.choices?.[0]?.message?.content || '',
342
+ toolCalls: data.choices?.[0]?.message?.tool_calls,
343
+ usage: {
344
+ input_tokens: data.usage?.prompt_tokens || 0,
345
+ output_tokens: data.usage?.completion_tokens || 0,
346
+ },
347
+ model: data.model || '',
348
+ };
349
+ }
350
+ }
351
+ // ============================================================
352
+ // MAIN: A3MChatModel
353
+ // ============================================================
354
+ class A3MChatModel {
355
+ // LangChain BaseChatModel compatibility fields
356
+ lc_namespace = ['adaptive_memory_multi_model_router', 'langchain'];
357
+ lc_sequential = true;
358
+ lc_runnable = true;
359
+ // Parsed options
360
+ modelName;
361
+ temperature;
362
+ maxTokens;
363
+ topP;
364
+ frequencyPenalty;
365
+ presencePenalty;
366
+ stop;
367
+ timeout;
368
+ apiKey;
369
+ baseUrl;
370
+ format;
371
+ extraHeaders;
372
+ verbose;
373
+ boundTools = [];
374
+ // A3M Router
375
+ registry;
376
+ constructor(options = {}) {
377
+ this.modelName = options.modelName || options.model || 'auto';
378
+ this.temperature = options.temperature ?? 0.7;
379
+ this.maxTokens = options.maxTokens ?? 4096;
380
+ this.topP = options.topP;
381
+ this.frequencyPenalty = options.frequencyPenalty;
382
+ this.presencePenalty = options.presencePenalty;
383
+ this.stop = options.stop;
384
+ this.timeout = options.timeout ?? 60000;
385
+ this.apiKey = options.apiKey;
386
+ this.baseUrl = options.baseUrl;
387
+ this.format = options.format;
388
+ this.extraHeaders = options.headers;
389
+ this.verbose = options.verbose ?? false;
390
+ // Initialize A3M Router
391
+ if (options.router) {
392
+ this.registry = options.router;
393
+ }
394
+ else {
395
+ this.registry = new registry_js_1.ProviderRegistry();
396
+ }
397
+ }
398
+ // ========================================================================
399
+ // LangChain compatibility: property getters
400
+ // ========================================================================
401
+ get model() {
402
+ return this.modelName;
403
+ }
404
+ get identifyingParams() {
405
+ return {
406
+ modelName: this.modelName,
407
+ temperature: this.temperature,
408
+ maxTokens: this.maxTokens,
409
+ };
410
+ }
411
+ get lc_aliases() {
412
+ return {
413
+ model: 'modelName',
414
+ };
415
+ }
416
+ // ========================================================================
417
+ // LangChain compatibility: serialize/deserialize
418
+ // ========================================================================
419
+ toJSON() {
420
+ return {
421
+ type: 'A3MChatModel',
422
+ kwargs: {
423
+ modelName: this.modelName,
424
+ temperature: this.temperature,
425
+ maxTokens: this.maxTokens,
426
+ },
427
+ };
428
+ }
429
+ static async deserialize(data) {
430
+ return new A3MChatModel(data.kwargs || {});
431
+ }
432
+ // ========================================================================
433
+ // Core: _generate (non-streaming)
434
+ // ========================================================================
435
+ async _generate(messages, options) {
436
+ const resolved = resolveProvider(this.modelName, this.registry);
437
+ if (!resolved) {
438
+ throw new Error(`A3M Router: No provider available for model "${this.modelName}". ` +
439
+ `Set API keys via environment variables (e.g., GROQ_API_KEY, OPENAI_API_KEY).`);
440
+ }
441
+ const { providerDef, model, providerName } = resolved;
442
+ const requestConfig = buildRequestConfig(providerDef, this.apiKey, this.extraHeaders);
443
+ const openaiMessages = langchainMessagesToOpenAI(messages);
444
+ const tools = options?.tools || this.boundTools;
445
+ const body = buildRequestBody(providerDef, model, openaiMessages, {
446
+ temperature: this.temperature,
447
+ maxTokens: this.maxTokens,
448
+ topP: this.topP,
449
+ frequencyPenalty: this.frequencyPenalty,
450
+ presencePenalty: this.presencePenalty,
451
+ stop: this.stop,
452
+ tools: tools.length > 0 ? tools : undefined,
453
+ stream: false,
454
+ });
455
+ if (this.verbose) {
456
+ console.log(`[A3M Router] ${providerName}/${model} -> ${requestConfig.url}`);
457
+ }
458
+ const data = await makeRequest(requestConfig.url, requestConfig.headers, body, this.timeout);
459
+ const parsed = parseResponse(providerDef, data);
460
+ // Build AIMessage-compatible response
461
+ const generationInfo = {
462
+ model: parsed.model,
463
+ provider: providerName,
464
+ };
465
+ if (parsed.usage) {
466
+ generationInfo.tokenUsage = {
467
+ promptTokens: parsed.usage.input_tokens,
468
+ completionTokens: parsed.usage.output_tokens,
469
+ totalTokens: parsed.usage.input_tokens + parsed.usage.output_tokens,
470
+ };
471
+ }
472
+ const aiMessage = {
473
+ content: parsed.content,
474
+ additional_kwargs: {},
475
+ response_metadata: generationInfo,
476
+ };
477
+ if (parsed.toolCalls && parsed.toolCalls.length > 0) {
478
+ aiMessage.tool_calls = parsed.toolCalls.map((tc) => ({
479
+ id: tc.id,
480
+ name: tc.function.name,
481
+ args: JSON.parse(tc.function.arguments),
482
+ }));
483
+ aiMessage.additional_kwargs.tool_calls = parsed.toolCalls;
484
+ }
485
+ return {
486
+ generations: [{
487
+ text: parsed.content,
488
+ message: aiMessage,
489
+ generationInfo,
490
+ }],
491
+ llmOutput: generationInfo,
492
+ };
493
+ }
494
+ // ========================================================================
495
+ // Core: invoke (high-level)
496
+ // ========================================================================
497
+ async invoke(input, options) {
498
+ const messages = typeof input === 'string'
499
+ ? [{ _getType: () => 'human', content: input }]
500
+ : input;
501
+ const result = await this._generate(messages, options);
502
+ return result.generations[0]?.message;
503
+ }
504
+ // ========================================================================
505
+ // Core: _streamResponseChunks (streaming)
506
+ // ========================================================================
507
+ async *_streamResponseChunks(messages, options) {
508
+ const resolved = resolveProvider(this.modelName, this.registry);
509
+ if (!resolved) {
510
+ throw new Error(`A3M Router: No provider available for model "${this.modelName}".`);
511
+ }
512
+ const { providerDef, model, providerName } = resolved;
513
+ const requestConfig = buildRequestConfig(providerDef, this.apiKey, this.extraHeaders);
514
+ const openaiMessages = langchainMessagesToOpenAI(messages);
515
+ const tools = options?.tools || this.boundTools;
516
+ const body = buildRequestBody(providerDef, model, openaiMessages, {
517
+ temperature: this.temperature,
518
+ maxTokens: this.maxTokens,
519
+ topP: this.topP,
520
+ frequencyPenalty: this.frequencyPenalty,
521
+ presencePenalty: this.presencePenalty,
522
+ stop: this.stop,
523
+ tools: tools.length > 0 ? tools : undefined,
524
+ stream: true,
525
+ });
526
+ if (this.verbose) {
527
+ console.log(`[A3M Router] streaming ${providerName}/${model} -> ${requestConfig.url}`);
528
+ }
529
+ const stream = makeStreamingRequest(requestConfig.url, requestConfig.headers, body, this.timeout * 2);
530
+ for await (const chunk of stream) {
531
+ try {
532
+ const parsed = JSON.parse(chunk);
533
+ // OpenAI streaming format
534
+ const delta = parsed.choices?.[0]?.delta;
535
+ if (delta) {
536
+ const content = delta.content || '';
537
+ const toolCalls = delta.tool_calls;
538
+ const messageChunk = {
539
+ content,
540
+ additional_kwargs: {},
541
+ };
542
+ if (toolCalls) {
543
+ messageChunk.tool_call_chunks = toolCalls.map((tc) => ({
544
+ id: tc.id || '',
545
+ name: tc.function?.name || '',
546
+ args: tc.function?.arguments || '',
547
+ }));
548
+ }
549
+ yield {
550
+ text: content,
551
+ message: messageChunk,
552
+ generationInfo: {
553
+ model: parsed.model || model,
554
+ provider: providerName,
555
+ },
556
+ };
557
+ }
558
+ }
559
+ catch {
560
+ // Skip malformed SSE chunks
561
+ }
562
+ }
563
+ }
564
+ // ========================================================================
565
+ // Core: stream (high-level)
566
+ // ========================================================================
567
+ async *stream(input, options) {
568
+ const messages = typeof input === 'string'
569
+ ? [{ _getType: () => 'human', content: input }]
570
+ : input;
571
+ yield* this._streamResponseChunks(messages, options);
572
+ }
573
+ // ========================================================================
574
+ // Tool calling support
575
+ // ========================================================================
576
+ bindTools(tools) {
577
+ const clone = new A3MChatModel({
578
+ router: this.registry,
579
+ modelName: this.modelName,
580
+ temperature: this.temperature,
581
+ maxTokens: this.maxTokens,
582
+ topP: this.topP,
583
+ frequencyPenalty: this.frequencyPenalty,
584
+ presencePenalty: this.presencePenalty,
585
+ stop: this.stop,
586
+ timeout: this.timeout,
587
+ apiKey: this.apiKey,
588
+ baseUrl: this.baseUrl,
589
+ format: this.format,
590
+ headers: this.extraHeaders,
591
+ verbose: this.verbose,
592
+ });
593
+ clone.boundTools = tools;
594
+ return clone;
595
+ }
596
+ // ========================================================================
597
+ // Structured output support
598
+ // ========================================================================
599
+ withStructuredOutput(schema, config) {
600
+ // Build a wrapper that parses the response as structured JSON
601
+ const self = this;
602
+ const schemaDef = typeof schema === 'object' && 'schema' in schema
603
+ ? schema.schema
604
+ : schema;
605
+ const toolDef = {
606
+ type: 'function',
607
+ function: {
608
+ name: 'structured_output',
609
+ description: 'Structured output',
610
+ parameters: schemaDef,
611
+ },
612
+ };
613
+ const bound = this.bindTools([toolDef]);
614
+ return {
615
+ async invoke(input) {
616
+ const result = await bound.invoke(input);
617
+ // Try to parse tool call args, fall back to content JSON parse
618
+ if (result?.tool_calls?.[0]?.args) {
619
+ return result.tool_calls[0].args;
620
+ }
621
+ if (result?.additional_kwargs?.tool_calls?.[0]?.function?.arguments) {
622
+ return JSON.parse(result.additional_kwargs.tool_calls[0].function.arguments);
623
+ }
624
+ if (typeof result?.content === 'string') {
625
+ try {
626
+ const jsonMatch = result.content.match(/```json\n([\s\S]*?)\n```/) ||
627
+ result.content.match(/\{[\s\S]*\}/);
628
+ if (jsonMatch) {
629
+ return JSON.parse(jsonMatch[0].replace(/```json\n?/g, '').replace(/\n?```/g, ''));
630
+ }
631
+ }
632
+ catch {
633
+ // Fall through
634
+ }
635
+ }
636
+ throw new Error('A3M Router: Failed to parse structured output from response');
637
+ },
638
+ };
639
+ }
640
+ // ========================================================================
641
+ // Provider management
642
+ // ========================================================================
643
+ /**
644
+ * Get the list of available providers
645
+ */
646
+ getAvailableProviders() {
647
+ return (0, providerConfig_js_1.getAvailableProviders)();
648
+ }
649
+ /**
650
+ * Get provider status from the registry
651
+ */
652
+ getProviderStatus() {
653
+ return this.registry.getStatus();
654
+ }
655
+ /**
656
+ * Switch the model at runtime
657
+ */
658
+ withModel(modelName) {
659
+ const clone = new A3MChatModel({
660
+ router: this.registry,
661
+ modelName,
662
+ temperature: this.temperature,
663
+ maxTokens: this.maxTokens,
664
+ topP: this.topP,
665
+ frequencyPenalty: this.frequencyPenalty,
666
+ presencePenalty: this.presencePenalty,
667
+ stop: this.stop,
668
+ timeout: this.timeout,
669
+ apiKey: this.apiKey,
670
+ baseUrl: this.baseUrl,
671
+ format: this.format,
672
+ headers: this.extraHeaders,
673
+ verbose: this.verbose,
674
+ });
675
+ clone.boundTools = [...this.boundTools];
676
+ return clone;
677
+ }
678
+ /**
679
+ * Set temperature
680
+ */
681
+ withTemperature(temperature) {
682
+ const clone = new A3MChatModel({
683
+ router: this.registry,
684
+ modelName: this.modelName,
685
+ temperature,
686
+ maxTokens: this.maxTokens,
687
+ topP: this.topP,
688
+ frequencyPenalty: this.frequencyPenalty,
689
+ presencePenalty: this.presencePenalty,
690
+ stop: this.stop,
691
+ timeout: this.timeout,
692
+ apiKey: this.apiKey,
693
+ baseUrl: this.baseUrl,
694
+ format: this.format,
695
+ headers: this.extraHeaders,
696
+ verbose: this.verbose,
697
+ });
698
+ clone.boundTools = [...this.boundTools];
699
+ return clone;
700
+ }
701
+ // ========================================================================
702
+ // LangChain event system compatibility
703
+ // ========================================================================
704
+ /** LLM type identifier for LangChain compatibility */
705
+ _llmType() {
706
+ return 'a3m-router';
707
+ }
708
+ }
709
+ exports.A3MChatModel = A3MChatModel;
710
+ // ============================================================
711
+ // FACTORY HELPERS
712
+ // ============================================================
713
+ /**
714
+ * Create an A3MChatModel pre-configured for a specific provider
715
+ */
716
+ function createA3MChatModel(provider, options = {}) {
717
+ return new A3MChatModel({
718
+ ...options,
719
+ modelName: provider,
720
+ });
721
+ }
722
+ /**
723
+ * Create an A3MChatModel with automatic routing
724
+ */
725
+ function createAutoRoutingChatModel(options = {}) {
726
+ return new A3MChatModel({
727
+ ...options,
728
+ modelName: 'auto',
729
+ });
730
+ }
731
+ //# sourceMappingURL=langchainAdapter.js.map