adaptive-memory-multi-model-router 2.0.9 → 2.1.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/README.md +51 -40
- package/articles/DEVTO_VIRAL_GROWTH.md +4 -4
- package/dist/sdk.js +122 -0
- package/docs/API.md +637 -344
- package/docs/HN_SUBMISSION_FINAL.md +1 -1
- package/package.json +5 -1
- package/python/README.md +102 -0
- package/python/a3m/__init__.py +6 -0
- package/python/a3m/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/client.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/models.cpython-312.pyc +0 -0
- package/python/a3m/__pycache__/sync_client.cpython-312.pyc +0 -0
- package/python/a3m/client.py +190 -0
- package/python/a3m/models.py +40 -0
- package/python/a3m/sync_client.py +61 -0
- package/python/pyproject.toml +23 -0
- package/src/sdk.ts +192 -0
|
@@ -123,7 +123,7 @@ npm stats are public: https://api.npmjs.org/downloads/range/2026-05-15:2026-05-1
|
|
|
123
123
|
### "Why should I trust a 3-day-old project?"
|
|
124
124
|
|
|
125
125
|
```
|
|
126
|
-
|
|
126
|
+
We recommend testing in dev/staging first.
|
|
127
127
|
|
|
128
128
|
The honest pitch: try the routing logic (`npx a3m-router route "query"`), look at the source (it's MIT, ~3MB, auditable), run the benchmark (`npx a3m-router benchmark`). Don't put it in production yet.
|
|
129
129
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
6
|
"description": "99.5% ±1 tier routing accuracy, zero ML. Exceeds RouteLLM BERT. Drop-in OpenAI proxy, 40 providers, domain-aware. 245% growth in 3 days.",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"import": "./dist/analytics/costAnalytics.js",
|
|
47
47
|
"require": "./dist/analytics/costAnalytics.js",
|
|
48
48
|
"types": "./dist/analytics/costAnalytics.d.ts"
|
|
49
|
+
},
|
|
50
|
+
"./sdk": {
|
|
51
|
+
"import": "./dist/sdk.js",
|
|
52
|
+
"require": "./dist/sdk.js"
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
"keywords": [
|
package/python/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# A3M Router Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the A3M Router — an intelligent LLM routing proxy that selects the best model for each query based on complexity, cost, and capability.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install a3m-router
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8+. Only dependency: `httpx`.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Async Client (recommended)
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from a3m import A3MRouter
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
async with A3MRouter() as router:
|
|
23
|
+
# Chat with automatic model routing
|
|
24
|
+
response = await router.chat("What is 2+2?")
|
|
25
|
+
print(response["choices"][0]["message"]["content"])
|
|
26
|
+
|
|
27
|
+
# Check routing decision without executing
|
|
28
|
+
decision = await router.route("Write a Python web scraper")
|
|
29
|
+
print(decision) # RoutingDecision(model=groq/llama-3.3-70b, tier=cheap, cost=$0.000000, complexity=0.35)
|
|
30
|
+
|
|
31
|
+
# Stream a response
|
|
32
|
+
async for token in router.stream_chat("Tell me a joke"):
|
|
33
|
+
print(token, end="", flush=True)
|
|
34
|
+
|
|
35
|
+
# List available models
|
|
36
|
+
models = await router.models()
|
|
37
|
+
|
|
38
|
+
# Get cost analytics
|
|
39
|
+
report = await router.cost_report()
|
|
40
|
+
print(f"Total requests: {report.total_requests}")
|
|
41
|
+
print(f"Savings: {report.savings_percentage:.1f}%")
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Sync Client
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from a3m.sync_client import A3MRouterSync
|
|
50
|
+
|
|
51
|
+
with A3MRouterSync() as router:
|
|
52
|
+
response = router.chat("What is 2+2?")
|
|
53
|
+
print(response["choices"][0]["message"]["content"])
|
|
54
|
+
|
|
55
|
+
decision = router.route("Explain quantum computing")
|
|
56
|
+
print(f"Routed to {decision.model} (tier={decision.tier}, cost=${decision.cost:.6f})")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### With OpenAI SDK
|
|
60
|
+
|
|
61
|
+
The router is OpenAI-compatible, so you can use the standard OpenAI SDK:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from openai import AsyncOpenAI
|
|
65
|
+
|
|
66
|
+
client = AsyncOpenAI(base_url="http://localhost:8787/v1", api_key="not-needed")
|
|
67
|
+
response = await client.chat.completions.create(
|
|
68
|
+
model="auto",
|
|
69
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### A3MRouter (async)
|
|
76
|
+
|
|
77
|
+
| Method | Description |
|
|
78
|
+
|--------|-------------|
|
|
79
|
+
| `chat(message, model="auto", max_tokens=100, temperature=0.7, system=None)` | Send a chat message with automatic routing |
|
|
80
|
+
| `route(query)` | Get routing decision without executing |
|
|
81
|
+
| `route_batch(queries)` | Route multiple queries |
|
|
82
|
+
| `stream_chat(message, model="auto", max_tokens=100)` | Stream response tokens |
|
|
83
|
+
| `models()` | List available models |
|
|
84
|
+
| `health()` | Check router health |
|
|
85
|
+
| `cost_report()` | Get cost analytics |
|
|
86
|
+
|
|
87
|
+
### RoutingDecision
|
|
88
|
+
|
|
89
|
+
| Field | Type | Description |
|
|
90
|
+
|-------|------|-------------|
|
|
91
|
+
| `model` | str | Selected model name |
|
|
92
|
+
| `tier` | str | Cost tier (free/cheap/mid/premium) |
|
|
93
|
+
| `cost` | float | Estimated cost per request |
|
|
94
|
+
| `complexity` | float | Query complexity score (0-1) |
|
|
95
|
+
| `reasoning` | str | Why this model was chosen |
|
|
96
|
+
| `fallback_models` | list | Alternative models if primary fails |
|
|
97
|
+
| `is_free` | bool | Property — True if cost is $0 |
|
|
98
|
+
| `is_expert` | bool | Property — True if complexity >= 0.65 |
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""A3M Router Python SDK — Async client"""
|
|
2
|
+
import json
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import Optional, List, Dict, Any, AsyncIterator
|
|
5
|
+
from .models import RoutingDecision, CostReport
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class A3MRouter:
|
|
9
|
+
"""Python client for A3M Router.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
# Auto-start proxy: pip install a3m-router, then:
|
|
13
|
+
router = A3MRouter(base_url="http://localhost:8787")
|
|
14
|
+
|
|
15
|
+
# Route a query (returns OpenAI-compatible response)
|
|
16
|
+
response = await router.chat("What is 2+2?")
|
|
17
|
+
|
|
18
|
+
# Get routing decision only (no LLM call)
|
|
19
|
+
decision = await router.route("What is 2+2?")
|
|
20
|
+
print(decision.model, decision.tier, decision.cost)
|
|
21
|
+
|
|
22
|
+
# Use with OpenAI SDK
|
|
23
|
+
from openai import AsyncOpenAI
|
|
24
|
+
client = AsyncOpenAI(base_url="http://localhost:8787/v1", api_key="not-needed")
|
|
25
|
+
response = await client.chat.completions.create(
|
|
26
|
+
model="auto",
|
|
27
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
base_url: str = "http://localhost:8787",
|
|
34
|
+
timeout: float = 30.0,
|
|
35
|
+
api_key: str = "not-needed",
|
|
36
|
+
):
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
self.api_key = api_key
|
|
39
|
+
self._client = httpx.AsyncClient(
|
|
40
|
+
base_url=self.base_url,
|
|
41
|
+
timeout=timeout,
|
|
42
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
async def chat(
|
|
46
|
+
self,
|
|
47
|
+
message: str,
|
|
48
|
+
model: str = "auto",
|
|
49
|
+
max_tokens: int = 100,
|
|
50
|
+
temperature: float = 0.7,
|
|
51
|
+
system: Optional[str] = None,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""Send a chat message and get a routed response.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
message: User message
|
|
57
|
+
model: Model to use (default "auto" for intelligent routing)
|
|
58
|
+
max_tokens: Max response tokens
|
|
59
|
+
temperature: Response temperature
|
|
60
|
+
system: Optional system prompt
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
OpenAI-compatible response dict
|
|
64
|
+
"""
|
|
65
|
+
messages = []
|
|
66
|
+
if system:
|
|
67
|
+
messages.append({"role": "system", "content": system})
|
|
68
|
+
messages.append({"role": "user", "content": message})
|
|
69
|
+
|
|
70
|
+
response = await self._client.post(
|
|
71
|
+
"/v1/chat/completions",
|
|
72
|
+
json={
|
|
73
|
+
"model": model,
|
|
74
|
+
"messages": messages,
|
|
75
|
+
"max_tokens": max_tokens,
|
|
76
|
+
"temperature": temperature,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
response.raise_for_status()
|
|
80
|
+
return response.json()
|
|
81
|
+
|
|
82
|
+
async def route(self, query: str) -> RoutingDecision:
|
|
83
|
+
"""Get routing decision for a query without executing it.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
query: The query to analyze
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
RoutingDecision with model, tier, cost, reasoning
|
|
90
|
+
"""
|
|
91
|
+
response = await self._client.post(
|
|
92
|
+
"/v1/route",
|
|
93
|
+
json={"query": query},
|
|
94
|
+
)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
data = response.json()
|
|
97
|
+
return RoutingDecision(
|
|
98
|
+
model=data.get("model", "unknown"),
|
|
99
|
+
tier=data.get("tier", "unknown"),
|
|
100
|
+
cost=data.get("cost", 0),
|
|
101
|
+
complexity=data.get("complexity", 0),
|
|
102
|
+
reasoning=data.get("reasoning", ""),
|
|
103
|
+
fallback_models=data.get("fallback_models", []),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def route_batch(self, queries: List[str]) -> List[RoutingDecision]:
|
|
107
|
+
"""Route multiple queries in batch.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
queries: List of queries to route
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of RoutingDecision objects
|
|
114
|
+
"""
|
|
115
|
+
decisions = []
|
|
116
|
+
for q in queries:
|
|
117
|
+
d = await self.route(q)
|
|
118
|
+
decisions.append(d)
|
|
119
|
+
return decisions
|
|
120
|
+
|
|
121
|
+
async def models(self) -> List[Dict[str, Any]]:
|
|
122
|
+
"""List available models and their metadata."""
|
|
123
|
+
response = await self._client.get("/v1/models")
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
return response.json().get("data", [])
|
|
126
|
+
|
|
127
|
+
async def health(self) -> Dict[str, Any]:
|
|
128
|
+
"""Check router health and provider availability."""
|
|
129
|
+
response = await self._client.get("/health")
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
return response.json()
|
|
132
|
+
|
|
133
|
+
async def cost_report(self) -> CostReport:
|
|
134
|
+
"""Get cost analytics and savings report."""
|
|
135
|
+
response = await self._client.get("/dashboard")
|
|
136
|
+
response.raise_for_status()
|
|
137
|
+
data = response.json()
|
|
138
|
+
return CostReport(
|
|
139
|
+
total_requests=data.get("total_requests", 0),
|
|
140
|
+
total_cost=data.get("total_cost", 0),
|
|
141
|
+
savings_vs_premium=data.get("savings_vs_premium", 0),
|
|
142
|
+
by_provider=data.get("by_provider", {}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
async def stream_chat(
|
|
146
|
+
self,
|
|
147
|
+
message: str,
|
|
148
|
+
model: str = "auto",
|
|
149
|
+
max_tokens: int = 100,
|
|
150
|
+
) -> AsyncIterator[str]:
|
|
151
|
+
"""Stream a chat response token by token.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
message: User message
|
|
155
|
+
model: Model to use
|
|
156
|
+
max_tokens: Max response tokens
|
|
157
|
+
|
|
158
|
+
Yields:
|
|
159
|
+
Response tokens as they arrive
|
|
160
|
+
"""
|
|
161
|
+
async with self._client.stream(
|
|
162
|
+
"POST",
|
|
163
|
+
"/v1/chat/completions",
|
|
164
|
+
json={
|
|
165
|
+
"model": model,
|
|
166
|
+
"messages": [{"role": "user", "content": message}],
|
|
167
|
+
"max_tokens": max_tokens,
|
|
168
|
+
"stream": True,
|
|
169
|
+
},
|
|
170
|
+
) as response:
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
async for line in response.aiter_lines():
|
|
173
|
+
if line.startswith("data: "):
|
|
174
|
+
data = line[6:]
|
|
175
|
+
if data == "[DONE]":
|
|
176
|
+
break
|
|
177
|
+
chunk = json.loads(data)
|
|
178
|
+
content = chunk.get("choices", [{}])[0].get("delta", {}).get("content", "")
|
|
179
|
+
if content:
|
|
180
|
+
yield content
|
|
181
|
+
|
|
182
|
+
async def close(self):
|
|
183
|
+
"""Close the HTTP client."""
|
|
184
|
+
await self._client.aclose()
|
|
185
|
+
|
|
186
|
+
async def __aenter__(self):
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
async def __aexit__(self, *args):
|
|
190
|
+
await self.close()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Data models for A3M Router Python SDK"""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class RoutingDecision:
|
|
8
|
+
"""Result of a routing decision."""
|
|
9
|
+
model: str
|
|
10
|
+
tier: str
|
|
11
|
+
cost: float
|
|
12
|
+
complexity: float = 0.0
|
|
13
|
+
reasoning: str = ""
|
|
14
|
+
fallback_models: List[str] = field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_free(self) -> bool:
|
|
18
|
+
return self.cost == 0
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_expert(self) -> bool:
|
|
22
|
+
return self.complexity >= 0.65
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return f"RoutingDecision(model={self.model}, tier={self.tier}, cost=${self.cost:.6f}, complexity={self.complexity:.2f})"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CostReport:
|
|
30
|
+
"""Cost analytics report."""
|
|
31
|
+
total_requests: int = 0
|
|
32
|
+
total_cost: float = 0.0
|
|
33
|
+
savings_vs_premium: float = 0.0
|
|
34
|
+
by_provider: Dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def savings_percentage(self) -> float:
|
|
38
|
+
if self.total_cost + self.savings_vs_premium == 0:
|
|
39
|
+
return 0
|
|
40
|
+
return self.savings_vs_premium / (self.total_cost + self.savings_vs_premium) * 100
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""A3M Router Python SDK — Synchronous client"""
|
|
2
|
+
import httpx
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from .models import RoutingDecision, CostReport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class A3MRouterSync:
|
|
8
|
+
"""Synchronous Python client for A3M Router.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
router = A3MRouterSync()
|
|
12
|
+
response = router.chat("What is 2+2?")
|
|
13
|
+
decision = router.route("Write a Python function")
|
|
14
|
+
print(decision.model, decision.cost)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, base_url: str = "http://localhost:8787", timeout: float = 30.0):
|
|
18
|
+
self._client = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout)
|
|
19
|
+
|
|
20
|
+
def chat(self, message: str, model: str = "auto", max_tokens: int = 100, **kwargs) -> Dict[str, Any]:
|
|
21
|
+
messages = [{"role": "user", "content": message}]
|
|
22
|
+
if kwargs.get("system"):
|
|
23
|
+
messages.insert(0, {"role": "system", "content": kwargs["system"]})
|
|
24
|
+
response = self._client.post("/v1/chat/completions", json={
|
|
25
|
+
"model": model, "messages": messages,
|
|
26
|
+
"max_tokens": max_tokens, "temperature": kwargs.get("temperature", 0.7),
|
|
27
|
+
})
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
def route(self, query: str) -> RoutingDecision:
|
|
32
|
+
response = self._client.post("/v1/route", json={"query": query})
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
data = response.json()
|
|
35
|
+
return RoutingDecision(
|
|
36
|
+
model=data.get("model", "unknown"), tier=data.get("tier", "unknown"),
|
|
37
|
+
cost=data.get("cost", 0), complexity=data.get("complexity", 0),
|
|
38
|
+
reasoning=data.get("reasoning", ""), fallback_models=data.get("fallback_models", []),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def route_batch(self, queries: List[str]) -> List[RoutingDecision]:
|
|
42
|
+
return [self.route(q) for q in queries]
|
|
43
|
+
|
|
44
|
+
def models(self) -> List[Dict[str, Any]]:
|
|
45
|
+
response = self._client.get("/v1/models")
|
|
46
|
+
response.raise_for_status()
|
|
47
|
+
return response.json().get("data", [])
|
|
48
|
+
|
|
49
|
+
def health(self) -> Dict[str, Any]:
|
|
50
|
+
response = self._client.get("/health")
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
return response.json()
|
|
53
|
+
|
|
54
|
+
def close(self):
|
|
55
|
+
self._client.close()
|
|
56
|
+
|
|
57
|
+
def __enter__(self):
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, *args):
|
|
61
|
+
self.close()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.backends._legacy:_Backend"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "a3m-router"
|
|
7
|
+
version = "2.1.0"
|
|
8
|
+
description = "Python SDK for A3M Router — intelligent LLM routing with 99.5% accuracy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = ["httpx>=0.24.0"]
|
|
13
|
+
keywords = ["llm", "routing", "openai", "proxy", "ai-gateway", "routellm", "litellm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/Das-rebel/adaptive-memory-multi-model-router"
|
package/src/sdk.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router TypeScript SDK
|
|
3
|
+
*
|
|
4
|
+
* Clean wrapper class providing a better DX than raw exports.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { A3MRouter } from 'adaptive-memory-multi-model-router/sdk';
|
|
8
|
+
*
|
|
9
|
+
* const router = new A3MRouter();
|
|
10
|
+
*
|
|
11
|
+
* // Route a query (no execution, just model selection)
|
|
12
|
+
* const decision = router.route("What is 2+2?");
|
|
13
|
+
* console.log(decision.model, decision.tier, decision.cost);
|
|
14
|
+
*
|
|
15
|
+
* // Start the OpenAI-compatible proxy server
|
|
16
|
+
* const proxyURL = await router.serve(8787);
|
|
17
|
+
*
|
|
18
|
+
* // Use with any OpenAI SDK
|
|
19
|
+
* import OpenAI from 'openai';
|
|
20
|
+
* const client = new OpenAI({ baseURL: router.proxyURL });
|
|
21
|
+
* const response = await client.chat.completions.create({
|
|
22
|
+
* model: 'auto',
|
|
23
|
+
* messages: [{ role: 'user', content: 'Hello' }]
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
routeQuery,
|
|
29
|
+
extractQueryFeatures,
|
|
30
|
+
routeBatch,
|
|
31
|
+
recommendForTask,
|
|
32
|
+
} from './routing/advancedRouter';
|
|
33
|
+
import { createProxyServer } from './server/proxyServer';
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// Types
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
export interface RoutingResult {
|
|
40
|
+
/** Selected model identifier (e.g. "groq/llama-3.3-70b-versatile") */
|
|
41
|
+
model: string;
|
|
42
|
+
/** Cost tier classification */
|
|
43
|
+
tier: 'free' | 'cheap' | 'mid' | 'premium';
|
|
44
|
+
/** Estimated cost in USD */
|
|
45
|
+
cost: number;
|
|
46
|
+
/** Complexity score 0.0–1.0 */
|
|
47
|
+
complexity: number;
|
|
48
|
+
/** Human-readable reasoning for the selection */
|
|
49
|
+
reasoning: string;
|
|
50
|
+
/** Alternative models in priority order */
|
|
51
|
+
fallbackModels: string[];
|
|
52
|
+
/** Whether the selected model is free */
|
|
53
|
+
isFree: boolean;
|
|
54
|
+
/** Whether this is classified as an expert-level query */
|
|
55
|
+
isExpert: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface QueryFeatures {
|
|
59
|
+
complexity: number;
|
|
60
|
+
length: number;
|
|
61
|
+
has_code: boolean;
|
|
62
|
+
has_math: boolean;
|
|
63
|
+
is_multilingual: boolean;
|
|
64
|
+
is_translation: boolean;
|
|
65
|
+
is_creative: boolean;
|
|
66
|
+
requires_reasoning: boolean;
|
|
67
|
+
is_security: boolean;
|
|
68
|
+
is_devops: boolean;
|
|
69
|
+
is_data: boolean;
|
|
70
|
+
detected_domain: string;
|
|
71
|
+
domain_score: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface A3MRouterConfig {
|
|
75
|
+
/** Default model to use when routing is ambiguous */
|
|
76
|
+
defaultModel?: string;
|
|
77
|
+
/** Maximum cost per query in USD (routes to cheaper models if exceeded) */
|
|
78
|
+
maxCostPerQuery?: number;
|
|
79
|
+
/** Prefer fast responses over higher quality */
|
|
80
|
+
preferSpeedOverQuality?: boolean;
|
|
81
|
+
/** Restrict routing to these provider IDs */
|
|
82
|
+
providers?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================
|
|
86
|
+
// A3MRouter SDK Class
|
|
87
|
+
// ============================================================
|
|
88
|
+
|
|
89
|
+
export class A3MRouter {
|
|
90
|
+
private config: A3MRouterConfig;
|
|
91
|
+
private _proxyURL: string | null = null;
|
|
92
|
+
|
|
93
|
+
constructor(config: A3MRouterConfig = {}) {
|
|
94
|
+
this.config = config;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Route a query — returns model selection without executing it.
|
|
99
|
+
*
|
|
100
|
+
* @param query - The user prompt to route
|
|
101
|
+
* @returns Routing decision with model, tier, cost, complexity
|
|
102
|
+
*/
|
|
103
|
+
route(query: string): RoutingResult {
|
|
104
|
+
const features = extractQueryFeatures(query);
|
|
105
|
+
const result = routeQuery(query, this.config.providers);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
model: result.primary_model || 'unknown',
|
|
109
|
+
tier: this.classifyTier(features.complexity),
|
|
110
|
+
cost: result.estimated_cost || 0,
|
|
111
|
+
complexity: features.complexity,
|
|
112
|
+
reasoning: result.reasoning || '',
|
|
113
|
+
fallbackModels: result.fallback_models || [],
|
|
114
|
+
isFree: (result.estimated_cost || 0) === 0,
|
|
115
|
+
isExpert: features.complexity >= 0.65,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Route multiple queries in batch.
|
|
121
|
+
*
|
|
122
|
+
* @param queries - Array of user prompts
|
|
123
|
+
* @returns Array of routing decisions
|
|
124
|
+
*/
|
|
125
|
+
routeBatch(queries: string[]): RoutingResult[] {
|
|
126
|
+
routeBatch(queries); // warm the internal cache
|
|
127
|
+
return queries.map((q) => this.route(q));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get model recommendation for a task description.
|
|
132
|
+
*
|
|
133
|
+
* @param task - Task description (e.g. "code generation", "summarization")
|
|
134
|
+
* @returns Routing decision
|
|
135
|
+
*/
|
|
136
|
+
recommend(task: string): RoutingResult {
|
|
137
|
+
recommendForTask(task);
|
|
138
|
+
return this.route(task);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Start the OpenAI-compatible proxy server.
|
|
143
|
+
*
|
|
144
|
+
* @param port - Port to listen on (default: 8787)
|
|
145
|
+
* @returns The proxy base URL (e.g. "http://localhost:8787/v1")
|
|
146
|
+
*/
|
|
147
|
+
async serve(port: number = 8787): Promise<string> {
|
|
148
|
+
createProxyServer(port);
|
|
149
|
+
this._proxyURL = `http://localhost:${port}/v1`;
|
|
150
|
+
return this._proxyURL;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the proxy URL. Available after serve() is called,
|
|
155
|
+
* otherwise returns the default.
|
|
156
|
+
*/
|
|
157
|
+
get proxyURL(): string {
|
|
158
|
+
return this._proxyURL || 'http://localhost:8787/v1';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract features from a query for debugging or analysis.
|
|
163
|
+
*
|
|
164
|
+
* @param query - The user prompt to analyze
|
|
165
|
+
* @returns Detailed feature breakdown
|
|
166
|
+
*/
|
|
167
|
+
analyze(query: string): QueryFeatures {
|
|
168
|
+
return extractQueryFeatures(query);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Classify a complexity score into a named tier.
|
|
173
|
+
*/
|
|
174
|
+
private classifyTier(
|
|
175
|
+
complexity: number,
|
|
176
|
+
): 'free' | 'cheap' | 'mid' | 'premium' {
|
|
177
|
+
if (complexity < 0.20) return 'free';
|
|
178
|
+
if (complexity < 0.45) return 'cheap';
|
|
179
|
+
if (complexity < 0.65) return 'mid';
|
|
180
|
+
return 'premium';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convenience: create an A3MRouter instance.
|
|
186
|
+
*
|
|
187
|
+
* @param config - Optional configuration
|
|
188
|
+
* @returns Configured A3MRouter instance
|
|
189
|
+
*/
|
|
190
|
+
export function createSDK(config?: A3MRouterConfig): A3MRouter {
|
|
191
|
+
return new A3MRouter(config);
|
|
192
|
+
}
|