david-baxter commited on
Commit
1e08a19
·
verified ·
1 Parent(s): 225774a

Upload 16 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+
3
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4
+
5
+ WORKDIR /app
6
+
7
+ COPY pyproject.toml uv.lock ./
8
+ RUN uv sync --frozen --no-dev --no-install-project
9
+
10
+ COPY config.py main.py ./
11
+ COPY api/ api/
12
+
13
+ ENV API_HOST=0.0.0.0
14
+ ENV API_PORT=8080
15
+
16
+ EXPOSE 8080
17
+
18
+ CMD ["uv", "run", "uvicorn", "api.server:app", "--host", "0.0.0.0", "--port", "8080"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XXXxx7258
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
api/__init__.py ADDED
File without changes
api/converter/__init__.py ADDED
File without changes
api/converter/messages.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert between OpenAI chat format and AI SDK v6 format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from nanoid import generate as nanoid
8
+
9
+ from config import MODEL_MAP
10
+
11
+ _ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
12
+
13
+
14
+ def _gen_id(prefix: str = "msg", size: int = 12) -> str:
15
+ return f"{prefix}_{nanoid(_ALPHABET, size)}"
16
+
17
+
18
+ def _resolve_model(model: str) -> str:
19
+ """Map short model name to assistant-ui API identifier."""
20
+ if model in MODEL_MAP:
21
+ return MODEL_MAP[model]
22
+ if "/" in model:
23
+ return model
24
+ return f"openai/{model}"
25
+
26
+
27
+ def _guess_media_type(url: str) -> str:
28
+ """Infer media type from a data-URI or file extension."""
29
+ if url.startswith("data:"):
30
+ # data:image/png;base64,...
31
+ header = url.split(",", 1)[0]
32
+ if ";" in header:
33
+ return header[5:].split(";")[0] # strip "data:" prefix
34
+ return header[5:]
35
+ lower = url.lower()
36
+ for ext, mt in (
37
+ (".png", "image/png"), (".jpg", "image/jpeg"), (".jpeg", "image/jpeg"),
38
+ (".gif", "image/gif"), (".webp", "image/webp"), (".svg", "image/svg+xml"),
39
+ ):
40
+ if ext in lower:
41
+ return mt
42
+ return "image/png"
43
+
44
+
45
+ def _convert_tools(tools: list[dict] | None) -> dict:
46
+ """Convert OpenAI tools list to AI SDK frontend tools format.
47
+
48
+ OpenAI format:
49
+ [{"type": "function", "function": {"name": "...", "description": "...",
50
+ "parameters": {...}}}]
51
+
52
+ AI SDK format:
53
+ {"tool_name": {"description": "...", "parameters": {...}}}
54
+ """
55
+ if not tools:
56
+ return {}
57
+ result = {}
58
+ for tool in tools:
59
+ if tool.get("type") != "function":
60
+ continue
61
+ func = tool.get("function", {})
62
+ name = func.get("name", "")
63
+ if not name:
64
+ continue
65
+ entry: dict = {"parameters": func.get("parameters", {"type": "object"})}
66
+ if func.get("description"):
67
+ entry["description"] = func["description"]
68
+ result[name] = entry
69
+ return result
70
+
71
+
72
+ def openai_to_ai_sdk(
73
+ messages: list[dict],
74
+ model: str,
75
+ tools: list[dict] | None = None,
76
+ ) -> dict:
77
+ """Convert an OpenAI chat-completions request to AI SDK v6 payload."""
78
+ sdk_messages: list[dict] = []
79
+
80
+ for msg in messages:
81
+ role = msg.get("role", "")
82
+ content = msg.get("content", "")
83
+
84
+ if role == "system":
85
+ # Inject as AI SDK system message in the messages array.
86
+ # convertToModelMessages() handles role:"system" and forwards
87
+ # it as a model-level system message — bypasses the server's
88
+ # missing top-level "system" param in streamText().
89
+ text = content if isinstance(content, str) else ""
90
+ if text:
91
+ sdk_messages.append({
92
+ "role": "system",
93
+ "parts": [{"type": "text", "text": text}],
94
+ "metadata": {"custom": {}},
95
+ "id": _gen_id("sys"),
96
+ })
97
+ continue
98
+
99
+ if role == "user":
100
+ if isinstance(content, list):
101
+ parts = []
102
+ for part in content:
103
+ if isinstance(part, str):
104
+ parts.append({"type": "text", "text": part})
105
+ elif isinstance(part, dict):
106
+ ptype = part.get("type", "")
107
+ if ptype == "text":
108
+ parts.append({"type": "text", "text": part["text"]})
109
+ elif ptype == "image_url":
110
+ # OpenAI vision format → AI SDK file part
111
+ img = part.get("image_url", {})
112
+ url = img.get("url", "") if isinstance(img, dict) else str(img)
113
+ media_type = _guess_media_type(url)
114
+ parts.append({
115
+ "type": "file",
116
+ "mediaType": media_type,
117
+ "url": url,
118
+ })
119
+ else:
120
+ parts = [{"type": "text", "text": str(content)}]
121
+ sdk_messages.append({
122
+ "role": "user",
123
+ "parts": parts,
124
+ "metadata": {"custom": {}},
125
+ "id": _gen_id("msg"),
126
+ })
127
+
128
+ elif role == "assistant":
129
+ parts = []
130
+ # Text content
131
+ if isinstance(content, str) and content:
132
+ parts.append({"type": "text", "text": content})
133
+ elif isinstance(content, list):
134
+ for part in content:
135
+ if isinstance(part, dict) and part.get("type") == "text":
136
+ parts.append({"type": "text", "text": part["text"]})
137
+ # Tool calls → tool-invocation parts
138
+ # Initially set state to "input-available" (args ready, no result yet)
139
+ for tc in msg.get("tool_calls") or []:
140
+ func = tc.get("function", {})
141
+ try:
142
+ args = json.loads(func.get("arguments", "{}"))
143
+ except (json.JSONDecodeError, TypeError):
144
+ args = {}
145
+ parts.append({
146
+ "type": "tool-invocation",
147
+ "toolCallId": tc.get("id", _gen_id("call")),
148
+ "toolName": func.get("name", ""),
149
+ "input": args,
150
+ "state": "input-available",
151
+ })
152
+ sdk_messages.append({
153
+ "role": "assistant",
154
+ "parts": parts,
155
+ "metadata": {"custom": {}},
156
+ "id": _gen_id("msg"),
157
+ })
158
+
159
+ elif role == "tool":
160
+ # Tool result — attach output to the matching tool-invocation
161
+ # in the preceding assistant message, using AI SDK v6 field names.
162
+ tool_call_id = msg.get("tool_call_id", "")
163
+ # Parse result: try JSON object first, fall back to string
164
+ if isinstance(content, str):
165
+ try:
166
+ result_obj = json.loads(content)
167
+ except (json.JSONDecodeError, TypeError):
168
+ result_obj = content
169
+ else:
170
+ result_obj = content
171
+ for prev in reversed(sdk_messages):
172
+ if prev["role"] != "assistant":
173
+ continue
174
+ for part in prev["parts"]:
175
+ if (
176
+ part.get("type") == "tool-invocation"
177
+ and part.get("toolCallId") == tool_call_id
178
+ ):
179
+ part["state"] = "output-available"
180
+ part["output"] = result_obj
181
+ break
182
+ break
183
+
184
+ return {
185
+ "system": "",
186
+ "config": {"modelName": _resolve_model(model)},
187
+ "tools": _convert_tools(tools),
188
+ "id": _gen_id("thread"),
189
+ "messages": sdk_messages,
190
+ "trigger": "submit-message",
191
+ "metadata": {},
192
+ }
api/converter/stream.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert AI SDK v6 Data Stream SSE to OpenAI chat-completions SSE format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from collections.abc import AsyncIterator
8
+
9
+
10
+ def _make_chunk(
11
+ request_id: str,
12
+ model: str,
13
+ *,
14
+ delta: dict,
15
+ finish_reason: str | None = None,
16
+ usage: dict | None = None,
17
+ ) -> str:
18
+ """Format a single OpenAI SSE chunk."""
19
+ chunk: dict = {
20
+ "id": request_id,
21
+ "object": "chat.completion.chunk",
22
+ "created": int(time.time()),
23
+ "model": model,
24
+ "choices": [
25
+ {
26
+ "index": 0,
27
+ "delta": delta,
28
+ "finish_reason": finish_reason,
29
+ }
30
+ ],
31
+ }
32
+ if usage is not None:
33
+ chunk["usage"] = usage
34
+ return f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
35
+
36
+
37
+ def _extract_usage(event: dict) -> dict | None:
38
+ """Extract usage from finish event (handles both /api/chat and /api/doc/chat)."""
39
+ meta = event.get("messageMetadata", {})
40
+ # /api/doc/chat nests under custom.usage
41
+ raw = meta.get("custom", {}).get("usage")
42
+ # /api/chat puts usage directly in messageMetadata
43
+ if not raw:
44
+ raw = meta.get("usage")
45
+ if not raw:
46
+ return None
47
+ return {
48
+ "prompt_tokens": raw.get("inputTokens", 0),
49
+ "completion_tokens": raw.get("outputTokens", 0),
50
+ "total_tokens": raw.get("totalTokens", 0),
51
+ }
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Streaming conversion
56
+ # ---------------------------------------------------------------------------
57
+
58
+ async def convert_stream(
59
+ lines: AsyncIterator[str],
60
+ model: str,
61
+ request_id: str,
62
+ ) -> AsyncIterator[str]:
63
+ """Yield OpenAI-compatible SSE strings from an AI SDK data-stream."""
64
+ role_sent = False
65
+ # Accumulate tool call argument deltas per toolCallId
66
+ tool_calls_index: dict[str, int] = {} # toolCallId → index
67
+ next_tool_index = 0
68
+
69
+ async for raw_line in lines:
70
+ line = raw_line.rstrip("\r\n")
71
+ if not line:
72
+ continue
73
+
74
+ if line == "data: [DONE]":
75
+ yield "data: [DONE]\n\n"
76
+ return
77
+
78
+ if not line.startswith("data: "):
79
+ continue
80
+
81
+ try:
82
+ event = json.loads(line[6:])
83
+ except json.JSONDecodeError:
84
+ continue
85
+
86
+ event_type = event.get("type")
87
+
88
+ # --- Text events ---
89
+ if event_type == "text-start":
90
+ if not role_sent:
91
+ yield _make_chunk(
92
+ request_id, model, delta={"role": "assistant", "content": ""}
93
+ )
94
+ role_sent = True
95
+
96
+ elif event_type == "text-delta":
97
+ if not role_sent:
98
+ yield _make_chunk(
99
+ request_id, model, delta={"role": "assistant", "content": ""}
100
+ )
101
+ role_sent = True
102
+ yield _make_chunk(
103
+ request_id, model, delta={"content": event.get("delta", "")}
104
+ )
105
+
106
+ # --- Tool call events ---
107
+ elif event_type == "tool-input-start":
108
+ tc_id = event.get("toolCallId", "")
109
+ tool_name = event.get("toolName", "")
110
+ idx = next_tool_index
111
+ tool_calls_index[tc_id] = idx
112
+ next_tool_index += 1
113
+
114
+ delta: dict = {"tool_calls": [{
115
+ "index": idx,
116
+ "id": tc_id,
117
+ "type": "function",
118
+ "function": {"name": tool_name, "arguments": ""},
119
+ }]}
120
+ if not role_sent:
121
+ delta["role"] = "assistant"
122
+ role_sent = True
123
+ yield _make_chunk(request_id, model, delta=delta)
124
+
125
+ elif event_type == "tool-input-delta":
126
+ tc_id = event.get("toolCallId", "")
127
+ idx = tool_calls_index.get(tc_id, 0)
128
+ yield _make_chunk(
129
+ request_id, model,
130
+ delta={"tool_calls": [{
131
+ "index": idx,
132
+ "function": {"arguments": event.get("inputTextDelta", "")},
133
+ }]},
134
+ )
135
+
136
+ # tool-input-available — full args ready; nothing extra needed for
137
+ # streaming (client already accumulated deltas), but we can skip it.
138
+
139
+ # --- Finish events ---
140
+ elif event_type == "finish":
141
+ finish_reason = event.get("finishReason", "stop")
142
+ if finish_reason == "tool-calls":
143
+ finish_reason = "tool_calls"
144
+ usage = _extract_usage(event)
145
+ yield _make_chunk(
146
+ request_id, model,
147
+ delta={},
148
+ finish_reason=finish_reason,
149
+ usage=usage,
150
+ )
151
+
152
+ yield "data: [DONE]\n\n"
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Non-streaming helpers
157
+ # ---------------------------------------------------------------------------
158
+
159
+ def parse_full_response(lines: list[str]) -> tuple[str, list[dict], str, dict | None]:
160
+ """Parse all SSE lines into (content, tool_calls, finish_reason, usage)."""
161
+ content_parts: list[str] = []
162
+ tool_calls: list[dict] = []
163
+ # Accumulate args per toolCallId
164
+ tool_args: dict[str, list[str]] = {}
165
+ tool_meta: dict[str, dict] = {} # toolCallId → {name, id}
166
+ finish_reason = "stop"
167
+ usage = None
168
+
169
+ for raw_line in lines:
170
+ line = raw_line.rstrip("\r\n")
171
+ if not line or not line.startswith("data: ") or line == "data: [DONE]":
172
+ continue
173
+ try:
174
+ event = json.loads(line[6:])
175
+ except json.JSONDecodeError:
176
+ continue
177
+
178
+ etype = event.get("type")
179
+
180
+ if etype == "text-delta":
181
+ content_parts.append(event.get("delta", ""))
182
+
183
+ elif etype == "tool-input-start":
184
+ tc_id = event.get("toolCallId", "")
185
+ tool_args[tc_id] = []
186
+ tool_meta[tc_id] = {
187
+ "name": event.get("toolName", ""),
188
+ "id": tc_id,
189
+ }
190
+
191
+ elif etype == "tool-input-delta":
192
+ tc_id = event.get("toolCallId", "")
193
+ tool_args.setdefault(tc_id, []).append(event.get("inputTextDelta", ""))
194
+
195
+ elif etype == "tool-input-available":
196
+ tc_id = event.get("toolCallId", "")
197
+ meta = tool_meta.get(tc_id, {"name": event.get("toolName", ""), "id": tc_id})
198
+ tool_calls.append({
199
+ "id": meta["id"],
200
+ "type": "function",
201
+ "function": {
202
+ "name": meta["name"],
203
+ "arguments": json.dumps(event.get("input", {}), ensure_ascii=False),
204
+ },
205
+ })
206
+
207
+ elif etype == "finish":
208
+ finish_reason = event.get("finishReason", "stop")
209
+ if finish_reason == "tool-calls":
210
+ finish_reason = "tool_calls"
211
+ usage = _extract_usage(event)
212
+
213
+ # If we got tool-input-start/delta but no tool-input-available, build from deltas
214
+ for tc_id, meta in tool_meta.items():
215
+ if not any(tc.get("id") == tc_id for tc in tool_calls):
216
+ tool_calls.append({
217
+ "id": meta["id"],
218
+ "type": "function",
219
+ "function": {
220
+ "name": meta["name"],
221
+ "arguments": "".join(tool_args.get(tc_id, [])),
222
+ },
223
+ })
224
+
225
+ return "".join(content_parts), tool_calls, finish_reason, usage
226
+
227
+
228
+ def build_non_stream_response(
229
+ request_id: str,
230
+ model: str,
231
+ content: str,
232
+ finish_reason: str = "stop",
233
+ usage: dict | None = None,
234
+ tool_calls: list[dict] | None = None,
235
+ ) -> dict:
236
+ """Build a non-streaming chat.completions response object."""
237
+ message: dict = {"role": "assistant", "content": content or None}
238
+ if tool_calls:
239
+ message["tool_calls"] = tool_calls
240
+ if not content:
241
+ message["content"] = None
242
+ resp: dict = {
243
+ "id": request_id,
244
+ "object": "chat.completion",
245
+ "created": int(time.time()),
246
+ "model": model,
247
+ "choices": [
248
+ {
249
+ "index": 0,
250
+ "message": message,
251
+ "finish_reason": finish_reason,
252
+ }
253
+ ],
254
+ "usage": usage or {
255
+ "prompt_tokens": 0,
256
+ "completion_tokens": 0,
257
+ "total_tokens": 0,
258
+ },
259
+ }
260
+ return resp
api/provider.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Call the assistant-ui upstream LLM endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+
7
+ import httpx
8
+
9
+ from config import UPSTREAM_HEADERS, UPSTREAM_URL
10
+
11
+
12
+ class UpstreamError(Exception):
13
+ def __init__(self, status_code: int, message: str):
14
+ self.status_code = status_code
15
+ super().__init__(message)
16
+
17
+
18
+ async def call_upstream(payload: dict) -> AsyncIterator[str]:
19
+ """POST to assistant-ui and yield raw SSE lines as they arrive."""
20
+ async with httpx.AsyncClient(timeout=httpx.Timeout(120, connect=10)) as client:
21
+ async with client.stream(
22
+ "POST",
23
+ UPSTREAM_URL,
24
+ json=payload,
25
+ headers=UPSTREAM_HEADERS,
26
+ ) as resp:
27
+ if resp.status_code == 429:
28
+ raise UpstreamError(429, "Rate limit exceeded (upstream: 5 req / 30s per IP)")
29
+ if resp.status_code >= 400:
30
+ body = await resp.aread()
31
+ raise UpstreamError(resp.status_code, body.decode(errors="replace")[:200])
32
+ async for line in resp.aiter_lines():
33
+ yield line
34
+
35
+
36
+ async def call_upstream_full(payload: dict) -> list[str]:
37
+ """POST to assistant-ui and collect all SSE lines (for non-stream mode)."""
38
+ lines: list[str] = []
39
+ async for line in call_upstream(payload):
40
+ lines.append(line)
41
+ return lines
api/routes/__init__.py ADDED
File without changes
api/routes/chat.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """POST /v1/chat/completions — OpenAI-compatible chat endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, Request
8
+ from fastapi.responses import JSONResponse, StreamingResponse
9
+ from pydantic import BaseModel
10
+
11
+ from api.converter.messages import openai_to_ai_sdk, _gen_id
12
+ from api.converter.stream import build_non_stream_response, convert_stream, parse_full_response
13
+ from api.provider import call_upstream, call_upstream_full
14
+ from config import DEFAULT_MODEL
15
+
16
+ router = APIRouter()
17
+
18
+
19
+ class FunctionDef(BaseModel):
20
+ name: str
21
+ description: str = ""
22
+ parameters: dict = {}
23
+
24
+
25
+ class ToolDef(BaseModel):
26
+ type: str = "function"
27
+ function: FunctionDef
28
+
29
+
30
+ class ChatMessage(BaseModel):
31
+ role: str
32
+ content: Any = ""
33
+ tool_calls: list[dict] | None = None
34
+ tool_call_id: str | None = None
35
+
36
+
37
+ class ChatRequest(BaseModel):
38
+ model: str = DEFAULT_MODEL
39
+ messages: list[ChatMessage]
40
+ stream: bool = False
41
+ tools: list[ToolDef] | None = None
42
+ tool_choice: Any = None
43
+ temperature: float | None = None
44
+ max_tokens: int | None = None
45
+
46
+
47
+ @router.post("/v1/chat/completions")
48
+ async def chat_completions(body: ChatRequest, request: Request):
49
+ request_id = f"chatcmpl-{_gen_id('', 24)}"
50
+ model = body.model
51
+
52
+ tools_raw = [t.model_dump() for t in body.tools] if body.tools else None
53
+
54
+ try:
55
+ payload = openai_to_ai_sdk(
56
+ [m.model_dump() for m in body.messages],
57
+ model,
58
+ tools=tools_raw,
59
+ )
60
+ except Exception as e:
61
+ return JSONResponse(
62
+ status_code=400,
63
+ content=_error_body(f"Invalid request: {e}", "invalid_request_error"),
64
+ )
65
+
66
+ if body.stream:
67
+ return StreamingResponse(
68
+ _stream_generator(payload, model, request_id),
69
+ media_type="text/event-stream",
70
+ headers={
71
+ "Cache-Control": "no-cache",
72
+ "X-Accel-Buffering": "no",
73
+ },
74
+ )
75
+
76
+ # Non-streaming
77
+ try:
78
+ lines = await call_upstream_full(payload)
79
+ except Exception as e:
80
+ return JSONResponse(
81
+ status_code=502,
82
+ content=_error_body(f"Upstream error: {e}", "upstream_error"),
83
+ )
84
+
85
+ content, tool_calls, finish_reason, usage = parse_full_response(lines)
86
+ return build_non_stream_response(
87
+ request_id, model, content, finish_reason, usage,
88
+ tool_calls=tool_calls or None,
89
+ )
90
+
91
+
92
+ async def _stream_generator(payload: dict, model: str, request_id: str):
93
+ try:
94
+ upstream = call_upstream(payload)
95
+ async for chunk in convert_stream(upstream, model, request_id):
96
+ yield chunk
97
+ except Exception as e:
98
+ error_chunk = {
99
+ "id": request_id,
100
+ "object": "chat.completion.chunk",
101
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "error"}],
102
+ "error": {"message": str(e), "type": "upstream_error"},
103
+ }
104
+ import json
105
+ yield f"data: {json.dumps(error_chunk)}\n\n"
106
+ yield "data: [DONE]\n\n"
107
+
108
+
109
+ def _error_body(message: str, error_type: str) -> dict:
110
+ return {"error": {"message": message, "type": error_type}}
api/routes/models.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """GET /v1/models — list available models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ from fastapi import APIRouter
8
+
9
+ from config import MODEL_MAP
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.get("/v1/models")
15
+ async def list_models() -> dict:
16
+ now = int(time.time())
17
+ data = [
18
+ {
19
+ "id": short_name,
20
+ "object": "model",
21
+ "created": now,
22
+ "owned_by": full_name.split("/")[0],
23
+ }
24
+ for short_name, full_name in MODEL_MAP.items()
25
+ ]
26
+ return {"object": "list", "data": data}
api/server.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI application — assistant-ui 2API service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import FastAPI, Request
6
+ from fastapi.responses import JSONResponse
7
+
8
+ from api.routes.chat import router as chat_router
9
+ from api.routes.models import router as models_router
10
+ from config import API_KEY
11
+
12
+ app = FastAPI(title="assistant-ui 2API", version="1.0.0")
13
+
14
+
15
+ # ---------- Auth middleware ----------
16
+ @app.middleware("http")
17
+ async def auth_middleware(request: Request, call_next):
18
+ # Skip auth for health / docs
19
+ if request.url.path in ("/", "/health", "/docs", "/openapi.json"):
20
+ return await call_next(request)
21
+
22
+ if API_KEY:
23
+ auth = request.headers.get("authorization", "")
24
+ token = auth.removeprefix("Bearer ").strip()
25
+ if token != API_KEY:
26
+ return JSONResponse(
27
+ status_code=401,
28
+ content={"error": {"message": "Invalid API key", "type": "auth_error"}},
29
+ )
30
+
31
+ return await call_next(request)
32
+
33
+
34
+ # ---------- Routes ----------
35
+ app.include_router(chat_router)
36
+ app.include_router(models_router)
37
+
38
+
39
+ @app.get("/")
40
+ async def root():
41
+ return {"status": "ok", "service": "assistant-ui 2API"}
42
+
43
+
44
+ @app.get("/health")
45
+ async def health():
46
+ return {"status": "ok"}
config.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ API_HOST = os.getenv("API_HOST", "0.0.0.0")
7
+ API_PORT = int(os.getenv("API_PORT", "8080"))
8
+ API_KEY = os.getenv("API_KEY", "sk-assistant-2api-free")
9
+
10
+ # /api/chat — clean endpoint, no hardcoded system prompt
11
+ # /api/doc/chat — docs assistant with hardcoded persona + tools (avoid)
12
+ UPSTREAM_URL = os.getenv(
13
+ "UPSTREAM_URL", "https://www.assistant-ui.com/api/chat"
14
+ )
15
+
16
+ # OpenAI-compatible name → assistant-ui API identifier
17
+ # Note: claude-sonnet-4.6 is disabled server-side (falls back to gpt-5.4)
18
+ MODEL_MAP: dict[str, str] = {
19
+ "gpt-5.4": "openai/gpt-5.4",
20
+ "gpt-5-nano": "openai/gpt-5-nano",
21
+ "gemini-3-flash": "google/gemini-3-flash",
22
+ "kimi-k2.5": "moonshotai/kimi-k2.5",
23
+ "deepseek-v3.2": "deepseek/deepseek-v3.2",
24
+ }
25
+
26
+ DEFAULT_MODEL = "gpt-5.4"
27
+
28
+ UPSTREAM_HEADERS: dict[str, str] = {
29
+ "content-type": "application/json",
30
+ "user-agent": "ai-sdk/6.0.116 runtime/browser",
31
+ "origin": "https://www.assistant-ui.com",
32
+ "referer": "https://www.assistant-ui.com/docs",
33
+ }
docker-compose.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ assistant-2api:
3
+ image: zhigengniao/assistant-2api:latest
4
+ ports:
5
+ - "${API_PORT:-8080}:8080"
6
+ env_file:
7
+ - .env
8
+ restart: unless-stopped
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import uvicorn
2
+
3
+ from config import API_HOST, API_PORT
4
+
5
+ if __name__ == "__main__":
6
+ uvicorn.run("api.server:app", host=API_HOST, port=API_PORT)
pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "assistant"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ "fastapi>=0.135.1",
8
+ "httpx>=0.28.1",
9
+ "nanoid>=2.0.0",
10
+ "openai>=2.26.0",
11
+ "pydantic>=2.12.5",
12
+ "python-dotenv>=1.2.2",
13
+ "uvicorn>=0.41.0",
14
+ ]
uv.lock ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "annotated-doc"
7
+ version = "0.0.4"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "annotated-types"
16
+ version = "0.7.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "anyio"
25
+ version = "4.12.1"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ dependencies = [
28
+ { name = "idna" },
29
+ ]
30
+ sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "assistant"
37
+ version = "0.1.0"
38
+ source = { virtual = "." }
39
+ dependencies = [
40
+ { name = "fastapi" },
41
+ { name = "httpx" },
42
+ { name = "nanoid" },
43
+ { name = "openai" },
44
+ { name = "pydantic" },
45
+ { name = "python-dotenv" },
46
+ { name = "uvicorn" },
47
+ ]
48
+
49
+ [package.metadata]
50
+ requires-dist = [
51
+ { name = "fastapi", specifier = ">=0.135.1" },
52
+ { name = "httpx", specifier = ">=0.28.1" },
53
+ { name = "nanoid", specifier = ">=2.0.0" },
54
+ { name = "openai", specifier = ">=2.26.0" },
55
+ { name = "pydantic", specifier = ">=2.12.5" },
56
+ { name = "python-dotenv", specifier = ">=1.2.2" },
57
+ { name = "uvicorn", specifier = ">=0.41.0" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "certifi"
62
+ version = "2026.2.25"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "click"
71
+ version = "8.3.1"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ dependencies = [
74
+ { name = "colorama", marker = "sys_platform == 'win32'" },
75
+ ]
76
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "colorama"
83
+ version = "0.4.6"
84
+ source = { registry = "https://pypi.org/simple" }
85
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
86
+ wheels = [
87
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
88
+ ]
89
+
90
+ [[package]]
91
+ name = "distro"
92
+ version = "1.9.0"
93
+ source = { registry = "https://pypi.org/simple" }
94
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
97
+ ]
98
+
99
+ [[package]]
100
+ name = "fastapi"
101
+ version = "0.135.1"
102
+ source = { registry = "https://pypi.org/simple" }
103
+ dependencies = [
104
+ { name = "annotated-doc" },
105
+ { name = "pydantic" },
106
+ { name = "starlette" },
107
+ { name = "typing-extensions" },
108
+ { name = "typing-inspection" },
109
+ ]
110
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "h11"
117
+ version = "0.16.0"
118
+ source = { registry = "https://pypi.org/simple" }
119
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
120
+ wheels = [
121
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
122
+ ]
123
+
124
+ [[package]]
125
+ name = "httpcore"
126
+ version = "1.0.9"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ dependencies = [
129
+ { name = "certifi" },
130
+ { name = "h11" },
131
+ ]
132
+ sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
133
+ wheels = [
134
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
135
+ ]
136
+
137
+ [[package]]
138
+ name = "httpx"
139
+ version = "0.28.1"
140
+ source = { registry = "https://pypi.org/simple" }
141
+ dependencies = [
142
+ { name = "anyio" },
143
+ { name = "certifi" },
144
+ { name = "httpcore" },
145
+ { name = "idna" },
146
+ ]
147
+ sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
148
+ wheels = [
149
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
150
+ ]
151
+
152
+ [[package]]
153
+ name = "idna"
154
+ version = "3.11"
155
+ source = { registry = "https://pypi.org/simple" }
156
+ sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
157
+ wheels = [
158
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
159
+ ]
160
+
161
+ [[package]]
162
+ name = "jiter"
163
+ version = "0.13.0"
164
+ source = { registry = "https://pypi.org/simple" }
165
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" }
166
+ wheels = [
167
+ { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" },
168
+ { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" },
169
+ { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" },
170
+ { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" },
171
+ { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" },
172
+ { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" },
173
+ { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" },
174
+ { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" },
175
+ { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" },
176
+ { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" },
177
+ { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" },
178
+ { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" },
179
+ { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" },
180
+ { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" },
181
+ { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" },
182
+ { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" },
183
+ { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" },
184
+ { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" },
185
+ { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" },
186
+ { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" },
187
+ { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" },
188
+ { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" },
189
+ { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" },
190
+ { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" },
191
+ { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" },
192
+ { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" },
193
+ { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" },
194
+ { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" },
195
+ { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" },
196
+ { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" },
197
+ { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" },
198
+ { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" },
199
+ { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" },
200
+ { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" },
201
+ { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" },
202
+ { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" },
203
+ { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" },
204
+ { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" },
205
+ { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" },
206
+ { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" },
207
+ { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" },
208
+ { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" },
209
+ { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" },
210
+ ]
211
+
212
+ [[package]]
213
+ name = "nanoid"
214
+ version = "2.0.0"
215
+ source = { registry = "https://pypi.org/simple" }
216
+ sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/0250bf5935d88e214df469d35eccc0f6ff7e9db046fc8a9aeb4b2a192775/nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68", size = 3290, upload-time = "2018-11-20T14:45:51.578Z" }
217
+ wheels = [
218
+ { url = "https://files.pythonhosted.org/packages/2e/0d/8630f13998638dc01e187fadd2e5c6d42d127d08aeb4943d231664d6e539/nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb", size = 5844, upload-time = "2018-11-20T14:45:50.165Z" },
219
+ ]
220
+
221
+ [[package]]
222
+ name = "openai"
223
+ version = "2.26.0"
224
+ source = { registry = "https://pypi.org/simple" }
225
+ dependencies = [
226
+ { name = "anyio" },
227
+ { name = "distro" },
228
+ { name = "httpx" },
229
+ { name = "jiter" },
230
+ { name = "pydantic" },
231
+ { name = "sniffio" },
232
+ { name = "tqdm" },
233
+ { name = "typing-extensions" },
234
+ ]
235
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" }
236
+ wheels = [
237
+ { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" },
238
+ ]
239
+
240
+ [[package]]
241
+ name = "pydantic"
242
+ version = "2.12.5"
243
+ source = { registry = "https://pypi.org/simple" }
244
+ dependencies = [
245
+ { name = "annotated-types" },
246
+ { name = "pydantic-core" },
247
+ { name = "typing-extensions" },
248
+ { name = "typing-inspection" },
249
+ ]
250
+ sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
251
+ wheels = [
252
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
253
+ ]
254
+
255
+ [[package]]
256
+ name = "pydantic-core"
257
+ version = "2.41.5"
258
+ source = { registry = "https://pypi.org/simple" }
259
+ dependencies = [
260
+ { name = "typing-extensions" },
261
+ ]
262
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
263
+ wheels = [
264
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
265
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
266
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
267
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
268
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
269
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
270
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
271
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
272
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
273
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
274
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
275
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
276
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
277
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
278
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
279
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
280
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
281
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
282
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
283
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
284
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
285
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
286
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
287
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
288
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
289
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
290
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
291
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
292
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
293
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
294
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
295
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
296
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
297
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
298
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
299
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
300
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
301
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
302
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
303
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
304
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
305
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
306
+ ]
307
+
308
+ [[package]]
309
+ name = "python-dotenv"
310
+ version = "1.2.2"
311
+ source = { registry = "https://pypi.org/simple" }
312
+ sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
313
+ wheels = [
314
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
315
+ ]
316
+
317
+ [[package]]
318
+ name = "sniffio"
319
+ version = "1.3.1"
320
+ source = { registry = "https://pypi.org/simple" }
321
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
322
+ wheels = [
323
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
324
+ ]
325
+
326
+ [[package]]
327
+ name = "starlette"
328
+ version = "0.52.1"
329
+ source = { registry = "https://pypi.org/simple" }
330
+ dependencies = [
331
+ { name = "anyio" },
332
+ ]
333
+ sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
334
+ wheels = [
335
+ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
336
+ ]
337
+
338
+ [[package]]
339
+ name = "tqdm"
340
+ version = "4.67.3"
341
+ source = { registry = "https://pypi.org/simple" }
342
+ dependencies = [
343
+ { name = "colorama", marker = "sys_platform == 'win32'" },
344
+ ]
345
+ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
346
+ wheels = [
347
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
348
+ ]
349
+
350
+ [[package]]
351
+ name = "typing-extensions"
352
+ version = "4.15.0"
353
+ source = { registry = "https://pypi.org/simple" }
354
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
355
+ wheels = [
356
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
357
+ ]
358
+
359
+ [[package]]
360
+ name = "typing-inspection"
361
+ version = "0.4.2"
362
+ source = { registry = "https://pypi.org/simple" }
363
+ dependencies = [
364
+ { name = "typing-extensions" },
365
+ ]
366
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
367
+ wheels = [
368
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
369
+ ]
370
+
371
+ [[package]]
372
+ name = "uvicorn"
373
+ version = "0.41.0"
374
+ source = { registry = "https://pypi.org/simple" }
375
+ dependencies = [
376
+ { name = "click" },
377
+ { name = "h11" },
378
+ ]
379
+ sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
380
+ wheels = [
381
+ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
382
+ ]