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.
Files changed (57) hide show
  1. router_maestro/__init__.py +3 -0
  2. router_maestro/__main__.py +6 -0
  3. router_maestro/auth/__init__.py +18 -0
  4. router_maestro/auth/github_oauth.py +181 -0
  5. router_maestro/auth/manager.py +136 -0
  6. router_maestro/auth/storage.py +91 -0
  7. router_maestro/cli/__init__.py +1 -0
  8. router_maestro/cli/auth.py +167 -0
  9. router_maestro/cli/client.py +322 -0
  10. router_maestro/cli/config.py +132 -0
  11. router_maestro/cli/context.py +146 -0
  12. router_maestro/cli/main.py +42 -0
  13. router_maestro/cli/model.py +288 -0
  14. router_maestro/cli/server.py +117 -0
  15. router_maestro/cli/stats.py +76 -0
  16. router_maestro/config/__init__.py +72 -0
  17. router_maestro/config/contexts.py +29 -0
  18. router_maestro/config/paths.py +50 -0
  19. router_maestro/config/priorities.py +93 -0
  20. router_maestro/config/providers.py +34 -0
  21. router_maestro/config/server.py +115 -0
  22. router_maestro/config/settings.py +76 -0
  23. router_maestro/providers/__init__.py +31 -0
  24. router_maestro/providers/anthropic.py +203 -0
  25. router_maestro/providers/base.py +123 -0
  26. router_maestro/providers/copilot.py +346 -0
  27. router_maestro/providers/openai.py +188 -0
  28. router_maestro/providers/openai_compat.py +175 -0
  29. router_maestro/routing/__init__.py +5 -0
  30. router_maestro/routing/router.py +526 -0
  31. router_maestro/server/__init__.py +5 -0
  32. router_maestro/server/app.py +87 -0
  33. router_maestro/server/middleware/__init__.py +11 -0
  34. router_maestro/server/middleware/auth.py +66 -0
  35. router_maestro/server/oauth_sessions.py +159 -0
  36. router_maestro/server/routes/__init__.py +8 -0
  37. router_maestro/server/routes/admin.py +358 -0
  38. router_maestro/server/routes/anthropic.py +228 -0
  39. router_maestro/server/routes/chat.py +142 -0
  40. router_maestro/server/routes/models.py +34 -0
  41. router_maestro/server/schemas/__init__.py +57 -0
  42. router_maestro/server/schemas/admin.py +87 -0
  43. router_maestro/server/schemas/anthropic.py +246 -0
  44. router_maestro/server/schemas/openai.py +107 -0
  45. router_maestro/server/translation.py +636 -0
  46. router_maestro/stats/__init__.py +14 -0
  47. router_maestro/stats/heatmap.py +154 -0
  48. router_maestro/stats/storage.py +228 -0
  49. router_maestro/stats/tracker.py +73 -0
  50. router_maestro/utils/__init__.py +16 -0
  51. router_maestro/utils/logging.py +81 -0
  52. router_maestro/utils/tokens.py +51 -0
  53. router_maestro-0.1.2.dist-info/METADATA +383 -0
  54. router_maestro-0.1.2.dist-info/RECORD +57 -0
  55. router_maestro-0.1.2.dist-info/WHEEL +4 -0
  56. router_maestro-0.1.2.dist-info/entry_points.txt +2 -0
  57. router_maestro-0.1.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,322 @@
