router-maestro 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- router_maestro/__init__.py +1 -1
- router_maestro/cli/client.py +0 -32
- router_maestro/cli/main.py +1 -2
- router_maestro/cli/server.py +1 -1
- router_maestro/config/__init__.py +0 -2
- router_maestro/config/paths.py +0 -1
- router_maestro/server/routes/admin.py +1 -73
- router_maestro/server/schemas/__init__.py +0 -4
- router_maestro/server/schemas/admin.py +0 -19
- {router_maestro-0.1.2.dist-info → router_maestro-0.1.3.dist-info}/METADATA +48 -11
- {router_maestro-0.1.2.dist-info → router_maestro-0.1.3.dist-info}/RECORD +14 -19
- router_maestro/cli/stats.py +0 -76
- router_maestro/stats/__init__.py +0 -14
- router_maestro/stats/heatmap.py +0 -154
- router_maestro/stats/storage.py +0 -228
- router_maestro/stats/tracker.py +0 -73
- {router_maestro-0.1.2.dist-info → router_maestro-0.1.3.dist-info}/WHEEL +0 -0
- {router_maestro-0.1.2.dist-info → router_maestro-0.1.3.dist-info}/entry_points.txt +0 -0
- {router_maestro-0.1.2.dist-info → router_maestro-0.1.3.dist-info}/licenses/LICENSE +0 -0
router_maestro/__init__.py
CHANGED
router_maestro/cli/client.py
CHANGED
|
@@ -241,38 +241,6 @@ class AdminClient:
|
|
|
241
241
|
self._handle_connection_error(e)
|
|
242
242
|
return False
|
|
243
243
|
|
|
244
|
-
async def get_stats(
|
|
245
|
-
self, days: int = 7, provider: str | None = None, model: str | None = None
|
|
246
|
-
) -> dict:
|
|
247
|
-
"""Get usage statistics.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
days: Number of days to query
|
|
251
|
-
provider: Optional provider filter
|
|
252
|
-
model: Optional model filter
|
|
253
|
-
|
|
254
|
-
Returns:
|
|
255
|
-
Stats dict with total_requests, total_tokens, by_provider, by_model
|
|
256
|
-
"""
|
|
257
|
-
try:
|
|
258
|
-
async with httpx.AsyncClient() as client:
|
|
259
|
-
params: dict = {"days": days}
|
|
260
|
-
if provider:
|
|
261
|
-
params["provider"] = provider
|
|
262
|
-
if model:
|
|
263
|
-
params["model"] = model
|
|
264
|
-
|
|
265
|
-
response = await client.get(
|
|
266
|
-
f"{self.endpoint}/api/admin/stats",
|
|
267
|
-
headers=self._get_headers(),
|
|
268
|
-
params=params,
|
|
269
|
-
)
|
|
270
|
-
response.raise_for_status()
|
|
271
|
-
return response.json()
|
|
272
|
-
except httpx.HTTPError as e:
|
|
273
|
-
self._handle_connection_error(e)
|
|
274
|
-
return {}
|
|
275
|
-
|
|
276
244
|
async def test_connection(self) -> dict:
|
|
277
245
|
"""Test connection to the server.
|
|
278
246
|
|
router_maestro/cli/main.py
CHANGED
|
@@ -14,14 +14,13 @@ app = typer.Typer(
|
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
16
|
# Import and register sub-commands
|
|
17
|
-
from router_maestro.cli import auth, config, context, model, server
|
|
17
|
+
from router_maestro.cli import auth, config, context, model, server # noqa: E402
|
|
18
18
|
|
|
19
19
|
app.add_typer(server.app, name="server", help="Manage the API server")
|
|
20
20
|
app.add_typer(auth.app, name="auth", help="Manage authentication for providers")
|
|
21
21
|
app.add_typer(model.app, name="model", help="Manage models and priorities")
|
|
22
22
|
app.add_typer(context.app, name="context", help="Manage deployment contexts")
|
|
23
23
|
app.add_typer(config.app, name="config", help="Manage configuration")
|
|
24
|
-
app.command(name="stats")(stats.stats)
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
@app.callback(invoke_without_command=True)
|
router_maestro/cli/server.py
CHANGED
|
@@ -98,7 +98,7 @@ def status() -> None:
|
|
|
98
98
|
|
|
99
99
|
try:
|
|
100
100
|
data = asyncio.run(client.test_connection())
|
|
101
|
-
console.print(
|
|
101
|
+
console.print("[green]Server is running[/green]")
|
|
102
102
|
console.print(f" Version: {data.get('version', 'unknown')}")
|
|
103
103
|
console.print(f" Status: {data.get('status', 'unknown')}")
|
|
104
104
|
except Exception as e:
|
|
@@ -8,7 +8,6 @@ from router_maestro.config.paths import (
|
|
|
8
8
|
PRIORITIES_FILE,
|
|
9
9
|
PROVIDERS_FILE,
|
|
10
10
|
SERVER_CONFIG_FILE,
|
|
11
|
-
STATS_DB_FILE,
|
|
12
11
|
get_config_dir,
|
|
13
12
|
get_data_dir,
|
|
14
13
|
)
|
|
@@ -45,7 +44,6 @@ __all__ = [
|
|
|
45
44
|
"PROVIDERS_FILE",
|
|
46
45
|
"PRIORITIES_FILE",
|
|
47
46
|
"CONTEXTS_FILE",
|
|
48
|
-
"STATS_DB_FILE",
|
|
49
47
|
"LOG_FILE",
|
|
50
48
|
# Provider models
|
|
51
49
|
"ModelConfig",
|
router_maestro/config/paths.py
CHANGED
|
@@ -46,5 +46,4 @@ SERVER_CONFIG_FILE = get_data_dir() / "server.json"
|
|
|
46
46
|
PROVIDERS_FILE = get_config_dir() / "providers.json"
|
|
47
47
|
PRIORITIES_FILE = get_config_dir() / "priorities.json"
|
|
48
48
|
CONTEXTS_FILE = get_config_dir() / "contexts.json"
|
|
49
|
-
STATS_DB_FILE = get_data_dir() / "stats.db"
|
|
50
49
|
LOG_FILE = get_data_dir() / "router-maestro.log"
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Admin API routes for remote management."""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated
|
|
4
|
-
|
|
5
3
|
import httpx
|
|
6
|
-
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
|
4
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
|
7
5
|
|
|
8
6
|
from router_maestro.auth import AuthManager, AuthType
|
|
9
7
|
from router_maestro.auth.github_oauth import (
|
|
@@ -29,9 +27,7 @@ from router_maestro.server.schemas.admin import (
|
|
|
29
27
|
OAuthStatusResponse,
|
|
30
28
|
PrioritiesResponse,
|
|
31
29
|
PrioritiesUpdateRequest,
|
|
32
|
-
StatsResponse,
|
|
33
30
|
)
|
|
34
|
-
from router_maestro.stats import StatsStorage
|
|
35
31
|
|
|
36
32
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
|
37
33
|
|
|
@@ -288,71 +284,3 @@ async def update_priorities(request: PrioritiesUpdateRequest) -> PrioritiesRespo
|
|
|
288
284
|
priorities=config.priorities,
|
|
289
285
|
fallback=config.fallback.model_dump(),
|
|
290
286
|
)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
# ============================================================================
|
|
294
|
-
# Stats endpoints
|
|
295
|
-
# ============================================================================
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
@router.get("/stats", response_model=StatsResponse)
|
|
299
|
-
async def get_stats(
|
|
300
|
-
days: Annotated[int, Query(ge=1, le=365)] = 7,
|
|
301
|
-
provider: str | None = None,
|
|
302
|
-
model: str | None = None,
|
|
303
|
-
) -> StatsResponse:
|
|
304
|
-
"""Get usage statistics."""
|
|
305
|
-
storage = StatsStorage()
|
|
306
|
-
|
|
307
|
-
# Get total stats
|
|
308
|
-
total = storage.get_total_usage(days=days)
|
|
309
|
-
|
|
310
|
-
# Get stats by model (which includes provider info)
|
|
311
|
-
by_model_raw = storage.get_usage_by_model(days=days)
|
|
312
|
-
|
|
313
|
-
# Aggregate by provider
|
|
314
|
-
by_provider: dict[str, dict] = {}
|
|
315
|
-
by_model: dict[str, dict] = {}
|
|
316
|
-
|
|
317
|
-
for record in by_model_raw:
|
|
318
|
-
provider_name = record["provider"]
|
|
319
|
-
model_name = record["model"]
|
|
320
|
-
model_key = f"{provider_name}/{model_name}"
|
|
321
|
-
|
|
322
|
-
# Filter if requested
|
|
323
|
-
if provider and provider_name != provider:
|
|
324
|
-
continue
|
|
325
|
-
if model and model_name != model:
|
|
326
|
-
continue
|
|
327
|
-
|
|
328
|
-
# Aggregate by provider
|
|
329
|
-
if provider_name not in by_provider:
|
|
330
|
-
by_provider[provider_name] = {
|
|
331
|
-
"total_tokens": 0,
|
|
332
|
-
"prompt_tokens": 0,
|
|
333
|
-
"completion_tokens": 0,
|
|
334
|
-
"request_count": 0,
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
by_provider[provider_name]["total_tokens"] += record.get("total_tokens", 0) or 0
|
|
338
|
-
by_provider[provider_name]["prompt_tokens"] += record.get("prompt_tokens", 0) or 0
|
|
339
|
-
by_provider[provider_name]["completion_tokens"] += record.get("completion_tokens", 0) or 0
|
|
340
|
-
by_provider[provider_name]["request_count"] += record.get("request_count", 0) or 0
|
|
341
|
-
|
|
342
|
-
# Store by model
|
|
343
|
-
by_model[model_key] = {
|
|
344
|
-
"total_tokens": record.get("total_tokens", 0) or 0,
|
|
345
|
-
"prompt_tokens": record.get("prompt_tokens", 0) or 0,
|
|
346
|
-
"completion_tokens": record.get("completion_tokens", 0) or 0,
|
|
347
|
-
"request_count": record.get("request_count", 0) or 0,
|
|
348
|
-
"avg_latency_ms": record.get("avg_latency_ms", 0) or 0,
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return StatsResponse(
|
|
352
|
-
total_requests=total.get("request_count", 0) or 0,
|
|
353
|
-
total_tokens=total.get("total_tokens", 0) or 0,
|
|
354
|
-
prompt_tokens=total.get("prompt_tokens", 0) or 0,
|
|
355
|
-
completion_tokens=total.get("completion_tokens", 0) or 0,
|
|
356
|
-
by_provider=by_provider,
|
|
357
|
-
by_model=by_model,
|
|
358
|
-
)
|
|
@@ -10,8 +10,6 @@ from router_maestro.server.schemas.admin import (
|
|
|
10
10
|
OAuthStatusResponse,
|
|
11
11
|
PrioritiesResponse,
|
|
12
12
|
PrioritiesUpdateRequest,
|
|
13
|
-
StatsQuery,
|
|
14
|
-
StatsResponse,
|
|
15
13
|
)
|
|
16
14
|
from router_maestro.server.schemas.openai import (
|
|
17
15
|
ChatCompletionChoice,
|
|
@@ -39,8 +37,6 @@ __all__ = [
|
|
|
39
37
|
"OAuthStatusResponse",
|
|
40
38
|
"PrioritiesResponse",
|
|
41
39
|
"PrioritiesUpdateRequest",
|
|
42
|
-
"StatsQuery",
|
|
43
|
-
"StatsResponse",
|
|
44
40
|
# OpenAI schemas
|
|
45
41
|
"ChatCompletionChoice",
|
|
46
42
|
"ChatCompletionChunk",
|
|
@@ -66,22 +66,3 @@ class PrioritiesUpdateRequest(BaseModel):
|
|
|
66
66
|
|
|
67
67
|
priorities: list[str] = Field(..., description="New priority list")
|
|
68
68
|
fallback: dict | None = Field(default=None, description="Optional fallback config update")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class StatsQuery(BaseModel):
|
|
72
|
-
"""Query parameters for stats."""
|
|
73
|
-
|
|
74
|
-
days: int = Field(default=7, ge=1, le=365, description="Number of days to query")
|
|
75
|
-
provider: str | None = Field(default=None, description="Filter by provider")
|
|
76
|
-
model: str | None = Field(default=None, description="Filter by model")
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class StatsResponse(BaseModel):
|
|
80
|
-
"""Response for usage statistics."""
|
|
81
|
-
|
|
82
|
-
total_requests: int = Field(default=0)
|
|
83
|
-
total_tokens: int = Field(default=0)
|
|
84
|
-
prompt_tokens: int = Field(default=0)
|
|
85
|
-
completion_tokens: int = Field(default=0)
|
|
86
|
-
by_provider: dict[str, dict] = Field(default_factory=dict)
|
|
87
|
-
by_model: dict[str, dict] = Field(default_factory=dict)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: router-maestro
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Multi-model routing and load balancing system with OpenAI-compatible API
|
|
5
5
|
Author-email: Kanwen Li <likanwen@icloud.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -52,7 +52,6 @@ Router-Maestro acts as a proxy that gives you access to models from multiple pro
|
|
|
52
52
|
- **Dual API compatibility**: Both OpenAI (`/v1/...`) and Anthropic (`/v1/messages`) API formats
|
|
53
53
|
- **Cross-provider translation**: Seamlessly route OpenAI requests to Anthropic providers and vice versa
|
|
54
54
|
- **Configuration hot-reload**: Auto-reload config files every 5 minutes without server restart
|
|
55
|
-
- **Usage tracking**: Token usage statistics with heatmap visualization
|
|
56
55
|
- **CLI management**: Full command-line interface for configuration and server control
|
|
57
56
|
- **Docker ready**: Production-ready Docker images with Traefik integration
|
|
58
57
|
|
|
@@ -60,6 +59,11 @@ Router-Maestro acts as a proxy that gives you access to models from multiple pro
|
|
|
60
59
|
|
|
61
60
|
- [Quick Start](#quick-start)
|
|
62
61
|
- [Core Concepts](#core-concepts)
|
|
62
|
+
- [Model Identification](#model-identification)
|
|
63
|
+
- [Auto-Routing](#auto-routing)
|
|
64
|
+
- [Priority & Fallback](#priority--fallback)
|
|
65
|
+
- [Cross-Provider Translation](#cross-provider-translation)
|
|
66
|
+
- [Contexts](#contexts)
|
|
63
67
|
- [CLI Reference](#cli-reference)
|
|
64
68
|
- [API Reference](#api-reference)
|
|
65
69
|
- [Configuration](#configuration)
|
|
@@ -70,20 +74,36 @@ Router-Maestro acts as a proxy that gives you access to models from multiple pro
|
|
|
70
74
|
|
|
71
75
|
Get up and running in 4 steps:
|
|
72
76
|
|
|
73
|
-
### 1.
|
|
77
|
+
### 1. Start the Server
|
|
78
|
+
|
|
79
|
+
#### Docker (recommended)
|
|
74
80
|
|
|
75
81
|
```bash
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
docker run -d -p 8080:8080 \
|
|
83
|
+
-v ~/.local/share/router-maestro:/home/maestro/.local/share/router-maestro \
|
|
84
|
+
-v ~/.config/router-maestro:/home/maestro/.config/router-maestro \
|
|
85
|
+
likanwen/router-maestro:latest
|
|
79
86
|
```
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
#### Install locally
|
|
82
89
|
|
|
83
90
|
```bash
|
|
91
|
+
pip install router-maestro
|
|
84
92
|
router-maestro server start --port 8080
|
|
85
93
|
```
|
|
86
94
|
|
|
95
|
+
### 2. Set Context (for Docker or Remote)
|
|
96
|
+
|
|
97
|
+
When running via Docker in remote VPS, set up a context to communicate with the containerized server:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install router-maestro # Install CLI locally
|
|
101
|
+
router-maestro context add docker --endpoint http://localhost:8080
|
|
102
|
+
router-maestro context set docker
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> **What's a context?** A context is a named connection profile (endpoint + API key) that lets you manage local or remote Router-Maestro servers. See [Contexts](#contexts) for details.
|
|
106
|
+
|
|
87
107
|
### 3. Authenticate with GitHub Copilot
|
|
88
108
|
|
|
89
109
|
```bash
|
|
@@ -171,6 +191,27 @@ POST /v1/messages {"model": "openai/gpt-4o", ...}
|
|
|
171
191
|
POST /v1/chat/completions {"model": "anthropic/claude-3-5-sonnet", ...}
|
|
172
192
|
```
|
|
173
193
|
|
|
194
|
+
### Contexts
|
|
195
|
+
|
|
196
|
+
A **context** is a named connection profile that stores an endpoint URL and API key. Contexts let you manage multiple Router-Maestro deployments from a single CLI.
|
|
197
|
+
|
|
198
|
+
| Context | Use Case |
|
|
199
|
+
|---------|----------|
|
|
200
|
+
| `local` | Default context for `router-maestro server start` |
|
|
201
|
+
| `docker` | Connect to a local Docker container |
|
|
202
|
+
| `my-vps` | Connect to a remote VPS deployment |
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Add a context
|
|
206
|
+
router-maestro context add my-vps --endpoint https://api.example.com --api-key xxx
|
|
207
|
+
|
|
208
|
+
# Switch contexts
|
|
209
|
+
router-maestro context set my-vps
|
|
210
|
+
|
|
211
|
+
# All CLI commands now target the remote server
|
|
212
|
+
router-maestro model list
|
|
213
|
+
```
|
|
214
|
+
|
|
174
215
|
## CLI Reference
|
|
175
216
|
|
|
176
217
|
### Server
|
|
@@ -214,8 +255,6 @@ POST /v1/chat/completions {"model": "anthropic/claude-3-5-sonnet", ...}
|
|
|
214
255
|
| Command | Description |
|
|
215
256
|
|---------|-------------|
|
|
216
257
|
| `config claude-code` | Generate Claude Code settings |
|
|
217
|
-
| `stats --days 7` | Show usage statistics |
|
|
218
|
-
| `stats --days 30 --heatmap` | Show heatmap visualization |
|
|
219
258
|
|
|
220
259
|
## API Reference
|
|
221
260
|
|
|
@@ -271,7 +310,6 @@ Following XDG Base Directory specification:
|
|
|
271
310
|
| **Data** | `~/.local/share/router-maestro/` | |
|
|
272
311
|
| | `auth.json` | OAuth tokens |
|
|
273
312
|
| | `server.json` | Server state |
|
|
274
|
-
| | `stats.db` | Usage statistics |
|
|
275
313
|
|
|
276
314
|
### Custom Providers
|
|
277
315
|
|
|
@@ -360,7 +398,6 @@ router-maestro context set my-vps
|
|
|
360
398
|
|
|
361
399
|
# Now all commands target the VPS
|
|
362
400
|
router-maestro model list
|
|
363
|
-
router-maestro stats --days 7
|
|
364
401
|
```
|
|
365
402
|
|
|
366
403
|
### HTTPS with Traefik
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
router_maestro/__init__.py,sha256=
|
|
1
|
+
router_maestro/__init__.py,sha256=uL361mf7XE2DqHpdJ9tIuNVF8iku3OIPbWlNADxwL5s,92
|
|
2
2
|
router_maestro/__main__.py,sha256=cUHr8B7JBiv5HhnN6l2iayDkGSBpI5Kf4I3jv9I_I3o,121
|
|
3
3
|
router_maestro/auth/__init__.py,sha256=0JgD1w2gtGSkj809kgSKQanYYkncg6eF-hHoz-jQPgo,353
|
|
4
4
|
router_maestro/auth/github_oauth.py,sha256=acQlAA2Zh6c8KQYdzXbC4ww0EJ41AgvbI5ixpFuNoRg,5060
|
|
@@ -6,16 +6,15 @@ router_maestro/auth/manager.py,sha256=OQjdofmjVndTQQIrAU7O-hJAL-w_ijYRU09fZNU8E3
|
|
|
6
6
|
router_maestro/auth/storage.py,sha256=TCLxgQ1lWcWD4xJXJzx5OMpvuAun_LSRItK0zhR6H0Y,2755
|
|
7
7
|
router_maestro/cli/__init__.py,sha256=yIAshaHpLL0WrDFmRpoMRM2EUe75x0wmM5NlGW3C89s,37
|
|
8
8
|
router_maestro/cli/auth.py,sha256=eq5LBUohbMnHS4dZeyvq4OQAjzdrJ-StP2FGuUhkKa0,5940
|
|
9
|
-
router_maestro/cli/client.py,sha256=
|
|
9
|
+
router_maestro/cli/client.py,sha256=mRzpsA_Dxn-Xq7W1_t6EiyddMI0a3cvuTL6-2JuV4mE,9383
|
|
10
10
|
router_maestro/cli/config.py,sha256=lVmMlUASUynbqOQawuQQhi8C3h2OvGScZvaeIArZ2ns,4662
|
|
11
11
|
router_maestro/cli/context.py,sha256=EPbT7fReIW17veU76CSAcv8QjzMsCIPm1QDBlGsV8fQ,4549
|
|
12
|
-
router_maestro/cli/main.py,sha256=
|
|
12
|
+
router_maestro/cli/main.py,sha256=5yiK4Q149goSB2KKzgMuF5EpcC8FBzOUCkEt8wY5NAU,1314
|
|
13
13
|
router_maestro/cli/model.py,sha256=2IG3IpQWh8Ejdv5Htcgr90O2v2UAa80TU15oOniPdvk,9054
|
|
14
|
-
router_maestro/cli/server.py,sha256=
|
|
15
|
-
router_maestro/
|
|
16
|
-
router_maestro/config/__init__.py,sha256=rATq3PIA8vKLpunsCLeCA7GbKjqyzBvlBgI7OwM-6FI,1664
|
|
14
|
+
router_maestro/cli/server.py,sha256=k2OOBBDob55jsQ7XmJ1Tk4_aJgrmFCu4rZPbZuMfmPQ,3696
|
|
15
|
+
router_maestro/config/__init__.py,sha256=-eiXX97Kbd5eIJvbpLbtD6Xrh7XmyFGM_485h6uBokc,1624
|
|
17
16
|
router_maestro/config/contexts.py,sha256=ujaH5I3xa3Eg_KlRaF5KlmsACuUqoKqsp1-i1J2kQz4,923
|
|
18
|
-
router_maestro/config/paths.py,sha256=
|
|
17
|
+
router_maestro/config/paths.py,sha256=F-Q37bGOnQq32MNNbUbvm_ZfZp6-Ik9nVmroEnNNVC8,1573
|
|
19
18
|
router_maestro/config/priorities.py,sha256=wEBs6CmLQiw0ehuA-kh7topyac6APopxaR83CpnHhDQ,2669
|
|
20
19
|
router_maestro/config/providers.py,sha256=yMU_DFh4_eQXMT8jGImqwzAPHp0L3fR34gG2r-4Zhnw,1158
|
|
21
20
|
router_maestro/config/server.py,sha256=Mr7Axbo8b1z3OFEe_G8VuO7wvx_pA32-ZBbT4nSRdX0,3049
|
|
@@ -35,23 +34,19 @@ router_maestro/server/translation.py,sha256=TrYfIb5M5MAUC4TD5By8X3RONSZ6cV1YS_Jw
|
|
|
35
34
|
router_maestro/server/middleware/__init__.py,sha256=PhtP2E04wApnOUBLE76mrOa0sSHp7reHmIl7PaCAylY,187
|
|
36
35
|
router_maestro/server/middleware/auth.py,sha256=Ak3k5cC8m4qPGUIheuOB--QiFvs6GIAcTRJqtCGCjAA,2018
|
|
37
36
|
router_maestro/server/routes/__init__.py,sha256=eGEpNCnSRVQC1pFL7_evDmZfkMrviuI-n1okAS-YnhM,397
|
|
38
|
-
router_maestro/server/routes/admin.py,sha256=
|
|
37
|
+
router_maestro/server/routes/admin.py,sha256=oub4hDrYaytuorXkJzmz0YZ4Z2rcyNuwKcK_4IGvcDY,8942
|
|
39
38
|
router_maestro/server/routes/anthropic.py,sha256=T5-rHBPDyPxP4Cs0yzm7Kvvn-zgV6jspnZdoSVDeH2w,8041
|
|
40
39
|
router_maestro/server/routes/chat.py,sha256=vyYX1ILhgAb9HYD87h1U3c5btpplqkTaejA81pWg4Oo,4752
|
|
41
40
|
router_maestro/server/routes/models.py,sha256=PTSXojNFN9j90Bke74ZO6sEsfIc8u_4A69eW1QzFIbc,716
|
|
42
|
-
router_maestro/server/schemas/__init__.py,sha256=
|
|
43
|
-
router_maestro/server/schemas/admin.py,sha256=
|
|
41
|
+
router_maestro/server/schemas/__init__.py,sha256=VmJZoTMLb-bF33m79urhbejVdLfjDGMqCJP5QvWbHsU,1176
|
|
42
|
+
router_maestro/server/schemas/admin.py,sha256=DuUojkCcq9n8pDhWG6L0SpzQooh91lmHjCRzgZ4AMwk,2369
|
|
44
43
|
router_maestro/server/schemas/anthropic.py,sha256=hNl6rZ7AX-HdLxtsd0cWpZjpIyK1AkEBcuiQpZQqPYc,6136
|
|
45
44
|
router_maestro/server/schemas/openai.py,sha256=s2487RYIn1h-CIaUpLue9BScDaTsafbVg5yc-kKhfME,2141
|
|
46
|
-
router_maestro/stats/__init__.py,sha256=igFtk7xFgvxk_AQsbQlzz8KBubvrmgnNfe-BS9ufuGM,403
|
|
47
|
-
router_maestro/stats/heatmap.py,sha256=G_yerQcLOdP6cND-KTlhXBBNaRdcOBxy6HzGyUlDj6A,5255
|
|
48
|
-
router_maestro/stats/storage.py,sha256=WsFrhmgs9eWKcAPRGDrhT4Ft1-Uzuiz3Q0fYNfgVvQs,7676
|
|
49
|
-
router_maestro/stats/tracker.py,sha256=iutAfdm6JL2rUTBLUnLbUiSaBEAxkzXmi8d5iEllCcI,1984
|
|
50
45
|
router_maestro/utils/__init__.py,sha256=oSQyV--FueMPggRfjWWVnAKtjkcZWFOm9hCTymu0oZU,409
|
|
51
46
|
router_maestro/utils/logging.py,sha256=gJWoRYibAxCWn4VmTmnrwpBRzQ7Uu5YIEk5zDiF9X_k,2393
|
|
52
47
|
router_maestro/utils/tokens.py,sha256=t2E5BrrE5X3VCgw-rYFMkic7heJ0huj9rrOXAIlKq8o,1330
|
|
53
|
-
router_maestro-0.1.
|
|
54
|
-
router_maestro-0.1.
|
|
55
|
-
router_maestro-0.1.
|
|
56
|
-
router_maestro-0.1.
|
|
57
|
-
router_maestro-0.1.
|
|
48
|
+
router_maestro-0.1.3.dist-info/METADATA,sha256=vk_n3Hqc8v9d6uBHNcZgjlA5oSvfcLZSwhD2sMXai1o,11343
|
|
49
|
+
router_maestro-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
50
|
+
router_maestro-0.1.3.dist-info/entry_points.txt,sha256=zoFUxxvNcFe0nTgpRbIdygIDEOla3KbvW6HbOCOlgv4,63
|
|
51
|
+
router_maestro-0.1.3.dist-info/licenses/LICENSE,sha256=Ea86BSGu7_tpLAuzif_JmM9zjMoKQEf95VVF9sZw3Jo,1084
|
|
52
|
+
router_maestro-0.1.3.dist-info/RECORD,,
|
router_maestro/cli/stats.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""Token usage statistics command."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.table import Table
|
|
8
|
-
|
|
9
|
-
from router_maestro.cli.client import AdminClientError, get_admin_client
|
|
10
|
-
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def stats(
|
|
15
|
-
days: int = typer.Option(7, "--days", "-d", help="Number of days to show"),
|
|
16
|
-
provider: str = typer.Option(None, "--provider", "-p", help="Filter by provider"),
|
|
17
|
-
model: str = typer.Option(None, "--model", "-m", help="Filter by model"),
|
|
18
|
-
) -> None:
|
|
19
|
-
"""Show token usage statistics."""
|
|
20
|
-
client = get_admin_client()
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
data = asyncio.run(client.get_stats(days=days, provider=provider, model=model))
|
|
24
|
-
except AdminClientError as e:
|
|
25
|
-
console.print(f"[red]{e}[/red]")
|
|
26
|
-
raise typer.Exit(1)
|
|
27
|
-
except Exception as e:
|
|
28
|
-
console.print(f"[red]Failed to get stats: {e}[/red]")
|
|
29
|
-
raise typer.Exit(1)
|
|
30
|
-
|
|
31
|
-
if data.get("total_requests", 0) == 0:
|
|
32
|
-
console.print("[dim]No usage data available.[/dim]")
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
# Summary table
|
|
36
|
-
console.print(f"\n[bold]Token Usage Summary (Last {days} Days)[/bold]\n")
|
|
37
|
-
|
|
38
|
-
summary_table = Table(show_header=False, box=None)
|
|
39
|
-
summary_table.add_column("Metric", style="cyan")
|
|
40
|
-
summary_table.add_column("Value", style="green", justify="right")
|
|
41
|
-
|
|
42
|
-
summary_table.add_row("Total Requests", f"{data.get('total_requests', 0):,}")
|
|
43
|
-
summary_table.add_row("Total Tokens", f"{data.get('total_tokens', 0):,}")
|
|
44
|
-
summary_table.add_row(" Prompt", f"{data.get('prompt_tokens', 0):,}")
|
|
45
|
-
summary_table.add_row(" Completion", f"{data.get('completion_tokens', 0):,}")
|
|
46
|
-
|
|
47
|
-
console.print(summary_table)
|
|
48
|
-
|
|
49
|
-
# By model table
|
|
50
|
-
by_model = data.get("by_model", {})
|
|
51
|
-
if by_model:
|
|
52
|
-
console.print("\n[bold]Usage by Model[/bold]\n")
|
|
53
|
-
|
|
54
|
-
model_table = Table()
|
|
55
|
-
model_table.add_column("Model", style="cyan")
|
|
56
|
-
model_table.add_column("Provider", style="magenta")
|
|
57
|
-
model_table.add_column("Requests", justify="right")
|
|
58
|
-
model_table.add_column("Total Tokens", justify="right", style="green")
|
|
59
|
-
model_table.add_column("Avg Latency", justify="right")
|
|
60
|
-
|
|
61
|
-
for model_key, record in by_model.items():
|
|
62
|
-
parts = model_key.split("/", 1)
|
|
63
|
-
provider_name = parts[0] if len(parts) > 1 else "-"
|
|
64
|
-
model_name = parts[1] if len(parts) > 1 else model_key
|
|
65
|
-
|
|
66
|
-
avg_latency = record.get("avg_latency_ms")
|
|
67
|
-
latency = f"{avg_latency:.0f} ms" if avg_latency else "-"
|
|
68
|
-
model_table.add_row(
|
|
69
|
-
model_name,
|
|
70
|
-
provider_name,
|
|
71
|
-
f"{record.get('request_count', 0):,}",
|
|
72
|
-
f"{record.get('total_tokens', 0):,}",
|
|
73
|
-
latency,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
console.print(model_table)
|
router_maestro/stats/__init__.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Stats module for router-maestro."""
|
|
2
|
-
|
|
3
|
-
from router_maestro.stats.heatmap import display_stats_summary, generate_heatmap
|
|
4
|
-
from router_maestro.stats.storage import StatsStorage, UsageRecord
|
|
5
|
-
from router_maestro.stats.tracker import RequestTimer, UsageTracker
|
|
6
|
-
|
|
7
|
-
__all__ = [
|
|
8
|
-
"StatsStorage",
|
|
9
|
-
"UsageRecord",
|
|
10
|
-
"UsageTracker",
|
|
11
|
-
"RequestTimer",
|
|
12
|
-
"generate_heatmap",
|
|
13
|
-
"display_stats_summary",
|
|
14
|
-
]
|
router_maestro/stats/heatmap.py
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
"""Terminal heatmap generation for token usage."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timedelta
|
|
4
|
-
|
|
5
|
-
import plotext as plt
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.table import Table
|
|
8
|
-
|
|
9
|
-
from router_maestro.stats.storage import StatsStorage
|
|
10
|
-
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def generate_heatmap(days: int = 7, provider: str | None = None, model: str | None = None) -> None:
|
|
15
|
-
"""Generate and display a terminal heatmap of token usage.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
days: Number of days to show
|
|
19
|
-
provider: Filter by provider (optional)
|
|
20
|
-
model: Filter by model (optional)
|
|
21
|
-
"""
|
|
22
|
-
storage = StatsStorage()
|
|
23
|
-
hourly_data = storage.get_usage_by_hour(days=days, provider=provider, model=model)
|
|
24
|
-
|
|
25
|
-
if not hourly_data:
|
|
26
|
-
console.print("[dim]No usage data available for the specified period.[/dim]")
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
# Build a matrix for the heatmap: rows = days, columns = hours
|
|
30
|
-
# Initialize with zeros
|
|
31
|
-
today = datetime.now().date()
|
|
32
|
-
dates = [(today - timedelta(days=i)) for i in range(days - 1, -1, -1)]
|
|
33
|
-
|
|
34
|
-
# Create a 2D matrix (days x 24 hours)
|
|
35
|
-
matrix = [[0 for _ in range(24)] for _ in range(len(dates))]
|
|
36
|
-
date_to_idx = {d: i for i, d in enumerate(dates)}
|
|
37
|
-
|
|
38
|
-
for record in hourly_data:
|
|
39
|
-
date = datetime.fromisoformat(record["date"]).date()
|
|
40
|
-
hour = record["hour"]
|
|
41
|
-
tokens = record["total_tokens"]
|
|
42
|
-
|
|
43
|
-
if date in date_to_idx:
|
|
44
|
-
matrix[date_to_idx[date]][hour] = tokens
|
|
45
|
-
|
|
46
|
-
# Display using plotext
|
|
47
|
-
plt.clear_figure()
|
|
48
|
-
plt.title(f"Token Usage Heatmap (Last {days} Days)")
|
|
49
|
-
|
|
50
|
-
# Create a simple bar chart by day since plotext doesn't have heatmap
|
|
51
|
-
daily_data = storage.get_usage_by_day(days=days, provider=provider, model=model)
|
|
52
|
-
|
|
53
|
-
if daily_data:
|
|
54
|
-
dates_str = [record["date"] for record in daily_data]
|
|
55
|
-
tokens = [record["total_tokens"] for record in daily_data]
|
|
56
|
-
|
|
57
|
-
plt.bar(dates_str, tokens)
|
|
58
|
-
plt.xlabel("Date")
|
|
59
|
-
plt.ylabel("Total Tokens")
|
|
60
|
-
plt.show()
|
|
61
|
-
|
|
62
|
-
# Also show a text-based heatmap using Rich
|
|
63
|
-
_display_text_heatmap(dates, matrix)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _display_text_heatmap(dates: list, matrix: list[list[int]]) -> None:
|
|
67
|
-
"""Display a text-based heatmap using Rich.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
dates: List of dates
|
|
71
|
-
matrix: 2D matrix of token counts (days x hours)
|
|
72
|
-
"""
|
|
73
|
-
console.print("\n[bold]Hourly Activity Heatmap:[/bold]")
|
|
74
|
-
|
|
75
|
-
# Find max value for scaling
|
|
76
|
-
max_val = max(max(row) for row in matrix) if matrix else 1
|
|
77
|
-
if max_val == 0:
|
|
78
|
-
max_val = 1
|
|
79
|
-
|
|
80
|
-
# Create intensity characters
|
|
81
|
-
intensity_chars = " ░▒▓█"
|
|
82
|
-
|
|
83
|
-
# Build the heatmap
|
|
84
|
-
hour_labels = " " + "".join(f"{h:2d}" for h in range(0, 24, 2))
|
|
85
|
-
console.print(f"[dim]{hour_labels}[/dim]")
|
|
86
|
-
|
|
87
|
-
for i, date in enumerate(dates):
|
|
88
|
-
row_str = f"{date.strftime('%m/%d')} "
|
|
89
|
-
for h in range(24):
|
|
90
|
-
value = matrix[i][h]
|
|
91
|
-
intensity = int((value / max_val) * (len(intensity_chars) - 1))
|
|
92
|
-
char = intensity_chars[intensity]
|
|
93
|
-
row_str += char
|
|
94
|
-
console.print(row_str)
|
|
95
|
-
|
|
96
|
-
# Legend
|
|
97
|
-
console.print(f"\n[dim]Legend: {' '.join(intensity_chars)} (low to high)[/dim]")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def display_stats_summary(days: int = 7) -> None:
|
|
101
|
-
"""Display a summary of token usage statistics.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
days: Number of days to summarize
|
|
105
|
-
"""
|
|
106
|
-
storage = StatsStorage()
|
|
107
|
-
total = storage.get_total_usage(days=days)
|
|
108
|
-
by_model = storage.get_usage_by_model(days=days)
|
|
109
|
-
|
|
110
|
-
if not total or total.get("total_tokens") is None:
|
|
111
|
-
console.print("[dim]No usage data available.[/dim]")
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
# Summary table
|
|
115
|
-
console.print(f"\n[bold]Token Usage Summary (Last {days} Days)[/bold]\n")
|
|
116
|
-
|
|
117
|
-
summary_table = Table(show_header=False, box=None)
|
|
118
|
-
summary_table.add_column("Metric", style="cyan")
|
|
119
|
-
summary_table.add_column("Value", style="green", justify="right")
|
|
120
|
-
|
|
121
|
-
summary_table.add_row("Total Requests", f"{total.get('request_count', 0):,}")
|
|
122
|
-
summary_table.add_row("Successful", f"{total.get('success_count', 0):,}")
|
|
123
|
-
summary_table.add_row("Total Tokens", f"{total.get('total_tokens', 0):,}")
|
|
124
|
-
summary_table.add_row(" Prompt", f"{total.get('prompt_tokens', 0):,}")
|
|
125
|
-
summary_table.add_row(" Completion", f"{total.get('completion_tokens', 0):,}")
|
|
126
|
-
|
|
127
|
-
if total.get("avg_latency_ms"):
|
|
128
|
-
summary_table.add_row("Avg Latency", f"{total.get('avg_latency_ms', 0):.0f} ms")
|
|
129
|
-
|
|
130
|
-
console.print(summary_table)
|
|
131
|
-
|
|
132
|
-
# By model table
|
|
133
|
-
if by_model:
|
|
134
|
-
console.print("\n[bold]Usage by Model[/bold]\n")
|
|
135
|
-
|
|
136
|
-
model_table = Table()
|
|
137
|
-
model_table.add_column("Model", style="cyan")
|
|
138
|
-
model_table.add_column("Provider", style="magenta")
|
|
139
|
-
model_table.add_column("Requests", justify="right")
|
|
140
|
-
model_table.add_column("Total Tokens", justify="right", style="green")
|
|
141
|
-
model_table.add_column("Avg Latency", justify="right")
|
|
142
|
-
|
|
143
|
-
for record in by_model:
|
|
144
|
-
avg_latency = record.get("avg_latency_ms")
|
|
145
|
-
latency = f"{avg_latency:.0f} ms" if avg_latency else "-"
|
|
146
|
-
model_table.add_row(
|
|
147
|
-
record["model"],
|
|
148
|
-
record["provider"],
|
|
149
|
-
f"{record['request_count']:,}",
|
|
150
|
-
f"{record['total_tokens']:,}",
|
|
151
|
-
latency,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
console.print(model_table)
|
router_maestro/stats/storage.py
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""SQLite storage for token usage statistics."""
|
|
2
|
-
|
|
3
|
-
import sqlite3
|
|
4
|
-
from collections.abc import Iterator
|
|
5
|
-
from contextlib import contextmanager
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel
|
|
10
|
-
|
|
11
|
-
from router_maestro.config import STATS_DB_FILE
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class UsageRecord(BaseModel):
|
|
15
|
-
"""A single token usage record."""
|
|
16
|
-
|
|
17
|
-
id: int | None = None
|
|
18
|
-
timestamp: datetime
|
|
19
|
-
provider: str
|
|
20
|
-
model: str
|
|
21
|
-
prompt_tokens: int
|
|
22
|
-
completion_tokens: int
|
|
23
|
-
total_tokens: int
|
|
24
|
-
success: bool
|
|
25
|
-
latency_ms: int | None = None
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class StatsStorage:
|
|
29
|
-
"""SQLite storage for token usage statistics."""
|
|
30
|
-
|
|
31
|
-
def __init__(self, db_path: Path = STATS_DB_FILE) -> None:
|
|
32
|
-
self.db_path = db_path
|
|
33
|
-
self._init_db()
|
|
34
|
-
|
|
35
|
-
def _init_db(self) -> None:
|
|
36
|
-
"""Initialize the database schema."""
|
|
37
|
-
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
|
|
39
|
-
with self._get_connection() as conn:
|
|
40
|
-
conn.execute("""
|
|
41
|
-
CREATE TABLE IF NOT EXISTS usage (
|
|
42
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
-
timestamp TEXT NOT NULL,
|
|
44
|
-
provider TEXT NOT NULL,
|
|
45
|
-
model TEXT NOT NULL,
|
|
46
|
-
prompt_tokens INTEGER NOT NULL,
|
|
47
|
-
completion_tokens INTEGER NOT NULL,
|
|
48
|
-
total_tokens INTEGER NOT NULL,
|
|
49
|
-
success INTEGER NOT NULL,
|
|
50
|
-
latency_ms INTEGER
|
|
51
|
-
)
|
|
52
|
-
""")
|
|
53
|
-
conn.execute("""
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage(timestamp)
|
|
55
|
-
""")
|
|
56
|
-
conn.execute("""
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage(provider)
|
|
58
|
-
""")
|
|
59
|
-
conn.execute("""
|
|
60
|
-
CREATE INDEX IF NOT EXISTS idx_usage_model ON usage(model)
|
|
61
|
-
""")
|
|
62
|
-
conn.commit()
|
|
63
|
-
|
|
64
|
-
@contextmanager
|
|
65
|
-
def _get_connection(self) -> Iterator[sqlite3.Connection]:
|
|
66
|
-
"""Get a database connection."""
|
|
67
|
-
conn = sqlite3.connect(self.db_path)
|
|
68
|
-
conn.row_factory = sqlite3.Row
|
|
69
|
-
try:
|
|
70
|
-
yield conn
|
|
71
|
-
finally:
|
|
72
|
-
conn.close()
|
|
73
|
-
|
|
74
|
-
def record(self, record: UsageRecord) -> None:
|
|
75
|
-
"""Record a usage event."""
|
|
76
|
-
with self._get_connection() as conn:
|
|
77
|
-
conn.execute(
|
|
78
|
-
"""
|
|
79
|
-
INSERT INTO usage (
|
|
80
|
-
timestamp, provider, model,
|
|
81
|
-
prompt_tokens, completion_tokens, total_tokens,
|
|
82
|
-
success, latency_ms
|
|
83
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
84
|
-
""",
|
|
85
|
-
(
|
|
86
|
-
record.timestamp.isoformat(),
|
|
87
|
-
record.provider,
|
|
88
|
-
record.model,
|
|
89
|
-
record.prompt_tokens,
|
|
90
|
-
record.completion_tokens,
|
|
91
|
-
record.total_tokens,
|
|
92
|
-
1 if record.success else 0,
|
|
93
|
-
record.latency_ms,
|
|
94
|
-
),
|
|
95
|
-
)
|
|
96
|
-
conn.commit()
|
|
97
|
-
|
|
98
|
-
def get_usage_by_day(
|
|
99
|
-
self, days: int = 7, provider: str | None = None, model: str | None = None
|
|
100
|
-
) -> list[dict]:
|
|
101
|
-
"""Get usage aggregated by day.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
days: Number of days to look back
|
|
105
|
-
provider: Filter by provider (optional)
|
|
106
|
-
model: Filter by model (optional)
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
List of dicts with date, total_tokens, request_count
|
|
110
|
-
"""
|
|
111
|
-
with self._get_connection() as conn:
|
|
112
|
-
query = """
|
|
113
|
-
SELECT
|
|
114
|
-
DATE(timestamp) as date,
|
|
115
|
-
SUM(total_tokens) as total_tokens,
|
|
116
|
-
SUM(prompt_tokens) as prompt_tokens,
|
|
117
|
-
SUM(completion_tokens) as completion_tokens,
|
|
118
|
-
COUNT(*) as request_count,
|
|
119
|
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count
|
|
120
|
-
FROM usage
|
|
121
|
-
WHERE timestamp >= datetime('now', ?)
|
|
122
|
-
"""
|
|
123
|
-
params: list = [f"-{days} days"]
|
|
124
|
-
|
|
125
|
-
if provider:
|
|
126
|
-
query += " AND provider = ?"
|
|
127
|
-
params.append(provider)
|
|
128
|
-
if model:
|
|
129
|
-
query += " AND model = ?"
|
|
130
|
-
params.append(model)
|
|
131
|
-
|
|
132
|
-
query += " GROUP BY DATE(timestamp) ORDER BY date"
|
|
133
|
-
|
|
134
|
-
cursor = conn.execute(query, params)
|
|
135
|
-
return [dict(row) for row in cursor.fetchall()]
|
|
136
|
-
|
|
137
|
-
def get_usage_by_hour(
|
|
138
|
-
self, days: int = 7, provider: str | None = None, model: str | None = None
|
|
139
|
-
) -> list[dict]:
|
|
140
|
-
"""Get usage aggregated by hour.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
days: Number of days to look back
|
|
144
|
-
provider: Filter by provider (optional)
|
|
145
|
-
model: Filter by model (optional)
|
|
146
|
-
|
|
147
|
-
Returns:
|
|
148
|
-
List of dicts with date, hour, total_tokens, request_count
|
|
149
|
-
"""
|
|
150
|
-
with self._get_connection() as conn:
|
|
151
|
-
query = """
|
|
152
|
-
SELECT
|
|
153
|
-
DATE(timestamp) as date,
|
|
154
|
-
CAST(strftime('%H', timestamp) AS INTEGER) as hour,
|
|
155
|
-
SUM(total_tokens) as total_tokens,
|
|
156
|
-
COUNT(*) as request_count
|
|
157
|
-
FROM usage
|
|
158
|
-
WHERE timestamp >= datetime('now', ?)
|
|
159
|
-
"""
|
|
160
|
-
params: list = [f"-{days} days"]
|
|
161
|
-
|
|
162
|
-
if provider:
|
|
163
|
-
query += " AND provider = ?"
|
|
164
|
-
params.append(provider)
|
|
165
|
-
if model:
|
|
166
|
-
query += " AND model = ?"
|
|
167
|
-
params.append(model)
|
|
168
|
-
|
|
169
|
-
query += " GROUP BY DATE(timestamp), hour ORDER BY date, hour"
|
|
170
|
-
|
|
171
|
-
cursor = conn.execute(query, params)
|
|
172
|
-
return [dict(row) for row in cursor.fetchall()]
|
|
173
|
-
|
|
174
|
-
def get_usage_by_model(self, days: int = 7) -> list[dict]:
|
|
175
|
-
"""Get usage aggregated by model.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
days: Number of days to look back
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
List of dicts with model, provider, total_tokens, request_count
|
|
182
|
-
"""
|
|
183
|
-
with self._get_connection() as conn:
|
|
184
|
-
cursor = conn.execute(
|
|
185
|
-
"""
|
|
186
|
-
SELECT
|
|
187
|
-
model,
|
|
188
|
-
provider,
|
|
189
|
-
SUM(total_tokens) as total_tokens,
|
|
190
|
-
SUM(prompt_tokens) as prompt_tokens,
|
|
191
|
-
SUM(completion_tokens) as completion_tokens,
|
|
192
|
-
COUNT(*) as request_count,
|
|
193
|
-
AVG(latency_ms) as avg_latency_ms
|
|
194
|
-
FROM usage
|
|
195
|
-
WHERE timestamp >= datetime('now', ?)
|
|
196
|
-
GROUP BY model, provider
|
|
197
|
-
ORDER BY total_tokens DESC
|
|
198
|
-
""",
|
|
199
|
-
(f"-{days} days",),
|
|
200
|
-
)
|
|
201
|
-
return [dict(row) for row in cursor.fetchall()]
|
|
202
|
-
|
|
203
|
-
def get_total_usage(self, days: int = 7) -> dict:
|
|
204
|
-
"""Get total usage statistics.
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
days: Number of days to look back
|
|
208
|
-
|
|
209
|
-
Returns:
|
|
210
|
-
Dict with total statistics
|
|
211
|
-
"""
|
|
212
|
-
with self._get_connection() as conn:
|
|
213
|
-
cursor = conn.execute(
|
|
214
|
-
"""
|
|
215
|
-
SELECT
|
|
216
|
-
SUM(total_tokens) as total_tokens,
|
|
217
|
-
SUM(prompt_tokens) as prompt_tokens,
|
|
218
|
-
SUM(completion_tokens) as completion_tokens,
|
|
219
|
-
COUNT(*) as request_count,
|
|
220
|
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as success_count,
|
|
221
|
-
AVG(latency_ms) as avg_latency_ms
|
|
222
|
-
FROM usage
|
|
223
|
-
WHERE timestamp >= datetime('now', ?)
|
|
224
|
-
""",
|
|
225
|
-
(f"-{days} days",),
|
|
226
|
-
)
|
|
227
|
-
row = cursor.fetchone()
|
|
228
|
-
return dict(row) if row else {}
|
router_maestro/stats/tracker.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""Token usage tracker."""
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
|
|
6
|
-
from router_maestro.stats.storage import StatsStorage, UsageRecord
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class UsageTracker:
|
|
10
|
-
"""Tracks token usage for requests."""
|
|
11
|
-
|
|
12
|
-
_instance: "UsageTracker | None" = None
|
|
13
|
-
|
|
14
|
-
def __init__(self) -> None:
|
|
15
|
-
self.storage = StatsStorage()
|
|
16
|
-
|
|
17
|
-
@classmethod
|
|
18
|
-
def get_instance(cls) -> "UsageTracker":
|
|
19
|
-
"""Get the singleton instance."""
|
|
20
|
-
if cls._instance is None:
|
|
21
|
-
cls._instance = cls()
|
|
22
|
-
return cls._instance
|
|
23
|
-
|
|
24
|
-
def record(
|
|
25
|
-
self,
|
|
26
|
-
provider: str,
|
|
27
|
-
model: str,
|
|
28
|
-
prompt_tokens: int,
|
|
29
|
-
completion_tokens: int,
|
|
30
|
-
success: bool = True,
|
|
31
|
-
latency_ms: int | None = None,
|
|
32
|
-
) -> None:
|
|
33
|
-
"""Record a usage event.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
provider: Provider name
|
|
37
|
-
model: Model name
|
|
38
|
-
prompt_tokens: Number of prompt tokens
|
|
39
|
-
completion_tokens: Number of completion tokens
|
|
40
|
-
success: Whether the request was successful
|
|
41
|
-
latency_ms: Latency in milliseconds
|
|
42
|
-
"""
|
|
43
|
-
record = UsageRecord(
|
|
44
|
-
timestamp=datetime.now(),
|
|
45
|
-
provider=provider,
|
|
46
|
-
model=model,
|
|
47
|
-
prompt_tokens=prompt_tokens,
|
|
48
|
-
completion_tokens=completion_tokens,
|
|
49
|
-
total_tokens=prompt_tokens + completion_tokens,
|
|
50
|
-
success=success,
|
|
51
|
-
latency_ms=latency_ms,
|
|
52
|
-
)
|
|
53
|
-
self.storage.record(record)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class RequestTimer:
|
|
57
|
-
"""Context manager for timing requests."""
|
|
58
|
-
|
|
59
|
-
def __init__(self) -> None:
|
|
60
|
-
self.start_time: float = 0
|
|
61
|
-
self.end_time: float = 0
|
|
62
|
-
|
|
63
|
-
def __enter__(self) -> "RequestTimer":
|
|
64
|
-
self.start_time = time.perf_counter()
|
|
65
|
-
return self
|
|
66
|
-
|
|
67
|
-
def __exit__(self, *args) -> None:
|
|
68
|
-
self.end_time = time.perf_counter()
|
|
69
|
-
|
|
70
|
-
@property
|
|
71
|
-
def elapsed_ms(self) -> int:
|
|
72
|
-
"""Get elapsed time in milliseconds."""
|
|
73
|
-
return int((self.end_time - self.start_time) * 1000)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|