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,228 @@
|
|
|
1
|
+
"""Anthropic Messages API compatible route."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from fastapi.responses import StreamingResponse
|
|
9
|
+
|
|
10
|
+
from router_maestro.providers import ChatRequest, ProviderError
|
|
11
|
+
from router_maestro.routing import Router, get_router
|
|
12
|
+
from router_maestro.server.schemas.anthropic import (
|
|
13
|
+
AnthropicCountTokensRequest,
|
|
14
|
+
AnthropicMessagesRequest,
|
|
15
|
+
AnthropicMessagesResponse,
|
|
16
|
+
AnthropicStreamState,
|
|
17
|
+
AnthropicTextBlock,
|
|
18
|
+
AnthropicUsage,
|
|
19
|
+
)
|
|
20
|
+
from router_maestro.server.translation import (
|
|
21
|
+
translate_anthropic_to_openai,
|
|
22
|
+
translate_openai_chunk_to_anthropic_events,
|
|
23
|
+
)
|
|
24
|
+
from router_maestro.utils import (
|
|
25
|
+
estimate_tokens_from_char_count,
|
|
26
|
+
get_logger,
|
|
27
|
+
map_openai_stop_reason_to_anthropic,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logger = get_logger("server.routes.anthropic")
|
|
31
|
+
|
|
32
|
+
router = APIRouter()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("/v1/messages")
|
|
36
|
+
@router.post("/api/anthropic/v1/messages")
|
|
37
|
+
async def messages(request: AnthropicMessagesRequest):
|
|
38
|
+
"""Handle Anthropic Messages API requests."""
|
|
39
|
+
logger.info(
|
|
40
|
+
"Received Anthropic messages request: model=%s, stream=%s",
|
|
41
|
+
request.model,
|
|
42
|
+
request.stream,
|
|
43
|
+
)
|
|
44
|
+
model_router = get_router()
|
|
45
|
+
|
|
46
|
+
# Translate Anthropic request to OpenAI format
|
|
47
|
+
chat_request = translate_anthropic_to_openai(request)
|
|
48
|
+
|
|
49
|
+
if request.stream:
|
|
50
|
+
# Estimate input tokens for context display
|
|
51
|
+
estimated_tokens = _estimate_input_tokens(request)
|
|
52
|
+
return StreamingResponse(
|
|
53
|
+
stream_response(model_router, chat_request, request.model, estimated_tokens),
|
|
54
|
+
media_type="text/event-stream",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
response, provider_name = await model_router.chat_completion(chat_request)
|
|
59
|
+
|
|
60
|
+
# Build Anthropic response
|
|
61
|
+
content = []
|
|
62
|
+
if response.content:
|
|
63
|
+
content.append(AnthropicTextBlock(type="text", text=response.content))
|
|
64
|
+
|
|
65
|
+
usage = AnthropicUsage(
|
|
66
|
+
input_tokens=response.usage.get("prompt_tokens", 0) if response.usage else 0,
|
|
67
|
+
output_tokens=response.usage.get("completion_tokens", 0) if response.usage else 0,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Map finish reason
|
|
71
|
+
stop_reason = _map_finish_reason(response.finish_reason)
|
|
72
|
+
|
|
73
|
+
return AnthropicMessagesResponse(
|
|
74
|
+
id=f"msg_{uuid.uuid4().hex[:24]}",
|
|
75
|
+
type="message",
|
|
76
|
+
role="assistant",
|
|
77
|
+
content=content,
|
|
78
|
+
model=response.model,
|
|
79
|
+
stop_reason=stop_reason,
|
|
80
|
+
stop_sequence=None,
|
|
81
|
+
usage=usage,
|
|
82
|
+
)
|
|
83
|
+
except ProviderError as e:
|
|
84
|
+
logger.error("Anthropic messages request failed: %s", e)
|
|
85
|
+
raise HTTPException(status_code=e.status_code, detail=str(e))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.post("/v1/messages/count_tokens")
|
|
89
|
+
@router.post("/api/anthropic/v1/messages/count_tokens")
|
|
90
|
+
async def count_tokens(request: AnthropicCountTokensRequest):
|
|
91
|
+
"""Count tokens for a messages request.
|
|
92
|
+
|
|
93
|
+
This is a simplified implementation that estimates tokens.
|
|
94
|
+
Since we're proxying to various providers, we can't get exact counts
|
|
95
|
+
without making an actual request.
|
|
96
|
+
"""
|
|
97
|
+
total_chars = 0
|
|
98
|
+
|
|
99
|
+
# Count system prompt
|
|
100
|
+
if request.system:
|
|
101
|
+
if isinstance(request.system, str):
|
|
102
|
+
total_chars += len(request.system)
|
|
103
|
+
else:
|
|
104
|
+
for block in request.system:
|
|
105
|
+
total_chars += len(block.text)
|
|
106
|
+
|
|
107
|
+
# Count messages
|
|
108
|
+
for msg in request.messages:
|
|
109
|
+
content = msg.content if hasattr(msg, "content") else msg.get("content", "")
|
|
110
|
+
if isinstance(content, str):
|
|
111
|
+
total_chars += len(content)
|
|
112
|
+
elif isinstance(content, list):
|
|
113
|
+
for block in content:
|
|
114
|
+
if isinstance(block, dict):
|
|
115
|
+
if block.get("type") == "text":
|
|
116
|
+
total_chars += len(block.get("text", ""))
|
|
117
|
+
elif hasattr(block, "text"):
|
|
118
|
+
total_chars += len(block.text)
|
|
119
|
+
|
|
120
|
+
return {"input_tokens": estimate_tokens_from_char_count(total_chars)}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _map_finish_reason(reason: str | None) -> str | None:
|
|
124
|
+
"""Map OpenAI finish reason to Anthropic stop reason."""
|
|
125
|
+
return map_openai_stop_reason_to_anthropic(reason)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _estimate_input_tokens(request: AnthropicMessagesRequest) -> int:
|
|
129
|
+
"""Estimate input tokens from request content.
|
|
130
|
+
|
|
131
|
+
Uses a rough approximation of ~4 characters per token for English text.
|
|
132
|
+
This provides an estimate for context display before actual usage is known.
|
|
133
|
+
"""
|
|
134
|
+
total_chars = 0
|
|
135
|
+
|
|
136
|
+
# Count system prompt
|
|
137
|
+
if request.system:
|
|
138
|
+
if isinstance(request.system, str):
|
|
139
|
+
total_chars += len(request.system)
|
|
140
|
+
else:
|
|
141
|
+
for block in request.system:
|
|
142
|
+
if hasattr(block, "text"):
|
|
143
|
+
total_chars += len(block.text)
|
|
144
|
+
|
|
145
|
+
# Count messages
|
|
146
|
+
for msg in request.messages:
|
|
147
|
+
content = msg.content if hasattr(msg, "content") else msg.get("content", "")
|
|
148
|
+
if isinstance(content, str):
|
|
149
|
+
total_chars += len(content)
|
|
150
|
+
elif isinstance(content, list):
|
|
151
|
+
for block in content:
|
|
152
|
+
if isinstance(block, dict):
|
|
153
|
+
if block.get("type") == "text":
|
|
154
|
+
total_chars += len(block.get("text", ""))
|
|
155
|
+
elif block.get("type") == "tool_result":
|
|
156
|
+
tool_content = block.get("content", "")
|
|
157
|
+
if isinstance(tool_content, str):
|
|
158
|
+
total_chars += len(tool_content)
|
|
159
|
+
elif isinstance(tool_content, list):
|
|
160
|
+
for tc in tool_content:
|
|
161
|
+
if isinstance(tc, dict) and tc.get("type") == "text":
|
|
162
|
+
total_chars += len(tc.get("text", ""))
|
|
163
|
+
elif hasattr(block, "text"):
|
|
164
|
+
total_chars += len(block.text)
|
|
165
|
+
|
|
166
|
+
# Count tools definitions if present
|
|
167
|
+
if request.tools:
|
|
168
|
+
for tool in request.tools:
|
|
169
|
+
if hasattr(tool, "name"):
|
|
170
|
+
total_chars += len(tool.name)
|
|
171
|
+
if hasattr(tool, "description") and tool.description:
|
|
172
|
+
total_chars += len(tool.description)
|
|
173
|
+
if hasattr(tool, "input_schema"):
|
|
174
|
+
# Rough estimate for schema
|
|
175
|
+
import json
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
schema_str = json.dumps(tool.input_schema)
|
|
179
|
+
total_chars += len(schema_str)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
return estimate_tokens_from_char_count(total_chars)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def stream_response(
|
|
187
|
+
model_router: Router,
|
|
188
|
+
request: ChatRequest,
|
|
189
|
+
original_model: str,
|
|
190
|
+
estimated_input_tokens: int = 0,
|
|
191
|
+
) -> AsyncGenerator[str, None]:
|
|
192
|
+
"""Stream Anthropic Messages API response."""
|
|
193
|
+
try:
|
|
194
|
+
stream, provider_name = await model_router.chat_completion_stream(request)
|
|
195
|
+
response_id = f"msg_{uuid.uuid4().hex[:24]}"
|
|
196
|
+
|
|
197
|
+
state = AnthropicStreamState(estimated_input_tokens=estimated_input_tokens)
|
|
198
|
+
|
|
199
|
+
async for chunk in stream:
|
|
200
|
+
# Build OpenAI-style chunk for translation
|
|
201
|
+
openai_chunk = {
|
|
202
|
+
"id": response_id,
|
|
203
|
+
"choices": [
|
|
204
|
+
{
|
|
205
|
+
"delta": {
|
|
206
|
+
"content": chunk.content if chunk.content else None,
|
|
207
|
+
"tool_calls": chunk.tool_calls,
|
|
208
|
+
},
|
|
209
|
+
"finish_reason": chunk.finish_reason,
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
"usage": chunk.usage, # Pass through usage info
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
events = translate_openai_chunk_to_anthropic_events(openai_chunk, state, original_model)
|
|
216
|
+
|
|
217
|
+
for event in events:
|
|
218
|
+
yield f"event: {event['type']}\ndata: {json.dumps(event)}\n\n"
|
|
219
|
+
|
|
220
|
+
except ProviderError as e:
|
|
221
|
+
error_event = {
|
|
222
|
+
"type": "error",
|
|
223
|
+
"error": {
|
|
224
|
+
"type": "api_error",
|
|
225
|
+
"message": str(e),
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
yield f"event: error\ndata: {json.dumps(error_event)}\n\n"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Chat completions route."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
|
|
11
|
+
from router_maestro.providers import ChatRequest, Message, ProviderError
|
|
12
|
+
from router_maestro.routing import Router, get_router
|
|
13
|
+
from router_maestro.server.schemas import (
|
|
14
|
+
ChatCompletionChoice,
|
|
15
|
+
ChatCompletionChunk,
|
|
16
|
+
ChatCompletionChunkChoice,
|
|
17
|
+
ChatCompletionChunkDelta,
|
|
18
|
+
ChatCompletionRequest,
|
|
19
|
+
ChatCompletionResponse,
|
|
20
|
+
ChatCompletionUsage,
|
|
21
|
+
ChatMessage,
|
|
22
|
+
)
|
|
23
|
+
from router_maestro.utils import get_logger
|
|
24
|
+
|
|
25
|
+
logger = get_logger("server.routes.chat")
|
|
26
|
+
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.post("/chat/completions")
|
|
31
|
+
@router.post("/v1/chat/completions")
|
|
32
|
+
async def chat_completions(request: ChatCompletionRequest):
|
|
33
|
+
"""Handle chat completion requests."""
|
|
34
|
+
logger.info(
|
|
35
|
+
"Received chat completion request: model=%s, stream=%s",
|
|
36
|
+
request.model,
|
|
37
|
+
request.stream,
|
|
38
|
+
)
|
|
39
|
+
model_router = get_router()
|
|
40
|
+
|
|
41
|
+
# Convert to internal format
|
|
42
|
+
chat_request = ChatRequest(
|
|
43
|
+
model=request.model,
|
|
44
|
+
messages=[Message(role=m.role, content=m.content) for m in request.messages],
|
|
45
|
+
temperature=request.temperature,
|
|
46
|
+
max_tokens=request.max_tokens,
|
|
47
|
+
stream=request.stream,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if request.stream:
|
|
51
|
+
return StreamingResponse(
|
|
52
|
+
stream_response(model_router, chat_request),
|
|
53
|
+
media_type="text/event-stream",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
response, provider_name = await model_router.chat_completion(chat_request)
|
|
58
|
+
|
|
59
|
+
usage = None
|
|
60
|
+
if response.usage:
|
|
61
|
+
usage = ChatCompletionUsage(
|
|
62
|
+
prompt_tokens=response.usage.get("prompt_tokens", 0),
|
|
63
|
+
completion_tokens=response.usage.get("completion_tokens", 0),
|
|
64
|
+
total_tokens=response.usage.get("total_tokens", 0),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return ChatCompletionResponse(
|
|
68
|
+
id=f"chatcmpl-{uuid.uuid4().hex[:8]}",
|
|
69
|
+
created=int(time.time()),
|
|
70
|
+
model=response.model,
|
|
71
|
+
choices=[
|
|
72
|
+
ChatCompletionChoice(
|
|
73
|
+
index=0,
|
|
74
|
+
message=ChatMessage(role="assistant", content=response.content),
|
|
75
|
+
finish_reason=response.finish_reason,
|
|
76
|
+
)
|
|
77
|
+
],
|
|
78
|
+
usage=usage,
|
|
79
|
+
)
|
|
80
|
+
except ProviderError as e:
|
|
81
|
+
logger.error("Chat completion request failed: %s", e)
|
|
82
|
+
raise HTTPException(status_code=e.status_code, detail=str(e))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def stream_response(model_router: Router, request: ChatRequest) -> AsyncGenerator[str, None]:
|
|
86
|
+
"""Stream chat completion response."""
|
|
87
|
+
try:
|
|
88
|
+
stream, provider_name = await model_router.chat_completion_stream(request)
|
|
89
|
+
response_id = f"chatcmpl-{uuid.uuid4().hex[:8]}"
|
|
90
|
+
created = int(time.time())
|
|
91
|
+
|
|
92
|
+
# Send initial chunk with role
|
|
93
|
+
initial_chunk = ChatCompletionChunk(
|
|
94
|
+
id=response_id,
|
|
95
|
+
created=created,
|
|
96
|
+
model=request.model,
|
|
97
|
+
choices=[
|
|
98
|
+
ChatCompletionChunkChoice(
|
|
99
|
+
index=0,
|
|
100
|
+
delta=ChatCompletionChunkDelta(role="assistant"),
|
|
101
|
+
finish_reason=None,
|
|
102
|
+
)
|
|
103
|
+
],
|
|
104
|
+
)
|
|
105
|
+
yield f"data: {initial_chunk.model_dump_json()}\n\n"
|
|
106
|
+
|
|
107
|
+
async for chunk in stream:
|
|
108
|
+
if chunk.content:
|
|
109
|
+
chunk_response = ChatCompletionChunk(
|
|
110
|
+
id=response_id,
|
|
111
|
+
created=created,
|
|
112
|
+
model=request.model,
|
|
113
|
+
choices=[
|
|
114
|
+
ChatCompletionChunkChoice(
|
|
115
|
+
index=0,
|
|
116
|
+
delta=ChatCompletionChunkDelta(content=chunk.content),
|
|
117
|
+
finish_reason=None,
|
|
118
|
+
)
|
|
119
|
+
],
|
|
120
|
+
)
|
|
121
|
+
yield f"data: {chunk_response.model_dump_json()}\n\n"
|
|
122
|
+
|
|
123
|
+
if chunk.finish_reason:
|
|
124
|
+
final_chunk = ChatCompletionChunk(
|
|
125
|
+
id=response_id,
|
|
126
|
+
created=created,
|
|
127
|
+
model=request.model,
|
|
128
|
+
choices=[
|
|
129
|
+
ChatCompletionChunkChoice(
|
|
130
|
+
index=0,
|
|
131
|
+
delta=ChatCompletionChunkDelta(),
|
|
132
|
+
finish_reason=chunk.finish_reason,
|
|
133
|
+
)
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
yield f"data: {final_chunk.model_dump_json()}\n\n"
|
|
137
|
+
|
|
138
|
+
yield "data: [DONE]\n\n"
|
|
139
|
+
|
|
140
|
+
except ProviderError as e:
|
|
141
|
+
error_data = {"error": {"message": str(e), "type": "provider_error"}}
|
|
142
|
+
yield f"data: {json.dumps(error_data)}\n\n"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Models route."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from router_maestro.routing import Router
|
|
8
|
+
from router_maestro.server.schemas import ModelList, ModelObject
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_router() -> Router:
|
|
14
|
+
"""Get the router instance."""
|
|
15
|
+
return Router()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/models")
|
|
19
|
+
@router.get("/v1/models")
|
|
20
|
+
async def list_models() -> ModelList:
|
|
21
|
+
"""List available models."""
|
|
22
|
+
model_router = get_router()
|
|
23
|
+
models = await model_router.list_models()
|
|
24
|
+
|
|
25
|
+
return ModelList(
|
|
26
|
+
data=[
|
|
27
|
+
ModelObject(
|
|
28
|
+
id=model.id,
|
|
29
|
+
created=int(time.time()),
|
|
30
|
+
owned_by=model.provider,
|
|
31
|
+
)
|
|
32
|
+
for model in models
|
|
33
|
+
]
|
|
34
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Server schemas."""
|
|
2
|
+
|
|
3
|
+
from router_maestro.server.schemas.admin import (
|
|
4
|
+
AuthListResponse,
|
|
5
|
+
AuthProviderInfo,
|
|
6
|
+
LoginRequest,
|
|
7
|
+
ModelInfo,
|
|
8
|
+
ModelsResponse,
|
|
9
|
+
OAuthInitResponse,
|
|
10
|
+
OAuthStatusResponse,
|
|
11
|
+
PrioritiesResponse,
|
|
12
|
+
PrioritiesUpdateRequest,
|
|
13
|
+
StatsQuery,
|
|
14
|
+
StatsResponse,
|
|
15
|
+
)
|
|
16
|
+
from router_maestro.server.schemas.openai import (
|
|
17
|
+
ChatCompletionChoice,
|
|
18
|
+
ChatCompletionChunk,
|
|
19
|
+
ChatCompletionChunkChoice,
|
|
20
|
+
ChatCompletionChunkDelta,
|
|
21
|
+
ChatCompletionRequest,
|
|
22
|
+
ChatCompletionResponse,
|
|
23
|
+
ChatCompletionUsage,
|
|
24
|
+
ChatMessage,
|
|
25
|
+
ErrorDetail,
|
|
26
|
+
ErrorResponse,
|
|
27
|
+
ModelList,
|
|
28
|
+
ModelObject,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Admin schemas
|
|
33
|
+
"AuthListResponse",
|
|
34
|
+
"AuthProviderInfo",
|
|
35
|
+
"LoginRequest",
|
|
36
|
+
"ModelInfo",
|
|
37
|
+
"ModelsResponse",
|
|
38
|
+
"OAuthInitResponse",
|
|
39
|
+
"OAuthStatusResponse",
|
|
40
|
+
"PrioritiesResponse",
|
|
41
|
+
"PrioritiesUpdateRequest",
|
|
42
|
+
"StatsQuery",
|
|
43
|
+
"StatsResponse",
|
|
44
|
+
# OpenAI schemas
|
|
45
|
+
"ChatCompletionChoice",
|
|
46
|
+
"ChatCompletionChunk",
|
|
47
|
+
"ChatCompletionChunkChoice",
|
|
48
|
+
"ChatCompletionChunkDelta",
|
|
49
|
+
"ChatCompletionRequest",
|
|
50
|
+
"ChatCompletionResponse",
|
|
51
|
+
"ChatCompletionUsage",
|
|
52
|
+
"ChatMessage",
|
|
53
|
+
"ErrorDetail",
|
|
54
|
+
"ErrorResponse",
|
|
55
|
+
"ModelList",
|
|
56
|
+
"ModelObject",
|
|
57
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Admin API schemas for remote management."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AuthProviderInfo(BaseModel):
|
|
7
|
+
"""Information about an authenticated provider."""
|
|
8
|
+
|
|
9
|
+
provider: str = Field(..., description="Provider name")
|
|
10
|
+
auth_type: str = Field(..., description="Authentication type: 'oauth' or 'api'")
|
|
11
|
+
status: str = Field(..., description="Status: 'active' or 'expired'")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthListResponse(BaseModel):
|
|
15
|
+
"""Response for listing authenticated providers."""
|
|
16
|
+
|
|
17
|
+
providers: list[AuthProviderInfo] = Field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LoginRequest(BaseModel):
|
|
21
|
+
"""Request to initiate login."""
|
|
22
|
+
|
|
23
|
+
provider: str = Field(..., description="Provider to authenticate with")
|
|
24
|
+
api_key: str | None = Field(default=None, description="API key for API key auth")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OAuthInitResponse(BaseModel):
|
|
28
|
+
"""Response for OAuth initialization (device flow)."""
|
|
29
|
+
|
|
30
|
+
session_id: str = Field(..., description="Session ID for polling status")
|
|
31
|
+
user_code: str = Field(..., description="Code to enter at verification URL")
|
|
32
|
+
verification_uri: str = Field(..., description="URL to visit for authorization")
|
|
33
|
+
expires_in: int = Field(..., description="Seconds until expiration")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OAuthStatusResponse(BaseModel):
|
|
37
|
+
"""Response for OAuth status polling."""
|
|
38
|
+
|
|
39
|
+
status: str = Field(..., description="Status: 'pending', 'complete', 'expired', or 'error'")
|
|
40
|
+
error: str | None = Field(default=None, description="Error message if status is 'error'")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ModelInfo(BaseModel):
|
|
44
|
+
"""Information about a model."""
|
|
45
|
+
|
|
46
|
+
provider: str = Field(..., description="Provider name")
|
|
47
|
+
id: str = Field(..., description="Model ID")
|
|
48
|
+
name: str = Field(..., description="Display name")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModelsResponse(BaseModel):
|
|
52
|
+
"""Response for listing models."""
|
|
53
|
+
|
|
54
|
+
models: list[ModelInfo] = Field(default_factory=list)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PrioritiesResponse(BaseModel):
|
|
58
|
+
"""Response for getting priorities."""
|
|
59
|
+
|
|
60
|
+
priorities: list[str] = Field(default_factory=list, description="Model priorities in order")
|
|
61
|
+
fallback: dict = Field(default_factory=dict, description="Fallback configuration")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PrioritiesUpdateRequest(BaseModel):
|
|
65
|
+
"""Request to update priorities."""
|
|
66
|
+
|
|
67
|
+
priorities: list[str] = Field(..., description="New priority list")
|
|
68
|
+
fallback: dict | None = Field(default=None, description="Optional fallback config update")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StatsQuery(BaseModel):
|
|
72
|
+
"""Query parameters for stats."""
|
|
73
|
+
|
|
74
|
+
days: int = Field(default=7, ge=1, le=365, description="Number of days to query")
|
|
75
|
+
provider: str | None = Field(default=None, description="Filter by provider")
|
|
76
|
+
model: str | None = Field(default=None, description="Filter by model")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class StatsResponse(BaseModel):
|
|
80
|
+
"""Response for usage statistics."""
|
|
81
|
+
|
|
82
|
+
total_requests: int = Field(default=0)
|
|
83
|
+
total_tokens: int = Field(default=0)
|
|
84
|
+
prompt_tokens: int = Field(default=0)
|
|
85
|
+
completion_tokens: int = Field(default=0)
|
|
86
|
+
by_provider: dict[str, dict] = Field(default_factory=dict)
|
|
87
|
+
by_model: dict[str, dict] = Field(default_factory=dict)
|