1
+ """Admin client for CLI operations.
2
+
3
+ All CLI commands use HTTP API to communicate with the server.
4
+ This ensures consistent behavior between local and remote contexts.
5
+ """
6
+
7
+ import httpx
8
+
9
+ from router_maestro.config import load_contexts_config
10
+
11
+
12
+ class AdminClientError(Exception):
13
+ """Error from admin client operations."""
14
+
15
+ pass
16
+
17
+
18
+ class ServerNotRunningError(AdminClientError):
19
+ """Server is not running."""
20
+
21
+ def __init__(self, endpoint: str) -> None:
22
+ self.endpoint = endpoint
23
+ super().__init__(
24
+ f"Server is not running at {endpoint}. Start it with: router-maestro server start"
25
+ )
26
+
27
+
28
+ class AdminClient:
29
+ """HTTP client for server admin operations.
30
+
31
+ All operations go through the HTTP API, ensuring consistent behavior
32
+ whether connecting to a local or remote server.
33
+ """
34
+
35
+ def __init__(self, endpoint: str, api_key: str | None) -> None:
36
+ self.endpoint = endpoint.rstrip("/")
37
+ self.api_key = api_key
38
+
39
+ def _get_headers(self) -> dict[str, str]:
40
+ """Get headers for API requests."""
41
+ headers = {"Content-Type": "application/json"}
42
+ if self.api_key:
43
+ headers["Authorization"] = f"Bearer {self.api_key}"
44
+ return headers
45
+
46
+ def _handle_connection_error(self, e: Exception) -> None:
47
+ """Handle connection errors with helpful messages."""
48
+ if isinstance(e, httpx.ConnectError):
49
+ raise ServerNotRunningError(self.endpoint) from e
50
+ raise AdminClientError(f"Request failed: {e}") from e
51
+
52
+ async def list_auth(self) -> list[dict]:
53
+ """List authenticated providers.
54
+
55
+ Returns:
56
+ List of dicts with provider, auth_type, status
57
+ """
58
+ try:
59
+ async with httpx.AsyncClient() as client:
60
+ response = await client.get(
61
+ f"{self.endpoint}/api/admin/auth",
62
+ headers=self._get_headers(),
63
+ )
64
+ response.raise_for_status()
65
+ data = response.json()
66
+ return data.get("providers", [])
67
+ except httpx.HTTPError as e:
68
+ self._handle_connection_error(e)
69
+ return [] # unreachable, for type checker
70
+
71
+ async def login_oauth(self, provider: str) -> dict:
72
+ """Initiate OAuth login.
73
+
74
+ Args:
75
+ provider: Provider name (e.g., 'github-copilot')
76
+
77
+ Returns:
78
+ Dict with session_id, user_code, verification_uri, expires_in
79
+ """
80
+ try:
81
+ async with httpx.AsyncClient() as client:
82
+ response = await client.post(
83
+ f"{self.endpoint}/api/admin/auth/login",
84
+ headers=self._get_headers(),
85
+ json={"provider": provider},
86
+ )
87
+ response.raise_for_status()
88
+ return response.json()
89
+ except httpx.HTTPError as e:
90
+ self._handle_connection_error(e)
91
+ return {}
92
+
93
+ async def login_api_key(self, provider: str, api_key: str) -> bool:
94
+ """Login with API key.
95
+
96
+ Args:
97
+ provider: Provider name
98
+ api_key: API key
99
+
100
+ Returns:
101
+ True if successful
102
+ """
103
+ try:
104
+ async with httpx.AsyncClient() as client:
105
+ response = await client.post(
106
+ f"{self.endpoint}/api/admin/auth/login",
107
+ headers=self._get_headers(),
108
+ json={"provider": provider, "api_key": api_key},
109
+ )
110
+ response.raise_for_status()
111
+ data = response.json()
112
+ return data.get("success", False)
113
+ except httpx.HTTPError as e:
114
+ self._handle_connection_error(e)
115
+ return False
116
+
117
+ async def logout(self, provider: str) -> bool:
118
+ """Logout from a provider.
119
+
120
+ Args:
121
+ provider: Provider name
122
+
123
+ Returns:
124
+ True if successful
125
+ """
126
+ try:
127
+ async with httpx.AsyncClient() as client:
128
+ response = await client.delete(
129
+ f"{self.endpoint}/api/admin/auth/{provider}",
130
+ headers=self._get_headers(),
131
+ )
132
+ if response.status_code == 404:
133
+ return False
134
+ response.raise_for_status()
135
+ data = response.json()
136
+ return data.get("success", False)
137
+ except httpx.HTTPError as e:
138
+ self._handle_connection_error(e)
139
+ return False
140
+
141
+ async def poll_oauth_status(self, session_id: str) -> dict:
142
+ """Poll OAuth session status.
143
+
144
+ Args:
145
+ session_id: Session ID from login_oauth
146
+
147
+ Returns:
148
+ Dict with status ('pending', 'complete', 'expired', 'error') and optional error
149
+ """
150
+ try:
151
+ async with httpx.AsyncClient() as client:
152
+ response = await client.get(
153
+ f"{self.endpoint}/api/admin/auth/oauth/status/{session_id}",
154
+ headers=self._get_headers(),
155
+ )
156
+ response.raise_for_status()
157
+ return response.json()
158
+ except httpx.HTTPError as e:
159
+ self._handle_connection_error(e)
160
+ return {}
161
+
162
+ async def list_models(self) -> list[dict]:
163
+ """List available models.
164
+
165
+ Returns:
166
+ List of dicts with provider, id, name
167
+ """
168
+ try:
169
+ async with httpx.AsyncClient() as client:
170
+ response = await client.get(
171
+ f"{self.endpoint}/api/admin/models",
172
+ headers=self._get_headers(),
173
+ )
174
+ response.raise_for_status()
175
+ data = response.json()
176
+ return data.get("models", [])
177
+ except httpx.HTTPError as e:
178
+ self._handle_connection_error(e)
179
+ return []
180
+
181
+ async def refresh_models(self) -> bool:
182
+ """Refresh the models cache on the server.
183
+
184
+ Returns:
185
+ True if successful
186
+ """
187
+ try:
188
+ async with httpx.AsyncClient() as client:
189
+ response = await client.post(
190
+ f"{self.endpoint}/api/admin/models/refresh",
191
+ headers=self._get_headers(),
192
+ )
193
+ response.raise_for_status()
194
+ return True
195
+ except httpx.HTTPError as e:
196
+ self._handle_connection_error(e)
197
+ return False
198
+
199
+ async def get_priorities(self) -> dict:
200
+ """Get priority configuration.
201
+
202
+ Returns:
203
+ Dict with priorities list and fallback config
204
+ """
205
+ try:
206
+ async with httpx.AsyncClient() as client:
207
+ response = await client.get(
208
+ f"{self.endpoint}/api/admin/priorities",
209
+ headers=self._get_headers(),
210
+ )
211
+ response.raise_for_status()
212
+ return response.json()
213
+ except httpx.HTTPError as e:
214
+ self._handle_connection_error(e)
215
+ return {}
216
+
217
+ async def set_priorities(self, priorities: list[str], fallback: dict | None = None) -> bool:
218
+ """Set priority configuration.
219
+
220
+ Args:
221
+ priorities: List of model keys (provider/model)
222
+ fallback: Optional fallback configuration
223
+
224
+ Returns:
225
+ True if successful
226
+ """
227
+ try:
228
+ async with httpx.AsyncClient() as client:
229
+ payload: dict = {"priorities": priorities}
230
+ if fallback is not None:
231
+ payload["fallback"] = fallback
232
+
233
+ response = await client.put(
234
+ f"{self.endpoint}/api/admin/priorities",
235
+ headers=self._get_headers(),
236
+ json=payload,
237
+ )
238
+ response.raise_for_status()
239
+ return True
240
+ except httpx.HTTPError as e:
241
+ self._handle_connection_error(e)
242
+ return False
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
+ async def test_connection(self) -> dict:
277
+ """Test connection to the server.
278
+
279
+ Returns:
280
+ Dict with server info (name, version, status)
281
+
282
+ Raises:
283
+ ServerNotRunningError: If server is not running
284
+ """
285
+ try:
286
+ async with httpx.AsyncClient(timeout=10.0) as client:
287
+ response = await client.get(
288
+ f"{self.endpoint}/",
289
+ headers=self._get_headers(),
290
+ )
291
+ response.raise_for_status()
292
+ return response.json()
293
+ except httpx.HTTPError as e:
294
+ self._handle_connection_error(e)
295
+ return {}
296
+
297
+
298
+ def get_admin_client() -> AdminClient:
299
+ """Get admin client for current context.
300
+
301
+ Returns:
302
+ AdminClient configured with current context's endpoint and API key
303
+ """
304
+ config = load_contexts_config()
305
+ ctx = config.contexts.get(config.current)
306
+
307
+ if not ctx:
308
+ # Fallback to default local endpoint
309
+ return AdminClient("http://localhost:8080", None)
310
+
311
+ return AdminClient(ctx.endpoint, ctx.api_key)
312
+
313
+
314
+ def get_current_endpoint() -> str:
315
+ """Get the endpoint for the current context.
316
+
317
+ Returns:
318
+ The endpoint URL
319
+ """
320
+ config = load_contexts_config()
321
+ ctx = config.contexts.get(config.current)
322
+ return ctx.endpoint if ctx else "http://localhost:8080"
@@ -0,0 +1,132 @@
1
+ """Configuration management commands."""
2
+
3
+ import asyncio
4
+ import json
5
+ import shutil
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.prompt import Confirm, Prompt
13
+ from rich.table import Table
14
+
15
+ from router_maestro.cli.client import ServerNotRunningError, get_admin_client
16
+ from router_maestro.config.server import get_current_context_api_key
17
+
18
+ app = typer.Typer(no_args_is_help=True)
19
+ console = Console()
20
+
21
+
22
+ def get_claude_code_paths() -> dict[str, Path]:
23
+ """Get Claude Code settings paths."""
24
+ return {
25
+ "user": Path.home() / ".claude" / "settings.json",
26
+ "project": Path.cwd() / ".claude" / "settings.json",
27
+ }
28
+
29
+
30
+ @app.command(name="claude-code")
31
+ def claude_code_config() -> None:
32
+ """Generate Claude Code CLI settings.json for router-maestro."""
33
+ # Step 1: Select level
34
+ console.print("\n[bold]Step 1: Select configuration level[/bold]")
35
+ console.print(" 1. User-level (~/.claude/settings.json)")
36
+ console.print(" 2. Project-level (./.claude/settings.json)")
37
+ choice = Prompt.ask("Select", choices=["1", "2"], default="1")
38
+
39
+ paths = get_claude_code_paths()
40
+ level = "user" if choice == "1" else "project"
41
+ settings_path = paths[level]
42
+
43
+ # Step 2: Backup if exists
44
+ if settings_path.exists():
45
+ console.print(f"\n[yellow]settings.json already exists at {settings_path}[/yellow]")
46
+ if Confirm.ask("Backup existing file?", default=True):
47
+ backup_path = settings_path.with_suffix(
48
+ f".json.backup.{datetime.now().strftime('%Y%m%d_%H%M%S')}"
49
+ )
50
+ shutil.copy(settings_path, backup_path)
51
+ console.print(f"[green]Backed up to {backup_path}[/green]")
52
+
53
+ # Step 3 & 4: Select models from server
54
+ try:
55
+ client = get_admin_client()
56
+ models = asyncio.run(client.list_models())
57
+ except ServerNotRunningError as e:
58
+ console.print(f"[red]{e}[/red]")
59
+ console.print("[dim]Tip: Start router-maestro server first.[/dim]")
60
+ raise typer.Exit(1)
61
+ except Exception as e:
62
+ console.print(f"[red]Error: {e}[/red]")
63
+ raise typer.Exit(1)
64
+
65
+ if not models:
66
+ console.print("[red]No models available. Please authenticate first.[/red]")
67
+ raise typer.Exit(1)
68
+
69
+ # Display models
70
+ console.print("\n[bold]Available models:[/bold]")
71
+ table = Table()
72
+ table.add_column("#", style="dim")
73
+ table.add_column("Model Key", style="green")
74
+ table.add_column("Name", style="white")
75
+ for i, model in enumerate(models, 1):
76
+ table.add_row(str(i), f"{model['provider']}/{model['id']}", model["name"])
77
+ console.print(table)
78
+
79
+ # Select main model
80
+ console.print("\n[bold]Step 3: Select main model[/bold]")
81
+ main_choice = Prompt.ask("Enter number (or 0 for auto-routing)", default="0")
82
+ main_model = "router-maestro"
83
+ if main_choice != "0" and main_choice.isdigit():
84
+ idx = int(main_choice) - 1
85
+ if 0 <= idx < len(models):
86
+ m = models[idx]
87
+ main_model = f"{m['provider']}/{m['id']}"
88
+
89
+ # Select fast model
90
+ console.print("\n[bold]Step 4: Select small/fast model[/bold]")
91
+ fast_choice = Prompt.ask("Enter number", default="1")
92
+ fast_model = "router-maestro"
93
+ if fast_choice.isdigit():
94
+ idx = int(fast_choice) - 1
95
+ if 0 <= idx < len(models):
96
+ m = models[idx]
97
+ fast_model = f"{m['provider']}/{m['id']}"
98
+
99
+ # Step 5: Generate config
100
+ auth_token = get_current_context_api_key() or "router-maestro"
101
+ client = get_admin_client()
102
+ base_url = (
103
+ client.endpoint.rstrip("/") if hasattr(client, "endpoint") else "http://localhost:8080"
104
+ )
105
+ anthropic_url = f"{base_url}/api/anthropic"
106
+
107
+ config = {
108
+ "env": {
109
+ "ANTHROPIC_BASE_URL": anthropic_url,
110
+ "ANTHROPIC_AUTH_TOKEN": auth_token,
111
+ "ANTHROPIC_MODEL": main_model,
112
+ "ANTHROPIC_SMALL_FAST_MODEL": fast_model,
113
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
114
+ }
115
+ }
116
+
117
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
118
+ with open(settings_path, "w", encoding="utf-8") as f:
119
+ json.dump(config, f, indent=2)
120
+
121
+ console.print(
122
+ Panel(
123
+ f"[green]Created {settings_path}[/green]\n\n"
124
+ f"Main model: {main_model}\n"
125
+ f"Fast model: {fast_model}\n\n"
126
+ f"Endpoint: {anthropic_url}\n\n"
127
+ "[dim]Start router-maestro server before using Claude Code:[/dim]\n"
128
+ " router-maestro server start",
129
+ title="Success",
130
+ border_style="green",
131
+ )
132
+ )
@@ -0,0 +1,146 @@
1
+ """Context management commands for remote deployments."""
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
+ from router_maestro.config import (
11
+ ContextConfig,
12
+ load_contexts_config,
13
+ save_contexts_config,
14
+ )
15
+
16
+ app = typer.Typer(no_args_is_help=True)
17
+ console = Console()
18
+
19
+
20
+ @app.command(name="list")
21
+ def list_contexts() -> None:
22
+ """List all configured contexts."""
23
+ config = load_contexts_config()
24
+
25
+ if not config.contexts:
26
+ console.print("[dim]No contexts configured.[/dim]")
27
+ return
28
+
29
+ table = Table(title="Deployment Contexts")
30
+ table.add_column("Name", style="cyan")
31
+ table.add_column("Endpoint", style="green")
32
+ table.add_column("API Key", style="yellow")
33
+ table.add_column("Current", style="magenta")
34
+
35
+ for name, ctx in config.contexts.items():
36
+ is_current = "✓" if name == config.current else ""
37
+ api_key_display = "****" if ctx.api_key else "-"
38
+ table.add_row(name, ctx.endpoint, api_key_display, is_current)
39
+
40
+ console.print(table)
41
+
42
+
43
+ @app.command(name="set")
44
+ def set_context(
45
+ name: str = typer.Argument(..., help="Context name to switch to"),
46
+ ) -> None:
47
+ """Switch to a different context."""
48
+ config = load_contexts_config()
49
+
50
+ if name not in config.contexts:
51
+ console.print(f"[red]Context '{name}' not found[/red]")
52
+ console.print(f"[dim]Available: {', '.join(config.contexts.keys())}[/dim]")
53
+ raise typer.Exit(1)
54
+
55
+ config.current = name
56
+ save_contexts_config(config)
57
+ console.print(f"[green]Switched to context: {name}[/green]")
58
+ console.print(f" Endpoint: {config.contexts[name].endpoint}")
59
+
60
+
61
+ @app.command()
62
+ def add(
63
+ name: str = typer.Argument(..., help="Name for the new context"),
64
+ endpoint: str = typer.Option(..., "--endpoint", "-e", help="API endpoint URL"),
65
+ api_key: str = typer.Option(None, "--api-key", "-k", help="API key (optional)"),
66
+ ) -> None:
67
+ """Add a new context."""
68
+ config = load_contexts_config()
69
+
70
+ if name in config.contexts:
71
+ console.print(f"[yellow]Context '{name}' already exists. Overwriting...[/yellow]")
72
+
73
+ config.contexts[name] = ContextConfig(endpoint=endpoint, api_key=api_key)
74
+ save_contexts_config(config)
75
+
76
+ console.print(f"[green]Added context: {name}[/green]")
77
+ console.print(f" Endpoint: {endpoint}")
78
+ if api_key:
79
+ console.print(" API Key: ****")
80
+
81
+
82
+ @app.command()
83
+ def remove(
84
+ name: str = typer.Argument(..., help="Context name to remove"),
85
+ ) -> None:
86
+ """Remove a context."""
87
+ config = load_contexts_config()
88
+
89
+ if name not in config.contexts:
90
+ console.print(f"[red]Context '{name}' not found[/red]")
91
+ raise typer.Exit(1)
92
+
93
+ if name == "local":
94
+ console.print("[red]Cannot remove the 'local' context[/red]")
95
+ raise typer.Exit(1)
96
+
97
+ if name == config.current:
98
+ console.print("[yellow]Switching to 'local' context first...[/yellow]")
99
+ config.current = "local"
100
+
101
+ del config.contexts[name]
102
+ save_contexts_config(config)
103
+ console.print(f"[green]Removed context: {name}[/green]")
104
+
105
+
106
+ @app.command()
107
+ def current() -> None:
108
+ """Show the current context."""
109
+ config = load_contexts_config()
110
+ ctx = config.contexts.get(config.current)
111
+
112
+ if ctx:
113
+ console.print(f"[bold]Current context:[/bold] {config.current}")
114
+ console.print(f" Endpoint: {ctx.endpoint}")
115
+ if ctx.api_key:
116
+ console.print(" API Key: ****")
117
+ else:
118
+ console.print("[yellow]No context selected[/yellow]")
119
+
120
+
121
+ @app.command()
122
+ def test() -> None:
123
+ """Test connection to the current context's server."""
124
+ config = load_contexts_config()
125
+ ctx = config.contexts.get(config.current)
126
+
127
+ if not ctx:
128
+ console.print("[red]No context selected[/red]")
129
+ raise typer.Exit(1)
130
+
131
+ console.print(f"[bold]Testing connection to:[/bold] {config.current}")
132
+ console.print(f" Endpoint: {ctx.endpoint}")
133
+
134
+ client = get_admin_client()
135
+
136
+ try:
137
+ result = asyncio.run(client.test_connection())
138
+ console.print("\n[green]✓ Connection successful![/green]")
139
+ console.print(f" Server: {result.get('name', 'Unknown')}")
140
+ console.print(f" Version: {result.get('version', 'Unknown')}")
141
+ except AdminClientError as e:
142
+ console.print(f"\n[red]✗ {e}[/red]")
143
+ raise typer.Exit(1)
144
+ except Exception as e:
145
+ console.print(f"\n[red]✗ Connection failed: {e}[/red]")
146
+ raise typer.Exit(1)
@@ -0,0 +1,42 @@
1
+ """Router-Maestro CLI application."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from router_maestro import __version__
7
+
8
+ app = typer.Typer(
9
+ name="router-maestro",
10
+ help="Multi-model routing and load balancing system with OpenAI-compatible API",
11
+ no_args_is_help=True,
12
+ )
13
+
14
+ console = Console()
15
+
16
+ # Import and register sub-commands
17
+ from router_maestro.cli import auth, config, context, model, server, stats # noqa: E402
18
+
19
+ app.add_typer(server.app, name="server", help="Manage the API server")
20
+ app.add_typer(auth.app, name="auth", help="Manage authentication for providers")
21
+ app.add_typer(model.app, name="model", help="Manage models and priorities")
22
+ app.add_typer(context.app, name="context", help="Manage deployment contexts")
23
+ app.add_typer(config.app, name="config", help="Manage configuration")
24
+ app.command(name="stats")(stats.stats)
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def main(
29
+ ctx: typer.Context,
30
+ version: bool = typer.Option(False, "--version", "-v", help="Show version and exit"),
31
+ ) -> None:
32
+ """Router-Maestro: Multi-model routing and load balancing system."""
33
+ if version:
34
+ console.print(f"router-maestro version {__version__}")
35
+ raise typer.Exit()
36
+
37
+ if ctx.invoked_subcommand is None:
38
+ console.print("[yellow]Use --help for usage information[/yellow]")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ app()