router-maestro 0.1.3__py3-none-any.whl → 0.1.5__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.
@@ -1,3 +1,3 @@
1
1
  """Router-Maestro: Multi-model routing and load balancing system."""
2
2
 
3
- __version__ = "0.1.3"
3
+ __version__ = "0.1.5"
@@ -15,9 +15,17 @@ from rich.table import Table
15
15
  from router_maestro.cli.client import ServerNotRunningError, get_admin_client
16
16
  from router_maestro.config.server import get_current_context_api_key
17
17
 
18
- app = typer.Typer(no_args_is_help=True)
18
+ app = typer.Typer(invoke_without_command=True)
19
19
  console = Console()
20
20
 
21
+ # Available CLI tools for configuration
22
+ CLI_TOOLS = {
23
+ "claude-code": {
24
+ "name": "Claude Code",
25
+ "description": "Generate settings.json for Claude Code CLI",
26
+ },
27
+ }
28
+
21
29
 
22
30
  def get_claude_code_paths() -> dict[str, Path]:
23
31
  """Get Claude Code settings paths."""
@@ -27,6 +35,33 @@ def get_claude_code_paths() -> dict[str, Path]:
27
35
  }
28
36
 
29
37
 
38
+ @app.callback(invoke_without_command=True)
39
+ def config_callback(ctx: typer.Context) -> None:
40
+ """Generate configuration for CLI tools (interactive selection if not specified)."""
41
+ if ctx.invoked_subcommand is not None:
42
+ return
43
+
44
+ # Interactive selection
45
+ console.print("\n[bold]Available CLI tools:[/bold]")
46
+ tools = list(CLI_TOOLS.items())
47
+ for i, (key, info) in enumerate(tools, 1):
48
+ console.print(f" {i}. {info['name']} - {info['description']}")
49
+
50
+ console.print()
51
+ choice = Prompt.ask(
52
+ "Select tool to configure",
53
+ choices=[str(i) for i in range(1, len(tools) + 1)],
54
+ default="1",
55
+ )
56
+
57
+ idx = int(choice) - 1
58
+ tool_key = tools[idx][0]
59
+
60
+ # Dispatch to the appropriate command
61
+ if tool_key == "claude-code":
62
+ claude_code_config()
63
+
64
+
30
65
  @app.command(name="claude-code")
31
66
  def claude_code_config() -> None:
32
67
  """Generate Claude Code CLI settings.json for router-maestro."""
@@ -104,19 +139,29 @@ def claude_code_config() -> None:
104
139
  )
105
140
  anthropic_url = f"{base_url}/api/anthropic"
106
141
 
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
- }
142
+ env_config = {
143
+ "ANTHROPIC_BASE_URL": anthropic_url,
144
+ "ANTHROPIC_AUTH_TOKEN": auth_token,
145
+ "ANTHROPIC_MODEL": main_model,
146
+ "ANTHROPIC_SMALL_FAST_MODEL": fast_model,
147
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
115
148
  }
116
149
 
150
+ # Load existing settings to preserve other sections (e.g., MCP servers)
151
+ existing_config: dict = {}
152
+ if settings_path.exists():
153
+ try:
154
+ with open(settings_path, encoding="utf-8") as f:
155
+ existing_config = json.load(f)
156
+ except (json.JSONDecodeError, OSError):
157
+ pass # If file is corrupted, start fresh
158
+
159
+ # Merge: update env section while preserving other sections
160
+ existing_config["env"] = env_config
161
+
117
162
  settings_path.parent.mkdir(parents=True, exist_ok=True)
118
163
  with open(settings_path, "w", encoding="utf-8") as f:
119
- json.dump(config, f, indent=2)
164
+ json.dump(existing_config, f, indent=2)
120
165
 
