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,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 []
|