adaptive-memory-multi-model-router 2.2.9 → 2.4.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/README.md +81 -902
- package/package.json +1 -1
- package/src/skills/__tests__/skill_manager.test.ts +328 -0
- package/assets/benchmark-results.png +0 -0
- package/assets/complexity-scoring-v2.png +0 -0
- package/assets/complexity-scoring.png +0 -0
- package/assets/cost-comparison-chart.png +0 -0
- package/assets/cost-comparison-v2.png +0 -0
- package/assets/feature-comparison-v2.png +0 -0
- package/assets/feature-comparison-v3.png +0 -0
- package/assets/provider-health-chart.png +0 -0
- package/assets/provider-health-v2.png +0 -0
- package/assets/routing-flow-v2.png +0 -0
- package/assets/routing-flow-v3.png +0 -0
- package/assets/routing-flow.png +0 -0
- package/assets/tier-distribution.png +0 -0
- package/dist/cache/cacheKeyGenerator.d.ts +0 -67
- package/dist/cache/cacheKeyGenerator.d.ts.map +0 -1
- package/dist/cache/cacheKeyGenerator.js +0 -211
- package/dist/cache/cacheKeyGenerator.js.map +0 -1
- package/dist/cost/preCallCostEstimator.d.ts +0 -114
- package/dist/cost/preCallCostEstimator.d.ts.map +0 -1
- package/dist/cost/preCallCostEstimator.js +0 -256
- package/dist/cost/preCallCostEstimator.js.map +0 -1
- package/dist/inference/speculativeDecoding.d.ts +0 -133
- package/dist/inference/speculativeDecoding.d.ts.map +0 -1
- package/dist/inference/speculativeDecoding.js +0 -276
- package/dist/inference/speculativeDecoding.js.map +0 -1
- package/dist/providers/providerHealth.d.ts +0 -117
- package/dist/providers/providerHealth.d.ts.map +0 -1
- package/dist/providers/providerHealth.js +0 -309
- package/dist/providers/providerHealth.js.map +0 -1
- package/dist/routing/difficultyClassifier.d.ts +0 -79
- package/dist/routing/difficultyClassifier.d.ts.map +0 -1
- package/dist/routing/difficultyClassifier.js +0 -329
- package/dist/routing/difficultyClassifier.js.map +0 -1
- package/dist/sdk.d.ts +0 -125
- package/docs/HN_CAMPAIGN.md +0 -785
- package/src/cache/cacheKeyGenerator.ts +0 -242
- package/src/cost/preCallCostEstimator.ts +0 -345
- package/src/inference/speculativeDecoding.ts +0 -373
- package/src/providers/providerHealth.ts +0 -397
- package/src/routing/difficultyClassifier.ts +0 -420
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
6
|
"description": "LLM router & AI gateway with 99.5% routing accuracy — supports 47 providers including DeepSeek, Kimi (Moonshot), Qwen, Zhipu GLM, Yi, Baichuan, MiniMax, StepFun. Zero ML, 19.5KB. Multi-signal routing, semantic cache, guardrails, cost analytics. MIT. TypeScript SDK + Python SDK + OpenAI proxy.",
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for SkillManager and TMLEnhancedAgent
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
6
|
+
import { SkillManager, Skill } from '../skill_manager';
|
|
7
|
+
import { TMLEnhancedAgent } from '../../agents/skill_enhanced_agent';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
// Mock fs operations
|
|
12
|
+
jest.mock('fs');
|
|
13
|
+
|
|
14
|
+
describe('SkillManager', () => {
|
|
15
|
+
let skillManager: SkillManager;
|
|
16
|
+
const mockSkillsDir = '/mock/skills';
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
jest.clearAllMocks();
|
|
20
|
+
skillManager = new SkillManager(mockSkillsDir);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('initialization', () => {
|
|
24
|
+
it('should create skill manager instance', () => {
|
|
25
|
+
expect(skillManager).toBeInstanceOf(SkillManager);
|
|
26
|
+
expect(skillManager.skills_dir).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should have empty skills initially if directory does not exist', () => {
|
|
30
|
+
expect(skillManager.list_skills()).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('load_skills_metadata', () => {
|
|
35
|
+
it('should load skill metadata from SKILL.md files', () => {
|
|
36
|
+
// Mock directory listing and file reading
|
|
37
|
+
const mockSkillMD = `---
|
|
38
|
+
name: "Test Skill"
|
|
39
|
+
description: "A test skill for testing"
|
|
40
|
+
---
|
|
41
|
+
# Test Skill Content`;
|
|
42
|
+
|
|
43
|
+
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
44
|
+
jest.spyOn(Path.prototype, 'isDirectory').mockReturnValue(true);
|
|
45
|
+
jest.spyOn(Path.prototype, 'iterdir').mockReturnValue([
|
|
46
|
+
{ name: 'SKILL.md', isFile: () => true }
|
|
47
|
+
] as any);
|
|
48
|
+
jest.spyOn(fs, 'readFileSync').mockReturnValue(mockSkillMD);
|
|
49
|
+
|
|
50
|
+
skillManager.reload_skills();
|
|
51
|
+
|
|
52
|
+
expect(skillManager.list_skills()).toContain('Test Skill');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should skip directories without SKILL.md', () => {
|
|
56
|
+
jest.spyOn(Path.prototype, 'existsSync').mockReturnValue(false);
|
|
57
|
+
|
|
58
|
+
skillManager.reload_skills();
|
|
59
|
+
|
|
60
|
+
expect(skillManager.list_skills()).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('get_relevant_skills', () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
// Add mock skills
|
|
67
|
+
skillManager.skills['React Development'] = new Skill(
|
|
68
|
+
'React Development',
|
|
69
|
+
'Best practices for React components',
|
|
70
|
+
Path('/mock/react'),
|
|
71
|
+
{}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
skillManager.skills['Node.js API'] = new Skill(
|
|
75
|
+
'Node.js API',
|
|
76
|
+
'Building backend APIs with Node.js and Express',
|
|
77
|
+
Path('/mock/nodejs'),
|
|
78
|
+
{}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
skillManager.skills['Python Django'] = new Skill(
|
|
82
|
+
'Python Django',
|
|
83
|
+
'Django web framework for Python',
|
|
84
|
+
Path('/mock/python'),
|
|
85
|
+
{}
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should find skills with keyword matching', () => {
|
|
90
|
+
const relevant = skillManager.get_relevant_skills(
|
|
91
|
+
'Build a React component for user login',
|
|
92
|
+
2
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(relevant).toContain('React Development');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return skills ordered by relevance', () => {
|
|
99
|
+
const relevant = skillManager.get_relevant_skills(
|
|
100
|
+
'Create a React component with Node.js backend',
|
|
101
|
+
3
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// React should come first (exact match)
|
|
105
|
+
expect(relevant[0]).toBe('React Development');
|
|
106
|
+
expect(relevant).toContain('Node.js API');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should respect threshold parameter', () => {
|
|
110
|
+
const relevant = skillManager.get_relevant_skills(
|
|
111
|
+
'Build a Go microservice',
|
|
112
|
+
2,
|
|
113
|
+
0.5 // Higher threshold
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Should return fewer or no skills due to high threshold
|
|
117
|
+
expect(relevant.length).toBeLessThanOrEqual(2);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('load_skill', () => {
|
|
122
|
+
it('should load full skill content on first call', () => {
|
|
123
|
+
const mockSkill = skillManager.skills['React Development'];
|
|
124
|
+
mockSkill.content = null;
|
|
125
|
+
|
|
126
|
+
const mockContent = '# React Development\n\nBest practices...';
|
|
127
|
+
|
|
128
|
+
jest.spyOn(fs, 'readFileSync').mockReturnValue(
|
|
129
|
+
`---\nname: "React Development"\ndescription: "Best practices"\n---\n${mockContent}`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const loaded = skillManager.load_skill('React Development');
|
|
133
|
+
|
|
134
|
+
expect(loaded.content).toBe(mockContent);
|
|
135
|
+
expect(loaded.loaded_at).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return cached content on subsequent calls', () => {
|
|
139
|
+
const mockSkill = skillManager.skills['React Development'];
|
|
140
|
+
mockSkill.content = 'Cached content';
|
|
141
|
+
|
|
142
|
+
const loaded1 = skillManager.load_skill('React Development');
|
|
143
|
+
const loaded2 = skillManager.load_skill('React Development');
|
|
144
|
+
|
|
145
|
+
expect(loaded1).toBe(loaded2);
|
|
146
|
+
expect(loaded1.content).toBe('Cached content');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw error for non-existent skill', () => {
|
|
150
|
+
expect(() => {
|
|
151
|
+
skillManager.load_skill('Non-existent Skill');
|
|
152
|
+
}).toThrow("Skill 'Non-existent Skill' not found");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('validate_skill', () => {
|
|
157
|
+
it('should return validation results for existing skill', () => {
|
|
158
|
+
const mockSkill = skillManager.skills['React Development'];
|
|
159
|
+
|
|
160
|
+
jest.spyOn(skillManager, 'list_additional_files').mockReturnValue([]);
|
|
161
|
+
|
|
162
|
+
const validation = skillManager.validate_skill('React Development');
|
|
163
|
+
|
|
164
|
+
expect(validation).toHaveProperty('exists', true);
|
|
165
|
+
expect(validation).toHaveProperty('has_name', true);
|
|
166
|
+
expect(validation).toHaveProperty('has_description', true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return all false for non-existent skill', () => {
|
|
170
|
+
const validation = skillManager.validate_skill('Non-existent');
|
|
171
|
+
|
|
172
|
+
expect(validation.exists).toBe(false);
|
|
173
|
+
expect(validation.has_skill_md).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('TMLEnhancedAgent', () => {
|
|
179
|
+
let agent: TMLEnhancedAgent;
|
|
180
|
+
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
agent = new TMLEnhancedAgent(
|
|
183
|
+
'frontend-agent',
|
|
184
|
+
'anthropic',
|
|
185
|
+
'claude-sonnet-4',
|
|
186
|
+
'mock-skills',
|
|
187
|
+
['React Frontend Development', 'TypeScript Best Practices']
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('initialization', () => {
|
|
192
|
+
it('should create agent with configuration', () => {
|
|
193
|
+
expect(agent.agent_id).toBe('frontend-agent');
|
|
194
|
+
expect(agent.provider).toBe('anthropic');
|
|
195
|
+
expect(agent.model).toBe('claude-sonnet-4');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should initialize with assigned skills', () => {
|
|
199
|
+
expect(agent.assigned_skills).toContain('React Frontend Development');
|
|
200
|
+
expect(agent.assigned_skills).toContain('TypeScript Best Practices');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('execute_task', () => {
|
|
205
|
+
it('should execute task with relevant skills', async () => {
|
|
206
|
+
const task = {
|
|
207
|
+
description: 'Build a React login form component',
|
|
208
|
+
context: 'Must include email and password fields',
|
|
209
|
+
requirements: 'Use TypeScript and Material-UI'
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Mock skill loading
|
|
213
|
+
jest.spyOn(agent, '_get_relevant_skills').mockReturnValue([]);
|
|
214
|
+
|
|
215
|
+
// Mock LLM call
|
|
216
|
+
jest.spyOn(agent, '_execute_llm_call').mockReturnValue({
|
|
217
|
+
success: true,
|
|
218
|
+
output: 'React component code...',
|
|
219
|
+
tokens_used: 150,
|
|
220
|
+
cost: 0.015,
|
|
221
|
+
execution_time: 3.2
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = agent.execute_task(task);
|
|
225
|
+
|
|
226
|
+
expect(result.success).toBe(true);
|
|
227
|
+
expect(result.output).toBeDefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should remember successful patterns', () => {
|
|
231
|
+
const task = { description: 'Test task' };
|
|
232
|
+
|
|
233
|
+
jest.spyOn(agent, '_get_relevant_skills').mockReturnValue([]);
|
|
234
|
+
jest.spyOn(agent, '_execute_llm_call').mockReturnValue({
|
|
235
|
+
success: true,
|
|
236
|
+
output: 'Success'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
jest.spyOn(agent, '_remember_success_pattern').mockImplementation(() => {});
|
|
240
|
+
|
|
241
|
+
agent.execute_task(task);
|
|
242
|
+
|
|
243
|
+
expect(agent._remember_success_pattern).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('skill management', () => {
|
|
248
|
+
it('should add skill to agent', () => {
|
|
249
|
+
agent.add_skill('Jest Testing');
|
|
250
|
+
|
|
251
|
+
expect(agent.assigned_skills).toContain('Jest Testing');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should remove skill from agent', () => {
|
|
255
|
+
agent.remove_skill('TypeScript Best Practices');
|
|
256
|
+
|
|
257
|
+
expect(agent.assigned_skills).not.toContain('TypeScript Best Practices');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should list all available skills', () => {
|
|
261
|
+
jest.spyOn(agent.skill_manager, 'list_skills').mockReturnValue([
|
|
262
|
+
'Skill 1',
|
|
263
|
+
'Skill 2',
|
|
264
|
+
'Skill 3'
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const skills = agent.list_available_skills();
|
|
268
|
+
|
|
269
|
+
expect(skills).toHaveLength(3);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('serialization', () => {
|
|
274
|
+
it('should convert to dictionary', () => {
|
|
275
|
+
const dict = agent.to_dict();
|
|
276
|
+
|
|
277
|
+
expect(dict).toHaveProperty('agent_id', 'frontend-agent');
|
|
278
|
+
expect(dict).toHaveProperty('provider', 'anthropic');
|
|
279
|
+
expect(dict).toHaveProperty('model', 'claude-sonnet-4');
|
|
280
|
+
expect(dict).toHaveProperty('assigned_skills');
|
|
281
|
+
expect(dict).toHaveProperty('available_skills');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('TMLEnhancedAgentFactory', () => {
|
|
287
|
+
describe('create_from_config', () => {
|
|
288
|
+
it('should create agent from config', () => {
|
|
289
|
+
const config = {
|
|
290
|
+
id: 'test-agent',
|
|
291
|
+
provider: 'openai',
|
|
292
|
+
model: 'gpt-4-turbo',
|
|
293
|
+
skills_dir: 'test-skills',
|
|
294
|
+
skills: ['Test Skill']
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const agent = TMLEnhancedAgentFactory.create_from_config(config);
|
|
298
|
+
|
|
299
|
+
expect(agent).toBeInstanceOf(TMLEnhancedAgent);
|
|
300
|
+
expect(agent.agent_id).toBe('test-agent');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('create_multiple_from_config', () => {
|
|
305
|
+
it('should create multiple agents from config list', () => {
|
|
306
|
+
const configs = [
|
|
307
|
+
{
|
|
308
|
+
id: 'agent-1',
|
|
309
|
+
provider: 'anthropic',
|
|
310
|
+
model: 'claude-sonnet-4',
|
|
311
|
+
skills: ['Skill A']
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: 'agent-2',
|
|
315
|
+
provider: 'openai',
|
|
316
|
+
model: 'gpt-4-turbo',
|
|
317
|
+
skills: ['Skill B']
|
|
318
|
+
}
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
const agents = TMLEnhancedAgentFactory.create_multiple_from_config(configs);
|
|
322
|
+
|
|
323
|
+
expect(agents).toHaveLength(2);
|
|
324
|
+
expect(agents[0].agent_id).toBe('agent-1');
|
|
325
|
+
expect(agents[1].agent_id).toBe('agent-2');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/routing-flow.png
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A3M Router - Cross-Provider Cache Key Generator
|
|
3
|
-
*
|
|
4
|
-
* Normalizes prompts so same semantic content maps to same cache key
|
|
5
|
-
* regardless of provider-specific formatting, system prompts, etc.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const cacheKey = generateCacheKey("What is Python?", { provider: "openai" });
|
|
9
|
-
* const key2 = generateCacheKey("What is Python?", { provider: "anthropic" });
|
|
10
|
-
* // key === key2 (same semantic content = same key)
|
|
11
|
-
*/
|
|
12
|
-
export interface CacheKeyOptions {
|
|
13
|
-
/** Target provider (affects normalization rules) */
|
|
14
|
-
provider?: string;
|
|
15
|
-
/** Target model (for model-specific normalization) */
|
|
16
|
-
model?: string;
|
|
17
|
-
/** Whether to include system prompt in normalization */
|
|
18
|
-
includeSystemPrompt?: boolean;
|
|
19
|
-
/** Custom normalization rules */
|
|
20
|
-
customRules?: NormalizationRule[];
|
|
21
|
-
}
|
|
22
|
-
export interface NormalizationRule {
|
|
23
|
-
pattern: RegExp;
|
|
24
|
-
replacement: string;
|
|
25
|
-
}
|
|
26
|
-
export interface CacheKeyResult {
|
|
27
|
-
/** The normalized cache key string */
|
|
28
|
-
key: string;
|
|
29
|
-
/** Hash of the normalized content */
|
|
30
|
-
hash: string;
|
|
31
|
-
/** Metadata about what was normalized */
|
|
32
|
-
metadata: {
|
|
33
|
-
originalLength: number;
|
|
34
|
-
normalizedLength: number;
|
|
35
|
-
rulesApplied: number;
|
|
36
|
-
provider?: string;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Normalize text for cross-provider cache key generation.
|
|
41
|
-
* Removes provider-specific formatting while preserving semantic content.
|
|
42
|
-
*/
|
|
43
|
-
export declare function normalizeForCacheKey(text: string, options?: CacheKeyOptions): string;
|
|
44
|
-
/**
|
|
45
|
-
* Generate a deterministic cache key from a query.
|
|
46
|
-
* Same semantic content = same key across providers.
|
|
47
|
-
*/
|
|
48
|
-
export declare function generateCacheKey(query: string, options?: CacheKeyOptions): CacheKeyResult;
|
|
49
|
-
/**
|
|
50
|
-
* Add cross-provider cache key methods to existing SemanticCache.
|
|
51
|
-
* Call this to enhance the cache with provider-normalized lookups.
|
|
52
|
-
*/
|
|
53
|
-
export declare function createCacheKeyGenerator(defaultOptions?: CacheKeyOptions): {
|
|
54
|
-
generateKey: (query: string, options?: CacheKeyOptions) => CacheKeyResult;
|
|
55
|
-
createNormalizedMatcher: (cache: Map<string, any>) => (query: string, options?: CacheKeyOptions) => string | null;
|
|
56
|
-
};
|
|
57
|
-
/**
|
|
58
|
-
* Quick cache key generation (simplified API).
|
|
59
|
-
* Use this for simple cross-provider cache lookups.
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* const key1 = toCacheKey("What is Python?", "openai");
|
|
63
|
-
* const key2 = toCacheKey("What is Python?", "anthropic");
|
|
64
|
-
* console.log(key1 === key2); // true
|
|
65
|
-
*/
|
|
66
|
-
export declare function toCacheKey(query: string, provider?: string): string;
|
|
67
|
-
//# sourceMappingURL=cacheKeyGenerator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cacheKeyGenerator.d.ts","sourceRoot":"","sources":["../../src/cache/cacheKeyGenerator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,iCAAiC;IACjC,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE;QACR,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC;QACzB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AA2BD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC5B,MAAM,CA8CR;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,eAAoB,GAC5B,cAAc,CAwChB;AAMD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,CAAC,EAAE,eAAe,GAC/B;IACD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,cAAc,CAAC;IAC1E,uBAAuB,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,MAAM,GAAG,IAAI,CAAC;CACnH,CAkCA;AAMD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnE"}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* A3M Router - Cross-Provider Cache Key Generator
|
|
4
|
-
*
|
|
5
|
-
* Normalizes prompts so same semantic content maps to same cache key
|
|
6
|
-
* regardless of provider-specific formatting, system prompts, etc.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const cacheKey = generateCacheKey("What is Python?", { provider: "openai" });
|
|
10
|
-
* const key2 = generateCacheKey("What is Python?", { provider: "anthropic" });
|
|
11
|
-
* // key === key2 (same semantic content = same key)
|
|
12
|
-
*/
|
|
13
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
-
if (k2 === undefined) k2 = k;
|
|
15
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
-
}
|
|
19
|
-
Object.defineProperty(o, k2, desc);
|
|
20
|
-
}) : (function(o, m, k, k2) {
|
|
21
|
-
if (k2 === undefined) k2 = k;
|
|
22
|
-
o[k2] = m[k];
|
|
23
|
-
}));
|
|
24
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
-
}) : function(o, v) {
|
|
27
|
-
o["default"] = v;
|
|
28
|
-
});
|
|
29
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
-
var ownKeys = function(o) {
|
|
31
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
-
var ar = [];
|
|
33
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
-
return ar;
|
|
35
|
-
};
|
|
36
|
-
return ownKeys(o);
|
|
37
|
-
};
|
|
38
|
-
return function (mod) {
|
|
39
|
-
if (mod && mod.__esModule) return mod;
|
|
40
|
-
var result = {};
|
|
41
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
-
__setModuleDefault(result, mod);
|
|
43
|
-
return result;
|
|
44
|
-
};
|
|
45
|
-
})();
|
|
46
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.normalizeForCacheKey = normalizeForCacheKey;
|
|
48
|
-
exports.generateCacheKey = generateCacheKey;
|
|
49
|
-
exports.createCacheKeyGenerator = createCacheKeyGenerator;
|
|
50
|
-
exports.toCacheKey = toCacheKey;
|
|
51
|
-
const crypto = __importStar(require("crypto"));
|
|
52
|
-
// ============================================================
|
|
53
|
-
// Provider-specific system prompt patterns
|
|
54
|
-
// ============================================================
|
|
55
|
-
const PROVIDER_SYSTEM_PATTERNS = {
|
|
56
|
-
anthropic: [
|
|
57
|
-
/<anthropic_thinking>[\s\S]*?<\/anthropic_thinking>/gi,
|
|
58
|
-
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
59
|
-
/Human:/gi,
|
|
60
|
-
/Assistant:/gi,
|
|
61
|
-
],
|
|
62
|
-
openai: [
|
|
63
|
-
/<|im_start|>/gi,
|
|
64
|
-
/<|im_end|>/gi,
|
|
65
|
-
],
|
|
66
|
-
google: [
|
|
67
|
-
/<content>[\s\S]*?<\/content>/gi,
|
|
68
|
-
/[\Parts|thought]/gi,
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
// ============================================================
|
|
72
|
-
// Core normalizer
|
|
73
|
-
// ============================================================
|
|
74
|
-
/**
|
|
75
|
-
* Normalize text for cross-provider cache key generation.
|
|
76
|
-
* Removes provider-specific formatting while preserving semantic content.
|
|
77
|
-
*/
|
|
78
|
-
function normalizeForCacheKey(text, options = {}) {
|
|
79
|
-
let normalized = text;
|
|
80
|
-
// Step 1: Unicode normalization (NFC)
|
|
81
|
-
normalized = normalized.normalize('NFC');
|
|
82
|
-
// Step 2: Collapse whitespace
|
|
83
|
-
normalized = normalized.replace(/\s+/g, ' ');
|
|
84
|
-
// Step 3: Remove control characters
|
|
85
|
-
normalized = normalized.replace(/[\x00-\x1F\x7F]/g, '');
|
|
86
|
-
// Step 4: Strip provider-specific formatting
|
|
87
|
-
if (options.provider) {
|
|
88
|
-
const patterns = PROVIDER_SYSTEM_PATTERNS[options.provider] || [];
|
|
89
|
-
for (const pattern of patterns) {
|
|
90
|
-
normalized = normalized.replace(pattern, '');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Step 5: General system/assistant role removal
|
|
94
|
-
normalized = normalized
|
|
95
|
-
.replace(/\b(system|user|assistant|human|bot)\s*:/gi, '')
|
|
96
|
-
.replace(/^(system|user|assistant|human|bot)\s*/gim, '');
|
|
97
|
-
// Step 6: Remove markdown formatting (often provider-specific)
|
|
98
|
-
normalized = normalized
|
|
99
|
-
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK]') // Preserve code block indicator
|
|
100
|
-
.replace(/`([^`]+)`/g, '$1') // Inline code content
|
|
101
|
-
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
|
|
102
|
-
.replace(/_([^_]+)_/g, '$1') // Italic
|
|
103
|
-
.replace(/#+\s*/g, '') // Headers
|
|
104
|
-
.replace(/^\s*[-*+]\s+/gm, '') // List bullets
|
|
105
|
-
.replace(/^\s*\d+\.\s+/gm, ''); // Numbered lists
|
|
106
|
-
// Step 7: Apply custom rules
|
|
107
|
-
if (options.customRules) {
|
|
108
|
-
for (const rule of options.customRules) {
|
|
109
|
-
normalized = normalized.replace(rule.pattern, rule.replacement);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Step 8: Collapse whitespace again after removals
|
|
113
|
-
normalized = normalized.replace(/\s+/g, ' ').trim();
|
|
114
|
-
return normalized;
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Generate a deterministic cache key from a query.
|
|
118
|
-
* Same semantic content = same key across providers.
|
|
119
|
-
*/
|
|
120
|
-
function generateCacheKey(query, options = {}) {
|
|
121
|
-
const originalLength = query.length;
|
|
122
|
-
// Normalize the query
|
|
123
|
-
let normalized = normalizeForCacheKey(query, {
|
|
124
|
-
...options,
|
|
125
|
-
includeSystemPrompt: false, // Always exclude for user query matching
|
|
126
|
-
});
|
|
127
|
-
// Count rules that were applied (approximate)
|
|
128
|
-
let rulesApplied = 3; // Base normalizations
|
|
129
|
-
if (options.provider)
|
|
130
|
-
rulesApplied += 2;
|
|
131
|
-
if (options.customRules)
|
|
132
|
-
rulesApplied += options.customRules.length;
|
|
133
|
-
// Generate hash
|
|
134
|
-
const hash = crypto
|
|
135
|
-
.createHash('sha256')
|
|
136
|
-
.update(normalized)
|
|
137
|
-
.digest('hex')
|
|
138
|
-
.substring(0, 16); // First 16 chars = 64-bit key
|
|
139
|
-
// Final key format: v1:{hash}:{provider?[:model]?}
|
|
140
|
-
let key = `v1:${hash}`;
|
|
141
|
-
if (options.provider) {
|
|
142
|
-
key += `:${options.provider}`;
|
|
143
|
-
if (options.model) {
|
|
144
|
-
key += `:${options.model}`;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
key,
|
|
149
|
-
hash,
|
|
150
|
-
metadata: {
|
|
151
|
-
originalLength,
|
|
152
|
-
normalizedLength: normalized.length,
|
|
153
|
-
rulesApplied,
|
|
154
|
-
provider: options.provider,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
// ============================================================
|
|
159
|
-
// SemanticCache enhancement
|
|
160
|
-
// ============================================================
|
|
161
|
-
/**
|
|
162
|
-
* Add cross-provider cache key methods to existing SemanticCache.
|
|
163
|
-
* Call this to enhance the cache with provider-normalized lookups.
|
|
164
|
-
*/
|
|
165
|
-
function createCacheKeyGenerator(defaultOptions) {
|
|
166
|
-
return {
|
|
167
|
-
/**
|
|
168
|
-
* Generate a cache key for a query.
|
|
169
|
-
*/
|
|
170
|
-
generateKey: (query, options) => {
|
|
171
|
-
return generateCacheKey(query, { ...defaultOptions, ...options });
|
|
172
|
-
},
|
|
173
|
-
/**
|
|
174
|
-
* Create a matcher function that finds existing cache entries
|
|
175
|
-
* by comparing normalized keys.
|
|
176
|
-
*/
|
|
177
|
-
createNormalizedMatcher: (cache) => {
|
|
178
|
-
return (query, options) => {
|
|
179
|
-
const { key } = generateCacheKey(query, { ...defaultOptions, ...options });
|
|
180
|
-
// Check exact match
|
|
181
|
-
if (cache.has(key)) {
|
|
182
|
-
return key;
|
|
183
|
-
}
|
|
184
|
-
// Check hash-only match (v1:{hash} prefix)
|
|
185
|
-
const hashPrefix = key.split(':').slice(0, 2).join(':');
|
|
186
|
-
for (const cachedKey of cache.keys()) {
|
|
187
|
-
if (cachedKey.startsWith(hashPrefix + ':')) {
|
|
188
|
-
return cachedKey;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
};
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
// ============================================================
|
|
197
|
-
// Convenience exports
|
|
198
|
-
// ============================================================
|
|
199
|
-
/**
|
|
200
|
-
* Quick cache key generation (simplified API).
|
|
201
|
-
* Use this for simple cross-provider cache lookups.
|
|
202
|
-
*
|
|
203
|
-
* @example
|
|
204
|
-
* const key1 = toCacheKey("What is Python?", "openai");
|
|
205
|
-
* const key2 = toCacheKey("What is Python?", "anthropic");
|
|
206
|
-
* console.log(key1 === key2); // true
|
|
207
|
-
*/
|
|
208
|
-
function toCacheKey(query, provider) {
|
|
209
|
-
return generateCacheKey(query, { provider }).key;
|
|
210
|
-
}
|
|
211
|
-
//# sourceMappingURL=cacheKeyGenerator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cacheKeyGenerator.js","sourceRoot":"","sources":["../../src/cache/cacheKeyGenerator.ts"],"names":[],"mappings":"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"}
|