adaptive-memory-multi-model-router 2.2.4 → 2.2.6
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 +17 -22
- package/README.md.bak +836 -0
- package/dist/analytics/costAnalytics.d.ts +1 -0
- package/dist/cache/cacheKeyGenerator.d.ts +67 -0
- package/dist/cache/cacheKeyGenerator.d.ts.map +1 -0
- package/dist/cache/cacheKeyGenerator.js +211 -0
- package/dist/cache/cacheKeyGenerator.js.map +1 -0
- package/dist/cache/semanticCache.d.ts +41 -0
- package/dist/cache/semanticCache.d.ts.map +1 -1
- package/dist/cache/semanticCache.js +142 -0
- package/dist/cache/semanticCache.js.map +1 -1
- package/dist/cli.js +35 -478
- package/dist/cost/costTracker.js +0 -3
- package/dist/cost/preCallCostEstimator.d.ts +114 -0
- package/dist/cost/preCallCostEstimator.d.ts.map +1 -0
- package/dist/cost/preCallCostEstimator.js +256 -0
- package/dist/cost/preCallCostEstimator.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +264 -64
- package/dist/index.js.map +1 -1
- package/dist/inference/speculativeDecoding.d.ts +133 -0
- package/dist/inference/speculativeDecoding.d.ts.map +1 -0
- package/dist/inference/speculativeDecoding.js +276 -0
- package/dist/inference/speculativeDecoding.js.map +1 -0
- package/dist/integrations/langchainAdapter.d.ts +1 -0
- package/dist/integrations/oauth.d.ts +1 -0
- package/dist/memory/autoFetch.d.ts +1 -0
- package/dist/memory/memoryTree.d.ts +1 -0
- package/dist/memory/obsidianVault.d.ts +1 -0
- package/dist/providers/providerConfig.d.ts +1 -0
- package/dist/providers/providerConfig.js +2 -0
- package/dist/providers/providerHealth.d.ts +117 -0
- package/dist/providers/providerHealth.d.ts.map +1 -0
- package/dist/providers/providerHealth.js +309 -0
- package/dist/providers/providerHealth.js.map +1 -0
- package/dist/providers/registry.js +126 -128
- package/dist/routing/advancedRouter.js +310 -427
- package/dist/routing/difficultyClassifier.d.ts +79 -0
- package/dist/routing/difficultyClassifier.d.ts.map +1 -0
- package/dist/routing/difficultyClassifier.js +329 -0
- package/dist/routing/difficultyClassifier.js.map +1 -0
- package/dist/sdk.d.ts +125 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +109 -100
- package/dist/sdk.js.map +1 -0
- package/dist/security/guardrails.d.ts +1 -0
- package/dist/server/dashboard.d.ts +1 -0
- package/dist/server/modelMapper.d.ts +1 -0
- package/dist/server/proxyServer.d.ts +1 -0
- package/package.json +106 -3
- package/src/cache/cacheKeyGenerator.ts +242 -0
- package/src/cache/semanticCache.ts +148 -0
- package/src/cost/preCallCostEstimator.ts +345 -0
- package/src/inference/speculativeDecoding.ts +373 -0
- package/src/providers/providerHealth.ts +397 -0
- package/src/routing/difficultyClassifier.ts +420 -0
- package/test/provider-test.js +2 -2
- package/test.js +7 -7
- package/test.js.bak +376 -0
- package/tsconfig.json +15 -5
- package/src/index.ts +0 -99
- package/src/skills/__tests__/skill_manager.test.ts +0 -328
package/dist/sdk.js
CHANGED
|
@@ -5,118 +5,127 @@
|
|
|
5
5
|
* Clean wrapper class providing a better DX than raw exports.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
|
-
*
|
|
8
|
+
* import { A3MRouter } from 'adaptive-memory-multi-model-router/sdk';
|
|
9
|
+
*
|
|
9
10
|
* const router = new A3MRouter();
|
|
11
|
+
*
|
|
12
|
+
* // Route a query (no execution, just model selection)
|
|
10
13
|
* const decision = router.route("What is 2+2?");
|
|
11
14
|
* console.log(decision.model, decision.tier, decision.cost);
|
|
15
|
+
*
|
|
16
|
+
* // Start the OpenAI-compatible proxy server
|
|
17
|
+
* const proxyURL = await router.serve(8787);
|
|
18
|
+
*
|
|
19
|
+
* // Use with any OpenAI SDK
|
|
20
|
+
* import OpenAI from 'openai';
|
|
21
|
+
* const client = new OpenAI({ baseURL: router.proxyURL });
|
|
22
|
+
* const response = await client.chat.completions.create({
|
|
23
|
+
* model: 'auto',
|
|
24
|
+
* messages: [{ role: 'user', content: 'Hello' }]
|
|
25
|
+
* });
|
|
12
26
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.A3MRouter = void 0;
|
|
29
|
+
exports.createSDK = createSDK;
|
|
30
|
+
const advancedRouter_1 = require("./routing/advancedRouter");
|
|
31
|
+
const proxyServer_1 = require("./server/proxyServer");
|
|
17
32
|
// ============================================================
|
|
18
33
|
// A3MRouter SDK Class
|
|
19
34
|
// ============================================================
|
|
20
|
-
|
|
21
35
|
class A3MRouter {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (complexity < 0.65) return 'mid';
|
|
107
|
-
return 'premium';
|
|
108
|
-
}
|
|
36
|
+
config;
|
|
37
|
+
_proxyURL = null;
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Route a query — returns model selection without executing it.
|
|
43
|
+
*
|
|
44
|
+
* @param query - The user prompt to route
|
|
45
|
+
* @returns Routing decision with model, tier, cost, complexity
|
|
46
|
+
*/
|
|
47
|
+
route(query) {
|
|
48
|
+
const features = (0, advancedRouter_1.extractQueryFeatures)(query);
|
|
49
|
+
const result = (0, advancedRouter_1.routeQuery)(query, this.config.providers);
|
|
50
|
+
return {
|
|
51
|
+
model: result.primary_model || 'unknown',
|
|
52
|
+
tier: this.classifyTier(features.complexity),
|
|
53
|
+
cost: result.estimated_cost || 0,
|
|
54
|
+
complexity: features.complexity,
|
|
55
|
+
reasoning: result.reasoning || '',
|
|
56
|
+
fallbackModels: result.fallback_models || [],
|
|
57
|
+
isFree: (result.estimated_cost || 0) === 0,
|
|
58
|
+
isExpert: features.complexity >= 0.65,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Route multiple queries in batch.
|
|
63
|
+
*
|
|
64
|
+
* @param queries - Array of user prompts
|
|
65
|
+
* @returns Array of routing decisions
|
|
66
|
+
*/
|
|
67
|
+
routeBatch(queries) {
|
|
68
|
+
(0, advancedRouter_1.routeBatch)(queries); // warm the internal cache
|
|
69
|
+
return queries.map((q) => this.route(q));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get model recommendation for a task description.
|
|
73
|
+
*
|
|
74
|
+
* @param task - Task description (e.g. "code generation", "summarization")
|
|
75
|
+
* @returns Routing decision
|
|
76
|
+
*/
|
|
77
|
+
recommend(task) {
|
|
78
|
+
(0, advancedRouter_1.recommendForTask)(task);
|
|
79
|
+
return this.route(task);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Start the OpenAI-compatible proxy server.
|
|
83
|
+
*
|
|
84
|
+
* @param port - Port to listen on (default: 8787)
|
|
85
|
+
* @returns The proxy base URL (e.g. "http://localhost:8787/v1")
|
|
86
|
+
*/
|
|
87
|
+
async serve(port = 8787) {
|
|
88
|
+
(0, proxyServer_1.createProxyServer)(port);
|
|
89
|
+
this._proxyURL = `http://localhost:${port}/v1`;
|
|
90
|
+
return this._proxyURL;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the proxy URL. Available after serve() is called,
|
|
94
|
+
* otherwise returns the default.
|
|
95
|
+
*/
|
|
96
|
+
get proxyURL() {
|
|
97
|
+
return this._proxyURL || 'http://localhost:8787/v1';
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Extract features from a query for debugging or analysis.
|
|
101
|
+
*
|
|
102
|
+
* @param query - The user prompt to analyze
|
|
103
|
+
* @returns Detailed feature breakdown
|
|
104
|
+
*/
|
|
105
|
+
analyze(query) {
|
|
106
|
+
return (0, advancedRouter_1.extractQueryFeatures)(query);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Classify a complexity score into a named tier.
|
|
110
|
+
*/
|
|
111
|
+
classifyTier(complexity) {
|
|
112
|
+
if (complexity < 0.20)
|
|
113
|
+
return 'free';
|
|
114
|
+
if (complexity < 0.45)
|
|
115
|
+
return 'cheap';
|
|
116
|
+
if (complexity < 0.65)
|
|
117
|
+
return 'mid';
|
|
118
|
+
return 'premium';
|
|
119
|
+
}
|
|
109
120
|
}
|
|
110
|
-
|
|
121
|
+
exports.A3MRouter = A3MRouter;
|
|
111
122
|
/**
|
|
112
123
|
* Convenience: create an A3MRouter instance.
|
|
113
124
|
*
|
|
114
|
-
* @param
|
|
115
|
-
* @returns
|
|
125
|
+
* @param config - Optional configuration
|
|
126
|
+
* @returns Configured A3MRouter instance
|
|
116
127
|
*/
|
|
117
128
|
function createSDK(config) {
|
|
118
|
-
|
|
129
|
+
return new A3MRouter(config);
|
|
119
130
|
}
|
|
120
|
-
|
|
121
|
-
module.exports = { A3MRouter, createSDK };
|
|
122
|
-
module.exports.default = A3MRouter;
|
|
131
|
+
//# sourceMappingURL=sdk.js.map
|
package/dist/sdk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../src/sdk.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAqKH,8BAEC;AArKD,6DAKkC;AAClC,sDAAyD;AAoDzD,+DAA+D;AAC/D,sBAAsB;AACtB,+DAA+D;AAE/D,MAAa,SAAS;IACZ,MAAM,CAAkB;IACxB,SAAS,GAAkB,IAAI,CAAC;IAExC,YAAY,SAA0B,EAAE;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAa;QACjB,MAAM,QAAQ,GAAG,IAAA,qCAAoB,EAAC,KAAK,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,IAAA,2BAAU,EAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAExD,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,aAAa,IAAI,SAAS;YACxC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC;YAC5C,IAAI,EAAE,MAAM,CAAC,cAAc,IAAI,CAAC;YAChC,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE;YACjC,cAAc,EAAE,MAAM,CAAC,eAAe,IAAI,EAAE;YAC5C,MAAM,EAAE,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,CAAC,KAAK,CAAC;YAC1C,QAAQ,EAAE,QAAQ,CAAC,UAAU,IAAI,IAAI;SACtC,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,UAAU,CAAC,OAAiB;QAC1B,IAAA,2BAAU,EAAC,OAAO,CAAC,CAAC,CAAC,0BAA0B;QAC/C,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,IAAY;QACpB,IAAA,iCAAgB,EAAC,IAAI,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK,CAAC,OAAe,IAAI;QAC7B,IAAA,+BAAiB,EAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,SAAS,GAAG,oBAAoB,IAAI,KAAK,CAAC;QAC/C,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;;;OAGG;IACH,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,SAAS,IAAI,0BAA0B,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,KAAa;QACnB,OAAO,IAAA,qCAAoB,EAAC,KAAK,CAAC,CAAC;IACrC,CAAC;IAED;;OAEG;IACK,YAAY,CAClB,UAAkB;QAElB,IAAI,UAAU,GAAG,IAAI;YAAE,OAAO,MAAM,CAAC;QACrC,IAAI,UAAU,GAAG,IAAI;YAAE,OAAO,OAAO,CAAC;QACtC,IAAI,UAAU,GAAG,IAAI;YAAE,OAAO,KAAK,CAAC;QACpC,OAAO,SAAS,CAAC;IACnB,CAAC;CACF;AA7FD,8BA6FC;AAED;;;;;GAKG;AACH,SAAgB,SAAS,CAAC,MAAwB;IAChD,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -56,3 +56,4 @@ export declare function registerProvider(id: string, name: string): void;
|
|
|
56
56
|
*/
|
|
57
57
|
export declare function handleDashboardRequest(req: http.IncomingMessage, res: http.ServerResponse): boolean;
|
|
58
58
|
export declare function getDashboardHTML(): string;
|
|
59
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
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.",
|
|
@@ -354,7 +354,108 @@
|
|
|
354
354
|
"pi-coding-agent",
|
|
355
355
|
"pi-agent",
|
|
356
356
|
"agent-discoverable",
|
|
357
|
-
"ai-native"
|
|
357
|
+
"ai-native",
|
|
358
|
+
"01-ai",
|
|
359
|
+
"01ai",
|
|
360
|
+
"128k上下文",
|
|
361
|
+
"ai-gateway-cn",
|
|
362
|
+
"ai路由",
|
|
363
|
+
"aliyun",
|
|
364
|
+
"baichuan-ai",
|
|
365
|
+
"baichuan2",
|
|
366
|
+
"baichuan2-flash",
|
|
367
|
+
"baiducloud",
|
|
368
|
+
"chinese-ai-proxy",
|
|
369
|
+
"chinese-api-gateway",
|
|
370
|
+
"chinese-chatgpt",
|
|
371
|
+
"chinese-language-model",
|
|
372
|
+
"chinese-llm-proxy",
|
|
373
|
+
"chinese-models",
|
|
374
|
+
"chinese-nlp",
|
|
375
|
+
"chinesellm",
|
|
376
|
+
"chinesellm-routing",
|
|
377
|
+
"deepseek-ai",
|
|
378
|
+
"deepseek-api",
|
|
379
|
+
"deepseek-chat",
|
|
380
|
+
"deepseek-coder",
|
|
381
|
+
"deepseek-reasoner",
|
|
382
|
+
"embedding-model",
|
|
383
|
+
"glm-4",
|
|
384
|
+
"glm-4-air",
|
|
385
|
+
"glm-4-flash",
|
|
386
|
+
"glm-4-long",
|
|
387
|
+
"glm-4-plus",
|
|
388
|
+
"huawei-cloud",
|
|
389
|
+
"langchain-cn",
|
|
390
|
+
"lingyi",
|
|
391
|
+
"lingyi-wanwu",
|
|
392
|
+
"llamaindex-cn",
|
|
393
|
+
"llm-gateway-cn",
|
|
394
|
+
"long-context-model",
|
|
395
|
+
"minimax-ai",
|
|
396
|
+
"minimax-api",
|
|
397
|
+
"minimax-chat",
|
|
398
|
+
"minimax-turbo",
|
|
399
|
+
"moonshot-ai",
|
|
400
|
+
"moonshot-v1",
|
|
401
|
+
"moonshot-v1-128k",
|
|
402
|
+
"moonshot-v1-32k",
|
|
403
|
+
"moonshot-v1-8k",
|
|
404
|
+
"multimodal",
|
|
405
|
+
"qianwen",
|
|
406
|
+
"qwen-long",
|
|
407
|
+
"qwen-max",
|
|
408
|
+
"qwen-plus",
|
|
409
|
+
"qwen-turbo",
|
|
410
|
+
"qwen2",
|
|
411
|
+
"qwen3",
|
|
412
|
+
"step-1",
|
|
413
|
+
"step-1v",
|
|
414
|
+
"step-2",
|
|
415
|
+
"tencentcloud",
|
|
416
|
+
"tongji",
|
|
417
|
+
"tongyi",
|
|
418
|
+
"tongyi-qianwen",
|
|
419
|
+
"vision-model",
|
|
420
|
+
"volcengine",
|
|
421
|
+
"wu-yuan",
|
|
422
|
+
"wuyuan",
|
|
423
|
+
"yi-ai",
|
|
424
|
+
"yi-large",
|
|
425
|
+
"yi-lightning",
|
|
426
|
+
"yi-medium",
|
|
427
|
+
"zai-glm",
|
|
428
|
+
"zai-glm-4",
|
|
429
|
+
"zhipu-ai",
|
|
430
|
+
"中国ai",
|
|
431
|
+
"中国大模型",
|
|
432
|
+
"中文chatgpt",
|
|
433
|
+
"中文embedding",
|
|
434
|
+
"中文langchain",
|
|
435
|
+
"中文llamaindex",
|
|
436
|
+
"中文nlp",
|
|
437
|
+
"中文seo",
|
|
438
|
+
"中文多模态",
|
|
439
|
+
"中文大模型",
|
|
440
|
+
"中文搜索引擎优化",
|
|
441
|
+
"中文模型",
|
|
442
|
+
"华为云",
|
|
443
|
+
"向量化",
|
|
444
|
+
"国产ai",
|
|
445
|
+
"国产大模型",
|
|
446
|
+
"多模型路由",
|
|
447
|
+
"多模态",
|
|
448
|
+
"大模型路由",
|
|
449
|
+
"字节ai",
|
|
450
|
+
"智能路由",
|
|
451
|
+
"火山引擎",
|
|
452
|
+
"百度ai",
|
|
453
|
+
"百度云",
|
|
454
|
+
"腾讯ai",
|
|
455
|
+
"腾讯云",
|
|
456
|
+
"长上下文",
|
|
457
|
+
"阿里ai",
|
|
458
|
+
"阿里云"
|
|
358
459
|
],
|
|
359
460
|
"author": "Das-rebel <subho@example.com>",
|
|
360
461
|
"license": "MIT",
|
|
@@ -370,7 +471,8 @@
|
|
|
370
471
|
"test": "node test.js && node test/provider-test.js",
|
|
371
472
|
"test:providers": "node test/provider-test.js",
|
|
372
473
|
"benchmark": "node test/benchmark.js",
|
|
373
|
-
"benchmark:verbose": "node test/benchmark.js --verbose"
|
|
474
|
+
"benchmark:verbose": "node test/benchmark.js --verbose",
|
|
475
|
+
"build": "tsc"
|
|
374
476
|
},
|
|
375
477
|
"engines": {
|
|
376
478
|
"node": ">=18.0.0"
|
|
@@ -388,6 +490,7 @@
|
|
|
388
490
|
},
|
|
389
491
|
"devDependencies": {
|
|
390
492
|
"@types/node": "^25.8.0",
|
|
493
|
+
"tsx": "^4.22.3",
|
|
391
494
|
"typescript": "^6.0.3"
|
|
392
495
|
}
|
|
393
496
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
export interface CacheKeyOptions {
|
|
20
|
+
/** Target provider (affects normalization rules) */
|
|
21
|
+
provider?: string;
|
|
22
|
+
/** Target model (for model-specific normalization) */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Whether to include system prompt in normalization */
|
|
25
|
+
includeSystemPrompt?: boolean;
|
|
26
|
+
/** Custom normalization rules */
|
|
27
|
+
customRules?: NormalizationRule[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NormalizationRule {
|
|
31
|
+
pattern: RegExp;
|
|
32
|
+
replacement: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CacheKeyResult {
|
|
36
|
+
/** The normalized cache key string */
|
|
37
|
+
key: string;
|
|
38
|
+
/** Hash of the normalized content */
|
|
39
|
+
hash: string;
|
|
40
|
+
/** Metadata about what was normalized */
|
|
41
|
+
metadata: {
|
|
42
|
+
originalLength: number;
|
|
43
|
+
normalizedLength: number;
|
|
44
|
+
rulesApplied: number;
|
|
45
|
+
provider?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================
|
|
50
|
+
// Provider-specific system prompt patterns
|
|
51
|
+
// ============================================================
|
|
52
|
+
|
|
53
|
+
const PROVIDER_SYSTEM_PATTERNS: Record<string, RegExp[]> = {
|
|
54
|
+
anthropic: [
|
|
55
|
+
/<anthropic_thinking>[\s\S]*?<\/anthropic_thinking>/gi,
|
|
56
|
+
/<thinking>[\s\S]*?<\/thinking>/gi,
|
|
57
|
+
/Human:/gi,
|
|
58
|
+
/Assistant:/gi,
|
|
59
|
+
],
|
|
60
|
+
openai: [
|
|
61
|
+
/<|im_start|>/gi,
|
|
62
|
+
/<|im_end|>/gi,
|
|
63
|
+
],
|
|
64
|
+
google: [
|
|
65
|
+
/<content>[\s\S]*?<\/content>/gi,
|
|
66
|
+
/[\Parts|thought]/gi,
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ============================================================
|
|
71
|
+
// Core normalizer
|
|
72
|
+
// ============================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalize text for cross-provider cache key generation.
|
|
76
|
+
* Removes provider-specific formatting while preserving semantic content.
|
|
77
|
+
*/
|
|
78
|
+
export function normalizeForCacheKey(
|
|
79
|
+
text: string,
|
|
80
|
+
options: CacheKeyOptions = {}
|
|
81
|
+
): string {
|
|
82
|
+
let normalized = text;
|
|
83
|
+
|
|
84
|
+
// Step 1: Unicode normalization (NFC)
|
|
85
|
+
normalized = normalized.normalize('NFC');
|
|
86
|
+
|
|
87
|
+
// Step 2: Collapse whitespace
|
|
88
|
+
normalized = normalized.replace(/\s+/g, ' ');
|
|
89
|
+
|
|
90
|
+
// Step 3: Remove control characters
|
|
91
|
+
normalized = normalized.replace(/[\x00-\x1F\x7F]/g, '');
|
|
92
|
+
|
|
93
|
+
// Step 4: Strip provider-specific formatting
|
|
94
|
+
if (options.provider) {
|
|
95
|
+
const patterns = PROVIDER_SYSTEM_PATTERNS[options.provider] || [];
|
|
96
|
+
for (const pattern of patterns) {
|
|
97
|
+
normalized = normalized.replace(pattern, '');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 5: General system/assistant role removal
|
|
102
|
+
normalized = normalized
|
|
103
|
+
.replace(/\b(system|user|assistant|human|bot)\s*:/gi, '')
|
|
104
|
+
.replace(/^(system|user|assistant|human|bot)\s*/gim, '');
|
|
105
|
+
|
|
106
|
+
// Step 6: Remove markdown formatting (often provider-specific)
|
|
107
|
+
normalized = normalized
|
|
108
|
+
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK]') // Preserve code block indicator
|
|
109
|
+
.replace(/`([^`]+)`/g, '$1') // Inline code content
|
|
110
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Bold
|
|
111
|
+
.replace(/_([^_]+)_/g, '$1') // Italic
|
|
112
|
+
.replace(/#+\s*/g, '') // Headers
|
|
113
|
+
.replace(/^\s*[-*+]\s+/gm, '') // List bullets
|
|
114
|
+
.replace(/^\s*\d+\.\s+/gm, ''); // Numbered lists
|
|
115
|
+
|
|
116
|
+
// Step 7: Apply custom rules
|
|
117
|
+
if (options.customRules) {
|
|
118
|
+
for (const rule of options.customRules) {
|
|
119
|
+
normalized = normalized.replace(rule.pattern, rule.replacement);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Step 8: Collapse whitespace again after removals
|
|
124
|
+
normalized = normalized.replace(/\s+/g, ' ').trim();
|
|
125
|
+
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a deterministic cache key from a query.
|
|
131
|
+
* Same semantic content = same key across providers.
|
|
132
|
+
*/
|
|
133
|
+
export function generateCacheKey(
|
|
134
|
+
query: string,
|
|
135
|
+
options: CacheKeyOptions = {}
|
|
136
|
+
): CacheKeyResult {
|
|
137
|
+
const originalLength = query.length;
|
|
138
|
+
|
|
139
|
+
// Normalize the query
|
|
140
|
+
let normalized = normalizeForCacheKey(query, {
|
|
141
|
+
...options,
|
|
142
|
+
includeSystemPrompt: false, // Always exclude for user query matching
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Count rules that were applied (approximate)
|
|
146
|
+
let rulesApplied = 3; // Base normalizations
|
|
147
|
+
if (options.provider) rulesApplied += 2;
|
|
148
|
+
if (options.customRules) rulesApplied += options.customRules.length;
|
|
149
|
+
|
|
150
|
+
// Generate hash
|
|
151
|
+
const hash = crypto
|
|
152
|
+
.createHash('sha256')
|
|
153
|
+
.update(normalized)
|
|
154
|
+
.digest('hex')
|
|
155
|
+
.substring(0, 16); // First 16 chars = 64-bit key
|
|
156
|
+
|
|
157
|
+
// Final key format: v1:{hash}:{provider?[:model]?}
|
|
158
|
+
let key = `v1:${hash}`;
|
|
159
|
+
if (options.provider) {
|
|
160
|
+
key += `:${options.provider}`;
|
|
161
|
+
if (options.model) {
|
|
162
|
+
key += `:${options.model}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
key,
|
|
168
|
+
hash,
|
|
169
|
+
metadata: {
|
|
170
|
+
originalLength,
|
|
171
|
+
normalizedLength: normalized.length,
|
|
172
|
+
rulesApplied,
|
|
173
|
+
provider: options.provider,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================
|
|
179
|
+
// SemanticCache enhancement
|
|
180
|
+
// ============================================================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Add cross-provider cache key methods to existing SemanticCache.
|
|
184
|
+
* Call this to enhance the cache with provider-normalized lookups.
|
|
185
|
+
*/
|
|
186
|
+
export function createCacheKeyGenerator(
|
|
187
|
+
defaultOptions?: CacheKeyOptions
|
|
188
|
+
): {
|
|
189
|
+
generateKey: (query: string, options?: CacheKeyOptions) => CacheKeyResult;
|
|
190
|
+
createNormalizedMatcher: (cache: Map<string, any>) => (query: string, options?: CacheKeyOptions) => string | null;
|
|
191
|
+
} {
|
|
192
|
+
return {
|
|
193
|
+
/**
|
|
194
|
+
* Generate a cache key for a query.
|
|
195
|
+
*/
|
|
196
|
+
generateKey: (query: string, options?: CacheKeyOptions): CacheKeyResult => {
|
|
197
|
+
return generateCacheKey(query, { ...defaultOptions, ...options });
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a matcher function that finds existing cache entries
|
|
202
|
+
* by comparing normalized keys.
|
|
203
|
+
*/
|
|
204
|
+
createNormalizedMatcher: (cache: Map<string, any>) => {
|
|
205
|
+
return (query: string, options?: CacheKeyOptions): string | null => {
|
|
206
|
+
const { key } = generateCacheKey(query, { ...defaultOptions, ...options });
|
|
207
|
+
|
|
208
|
+
// Check exact match
|
|
209
|
+
if (cache.has(key)) {
|
|
210
|
+
return key;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check hash-only match (v1:{hash} prefix)
|
|
214
|
+
const hashPrefix = key.split(':').slice(0, 2).join(':');
|
|
215
|
+
for (const cachedKey of cache.keys()) {
|
|
216
|
+
if (cachedKey.startsWith(hashPrefix + ':')) {
|
|
217
|
+
return cachedKey;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ============================================================
|
|
228
|
+
// Convenience exports
|
|
229
|
+
// ============================================================
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Quick cache key generation (simplified API).
|
|
233
|
+
* Use this for simple cross-provider cache lookups.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* const key1 = toCacheKey("What is Python?", "openai");
|
|
237
|
+
* const key2 = toCacheKey("What is Python?", "anthropic");
|
|
238
|
+
* console.log(key1 === key2); // true
|
|
239
|
+
*/
|
|
240
|
+
export function toCacheKey(query: string, provider?: string): string {
|
|
241
|
+
return generateCacheKey(query, { provider }).key;
|
|
242
|
+
}
|