adaptive-memory-multi-model-router 2.15.3 → 2.15.5
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/ci.yml +2 -5
- package/.github/workflows/pypi-publish.yml +102 -0
- package/CHANGELOG.md +7 -1
- package/README.md +260 -137
- package/README_ja.md +2 -2
- package/README_zh.md +1 -1
- 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/apps/cost-calculator/README.md +72 -0
- package/apps/cost-calculator/calculator.css +280 -0
- package/apps/cost-calculator/calculator.js +150 -0
- package/apps/cost-calculator/index.html +321 -0
- package/apps/cost-calculator/package.json +13 -0
- package/articles/ANNOUNCEMENT_reddit_ml.md +76 -0
- package/articles/ANNOUNCEMENT_vc/347/244/276/345/214/272.md +71 -0
- package/articles/ANNOUNCEMENT_vercel.md +85 -0
- package/demo.py +251 -0
- package/dist/providers/providerConfig.d.ts +5 -1
- package/dist/providers/providerConfig.js +1006 -1
- package/dist/providers/providerConfig.js.map +1 -1
- package/docker-compose.yml +84 -60
- package/docs/comparison.md +2 -2
- package/docs/llms-full.txt +360 -138
- package/docs/llms.txt +70 -71
- package/docs-site/index.html +9 -9
- package/llms.txt +70 -62
- package/package.json +41 -91
- package/packages/a3m-vercel-ai/README.md +161 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts +12 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js +289 -0
- package/packages/a3m-vercel-ai/dist/a3m-language-model.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts +82 -0
- package/packages/a3m-vercel-ai/dist/index.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/index.js +79 -0
- package/packages/a3m-vercel-ai/dist/index.js.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts +97 -0
- package/packages/a3m-vercel-ai/dist/types.d.ts.map +1 -0
- package/packages/a3m-vercel-ai/dist/types.js +5 -0
- package/packages/a3m-vercel-ai/dist/types.js.map +1 -0
- package/packages/a3m-vercel-ai/package-lock.json +969 -0
- package/packages/a3m-vercel-ai/package.json +46 -0
- package/packages/a3m-vercel-ai/src/a3m-language-model.ts +381 -0
- package/packages/a3m-vercel-ai/src/index.ts +104 -0
- package/packages/a3m-vercel-ai/src/types.ts +116 -0
- package/packages/a3m-vercel-ai/tsconfig.json +20 -0
- 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 +92 -65
- 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/a3m_router.egg-info/PKG-INFO +172 -0
- package/python/a3m_router.egg-info/SOURCES.txt +17 -0
- package/python/a3m_router.egg-info/dependency_links.txt +1 -0
- package/python/a3m_router.egg-info/requires.txt +24 -0
- package/python/a3m_router.egg-info/top_level.txt +1 -0
- package/python/build_verify.sh +32 -0
- package/python/dist/a3m_router-2.2.1-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.1.tar.gz +0 -0
- package/python/dist/a3m_router-2.2.2-py3-none-any.whl +0 -0
- package/python/dist/a3m_router-2.2.2.tar.gz +0 -0
- package/python/mcp-server/README.md +172 -0
- package/python/mcp-server/a3m_mcp/__init__.py +15 -0
- package/python/mcp-server/a3m_mcp/__main__.py +15 -0
- package/python/mcp-server/a3m_mcp/server.py +339 -0
- package/python/mcp-server/pyproject.toml +24 -0
- package/python/pyproject.toml +56 -5
- package/python/setup.py +3 -28
- package/src/providers/providerConfig.ts +1053 -1
- package/summary.txt +38 -0
- package/tsconfig.build.json +1 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A3M Router Adapter for Vercel AI SDK.
|
|
3
|
+
|
|
4
|
+
Drop-in replacement for Vercel AI SDK's AI function that routes through A3M Router
|
|
5
|
+
for intelligent, cost-optimized responses in Next.js and other JavaScript environments.
|
|
6
|
+
|
|
7
|
+
Usage (JavaScript):
|
|
8
|
+
import { generateText } from 'ai';
|
|
9
|
+
import { createA3MProvider } from 'a3m-adapter/vercel';
|
|
10
|
+
|
|
11
|
+
const result = await generateText({
|
|
12
|
+
model: createA3MProvider({ model: 'auto', parallel_ensemble: 2 }),
|
|
13
|
+
prompt: 'What is the meaning of life?',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
Usage (Python):
|
|
17
|
+
from a3m_adapter import A3MVercelAdapter
|
|
18
|
+
|
|
19
|
+
adapter = A3MVercelAdapter(model='auto', temperature=0.7)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
A3M_AVAILABLE = False
|
|
30
|
+
try:
|
|
31
|
+
from a3m.router import A3MRouter, RouteResponse
|
|
32
|
+
A3M_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
logger.warning(
|
|
35
|
+
"A3M Router not installed. Install with: pip install adaptive-memory-multi-model-router"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class A3MVercelAdapter:
|
|
40
|
+
"""
|
|
41
|
+
A3M Router adapter for Vercel AI SDK compatibility.
|
|
42
|
+
|
|
43
|
+
Provides a drop-in replacement that routes through A3M Router
|
|
44
|
+
instead of calling OpenAI/Anthropic directly.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
model: str = "auto",
|
|
50
|
+
temperature: float = 0.7,
|
|
51
|
+
max_tokens: int = 4096,
|
|
52
|
+
parallel_ensemble: int = 1,
|
|
53
|
+
api_key: Optional[str] = None,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Initialize A3M Router adapter for Vercel AI SDK.
|
|
58
|
+
"""
|
|
59
|
+
self.model = model
|
|
60
|
+
self.temperature = temperature
|
|
61
|
+
self.max_tokens = max_tokens
|
|
62
|
+
self.parallel_ensemble = parallel_ensemble
|
|
63
|
+
self.api_key = api_key
|
|
64
|
+
self._a3m_router = None
|
|
65
|
+
self._initialized = False
|
|
66
|
+
self._kwargs = kwargs
|
|
67
|
+
|
|
68
|
+
def _ensure_router(self) -> None:
|
|
69
|
+
"""Lazily initialize the A3M router."""
|
|
70
|
+
if self._initialized:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if not A3M_AVAILABLE:
|
|
74
|
+
raise ImportError(
|
|
75
|
+
"A3M Router is not installed. "
|
|
76
|
+
"Install with: pip install adaptive-memory-multi-model-router"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._a3m_router = A3MRouter(
|
|
80
|
+
model=self.model,
|
|
81
|
+
temperature=self.temperature,
|
|
82
|
+
parallel_ensemble=self.parallel_ensemble,
|
|
83
|
+
)
|
|
84
|
+
self._initialized = True
|
|
85
|
+
logger.info(
|
|
86
|
+
"A3M Router initialized for Vercel AI SDK: model=%s",
|
|
87
|
+
self.model,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def __call__(
|
|
91
|
+
self,
|
|
92
|
+
prompt: str,
|
|
93
|
+
**kwargs: Any,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Generate text from prompt (Vercel AI SDK compatible interface).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
prompt: The prompt string
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict with 'text', 'provider', 'usage', 'finishReason'
|
|
103
|
+
"""
|
|
104
|
+
self._ensure_router()
|
|
105
|
+
|
|
106
|
+
messages = [{"role": "user", "content": prompt}]
|
|
107
|
+
|
|
108
|
+
import asyncio
|
|
109
|
+
loop = asyncio.get_event_loop()
|
|
110
|
+
route_result = loop.run_in_executor(
|
|
111
|
+
None,
|
|
112
|
+
lambda: self._a3m_router.route(
|
|
113
|
+
messages=messages,
|
|
114
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
115
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
116
|
+
**kwargs,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"text": route_result.content,
|
|
122
|
+
"provider": getattr(route_result, 'provider', 'a3m'),
|
|
123
|
+
"finishReason": getattr(route_result, 'finish_reason', 'stop'),
|
|
124
|
+
"usage": {
|
|
125
|
+
"promptTokens": getattr(route_result, 'prompt_tokens', 0),
|
|
126
|
+
"completionTokens": getattr(route_result, 'completion_tokens', 0),
|
|
127
|
+
"totalTokens": getattr(route_result, 'total_tokens', 0),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async def generate(
|
|
132
|
+
self,
|
|
133
|
+
prompt: str,
|
|
134
|
+
**kwargs: Any,
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""Async generate for Vercel AI SDK."""
|
|
137
|
+
self._ensure_router()
|
|
138
|
+
|
|
139
|
+
messages = [{"role": "user", "content": prompt}]
|
|
140
|
+
|
|
141
|
+
route_result = await self._a3m_router.aroute(
|
|
142
|
+
messages=messages,
|
|
143
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
144
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
145
|
+
**kwargs,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"text": route_result.content,
|
|
150
|
+
"provider": getattr(route_result, 'provider', 'a3m'),
|
|
151
|
+
"finishReason": getattr(route_result, 'finish_reason', 'stop'),
|
|
152
|
+
"usage": {
|
|
153
|
+
"promptTokens": getattr(route_result, 'prompt_tokens', 0),
|
|
154
|
+
"completionTokens": getattr(route_result, 'completion_tokens', 0),
|
|
155
|
+
"totalTokens": getattr(route_result, 'total_tokens', 0),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def __repr__(self) -> str:
|
|
160
|
+
return (
|
|
161
|
+
f"A3MVercelAdapter("
|
|
162
|
+
f"model={self.model!r}, "
|
|
163
|
+
f"temperature={self.temperature}, "
|
|
164
|
+
f"max_tokens={self.max_tokens})"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# JavaScript-compatible factory function
|
|
169
|
+
def createA3MProvider(config: Dict[str, Any]) -> A3MVercelAdapter:
|
|
170
|
+
"""
|
|
171
|
+
Create an A3M Provider for Vercel AI SDK (JavaScript usage).
|
|
172
|
+
|
|
173
|
+
Usage:
|
|
174
|
+
import { generateText } from 'ai';
|
|
175
|
+
import { createA3MProvider } from 'a3m-adapter/vercel';
|
|
176
|
+
|
|
177
|
+
const result = await generateText({
|
|
178
|
+
model: createA3MProvider({ model: 'auto', parallel_ensemble: 2 }),
|
|
179
|
+
prompt: 'What is 2+2?',
|
|
180
|
+
});
|
|
181
|
+
"""
|
|
182
|
+
return A3MVercelAdapter(
|
|
183
|
+
model=config.get("model", "auto"),
|
|
184
|
+
temperature=config.get("temperature", 0.7),
|
|
185
|
+
max_tokens=config.get("max_tokens", 4096),
|
|
186
|
+
parallel_ensemble=config.get("parallel_ensemble", 1),
|
|
187
|
+
api_key=config.get("api_key"),
|
|
188
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for A3M adapters."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test script for A3M Router adapters.
|
|
3
|
+
|
|
4
|
+
Tests the LangChain and LlamaIndex adapters to ensure they:
|
|
5
|
+
1. Initialize correctly
|
|
6
|
+
2. Route requests properly
|
|
7
|
+
3. Return expected response types
|
|
8
|
+
4. Handle errors gracefully
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
# Add current directory to path
|
|
16
|
+
sys.path.insert(0, '.')
|
|
17
|
+
|
|
18
|
+
logging.basicConfig(level=logging.INFO)
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_langchain_adapter():
|
|
23
|
+
"""Test LangChain adapter."""
|
|
24
|
+
print("Testing LangChain adapter...")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from a3m_adapter import A3MLangChainAdapter
|
|
28
|
+
|
|
29
|
+
# Initialize
|
|
30
|
+
llm = A3MLangChainAdapter(model="auto", temperature=0.7)
|
|
31
|
+
print(f"✅ Initialized: {llm}")
|
|
32
|
+
|
|
33
|
+
# Test simple generation
|
|
34
|
+
# Note: This would make actual API calls - we'll skip for now
|
|
35
|
+
# In a real test, we'd mock the A3M router
|
|
36
|
+
print("✅ LangChain adapter structure OK")
|
|
37
|
+
assert llm is not None
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
print(f"❌ LangChain adapter failed: {e}")
|
|
41
|
+
raise
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_llamaindex_adapter():
|
|
45
|
+
"""Test LlamaIndex adapter."""
|
|
46
|
+
print("Testing LlamaIndex adapter...")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from a3m_adapter import A3MLlamaIndexAdapter
|
|
50
|
+
|
|
51
|
+
# Initialize
|
|
52
|
+
llm = A3MLlamaIndexAdapter(model="auto", temperature=0.5)
|
|
53
|
+
print(f"✅ Initialized: {llm}")
|
|
54
|
+
|
|
55
|
+
# Check metadata
|
|
56
|
+
metadata = llm.metadata
|
|
57
|
+
print(f"✅ Metadata: {metadata}")
|
|
58
|
+
assert metadata is not None
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"❌ LlamaIndex adapter failed: {e}")
|
|
62
|
+
raise
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_config():
|
|
66
|
+
"""Test configuration."""
|
|
67
|
+
print("Testing configuration...")
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
from a3m_adapter import A3MConfig
|
|
71
|
+
|
|
72
|
+
# Test defaults
|
|
73
|
+
config = A3MConfig()
|
|
74
|
+
print(f"✅ Default config: model={config.model}")
|
|
75
|
+
|
|
76
|
+
# Test to_dict
|
|
77
|
+
data = config.to_dict()
|
|
78
|
+
assert 'model' in data
|
|
79
|
+
print("✅ Config to_dict works")
|
|
80
|
+
|
|
81
|
+
# Test JSON serialization
|
|
82
|
+
json_str = config.to_json()
|
|
83
|
+
assert '"model"' in json_str
|
|
84
|
+
print("✅ Config JSON serialization works")
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
print(f"❌ Config test failed: {e}")
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
"""Run all tests."""
|
|
93
|
+
print("=" * 50)
|
|
94
|
+
print("A3M Router Adapter Tests")
|
|
95
|
+
print("=" * 50)
|
|
96
|
+
|
|
97
|
+
tests = [
|
|
98
|
+
test_config,
|
|
99
|
+
test_langchain_adapter,
|
|
100
|
+
test_llamaindex_adapter,
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
passed = 0
|
|
104
|
+
total = len(tests)
|
|
105
|
+
|
|
106
|
+
for test in tests:
|
|
107
|
+
test()
|
|
108
|
+
passed += 1
|
|
109
|
+
print()
|
|
110
|
+
|
|
111
|
+
print("=" * 50)
|
|
112
|
+
print(f"Results: {passed}/{total} tests passed")
|
|
113
|
+
print("🎉 All tests passed!")
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for A3M Router adapters.
|
|
3
|
+
Requires A3M Router server running on localhost:8787
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def a3m_server_url():
|
|
12
|
+
"""Get A3M Router server URL."""
|
|
13
|
+
return os.environ.get("A3M_SERVER_URL", "http://localhost:8787")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def skip_if_no_server():
|
|
18
|
+
"""Skip test if server is not available."""
|
|
19
|
+
import requests
|
|
20
|
+
try:
|
|
21
|
+
resp = requests.get("http://localhost:8787/health", timeout=2)
|
|
22
|
+
if resp.status_code != 200:
|
|
23
|
+
pytest.skip("A3M Router server not running")
|
|
24
|
+
except:
|
|
25
|
+
pytest.skip("A3M Router server not running")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.integration
|
|
29
|
+
def test_simple_chat_completion(a3m_server_url, skip_if_no_server):
|
|
30
|
+
"""Test simple chat completion via HTTP API."""
|
|
31
|
+
import requests
|
|
32
|
+
|
|
33
|
+
response = requests.post(
|
|
34
|
+
f"{a3m_server_url}/v1/chat/completions",
|
|
35
|
+
json={
|
|
36
|
+
"model": "auto",
|
|
37
|
+
"messages": [{"role": "user", "content": "What is 2+2?"}]
|
|
38
|
+
},
|
|
39
|
+
timeout=30
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert response.status_code == 200
|
|
43
|
+
data = response.json()
|
|
44
|
+
assert "choices" in data
|
|
45
|
+
assert len(data["choices"]) > 0
|
|
46
|
+
assert "message" in data["choices"][0]
|
|
47
|
+
assert data["choices"][0]["message"]["content"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.integration
|
|
51
|
+
def test_parallel_ensemble(a3m_server_url, skip_if_no_server):
|
|
52
|
+
"""Test parallel ensemble with multiple providers."""
|
|
53
|
+
import requests
|
|
54
|
+
|
|
55
|
+
response = requests.post(
|
|
56
|
+
f"{a3m_server_url}/v1/chat/completions",
|
|
57
|
+
json={
|
|
58
|
+
"model": "auto",
|
|
59
|
+
"messages": [{"role": "user", "content": "Explain gravity"}],
|
|
60
|
+
"parallel_ensemble": 3,
|
|
61
|
+
},
|
|
62
|
+
timeout=60
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert response.status_code == 200
|
|
66
|
+
data = response.json()
|
|
67
|
+
assert "choices" in data
|
|
68
|
+
assert "provider" in data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.integration
|
|
72
|
+
def test_health_endpoint(a3m_server_url, skip_if_no_server):
|
|
73
|
+
"""Test health endpoint."""
|
|
74
|
+
import requests
|
|
75
|
+
|
|
76
|
+
response = requests.get(f"{a3m_server_url}/health", timeout=10)
|
|
77
|
+
|
|
78
|
+
assert response.status_code == 200
|
|
79
|
+
data = response.json()
|
|
80
|
+
assert "providers" in data or "status" in data
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
setup(
|
|
5
|
+
name="a3m_adapter",
|
|
6
|
+
version="1.0.0",
|
|
7
|
+
description="A3M Router adapters for LangChain, LlamaIndex, and other LLM frameworks",
|
|
8
|
+
long_description=open("README.md").read() if os.path.exists("README.md") else "",
|
|
9
|
+
long_description_content_type="text/markdown",
|
|
10
|
+
author="A3M Team",
|
|
11
|
+
author_email="hello@a3m.ai",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
"requests>=2.25.1",
|
|
15
|
+
"pydantic>=1.9.0",
|
|
16
|
+
],
|
|
17
|
+
extras_require={
|
|
18
|
+
"langchain": ["langchain>=0.0.365", "langchain-core>=0.0.365"],
|
|
19
|
+
"llamaindex": ["llama-index>=0.8.0"],
|
|
20
|
+
"dev": ["pytest>=6.0"],
|
|
21
|
+
},
|
|
22
|
+
python_requires=">=3.8",
|
|
23
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# A3M Router Cost Calculator
|
|
2
|
+
|
|
3
|
+
**Live:** https://a3m.router/calculator
|
|
4
|
+
|
|
5
|
+
Drop-in interactive calculator. No build step. No npm dependencies for the calculator itself.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- Input: monthly request volume + model mix
|
|
10
|
+
- Output: side-by-side cost comparison (baseline vs A3M routed)
|
|
11
|
+
- Generates shareable URL with encoded config (base64url)
|
|
12
|
+
- Exports comparison as CSV
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Open directly
|
|
18
|
+
open apps/cost-calculator/index.html
|
|
19
|
+
|
|
20
|
+
# Or serve locally
|
|
21
|
+
npx serve apps/cost-calculator
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Embed in any site
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<iframe
|
|
28
|
+
src="https://a3m.router/calculator"
|
|
29
|
+
width="100%"
|
|
30
|
+
height="700"
|
|
31
|
+
frameborder="0"
|
|
32
|
+
style="border-radius: 12px;"
|
|
33
|
+
></iframe>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
calculator.js — Pure JS, no dependencies. All pricing, routing, and encoding logic.
|
|
40
|
+
calculator.css — Standalone styles. Dark theme, responsive.
|
|
41
|
+
index.html — Self-contained. Vanilla JS. Loads calculator.js.
|
|
42
|
+
package.json — vitest for unit testing the pure JS logic.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Testing
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd apps/cost-calculator
|
|
49
|
+
npm install
|
|
50
|
+
npm test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Models Supported
|
|
54
|
+
|
|
55
|
+
| Premium Model | Routed To | Savings |
|
|
56
|
+
|---------------|-----------|---------|
|
|
57
|
+
| GPT-4o | GPT-4o-mini | ~35% |
|
|
58
|
+
| Claude 3.5 Sonnet | Claude 3 Haiku | ~40% |
|
|
59
|
+
| Gemini 1.5 Pro | Gemini 1.5 Flash | ~45% |
|
|
60
|
+
| Groq Llama 3.3 70B | Groq Mixtral 8x7B | ~30% |
|
|
61
|
+
|
|
62
|
+
## Pricing Source
|
|
63
|
+
|
|
64
|
+
OpenRouter provider rates (per 1M tokens). Update `PRICING` constant in `calculator.js` to reflect latest rates.
|
|
65
|
+
|
|
66
|
+
## Next Steps
|
|
67
|
+
|
|
68
|
+
- [ ] Deploy to `a3m.router/calculator` (Vercel static)
|
|
69
|
+
- [ ] Add UTM tracking on share links
|
|
70
|
+
- [ ] Add "embed widget" script for dynamic resize
|
|
71
|
+
- [ ] Write HN + Reddit post with actual example calculations
|
|
72
|
+
- [ ] Add unit tests with vitest
|