router-maestro 0.1.2__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 +3 -0
- router_maestro/__main__.py +6 -0
- router_maestro/auth/__init__.py +18 -0
- router_maestro/auth/github_oauth.py +181 -0
- router_maestro/auth/manager.py +136 -0
- router_maestro/auth/storage.py +91 -0
- router_maestro/cli/__init__.py +1 -0
- router_maestro/cli/auth.py +167 -0
- router_maestro/cli/client.py +322 -0
- router_maestro/cli/config.py +132 -0
- router_maestro/cli/context.py +146 -0
- router_maestro/cli/main.py +42 -0
- router_maestro/cli/model.py +288 -0
- router_maestro/cli/server.py +117 -0
- router_maestro/cli/stats.py +76 -0
- router_maestro/config/__init__.py +72 -0
- router_maestro/config/contexts.py +29 -0
- router_maestro/config/paths.py +50 -0
- router_maestro/config/priorities.py +93 -0
- router_maestro/config/providers.py +34 -0
- router_maestro/config/server.py +115 -0
- router_maestro/config/settings.py +76 -0
- router_maestro/providers/__init__.py +31 -0
- router_maestro/providers/anthropic.py +203 -0
- router_maestro/providers/base.py +123 -0
- router_maestro/providers/copilot.py +346 -0
- router_maestro/providers/openai.py +188 -0
- router_maestro/providers/openai_compat.py +175 -0
- router_maestro/routing/__init__.py +5 -0
- router_maestro/routing/router.py +526 -0
- router_maestro/server/__init__.py +5 -0
- router_maestro/server/app.py +87 -0
- router_maestro/server/middleware/__init__.py +11 -0
- router_maestro/server/middleware/auth.py +66 -0
- router_maestro/server/oauth_sessions.py +159 -0
- router_maestro/server/routes/__init__.py +8 -0
- router_maestro/server/routes/admin.py +358 -0
- router_maestro/server/routes/anthropic.py +228 -0
- router_maestro/server/routes/chat.py +142 -0
- router_maestro/server/routes/models.py +34 -0
- router_maestro/server/schemas/__init__.py +57 -0
- router_maestro/server/schemas/admin.py +87 -0
- router_maestro/server/schemas/anthropic.py +246 -0
- router_maestro/server/schemas/openai.py +107 -0
- router_maestro/server/translation.py +636 -0
- router_maestro/stats/__init__.py +14 -0
- router_maestro/stats/heatmap.py +154 -0
- router_maestro/stats/storage.py +228 -0
- router_maestro/stats/tracker.py +73 -0
- router_maestro/utils/__init__.py +16 -0
- router_maestro/utils/logging.py +81 -0
- router_maestro/utils/tokens.py +51 -0
- router_maestro-0.1.2.dist-info/METADATA +383 -0
- router_maestro-0.1.2.dist-info/RECORD +57 -0
- router_maestro-0.1.2.dist-info/WHEEL +4 -0
- router_maestro-0.1.2.dist-info/entry_points.txt +2 -0
- router_maestro-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Model management commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from router_maestro.cli.client import ServerNotRunningError, get_admin_client
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(no_args_is_help=True)
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _handle_server_error(e: Exception) -> None:
|
|
17
|
+
"""Handle server connection errors."""
|
|
18
|
+
if isinstance(e, ServerNotRunningError):
|
|
19
|
+
console.print(f"[red]{e}[/red]")
|
|
20
|
+
else:
|
|
21
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command(name="list")
|
|
26
|
+
def list_models() -> None:
|
|
27
|
+
"""List all available models with their priorities."""
|
|
28
|
+
client = get_admin_client()
|
|
29
|
+
|
|
30
|
+
# Get models and priorities
|
|
31
|
+
try:
|
|
32
|
+
models = asyncio.run(client.list_models())
|
|
33
|
+
priorities_data = asyncio.run(client.get_priorities())
|
|
34
|
+
priorities_list = priorities_data.get("priorities", [])
|
|
35
|
+
except Exception as e:
|
|
36
|
+
_handle_server_error(e)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if not models:
|
|
40
|
+
console.print("[dim]No models available.[/dim]")
|
|
41
|
+
console.print("[dim]Make sure you have authenticated with at least one provider.[/dim]")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
table = Table(title="Available Models")
|
|
45
|
+
table.add_column("Priority", style="cyan", justify="right")
|
|
46
|
+
table.add_column("Model Key", style="green")
|
|
47
|
+
table.add_column("Display Name", style="white")
|
|
48
|
+
table.add_column("Provider", style="magenta")
|
|
49
|
+
|
|
50
|
+
for model in models:
|
|
51
|
+
model_key = f"{model['provider']}/{model['id']}"
|
|
52
|
+
# Check if this model is in the priority list
|
|
53
|
+
try:
|
|
54
|
+
priority_idx = priorities_list.index(model_key)
|
|
55
|
+
priority_str = str(priority_idx + 1)
|
|
56
|
+
except ValueError:
|
|
57
|
+
priority_str = "-"
|
|
58
|
+
|
|
59
|
+
table.add_row(
|
|
60
|
+
priority_str,
|
|
61
|
+
model_key,
|
|
62
|
+
model["name"],
|
|
63
|
+
model["provider"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
console.print(table)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command(name="refresh")
|
|
70
|
+
def refresh_models() -> None:
|
|
71
|
+
"""Refresh the models cache from all providers."""
|
|
72
|
+
client = get_admin_client()
|
|
73
|
+
|
|
74
|
+
console.print("[dim]Refreshing models cache...[/dim]")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
success = asyncio.run(client.refresh_models())
|
|
78
|
+
if success:
|
|
79
|
+
console.print("[green]Models cache refreshed successfully[/green]")
|
|
80
|
+
else:
|
|
81
|
+
console.print("[red]Failed to refresh models cache[/red]")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
_handle_server_error(e)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Priority subcommand group
|
|
88
|
+
priority_app = typer.Typer(no_args_is_help=True, help="Manage model priorities")
|
|
89
|
+
app.add_typer(priority_app, name="priority")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@priority_app.command(name="list")
|
|
93
|
+
def priority_list() -> None:
|
|
94
|
+
"""List current model priorities."""
|
|
95
|
+
client = get_admin_client()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
data = asyncio.run(client.get_priorities())
|
|
99
|
+
priorities = data.get("priorities", [])
|
|
100
|
+
except Exception as e:
|
|
101
|
+
_handle_server_error(e)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if not priorities:
|
|
105
|
+
console.print("[dim]No priorities configured.[/dim]")
|
|
106
|
+
console.print(
|
|
107
|
+
"[dim]Use 'router-maestro model priority add <provider/model>' to add priorities.[/dim]"
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
table = Table(title="Model Priorities")
|
|
112
|
+
table.add_column("#", style="cyan", justify="right")
|
|
113
|
+
table.add_column("Model Key", style="green")
|
|
114
|
+
|
|
115
|
+
for idx, model_key in enumerate(priorities):
|
|
116
|
+
table.add_row(str(idx + 1), model_key)
|
|
117
|
+
|
|
118
|
+
console.print(table)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@priority_app.command(name="add")
|
|
122
|
+
def priority_add(
|
|
123
|
+
model_key: Annotated[str, typer.Argument(help="Model key in format 'provider/model'")],
|
|
124
|
+
position: Annotated[
|
|
125
|
+
int | None,
|
|
126
|
+
typer.Option("--position", "-p", help="Position in priority list (1-based)"),
|
|
127
|
+
] = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Add or move a model in the priority list."""
|
|
130
|
+
if "/" not in model_key:
|
|
131
|
+
console.print("[red]Model key must be in format 'provider/model'[/red]")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
client = get_admin_client()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Get current priorities
|
|
138
|
+
data = asyncio.run(client.get_priorities())
|
|
139
|
+
priorities = data.get("priorities", [])
|
|
140
|
+
fallback = data.get("fallback")
|
|
141
|
+
|
|
142
|
+
# Remove if already exists
|
|
143
|
+
if model_key in priorities:
|
|
144
|
+
priorities.remove(model_key)
|
|
145
|
+
|
|
146
|
+
# Insert at position
|
|
147
|
+
if position is None:
|
|
148
|
+
priorities.append(model_key)
|
|
149
|
+
else:
|
|
150
|
+
pos = position - 1 # Convert 1-based to 0-based
|
|
151
|
+
priorities.insert(pos, model_key)
|
|
152
|
+
|
|
153
|
+
# Save
|
|
154
|
+
asyncio.run(client.set_priorities(priorities, fallback))
|
|
155
|
+
|
|
156
|
+
if position:
|
|
157
|
+
console.print(f"[green]Added '{model_key}' at position {position}[/green]")
|
|
158
|
+
else:
|
|
159
|
+
console.print(f"[green]Added '{model_key}' to end of priority list[/green]")
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
_handle_server_error(e)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@priority_app.command(name="remove")
|
|
166
|
+
def priority_remove(
|
|
167
|
+
model_key: Annotated[str, typer.Argument(help="Model key in format 'provider/model'")],
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Remove a model from the priority list."""
|
|
170
|
+
if "/" not in model_key:
|
|
171
|
+
console.print("[red]Model key must be in format 'provider/model'[/red]")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
client = get_admin_client()
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Get current priorities
|
|
178
|
+
data = asyncio.run(client.get_priorities())
|
|
179
|
+
priorities = data.get("priorities", [])
|
|
180
|
+
fallback = data.get("fallback")
|
|
181
|
+
|
|
182
|
+
if model_key in priorities:
|
|
183
|
+
priorities.remove(model_key)
|
|
184
|
+
asyncio.run(client.set_priorities(priorities, fallback))
|
|
185
|
+
console.print(f"[green]Removed '{model_key}' from priority list[/green]")
|
|
186
|
+
else:
|
|
187
|
+
console.print(f"[yellow]'{model_key}' was not in the priority list[/yellow]")
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
_handle_server_error(e)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@priority_app.command(name="clear")
|
|
194
|
+
def priority_clear() -> None:
|
|
195
|
+
"""Clear all priorities."""
|
|
196
|
+
client = get_admin_client()
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
data = asyncio.run(client.get_priorities())
|
|
200
|
+
fallback = data.get("fallback")
|
|
201
|
+
asyncio.run(client.set_priorities([], fallback))
|
|
202
|
+
console.print("[green]Cleared all priorities[/green]")
|
|
203
|
+
except Exception as e:
|
|
204
|
+
_handle_server_error(e)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Fallback subcommand group
|
|
208
|
+
fallback_app = typer.Typer(no_args_is_help=True, help="Manage fallback configuration")
|
|
209
|
+
app.add_typer(fallback_app, name="fallback")
|
|
210
|
+
|
|
211
|
+
VALID_STRATEGIES = ["priority", "same-model", "none"]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@fallback_app.command(name="show")
|
|
215
|
+
def fallback_show() -> None:
|
|
216
|
+
"""Show current fallback configuration."""
|
|
217
|
+
client = get_admin_client()
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
data = asyncio.run(client.get_priorities())
|
|
221
|
+
fallback = data.get("fallback", {})
|
|
222
|
+
except Exception as e:
|
|
223
|
+
_handle_server_error(e)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
strategy = fallback.get("strategy", "priority")
|
|
227
|
+
max_retries = fallback.get("maxRetries", 2)
|
|
228
|
+
|
|
229
|
+
console.print()
|
|
230
|
+
console.print("[bold]Fallback Configuration[/bold]")
|
|
231
|
+
console.print(f" Strategy: [cyan]{strategy}[/cyan]")
|
|
232
|
+
console.print(f" Max Retries: [cyan]{max_retries}[/cyan]")
|
|
233
|
+
console.print()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@fallback_app.command(name="set")
|
|
237
|
+
def fallback_set(
|
|
238
|
+
strategy: Annotated[
|
|
239
|
+
str | None,
|
|
240
|
+
typer.Option("--strategy", "-s", help="Fallback strategy (priority, same-model, none)"),
|
|
241
|
+
] = None,
|
|
242
|
+
max_retries: Annotated[
|
|
243
|
+
int | None,
|
|
244
|
+
typer.Option("--max-retries", "-r", help="Maximum number of fallback retries (0-10)"),
|
|
245
|
+
] = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Set fallback configuration."""
|
|
248
|
+
# Validate that at least one option is provided
|
|
249
|
+
if strategy is None and max_retries is None:
|
|
250
|
+
console.print("[red]At least one of --strategy or --max-retries must be provided[/red]")
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
# Validate strategy
|
|
254
|
+
if strategy is not None and strategy not in VALID_STRATEGIES:
|
|
255
|
+
console.print(f"[red]Invalid strategy '{strategy}'[/red]")
|
|
256
|
+
console.print(f"[dim]Valid strategies: {', '.join(VALID_STRATEGIES)}[/dim]")
|
|
257
|
+
raise typer.Exit(1)
|
|
258
|
+
|
|
259
|
+
# Validate max_retries
|
|
260
|
+
if max_retries is not None and (max_retries < 0 or max_retries > 10):
|
|
261
|
+
console.print("[red]max-retries must be between 0 and 10[/red]")
|
|
262
|
+
raise typer.Exit(1)
|
|
263
|
+
|
|
264
|
+
client = get_admin_client()
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# Get current config
|
|
268
|
+
data = asyncio.run(client.get_priorities())
|
|
269
|
+
priorities = data.get("priorities", [])
|
|
270
|
+
fallback = data.get("fallback", {})
|
|
271
|
+
|
|
272
|
+
# Update fallback config
|
|
273
|
+
if strategy is not None:
|
|
274
|
+
fallback["strategy"] = strategy
|
|
275
|
+
if max_retries is not None:
|
|
276
|
+
fallback["maxRetries"] = max_retries
|
|
277
|
+
|
|
278
|
+
# Save
|
|
279
|
+
asyncio.run(client.set_priorities(priorities, fallback))
|
|
280
|
+
|
|
281
|
+
console.print("[green]Fallback configuration updated[/green]")
|
|
282
|
+
|
|
283
|
+
# Show updated config
|
|
284
|
+
console.print(f" Strategy: [cyan]{fallback.get('strategy', 'priority')}[/cyan]")
|
|
285
|
+
console.print(f" Max Retries: [cyan]{fallback.get('maxRetries', 2)}[/cyan]")
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
_handle_server_error(e)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Server management commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
import uvicorn
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from router_maestro.config.server import get_current_context_api_key, get_or_create_api_key
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(no_args_is_help=True)
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool:
|
|
18
|
+
"""Check if a port is in use."""
|
|
19
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
20
|
+
return s.connect_ex((host, port)) == 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command()
|
|
24
|
+
def start(
|
|
25
|
+
port: int = typer.Option(8080, "--port", "-p", help="Port to listen on"),
|
|
26
|
+
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
|
|
27
|
+
api_key: str | None = typer.Option(None, "--api-key", "-k", help="API key for authentication"),
|
|
28
|
+
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"),
|
|
29
|
+
log_level: str = typer.Option(
|
|
30
|
+
"INFO",
|
|
31
|
+
"--log-level",
|
|
32
|
+
"-l",
|
|
33
|
+
help="Log level (DEBUG, INFO, WARNING, ERROR)",
|
|
34
|
+
case_sensitive=False,
|
|
35
|
+
),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Start router-maestro API server."""
|
|
38
|
+
if is_port_in_use(port, host):
|
|
39
|
+
console.print(f"[red]Error: Port {port} is already in use[/red]")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
# Validate log level
|
|
43
|
+
valid_levels = ("DEBUG", "INFO", "WARNING", "ERROR")
|
|
44
|
+
log_level = log_level.upper()
|
|
45
|
+
if log_level not in valid_levels:
|
|
46
|
+
console.print(
|
|
47
|
+
f"[red]Error: Invalid log level '{log_level}'. Valid: {', '.join(valid_levels)}[/red]"
|
|
48
|
+
)
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
# Get or create API key
|
|
52
|
+
key, was_generated = get_or_create_api_key(api_key)
|
|
53
|
+
|
|
54
|
+
# Set environment variables for server process to read
|
|
55
|
+
os.environ["ROUTER_MAESTRO_API_KEY"] = key
|
|
56
|
+
os.environ["ROUTER_MAESTRO_LOG_LEVEL"] = log_level
|
|
57
|
+
|
|
58
|
+
console.print(f"[green]Starting Router-Maestro server on {host}:{port}...[/green]")
|
|
59
|
+
console.print(f"[dim]API endpoint: http://{host}:{port}/v1[/dim]")
|
|
60
|
+
console.print(f"[dim]Log level: {log_level}[/dim]")
|
|
61
|
+
|
|
62
|
+
if was_generated:
|
|
63
|
+
console.print(
|
|
64
|
+
Panel(
|
|
65
|
+
f"[yellow]API Key (auto-generated):[/yellow]\n[bold]{key}[/bold]",
|
|
66
|
+
title="Authentication",
|
|
67
|
+
border_style="yellow",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"[dim]API Key: {key[:12]}...{key[-4:]}[/dim]")
|
|
72
|
+
|
|
73
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]\n")
|
|
74
|
+
|
|
75
|
+
# Map log level to uvicorn format
|
|
76
|
+
uvicorn_level = log_level.lower()
|
|
77
|
+
|
|
78
|
+
uvicorn.run(
|
|
79
|
+
"router_maestro.server:app",
|
|
80
|
+
host=host,
|
|
81
|
+
port=port,
|
|
82
|
+
reload=reload,
|
|
83
|
+
log_level=uvicorn_level,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def status() -> None:
|
|
89
|
+
"""Show current server status (uses current context)."""
|
|
90
|
+
import asyncio
|
|
91
|
+
|
|
92
|
+
from router_maestro.cli.client import get_admin_client, get_current_endpoint
|
|
93
|
+
|
|
94
|
+
client = get_admin_client()
|
|
95
|
+
endpoint = get_current_endpoint()
|
|
96
|
+
|
|
97
|
+
console.print(f"[dim]Context endpoint: {endpoint}[/dim]")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
data = asyncio.run(client.test_connection())
|
|
101
|
+
console.print(f"[green]Server is running[/green]")
|
|
102
|
+
console.print(f" Version: {data.get('version', 'unknown')}")
|
|
103
|
+
console.print(f" Status: {data.get('status', 'unknown')}")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
console.print(f"[yellow]Server not reachable: {e}[/yellow]")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def show_key() -> None:
|
|
110
|
+
"""Show current server API key."""
|
|
111
|
+
api_key = get_current_context_api_key()
|
|
112
|
+
if api_key:
|
|
113
|
+
console.print(f"[green]API Key:[/green] {api_key}")
|
|
114
|
+
else:
|
|
115
|
+
console.print(
|
|
116
|
+
"[yellow]No API key configured. One will be generated on server start.[/yellow]"
|
|
117
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
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)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Config module for router-maestro."""
|
|
2
|
+
|
|
3
|
+
from router_maestro.config.contexts import ContextConfig, ContextsConfig
|
|
4
|
+
from router_maestro.config.paths import (
|
|
5
|
+
AUTH_FILE,
|
|
6
|
+
CONTEXTS_FILE,
|
|
7
|
+
LOG_FILE,
|
|
8
|
+
PRIORITIES_FILE,
|
|
9
|
+
PROVIDERS_FILE,
|
|
10
|
+
SERVER_CONFIG_FILE,
|
|
11
|
+
STATS_DB_FILE,
|
|
12
|
+
get_config_dir,
|
|
13
|
+
get_data_dir,
|
|
14
|
+
)
|
|
15
|
+
from router_maestro.config.priorities import (
|
|
16
|
+
FallbackConfig,
|
|
17
|
+
FallbackStrategy,
|
|
18
|
+
PrioritiesConfig,
|
|
19
|
+
)
|
|
20
|
+
from router_maestro.config.providers import (
|
|
21
|
+
CustomProviderConfig,
|
|
22
|
+
ModelConfig,
|
|
23
|
+
ProvidersConfig,
|
|
24
|
+
)
|
|
25
|
+
from router_maestro.config.server import (
|
|
26
|
+
get_current_context_api_key,
|
|
27
|
+
get_or_create_api_key,
|
|
28
|
+
set_local_api_key,
|
|
29
|
+
)
|
|
30
|
+
from router_maestro.config.settings import (
|
|
31
|
+
load_contexts_config,
|
|
32
|
+
load_priorities_config,
|
|
33
|
+
load_providers_config,
|
|
34
|
+
save_contexts_config,
|
|
35
|
+
save_priorities_config,
|
|
36
|
+
save_providers_config,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Paths
|
|
41
|
+
"get_data_dir",
|
|
42
|
+
"get_config_dir",
|
|
43
|
+
"AUTH_FILE",
|
|
44
|
+
"SERVER_CONFIG_FILE",
|
|
45
|
+
"PROVIDERS_FILE",
|
|
46
|
+
"PRIORITIES_FILE",
|
|
47
|
+
"CONTEXTS_FILE",
|
|
48
|
+
"STATS_DB_FILE",
|
|
49
|
+
"LOG_FILE",
|
|
50
|
+
# Provider models
|
|
51
|
+
"ModelConfig",
|
|
52
|
+
"CustomProviderConfig",
|
|
53
|
+
"ProvidersConfig",
|
|
54
|
+
# Priority models
|
|
55
|
+
"PrioritiesConfig",
|
|
56
|
+
"FallbackConfig",
|
|
57
|
+
"FallbackStrategy",
|
|
58
|
+
# Context models
|
|
59
|
+
"ContextConfig",
|
|
60
|
+
"ContextsConfig",
|
|
61
|
+
# Settings functions
|
|
62
|
+
"load_providers_config",
|
|
63
|
+
"save_providers_config",
|
|
64
|
+
"load_priorities_config",
|
|
65
|
+
"save_priorities_config",
|
|
66
|
+
"load_contexts_config",
|
|
67
|
+
"save_contexts_config",
|
|
68
|
+
# Server functions
|
|
69
|
+
"get_current_context_api_key",
|
|
70
|
+
"get_or_create_api_key",
|
|
71
|
+
"set_local_api_key",
|
|
72
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Context configuration for remote deployments."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContextConfig(BaseModel):
|
|
7
|
+
"""Configuration for a single deployment context."""
|
|
8
|
+
|
|
9
|
+
endpoint: str = Field(..., description="API endpoint URL")
|
|
10
|
+
api_key: str | None = Field(default=None, description="API key for authentication")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextsConfig(BaseModel):
|
|
14
|
+
"""Root configuration for deployment contexts."""
|
|
15
|
+
|
|
16
|
+
current: str = Field(default="local", description="Currently active context name")
|
|
17
|
+
contexts: dict[str, ContextConfig] = Field(
|
|
18
|
+
default_factory=dict, description="Available contexts"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def get_default(cls) -> "ContextsConfig":
|
|
23
|
+
"""Get default configuration with local context."""
|
|
24
|
+
return cls(
|
|
25
|
+
current="local",
|
|
26
|
+
contexts={
|
|
27
|
+
"local": ContextConfig(endpoint="http://localhost:8080"),
|
|
28
|
+
},
|
|
29
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""File path definitions for router-maestro."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_data_dir() -> Path:
|
|
8
|
+
"""Get the data directory for router-maestro.
|
|
9
|
+
|
|
10
|
+
Returns ~/.local/share/router-maestro on Unix-like systems.
|
|
11
|
+
Returns %LOCALAPPDATA%/router-maestro on Windows.
|
|
12
|
+
"""
|
|
13
|
+
if os.name == "nt":
|
|
14
|
+
# Windows
|
|
15
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
16
|
+
else:
|
|
17
|
+
# Unix-like (Linux, macOS)
|
|
18
|
+
base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
19
|
+
|
|
20
|
+
data_dir = base / "router-maestro"
|
|
21
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return data_dir
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_config_dir() -> Path:
|
|
26
|
+
"""Get the config directory for router-maestro.
|
|
27
|
+
|
|
28
|
+
Returns ~/.config/router-maestro on Unix-like systems.
|
|
29
|
+
Returns %LOCALAPPDATA%/router-maestro on Windows.
|
|
30
|
+
"""
|
|
31
|
+
if os.name == "nt":
|
|
32
|
+
# Windows - use same as data dir
|
|
33
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
34
|
+
else:
|
|
35
|
+
# Unix-like (Linux, macOS)
|
|
36
|
+
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
37
|
+
|
|
38
|
+
config_dir = base / "router-maestro"
|
|
39
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
return config_dir
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# File paths
|
|
44
|
+
AUTH_FILE = get_data_dir() / "auth.json"
|
|
45
|
+
SERVER_CONFIG_FILE = get_data_dir() / "server.json"
|
|
46
|
+
PROVIDERS_FILE = get_config_dir() / "providers.json"
|
|
47
|
+
PRIORITIES_FILE = get_config_dir() / "priorities.json"
|
|
48
|
+
CONTEXTS_FILE = get_config_dir() / "contexts.json"
|
|
49
|
+
STATS_DB_FILE = get_data_dir() / "stats.db"
|
|
50
|
+
LOG_FILE = get_data_dir() / "router-maestro.log"
|