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,175 @@
1
+ """OpenAI-compatible provider for custom endpoints."""
2
+
3
+ from collections.abc import AsyncIterator
4
+
5
+ import httpx
6
+
7
+ from router_maestro.providers.base import (
8
+ BaseProvider,
9
+ ChatRequest,
10
+ ChatResponse,
11
+ ChatStreamChunk,
12
+ ModelInfo,
13
+ ProviderError,
14
+ )
15
+
16
+
17
+ class OpenAICompatibleProvider(BaseProvider):
18
+ """OpenAI-compatible provider for custom endpoints."""
19
+
20
+ def __init__(
21
+ self,
22
+ name: str,
23
+ base_url: str,
24
+ api_key: str,
25
+ models: dict[str, str] | None = None,
26
+ ) -> None:
27
+ """Initialize the provider.
28
+
29
+ Args:
30
+ name: Provider name
31
+ base_url: Base URL for API requests
32
+ api_key: API key for authentication
33
+ models: Dict of model_id -> display_name
34
+ """
35
+ self.name = name
36
+ self.base_url = base_url.rstrip("/")
37
+ self.api_key = api_key
38
+ self._models = models or {}
39
+
40
+ def is_authenticated(self) -> bool:
41
+ """Check if authenticated (always true for custom providers)."""
42
+ return bool(self.api_key)
43
+
44
+ def _get_headers(self) -> dict[str, str]:
45
+ """Get headers for API requests."""
46
+ return {
47
+ "Authorization": f"Bearer {self.api_key}",
48
+ "Content-Type": "application/json",
49
+ }
50
+
51
+ async def chat_completion(self, request: ChatRequest) -> ChatResponse:
52
+ """Generate a chat completion."""
53
+ payload = {
54
+ "model": request.model,
55
+ "messages": [{"role": m.role, "content": m.content} for m in request.messages],
56
+ "temperature": request.temperature,
57
+ "stream": False,
58
+ }
59
+ if request.max_tokens:
60
+ payload["max_tokens"] = request.max_tokens
61
+
62
+ # Merge any extra parameters
63
+ payload.update(request.extra)
64
+
65
+ async with httpx.AsyncClient() as client:
66
+ try:
67
+ response = await client.post(
68
+ f"{self.base_url}/chat/completions",
69
+ json=payload,
70
+ headers=self._get_headers(),
71
+ timeout=120.0,
72
+ )
73
+ response.raise_for_status()
74
+ data = response.json()
75
+
76
+ return ChatResponse(
77
+ content=data["choices"][0]["message"]["content"],
78
+ model=data.get("model", request.model),
79
+ finish_reason=data["choices"][0].get("finish_reason", "stop"),
80
+ usage=data.get("usage"),
81
+ )
82
+ except httpx.HTTPStatusError as e:
83
+ retryable = e.response.status_code in (429, 500, 502, 503, 504)
84
+ raise ProviderError(
85
+ f"{self.name} API error: {e.response.status_code}",
86
+ status_code=e.response.status_code,
87
+ retryable=retryable,
88
+ )
89
+ except httpx.HTTPError as e:
90
+ raise ProviderError(f"HTTP error: {e}", retryable=True)
91
+
92
+ async def chat_completion_stream(self, request: ChatRequest) -> AsyncIterator[ChatStreamChunk]:
93
+ """Generate a streaming chat completion."""
94
+ payload = {
95
+ "model": request.model,
96
+ "messages": [{"role": m.role, "content": m.content} for m in request.messages],
97
+ "temperature": request.temperature,
98
+ "stream": True,
99
+ "stream_options": {"include_usage": True}, # Request usage info in stream
100
+ }
101
+ if request.max_tokens:
102
+ payload["max_tokens"] = request.max_tokens
103
+
104
+ payload.update(request.extra)
105
+
106
+ async with httpx.AsyncClient() as client:
107
+ try:
108
+ async with client.stream(
109
+ "POST",
110
+ f"{self.base_url}/chat/completions",
111
+ json=payload,
112
+ headers=self._get_headers(),
113
+ timeout=120.0,
114
+ ) as response:
115
+ response.raise_for_status()
116
+
117
+ async for line in response.aiter_lines():
118
+ if not line or not line.startswith("data: "):
119
+ continue
120
+
121
+ data_str = line[6:]
122
+ if data_str == "[DONE]":
123
+ break
124
+
125
+ import json
126
+
127
+ data = json.loads(data_str)
128
+
129
+ if "choices" in data and data["choices"]:
130
+ delta = data["choices"][0].get("delta", {})
131
+ content = delta.get("content", "")
132
+ finish_reason = data["choices"][0].get("finish_reason")
133
+ usage = data.get("usage") # Capture usage info
134
+
135
+ if content or finish_reason:
136
+ yield ChatStreamChunk(
137
+ content=content,
138
+ finish_reason=finish_reason,
139
+ usage=usage,
140
+ )
141
+ except httpx.HTTPStatusError as e:
142
+ retryable = e.response.status_code in (429, 500, 502, 503, 504)
143
+ raise ProviderError(
144
+ f"{self.name} API error: {e.response.status_code}",
145
+ status_code=e.response.status_code,
146
+ retryable=retryable,
147
+ )
148
+ except httpx.HTTPError as e:
149
+ raise ProviderError(f"HTTP error: {e}", retryable=True)
150
+
151
+ async def list_models(self) -> list[ModelInfo]:
152
+ """List available models."""
153
+ if self._models:
154
+ return [
155
+ ModelInfo(id=model_id, name=name, provider=self.name)
156
+ for model_id, name in self._models.items()
157
+ ]
158
+
159
+ # Try to fetch from API
160
+ async with httpx.AsyncClient() as client:
161
+ try:
162
+ response = await client.get(
163
+ f"{self.base_url}/models",
164
+ headers=self._get_headers(),
165
+ timeout=30.0,
166
+ )
167
+ response.raise_for_status()
168
+ data = response.json()
169
+
170
+ return [
171
+ ModelInfo(id=model["id"], name=model["id"], provider=self.name)
172
+ for model in data.get("data", [])
173
+ ]
174
+ except httpx.HTTPError:
175
+ return []
@@ -0,0 +1,5 @@
1
+ """Routing module for router-maestro."""
2
+
3
+ from router_maestro.routing.router import Router, get_router, reset_router
4
+
5
+ __all__ = ["Router", "get_router", "reset_router"]