adaptive-memory-multi-model-router 2.16.0 → 2.16.1
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/workflows/adapters-ci.yml +142 -0
- package/.github/workflows/auto-submit-sitemap.yml +41 -0
- package/.github/workflows/ci.yml +2 -5
- package/.github/workflows/mcp-pypi-publish.yml +34 -0
- package/.github/workflows/pypi-publish.yml +146 -0
- package/.github/workflows/tmlpd-publish.yml +23 -0
- package/README.md +245 -148
- package/RELEASE_v2.16.0.md +149 -0
- package/TECHNICAL_README.md +253 -0
- package/adapters/README.md +36 -0
- package/adapters/__init__.py +25 -0
- package/adapters/a3m_adapter/__init__.py +51 -0
- package/adapters/a3m_adapter/adapter/__init__.py +22 -0
- package/adapters/a3m_adapter/adapter/autogen.py +169 -0
- package/adapters/a3m_adapter/adapter/config.py +100 -0
- package/adapters/a3m_adapter/adapter/haystack.py +197 -0
- package/adapters/a3m_adapter/adapter/langchain.py +155 -0
- package/adapters/a3m_adapter/adapter/langgraph.py +196 -0
- package/adapters/a3m_adapter/adapter/llamaindex.py +162 -0
- package/adapters/a3m_adapter/adapter/pinecone.py +217 -0
- package/adapters/a3m_adapter/adapter/vercel.py +188 -0
- package/adapters/a3m_adapter/tests/__init__.py +1 -0
- package/adapters/a3m_adapter/tests/test_adapters.py +118 -0
- package/adapters/a3m_adapter/tests/test_integration.py +80 -0
- package/adapters/requirements-dev.txt +6 -0
- package/adapters/requirements.txt +4 -0
- package/adapters/setup.py +23 -0
- package/demo.py +251 -0
- package/discoverability-diagnosis.md +280 -0
- package/dist/analytics/costAnalytics.d.ts.map +1 -1
- package/dist/benchmark/reproducible.d.ts.map +1 -1
- package/dist/cache/semanticCache.d.ts.map +1 -1
- package/dist/cli/setupWizard.d.ts +257 -50
- package/dist/cli/setupWizard.d.ts.map +1 -1
- package/dist/cli/setupWizard.js +419 -109
- package/dist/cli/setupWizard.js.map +1 -1
- package/dist/cli/tui.js +96 -67
- package/dist/cli.js +9 -0
- package/dist/cost/budgetEnforcer.d.ts.map +1 -1
- package/dist/cost/costTracker.d.ts.map +1 -1
- package/dist/ensemble/multiRoundDialog.d.ts.map +1 -1
- package/dist/ensemble/shapleyValue.d.ts.map +1 -1
- package/dist/ensemble.d.ts +1 -1
- package/dist/ensemble.js +141 -0
- package/dist/integrations/langchainAdapter.d.ts.map +1 -1
- package/dist/integrations/langchainAdapter.js +3 -3
- package/dist/integrations/langchainAdapter.js.map +1 -1
- package/dist/integrations/oauth.d.ts.map +1 -1
- package/dist/integrations/scienceAdapter.d.ts.map +1 -1
- package/dist/memory/autoFetch.d.ts.map +1 -1
- package/dist/memory/hybridMemory.d.ts.map +1 -1
- package/dist/memory/memoryTree.d.ts.map +1 -1
- package/dist/memory/obsidianVault.d.ts.map +1 -1
- package/dist/memory/reasoningBank.d.ts.map +1 -1
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/tracer.d.ts.map +1 -1
- package/dist/providers/providerConfig.d.ts.map +1 -1
- package/dist/providers/providerConfig.js +32 -17
- package/dist/providers/providerConfig.js.map +1 -1
- package/dist/routing/advancedRouter.d.ts.map +1 -1
- package/dist/routing/advancedRouter.js +106 -14
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/providerHealth.d.ts.map +1 -1
- package/dist/routing/providerRetry.d.ts.map +1 -1
- package/dist/routing/shadowSampler.js.map +1 -1
- package/dist/security/guardrails.d.ts.map +1 -1
- package/dist/server/handlers/chatHandler.d.ts.map +1 -1
- package/dist/server/handlers/completionsHandler.d.ts.map +1 -1
- package/dist/server/handlers/embeddingsHandler.d.ts.map +1 -1
- package/dist/server/handlers/healthHandler.d.ts.map +1 -1
- package/dist/server/handlers/metricsHandler.d.ts.map +1 -1
- package/dist/server/handlers/modelsHandler.d.ts.map +1 -1
- package/dist/server/metrics.d.ts.map +1 -1
- package/dist/server/proxyServer.d.ts.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/state.d.ts.map +1 -1
- package/dist/skills/__tests__/skill_manager.test.js +5 -265
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -1
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/docker-compose.yml +84 -60
- package/docs/ARTICLE_Biology_Inspired_Routing.md +208 -0
- package/docs/ARTICLE_Master.md +78 -0
- package/docs/ARTICLE_Master_CN.md +78 -0
- package/docs/ARTICLE_OpenRouter_Stripe.md +140 -0
- package/docs/DEVPTO_ARTICLE.md +84 -0
- package/docs/HUMAN_STYLE_GUIDE.md +75 -0
- package/docs/IMPRINT_PLAN.md +88 -0
- package/docs/OPENROUTER_ALTERNATIVE.md +184 -0
- package/docs/SOCIAL_CAMPAIGN.md +316 -0
- package/docs/anthropic.html +45 -0
- package/docs/best-llm-routers-2025.html +157 -0
- package/docs/cerebras.html +43 -0
- package/docs/cli-cheatsheet.md +286 -212
- package/docs/comparison.md +2 -2
- package/docs/deepseek.html +44 -0
- package/docs/google.html +47 -0
- package/docs/groq.html +44 -0
- package/docs/llms-full.txt +360 -138
- package/docs/llms.txt +70 -71
- package/docs/mistral.html +43 -0
- package/docs/ollama.html +50 -0
- package/docs/openai.html +57 -0
- package/docs/sitemap.xml +69 -57
- package/docs-site/blog/best-llm-routers-2025.html +157 -0
- package/docs-site/index.html +68 -9
- package/docs-site/providers/anthropic.html +45 -0
- package/docs-site/providers/cerebras.html +43 -0
- package/docs-site/providers/deepseek.html +44 -0
- package/docs-site/providers/google.html +47 -0
- package/docs-site/providers/groq.html +44 -0
- package/docs-site/providers/index.html +41 -0
- package/docs-site/providers/mistral.html +43 -0
- package/docs-site/providers/ollama.html +50 -0
- package/docs-site/providers/openai.html +57 -0
- package/docs-site/sitemap.xml +69 -0
- package/llms.txt +70 -62
- package/package.json +44 -182
- package/packages/agentkit-adapter/LICENSE +21 -0
- package/packages/agentkit-adapter/README.md +126 -0
- package/packages/agentkit-adapter/examples/agentkit-example.ts +139 -0
- package/packages/agentkit-adapter/package.json +57 -0
- package/packages/agentkit-adapter/src/adapter.ts +381 -0
- package/packages/agentkit-adapter/src/index.ts +36 -0
- package/packages/agentkit-adapter/src/types.ts +105 -0
- package/packages/agentkit-adapter/src/util.ts +13 -0
- package/packages/agentkit-adapter/tsconfig.json +22 -0
- package/prometheus.yml +8 -0
- package/python/README.md +35 -81
- package/python/a3m/__init__.py +32 -3
- package/python/a3m/adapters/__init__.py +21 -0
- package/python/a3m/adapters/langchain.py +190 -0
- package/python/a3m/adapters/llamaindex.py +249 -0
- package/python/a3m/adapters/qdrant.py +240 -0
- package/python/a3m/adapters/weaviate.py +263 -0
- package/python/a3m/client.py +5 -0
- package/python/build_verify.sh +32 -0
- package/python/mcp-server/README.md +172 -0
- package/python/mcp-server/a3m_mcp/__init__.py +25 -0
- package/python/mcp-server/a3m_mcp/__main__.py +15 -0
- package/python/mcp-server/a3m_mcp/server.py +205 -0
- package/python/mcp-server/pyproject.toml +24 -0
- package/python/pyproject.toml +60 -6
- package/python/setup.py +3 -28
- package/scripts/submit-sitemap.sh +52 -0
- package/src/__types__/registry.d.ts +14 -0
- package/src/cli/setupWizard.ts +443 -112
- package/src/cli/tui.ts +159 -0
- package/src/ensemble.ts +154 -1
- package/src/integrations/langchainAdapter.ts +2 -2
- package/src/providers/providerConfig.ts +32 -17
- package/src/providers/registry.js +27 -0
- package/src/routing/advancedRouter.ts +99 -14
- package/src/routing/shadowSampler.ts +1 -1
- package/test-install/package.json +12 -0
- package/tests/tsconfig.json +0 -1
- package/tmlpd-pi-extension/README.md +105 -44
- package/tmlpd-pi-extension/docs/demo.svg +33 -0
- package/tmlpd-pi-extension/package.json +35 -106
- package/tmlpd-pi-extension/src/tokenOptimization/contextStratifier.ts +163 -0
- package/tmlpd-pi-extension/src/tokenOptimization/fetchOnceLocal.ts +136 -0
- package/tmlpd-pi-extension/src/tokenOptimization/index.ts +197 -0
- package/tmlpd-pi-extension/src/tokenOptimization/interAgentCompression.ts +157 -0
- package/tmlpd-pi-extension/src/tokenOptimization/schemaContract.ts +101 -0
- package/tmlpd-pi-extension/src/tokenOptimization/semanticCache.ts +248 -0
- package/tmlpd-pi-extension/src/tokenOptimization/tokenAwareFallback.ts +192 -0
- package/tmlpd-pi-extension/test/verify.js +21 -0
- package/tsconfig.build.json +3 -2
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +0 -12
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js +0 -289
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +0 -1
- package/packages/a3m-vercel-ai/dist/index.d.ts +0 -82
- package/packages/a3m-vercel-ai/dist/index.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/index.js +0 -79
- package/packages/a3m-vercel-ai/dist/index.js.map +0 -1
- package/packages/a3m-vercel-ai/dist/types.d.ts +0 -97
- package/packages/a3m-vercel-ai/dist/types.d.ts.map +0 -1
- package/packages/a3m-vercel-ai/dist/types.js +0 -5
- package/packages/a3m-vercel-ai/dist/types.js.map +0 -1
- package/src/skills/__tests__/skill_manager.test.ts +0 -328
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts +0 -114
- package/tmlpd-pi-extension/dist/cache/prefixCache.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cache/prefixCache.js +0 -285
- package/tmlpd-pi-extension/dist/cache/prefixCache.js.map +0 -1
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts +0 -58
- package/tmlpd-pi-extension/dist/cache/responseCache.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cache/responseCache.js +0 -153
- package/tmlpd-pi-extension/dist/cache/responseCache.js.map +0 -1
- package/tmlpd-pi-extension/dist/cli.js +0 -59
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts +0 -95
- package/tmlpd-pi-extension/dist/cost/costTracker.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/cost/costTracker.js +0 -240
- package/tmlpd-pi-extension/dist/cost/costTracker.js.map +0 -1
- package/tmlpd-pi-extension/dist/index.d.ts +0 -723
- package/tmlpd-pi-extension/dist/index.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/index.js +0 -239
- package/tmlpd-pi-extension/dist/index.js.map +0 -1
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts +0 -82
- package/tmlpd-pi-extension/dist/memory/episodicMemory.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js +0 -145
- package/tmlpd-pi-extension/dist/memory/episodicMemory.js.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts +0 -102
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js +0 -207
- package/tmlpd-pi-extension/dist/orchestration/haloOrchestrator.js.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts +0 -85
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js +0 -210
- package/tmlpd-pi-extension/dist/orchestration/mctsWorkflow.js.map +0 -1
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts +0 -102
- package/tmlpd-pi-extension/dist/providers/localProvider.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/providers/localProvider.js +0 -338
- package/tmlpd-pi-extension/dist/providers/localProvider.js.map +0 -1
- package/tmlpd-pi-extension/dist/providers/registry.d.ts +0 -55
- package/tmlpd-pi-extension/dist/providers/registry.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/providers/registry.js +0 -138
- package/tmlpd-pi-extension/dist/providers/registry.js.map +0 -1
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts +0 -68
- package/tmlpd-pi-extension/dist/routing/advancedRouter.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js +0 -332
- package/tmlpd-pi-extension/dist/routing/advancedRouter.js.map +0 -1
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts +0 -101
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js +0 -368
- package/tmlpd-pi-extension/dist/tools/tmlpdTools.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts +0 -96
- package/tmlpd-pi-extension/dist/utils/batchProcessor.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js +0 -170
- package/tmlpd-pi-extension/dist/utils/batchProcessor.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/compression.d.ts +0 -61
- package/tmlpd-pi-extension/dist/utils/compression.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/compression.js +0 -281
- package/tmlpd-pi-extension/dist/utils/compression.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts +0 -74
- package/tmlpd-pi-extension/dist/utils/reliability.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/reliability.js +0 -177
- package/tmlpd-pi-extension/dist/utils/reliability.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts +0 -117
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js +0 -246
- package/tmlpd-pi-extension/dist/utils/speculativeDecoding.js.map +0 -1
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts +0 -50
- package/tmlpd-pi-extension/dist/utils/tokenUtils.d.ts.map +0 -1
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js +0 -124
- package/tmlpd-pi-extension/dist/utils/tokenUtils.js.map +0 -1
package/src/cli/tui.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router Quick TUI
|
|
4
|
+
* Zero-config experience - no API key needed for free tier
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
if (args.length === 0) {
|
|
12
|
+
console.log(`
|
|
13
|
+
╔══════════════════════════════════════════════╗
|
|
14
|
+
║ 🔀 A3M Router - Zero Config ║
|
|
15
|
+
╠══════════════════════════════════════════════╣
|
|
16
|
+
║ ║
|
|
17
|
+
║ One prompt in. The right model out. ║
|
|
18
|
+
║ ║
|
|
19
|
+
║ Usage: ║
|
|
20
|
+
║ a3m hello world # Route a query ║
|
|
21
|
+
║ a3m --serve # Start proxy ║
|
|
22
|
+
║ a3m --setup # Configure API keys ║
|
|
23
|
+
║ a3m --demo # Try without keys ║
|
|
24
|
+
║ ║
|
|
25
|
+
╚══════════════════════════════════════════════╝
|
|
26
|
+
`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const command = args[0];
|
|
31
|
+
|
|
32
|
+
// Demo mode - works without API key
|
|
33
|
+
if (command === '--demo') {
|
|
34
|
+
console.log(`
|
|
35
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
🎮 A3M Router Demo Mode
|
|
37
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
|
|
39
|
+
Try these queries:
|
|
40
|
+
• "Write Python hello world"
|
|
41
|
+
• "Explain quantum physics"
|
|
42
|
+
• "Translate to Japanese: Hello"
|
|
43
|
+
• "Write a haiku about coding"
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
|
|
48
|
+
const demoResults = [
|
|
49
|
+
{ provider: 'groq', model: 'llama-3.3-70b-versatile', tier: 'free', cost: '$0.00' },
|
|
50
|
+
{ provider: 'deepseek', model: 'deepseek-chat-v3', tier: 'cheap', cost: '$0.00' },
|
|
51
|
+
{ provider: 'kimi', model: 'moonshot-v1-8k', tier: 'cheap', cost: '$0.00' },
|
|
52
|
+
{ provider: 'openai', model: 'gpt-4o-mini', tier: 'paid', cost: '$0.00' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
let count = 0;
|
|
56
|
+
function ask(): void {
|
|
57
|
+
rl.question('\n🔀 Query: ', (q: string) => {
|
|
58
|
+
if (!q || q.toLowerCase() === 'exit') {
|
|
59
|
+
console.log('\n👋 Ready for the real thing? Run: npm install adaptive-memory-multi-model-router\n');
|
|
60
|
+
rl.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const r = demoResults[count++ % demoResults.length];
|
|
64
|
+
console.log(`\n 🏆 Best: ${r.provider} / ${r.model}`);
|
|
65
|
+
console.log(` 💰 Cost: ${r.cost} (${r.tier})`);
|
|
66
|
+
ask();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
ask();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Serve mode
|
|
74
|
+
if (command === '--serve') {
|
|
75
|
+
console.log(`
|
|
76
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
|
+
🚀 A3M Router Proxy Server
|
|
78
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
|
|
80
|
+
Server: http://localhost:8787/v1/chat/completions
|
|
81
|
+
API: OpenAI-compatible
|
|
82
|
+
|
|
83
|
+
No API keys configured? Add free tier:
|
|
84
|
+
GROQ_API_KEY=your_key npx a3m-router serve
|
|
85
|
+
|
|
86
|
+
Or run setup: a3m --setup
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
91
|
+
const server = require('../server/proxyServer.js');
|
|
92
|
+
const port = parseInt(args[1], 10) || 8787;
|
|
93
|
+
server.createProxyServer({ port });
|
|
94
|
+
console.log(`\n✅ Server running at http://localhost:${port}`);
|
|
95
|
+
} catch (e: unknown) {
|
|
96
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
97
|
+
console.log('\n⚠️ Run "npm install" first, then "npm run build"\n');
|
|
98
|
+
console.error('Error:', msg);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Setup mode
|
|
104
|
+
if (command === '--setup') {
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
106
|
+
const wizard = require('./setupWizard.js');
|
|
107
|
+
wizard.runWizard();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Zero-config mode - route with free tier defaults
|
|
112
|
+
const query = args.join(' ');
|
|
113
|
+
|
|
114
|
+
// Check for GROQ_API_KEY first (free tier)
|
|
115
|
+
if (process.env.GROQ_API_KEY) {
|
|
116
|
+
console.log('\n🔀 Routing: "' + query + '"');
|
|
117
|
+
console.log('📡 Using free tier (Groq)...\n');
|
|
118
|
+
|
|
119
|
+
// Quick route via groq
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
121
|
+
const { routeQuery } = require('../index.js');
|
|
122
|
+
routeQuery(query).then((r: { provider?: string; model?: string; estimated_cost?: number }) => {
|
|
123
|
+
console.log(` 🏆 Provider: ${r.provider || 'groq'}`);
|
|
124
|
+
console.log(` 🤖 Model: ${r.model || 'llama-3.3-70b-versatile'}`);
|
|
125
|
+
console.log(` 💰 Est: $${(r.estimated_cost || 0.0001).toFixed(6)}`);
|
|
126
|
+
}).catch((e: unknown) => {
|
|
127
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
128
|
+
console.log(' ⚠️ Error:', msg);
|
|
129
|
+
console.log(' Tip: Get free key at https://console.groq.com\n');
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// No API key - show helpful message
|
|
135
|
+
console.log(`
|
|
136
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
137
|
+
⚠️ No API key detected
|
|
138
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
139
|
+
|
|
140
|
+
A3M Router needs at least one API key to route queries.
|
|
141
|
+
|
|
142
|
+
🔥 FREE TIER OPTIONS:
|
|
143
|
+
1. Groq (fastest, 1000+ req/day free)
|
|
144
|
+
→ https://console.groq.com/apikeys
|
|
145
|
+
→ Set: export GROQ_API_KEY=your_key
|
|
146
|
+
|
|
147
|
+
2. Cerebras (fastest inference, free)
|
|
148
|
+
→ https://cerebras.ai/labs
|
|
149
|
+
→ Set: export CEREBRAS_API_KEY=your_key
|
|
150
|
+
|
|
151
|
+
3. Google AI (Gemini, free tier)
|
|
152
|
+
→ https://aistudio.google.com/app/apikey
|
|
153
|
+
→ Set: export GOOGLE_API_KEY=your_key
|
|
154
|
+
|
|
155
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
156
|
+
|
|
157
|
+
Quick setup: npx a3m --setup
|
|
158
|
+
Quick start: GROQ_API_KEY=your_key npx a3m-router serve
|
|
159
|
+
`);
|
package/src/ensemble.ts
CHANGED
|
@@ -21,6 +21,126 @@ interface RouteDecision {
|
|
|
21
21
|
reasoning: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ============================================================
|
|
25
|
+
// SEMANTIC VOTING: Cluster similar answers together
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Simple word-overlap based semantic similarity
|
|
30
|
+
* Returns 0-1 similarity score
|
|
31
|
+
*/
|
|
32
|
+
function wordOverlapSimilarity(a: string, b: string): number {
|
|
33
|
+
// Normalize: lowercase, remove code blocks, extract words
|
|
34
|
+
const normalize = (s: string) => {
|
|
35
|
+
return s.toLowerCase()
|
|
36
|
+
.replace(/```[\s\S]*?```/g, ' CODE ') // Replace code blocks
|
|
37
|
+
.replace(/`[^`]*`/g, ' CODE ') // Replace inline code
|
|
38
|
+
.replace(/[^a-z0-9\s]/g, ' ') // Remove punctuation
|
|
39
|
+
.split(/\s+/)
|
|
40
|
+
.filter(w => w.length > 2); // Remove stopwords
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const wordsA = normalize(a);
|
|
44
|
+
const wordsB = normalize(b);
|
|
45
|
+
|
|
46
|
+
if (wordsA.length === 0 || wordsB.length === 0) return 0;
|
|
47
|
+
|
|
48
|
+
// Jaccard similarity
|
|
49
|
+
const setA = new Set(wordsA);
|
|
50
|
+
const setB = new Set(wordsB);
|
|
51
|
+
let intersection = 0;
|
|
52
|
+
for (const w of setA) {
|
|
53
|
+
if (setB.has(w)) intersection++;
|
|
54
|
+
}
|
|
55
|
+
const union = setA.size + setB.size - intersection;
|
|
56
|
+
return union > 0 ? intersection / union : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Cluster answers by semantic similarity using agglomerative clustering
|
|
61
|
+
* Returns clusters of (representative, [provider indices])
|
|
62
|
+
*/
|
|
63
|
+
function semanticCluster(
|
|
64
|
+
answers: { provider: string; answer: string }[],
|
|
65
|
+
threshold = 0.6
|
|
66
|
+
): { cluster: string; providers: string[] }[] {
|
|
67
|
+
if (answers.length === 0) return [];
|
|
68
|
+
if (answers.length === 1) return [{ cluster: answers[0].answer, providers: [answers[0].provider] }];
|
|
69
|
+
|
|
70
|
+
// Build similarity matrix
|
|
71
|
+
const n = answers.length;
|
|
72
|
+
const sims: number[][] = Array(n).fill(null).map(() => Array(n).fill(0));
|
|
73
|
+
for (let i = 0; i < n; i++) {
|
|
74
|
+
for (let j = 0; j < n; j++) {
|
|
75
|
+
if (i === j) {
|
|
76
|
+
sims[i][j] = 1.0;
|
|
77
|
+
} else if (j > i) {
|
|
78
|
+
const s = wordOverlapSimilarity(answers[i].answer, answers[j].answer);
|
|
79
|
+
sims[i][j] = s;
|
|
80
|
+
sims[j][i] = s;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Agglomerative clustering: greedily merge most similar pairs
|
|
86
|
+
const clusters: Set<number>[] = answers.map((_, i) => new Set([i]));
|
|
87
|
+
const active = new Set(answers.map((_, i) => i));
|
|
88
|
+
|
|
89
|
+
while (active.size > 1) {
|
|
90
|
+
let bestSim = 0;
|
|
91
|
+
let bestI = -1, bestJ = -1;
|
|
92
|
+
|
|
93
|
+
for (const i of active) {
|
|
94
|
+
for (const j of active) {
|
|
95
|
+
if (i >= j) continue;
|
|
96
|
+
// Average similarity between all pairs in two clusters
|
|
97
|
+
let sumSim = 0, count = 0;
|
|
98
|
+
for (const ci of clusters[i]) {
|
|
99
|
+
for (const cj of clusters[j]) {
|
|
100
|
+
sumSim += sims[ci][cj];
|
|
101
|
+
count++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const avgSim = count > 0 ? sumSim / count : 0;
|
|
105
|
+
if (avgSim > bestSim) {
|
|
106
|
+
bestSim = avgSim;
|
|
107
|
+
bestI = i;
|
|
108
|
+
bestJ = j;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (bestSim < threshold || bestI < 0) break;
|
|
114
|
+
|
|
115
|
+
// Merge clusters
|
|
116
|
+
for (const idx of clusters[bestJ]) {
|
|
117
|
+
clusters[bestI].add(idx);
|
|
118
|
+
}
|
|
119
|
+
active.delete(bestJ);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build result
|
|
123
|
+
return clusters
|
|
124
|
+
.filter((_, i) => active.has(i))
|
|
125
|
+
.map(cluster => {
|
|
126
|
+
const indices = Array.from(cluster);
|
|
127
|
+
// Use the longest answer as cluster representative (most informative)
|
|
128
|
+
let longestIdx = indices[0];
|
|
129
|
+
let longestLen = answers[indices[0]].answer.length;
|
|
130
|
+
for (const idx of indices) {
|
|
131
|
+
if (answers[idx].answer.length > longestLen) {
|
|
132
|
+
longestLen = answers[idx].answer.length;
|
|
133
|
+
longestIdx = idx;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
cluster: answers[longestIdx].answer,
|
|
138
|
+
providers: indices.map(i => answers[i].provider)
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
.sort((a, b) => b.providers.length - a.providers.length); // Largest cluster first
|
|
142
|
+
}
|
|
143
|
+
|
|
24
144
|
// Type alias for external consumers
|
|
25
145
|
export type RouterDecision = RouteDecision;
|
|
26
146
|
|
|
@@ -29,7 +149,7 @@ export type RouterDecision = RouteDecision;
|
|
|
29
149
|
export const A3MRouter = createA3MRouter as any;
|
|
30
150
|
export { createA3MRouter };
|
|
31
151
|
|
|
32
|
-
export type EnsembleStrategy = 'majority' | 'weighted' | 'conservative' | 'shapley';
|
|
152
|
+
export type EnsembleStrategy = 'majority' | 'weighted' | 'conservative' | 'shapley' | 'semantic';
|
|
33
153
|
|
|
34
154
|
export interface EnsembleResponse {
|
|
35
155
|
finalAnswer: string;
|
|
@@ -157,6 +277,39 @@ export class EnsembleOrchestrator {
|
|
|
157
277
|
this.handicapCalc.record(r.provider, cost, isCorrect(r.answer));
|
|
158
278
|
});
|
|
159
279
|
}
|
|
280
|
+
else if (strategy === 'semantic') {
|
|
281
|
+
// === SEMANTIC VOTING: Cluster similar answers together ===
|
|
282
|
+
// This handles cases where models give equivalent answers in different words
|
|
283
|
+
// e.g., "The answer is 42" vs "42 is correct" would be clustered together
|
|
284
|
+
|
|
285
|
+
const clusters = semanticCluster(successful, 0.55); // 55% similarity threshold
|
|
286
|
+
|
|
287
|
+
if (clusters.length === 0) {
|
|
288
|
+
winnerAnswer = 'UNCERTAIN';
|
|
289
|
+
confidence = 0;
|
|
290
|
+
winnerProvider = 'none';
|
|
291
|
+
} else {
|
|
292
|
+
// Use weighted voting: cluster size * average provider weight
|
|
293
|
+
const clusterScores = clusters.map(cluster => {
|
|
294
|
+
const totalWeight = cluster.providers.reduce((sum, p) => sum + (weights[p] || 1.0), 0);
|
|
295
|
+
return {
|
|
296
|
+
cluster: cluster.cluster,
|
|
297
|
+
providers: cluster.providers,
|
|
298
|
+
score: cluster.providers.length * totalWeight / cluster.providers.length
|
|
299
|
+
};
|
|
300
|
+
}).sort((a, b) => b.score - a.score);
|
|
301
|
+
|
|
302
|
+
winnerAnswer = clusterScores[0].cluster;
|
|
303
|
+
winnerProvider = clusterScores[0].providers[0];
|
|
304
|
+
confidence = clusterScores[0].providers.length / successful.length;
|
|
305
|
+
|
|
306
|
+
// Log cluster info for debugging
|
|
307
|
+
console.log(`[SEMANTIC] ${clusters.length} clusters formed, winner has ${clusterScores[0].providers.length}/${successful.length} providers`);
|
|
308
|
+
clusters.slice(1, 3).forEach((c, i) => {
|
|
309
|
+
console.log(`[SEMANTIC] Cluster ${i+2}: ${c.providers.length} providers, preview: ${c.cluster.substring(0, 50)}...`);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
160
313
|
|
|
161
314
|
// Record loyalty: successful collaborations build trust
|
|
162
315
|
if (strategy === 'shapley') {
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
registerProvider,
|
|
75
75
|
} from '../providers/providerConfig.js';
|
|
76
76
|
|
|
77
|
-
import { ProviderRegistry } from '../providers/registry
|
|
77
|
+
import { ProviderRegistry } from '../providers/registry';
|
|
78
78
|
|
|
79
79
|
// ============================================================
|
|
80
80
|
// TYPES
|
|
@@ -136,7 +136,7 @@ function langchainMessagesToOpenAI(messages: BaseMessage[]): Array<{
|
|
|
136
136
|
tool_calls?: A3MToolCall[];
|
|
137
137
|
}> {
|
|
138
138
|
return messages.map((msg) => {
|
|
139
|
-
const role = msg._getType() as 'system' | 'user' | 'assistant' | 'tool';
|
|
139
|
+
const role = ((msg as any)._getType()) as 'system' | 'user' | 'assistant' | 'tool';
|
|
140
140
|
|
|
141
141
|
let content: string | Array<{ type: string; text?: string; image_url?: any }>;
|
|
142
142
|
if (typeof msg.content === 'string') {
|
|
@@ -639,30 +639,44 @@ export const DEFAULT_PROVIDERS: Record<string, ProviderDefinition> = {
|
|
|
639
639
|
baseUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
|
640
640
|
apiKeyEnv: 'OPENROUTER_API_KEY',
|
|
641
641
|
models: [
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
'
|
|
645
|
-
'
|
|
646
|
-
'nvidia/nemotron-3-
|
|
647
|
-
'
|
|
648
|
-
'google/gemma-4-
|
|
649
|
-
|
|
650
|
-
'
|
|
651
|
-
|
|
652
|
-
'
|
|
653
|
-
//
|
|
642
|
+
// ✅ VERIFIED WORKING FREE MODELS (tested 2026-08-16 + recheck)
|
|
643
|
+
// Premium-tier free models (high context, powerful)
|
|
644
|
+
'nvidia/nemotron-3.5-lightning:free', // 1M context, reasoning
|
|
645
|
+
'nvidia/nemotron-3-ultra-550b-a55b:free', // 1M context, premium quality
|
|
646
|
+
'nvidia/nemotron-3-super-120b-a12b:free', // 262K context
|
|
647
|
+
'google/gemma-4-31b-it:free', // 262K context, multilingual
|
|
648
|
+
'google/gemma-4-26b-a4b-it:free', // 262K context
|
|
649
|
+
// Reasoning-capable models
|
|
650
|
+
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', // 256K, reasoning
|
|
651
|
+
// Mid-tier free models
|
|
652
|
+
'nvidia/nemotron-3-nano-30b-a3b:free', // 256K context
|
|
653
|
+
'nvidia/nemotron-nano-9b-v2:free', // 128K context
|
|
654
|
+
'nvidia/nemotron-nano-12b-v2-vl:free', // 128K context, multimodal
|
|
655
|
+
// Auto-route free model
|
|
656
|
+
'openrouter/free', // Auto-selects best available free model
|
|
657
|
+
// ❌ REMOVED (unavailable): kimi-k2.6, qwen3-coder, qwen3-next, hermes-3-405b, gpt-oss-120b, llama-3.3-70b, laguna-s-2.1
|
|
658
|
+
// FREE Gemini 3.x (newer, better than 2.x)
|
|
659
|
+
'google/gemini-3.5-flash:free', // 131K context, reasoning, free
|
|
660
|
+
'google/gemini-3.6-flash:free', // 131K context, reasoning, free
|
|
661
|
+
// FREE Qwen 3.7 (latest Qwen with extended context)
|
|
662
|
+
'qwen/qwen3.7-flash:free', // 131K context, reasoning, free
|
|
663
|
+
'qwen/qwen3.7-max:free', // 131K context, reasoning, free
|
|
664
|
+
// FREE DeepSeek V4 Flash
|
|
665
|
+
'deepseek/deepseek-v4-flash-latest:free', // 131K context, reasoning, free
|
|
666
|
+
// Paid models (passthrough pricing)
|
|
654
667
|
'openai/gpt-4o',
|
|
655
668
|
'anthropic/claude-3.5-sonnet',
|
|
656
669
|
'google/gemini-pro-1.5',
|
|
657
670
|
'meta-llama/llama-3.1-70b-instruct',
|
|
658
671
|
'mistralai/mistral-large',
|
|
659
672
|
],
|
|
660
|
-
costPerK: { input: 0, output: 0 }, //
|
|
661
|
-
tier: 'cheap'
|
|
673
|
+
costPerK: { input: 0, output: 0 }, // Free tier models cost 0; paid use passthrough
|
|
674
|
+
tier: 'mid', // Changed from 'cheap' — premium free models are mid-tier quality
|
|
662
675
|
format: 'openai',
|
|
663
676
|
type: 'api',
|
|
664
677
|
priority: 34,
|
|
665
|
-
maxTokens:
|
|
678
|
+
maxTokens: 1048576, // Support 1M context for nemotron-3.5-lightning
|
|
679
|
+
strategy: 'balanced',
|
|
666
680
|
},
|
|
667
681
|
|
|
668
682
|
// ========================================================================
|
|
@@ -718,12 +732,13 @@ export const DEFAULT_PROVIDERS: Record<string, ProviderDefinition> = {
|
|
|
718
732
|
'mistralai/mistral-large-3-675b-instruct-2512',
|
|
719
733
|
'z-ai/glm-5.1',
|
|
720
734
|
],
|
|
721
|
-
costPerK: { input: 0, output: 0 },
|
|
735
|
+
costPerK: { input: 0, output: 0 }, // Free tier available
|
|
722
736
|
tier: 'free',
|
|
723
737
|
format: 'openai',
|
|
724
738
|
type: 'api',
|
|
725
739
|
priority: 4,
|
|
726
|
-
maxTokens:
|
|
740
|
+
maxTokens: 131072, // Support long context for premium models
|
|
741
|
+
strategy: 'balanced',
|
|
727
742
|
},
|
|
728
743
|
|
|
729
744
|
// ========================================================================
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Registry - Central registry for all A3M Router providers
|
|
3
|
+
*/
|
|
4
|
+
export const PROVIDERS = new Map();
|
|
5
|
+
|
|
6
|
+
// Register providers by name
|
|
7
|
+
export function registerProvider(name, config) {
|
|
8
|
+
PROVIDERS.set(name, config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Get provider by name
|
|
12
|
+
export function getProvider(name) {
|
|
13
|
+
return PROVIDERS.get(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// List all registered providers
|
|
17
|
+
export function listProviders() {
|
|
18
|
+
return [...PROVIDERS.entries()];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Remove provider by name
|
|
22
|
+
export function removeProvider(name) {
|
|
23
|
+
return PROVIDERS.delete(name);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Export all providers
|
|
27
|
+
export * from './registry.js';
|
|
@@ -137,6 +137,12 @@ function buildModelProfiles(): Record<string, ModelProfile> {
|
|
|
137
137
|
'google/gemini-pro-1.5': [1.25, 5],
|
|
138
138
|
'meta-llama/llama-3.1-70b-instruct': [0.18, 0.18],
|
|
139
139
|
'mistralai/mistral-large': [2, 6],
|
|
140
|
+
// Newer free models (these override the default 0,0 for free tier)
|
|
141
|
+
'gemini-3.5-flash:free': [0, 0],
|
|
142
|
+
'gemini-3.6-flash:free': [0, 0],
|
|
143
|
+
'qwen3.7-flash:free': [0, 0],
|
|
144
|
+
'qwen3.7-max:free': [0, 0],
|
|
145
|
+
'deepseek-v4-flash-latest:free': [0, 0],
|
|
140
146
|
};
|
|
141
147
|
const orKey = model.includes('/') ? model : 'openrouter/' + model;
|
|
142
148
|
// Try matching by full key or by model name
|
|
@@ -165,18 +171,46 @@ function buildModelProfiles(): Record<string, ModelProfile> {
|
|
|
165
171
|
// OpenRouter premium free models get quality boosts
|
|
166
172
|
if (provider.name === 'OpenRouter') {
|
|
167
173
|
const modelLower = modelKey.toLowerCase();
|
|
168
|
-
// Premium-tier free models (large, high-context)
|
|
169
|
-
if (modelLower.includes('kimi') || modelLower.includes('qwen3-coder') ||
|
|
174
|
+
// Premium-tier free models (large, high-context) — these are equivalent to paid mid-tier
|
|
175
|
+
if (modelLower.includes('kimi-k2') || modelLower.includes('qwen3-coder') ||
|
|
170
176
|
modelLower.includes('nemotron-3-ultra') || modelLower.includes('nemotron-3-super') ||
|
|
171
|
-
modelLower.includes('
|
|
177
|
+
modelLower.includes('nemotron-3.5-lightning') || modelLower.includes('nemotron-3-nano-omni') ||
|
|
178
|
+
modelLower.includes('hermes-3') || modelLower.includes('gemma-4-31b') ||
|
|
179
|
+
modelLower.includes('laguna-s-2.1') ||
|
|
180
|
+
// Gemini 3.x free models — latest reasoning models
|
|
181
|
+
modelLower.includes('gemini-3.5-flash') || modelLower.includes('gemini-3.6-flash') ||
|
|
182
|
+
// Qwen 3.7 free models — latest with extended context
|
|
183
|
+
modelLower.includes('qwen3.7-flash') || modelLower.includes('qwen3.7-max') ||
|
|
184
|
+
// DeepSeek V4 free
|
|
185
|
+
modelLower.includes('deepseek-v4-flash')) {
|
|
172
186
|
strengths.push('reasoning', 'long-context', 'premium');
|
|
173
187
|
}
|
|
174
|
-
// Mid-tier free models (good quality, smaller)
|
|
188
|
+
// Mid-tier free models (good quality, smaller) — equivalent to cheap tier
|
|
175
189
|
else if (modelLower.includes('gpt-oss') || modelLower.includes('qwen3-next') ||
|
|
176
|
-
modelLower.includes('gemma-4') || modelLower.includes('llama-3.3')
|
|
190
|
+
modelLower.includes('gemma-4-26b') || modelLower.includes('llama-3.3-70b') ||
|
|
191
|
+
modelLower.includes('nemotron-3-nano-30b') || modelLower.includes('nemotron-nano-9b')) {
|
|
177
192
|
strengths.push('fast', 'reasoning');
|
|
178
193
|
}
|
|
179
|
-
// Budget free models
|
|
194
|
+
// Budget free models (small models)
|
|
195
|
+
else {
|
|
196
|
+
strengths.push('fast');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// NVIDIA NIM premium free models (high quality, long context)
|
|
200
|
+
if (provider.name === 'NVIDIA NIM') {
|
|
201
|
+
const modelLower = modelKey.toLowerCase();
|
|
202
|
+
// Premium tier: nemotron-super, gemma-4-31b, llama-3.3-70b
|
|
203
|
+
if (modelLower.includes('nemotron-3-super') || modelLower.includes('gemma-4-31b') ||
|
|
204
|
+
modelLower.includes('llama-3.3-70b') || modelLower.includes('mistral-large') ||
|
|
205
|
+
modelLower.includes('glm-5')) {
|
|
206
|
+
strengths.push('reasoning', 'long-context', 'premium');
|
|
207
|
+
}
|
|
208
|
+
// Mid tier: nemotron-mini, llama-3.1-8b, qwen
|
|
209
|
+
else if (modelLower.includes('nemotron-mini') || modelLower.includes('llama-3.1') ||
|
|
210
|
+
modelLower.includes('qwen')) {
|
|
211
|
+
strengths.push('fast', 'reasoning');
|
|
212
|
+
}
|
|
213
|
+
// Budget tier
|
|
180
214
|
else {
|
|
181
215
|
strengths.push('fast');
|
|
182
216
|
}
|
|
@@ -200,6 +234,10 @@ function buildModelProfiles(): Record<string, ModelProfile> {
|
|
|
200
234
|
if (provider.name === 'Anthropic') {
|
|
201
235
|
strengths.push('reasoning', 'creative', 'analysis');
|
|
202
236
|
}
|
|
237
|
+
// Cloudflare Workers AI models (free but limited)
|
|
238
|
+
if (provider.name === 'Cloudflare Workers AI') {
|
|
239
|
+
strengths.push('free', 'fast');
|
|
240
|
+
}
|
|
203
241
|
|
|
204
242
|
// Detect multimodal support
|
|
205
243
|
const supportsMultimodal = provider.supports_multimodal === true;
|
|
@@ -209,6 +247,30 @@ function buildModelProfiles(): Record<string, ModelProfile> {
|
|
|
209
247
|
strengths.push(provider.strategy);
|
|
210
248
|
}
|
|
211
249
|
|
|
250
|
+
// === QUALITY SCORE ASSIGNMENT ===
|
|
251
|
+
// Priority: premium/reasoning > fast+paid > fast/free > budget > default
|
|
252
|
+
// Free models with premium/reasoning tags get same quality as paid models
|
|
253
|
+
let quality_score: number;
|
|
254
|
+
if (strengths.includes('premium') || strengths.includes('reasoning')) {
|
|
255
|
+
// Premium models (including premium free models like nemotron-3-super:free)
|
|
256
|
+
// get high quality score — same as paid premium models
|
|
257
|
+
quality_score = 0.92;
|
|
258
|
+
} else if (strengths.includes('fast') && costPerKInput > 0.3) {
|
|
259
|
+
// Fast mid-tier models (Groq, Cerebras, etc.) get competitive quality
|
|
260
|
+
quality_score = 0.89;
|
|
261
|
+
} else if (strengths.includes('fast') && costPerKInput === 0) {
|
|
262
|
+
// Fast free models (Cloudflare, local models) get moderate quality
|
|
263
|
+
quality_score = 0.78;
|
|
264
|
+
} else if (strengths.includes('budget')) {
|
|
265
|
+
// Budget models (small, cheap)
|
|
266
|
+
quality_score = 0.75;
|
|
267
|
+
} else if (provider.name === 'OpenRouter' && costPerKInput === 0) {
|
|
268
|
+
// OpenRouter free models that don't match premium patterns get boosted
|
|
269
|
+
quality_score = 0.82;
|
|
270
|
+
} else {
|
|
271
|
+
quality_score = 0.80;
|
|
272
|
+
}
|
|
273
|
+
|
|
212
274
|
profiles[modelKey] = {
|
|
213
275
|
name: modelKey,
|
|
214
276
|
provider: providerId,
|
|
@@ -216,9 +278,7 @@ function buildModelProfiles(): Record<string, ModelProfile> {
|
|
|
216
278
|
cost_per_1k_input: costPerKInput,
|
|
217
279
|
cost_per_1k_output: costPerKOutput,
|
|
218
280
|
latency_ms: provider.type === 'cli' ? 5000 : (provider.priority * 200 + 300),
|
|
219
|
-
quality_score
|
|
220
|
-
(strengths.includes('fast') && costPerKInput > 0.3) ? 0.95 : // Boosted for mid-tier competitiveness
|
|
221
|
-
(strengths.includes('budget') || strengths.includes('free')) ? 0.72 : 0.80,
|
|
281
|
+
quality_score,
|
|
222
282
|
strengths,
|
|
223
283
|
context_window: provider.maxTokens || 8192,
|
|
224
284
|
type: provider.type,
|
|
@@ -609,7 +669,13 @@ function scoreModelFit(model: ModelProfile, features: QueryFeatures): number {
|
|
|
609
669
|
// If user has free + groq + openai → quartiles split them naturally.
|
|
610
670
|
const modelCost = (model.cost_per_1k_input + model.cost_per_1k_output) / 2;
|
|
611
671
|
let tierFromModel: string;
|
|
612
|
-
|
|
672
|
+
// Premium free models (with 'premium' or 'reasoning' tag) are treated as 'mid' tier
|
|
673
|
+
// They should be selected for quality, not penalized for being "free"
|
|
674
|
+
const isPremiumFree = model.strengths.includes('free') &&
|
|
675
|
+
(model.strengths.includes('premium') || model.strengths.includes('reasoning'));
|
|
676
|
+
if (isPremiumFree) {
|
|
677
|
+
tierFromModel = 'mid'; // Treat premium free models as mid-tier for scoring
|
|
678
|
+
} else if (model.strengths.includes('free') || modelCost === 0) {
|
|
613
679
|
tierFromModel = 'free';
|
|
614
680
|
} else if (modelCost <= _costPercentiles.p25) {
|
|
615
681
|
tierFromModel = 'cheap';
|
|
@@ -662,8 +728,21 @@ function scoreModelFit(model: ModelProfile, features: QueryFeatures): number {
|
|
|
662
728
|
};
|
|
663
729
|
const bonuses = domainBonus[features.domain] || [];
|
|
664
730
|
if (bonuses.some(b => model.strengths.includes(b))) {
|
|
665
|
-
|
|
666
|
-
|
|
731
|
+
// Separate budget from premium free models
|
|
732
|
+
const isBudget = model.strengths.includes('budget');
|
|
733
|
+
const isPremiumFree = model.strengths.includes('free') &&
|
|
734
|
+
(model.strengths.includes('premium') || model.strengths.includes('reasoning'));
|
|
735
|
+
const isBudgetFree = model.strengths.includes('free') && !isPremiumFree;
|
|
736
|
+
|
|
737
|
+
if (isBudget) {
|
|
738
|
+
score += 0.05; // Budget models get minimal domain boost
|
|
739
|
+
} else if (isPremiumFree) {
|
|
740
|
+
score += 0.15; // Premium free models get significant boost (quality match)
|
|
741
|
+
} else if (isBudgetFree) {
|
|
742
|
+
score += 0.05; // Budget free models get minimal boost
|
|
743
|
+
} else {
|
|
744
|
+
score += 0.25; // Paid premium models get full boost
|
|
745
|
+
}
|
|
667
746
|
}
|
|
668
747
|
}
|
|
669
748
|
|
|
@@ -706,9 +785,15 @@ function scoreModelFit(model: ModelProfile, features: QueryFeatures): number {
|
|
|
706
785
|
score *= 0.8; // Mild penalty for conservative providers on low-risk (cost waste)
|
|
707
786
|
}
|
|
708
787
|
|
|
709
|
-
// Free tier preference for simple queries
|
|
788
|
+
// Free tier preference for simple queries — ONLY for truly budget free models
|
|
789
|
+
// Premium free models (nemotron-super, gemma-4-31b, etc.) should be selected on quality,
|
|
790
|
+
// not because they're free, so they don't get this bonus
|
|
710
791
|
if (features.complexity < 0.5 && model.strengths.includes('free')) {
|
|
711
|
-
|
|
792
|
+
const isBudgetFree = !model.strengths.includes('premium') && !model.strengths.includes('reasoning');
|
|
793
|
+
if (isBudgetFree) {
|
|
794
|
+
score += 0.05; // Small bonus only — prevent budget free models from hitting cap
|
|
795
|
+
}
|
|
796
|
+
// Premium free models: NO bonus — they're selected on quality merits
|
|
712
797
|
}
|
|
713
798
|
|
|
714
799
|
// Fast provider for simple queries
|
|
@@ -127,7 +127,7 @@ export interface ShadowedResponse {
|
|
|
127
127
|
* complexity_adj = query complexity signal
|
|
128
128
|
*/
|
|
129
129
|
export class ShadowSampler {
|
|
130
|
-
private config: Required<Omit<ShadowSamplerConfig, 'forceShadowProvider'>> & { forceShadowProvider: boolean };
|
|
130
|
+
private config: Required<Omit<ShadowSamplerConfig, 'forceShadowProvider' | 'shadowProvider'>> & { forceShadowProvider: boolean; shadowProvider: string | null; };
|
|
131
131
|
|
|
132
132
|
// Cached shadow provider (auto-selected on first call)
|
|
133
133
|
private _shadowProvider: string | null = null;
|