121
166
  console.print(
122
167
  Panel(
@@ -382,7 +382,7 @@ class Router:
382
382
  try:
383
383
  await provider.ensure_token()
384
384
  if is_stream:
385
- stream = provider.chat_completion_stream(actual_request)
385
+ stream = await provider.chat_completion_stream(actual_request)
386
386
  logger.info("Stream request routed to %s", provider_name)
387
387
  return stream, provider_name
388
388
  else:
@@ -417,7 +417,7 @@ class Router:
417
417
  try:
418
418
  await other_provider.ensure_token()
419
419
  if is_stream:
420
- stream = other_provider.chat_completion_stream(fallback_request)
420
+ stream = await other_provider.chat_completion_stream(fallback_request)
421
421
  logger.info("Stream fallback succeeded via %s", other_name)
422
422
  return stream, other_name
423
423
  else:
@@ -26,6 +26,7 @@ from router_maestro.utils import (
26
26
  get_logger,
27
27
  map_openai_stop_reason_to_anthropic,
28
28
  )
29
+ from router_maestro.utils.tokens import AnthropicStopReason
29
30
 
30
31
  logger = get_logger("server.routes.anthropic")
31
32
 
@@ -106,7 +107,7 @@ async def count_tokens(request: AnthropicCountTokensRequest):
106
107
 
107
108
  # Count messages
108
109
  for msg in request.messages:
109
- content = msg.content if hasattr(msg, "content") else msg.get("content", "")
110
+ content = msg.content
110
111
  if isinstance(content, str):
111
112
  total_chars += len(content)
112
113
  elif isinstance(content, list):
@@ -115,12 +116,12 @@ async def count_tokens(request: AnthropicCountTokensRequest):
115
116
  if block.get("type") == "text":
116
117
  total_chars += len(block.get("text", ""))
117
118
  elif hasattr(block, "text"):
118
- total_chars += len(block.text)
119
+ total_chars += len(block.text) # type: ignore[union-attr]
119
120
 
120
121
  return {"input_tokens": estimate_tokens_from_char_count(total_chars)}
121
122
 
122
123
 
123
- def _map_finish_reason(reason: str | None) -> str | None:
124
+ def _map_finish_reason(reason: str | None) -> AnthropicStopReason | None:
124
125
  """Map OpenAI finish reason to Anthropic stop reason."""
125
126
  return map_openai_stop_reason_to_anthropic(reason)
126
127
 
@@ -144,7 +145,7 @@ def _estimate_input_tokens(request: AnthropicMessagesRequest) -> int:
144
145
 
145
146
  # Count messages
146
147
  for msg in request.messages:
147
- content = msg.content if hasattr(msg, "content") else msg.get("content", "")
148
+ content = msg.content
148
149
  if isinstance(content, str):
149
150
  total_chars += len(content)
150
151
  elif isinstance(content, list):
@@ -161,7 +162,7 @@ def _estimate_input_tokens(request: AnthropicMessagesRequest) -> int:
161
162
  if isinstance(tc, dict) and tc.get("type") == "text":
162
163
  total_chars += len(tc.get("text", ""))
163
164
  elif hasattr(block, "text"):
164
- total_chars += len(block.text)
165
+ total_chars += len(block.text) # type: ignore[union-attr]
165
166
 
166
167
  # Count tools definitions if present
167
168
  if request.tools:
@@ -1,8 +1,14 @@
1
1
  """Token estimation utilities."""
2
2
 
3
+ from typing import Literal
4
+
3
5
  # Approximate characters per token for English text
4
6
  CHARS_PER_TOKEN = 4
5
7
 
8
+ AnthropicStopReason = Literal[
9
+ "end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"
10
+ ]
11
+
6
12
 
7
13
  def estimate_tokens(text: str) -> int:
8
14
  """Estimate token count from text.
@@ -31,7 +37,9 @@ def estimate_tokens_from_char_count(char_count: int) -> int:
31
37
  return char_count // CHARS_PER_TOKEN
32
38
 
33
39
 
34
- def map_openai_stop_reason_to_anthropic(openai_reason: str | None) -> str | None:
40
+ def map_openai_stop_reason_to_anthropic(
41
+ openai_reason: str | None,
42
+ ) -> AnthropicStopReason | None:
35
43
  """Map OpenAI finish reason to Anthropic stop reason.
36
44
 
37
45
  Args:
@@ -42,7 +50,7 @@ def map_openai_stop_reason_to_anthropic(openai_reason: str | None) -> str | None
42
50
  """
43
51
  if openai_reason is None:
44
52
  return None
45
- mapping = {
53
+ mapping: dict[str, AnthropicStopReason] = {
46
54
  "stop": "end_turn",
47
55
  "length": "max_tokens",
48
56
  "tool_calls": "tool_use",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: router-maestro
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
@@ -132,8 +132,8 @@ router-maestro config claude-code
132
132
 
133
133
  Models are identified using the format `{provider}/{model-id}`:
134
134
 
135
- | Example | Description |
136
- |---------|-------------|
135
+ | Example | Description |
136
+ | --------------------------------- | ----------------------------------- |
137
137
  | `github-copilot/gpt-4o` | GPT-4o via GitHub Copilot |
138
138
  | `github-copilot/claude-sonnet-4` | Claude Sonnet 4 via GitHub Copilot |
139
139
  | `openai/gpt-4-turbo` | GPT-4 Turbo via OpenAI |
@@ -164,8 +164,8 @@ router-maestro model priority list
164
164
 
165
165
  **Fallback** triggers when a request fails with a retryable error (429, 5xx):
166
166
 
167
- | Strategy | Behavior |
168
- |----------|----------|
167
+ | Strategy | Behavior |
168
+ | ------------ | ------------------------------------ |
169
169
  | `priority` | Try next model in priorities list |
170
170
  | `same-model` | Try same model on different provider |
171
171
  | `none` | Fail immediately |
@@ -195,8 +195,8 @@ POST /v1/chat/completions {"model": "anthropic/claude-3-5-sonnet", ...}
195
195
 
196
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
197
 
198
- | Context | Use Case |
199
- |---------|----------|
198
+ | Context | Use Case |
199
+ | -------- | ------------------------------------------ |
200
200
  | `local` | Default context for `router-maestro server start` |
201
201
  | `docker` | Connect to a local Docker container |
202
202
  | `my-vps` | Connect to a remote VPS deployment |
@@ -216,25 +216,25 @@ router-maestro model list
216
216
 
217
217
  ### Server
218
218
 
219
- | Command | Description |
220
- |---------|-------------|
221
- | `server start --port 8080` | Start the server |
219
+ | Command | Description |
220
+ | -------------------------- | ------------------ |
221
+ | `server start --port 8080` | Start the server |
222
222
  | `server stop` | Stop the server |
223
223
  | `server info` | Show server status |
224
224
 
225
225
  ### Authentication
226
226
 
227
- | Command | Description |
228
- |---------|-------------|
229
- | `auth login [provider]` | Authenticate with a provider |
227
+ | Command | Description |
228
+ | ----------------------- | ------------------------------ |
229
+ | `auth login [provider]` | Authenticate with a provider |
230
230
  | `auth logout <provider>` | Remove authentication |
231
231
  | `auth list` | List authenticated providers |
232
232
 
233
233
  ### Models
234
234
 
235
- | Command | Description |
236
- |---------|-------------|
237
- | `model list` | List available models |
235
+ | Command | Description |
236
+ | ---------------------------------- | ---------------------- |
237
+ | `model list` | List available models |
238
238
  | `model refresh` | Refresh models cache |
239
239
  | `model priority list` | Show priorities |
240
240
  | `model priority <model> --position <n>` | Set priority |
@@ -242,9 +242,9 @@ router-maestro model list
242
242
 
243
243
  ### Contexts (Remote Management)
244
244
 
245
- | Command | Description |
246
- |---------|-------------|
247
- | `context show` | Show current context |
245
+ | Command | Description |
246
+ | ---------------------------------------------------- | -------------------- |
247
+ | `context show` | Show current context |
248
248
  | `context list` | List all contexts |
249
249
  | `context set <name>` | Switch context |
250
250
  | `context add <name> --endpoint <url> --api-key <key>` | Add remote context |
@@ -252,8 +252,8 @@ router-maestro model list
252
252
 
253
253
  ### Other
254
254
 
255
- | Command | Description |
256
- |---------|-------------|
255
+ | Command | Description |
256
+ | -------------------- | ----------------------------- |
257
257
  | `config claude-code` | Generate Claude Code settings |
258
258
 
259
259
  ## API Reference
@@ -301,8 +301,8 @@ POST /api/admin/models/refresh # Refresh model cache
301
301
 
302
302
  Following XDG Base Directory specification:
303
303
 
304
- | Type | Path | Contents |
305
- |------|------|----------|
304
+ | Type | Path | Contents |
305
+ | ---------- | ---------------------------------- | ---------------------------- |
306
306
  | **Config** | `~/.config/router-maestro/` | |
307
307
  | | `providers.json` | Custom provider definitions |
308
308
  | | `priorities.json` | Model priorities and fallback |
@@ -340,8 +340,8 @@ export OLLAMA_API_KEY="sk-..."
340
340
 
341
341
  Configuration files are automatically reloaded every 5 minutes:
342
342
 
343
- | File | Auto-Reload |
344
- |------|-------------|
343
+ | File | Auto-Reload |
344
+ | ------------------ | ---------------- |
345
345
  | `priorities.json` | ✓ (5 min) |
346
346
  | `providers.json` | ✓ (5 min) |
347
347
  | `auth.json` | Requires restart |
@@ -405,6 +405,7 @@ router-maestro model list
405
405
  The Docker Compose setup includes Traefik for automatic HTTPS via Let's Encrypt with DNS challenge.
406
406
 
407
407
  For detailed configuration options including:
408
+
408
409
  - Other DNS providers (Route53, DigitalOcean, etc.)
409
410
  - HTTP challenge setup
410
411
  - Traefik dashboard configuration
@@ -1,4 +1,4 @@
1
- router_maestro/__init__.py,sha256=uL361mf7XE2DqHpdJ9tIuNVF8iku3OIPbWlNADxwL5s,92
1
+ router_maestro/__init__.py,sha256=E3wx7rchrL5OkqXGemj0n4c8To9tSZh-9KiPk7jpf5o,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
@@ -7,7 +7,7 @@ router_maestro/auth/storage.py,sha256=TCLxgQ1lWcWD4xJXJzx5OMpvuAun_LSRItK0zhR6H0
7
7
  router_maestro/cli/__init__.py,sha256=yIAshaHpLL0WrDFmRpoMRM2EUe75x0wmM5NlGW3C89s,37
8
8
  router_maestro/cli/auth.py,sha256=eq5LBUohbMnHS4dZeyvq4OQAjzdrJ-StP2FGuUhkKa0,5940
9
9
  router_maestro/cli/client.py,sha256=mRzpsA_Dxn-Xq7W1_t6EiyddMI0a3cvuTL6-2JuV4mE,9383
10
- router_maestro/cli/config.py,sha256=lVmMlUASUynbqOQawuQQhi8C3h2OvGScZvaeIArZ2ns,4662
10
+ router_maestro/cli/config.py,sha256=SGrWdd59eX1F8KzhHMJ_u2AkYfFuYlyLl6Nfz6u_rNk,6100
11
11
  router_maestro/cli/context.py,sha256=EPbT7fReIW17veU76CSAcv8QjzMsCIPm1QDBlGsV8fQ,4549
12
12
  router_maestro/cli/main.py,sha256=5yiK4Q149goSB2KKzgMuF5EpcC8FBzOUCkEt8wY5NAU,1314
13
13
  router_maestro/cli/model.py,sha256=2IG3IpQWh8Ejdv5Htcgr90O2v2UAa80TU15oOniPdvk,9054
@@ -26,7 +26,7 @@ router_maestro/providers/copilot.py,sha256=NieOXBecwymSnB1Nm_JKVvzeZyZnO8g6Y1kNa
26
26
  router_maestro/providers/openai.py,sha256=Bsq5mzAVf4CawH2Tn80y3-MyLLVeZ3VsPxGDNH1t_Nk,7647
27
27
  router_maestro/providers/openai_compat.py,sha256=ef4RttKVZUTBiRed4BEuC2Jg8vr5GM7YqPUtFYeAhZo,6383
28
28
  router_maestro/routing/__init__.py,sha256=eCEQVbg1LAfcSVLQZpZtYf8ImbOhFIaR7POUb1pCbXM,169
29
- router_maestro/routing/router.py,sha256=4T-yBI9fCRKXeyjeVT6aSfALj2OMaNwsn11qXntno-o,20390
29
+ router_maestro/routing/router.py,sha256=uPk_sSyxdcmq-b-LJgPwtQOLNXDAvjDjs2A8po7MqQE,20402
30
30
  router_maestro/server/__init__.py,sha256=YzExJfP0jw6hXx84lo0yPVU0wG17B16SfdyEpjstpxk,128
31
31
  router_maestro/server/app.py,sha256=rI2TuCS_STzmzdds5Tq2vzoP9y9bpRFJJwNs5e8uTbU,2707
32
32
  router_maestro/server/oauth_sessions.py,sha256=r_VM6vAtbo5HAmXjYt6XoECBcWGxqABKGbVmW8HoCz0,4625
@@ -35,7 +35,7 @@ router_maestro/server/middleware/__init__.py,sha256=PhtP2E04wApnOUBLE76mrOa0sSHp
35
35
  router_maestro/server/middleware/auth.py,sha256=Ak3k5cC8m4qPGUIheuOB--QiFvs6GIAcTRJqtCGCjAA,2018
36
36
  router_maestro/server/routes/__init__.py,sha256=eGEpNCnSRVQC1pFL7_evDmZfkMrviuI-n1okAS-YnhM,397
37
37
  router_maestro/server/routes/admin.py,sha256=oub4hDrYaytuorXkJzmz0YZ4Z2rcyNuwKcK_4IGvcDY,8942
38
- router_maestro/server/routes/anthropic.py,sha256=T5-rHBPDyPxP4Cs0yzm7Kvvn-zgV6jspnZdoSVDeH2w,8041
38
+ router_maestro/server/routes/anthropic.py,sha256=3ILc6m9UlqSsRsWXZYk7DWSu1o5LI7FZgoqPo16D56U,8063
39
39
  router_maestro/server/routes/chat.py,sha256=vyYX1ILhgAb9HYD87h1U3c5btpplqkTaejA81pWg4Oo,4752
40
40
  router_maestro/server/routes/models.py,sha256=PTSXojNFN9j90Bke74ZO6sEsfIc8u_4A69eW1QzFIbc,716
41
41
  router_maestro/server/schemas/__init__.py,sha256=VmJZoTMLb-bF33m79urhbejVdLfjDGMqCJP5QvWbHsU,1176
@@ -44,9 +44,9 @@ router_maestro/server/schemas/anthropic.py,sha256=hNl6rZ7AX-HdLxtsd0cWpZjpIyK1Ak
44
44
  router_maestro/server/schemas/openai.py,sha256=s2487RYIn1h-CIaUpLue9BScDaTsafbVg5yc-kKhfME,2141
45
45
  router_maestro/utils/__init__.py,sha256=oSQyV--FueMPggRfjWWVnAKtjkcZWFOm9hCTymu0oZU,409
46
46
  router_maestro/utils/logging.py,sha256=gJWoRYibAxCWn4VmTmnrwpBRzQ7Uu5YIEk5zDiF9X_k,2393
47
- router_maestro/utils/tokens.py,sha256=t2E5BrrE5X3VCgw-rYFMkic7heJ0huj9rrOXAIlKq8o,1330
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,,
47
+ router_maestro/utils/tokens.py,sha256=U5PXJv_6ba5xgMBG0c5qB96Yu6uLscSUjMWYTdNests,1530
48
+ router_maestro-0.1.5.dist-info/METADATA,sha256=xqHRuMfsP0uG-H9Vv89iWmH0-wjaMrdGvuov1WH4juA,12161
49
+ router_maestro-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
50
+ router_maestro-0.1.5.dist-info/entry_points.txt,sha256=zoFUxxvNcFe0nTgpRbIdygIDEOla3KbvW6HbOCOlgv4,63
51
+ router_maestro-0.1.5.dist-info/licenses/LICENSE,sha256=Ea86BSGu7_tpLAuzif_JmM9zjMoKQEf95VVF9sZw3Jo,1084
52
+ router_maestro-0.1.5.dist-info/RECORD,,