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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
- package/.github/workflows/npm-stats-validation.yml +152 -0
- package/.github/workflows/pages.yml +37 -0
- package/CHANGELOG.md +122 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +110 -0
- package/LAUNCH-PAIN-DRIVEN.md +339 -0
- package/LAUNCH.md +575 -0
- package/POPULARITY_BOOSTERS.md +285 -0
- package/README.md +231 -300
- package/SECURITY.md +69 -0
- package/articles/CONTENT_STRUCTURE.md +292 -0
- package/articles/DEVTO_COST_GUIDE.md +473 -0
- package/articles/DEVTO_FINAL.md +416 -0
- package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
- package/articles/HN_10X_BETTER.md +430 -0
- package/articles/HN_CHINESE_STYLE.md +308 -0
- package/articles/HN_FINAL.md +199 -0
- package/articles/HN_POSTED_VERSION.md +56 -0
- package/articles/HN_RESEARCH.md +364 -0
- package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
- package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
- package/articles/PAIN-DRIVEN-devto.md +242 -0
- package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
- package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
- package/articles/PAIN-DRIVEN-hackernews.md +131 -0
- package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
- package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
- package/articles/PAIN-DRIVEN-reddit.md +218 -0
- package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
- package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
- package/articles/PAIN-DRIVEN-twitter.md +120 -0
- package/articles/PORTKEY_VS_A3M.md +147 -0
- package/articles/REDDIT_FINAL.md +232 -0
- package/articles/TWITTER_FINAL.md +167 -0
- package/articles/WHY_10X_BETTER.md +261 -0
- package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
- package/articles/ai-discoverability-llm-routing.md +210 -0
- package/articles/devto-llm-routing.md +109 -0
- package/articles/hackernews-show-hn.md +65 -0
- package/articles/hashnode-llm-cost-optimization.md +125 -0
- package/articles/medium-building-llm-router.md +205 -0
- package/articles/reddit-ml.md +86 -0
- package/articles/twitter-thread-cost-savings.md +98 -0
- package/articles/youtube-tutorial-script.md +262 -0
- package/assets/banner.svg +109 -0
- package/assets/logo.svg +68 -0
- package/assets/social-preview.svg +64 -0
- package/demo/demo-script.md +53 -0
- package/dist/analytics/costAnalytics.d.ts +77 -0
- package/dist/analytics/costAnalytics.d.ts.map +1 -0
- package/dist/analytics/costAnalytics.js +219 -0
- package/dist/analytics/costAnalytics.js.map +1 -0
- package/dist/cache/semanticCache.d.ts +62 -0
- package/dist/cache/semanticCache.d.ts.map +1 -0
- package/dist/cache/semanticCache.js +176 -0
- package/dist/cache/semanticCache.js.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/geo/generativeEngineOptimization.js +321 -0
- package/dist/geo/geoRouter.js +387 -0
- package/dist/index.d.ts +4 -723
- package/dist/index.js +11 -344
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchainAdapter.d.ts +146 -0
- package/dist/integrations/langchainAdapter.d.ts.map +1 -0
- package/dist/integrations/langchainAdapter.js +731 -0
- package/dist/integrations/langchainAdapter.js.map +1 -0
- package/dist/integrations/oauth.d.ts +69 -0
- package/dist/integrations/oauth.d.ts.map +1 -0
- package/dist/integrations/oauth.js +225 -21
- package/dist/integrations/oauth.js.map +1 -0
- package/dist/memory/autoFetch.d.ts +39 -0
- package/dist/memory/autoFetch.d.ts.map +1 -0
- package/dist/memory/autoFetch.js +80 -88
- package/dist/memory/autoFetch.js.map +1 -0
- package/dist/memory/memoryTree.d.ts +76 -0
- package/dist/memory/memoryTree.d.ts.map +1 -0
- package/dist/memory/memoryTree.js +185 -130
- package/dist/memory/memoryTree.js.map +1 -0
- package/dist/memory/obsidianVault.d.ts +71 -0
- package/dist/memory/obsidianVault.d.ts.map +1 -0
- package/dist/memory/obsidianVault.js +207 -22
- package/dist/memory/obsidianVault.js.map +1 -0
- package/dist/providers/providerConfig.d.ts +49 -0
- package/dist/providers/providerConfig.d.ts.map +1 -0
- package/dist/providers/providerConfig.js +806 -401
- package/dist/providers/providerConfig.js.map +1 -0
- package/dist/security/guardrails.d.ts +76 -0
- package/dist/security/guardrails.d.ts.map +1 -0
- package/dist/security/guardrails.js +479 -0
- package/dist/security/guardrails.js.map +1 -0
- package/dist/security/inputValidation.js +351 -0
- package/dist/server/dashboard.d.ts +58 -0
- package/dist/server/dashboard.d.ts.map +1 -0
- package/dist/server/dashboard.js +553 -0
- package/dist/server/dashboard.js.map +1 -0
- package/dist/server/modelMapper.d.ts +43 -0
- package/dist/server/modelMapper.d.ts.map +1 -0
- package/dist/server/modelMapper.js +154 -0
- package/dist/server/modelMapper.js.map +1 -0
- package/dist/server/proxyServer.d.ts +41 -0
- package/dist/server/proxyServer.d.ts.map +1 -0
- package/dist/server/proxyServer.js +932 -0
- package/dist/server/proxyServer.js.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.js +268 -0
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
- package/docs/geo/GENERATIVE_ENGINE_OPTIMIZATION.md +232 -0
- package/docs-site/index.html +347 -0
- package/llms.txt +138 -0
- package/package.json +72 -7
- package/playground/README.md +51 -0
- package/playground/codesandbox.json +12 -0
- package/playground/index.js +39 -0
- package/scripts/update-npm-badges.js +158 -0
- package/src/analytics/costAnalytics.ts +304 -0
- package/src/cache/semanticCache.ts +221 -0
- package/src/index.ts +6 -0
- package/src/integrations/langchainAdapter.ts +955 -0
- package/src/providers/providerConfig.ts +923 -0
- package/src/security/guardrails.ts +585 -0
- package/src/server/dashboard.ts +610 -0
- package/src/server/modelMapper.ts +182 -0
- package/src/server/proxyServer.ts +1105 -0
- package/src/types/langchain.d.ts +83 -0
- 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
|
+
}
|