From 5940c57845fd4e12a8680c282a167da888d7744c Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Wed, 4 Mar 2026 18:49:37 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E5=9B=9E=E5=A4=8D=E6=B6=88=E6=81=AF=20(reply=5Fto)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 打通 message_id 全链路:存储 → 暴露 → 发送。 - history.py: add_group/private_message 增加 message_id 可选参数 - handlers.py: 用户消息入历史时传入 trigger_message_id - sender.py: 调整为先发送再写历史,提取 bot message_id; 增加 reply_to 参数,在消息段开头插入 reply 段 - prompts.py: 历史消息 XML 标签增加 message_id 属性 - get_recent_messages/get_messages_by_time: XML 输出增加 message_id - send_message/send_private_message: 新增 reply_to 工具参数 Co-Authored-By: Claude Opus 4.6 --- src/Undefined/ai/prompts.py | 9 +- src/Undefined/handlers.py | 2 + .../messages/get_messages_by_time/handler.py | 7 +- .../messages/get_recent_messages/handler.py | 7 +- .../messages/send_message/config.json | 4 + .../toolsets/messages/send_message/handler.py | 16 +- .../messages/send_private_message/config.json | 4 + .../messages/send_private_message/handler.py | 10 +- src/Undefined/utils/history.py | 54 ++++--- src/Undefined/utils/sender.py | 137 +++++++++++++----- 10 files changed, 183 insertions(+), 67 deletions(-) diff --git a/src/Undefined/ai/prompts.py b/src/Undefined/ai/prompts.py index f5c76f2d..43901f25 100644 --- a/src/Undefined/ai/prompts.py +++ b/src/Undefined/ai/prompts.py @@ -548,6 +548,7 @@ async def _inject_recent_messages( text = msg.get("message", "") role = msg.get("role", "member") title = msg.get("title", "") + message_id = msg.get("message_id") safe_sender = escape_xml_attr(sender_name) safe_sender_id = escape_xml_attr(sender_id) @@ -558,13 +559,17 @@ async def _inject_recent_messages( safe_time = escape_xml_attr(timestamp) safe_text = escape_xml_text(str(text)) + msg_id_attr = "" + if message_id is not None: + msg_id_attr = f' message_id="{escape_xml_attr(str(message_id))}"' + if msg_type_val == "group": location = ( chat_name if chat_name.endswith("群") else f"{chat_name}群" ) safe_location = escape_xml_attr(location) xml_msg = ( - f'\n{safe_text}\n' ) @@ -572,7 +577,7 @@ async def _inject_recent_messages( location = "私聊" safe_location = escape_xml_attr(location) xml_msg = ( - f'\n{safe_text}\n' ) context_lines.append(xml_msg) diff --git a/src/Undefined/handlers.py b/src/Undefined/handlers.py index a9cc1ac1..46beae88 100644 --- a/src/Undefined/handlers.py +++ b/src/Undefined/handlers.py @@ -272,6 +272,7 @@ async def handle_message(self, event: dict[str, Any]) -> None: text_content=parsed_content, display_name=private_sender_nickname, user_name=user_name, + message_id=trigger_message_id, ) # 如果是 bot 自己的消息,只保存不触发回复,避免无限循环 @@ -391,6 +392,7 @@ async def handle_message(self, event: dict[str, Any]) -> None: group_name=group_name, role=sender_role, title=sender_title, + message_id=trigger_message_id, ) # 如果是 bot 自己的消息,只保存不触发回复,避免无限循环 diff --git a/src/Undefined/skills/toolsets/messages/get_messages_by_time/handler.py b/src/Undefined/skills/toolsets/messages/get_messages_by_time/handler.py index 9d98116c..05f4ba6c 100644 --- a/src/Undefined/skills/toolsets/messages/get_messages_by_time/handler.py +++ b/src/Undefined/skills/toolsets/messages/get_messages_by_time/handler.py @@ -177,6 +177,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: chat_name = msg.get("chat_name", "未知群聊") timestamp_str = msg.get("timestamp", "") text = msg.get("message", "") + message_id = msg.get("message_id") if msg_type_val == "group": # 确保群名以"群"结尾 @@ -184,8 +185,12 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: else: location = "私聊" + msg_id_attr = "" + if message_id is not None: + msg_id_attr = f' message_id="{message_id}"' + # 格式:XML 标准化 - formatted.append(f""" + formatted.append(f""" {text} """) diff --git a/src/Undefined/skills/toolsets/messages/get_recent_messages/handler.py b/src/Undefined/skills/toolsets/messages/get_recent_messages/handler.py index 6077e883..fda39efe 100644 --- a/src/Undefined/skills/toolsets/messages/get_recent_messages/handler.py +++ b/src/Undefined/skills/toolsets/messages/get_recent_messages/handler.py @@ -104,10 +104,15 @@ def _format_message_xml(msg: dict[str, Any]) -> str: chat_name = msg.get("chat_name", "未知群聊") timestamp = msg.get("timestamp", "") text = msg.get("message", "") + message_id = msg.get("message_id") location = _format_message_location(msg_type_val, chat_name) - return f""" + msg_id_attr = "" + if message_id is not None: + msg_id_attr = f' message_id="{message_id}"' + + return f""" {text} """ diff --git a/src/Undefined/skills/toolsets/messages/send_message/config.json b/src/Undefined/skills/toolsets/messages/send_message/config.json index 7817db5f..72346de9 100644 --- a/src/Undefined/skills/toolsets/messages/send_message/config.json +++ b/src/Undefined/skills/toolsets/messages/send_message/config.json @@ -18,6 +18,10 @@ "target_id": { "type": "integer", "description": "可选。目标会话 ID:当 target_type=group 时为群号;target_type=private 时为用户 QQ 号。" + }, + "reply_to": { + "type": "integer", + "description": "可选。要引用回复的消息 ID (message_id)。设置后消息将以引用回复形式出现,可从历史消息的 message_id 属性获取。" } }, "required": ["message"] diff --git a/src/Undefined/skills/toolsets/messages/send_message/handler.py b/src/Undefined/skills/toolsets/messages/send_message/handler.py index ebd2dd0a..83635d91 100644 --- a/src/Undefined/skills/toolsets/messages/send_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_message/handler.py @@ -173,6 +173,14 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: logger.warning("[发送消息] 收到空消息请求") return "消息内容不能为空" + # 解析 reply_to 参数 + reply_to_raw = args.get("reply_to") + reply_to_id: int | None = None + if reply_to_raw is not None: + reply_to_id, reply_error = _parse_positive_int(reply_to_raw, "reply_to") + if reply_error: + return f"发送失败:{reply_error}" + runtime_config = context.get("runtime_config") send_message_callback = context.get("send_message_callback") @@ -206,10 +214,14 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: try: if target_type == "group": logger.info("[发送消息] 准备发送到群 %s: %s", target_id, message[:100]) - await sender.send_group_message(target_id, message) + await sender.send_group_message( + target_id, message, reply_to=reply_to_id + ) else: logger.info("[发送消息] 准备发送私聊 %s: %s", target_id, message[:100]) - await sender.send_private_message(target_id, message) + await sender.send_private_message( + target_id, message, reply_to=reply_to_id + ) context["message_sent_this_turn"] = True return "消息已发送" except Exception as e: diff --git a/src/Undefined/skills/toolsets/messages/send_private_message/config.json b/src/Undefined/skills/toolsets/messages/send_private_message/config.json index 1ddba7d6..28026131 100644 --- a/src/Undefined/skills/toolsets/messages/send_private_message/config.json +++ b/src/Undefined/skills/toolsets/messages/send_private_message/config.json @@ -13,6 +13,10 @@ "message": { "type": "string", "description": "要发送的私聊消息内容" + }, + "reply_to": { + "type": "integer", + "description": "可选。要引用回复的消息 ID (message_id)。设置后消息将以引用回复形式出现,可从历史消息的 message_id 属性获取。" } }, "required": ["message"] diff --git a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py index 84ed8589..5e6f2378 100644 --- a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py @@ -40,6 +40,14 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: user_id, user_error = _parse_positive_int(user_id_raw, "user_id") message = str(args.get("message", "")) + # 解析 reply_to 参数 + reply_to_raw = args.get("reply_to") + reply_to_id: int | None = None + if reply_to_raw is not None: + reply_to_id, reply_error = _parse_positive_int(reply_to_raw, "reply_to") + if reply_error: + return f"发送失败:{reply_error}" + if user_error: return f"发送失败:{user_error}" if user_id is None: @@ -57,7 +65,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if sender: try: - await sender.send_private_message(user_id, message) + await sender.send_private_message(user_id, message, reply_to=reply_to_id) context["message_sent_this_turn"] = True return f"私聊消息已发送给用户 {user_id}" except Exception as e: diff --git a/src/Undefined/utils/history.py b/src/Undefined/utils/history.py index 15496392..4f79a107 100644 --- a/src/Undefined/utils/history.py +++ b/src/Undefined/utils/history.py @@ -267,6 +267,7 @@ async def add_group_message( group_name: str = "", role: str = "member", title: str = "", + message_id: int | None = None, ) -> None: """异步保存群消息到历史记录""" await self._ensure_initialized() @@ -285,19 +286,21 @@ async def add_group_message( f"[历史记录] 追加群消息: group={group_id}, current_count={current_count}" ) - self._message_history[group_id_str].append( - { - "type": "group", - "chat_id": group_id_str, - "chat_name": group_name or f"群{group_id_str}", - "user_id": sender_id_str, - "display_name": display_name, - "role": role, - "title": title, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "message": text_content, - } - ) + record: dict[str, Any] = { + "type": "group", + "chat_id": group_id_str, + "chat_name": group_name or f"群{group_id_str}", + "user_id": sender_id_str, + "display_name": display_name, + "role": role, + "title": title, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "message": text_content, + } + if message_id is not None: + record["message_id"] = message_id + + self._message_history[group_id_str].append(record) if len(self._message_history[group_id_str]) > self._max_records: self._message_history[group_id_str] = self._message_history[ @@ -315,6 +318,7 @@ async def add_private_message( text_content: str, display_name: str = "", user_name: str = "", + message_id: int | None = None, ) -> None: """异步保存私聊消息到历史记录""" await self._ensure_initialized() @@ -330,17 +334,19 @@ async def add_private_message( f"[历史记录] 追加私聊消息: user={user_id}, current_count={current_count}" ) - self._private_message_history[user_id_str].append( - { - "type": "private", - "chat_id": user_id_str, - "chat_name": user_name or f"QQ用户{user_id_str}", - "user_id": user_id_str, - "display_name": display_name or user_name or user_id_str, - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "message": text_content, - } - ) + record: dict[str, Any] = { + "type": "private", + "chat_id": user_id_str, + "chat_name": user_name or f"QQ用户{user_id_str}", + "user_id": user_id_str, + "display_name": display_name or user_name or user_id_str, + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "message": text_content, + } + if message_id is not None: + record["message_id"] = message_id + + self._private_message_history[user_id_str].append(record) if len(self._private_message_history[user_id_str]) > self._max_records: self._private_message_history[user_id_str] = ( diff --git a/src/Undefined/utils/sender.py b/src/Undefined/utils/sender.py index 7a8921b2..cd92bb56 100644 --- a/src/Undefined/utils/sender.py +++ b/src/Undefined/utils/sender.py @@ -65,6 +65,7 @@ async def send_group_message( history_prefix: str = "", *, mark_sent: bool = True, + reply_to: int | None = None, ) -> None: """发送群消息""" if not self.config.is_group_allowed(group_id): @@ -87,37 +88,57 @@ async def send_group_message( # 将 [@{qq_id}] 格式转换为 [CQ:at,qq={qq_id}] message = process_at_mentions(message) - # 保存到历史记录 + # 准备历史记录文本(不含 reply 段) + history_content: str | None = None if auto_history: - # 解析消息以便正确处理 CQ 码(如图片) - segments = message_to_segments(message) - history_content = extract_text(segments, self.bot_qq) + hist_segments = message_to_segments(message) + history_content = extract_text(hist_segments, self.bot_qq) if history_prefix: history_content = f"{history_prefix}{history_content}" - logger.debug(f"[历史记录] 正在保存 Bot 群聊回复: group={group_id}") + # 发送消息 + bot_message_id: int | None = None + if len(message) <= MAX_MESSAGE_LENGTH: + segments = message_to_segments(message) + if reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_group_message( + group_id, segments, mark_sent=mark_sent + ) + if isinstance(result, dict): + bot_message_id = result.get("message_id") + else: + bot_message_id = await self._send_chunked_group( + group_id, message, mark_sent=mark_sent, reply_to=reply_to + ) + + # 发送成功后写入历史记录 + if auto_history and history_content is not None: + logger.debug(f"[历史记录] 正在保存 Bot 群聊回复: group={group_id}") await self.history_manager.add_group_message( group_id=group_id, sender_id=self.bot_qq, text_content=history_content, sender_nickname="Bot", - group_name="", # 群名暂时未知,通常不需要Bot去获取 - ) - - # 自动分段发送 - if len(message) <= MAX_MESSAGE_LENGTH: - segments = message_to_segments(message) - await self.onebot.send_group_message( - group_id, segments, mark_sent=mark_sent + group_name="", + message_id=bot_message_id, ) - return - # 按行分割 + async def _send_chunked_group( + self, + group_id: int, + message: str, + *, + mark_sent: bool = True, + reply_to: int | None = None, + ) -> int | None: + """分段发送群消息,返回第一段的 message_id。""" logger.info(f"[消息分段] 消息过长 ({len(message)} 字符),正在自动分段发送...") lines = message.split("\n") current_chunk: list[str] = [] current_length = 0 chunk_count = 0 + first_message_id: int | None = None for line in lines: line_length = len(line) + 1 @@ -126,9 +147,14 @@ async def send_group_message( chunk_count += 1 chunk_text = "\n".join(current_chunk) logger.debug(f"[消息分段] 发送第 {chunk_count} 段") - await self.onebot.send_group_message( - group_id, message_to_segments(chunk_text), mark_sent=mark_sent + segments = message_to_segments(chunk_text) + if chunk_count == 1 and reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_group_message( + group_id, segments, mark_sent=mark_sent ) + if chunk_count == 1 and isinstance(result, dict): + first_message_id = result.get("message_id") current_chunk = [] current_length = 0 @@ -139,11 +165,17 @@ async def send_group_message( chunk_count += 1 chunk_text = "\n".join(current_chunk) logger.debug(f"[消息分段] 发送第 {chunk_count} 段 (最后一段)") - await self.onebot.send_group_message( - group_id, message_to_segments(chunk_text), mark_sent=mark_sent + segments = message_to_segments(chunk_text) + if chunk_count == 1 and reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_group_message( + group_id, segments, mark_sent=mark_sent ) + if chunk_count == 1 and isinstance(result, dict): + first_message_id = result.get("message_id") logger.info(f"[消息分段] 已完成 {chunk_count} 段消息的发送") + return first_message_id async def send_private_message( self, @@ -152,6 +184,7 @@ async def send_private_message( auto_history: bool = True, *, mark_sent: bool = True, + reply_to: int | None = None, ) -> None: """发送私聊消息""" if not self.config.is_private_allowed(user_id): @@ -170,34 +203,55 @@ async def send_private_message( safe_message = redact_string(message) logger.info(f"[发送消息] 目标用户:{user_id} | 内容摘要:{safe_message[:100]}...") - # 保存到历史记录 + + # 准备历史记录文本 + history_content: str | None = None if auto_history: - # 解析消息以便正确处理 CQ 码 + hist_segments = message_to_segments(message) + history_content = extract_text(hist_segments, self.bot_qq) + + # 发送消息 + bot_message_id: int | None = None + if len(message) <= MAX_MESSAGE_LENGTH: segments = message_to_segments(message) - history_content = extract_text(segments, self.bot_qq) - logger.debug(f"[历史记录] 正在保存 Bot 私聊回复: user={user_id}") + if reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_private_message( + user_id, segments, mark_sent=mark_sent + ) + if isinstance(result, dict): + bot_message_id = result.get("message_id") + else: + bot_message_id = await self._send_chunked_private( + user_id, message, mark_sent=mark_sent, reply_to=reply_to + ) + # 发送成功后写入历史记录 + if auto_history and history_content is not None: + logger.debug(f"[历史记录] 正在保存 Bot 私聊回复: user={user_id}") await self.history_manager.add_private_message( user_id=user_id, text_content=history_content, display_name="Bot", user_name="Bot", + message_id=bot_message_id, ) - # 自动分段发送 - if len(message) <= MAX_MESSAGE_LENGTH: - segments = message_to_segments(message) - await self.onebot.send_private_message( - user_id, segments, mark_sent=mark_sent - ) - return - - # 按行分割 + async def _send_chunked_private( + self, + user_id: int, + message: str, + *, + mark_sent: bool = True, + reply_to: int | None = None, + ) -> int | None: + """分段发送私聊消息,返回第一段的 message_id。""" logger.info(f"[消息分段] 消息过长 ({len(message)} 字符),正在自动分段发送...") lines = message.split("\n") current_chunk: list[str] = [] current_length = 0 chunk_count = 0 + first_message_id: int | None = None for line in lines: line_length = len(line) + 1 @@ -206,9 +260,14 @@ async def send_private_message( chunk_count += 1 chunk_text = "\n".join(current_chunk) logger.debug(f"[消息分段] 发送第 {chunk_count} 段") - await self.onebot.send_private_message( - user_id, message_to_segments(chunk_text), mark_sent=mark_sent + segments = message_to_segments(chunk_text) + if chunk_count == 1 and reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_private_message( + user_id, segments, mark_sent=mark_sent ) + if chunk_count == 1 and isinstance(result, dict): + first_message_id = result.get("message_id") current_chunk = [] current_length = 0 @@ -219,11 +278,17 @@ async def send_private_message( chunk_count += 1 chunk_text = "\n".join(current_chunk) logger.debug(f"[消息分段] 发送第 {chunk_count} 段 (最后一段)") - await self.onebot.send_private_message( - user_id, message_to_segments(chunk_text), mark_sent=mark_sent + segments = message_to_segments(chunk_text) + if chunk_count == 1 and reply_to is not None: + segments.insert(0, {"type": "reply", "data": {"id": str(reply_to)}}) + result = await self.onebot.send_private_message( + user_id, segments, mark_sent=mark_sent ) + if chunk_count == 1 and isinstance(result, dict): + first_message_id = result.get("message_id") logger.info(f"[消息分段] 已完成 {chunk_count} 段消息的发送") + return first_message_id async def send_group_poke( self, From 59a978a57f4f62017b9e8dae77a2e76882b88dc7 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Wed, 4 Mar 2026 19:50:51 +0800 Subject: [PATCH 02/21] =?UTF-8?q?feat(python=5Finterpreter):=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=AE=89=E8=A3=85=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=93?= =?UTF-8?q?=E5=92=8C=E5=8F=91=E9=80=81=E8=BE=93=E5=87=BA=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 libraries 参数:可选指定 pip 安装的第三方库列表,启用网络下载 - 新增 send_files 参数:代码执行成功后自动发送生成的文件给用户 - 图片文件通过 CQ 码内联发送,其他文件通过上传接口发送为附件 - 代码写入脚本文件执行,避免 shell 引号转义问题 - 库名正则校验防止 requirements.txt 注入(-r/-e/--index-url) - 有库时内存上限 512m、超时 10 分钟;无库时保持原有安全限制 Co-Authored-By: Claude Opus 4.6 --- .../tools/python_interpreter/config.json | 20 +- .../tools/python_interpreter/handler.py | 236 ++++++++++++++---- 2 files changed, 204 insertions(+), 52 deletions(-) diff --git a/src/Undefined/skills/tools/python_interpreter/config.json b/src/Undefined/skills/tools/python_interpreter/config.json index 1685b0e2..4431491b 100644 --- a/src/Undefined/skills/tools/python_interpreter/config.json +++ b/src/Undefined/skills/tools/python_interpreter/config.json @@ -2,13 +2,27 @@ "type": "function", "function": { "name": "python_interpreter", - "description": "在隔离的 Docker 容器中执行 Python 代码。适用于数学计算、数据处理和逻辑验证。无法访问网络或宿主机文件系统。", + "description": "在隔离的 Docker 容器中执行 Python 代码。适用于数学计算、数据处理、图表绘制等任务。可选安装第三方库,并可在执行后自动发送生成的文件(如图片、文档等)。输出文件请写入 /tmp/ 目录。", "parameters": { "type": "object", "properties": { "code": { "type": "string", - "description": "要执行的 Python 代码。" + "description": "要执行的 Python 代码。如需生成文件,请写入 /tmp/ 目录(如 /tmp/output.png)。" + }, + "libraries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "可选。需要通过 pip 安装的第三方库列表,如 [\"matplotlib\", \"pillow\"]。指定后容器将启用网络以下载安装。" + }, + "send_files": { + "type": "array", + "items": { + "type": "string" + }, + "description": "可选。代码执行后要发送给用户的文件路径列表(容器内 /tmp/ 下的路径),如 [\"/tmp/chart.png\", \"/tmp/result.csv\"]。图片文件将内联发送,其他文件作为附件上传。仅在代码执行成功时发送。" } }, "required": [ @@ -16,4 +30,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/Undefined/skills/tools/python_interpreter/handler.py b/src/Undefined/skills/tools/python_interpreter/handler.py index 223f9e56..43bc212f 100644 --- a/src/Undefined/skills/tools/python_interpreter/handler.py +++ b/src/Undefined/skills/tools/python_interpreter/handler.py @@ -1,5 +1,10 @@ import asyncio import logging +import os +import re +import shutil +import tempfile +from pathlib import Path from typing import Any, Dict logger = logging.getLogger(__name__) @@ -7,51 +12,94 @@ # Docker 执行配置 DOCKER_IMAGE = "python:3.11-slim" MEMORY_LIMIT = "128m" +MEMORY_LIMIT_WITH_LIBS = "512m" CPU_LIMIT = "0.5" TIMEOUT = 480 # 8 分钟 +TIMEOUT_WITH_LIBS = 600 # 10 分钟(pip 安装需要更多时间) + +# 图片扩展名(内联发送而非文件附件) +_IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}) + +# 安全:库名仅允许 PyPI 合法字符,必须以字母/数字开头(防止 -r/-e/--index-url 注入) +_SAFE_LIB_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._\-\[\],<>=!~]*$") async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: - """ - 在 Docker 容器中执行 Python 代码。 - """ + """在 Docker 容器中执行 Python 代码,可选安装库和发送输出文件。""" code = args.get("code", "") if not code: return "错误: 未提供代码。" - # 构建 docker run 命令 - # --rm: 执行后自动删除容器 - # --network none: 禁用网络 - # --memory/--cpus: 资源限制 - # --read-only: 只读文件系统 - # --tmpfs /tmp: 允许在 /tmp 写入 - # -u 1000:1000: 以非 root 用户运行 (在 slim 镜像中通常存在 'python' 用户,或者用 UID) - - cmd = [ - "docker", - "run", - "--rm", - "--network", - "none", - "--memory", - MEMORY_LIMIT, - "--cpus", - CPU_LIMIT, - "--read-only", - "--tmpfs", - "/tmp", - "-i", # 交互模式,用于传入代码 - DOCKER_IMAGE, - "python", - "-c", - code, - ] - - logger.info(f"[Python解释器] 开始执行代码,超时限制: {TIMEOUT}s") - logger.debug(f"[Python解释器] 代码内容:\n{code}") + libraries: list[str] = args.get("libraries") or [] + send_files: list[str] = args.get("send_files") or [] + + # 验证库名 + for lib in libraries: + if not _SAFE_LIB_PATTERN.match(lib): + return f"错误: 无效的库名 '{lib}'。" + + # 验证文件路径必须在 /tmp/ 下 + for fpath in send_files: + if not fpath.startswith("/tmp/"): + return f"错误: 输出文件路径必须在 /tmp/ 目录下: '{fpath}'" + + has_libs = bool(libraries) + memory = MEMORY_LIMIT_WITH_LIBS if has_libs else MEMORY_LIMIT + timeout = TIMEOUT_WITH_LIBS if has_libs else TIMEOUT + + # 创建宿主机临时目录,绑定挂载到容器 /tmp + host_tmpdir = tempfile.mkdtemp(prefix="pyinterp_") try: - # 使用 asyncio 执行子进程 + # 将代码写入脚本文件(避免 shell 引号转义问题) + script_path = os.path.join(host_tmpdir, "_script.py") + with open(script_path, "w", encoding="utf-8") as f: + f.write(code) + + # 构建 docker 命令 + cmd: list[str] = [ + "docker", + "run", + "--rm", + "--memory", + memory, + "--cpus", + CPU_LIMIT, + "-v", + f"{host_tmpdir}:/tmp", + ] + + if has_libs: + # 需要网络下载包、需要写权限安装包 + req_path = os.path.join(host_tmpdir, "_requirements.txt") + with open(req_path, "w", encoding="utf-8") as f: + for lib in libraries: + f.write(lib + "\n") + cmd.append(DOCKER_IMAGE) + # pip install → 运行代码 → 修正文件权限以便宿主机清理 + cmd.extend( + [ + "sh", + "-c", + "pip install --quiet -r /tmp/_requirements.txt " + "&& python /tmp/_script.py; " + "_e=$?; chmod -R a+rw /tmp 2>/dev/null; exit $_e", + ] + ) + else: + # 无需网络、只读文件系统 + cmd.extend(["--network", "none", "--read-only"]) + cmd.append(DOCKER_IMAGE) + cmd.extend(["python", "/tmp/_script.py"]) + + logger.info( + "[Python解释器] 开始执行, 超时: %ss, 库: %s, 输出文件: %s", + timeout, + libraries, + send_files, + ) + logger.debug("[Python解释器] 代码内容:\n%s", code) + process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, @@ -59,35 +107,125 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: ) try: - # 等待完成,带超时 - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=TIMEOUT + stdout_bytes, stderr_bytes = await asyncio.wait_for( + process.communicate(), timeout=timeout ) exit_code = process.returncode - response = stdout.decode("utf-8", errors="replace") - error_output = stderr.decode("utf-8", errors="replace") + response = stdout_bytes.decode("utf-8", errors="replace") + error_output = stderr_bytes.decode("utf-8", errors="replace") + # 构建结果 + parts: list[str] = [] if exit_code == 0: - return response if response.strip() else "代码执行成功 (无输出)。" + parts.append( + response if response.strip() else "代码执行成功 (无输出)。" + ) else: - return ( + parts.append( f"代码执行失败 (退出代码: {exit_code}):\n{error_output}\n{response}" ) + # 执行成功时发送文件 + if send_files and exit_code == 0: + file_result = await _send_output_files(send_files, host_tmpdir, context) + if file_result: + parts.append(file_result) + + return "\n".join(parts) + except asyncio.TimeoutError: - # 超时处理 try: - # 尝试杀掉容器 (由于用了 --rm,杀掉容器会清理资源) - # 注意:子进程是 docker 客户端,单纯 kill 客户端可能不会立刻停止远程容器 - # 但在这里 docker run -i 配合 communicate(),终止子进程通常足够 process.terminate() await process.wait() except Exception as e: - logger.error(f"[Python解释器] 终止超时进程失败: {e}") - - return f"错误: 代码执行超时 ({TIMEOUT}s)。" + logger.error("[Python解释器] 终止超时进程失败: %s", e) + return f"错误: 代码执行超时 ({timeout}s)。" except Exception as e: - logger.exception(f"[Python解释器] 执行出错: {e}") + logger.exception("[Python解释器] 执行出错: %s", e) return "执行出错,请检查代码或重试" + finally: + shutil.rmtree(host_tmpdir, ignore_errors=True) + + +async def _send_output_files( + send_files: list[str], + host_tmpdir: str, + context: Dict[str, Any], +) -> str: + """发送输出文件给用户,返回状态摘要。""" + sender = context.get("sender") + if sender is None: + return "文件发送失败:发送通道不可用" + + request_type = context.get("request_type") + group_id = context.get("group_id") + user_id = context.get("user_id") + + results: list[str] = [] + for container_path in send_files: + # /tmp/output.png → {host_tmpdir}/output.png + relative = container_path.removeprefix("/tmp/") + host_path = os.path.join(host_tmpdir, relative) + + if not os.path.isfile(host_path): + results.append(f"文件未找到: {container_path}") + continue + + file_name = os.path.basename(host_path) + ext = Path(host_path).suffix.lower() + + try: + if ext in _IMAGE_EXTENSIONS: + # 图片:通过 CQ 码内联发送 + await _send_image_inline( + sender, request_type, group_id, user_id, host_path + ) + else: + # 其他文件:通过文件上传接口发送 + await _send_file_upload( + sender, request_type, group_id, user_id, host_path, file_name + ) + results.append(f"已发送: {file_name}") + except Exception as e: + logger.exception("[Python解释器] 文件发送失败: %s", container_path) + results.append(f"发送失败: {file_name} ({e})") + + return "\n".join(results) if results else "" + + +async def _send_image_inline( + sender: Any, + request_type: str | None, + group_id: Any, + user_id: Any, + host_path: str, +) -> None: + """通过 CQ:image 内联发送图片。""" + abs_path = Path(host_path).resolve() + image_cq = f"[CQ:image,file=file://{abs_path}]" + + if request_type == "group" and group_id: + await sender.send_group_message(int(group_id), image_cq, auto_history=False) + elif user_id: + await sender.send_private_message(int(user_id), image_cq, auto_history=False) + else: + raise RuntimeError("无法确定发送目标") + + +async def _send_file_upload( + sender: Any, + request_type: str | None, + group_id: Any, + user_id: Any, + host_path: str, + file_name: str, +) -> None: + """通过文件上传接口发送文件。""" + if request_type == "group" and group_id: + await sender.send_group_file(int(group_id), host_path, file_name) + elif user_id: + await sender.send_private_file(int(user_id), host_path, file_name) + else: + raise RuntimeError("无法确定发送目标") From 32b871da01b6807466015dc9f461066435b645a2 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Wed, 4 Mar 2026 20:29:03 +0800 Subject: [PATCH 03/21] =?UTF-8?q?fix:=20reply=5Fto=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=E6=97=B6=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=E8=80=8C=E9=9D=9E=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../skills/toolsets/messages/send_message/handler.py | 9 ++------- .../toolsets/messages/send_private_message/handler.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Undefined/skills/toolsets/messages/send_message/handler.py b/src/Undefined/skills/toolsets/messages/send_message/handler.py index 83635d91..1efb5fcc 100644 --- a/src/Undefined/skills/toolsets/messages/send_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_message/handler.py @@ -173,13 +173,8 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: logger.warning("[发送消息] 收到空消息请求") return "消息内容不能为空" - # 解析 reply_to 参数 - reply_to_raw = args.get("reply_to") - reply_to_id: int | None = None - if reply_to_raw is not None: - reply_to_id, reply_error = _parse_positive_int(reply_to_raw, "reply_to") - if reply_error: - return f"发送失败:{reply_error}" + # 解析 reply_to 参数(无效值静默忽略,视为未传) + reply_to_id, _ = _parse_positive_int(args.get("reply_to"), "reply_to") runtime_config = context.get("runtime_config") diff --git a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py index 5e6f2378..6ac26cf0 100644 --- a/src/Undefined/skills/toolsets/messages/send_private_message/handler.py +++ b/src/Undefined/skills/toolsets/messages/send_private_message/handler.py @@ -40,13 +40,8 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: user_id, user_error = _parse_positive_int(user_id_raw, "user_id") message = str(args.get("message", "")) - # 解析 reply_to 参数 - reply_to_raw = args.get("reply_to") - reply_to_id: int | None = None - if reply_to_raw is not None: - reply_to_id, reply_error = _parse_positive_int(reply_to_raw, "reply_to") - if reply_error: - return f"发送失败:{reply_error}" + # 解析 reply_to 参数(无效值静默忽略,视为未传) + reply_to_id, _ = _parse_positive_int(args.get("reply_to"), "reply_to") if user_error: return f"发送失败:{user_error}" From 497633b4add9a1a14576a05deb23aa02a719db85 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Wed, 4 Mar 2026 21:25:23 +0800 Subject: [PATCH 04/21] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E4=BE=A7?= =?UTF-8?q?=E5=86=99=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/prompts/historian_profile_merge.md | 36 ++++++++++++++++++----- src/Undefined/skills/tools/end/handler.py | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/res/prompts/historian_profile_merge.md b/res/prompts/historian_profile_merge.md index 71300314..11216200 100644 --- a/res/prompts/historian_profile_merge.md +++ b/res/prompts/historian_profile_merge.md @@ -1,4 +1,4 @@ -你是一个侧写维护员。根据新事件更新指定目标实体的侧写。 +你是一个人物侧写师。你的工作是维护每个实体的**立体画像**——读完侧写,就能在脑海中浮现这个人/群的轮廓。 必须遵守的硬约束: 1. 本次只允许更新目标实体:`{target_entity_type}:{target_entity_id}`。 @@ -48,8 +48,8 @@ 2. 保留现有稳定特征,整合新信息;参考历史事件判断现有特征是否仍然成立,避免因本轮未提及而误删长期稳定特征 3. 矛盾时以新信息为准 4. tags 只写"这个实体**是什么**"(身份/角色/核心领域),不写"聊过什么话题";最多 10 个,话题级细节已在 summary 中覆盖。若现有 tags 不符合此规范(数量过多或含话题标签),直接按规范重写,不必保留旧 tags -5. 保持简洁,只记录长期稳定的特征 -6. 侧写是"稳定画像",不是"事件流水账" +5. 侧写是"人物素描",不是"属性清单"或"事件流水账"——要有主线、有层次,能合并的特征就融成一句话 +6. 只保留长期稳定的特征,表达紧凑但不丢失关键信息 7. 若 `current_profile` 本身不符合以上规范,可直接整体重写为合规版本(不必保留其原有写法) 严禁写入以下内容(这些属于事件记忆,不应进入侧写): @@ -60,10 +60,32 @@ 若本轮只有事件细节、无法抽象为长期稳定特征,必须 `skip=true`。 -`summary` 输出格式约束: -- 使用 Markdown 项目符号(`- `)输出 4-8 条 -- 每条只写"长期稳定特征/偏好/角色关系/沟通风格" -- 句子短而概括,避免冗长复述 +`summary` 输出格式约束(核心原则:画一个人/群,不是列属性卡): + +**用户侧写**结构: +1. **核心画像**(第一句):一句话抓住这个人最本质的特质(身份 + 核心驱动力/风格),读完就能大致"看到"这个人 +2. **展开描述**(后续 1-3 句群):围绕 2-3 个维度用连贯的短句展开,维度间要有逻辑关联而非孤立罗列 + - 维度参考:技术/专业方向与能力、做事风格与决策偏好、沟通方式与性格特征 +3. 总字数 100-300 字;关键信息不可丢失,但表达要紧凑——能合并的特征融成一句,不要拆成多条孤立要点 + +**群聊侧写**结构: +1. **核心定位**(第一句):一句话概括群的本质定位与氛围基调 +2. **展开描述**(后续):成员构成、讨论风格、群文化等,可用项目符号辅助,但每条要有信息密度 +3. 总字数 80-250 字 + +**反面示例**(不要这样写——碎片化,读完记不住是谁): +``` +- 在校学生兼独立开发者 +- AI/Agent 工程实践者,偏工程化落地 +- 沟通中喜欢追问到具体机制 +- 对表述与参数准确性敏感 +- 关注稳定性与成本 +``` + +**正面示例**(应该这样写——精炼连贯,读完脑中浮现一个人): +``` +务实的工程型开发者,在校学生身份下独立运营开源项目。核心驱动力是"让东西真正能用"——偏好可配置、可维护的方案,习惯小实验快速验证,不可行就止损。沟通直奔具体机制与数据结构,对表述准确性敏感,发现偏差会刨根问底。 +``` 输出规则(调用 `update_profile` 工具): - 若应跳过更新:`skip=true`,并给出 `skip_reason`;`summary` 置空字符串,`tags` 可为空数组。**调用后流程立即终止,无法再执行任何操作,请确保在此之前已完成所有必要的读取与更新。** diff --git a/src/Undefined/skills/tools/end/handler.py b/src/Undefined/skills/tools/end/handler.py index d48e5a02..d122ae25 100644 --- a/src/Undefined/skills/tools/end/handler.py +++ b/src/Undefined/skills/tools/end/handler.py @@ -280,7 +280,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: "拒绝结束对话:你填写了 memo(本轮行动备忘)但本轮未发送任何消息或媒体内容。" "请先发送消息给用户,或使用 force=true 强制结束。" "若本轮确实未做任何事,建议留空 memo 以避免记忆噪声。当然,你要存也没关系。这只是个提示,防止你忘了。" - "若你获取到了新信息,考虑填写 observations 字段以保存这些信息,而不是放在 memo 里。" + "若你获取到了新信息,应填写 observations 字段以保存这些信息,而不是放在 memo 里。" ) if summary: From e4176182791bab4de09ae78f7901505173582bf6 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Fri, 6 Mar 2026 19:33:24 +0800 Subject: [PATCH 05/21] =?UTF-8?q?fix:=20=E8=B0=83=E6=95=B4=E5=8F=B2?= =?UTF-8?q?=E5=AE=98=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- res/prompts/historian_profile_merge.md | 68 +++++++++++++++++--------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/res/prompts/historian_profile_merge.md b/res/prompts/historian_profile_merge.md index 11216200..c0716f3f 100644 --- a/res/prompts/historian_profile_merge.md +++ b/res/prompts/historian_profile_merge.md @@ -45,12 +45,17 @@ 要求: 1. **先调用 `read_profile` 读取目标实体的当前侧写**,再决定如何更新 -2. 保留现有稳定特征,整合新信息;参考历史事件判断现有特征是否仍然成立,避免因本轮未提及而误删长期稳定特征 -3. 矛盾时以新信息为准 +2. **增量更新原则**(核心): + - 现有侧写是长期积累的全貌,本轮新事件只是一个增量片段 + - 默认保留所有现有稳定特征,新信息用于"补充细节"或"修正矛盾",而非"重新定义" + - 参考历史事件列表判断旧特征是否仍然成立——只要历史中反复出现,就应保留 + - 只有当新信息与旧特征**明确矛盾**时才覆盖,否则应融合表达(如"既...也...") +3. 若新旧信息矛盾且新信息更可靠,以新信息为准并说明变化 4. tags 只写"这个实体**是什么**"(身份/角色/核心领域),不写"聊过什么话题";最多 10 个,话题级细节已在 summary 中覆盖。若现有 tags 不符合此规范(数量过多或含话题标签),直接按规范重写,不必保留旧 tags -5. 侧写是"人物素描",不是"属性清单"或"事件流水账"——要有主线、有层次,能合并的特征就融成一句话 -6. 只保留长期稳定的特征,表达紧凑但不丢失关键信息 -7. 若 `current_profile` 本身不符合以上规范,可直接整体重写为合规版本(不必保留其原有写法) +5. 侧写要有"主线"——第一条定调,后续条目围绕它展开,而非孤立罗列无关特征 +6. **信息密度优先于表达精炼**:每条要有具体细节(技术栈/工具/行为模式),不要为了简洁而泛化成抽象描述 +7. **核心画像要抓独特性**:第一句要写出"这个人区别于其他人的本质",而非通用描述(如"开发者"太泛,"把系统当产线打理的工程型开发者"才有辨识度) +8. 若 `current_profile` 本身不符合以上规范,可直接整体重写为合规版本(不必保留其原有写法) 严禁写入以下内容(这些属于事件记忆,不应进入侧写): - 某次/近期/今天/昨天的具体经过 @@ -60,31 +65,50 @@ 若本轮只有事件细节、无法抽象为长期稳定特征,必须 `skip=true`。 -`summary` 输出格式约束(核心原则:画一个人/群,不是列属性卡): +`summary` 输出格式约束: -**用户侧写**结构: -1. **核心画像**(第一句):一句话抓住这个人最本质的特质(身份 + 核心驱动力/风格),读完就能大致"看到"这个人 -2. **展开描述**(后续 1-3 句群):围绕 2-3 个维度用连贯的短句展开,维度间要有逻辑关联而非孤立罗列 - - 维度参考:技术/专业方向与能力、做事风格与决策偏好、沟通方式与性格特征 -3. 总字数 100-300 字;关键信息不可丢失,但表达要紧凑——能合并的特征融成一句,不要拆成多条孤立要点 +**用户侧写**结构(使用 Markdown 项目符号 `- `): +1. **第一条:核心画像** + - 一句话定位这个人的身份与核心特质,为后续条目定调 + - 要具体,不要泛泛而谈(如"在校学生/业余开发者,做技术取舍会权衡时间、算力与预算"比"务实的开发者"信息量大) + +2. **后续 4-8 条:多维度展开** + - 每条聚焦一个维度:技术栈/工作方式/决策偏好/沟通风格/排障思路等 + - **关键:每条要有具体细节**,不要抽象概括(如"模型名需包含完整前缀与斜杠"比"注重细节"有用) + - 条目间要有逻辑关联,共同支撑第一条的核心画像 + +3. **信息密度优先**:宁可多写一条具体特征,也不要为了精炼而泛化 +4. 总字数 150-400 字 **群聊侧写**结构: -1. **核心定位**(第一句):一句话概括群的本质定位与氛围基调 -2. **展开描述**(后续):成员构成、讨论风格、群文化等,可用项目符号辅助,但每条要有信息密度 -3. 总字数 80-250 字 +- 同样使用项目符号,第一条定位群的核心属性,后续展开成员构成、讨论风格、群文化等 +- 总字数 120-350 字 -**反面示例**(不要这样写——碎片化,读完记不住是谁): +**反面示例**(不要这样写): ``` -- 在校学生兼独立开发者 -- AI/Agent 工程实践者,偏工程化落地 -- 沟通中喜欢追问到具体机制 -- 对表述与参数准确性敏感 -- 关注稳定性与成本 +❌ 过度抽象,丢失具体信息: +"Null 是那种把 AI 系统当工程产线来打理的人,核心驱动力不是炫技,而是让模型、链路和配置始终保持可用、可控、可回退。" +→ 问题:比喻虽好,但"权衡算力与预算"、"OpenAI 兼容接口"、"容器化"等关键细节全丢了 + +❌ 孤立罗列,缺乏主线: +- 会 Python +- 用过 Docker +- 喜欢开源 +→ 问题:每条独立,看不出这个人的核心特质和做事逻辑 ``` -**正面示例**(应该这样写——精炼连贯,读完脑中浮现一个人): +**正面示例**(应该这样写): ``` -务实的工程型开发者,在校学生身份下独立运营开源项目。核心驱动力是"让东西真正能用"——偏好可配置、可维护的方案,习惯小实验快速验证,不可行就止损。沟通直奔具体机制与数据结构,对表述准确性敏感,发现偏差会刨根问底。 +✓ 第一条定调 + 后续高密度展开: +- 在校学生/业余开发者,做技术取舍会权衡时间、算力与预算,偏好高性价比方案。 +- 独立开发并维护开源项目,关注 AI 应用与 Agent 的工程化落地。 +- 熟悉本地/自建模型服务与 OpenAI 兼容接口的接入联调,强调配置项必须与实际资源严格一致。 +- 对"标识符/名称"细节较敏感(如模型名需包含完整前缀与斜杠、tag 等),倾向要求原样填写以避免隐性错误。 +- 有容器化与自建基础设施经验,偏好自动化运维与稳定可控的部署方式。 +- 排障思路偏"先核对关键信息—再做最小改动验证",会主动索要配置/截图与报错信息。 +- 沟通风格简短直接,在社群中常承担技术支持与规则把关角色。 + +(注:每条都有具体细节,共同支撑"务实的工程型开发者"这个核心画像) ``` 输出规则(调用 `update_profile` 工具): From d2bd479dd86e4a3aaa4fbf2c7fdd5c5b21262670 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 13:40:30 +0800 Subject: [PATCH 06/21] feat(config): support model request params --- config.toml.example | 28 + docs/configuration.md | 18 +- src/Undefined/ai/llm.py | 68 +- src/Undefined/ai/model_selector.py | 2 + src/Undefined/ai/retrieval.py | 56 ++ src/Undefined/config/loader.py | 29 + src/Undefined/config/models.py | 8 + src/Undefined/utils/request_params.py | 45 ++ src/Undefined/webui/static/js/config-form.js | 694 ++++++++++++++----- src/Undefined/webui/utils/toml_render.py | 47 +- tests/test_config_request_params.py | 98 +++ tests/test_llm_request_params.py | 85 +++ tests/test_retrieval.py | 74 ++ tests/test_webui_render_toml.py | 53 ++ 14 files changed, 1132 insertions(+), 173 deletions(-) create mode 100644 src/Undefined/utils/request_params.py create mode 100644 tests/test_config_request_params.py create mode 100644 tests/test_llm_request_params.py diff --git a/config.toml.example b/config.toml.example index 01a5d639..70fca10f 100644 --- a/config.toml.example +++ b/config.toml.example @@ -102,6 +102,10 @@ thinking_include_budget = true # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = false +# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +[models.chat.request_params] + # zh: 模型池配置(可选,支持多模型轮询/随机/用户指定)。 # en: Model pool configuration (optional, supports round-robin/random/user-specified). [models.chat.pool] @@ -143,6 +147,10 @@ thinking_include_budget = true # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = false +# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +[models.vision.request_params] + # zh: 安全模型配置(用于防注入检测和注入后回复生成)。 # en: Security model config (injection detection and post-injection responses). [models.security] @@ -177,6 +185,10 @@ thinking_include_budget = true # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = false +# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +[models.security.request_params] + # zh: Agent 模型配置(用于执行 agents)。 # en: Agent model config (used to run agents). [models.agent] @@ -208,6 +220,10 @@ thinking_include_budget = true # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = false +# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +[models.agent.request_params] + # zh: Agent 模型池配置(可选,支持多模型轮询/随机/用户指定)。 # en: Agent model pool configuration (optional, supports round-robin/random/user-specified). [models.agent.pool] @@ -252,6 +268,10 @@ thinking_include_budget = true # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = false +# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +[models.historian.request_params] + # zh: 嵌入模型配置(知识库语义检索使用)。 # en: Embedding model config (used by knowledge semantic retrieval). [models.embedding] @@ -277,6 +297,10 @@ query_instruction = "" # en: Document instruction prefix (optional, common for E5-style models, e.g. "passage: "). document_instruction = "" +# zh: 额外请求体参数(可选),用于 embedding 供应商的扩展字段。 +# en: Extra request-body params (optional) for embedding-provider-specific fields. +[models.embedding.request_params] + # zh: 重排模型配置(知识库二阶段检索使用)。 # en: Rerank model config (used in second-stage knowledge retrieval). [models.rerank] @@ -296,6 +320,10 @@ queue_interval_seconds = 1.0 # en: Query instruction prefix (optional, required by some rerank models, e.g. "Instruct: ...\\nQuery: "). query_instruction = "" +# zh: 额外请求体参数(可选),用于 rerank 供应商的扩展字段。 +# en: Extra request-body params (optional) for rerank-provider-specific fields. +[models.rerank.request_params] + # zh: 本地知识库配置。 # en: Local knowledge base settings. [knowledge] diff --git a/docs/configuration.md b/docs/configuration.md index 0481f77f..7659fb1e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -146,6 +146,7 @@ model_name = "gpt-4o-mini" | `thinking_budget_tokens` | thinking 预算 | | `thinking_include_budget` | 是否发送 budget_tokens | | `thinking_tool_call_compat` | Tool Calls 兼容模式(回传 reasoning_content) | +| `request_params` | 额外请求体参数(透传给模型 API,保留字段会忽略) | 兼容字段(旧配置): - `models..deepseek_new_cot_support` @@ -159,6 +160,9 @@ model_name = "gpt-4o-mini" - `queue_interval_seconds=1.0`(`<=0` 回退 `1.0`) - `thinking_budget_tokens=20000` +补充: +- 可通过 `[models.chat.request_params]` 传入 `temperature`、`response_format`、`reasoning_effort` 或兼容网关私有字段。 + ### 4.4.3 `[models.vision]` 视觉模型 默认: @@ -205,7 +209,11 @@ model_name = "gpt-4o-mini" `models` 条目支持字段: - `model_name`(必填) -- `api_url`/`api_key`/`max_tokens`/`queue_interval_seconds`/`thinking_*`(可选,缺省继承主模型) +- `api_url`/`api_key`/`max_tokens`/`queue_interval_seconds`/`thinking_*`/`request_params`(可选,缺省继承主模型) + +`request_params` 继承规则: +- `[[models.chat.pool.models]]` 与 `[[models.agent.pool.models]]` 的 `request_params` 会与主模型按顶层键浅合并。 +- 同名键由池条目覆盖;嵌套对象按整键替换,不做深合并。 生效条件(全部满足才启用池): 1. `features.pool_enabled=true` @@ -223,6 +231,7 @@ model_name = "gpt-4o-mini" | `dimensions` | `0` | 向量维度;`0`/空视为 `None`(模型默认) | | `query_instruction` | `""` | 查询前缀 | | `document_instruction` | `""` | 文档前缀 | +| `request_params` | `{}` | 额外请求体参数;保留字段如 `model`/`input`/`dimensions` 会忽略 | ### 4.4.9 `[models.rerank]` 重排模型 @@ -233,6 +242,13 @@ model_name = "gpt-4o-mini" | `model_name` | `""` | 模型名 | | `queue_interval_seconds` | `1.0` | `<=0` 回退 `1.0` | | `query_instruction` | `""` | 查询前缀 | +| `request_params` | `{}` | 额外请求体参数;保留字段如 `model`/`query`/`documents`/`top_n` 会忽略 | + +`request_params` 说明: +- 仅用于**请求体**字段,不包含 `api_key`、`base_url`、`timeout`、`extra_headers` 等 client 选项。 +- 聊天类保留字段:`model`、`messages`、`max_tokens`、`tools`、`tool_choice`、`stream`、`stream_options`。 +- embedding 保留字段:`model`、`input`、`dimensions`。 +- rerank 保留字段:`model`、`query`、`documents`、`top_n`、`return_documents`。 --- diff --git a/src/Undefined/ai/llm.py b/src/Undefined/ai/llm.py index db00aaca..bd7fd86f 100644 --- a/src/Undefined/ai/llm.py +++ b/src/Undefined/ai/llm.py @@ -35,6 +35,10 @@ ) from Undefined.token_usage_storage import TokenUsageStorage, TokenUsage from Undefined.utils.logging import log_debug_json, redact_string +from Undefined.utils.request_params import ( + merge_request_params, + split_reserved_request_params, +) from Undefined.utils.tool_calls import normalize_tool_arguments_json logger = logging.getLogger(__name__) @@ -72,6 +76,25 @@ "top_logprobs", } +_SDK_REQUEST_OPTION_FIELDS: frozenset[str] = frozenset( + {"extra_headers", "extra_query", "extra_body", "timeout"} +) + +_CHAT_COMPLETIONS_RESERVED_FIELDS: frozenset[str] = ( + frozenset( + { + "model", + "messages", + "max_tokens", + "tools", + "tool_choice", + "stream", + "stream_options", + } + ) + | _SDK_REQUEST_OPTION_FIELDS +) + _THINKING_KEYS: tuple[str, ...] = ( "thinking", "reasoning", @@ -729,6 +752,44 @@ def _normalize_openai_base_url( return normalized, default_query, True +def _warn_ignored_request_params( + *, + call_type: str, + model_name: str, + ignored: dict[str, Any], +) -> None: + if not ignored: + return + logger.warning( + "[request_params] ignored_keys=%s type=%s model=%s", + ",".join(sorted(ignored)), + call_type, + model_name, + ) + + +def _build_effective_request_kwargs( + model_config: ModelConfig, + *, + call_type: str, + overrides: dict[str, Any], +) -> dict[str, Any]: + merged = merge_request_params( + getattr(model_config, "request_params", {}), + overrides, + ) + allowed, ignored = split_reserved_request_params( + merged, + _CHAT_COMPLETIONS_RESERVED_FIELDS, + ) + _warn_ignored_request_params( + call_type=call_type, + model_name=model_config.model_name, + ignored=ignored, + ) + return allowed + + class ModelRequester: """统一的模型请求封装。""" @@ -774,13 +835,18 @@ async def request( tool_args_fixed, len(messages_for_api), ) + effective_kwargs = _build_effective_request_kwargs( + model_config, + call_type=call_type, + overrides=dict(kwargs), + ) request_body = build_request_body( model_config=model_config, messages=messages_for_api, max_tokens=max_tokens, tools=tools, tool_choice=tool_choice, - **kwargs, + **effective_kwargs, ) api_to_internal: dict[str, str] = {} diff --git a/src/Undefined/ai/model_selector.py b/src/Undefined/ai/model_selector.py index 62515908..d255ca6d 100644 --- a/src/Undefined/ai/model_selector.py +++ b/src/Undefined/ai/model_selector.py @@ -245,6 +245,7 @@ def _entry_to_chat_config( thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + request_params=entry.request_params, pool=primary.pool, ) @@ -263,5 +264,6 @@ def _entry_to_agent_config( thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + request_params=entry.request_params, pool=primary.pool, ) diff --git a/src/Undefined/ai/retrieval.py b/src/Undefined/ai/retrieval.py index f0e5971a..82c42d42 100644 --- a/src/Undefined/ai/retrieval.py +++ b/src/Undefined/ai/retrieval.py @@ -11,6 +11,7 @@ from Undefined.ai.tokens import TokenCounter from Undefined.config import EmbeddingModelConfig, RerankModelConfig +from Undefined.utils.request_params import split_reserved_request_params logger = logging.getLogger(__name__) @@ -20,6 +21,18 @@ _TokenCounterGetter: TypeAlias = Callable[[str], TokenCounter] _RecordUsageCallback: TypeAlias = Callable[..., None] +_SDK_REQUEST_OPTION_FIELDS: frozenset[str] = frozenset( + {"extra_headers", "extra_query", "extra_body", "timeout"} +) +_EMBEDDING_KNOWN_FIELDS: frozenset[str] = frozenset({"encoding_format", "user"}) +_EMBEDDING_RESERVED_FIELDS: frozenset[str] = ( + frozenset({"model", "input", "dimensions"}) | _SDK_REQUEST_OPTION_FIELDS +) +_RERANK_RESERVED_FIELDS: frozenset[str] = ( + frozenset({"model", "query", "documents", "top_n", "return_documents"}) + | _SDK_REQUEST_OPTION_FIELDS +) + class RetrievalRequester: """统一处理嵌入与重排请求,便于复用统计与 SDK 调用逻辑。""" @@ -37,6 +50,26 @@ def __init__( self._get_token_counter = get_token_counter self._record_usage = record_usage + def _split_request_params( + self, + model_config: _ModelConfig, + *, + reserved_fields: frozenset[str], + call_type: str, + ) -> tuple[dict[str, Any], dict[str, Any]]: + allowed, ignored = split_reserved_request_params( + getattr(model_config, "request_params", {}), + reserved_fields, + ) + if ignored: + logger.warning( + "[request_params] ignored_keys=%s type=%s model=%s", + ",".join(sorted(ignored)), + call_type, + model_config.model_name, + ) + return allowed, ignored + async def embed( self, model_config: EmbeddingModelConfig, texts: list[str] ) -> list[list[float]]: @@ -46,10 +79,27 @@ async def embed( start_time = time.perf_counter() client = self._get_openai_client(model_config) + request_params, _ = self._split_request_params( + model_config, + reserved_fields=_EMBEDDING_RESERVED_FIELDS, + call_type="embedding", + ) + method_kwargs = { + key: value + for key, value in request_params.items() + if key in _EMBEDDING_KNOWN_FIELDS + } + extra_body = { + key: value + for key, value in request_params.items() + if key not in _EMBEDDING_KNOWN_FIELDS + } response = await client.embeddings.create( model=model_config.model_name, input=texts, dimensions=model_config.dimensions or NOT_GIVEN, # type: ignore[arg-type] + extra_body=extra_body or None, + **method_kwargs, ) response_dict = self._response_to_dict(response) embeddings = [item.embedding for item in response.data] @@ -96,7 +146,13 @@ async def rerank( start_time = time.perf_counter() client = self._get_openai_client(model_config) + request_params, _ = self._split_request_params( + model_config, + reserved_fields=_RERANK_RESERVED_FIELDS, + call_type="rerank", + ) request_body: dict[str, Any] = { + **request_params, "model": model_config.model_name, "query": query, "documents": documents, diff --git a/src/Undefined/config/loader.py b/src/Undefined/config/loader.py index 053f0ef4..c9081497 100644 --- a/src/Undefined/config/loader.py +++ b/src/Undefined/config/loader.py @@ -39,6 +39,10 @@ def load_dotenv( SecurityModelConfig, VisionModelConfig, ) +from Undefined.utils.request_params import ( + merge_request_params, + normalize_request_params, +) logger = logging.getLogger(__name__) @@ -225,6 +229,16 @@ def _coerce_str_list(value: Any) -> list[str]: return [] +def _coerce_request_params(value: Any) -> dict[str, Any]: + return normalize_request_params(value) + + +def _get_model_request_params(data: dict[str, Any], model_name: str) -> dict[str, Any]: + return _coerce_request_params( + _get_nested(data, ("models", model_name, "request_params")) + ) + + def _get_value( data: dict[str, Any], path: tuple[str, ...], @@ -1511,6 +1525,10 @@ def _parse_model_pool( item.get("thinking_tool_call_compat"), primary_config.thinking_tool_call_compat, ), + request_params=merge_request_params( + primary_config.request_params, + item.get("request_params"), + ), ) ) @@ -1554,6 +1572,7 @@ def _parse_embedding_model_config(data: dict[str, Any]) -> EmbeddingModelConfig: _get_value(data, ("models", "embedding", "document_instruction"), None), "", ), + request_params=_get_model_request_params(data, "embedding"), ) @staticmethod @@ -1587,6 +1606,7 @@ def _parse_rerank_model_config(data: dict[str, Any]) -> RerankModelConfig: query_instruction=_coerce_str( _get_value(data, ("models", "rerank", "query_instruction"), None), "" ), + request_params=_get_model_request_params(data, "rerank"), ) @staticmethod @@ -1648,6 +1668,7 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + request_params=_get_model_request_params(data, "chat"), ) config.pool = Config._parse_model_pool(data, "chat", config) return config @@ -1711,6 +1732,7 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + request_params=_get_model_request_params(data, "vision"), ) @staticmethod @@ -1788,6 +1810,7 @@ def _parse_security_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + request_params=_get_model_request_params(data, "security"), ) logger.warning("未配置安全模型,将使用对话模型作为后备") @@ -1801,6 +1824,7 @@ def _parse_security_model_config( thinking_budget_tokens=0, thinking_include_budget=True, thinking_tool_call_compat=False, + request_params=merge_request_params(chat_model.request_params), ) @staticmethod @@ -1862,6 +1886,7 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + request_params=_get_model_request_params(data, "agent"), ) config.pool = Config._parse_model_pool(data, "agent", config) return config @@ -2009,6 +2034,10 @@ def _parse_historian_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + request_params=merge_request_params( + fallback.request_params, + h.get("request_params"), + ), ) @staticmethod diff --git a/src/Undefined/config/models.py b/src/Undefined/config/models.py index 2a256ab8..0f0992e0 100644 --- a/src/Undefined/config/models.py +++ b/src/Undefined/config/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from typing import Any @dataclass @@ -18,6 +19,7 @@ class ModelPoolEntry: thinking_budget_tokens: int = 0 thinking_include_budget: bool = True thinking_tool_call_compat: bool = False + request_params: dict[str, Any] = field(default_factory=dict) @dataclass @@ -44,6 +46,7 @@ class ChatModelConfig: thinking_tool_call_compat: bool = ( False # 思维链 + 工具调用兼容(回传 reasoning_content) ) + request_params: dict[str, Any] = field(default_factory=dict) pool: ModelPool | None = None # 模型池配置 @@ -61,6 +64,7 @@ class VisionModelConfig: thinking_tool_call_compat: bool = ( False # 思维链 + 工具调用兼容(回传 reasoning_content) ) + request_params: dict[str, Any] = field(default_factory=dict) @dataclass @@ -78,6 +82,7 @@ class SecurityModelConfig: thinking_tool_call_compat: bool = ( False # 思维链 + 工具调用兼容(回传 reasoning_content) ) + request_params: dict[str, Any] = field(default_factory=dict) @dataclass @@ -91,6 +96,7 @@ class EmbeddingModelConfig: dimensions: int | None = None query_instruction: str = "" # 查询端指令前缀(如 Qwen3-Embedding 需要) document_instruction: str = "" # 文档端指令前缀(如 E5 系列需要 "passage: ") + request_params: dict[str, Any] = field(default_factory=dict) @dataclass @@ -102,6 +108,7 @@ class RerankModelConfig: model_name: str queue_interval_seconds: float = 1.0 query_instruction: str = "" # 查询端指令前缀(如部分 rerank 模型需要) + request_params: dict[str, Any] = field(default_factory=dict) @dataclass @@ -119,6 +126,7 @@ class AgentModelConfig: thinking_tool_call_compat: bool = ( False # 思维链 + 工具调用兼容(回传 reasoning_content) ) + request_params: dict[str, Any] = field(default_factory=dict) pool: ModelPool | None = None # 模型池配置 diff --git a/src/Undefined/utils/request_params.py b/src/Undefined/utils/request_params.py new file mode 100644 index 00000000..58139a77 --- /dev/null +++ b/src/Undefined/utils/request_params.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import Any, Mapping + + +def _clone_json_like(value: Any) -> Any: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {str(key): _clone_json_like(item) for key, item in value.items()} + if isinstance(value, list): + return [_clone_json_like(item) for item in value] + if isinstance(value, tuple): + return [_clone_json_like(item) for item in value] + return str(value) + + +def normalize_request_params(value: Any) -> dict[str, Any]: + if not isinstance(value, Mapping): + return {} + return {str(key): _clone_json_like(item) for key, item in value.items()} + + +def merge_request_params(*values: Any) -> dict[str, Any]: + merged: dict[str, Any] = {} + for value in values: + if not isinstance(value, Mapping): + continue + merged.update(normalize_request_params(value)) + return merged + + +def split_reserved_request_params( + params: Mapping[str, Any] | None, + reserved_keys: set[str] | frozenset[str], +) -> tuple[dict[str, Any], dict[str, Any]]: + normalized = normalize_request_params(params) + allowed: dict[str, Any] = {} + reserved: dict[str, Any] = {} + for key, value in normalized.items(): + if key in reserved_keys: + reserved[key] = value + else: + allowed[key] = value + return allowed, reserved diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index 13eecfca..3260f9eb 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -42,6 +42,40 @@ function updateConfigSearchIndex() { }); } +function isPlainObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) +} + +function isAotCollection(path, value) { + return Array.isArray(value) && (AOT_PATHS.has(path) || value.some(item => item !== null && typeof item === "object")) +} + +function isRequestParamsPath(path) { + return path === "request_params" || path.endsWith(".request_params") +} + +function scheduleAutoSave() { + if (state.saveTimer) clearTimeout(state.saveTimer) + showSaveStatus("saving", t("config.typing")) + state.saveTimer = setTimeout(() => { + state.saveTimer = null + autoSave() + }, 1000) +} + +function createEditorNode(path, value) { + if (isRequestParamsPath(path)) { + return createRequestParamsWidget(path, isPlainObject(value) ? value : {}) + } + if (isPlainObject(value)) { + return createSubSubSection(path, value) + } + if (isAotCollection(path, value)) { + return createAotWidget(path, value) + } + return createField(path, value) +} + function buildConfigForm() { const container = get("formSections"); if (!container) return; @@ -93,42 +127,7 @@ function buildConfigForm() { card.appendChild(fieldGrid); for (const [key, val] of Object.entries(values)) { - if (typeof val === "object" && !Array.isArray(val)) { - const subSection = document.createElement("div"); - subSection.className = "form-subsection"; - - const subTitle = document.createElement("div"); - subTitle.className = "form-subtitle"; - subTitle.innerText = `[${section}.${key}]`; - subSection.appendChild(subTitle); - - const subCommentKey = `${section}.${key}`; - const subComment = getComment(subCommentKey); - if (subComment) { - const subHint = document.createElement("div"); - subHint.className = "form-subtitle-hint"; - subHint.innerText = subComment; - subHint.dataset.commentPath = subCommentKey; - subSection.appendChild(subHint); - } - - const subGrid = document.createElement("div"); - subGrid.className = "form-fields"; - for (const [sk, sv] of Object.entries(val)) { - const subPath = `${section}.${key}.${sk}`; - if (sv !== null && typeof sv === "object" && !Array.isArray(sv)) { - subGrid.appendChild(createSubSubSection(subPath, sv)); - } else if (Array.isArray(sv) && (AOT_PATHS.has(subPath) || sv.some(i => typeof i === "object" && i !== null))) { - subGrid.appendChild(createAotWidget(subPath, sv)); - } else { - subGrid.appendChild(createField(subPath, sv)); - } - } - subSection.appendChild(subGrid); - fieldGrid.appendChild(subSection); - continue; - } - fieldGrid.appendChild(createField(`${section}.${key}`, val)); + fieldGrid.appendChild(createEditorNode(`${section}.${key}`, val)); } container.appendChild(card); } @@ -294,11 +293,7 @@ function createField(path, val) { input.dataset.path = path; group.appendChild(input); - input.oninput = () => { - if (state.saveTimer) clearTimeout(state.saveTimer); - showSaveStatus("saving", t("config.typing")); - state.saveTimer = setTimeout(() => { state.saveTimer = null; autoSave(); }, 1000); - }; + input.oninput = () => scheduleAutoSave(); } return group; } @@ -324,145 +319,534 @@ function createSubSubSection(path, obj) { grid.className = "form-fields"; for (const [k, v] of Object.entries(obj)) { const subPath = `${path}.${k}`; - if (AOT_PATHS.has(subPath) || (Array.isArray(v) && v.length > 0 && v.every(i => typeof i === "object" && i !== null))) { - grid.appendChild(createAotWidget(subPath, v)); - } else { - grid.appendChild(createField(subPath, v)); - } + grid.appendChild(createEditorNode(subPath, v)); } div.appendChild(grid); return div; } +function buildEmptyStructuredValue(value) { + if (Array.isArray(value)) { + return value.length > 0 ? [buildEmptyStructuredValue(value[0])] : [] + } + if (isPlainObject(value)) { + return Object.fromEntries(Object.keys(value).map(key => [key, buildEmptyStructuredValue(value[key])])) + } + if (typeof value === "boolean") return false + if (value === null) return null + return "" +} + +function inferStructuredType(value) { + if (Array.isArray(value)) return "array" + if (isPlainObject(value)) return "object" + return "scalar" +} + +function inferScalarType(value) { + if (value === null) return "null" + if (typeof value === "number") return "number" + if (typeof value === "boolean") return "boolean" + return "string" +} + +function createRequestParamsWidget(path, value) { + const group = document.createElement("div") + group.className = "form-group" + group.dataset.path = path + + const label = document.createElement("label") + label.className = "form-label" + label.innerText = path.split(".").pop() + group.appendChild(label) + + const comment = getComment(path) + if (comment) { + const hint = document.createElement("div") + hint.className = "form-hint" + hint.innerText = comment + hint.dataset.commentPath = path + group.appendChild(hint) + } + + group.dataset.searchText = `${path} ${comment || ""}`.toLowerCase() + const editor = createStructuredValueEditor(value, { rootType: "object" }) + editor.dataset.requestParamsRoot = "true" + group.appendChild(editor) + return group +} + +function createStructuredActionButton(text, onClick) { + const button = document.createElement("button") + button.type = "button" + button.className = "btn ghost btn-sm" + button.innerText = text + button.onclick = onClick + return button +} + +function createStructuredValueEditor(value, options = {}) { + const rootType = options.rootType || inferStructuredType(value) + if (rootType === "object") { + return createStructuredObjectEditor(isPlainObject(value) ? value : {}) + } + if (rootType === "array") { + return createStructuredArrayEditor(Array.isArray(value) ? value : []) + } + return createStructuredScalarEditor(value) +} + +function createStructuredObjectEditor(value) { + const wrapper = document.createElement("div") + wrapper.dataset.structuredType = "object" + + const body = document.createElement("div") + body.className = "request-params-object-body" + wrapper.appendChild(body) + + Object.entries(value).forEach(([key, itemValue]) => { + body.appendChild(createStructuredObjectEntry(key, itemValue)) + }) + + const actions = document.createElement("div") + actions.style.marginTop = "8px" + actions.style.display = "flex" + actions.style.gap = "8px" + actions.style.flexWrap = "wrap" + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Field`, () => { + body.appendChild(createStructuredObjectEntry("", "")) + autoSave() + })) + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Object`, () => { + body.appendChild(createStructuredObjectEntry("", {})) + autoSave() + })) + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Array`, () => { + body.appendChild(createStructuredObjectEntry("", [])) + autoSave() + })) + wrapper.appendChild(actions) + + return wrapper +} + +function createStructuredObjectEntry(key, value) { + const entry = document.createElement("div") + entry.className = "request-params-object-entry" + entry.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" + + const keyGroup = document.createElement("div") + keyGroup.className = "form-group" + const keyLabel = document.createElement("label") + keyLabel.className = "form-label" + keyLabel.innerText = "key" + keyGroup.appendChild(keyLabel) + const keyInput = document.createElement("input") + keyInput.type = "text" + keyInput.className = "form-control request-params-key-input" + keyInput.value = key || "" + keyInput.oninput = () => scheduleAutoSave() + keyGroup.appendChild(keyInput) + entry.appendChild(keyGroup) + + const valueContainer = document.createElement("div") + valueContainer.className = "request-params-entry-value" + valueContainer.appendChild(createStructuredValueEditor(value)) + entry.appendChild(valueContainer) + + const removeBtn = createStructuredActionButton(t("config.aot_remove"), () => { + entry.remove() + autoSave() + }) + entry.appendChild(removeBtn) + + return entry +} + +function createStructuredArrayEditor(value) { + const wrapper = document.createElement("div") + wrapper.dataset.structuredType = "array" + + const body = document.createElement("div") + body.className = "request-params-array-body" + wrapper.appendChild(body) + + value.forEach(itemValue => { + body.appendChild(createStructuredArrayEntry(itemValue)) + }) + + const actions = document.createElement("div") + actions.style.marginTop = "8px" + actions.style.display = "flex" + actions.style.gap = "8px" + actions.style.flexWrap = "wrap" + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Value`, () => { + body.appendChild(createStructuredArrayEntry("")) + autoSave() + })) + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Object`, () => { + body.appendChild(createStructuredArrayEntry({})) + autoSave() + })) + actions.appendChild(createStructuredActionButton(`${t("config.aot_add")} Array`, () => { + body.appendChild(createStructuredArrayEntry([])) + autoSave() + })) + wrapper.appendChild(actions) + + return wrapper +} + +function createStructuredArrayEntry(value) { + const entry = document.createElement("div") + entry.className = "request-params-array-entry" + entry.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" + + const valueContainer = document.createElement("div") + valueContainer.className = "request-params-entry-value" + valueContainer.appendChild(createStructuredValueEditor(value)) + entry.appendChild(valueContainer) + + const removeBtn = createStructuredActionButton(t("config.aot_remove"), () => { + entry.remove() + autoSave() + }) + entry.appendChild(removeBtn) + + return entry +} + +function createStructuredScalarEditor(value) { + const wrapper = document.createElement("div") + wrapper.dataset.structuredType = "scalar" + + const typeGroup = document.createElement("div") + typeGroup.className = "form-group" + const typeLabel = document.createElement("label") + typeLabel.className = "form-label" + typeLabel.innerText = "type" + typeGroup.appendChild(typeLabel) + + const typeSelect = document.createElement("select") + typeSelect.className = "form-control request-params-scalar-type" + ;["string", "number", "boolean", "null"].forEach(type => { + const option = document.createElement("option") + option.value = type + option.innerText = type + typeSelect.appendChild(option) + }) + typeSelect.value = inferScalarType(value) + typeGroup.appendChild(typeSelect) + wrapper.appendChild(typeGroup) + + const valueContainer = document.createElement("div") + valueContainer.className = "request-params-scalar-value" + wrapper.appendChild(valueContainer) + + const renderValueInput = () => { + const scalarType = typeSelect.value + valueContainer.textContent = "" + + if (scalarType === "null") { + const emptyHint = document.createElement("div") + emptyHint.className = "form-hint" + emptyHint.innerText = "null" + valueContainer.appendChild(emptyHint) + return + } + + if (scalarType === "boolean") { + const booleanWrap = document.createElement("label") + booleanWrap.className = "toggle-wrapper" + const booleanInput = document.createElement("input") + booleanInput.type = "checkbox" + booleanInput.className = "toggle-input request-params-scalar-input" + booleanInput.checked = typeof value === "boolean" ? value : false + booleanInput.onchange = () => autoSave() + const track = document.createElement("span") + track.className = "toggle-track" + const handle = document.createElement("span") + handle.className = "toggle-handle" + track.appendChild(handle) + booleanWrap.appendChild(booleanInput) + booleanWrap.appendChild(track) + valueContainer.appendChild(booleanWrap) + return + } + + const input = document.createElement("input") + input.className = "form-control request-params-scalar-input" + input.type = scalarType === "number" ? "number" : "text" + if (scalarType === "number") { + input.step = "any" + input.value = typeof value === "number" ? String(value) : "" + } else { + input.value = value == null ? "" : String(value) + } + input.oninput = () => scheduleAutoSave() + valueContainer.appendChild(input) + } + + typeSelect.onchange = () => { + value = typeSelect.value === "boolean" ? false : typeSelect.value === "null" ? null : "" + renderValueInput() + autoSave() + } + + renderValueInput() + return wrapper +} + +function getStructuredValueChild(container) { + return Array.from(container.children).find(child => child.dataset && child.dataset.structuredType) +} + +function readStructuredValueEditor(node) { + const type = node.dataset.structuredType + if (type === "object") { + const result = {} + const body = node.querySelector(".request-params-object-body") + Array.from(body ? body.children : []).forEach(entry => { + const keyInput = entry.querySelector(".request-params-key-input") + const key = keyInput ? keyInput.value.trim() : "" + if (!key) return + const valueContainer = entry.querySelector(".request-params-entry-value") + const valueNode = valueContainer ? getStructuredValueChild(valueContainer) : null + if (!valueNode) return + result[key] = readStructuredValueEditor(valueNode) + }) + return result + } + if (type === "array") { + const body = node.querySelector(".request-params-array-body") + return Array.from(body ? body.children : []).map(entry => { + const valueContainer = entry.querySelector(".request-params-entry-value") + const valueNode = valueContainer ? getStructuredValueChild(valueContainer) : null + return valueNode ? readStructuredValueEditor(valueNode) : null + }) + } + + const scalarType = node.querySelector(".request-params-scalar-type")?.value || "string" + if (scalarType === "null") return null + const scalarInput = node.querySelector(".request-params-scalar-input") + if (scalarType === "boolean") { + return Boolean(scalarInput?.checked) + } + const raw = scalarInput ? scalarInput.value : "" + if (scalarType === "number") { + const parsed = Number(raw) + return Number.isNaN(parsed) ? 0 : parsed + } + return raw +} + +function createAotScalarInput(key, value) { + const isLong = isLongText(value) + const isNumber = typeof value === "number" + const isBoolean = typeof value === "boolean" + const isArray = Array.isArray(value) + const isSecret = isSensitiveKey(key) + + if (isBoolean) { + const wrapper = document.createElement("label") + wrapper.className = "toggle-wrapper" + const toggle = document.createElement("input") + toggle.type = "checkbox" + toggle.className = "toggle-input aot-field-input" + toggle.dataset.valueType = "boolean" + toggle.checked = Boolean(value) + toggle.onchange = () => autoSave() + const track = document.createElement("span") + track.className = "toggle-track" + const handle = document.createElement("span") + handle.className = "toggle-handle" + track.appendChild(handle) + wrapper.appendChild(toggle) + wrapper.appendChild(track) + return wrapper + } + + let input + if (isLong) { + input = document.createElement("textarea") + input.className = "form-control form-textarea aot-field-input" + } else { + input = document.createElement("input") + input.className = "form-control aot-field-input" + input.type = isNumber ? "number" : isSecret ? "password" : "text" + if (isNumber) input.step = "any" + if (isSecret) input.setAttribute("autocomplete", "new-password") + } + + input.dataset.valueType = isNumber ? "number" : isArray ? "array" : "string" + if (isArray) { + input.dataset.arrayType = value.every(item => typeof item === "number") ? "number" : "string" + input.value = value.join(", ") + } else { + input.value = value == null ? "" : String(value) + } + input.oninput = () => scheduleAutoSave() + return input +} + function createAotEntry(path, entry) { - const div = document.createElement("div"); - div.className = "aot-entry"; - div.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;"; - const fields = document.createElement("div"); - fields.className = "form-fields"; - for (const [k, v] of Object.entries(entry)) { - const fg = document.createElement("div"); - fg.className = "form-group"; - fg.dataset.path = `${path}[].${k}`; - const lbl = document.createElement("label"); - lbl.className = "form-label"; - lbl.innerText = k; - fg.appendChild(lbl); - const isSecret = isSensitiveKey(k); - const inp = document.createElement("input"); - inp.className = "form-control aot-field-input"; - inp.type = isSecret ? "password" : "text"; - inp.value = v == null ? "" : String(v); - inp.dataset.fieldKey = k; - if (isSecret) inp.setAttribute("autocomplete", "new-password"); - inp.oninput = () => { - if (state.saveTimer) clearTimeout(state.saveTimer); - showSaveStatus("saving", t("config.typing")); - state.saveTimer = setTimeout(() => { state.saveTimer = null; autoSave(); }, 1000); - }; - fg.appendChild(inp); - fields.appendChild(fg); - } - div.appendChild(fields); - const removeBtn = document.createElement("button"); - removeBtn.type = "button"; - removeBtn.className = "btn ghost btn-sm"; - removeBtn.innerText = t("config.aot_remove"); - removeBtn.onclick = () => { div.remove(); autoSave(); }; - div.appendChild(removeBtn); - return div; + const div = document.createElement("div") + div.className = "aot-entry" + div.style.cssText = "border:1px solid var(--border);border-radius:6px;padding:10px;margin-bottom:8px;" + const fields = document.createElement("div") + fields.className = "form-fields" + + for (const [key, value] of Object.entries(entry)) { + const field = document.createElement("div") + field.className = "form-group aot-entry-field" + field.dataset.fieldKey = key + field.dataset.path = `${path}[].${key}` + + const label = document.createElement("label") + label.className = "form-label" + label.innerText = key + field.appendChild(label) + + const fieldPath = `${path}.${key}` + if (isPlainObject(value) || Array.isArray(value)) { + field.dataset.fieldEditor = "structured" + const editor = createStructuredValueEditor(value, { rootType: isRequestParamsPath(fieldPath) ? "object" : inferStructuredType(value) }) + field.appendChild(editor) + } else { + field.dataset.fieldEditor = "scalar" + field.appendChild(createAotScalarInput(key, value)) + } + fields.appendChild(field) + } + + div.appendChild(fields) + const removeBtn = document.createElement("button") + removeBtn.type = "button" + removeBtn.className = "btn ghost btn-sm" + removeBtn.innerText = t("config.aot_remove") + removeBtn.onclick = () => { div.remove(); autoSave() } + div.appendChild(removeBtn) + return div +} + +function buildAotTemplate(path, arr) { + if (arr && arr.length > 0) { + const template = buildEmptyStructuredValue(arr[0]) + if (AOT_PATHS.has(path) && !Object.prototype.hasOwnProperty.call(template, "request_params")) { + template.request_params = {} + } + return template + } + return { model_name: "", api_url: "", api_key: "", request_params: {} } } function createAotWidget(path, arr) { - const DEFAULT_ENTRY = { model_name: "", api_url: "", api_key: "" }; - const container = document.createElement("div"); - container.className = "form-group"; - container.dataset.path = path; - const lbl = document.createElement("div"); - lbl.className = "form-label"; - lbl.innerText = path.split(".").pop(); - container.appendChild(lbl); - const comment = getComment(path); + const container = document.createElement("div") + container.className = "form-group" + container.dataset.path = path + const lbl = document.createElement("div") + lbl.className = "form-label" + lbl.innerText = path.split(".").pop() + container.appendChild(lbl) + const comment = getComment(path) if (comment) { - const hint = document.createElement("div"); - hint.className = "form-hint"; - hint.innerText = comment; - hint.dataset.commentPath = path; - container.appendChild(hint); - } - const entriesDiv = document.createElement("div"); - entriesDiv.dataset.aotPath = path; - container.appendChild(entriesDiv); - (arr || []).forEach(entry => entriesDiv.appendChild(createAotEntry(path, entry))); - const addBtn = document.createElement("button"); - addBtn.type = "button"; - addBtn.className = "btn ghost btn-sm"; - addBtn.style.marginTop = "4px"; - addBtn.innerText = t("config.aot_add"); + const hint = document.createElement("div") + hint.className = "form-hint" + hint.innerText = comment + hint.dataset.commentPath = path + container.appendChild(hint) + } + container.dataset.searchText = `${path} ${comment || ""}`.toLowerCase() + const entriesDiv = document.createElement("div") + entriesDiv.dataset.aotPath = path + container.appendChild(entriesDiv) + ;(arr || []).forEach(entry => entriesDiv.appendChild(createAotEntry(path, entry))) + const addBtn = document.createElement("button") + addBtn.type = "button" + addBtn.className = "btn ghost btn-sm" + addBtn.style.marginTop = "4px" + addBtn.innerText = t("config.aot_add") addBtn.onclick = () => { - const template = arr && arr.length > 0 ? Object.fromEntries(Object.keys(arr[0]).map(k => [k, ""])) : DEFAULT_ENTRY; - entriesDiv.appendChild(createAotEntry(path, template)); - autoSave(); - }; - container.appendChild(addBtn); - return container; + entriesDiv.appendChild(createAotEntry(path, buildAotTemplate(path, arr))) + autoSave() + } + container.appendChild(addBtn) + return container +} + +function parseInputValue(input) { + const valueType = input.dataset.valueType || "string" + if (valueType === "boolean") { + return input.checked + } + const raw = input.value + if (valueType === "number") { + const trimmed = raw.trim() + if (!trimmed) return "" + const parsed = trimmed.includes(".") ? parseFloat(trimmed) : parseInt(trimmed, 10) + return Number.isNaN(parsed) ? raw : parsed + } + if (valueType === "array") { + const items = raw.split(",").map(item => item.trim()).filter(Boolean) + return input.dataset.arrayType === "number" + ? items.map(item => { + const number = Number(item) + return Number.isNaN(number) ? item : number + }) + : items + } + return raw } async function autoSave() { - showSaveStatus("saving"); + showSaveStatus("saving") - const patch = {}; + const patch = {} document.querySelectorAll(".config-input").forEach(input => { - const path = input.dataset.path; - let val; - if (input.type === "checkbox") { - val = input.checked; - } else { - const raw = input.value; - const valueType = input.dataset.valueType || "string"; - if (valueType === "number") { - const trimmed = raw.trim(); - if (!trimmed) { val = ""; } - else { - val = trimmed.includes(".") ? parseFloat(trimmed) : parseInt(trimmed, 10); - if (Number.isNaN(val)) val = raw; - } - } else if (valueType === "array") { - const items = raw.split(",").map(s => s.trim()).filter(Boolean); - val = input.dataset.arrayType === "number" - ? items.map(item => { const num = Number(item); return Number.isNaN(num) ? item : num; }) - : items; - } else { - val = raw; - } - } - patch[path] = val; - }); + patch[input.dataset.path] = parseInputValue(input) + }) + + document.querySelectorAll("[data-request-params-root]").forEach(editor => { + const group = editor.closest(".form-group") + if (!group?.dataset.path) return + patch[group.dataset.path] = readStructuredValueEditor(editor) + }) document.querySelectorAll("[data-aot-path]").forEach(container => { - const aotPath = container.dataset.aotPath; - const entries = []; + const aotPath = container.dataset.aotPath + const entries = [] container.querySelectorAll(".aot-entry").forEach(entry => { - const obj = {}; - entry.querySelectorAll(".aot-field-input").forEach(inp => { obj[inp.dataset.fieldKey] = inp.value; }); - entries.push(obj); - }); - patch[aotPath] = entries; - }); + const obj = {} + entry.querySelectorAll(".aot-entry-field").forEach(field => { + const key = field.dataset.fieldKey + if (!key) return + if (field.dataset.fieldEditor === "structured") { + const valueNode = getStructuredValueChild(field) + obj[key] = valueNode ? readStructuredValueEditor(valueNode) : {} + return + } + const input = field.querySelector(".aot-field-input") + if (!input) return + obj[key] = parseInputValue(input) + }) + entries.push(obj) + }) + patch[aotPath] = entries + }) try { - const res = await api("/api/patch", { method: "POST", body: JSON.stringify({ patch }) }); - const data = await res.json(); + const res = await api("/api/patch", { method: "POST", body: JSON.stringify({ patch }) }) + const data = await res.json() if (data.success) { - showSaveStatus("saved"); - if (data.warning) showToast(`${t("common.warning")}: ${data.warning}`, "warning", 5000); + showSaveStatus("saved") + if (data.warning) showToast(`${t("common.warning")}: ${data.warning}`, "warning", 5000) } else { - showSaveStatus("error", t("config.save_error")); - showToast(`${t("common.error")}: ${data.error}`, "error", 5000); + showSaveStatus("error", t("config.save_error")) + showToast(`${t("common.error")}: ${data.error}`, "error", 5000) } } catch (e) { - showSaveStatus("error", t("config.save_network_error")); - showToast(`${t("common.error")}: ${e.message}`, "error", 5000); + showSaveStatus("error", t("config.save_network_error")) + showToast(`${t("common.error")}: ${e.message}`, "error", 5000) } } diff --git a/src/Undefined/webui/utils/toml_render.py b/src/Undefined/webui/utils/toml_render.py index 467e7dbd..532b0db6 100644 --- a/src/Undefined/webui/utils/toml_render.py +++ b/src/Undefined/webui/utils/toml_render.py @@ -61,13 +61,40 @@ def _is_array_of_tables(value: Any) -> bool: ) -def render_table(path: list[str], table: TomlData) -> list[str]: - lines: list[str] = [] - items = [ +def _render_scalar_items(path: list[str], table: TomlData) -> list[str]: + return [ f"{key} = {format_value(table[key])}" for key in sorted_keys(table, path) if not isinstance(table[key], dict) and not _is_array_of_tables(table[key]) ] + + +def _render_nested_items(path: list[str], table: TomlData) -> list[str]: + lines: list[str] = [] + for key in sorted_keys(table, path): + value = table[key] + if isinstance(value, dict): + lines.extend(render_table(path + [key], value)) + elif _is_array_of_tables(value): + aot_path = path + [key] + for item in value: + lines.extend( + _render_array_of_tables_item(aot_path, cast(TomlData, item)) + ) + return lines + + +def _render_array_of_tables_item(path: list[str], table: TomlData) -> list[str]: + lines = [f"[[{'.'.join(path)}]]"] + lines.extend(_render_scalar_items(path, table)) + lines.append("") + lines.extend(_render_nested_items(path, table)) + return lines + + +def render_table(path: list[str], table: TomlData) -> list[str]: + lines: list[str] = [] + items = _render_scalar_items(path, table) if items and path: lines.append(f"[{'.'.join(path)}]") lines.extend(items) @@ -75,19 +102,7 @@ def render_table(path: list[str], table: TomlData) -> list[str]: elif items: lines.extend(items) lines.append("") - for key in sorted_keys(table, path): - value = table[key] - if isinstance(value, dict): - lines.extend(render_table(path + [key], value)) - elif _is_array_of_tables(value): - aot_path = ".".join(path + [key]) - for item in value: - lines.append(f"[[{aot_path}]]") - for k in sorted_keys(item, path + [key]): - v = item[k] - if not isinstance(v, dict): - lines.append(f"{k} = {format_value(v)}") - lines.append("") + lines.extend(_render_nested_items(path, table)) return lines diff --git a/tests/test_config_request_params.py b/tests/test_config_request_params.py new file mode 100644 index 00000000..f52384ac --- /dev/null +++ b/tests/test_config_request_params.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from pathlib import Path + +from Undefined.config.loader import Config + + +def _load_config(path: Path, text: str) -> Config: + path.write_text(text, "utf-8") + return Config.load(path, strict=False) + + +def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: + cfg = _load_config( + tmp_path / "config.toml", + """ +[onebot] +ws_url = "ws://127.0.0.1:3001" + +[models.chat] +api_url = "https://api.openai.com/v1" +api_key = "sk-chat" +model_name = "gpt-chat" + +[models.chat.request_params] +temperature = 0.2 +metadata = { source = "chat" } + +[models.chat.pool] +enabled = true +strategy = "round_robin" + +[[models.chat.pool.models]] +model_name = "gpt-chat-b" +api_url = "https://pool.example/v1" +api_key = "sk-pool" + +[models.chat.pool.models.request_params] +temperature = 0.6 +provider = { name = "pool" } + +[models.agent] +api_url = "https://api.openai.com/v1" +api_key = "sk-agent" +model_name = "gpt-agent" + +[models.agent.request_params] +temperature = 0.3 +metadata = { source = "agent" } +response_format = { type = "json_object" } + +[models.historian] +model_name = "gpt-historian" + +[models.historian.request_params] +temperature = 0.1 +metadata = { source = "historian" } + +[models.embedding] +api_url = "https://api.openai.com/v1" +api_key = "sk-embed" +model_name = "text-embedding-3-small" + +[models.embedding.request_params] +encoding_format = "base64" +metadata = { source = "embed" } + +[models.rerank] +api_url = "https://api.openai.com/v1" +api_key = "sk-rerank" +model_name = "text-rerank-001" + +[models.rerank.request_params] +priority = "high" +""", + ) + + assert cfg.chat_model.request_params == { + "temperature": 0.2, + "metadata": {"source": "chat"}, + } + assert cfg.chat_model.pool is not None + assert cfg.chat_model.pool.models[0].request_params == { + "temperature": 0.6, + "metadata": {"source": "chat"}, + "provider": {"name": "pool"}, + } + assert cfg.security_model.request_params == cfg.chat_model.request_params + assert cfg.historian_model.request_params == { + "temperature": 0.1, + "metadata": {"source": "historian"}, + "response_format": {"type": "json_object"}, + } + assert cfg.embedding_model.request_params == { + "encoding_format": "base64", + "metadata": {"source": "embed"}, + } + assert cfg.rerank_model.request_params == {"priority": "high"} diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py new file mode 100644 index 00000000..7cabc398 --- /dev/null +++ b/tests/test_llm_request_params.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import Any, cast + +import httpx +import pytest +from openai import AsyncOpenAI + +from Undefined.ai.llm import ModelRequester +from Undefined.config.models import ChatModelConfig +from Undefined.token_usage_storage import TokenUsageStorage + + +class _FakeUsageStorage: + async def record(self, _usage: Any) -> None: + return None + + +class _FakeChatCompletionsAPI: + def __init__(self) -> None: + self.last_kwargs: dict[str, Any] | None = None + + async def create(self, **kwargs: Any) -> dict[str, Any]: + self.last_kwargs = dict(kwargs) + return { + "choices": [{"message": {"content": "ok"}}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + + +class _FakeChatClient: + def __init__(self) -> None: + self.chat = type("_Chat", (), {"completions": _FakeChatCompletionsAPI()})() + + +@pytest.mark.asyncio +async def test_request_uses_model_request_params_and_call_overrides( + caplog: pytest.LogCaptureFixture, +) -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeChatClient() + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + request_params={ + "temperature": 0.2, + "metadata": {"source": "config"}, + "stream": True, + "model": "should-be-ignored", + }, + ) + + await requester.request( + model_config=cfg, + messages=[{"role": "user", "content": "hello"}], + max_tokens=128, + call_type="chat", + temperature=0.7, + reasoning_effort="high", + ) + + assert fake_client.chat.completions.last_kwargs is not None + assert fake_client.chat.completions.last_kwargs["model"] == "gpt-test" + assert fake_client.chat.completions.last_kwargs["max_tokens"] == 128 + assert fake_client.chat.completions.last_kwargs["temperature"] == 0.7 + assert fake_client.chat.completions.last_kwargs["extra_body"] == { + "metadata": {"source": "config"}, + "reasoning_effort": "high", + } + assert ( + "ignored_keys=model,stream" in caplog.text + or "ignored_keys=stream,model" in caplog.text + ) + + await requester._http_client.aclose() diff --git a/tests/test_retrieval.py b/tests/test_retrieval.py index 5b7d558c..39bc65cc 100644 --- a/tests/test_retrieval.py +++ b/tests/test_retrieval.py @@ -106,6 +106,45 @@ async def test_embed_passes_dimensions_to_openai_sdk() -> None: assert fake_client.embeddings.last_kwargs["dimensions"] == 768 +@pytest.mark.asyncio +async def test_embed_passes_request_params_and_ignores_reserved_fields( + caplog: pytest.LogCaptureFixture, +) -> None: + fake_client = _FakeClient() + requester = RetrievalRequester( + get_openai_client=lambda _cfg: cast(AsyncOpenAI, fake_client), + response_to_dict=lambda response: { + "usage": getattr(response, "usage", {}), + }, + get_token_counter=lambda _model: cast(TokenCounter, _DummyCounter()), + record_usage=lambda **_kwargs: None, + ) + cfg = EmbeddingModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="text-embedding-3-small", + queue_interval_seconds=1.0, + dimensions=768, + request_params={ + "encoding_format": "base64", + "user": "tester", + "metadata": {"source": "embed"}, + "dimensions": 1024, + }, + ) + + await requester.embed(cfg, ["hello"]) + + assert fake_client.embeddings.last_kwargs is not None + assert fake_client.embeddings.last_kwargs["dimensions"] == 768 + assert fake_client.embeddings.last_kwargs["encoding_format"] == "base64" + assert fake_client.embeddings.last_kwargs["user"] == "tester" + assert fake_client.embeddings.last_kwargs["extra_body"] == { + "metadata": {"source": "embed"} + } + assert "ignored_keys=dimensions" in caplog.text + + class _FakeRerankClient: def __init__(self) -> None: self.last_post_path: str | None = None @@ -142,6 +181,41 @@ async def test_rerank_disables_return_documents_in_request() -> None: assert result == [{"index": 1, "relevance_score": 0.88, "document": "doc B"}] +@pytest.mark.asyncio +async def test_rerank_passes_request_params_and_ignores_reserved_fields( + caplog: pytest.LogCaptureFixture, +) -> None: + fake_client = _FakeRerankClient() + requester = RetrievalRequester( + get_openai_client=lambda _cfg: cast(AsyncOpenAI, fake_client), + response_to_dict=lambda response: cast(dict[str, Any], response), + get_token_counter=lambda _model: cast(TokenCounter, _DummyCounter()), + record_usage=lambda **_kwargs: None, + ) + cfg = RerankModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="text-rerank-001", + queue_interval_seconds=1.0, + request_params={ + "priority": "high", + "return_documents": True, + "query": "ignored", + }, + ) + + await requester.rerank(cfg, query="hello", documents=["doc A", "doc B"], top_n=1) + + assert fake_client.last_post_body is not None + assert fake_client.last_post_body["priority"] == "high" + assert fake_client.last_post_body["query"] == "hello" + assert fake_client.last_post_body["return_documents"] is False + assert ( + "ignored_keys=query,return_documents" in caplog.text + or "ignored_keys=return_documents,query" in caplog.text + ) + + @pytest.mark.asyncio async def test_rerank_falls_back_to_local_estimation_when_usage_is_zero() -> None: fake_client = _FakeRerankClient() diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 83773f9c..6d8665a7 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -58,3 +58,56 @@ def test_aot_not_rendered_as_string(self) -> None: rendered = render_toml(tomllib.loads(src)) assert '"{' not in rendered assert "[[items]]" in rendered + + def test_pool_model_request_params_roundtrip(self) -> None: + """模型池条目下的 request_params 嵌套结构应完整往返""" + src = """ +[models.chat.pool] +enabled = true + +[[models.chat.pool.models]] +model_name = "gpt-5" +api_url = "https://api.openai.com/v1" +api_key = "sk-a" + +[models.chat.pool.models.request_params] +temperature = 0.7 + +[models.chat.pool.models.request_params.metadata] +source = "webui" + +[[models.chat.pool.models.request_params.tags]] +name = "alpha" + +[[models.chat.pool.models.request_params.tags]] +name = "beta" +""" + data = _roundtrip(src) + model = data["models"]["chat"]["pool"]["models"][0] + params = model["request_params"] + assert params["temperature"] == 0.7 + assert params["metadata"]["source"] == "webui" + assert [item["name"] for item in params["tags"]] == ["alpha", "beta"] + + def test_nested_aot_child_tables_roundtrip(self) -> None: + """数组表项下的嵌套表与子数组表不能在渲染时丢失""" + src = """ +[[items]] +name = "root" + +[items.meta] +enabled = true + +[[items.meta.children]] +name = "child-a" + +[[items.meta.children]] +name = "child-b" +""" + data = _roundtrip(src) + item = data["items"][0] + assert item["meta"]["enabled"] is True + assert [child["name"] for child in item["meta"]["children"]] == [ + "child-a", + "child-b", + ] From 5cf9ed8ed9eb9dc9317788131bf7feb162ae4df2 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 15:42:34 +0800 Subject: [PATCH 07/21] feat(ai): support responses transport and reasoning config --- AGENTS.md | 43 +- config.toml.example | 75 +++- docs/configuration.md | 51 ++- docs/openapi.md | 14 +- src/Undefined/ai/client.py | 26 +- src/Undefined/ai/llm.py | 201 +++++++-- src/Undefined/ai/model_selector.py | 6 + src/Undefined/ai/parsing.py | 11 + src/Undefined/ai/transports/__init__.py | 23 + .../ai/transports/openai_transport.py | 412 ++++++++++++++++++ src/Undefined/api/app.py | 6 + src/Undefined/cognitive/historian.py | 9 + src/Undefined/config/loader.py | 157 ++++++- src/Undefined/config/models.py | 25 +- src/Undefined/injection_response_agent.py | 6 +- src/Undefined/services/ai_coordinator.py | 1 + src/Undefined/services/security.py | 6 +- src/Undefined/skills/agents/README.md | 15 +- src/Undefined/skills/agents/runner.py | 9 + src/Undefined/webui/static/js/config-form.js | 50 ++- tests/test_config_request_params.py | 40 +- tests/test_llm_request_params.py | 252 ++++++++++- tests/test_runtime_api_probes.py | 65 +++ tests/test_webui_render_toml.py | 8 + 24 files changed, 1406 insertions(+), 105 deletions(-) create mode 100644 src/Undefined/ai/transports/__init__.py create mode 100644 src/Undefined/ai/transports/openai_transport.py create mode 100644 tests/test_runtime_api_probes.py diff --git a/AGENTS.md b/AGENTS.md index 9d2bd030..cba03cf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,37 +1,28 @@ # Repository Guidelines ## Project Structure & Module Organization -Core Python code lives in `src/Undefined/`, organized by domain: `ai/`, `services/`, `cognitive/`, `skills/`, `webui/`, and `utils/`. -Tests are in `tests/` (pytest auto-discovers from this directory). -Runtime/config assets live in `res/`, `img/`, and `config/`. -Operational scripts are in `scripts/` (for example, `scripts/reembed_cognitive.py`). -Documentation is in `docs/`, and CI/release workflows are under `.github/workflows/`. +Primary code lives in `src/Undefined/`. Keep changes in the matching domain package: `ai/` for model orchestration, `cognitive/` for memory pipelines, `services/` for runtime services, `skills/` for tools/agents/commands, `webui/` for the management UI, and `utils/` for shared helpers. Tests live in `tests/`. Packaged assets and defaults live in `res/`, `img/`, and `config/`. Scripts are in `scripts/`; docs are in `docs/`. ## Build, Test, and Development Commands -- `uv sync --group dev -p 3.12`: install project + development dependencies. -- `uv run playwright install`: install browser runtime used by rendering/web features. -- `uv run Undefined` or `uv run Undefined-webui`: run bot or WebUI (choose one, do not run both). -- `uv run ruff format .`: auto-format code. -- `uv run ruff check .`: lint checks. -- `uv run mypy .`: strict type checking (project is configured with `mypy` strict mode). -- `uv run pytest tests/`: run full test suite. -- `uv build --wheel`: build distributable wheel (CI also validates packaged resources). +- `uv sync --group dev -p 3.12`: install the project with contributor tooling. +- `uv run playwright install`: install the browser runtime used by rendering and web-driven features. +- `cp config.toml.example config.toml`: create a local config before running the app. +- `uv run Undefined`: start the bot process directly. +- `uv run Undefined-webui`: start the WebUI manager. Do not run this alongside `Undefined`. +- `uv run ruff format .`: apply formatting. +- `uv run ruff check .`: run lint checks. +- `uv run mypy .`: run strict type checking. +- `uv run pytest tests/`: run the full test suite. +- `uv build --wheel`: build the distribution and verify packaged resources. ## Coding Style & Naming Conventions -Use 4-space indentation, type annotations, and `async`/`await` for I/O paths when applicable. -Follow Ruff formatting output; do not hand-tune style against formatter/linter. -Use `snake_case` for modules/functions/variables, `PascalCase` for classes, and `UPPER_SNAKE_CASE` for constants. -Keep modules focused by capability (for example, add chat command logic under `skills/commands/`). +Use 4-space indentation, Python type hints, and `async`/`await` for I/O paths. Let Ruff drive formatting instead of hand-formatting around it. Use `snake_case` for modules, functions, and variables; `PascalCase` for classes; `UPPER_SNAKE_CASE` for constants. Keep modules narrow in scope, and place new Skills content under the correct subtree such as `skills/tools/`, `skills/toolsets/`, or `skills/agents/`. ## Testing Guidelines -Frameworks: `pytest`, `pytest-asyncio` (`asyncio_mode = auto`). -Name tests as `test_*.py` and test functions as `test_*`. -Prefer targeted runs during development, e.g. `uv run pytest tests/test_parse_command.py -q`. -Before opening a PR, run format, lint, type check, and full tests locally. -No fixed coverage gate is enforced, but add tests for behavior changes and regressions. +The project uses `pytest` with `pytest-asyncio` (`asyncio_mode = auto`). Name files `tests/test_*.py` and test functions `test_*`. Prefer focused runs while iterating, for example `uv run pytest tests/test_parse_command.py -q`, then finish with the full suite. Add regression coverage for behavior changes in handlers, config loading, Skills discovery, and WebUI routes. ## Commit & Pull Request Guidelines -Use Conventional Commit style seen in history: `feat: ...`, `fix(scope): ...`, `chore(version): ...`, `refactor: ...`. -Release tooling groups commits by `feat`/`fix`, so use these prefixes accurately. -PRs should include: concise summary, linked issue (if any), test evidence (commands/results), and screenshots for WebUI changes. -Ensure CI passes (`ruff`, `mypy`, `pytest`, build checks) before requesting review. +Follow the commit style already used in history: `feat: ...`, `fix(scope): ...`, `chore(version): ...`. Keep subjects short and imperative. PRs should include a clear summary, linked issue when applicable, the commands you ran (`ruff`, `mypy`, `pytest`), and screenshots for WebUI changes. If you modify `res/`, `img/`, or `config.toml.example`, note that wheel packaging was checked with `uv build --wheel`. + +## Security & Configuration Tips +Treat `config.toml` as runtime state and avoid committing secrets. Prefer `config.toml.example` for documented defaults. Outputs under `data/` and `logs/` should stay out of feature commits unless the change explicitly targets fixtures or diagnostics. diff --git a/config.toml.example b/config.toml.example index 70fca10f..4bd8c629 100644 --- a/config.toml.example +++ b/config.toml.example @@ -89,6 +89,15 @@ max_tokens = 8192 # zh: 队列发车间隔(秒)。 # en: Queue interval (seconds). queue_interval_seconds = 1.0 +# zh: API 模式:传统 chat.completions 或新版 responses。 +# en: API mode: classic chat.completions or the newer responses API. +api_mode = "chat_completions" +# zh: 是否启用 reasoning.effort。 +# en: Enable reasoning.effort. +reasoning_enabled = false +# zh: reasoning.effort 档位:none / minimal / low / medium / high / xhigh。 +# en: reasoning.effort level: none / minimal / low / medium / high / xhigh. +reasoning_effort = "medium" # zh: 是否启用 thinking(思维链)。 # en: Enable thinking (reasoning). thinking_enabled = false @@ -100,10 +109,10 @@ thinking_budget_tokens = 20000 thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. -thinking_tool_call_compat = false +thinking_tool_call_compat = true -# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 -# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +# zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. [models.chat.request_params] # zh: 模型池配置(可选,支持多模型轮询/随机/用户指定)。 @@ -134,6 +143,15 @@ model_name = "" # zh: 队列发车间隔(秒)。 # en: Queue interval (seconds). queue_interval_seconds = 1.0 +# zh: API 模式:传统 chat.completions 或新版 responses。 +# en: API mode: classic chat.completions or the newer responses API. +api_mode = "chat_completions" +# zh: 是否启用 reasoning.effort。 +# en: Enable reasoning.effort. +reasoning_enabled = false +# zh: reasoning.effort 档位:none / minimal / low / medium / high / xhigh。 +# en: reasoning.effort level: none / minimal / low / medium / high / xhigh. +reasoning_effort = "medium" # zh: 是否启用 thinking(思维链)。 # en: Enable thinking (reasoning). thinking_enabled = false @@ -145,10 +163,10 @@ thinking_budget_tokens = 20000 thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. -thinking_tool_call_compat = false +thinking_tool_call_compat = true -# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 -# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +# zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. [models.vision.request_params] # zh: 安全模型配置(用于防注入检测和注入后回复生成)。 @@ -172,6 +190,15 @@ max_tokens = 100 # zh: 队列发车间隔(秒)。 # en: Queue interval (seconds). queue_interval_seconds = 1.0 +# zh: API 模式:传统 chat.completions 或新版 responses。 +# en: API mode: classic chat.completions or the newer responses API. +api_mode = "chat_completions" +# zh: 是否启用 reasoning.effort。 +# en: Enable reasoning.effort. +reasoning_enabled = false +# zh: reasoning.effort 档位:none / minimal / low / medium / high / xhigh。 +# en: reasoning.effort level: none / minimal / low / medium / high / xhigh. +reasoning_effort = "medium" # zh: 是否启用 thinking(思维链)。 # en: Enable thinking (reasoning). thinking_enabled = false @@ -183,10 +210,10 @@ thinking_budget_tokens = 0 thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. -thinking_tool_call_compat = false +thinking_tool_call_compat = true -# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 -# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +# zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. [models.security.request_params] # zh: Agent 模型配置(用于执行 agents)。 @@ -207,6 +234,15 @@ max_tokens = 4096 # zh: 队列发车间隔(秒)。 # en: Queue interval (seconds). queue_interval_seconds = 1.0 +# zh: API 模式:传统 chat.completions 或新版 responses。 +# en: API mode: classic chat.completions or the newer responses API. +api_mode = "chat_completions" +# zh: 是否启用 reasoning.effort。 +# en: Enable reasoning.effort. +reasoning_enabled = false +# zh: reasoning.effort 档位:none / minimal / low / medium / high / xhigh。 +# en: reasoning.effort level: none / minimal / low / medium / high / xhigh. +reasoning_effort = "medium" # zh: 是否启用 thinking(思维链)。 # en: Enable thinking (reasoning). thinking_enabled = false @@ -218,10 +254,10 @@ thinking_budget_tokens = 0 thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. -thinking_tool_call_compat = false +thinking_tool_call_compat = true -# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 -# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +# zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. [models.agent.request_params] # zh: Agent 模型池配置(可选,支持多模型轮询/随机/用户指定)。 @@ -255,6 +291,15 @@ max_tokens = 4096 # zh: 队列发车间隔(秒)。 # en: Queue interval (seconds). queue_interval_seconds = 1.0 +# zh: API 模式:传统 chat.completions 或新版 responses。 +# en: API mode: classic chat.completions or the newer responses API. +api_mode = "chat_completions" +# zh: 是否启用 reasoning.effort。 +# en: Enable reasoning.effort. +reasoning_enabled = false +# zh: reasoning.effort 档位:none / minimal / low / medium / high / xhigh。 +# en: reasoning.effort level: none / minimal / low / medium / high / xhigh. +reasoning_effort = "medium" # zh: 是否启用 thinking(思维链)。 # en: Enable thinking (reasoning). thinking_enabled = false @@ -266,10 +311,10 @@ thinking_budget_tokens = 0 thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. -thinking_tool_call_compat = false +thinking_tool_call_compat = true -# zh: 额外请求体参数(可选),可用于 temperature、reasoning_effort 或供应商私有参数。 -# en: Extra request-body params (optional), e.g. temperature, reasoning_effort, or vendor-specific fields. +# zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 +# en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. [models.historian.request_params] # zh: 嵌入模型配置(知识库语义检索使用)。 diff --git a/docs/configuration.md b/docs/configuration.md index 7659fb1e..4ed6a221 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -142,12 +142,27 @@ model_name = "gpt-4o-mini" | `model_name` | 模型名 | | `max_tokens` | 最大输出 token(vision 无此字段) | | `queue_interval_seconds` | 该模型请求队列发车间隔(秒) | -| `thinking_enabled` | 是否启用思维链参数 | +| `api_mode` | 请求模式:`chat_completions` 或 `responses` | +| `reasoning_enabled` | 是否启用 `reasoning.effort` | +| `reasoning_effort` | `reasoning.effort` 档位:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` | +| `thinking_enabled` | 是否启用旧式 `thinking` 参数 | | `thinking_budget_tokens` | thinking 预算 | -| `thinking_include_budget` | 是否发送 budget_tokens | -| `thinking_tool_call_compat` | Tool Calls 兼容模式(回传 reasoning_content) | +| `thinking_include_budget` | 是否发送 `budget_tokens` | +| `thinking_tool_call_compat` | Tool Calls 兼容模式:在多轮工具调用中回填 `reasoning_content`;默认 `true` | | `request_params` | 额外请求体参数(透传给模型 API,保留字段会忽略) | +请求模式说明: +- `api_mode="chat_completions"`:走 `client.chat.completions.create(...)` + - `thinking_enabled=true` 时发送旧式 `thinking` + - `reasoning_enabled=true` 时额外发送 `reasoning={ effort = ... }` +- `api_mode="responses"`:走 `client.responses.create(...)` + - 仅在 `reasoning_enabled=true` 时发送 `reasoning={ effort = ... }` + - 旧式 `thinking_*` 不会下发到 `responses` + +`request_params` 说明: +- 适合放 provider 私有请求体字段,例如 `metadata`、`temperature`、兼容网关扩展参数等。 +- 不要再通过 `request_params` 传 `reasoning` / `reasoning_effort` / `thinking`;这些现在有正式配置字段控制。 + 兼容字段(旧配置): - `models..deepseek_new_cot_support` - 若开启:等效默认 `thinking_include_budget=false` + `thinking_tool_call_compat=true` @@ -158,37 +173,51 @@ model_name = "gpt-4o-mini" 默认: - `max_tokens=8192` - `queue_interval_seconds=1.0`(`<=0` 回退 `1.0`) +- `api_mode="chat_completions"` +- `reasoning_enabled=false` +- `reasoning_effort="medium"` - `thinking_budget_tokens=20000` +- `thinking_tool_call_compat=true` 补充: -- 可通过 `[models.chat.request_params]` 传入 `temperature`、`response_format`、`reasoning_effort` 或兼容网关私有字段。 +- 若上游只对 `/v1/responses` 识别自定义参数,可将 `api_mode` 切到 `responses`。 +- `[models.chat.request_params]` 仍可放 `temperature`、`response_format` 或兼容网关私有字段,但不再用于 `reasoning` 配置。 ### 4.4.3 `[models.vision]` 视觉模型 默认: - `queue_interval_seconds=1.0`(`<=0` 回退 `1.0`) +- `api_mode="chat_completions"` +- `reasoning_enabled=false` +- `reasoning_effort="medium"` - `thinking_budget_tokens=20000` +- `thinking_tool_call_compat=true` ### 4.4.4 `[models.security]` 安全模型 字段: - 额外开关:`enabled=true` -- 默认:`max_tokens=100`,`thinking_budget_tokens=0` +- 默认:`max_tokens=100`、`api_mode="chat_completions"`、`reasoning_enabled=false`、`reasoning_effort="medium"`、`thinking_budget_tokens=0`、`thinking_tool_call_compat=true` 关键回退逻辑: - 若 `api_url/api_key/model_name` 任一缺失,会自动回退为 chat 模型(并告警)。 +- 回退时会继承 chat 的 `api_mode`、`reasoning_*` 与 `request_params`;旧 `thinking_*` 仍保持安全模型自身默认值。 ### 4.4.5 `[models.agent]` Agent 执行模型 默认: - `max_tokens=4096` - `queue_interval_seconds=1.0`(`<=0` 回退 `1.0`) +- `api_mode="chat_completions"` +- `reasoning_enabled=false` +- `reasoning_effort="medium"` +- `thinking_tool_call_compat=true` ### 4.4.6 `[models.historian]` 史官模型 - 用于认知记忆后台改写。 - 若整个节缺失或为空:完整回退到 `models.agent`。 -- 若部分字段缺失:逐项继承 agent 配置。 +- 若部分字段缺失:逐项继承 agent 配置,包括 `api_mode`、`reasoning_*`、`thinking_*` 与 `request_params`。 - `queue_interval_seconds<=0` 时回退到 agent 的间隔。 ### 4.4.7 模型池 @@ -209,7 +238,10 @@ model_name = "gpt-4o-mini" `models` 条目支持字段: - `model_name`(必填) -- `api_url`/`api_key`/`max_tokens`/`queue_interval_seconds`/`thinking_*`/`request_params`(可选,缺省继承主模型) +- `api_url` / `api_key` / `max_tokens` / `queue_interval_seconds` +- `api_mode` / `reasoning_enabled` / `reasoning_effort` +- `thinking_*` / `request_params` +- 以上可选字段缺省继承主模型 `request_params` 继承规则: - `[[models.chat.pool.models]]` 与 `[[models.agent.pool.models]]` 的 `request_params` 会与主模型按顶层键浅合并。 @@ -246,7 +278,8 @@ model_name = "gpt-4o-mini" `request_params` 说明: - 仅用于**请求体**字段,不包含 `api_key`、`base_url`、`timeout`、`extra_headers` 等 client 选项。 -- 聊天类保留字段:`model`、`messages`、`max_tokens`、`tools`、`tool_choice`、`stream`、`stream_options`。 +- 聊天类(`chat_completions`)保留字段:`model`、`messages`、`max_tokens`、`tools`、`tool_choice`、`stream`、`stream_options`、`reasoning`、`reasoning_effort`。 +- 聊天类(`responses`)保留字段:`model`、`input`、`instructions`、`max_output_tokens`、`tools`、`tool_choice`、`previous_response_id`、`stream`、`stream_options`、`thinking`、`reasoning`、`reasoning_effort`。 - embedding 保留字段:`model`、`input`、`dimensions`。 - rerank 保留字段:`model`、`query`、`documents`、`top_n`、`return_documents`。 @@ -638,7 +671,9 @@ model_name = "gpt-4o-mini" - `BOT_QQ` / `SUPERADMIN_QQ` - `ONEBOT_WS_URL` / `ONEBOT_TOKEN` - `CHAT_MODEL_API_URL` / `CHAT_MODEL_API_KEY` / `CHAT_MODEL_NAME` +- `CHAT_MODEL_API_MODE` / `CHAT_MODEL_REASONING_ENABLED` / `CHAT_MODEL_REASONING_EFFORT` - `VISION_MODEL_*` / `AGENT_MODEL_*` / `SECURITY_MODEL_*` +- 上述模型环境变量同样覆盖 `*_THINKING_ENABLED`、`*_THINKING_BUDGET_TOKENS`、`*_THINKING_TOOL_CALL_COMPAT` - `EMBEDDING_MODEL_*` / `RERANK_MODEL_*` - `SEARXNG_URL` - `HTTP_PROXY` / `HTTPS_PROXY` diff --git a/docs/openapi.md b/docs/openapi.md index f683f6a1..eca5d6df 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -79,7 +79,7 @@ curl http://127.0.0.1:8788/openapi.json | `cognitive` | `object` | 认知服务(`enabled`、`queue`) | | `api` | `object` | Runtime API 配置(`enabled`、`host`、`port`、`openapi_enabled`) | | `skills` | `object` | 技能统计,包含 `tools`、`agents`、`anthropic_skills` 三个子对象 | -| `models` | `object` | 模型配置,包含各模型的 `model_name`、脱敏 `api_url`、`thinking_enabled` | +| `models` | `object` | 模型配置;聊天类模型包含 `model_name`、脱敏 `api_url`、`api_mode`、`thinking_enabled`、`thinking_tool_call_compat`、`reasoning_enabled`、`reasoning_effort` | `skills` 子对象结构: @@ -93,11 +93,19 @@ curl http://127.0.0.1:8788/openapi.json } ``` -`models` 子对象结构(URL 经脱敏处理,仅保留 scheme + host): +`models` 子对象结构(URL 经脱敏处理,仅保留 scheme + host;embedding/rerank 仅返回 `model_name` 与 `api_url`): ```json { - "chat_model": { "model_name": "claude-sonnet-4-20250514", "api_url": "https://api.example.com/...", "thinking_enabled": false }, + "chat_model": { + "model_name": "claude-sonnet-4-20250514", + "api_url": "https://api.example.com/...", + "api_mode": "responses", + "thinking_enabled": false, + "thinking_tool_call_compat": true, + "reasoning_enabled": true, + "reasoning_effort": "high" + }, "embedding_model": { "model_name": "text-embedding-3-small", "api_url": "https://api.example.com/..." } } ``` diff --git a/src/Undefined/ai/client.py b/src/Undefined/ai/client.py index 113bd86b..86153ddc 100644 --- a/src/Undefined/ai/client.py +++ b/src/Undefined/ai/client.py @@ -320,6 +320,7 @@ async def submit_background_llm_call( tool_choice: Any = "auto", call_type: str = "background", max_tokens: int | None = None, + transport_state: dict[str, Any] | None = None, ) -> dict[str, Any]: """将 LLM 调用投递到后台队列,走统一发车间隔和 Token 统计。 无 queue_manager 时降级为直接调用。""" @@ -336,6 +337,7 @@ async def submit_background_llm_call( tool_choice=tool_choice, call_type=call_type, max_tokens=effective_max_tokens, + transport_state=transport_state, ) request_id = uuid4().hex event: asyncio.Event = asyncio.Event() @@ -351,6 +353,7 @@ async def submit_background_llm_call( "tool_choice": tool_choice, "call_type": call_type, "max_tokens": effective_max_tokens, + "transport_state": transport_state, }, model_name=model_name, ) @@ -642,10 +645,18 @@ async def request_model( call_type: str = "chat", tools: list[dict[str, Any]] | None = None, tool_choice: str = "auto", + transport_state: dict[str, Any] | None = None, **kwargs: Any, ) -> dict[str, Any]: + message_count_for_transport = len(messages) tools = self.tool_manager.maybe_merge_agent_tools(call_type, tools) - messages, tools = await self._maybe_prefetch_tools(messages, tools, call_type) + if not ( + isinstance(transport_state, dict) + and transport_state.get("previous_response_id") + ): + messages, tools = await self._maybe_prefetch_tools( + messages, tools, call_type + ) return await self._requester.request( model_config=model_config, messages=messages, @@ -653,6 +664,8 @@ async def request_model( call_type=call_type, tools=tools, tool_choice=tool_choice, + transport_state=transport_state, + message_count_for_transport=message_count_for_transport, **kwargs, ) @@ -859,6 +872,7 @@ async def ask( cot_compat = getattr(effective_chat_config, "thinking_tool_call_compat", False) cot_compat_logged = False cot_missing_logged = False + transport_state: dict[str, Any] | None = None while iteration < max_iterations: iteration += 1 @@ -872,6 +886,7 @@ async def ask( call_type="chat", tools=tools, tool_choice="auto", + transport_state=transport_state, ) tool_name_map = ( @@ -885,6 +900,15 @@ async def ask( str(k): str(v) for k, v in raw_api_to_internal.items() } + next_transport_state = ( + result.get("_transport_state") if isinstance(result, dict) else None + ) + transport_state = ( + next_transport_state + if isinstance(next_transport_state, dict) + else None + ) + choice = result.get("choices", [{}])[0] message = choice.get("message", {}) content: str = message.get("content") or "" diff --git a/src/Undefined/ai/llm.py b/src/Undefined/ai/llm.py index bd7fd86f..0b584846 100644 --- a/src/Undefined/ai/llm.py +++ b/src/Undefined/ai/llm.py @@ -21,6 +21,13 @@ ) from Undefined.ai.parsing import extract_choices_content +from Undefined.ai.transports import ( + API_MODE_RESPONSES, + build_responses_request_body, + get_api_mode, + get_reasoning_payload, + normalize_responses_result, +) from Undefined.ai.retrieval import RetrievalRequester from Undefined.ai.tokens import TokenCounter from Undefined.config import ( @@ -80,6 +87,26 @@ {"extra_headers", "extra_query", "extra_body", "timeout"} ) +_RESPONSES_KNOWN_FIELDS: set[str] = { + "model", + "input", + "instructions", + "max_output_tokens", + "metadata", + "previous_response_id", + "reasoning", + "temperature", + "top_p", + "tools", + "tool_choice", + "parallel_tool_calls", + "stream", + "stream_options", + "text", + "truncation", + "user", +} + _CHAT_COMPLETIONS_RESERVED_FIELDS: frozenset[str] = ( frozenset( { @@ -90,6 +117,28 @@ "tool_choice", "stream", "stream_options", + "reasoning", + "reasoning_effort", + } + ) + | _SDK_REQUEST_OPTION_FIELDS +) + +_RESPONSES_RESERVED_FIELDS: frozenset[str] = ( + frozenset( + { + "model", + "input", + "instructions", + "max_output_tokens", + "tools", + "tool_choice", + "previous_response_id", + "stream", + "stream_options", + "thinking", + "reasoning", + "reasoning_effort", } ) | _SDK_REQUEST_OPTION_FIELDS @@ -332,6 +381,19 @@ def _split_chat_completion_params( return known, extra +def _split_responses_params( + body: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any]]: + known: dict[str, Any] = {} + extra: dict[str, Any] = {} + for key, value in body.items(): + if key in _RESPONSES_KNOWN_FIELDS: + known[key] = value + else: + extra[key] = value + return known, extra + + def _is_deepseek_provider(model_config: ModelConfig) -> bool: model_name = str(getattr(model_config, "model_name", "") or "").lower() if model_name.startswith("deepseek"): @@ -778,9 +840,14 @@ def _build_effective_request_kwargs( getattr(model_config, "request_params", {}), overrides, ) + reserved_fields = ( + _RESPONSES_RESERVED_FIELDS + if get_api_mode(model_config) == API_MODE_RESPONSES + else _CHAT_COMPLETIONS_RESERVED_FIELDS + ) allowed, ignored = split_reserved_request_params( merged, - _CHAT_COMPLETIONS_RESERVED_FIELDS, + reserved_fields, ) _warn_ignored_request_params( call_type=call_type, @@ -821,11 +888,19 @@ async def request( call_type: str = "chat", tools: list[dict[str, Any]] | None = None, tool_choice: str = "auto", + transport_state: dict[str, Any] | None = None, + message_count_for_transport: int | None = None, **kwargs: Any, ) -> dict[str, Any]: """发送请求到模型 API。""" start_time = time.perf_counter() cot_compat = getattr(model_config, "thinking_tool_call_compat", False) + api_mode = get_api_mode(model_config) + transport_message_count = ( + message_count_for_transport + if message_count_for_transport is not None + else len(messages) + ) messages_for_api, tool_args_fixed = _sanitize_openai_messages_tool_arguments( messages ) @@ -835,32 +910,30 @@ async def request( tool_args_fixed, len(messages_for_api), ) - effective_kwargs = _build_effective_request_kwargs( - model_config, - call_type=call_type, - overrides=dict(kwargs), - ) - request_body = build_request_body( - model_config=model_config, - messages=messages_for_api, - max_tokens=max_tokens, - tools=tools, - tool_choice=tool_choice, - **effective_kwargs, - ) + tools_for_api = tools api_to_internal: dict[str, str] = {} internal_to_api: dict[str, str] = {} - if isinstance(request_body.get("tools"), list): + if isinstance(tools_for_api, list): + request_for_sanitize = { + "messages": messages_for_api, + "tools": list(tools_for_api), + } api_to_internal, internal_to_api = _sanitize_openai_tool_names_in_request( - request_body + request_for_sanitize ) - - if "tools" in request_body and isinstance(request_body.get("tools"), list): + raw_messages = request_for_sanitize.get("messages") + if isinstance(raw_messages, list): + messages_for_api = raw_messages + raw_tools = request_for_sanitize.get("tools") + if isinstance(raw_tools, list): + tools_for_api = raw_tools + + if isinstance(tools_for_api, list): sanitized_tools, changed_count, changes = _sanitize_openai_tools( - request_body["tools"] + tools_for_api ) - request_body["tools"] = sanitized_tools + tools_for_api = sanitized_tools if changed_count and logger.isEnabledFor(logging.INFO): logger.info( "[tools.sanitize] changed=%s total=%s truncate_enabled=%s max_desc_len=%s", @@ -882,13 +955,30 @@ async def request( change.get("new_preview"), ) + effective_kwargs = _build_effective_request_kwargs( + model_config, + call_type=call_type, + overrides=dict(kwargs), + ) + request_body = build_request_body( + model_config=model_config, + messages=messages_for_api, + max_tokens=max_tokens, + tools=tools_for_api, + tool_choice=tool_choice, + internal_to_api=internal_to_api, + transport_state=transport_state, + **effective_kwargs, + ) + try: if cot_compat and logger.isEnabledFor(logging.DEBUG): logger.debug( - "[思维链兼容] enabled=%s type=%s model=%s thinking_enabled=%s tools=%s messages=%s", + "[思维链兼容] enabled=%s type=%s model=%s api_mode=%s thinking_enabled=%s tools=%s messages=%s", cot_compat, call_type, model_config.model_name, + api_mode, getattr(model_config, "thinking_enabled", False), bool(tools), len(messages), @@ -896,19 +986,45 @@ async def request( if logger.isEnabledFor(logging.DEBUG): logger.debug( - "[API请求] type=%s model=%s url=%s max_tokens=%s tools=%s tool_choice=%s messages=%s", + "[API请求] type=%s model=%s api_mode=%s url=%s max_tokens=%s tools=%s tool_choice=%s messages=%s", call_type, model_config.model_name, + api_mode, model_config.api_url, max_tokens, - bool(tools), + bool(tools_for_api), tool_choice, len(messages), ) log_debug_json(logger, "[API请求体]", request_body) - result = await self._request_with_openai(model_config, request_body) - result = self._normalize_result(result) + raw_result = await self._request_with_openai(model_config, request_body) + if api_mode == API_MODE_RESPONSES: + result = normalize_responses_result( + raw_result, + api_to_internal if api_to_internal else None, + ) + response_id = str( + raw_result.get("id") or result.get("id") or "" + ).strip() + if response_id: + choice = result.get("choices", [{}])[0] + message = ( + choice.get("message", {}) if isinstance(choice, dict) else {} + ) + tool_calls = ( + message.get("tool_calls", []) + if isinstance(message, dict) + else [] + ) + result["_transport_state"] = { + "api_mode": api_mode, + "previous_response_id": response_id, + "tool_result_start_index": transport_message_count + + (1 if tool_calls else 0), + } + else: + result = self._normalize_result(raw_result) if api_to_internal: result["_tool_name_map"] = { "api_to_internal": api_to_internal, @@ -1038,6 +1154,12 @@ async def _request_with_openai( self, model_config: ModelConfig, request_body: dict[str, Any] ) -> dict[str, Any]: client = self._get_openai_client_for_model(model_config) + if get_api_mode(model_config) == API_MODE_RESPONSES: + params, extra_body = _split_responses_params(request_body) + if extra_body: + params["extra_body"] = extra_body + response = await client.responses.create(**params) + return self._response_to_dict(response) params, extra_body = _split_chat_completion_params(request_body) if extra_body: params["extra_body"] = extra_body @@ -1236,16 +1358,36 @@ def build_request_body( max_tokens: int, tools: list[dict[str, Any]] | None = None, tool_choice: str = "auto", + internal_to_api: dict[str, str] | None = None, + transport_state: dict[str, Any] | None = None, **kwargs: Any, ) -> dict[str, Any]: """构建 API 请求体。""" + api_mode = get_api_mode(model_config) + extra_kwargs: dict[str, Any] = dict(kwargs) + reasoning_payload = get_reasoning_payload(model_config) + + if api_mode == API_MODE_RESPONSES: + extra_kwargs.pop("thinking", None) + extra_kwargs.pop("reasoning", None) + extra_kwargs.pop("reasoning_effort", None) + return build_responses_request_body( + model_config, + messages, + max_tokens, + tools=tools, + tool_choice=tool_choice, + extra_kwargs=extra_kwargs, + internal_to_api=internal_to_api or {}, + transport_state=transport_state, + ) + body: dict[str, Any] = { "model": model_config.model_name, "messages": messages, "max_tokens": max_tokens, } - extra_kwargs: dict[str, Any] = dict(kwargs) if "thinking" in extra_kwargs: normalized = _normalize_thinking_override( extra_kwargs.get("thinking"), model_config @@ -1255,6 +1397,9 @@ def build_request_body( else: extra_kwargs["thinking"] = normalized + extra_kwargs.pop("reasoning", None) + extra_kwargs.pop("reasoning_effort", None) + if getattr(model_config, "thinking_enabled", False): thinking_param: dict[str, Any] = {"type": "enabled"} if getattr(model_config, "thinking_include_budget", True): @@ -1263,9 +1408,11 @@ def build_request_body( ) body["thinking"] = thinking_param + if reasoning_payload is not None: + body["reasoning"] = reasoning_payload + if tools: body["tools"] = tools - # thinking 模式不支持强制指定 tool_choice(specified),降级为 auto thinking_active = "thinking" in body if thinking_active and isinstance(tool_choice, dict): body["tool_choice"] = "auto" diff --git a/src/Undefined/ai/model_selector.py b/src/Undefined/ai/model_selector.py index d255ca6d..989da9bb 100644 --- a/src/Undefined/ai/model_selector.py +++ b/src/Undefined/ai/model_selector.py @@ -241,10 +241,13 @@ def _entry_to_chat_config( model_name=entry.model_name, max_tokens=entry.max_tokens, queue_interval_seconds=entry.queue_interval_seconds, + api_mode=entry.api_mode, thinking_enabled=entry.thinking_enabled, thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + reasoning_enabled=entry.reasoning_enabled, + reasoning_effort=entry.reasoning_effort, request_params=entry.request_params, pool=primary.pool, ) @@ -260,10 +263,13 @@ def _entry_to_agent_config( model_name=entry.model_name, max_tokens=entry.max_tokens, queue_interval_seconds=entry.queue_interval_seconds, + api_mode=entry.api_mode, thinking_enabled=entry.thinking_enabled, thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + reasoning_enabled=entry.reasoning_enabled, + reasoning_effort=entry.reasoning_effort, request_params=entry.request_params, pool=primary.pool, ) diff --git a/src/Undefined/ai/parsing.py b/src/Undefined/ai/parsing.py index d76a8edc..b25f26e0 100644 --- a/src/Undefined/ai/parsing.py +++ b/src/Undefined/ai/parsing.py @@ -47,6 +47,13 @@ def _extract_from_choice(choice: Any) -> str: return content or "" +def _extract_output_text(result: dict[str, Any]) -> str: + value = result.get("output_text") + if isinstance(value, str): + return value + return "" + + def _find_first_choice(result: dict[str, Any]) -> dict[str, Any] | None: """在响应中查找第一个选项。 @@ -114,6 +121,10 @@ def extract_choices_content(result: dict[str, Any]) -> str: """ logger.debug(f"提取 choices 内容,响应结构: {list(result.keys())}") + output_text = _extract_output_text(result) + if output_text: + return output_text + # 查找第一个选项 choice = _find_first_choice(result) diff --git a/src/Undefined/ai/transports/__init__.py b/src/Undefined/ai/transports/__init__.py new file mode 100644 index 00000000..9f33ccf8 --- /dev/null +++ b/src/Undefined/ai/transports/__init__.py @@ -0,0 +1,23 @@ +"""LLM transport helpers.""" + +from .openai_transport import ( + API_MODE_CHAT_COMPLETIONS, + API_MODE_RESPONSES, + build_responses_request_body, + get_api_mode, + get_reasoning_payload, + normalize_api_mode, + normalize_reasoning_effort, + normalize_responses_result, +) + +__all__ = [ + "API_MODE_CHAT_COMPLETIONS", + "API_MODE_RESPONSES", + "build_responses_request_body", + "get_api_mode", + "get_reasoning_payload", + "normalize_api_mode", + "normalize_reasoning_effort", + "normalize_responses_result", +] diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py new file mode 100644 index 00000000..70f2b809 --- /dev/null +++ b/src/Undefined/ai/transports/openai_transport.py @@ -0,0 +1,412 @@ +from __future__ import annotations + +import json +from typing import Any + +API_MODE_CHAT_COMPLETIONS = "chat_completions" +API_MODE_RESPONSES = "responses" +_VALID_API_MODES = {API_MODE_CHAT_COMPLETIONS, API_MODE_RESPONSES} +_VALID_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} + + +def normalize_api_mode(value: Any, default: str = API_MODE_CHAT_COMPLETIONS) -> str: + text = str(value or default).strip().lower() + if text not in _VALID_API_MODES: + return default + return text + + +def get_api_mode(model_config: Any) -> str: + return normalize_api_mode( + getattr(model_config, "api_mode", API_MODE_CHAT_COMPLETIONS) + ) + + +def normalize_reasoning_effort(value: Any, default: str = "medium") -> str: + text = str(value or default).strip().lower() + if text not in _VALID_REASONING_EFFORTS: + return default + return text + + +def get_reasoning_payload(model_config: Any) -> dict[str, Any] | None: + if not bool(getattr(model_config, "reasoning_enabled", False)): + return None + return { + "effort": normalize_reasoning_effort( + getattr(model_config, "reasoning_effort", "medium") + ) + } + + +def _stringify_content(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value + if isinstance(value, list): + chunks: list[str] = [] + for item in value: + if isinstance(item, str): + chunks.append(item) + continue + if not isinstance(item, dict): + continue + item_type = str(item.get("type", "")).strip().lower() + if item_type in { + "text", + "input_text", + "output_text", + "reasoning_text", + "summary_text", + }: + text = item.get("text") + if text is not None: + chunks.append(str(text)) + continue + if item_type == "refusal": + refusal = item.get("refusal") + if refusal is not None: + chunks.append(str(refusal)) + continue + if item_type == "image_url": + image_url = item.get("image_url") or {} + if isinstance(image_url, dict) and image_url.get("url"): + chunks.append(str(image_url.get("url"))) + continue + if item_type.endswith("_url"): + payload = item.get(item_type) or {} + if isinstance(payload, dict) and payload.get("url"): + chunks.append(str(payload.get("url"))) + continue + data = item.get("data") + if isinstance(data, dict) and data.get("text") is not None: + chunks.append(str(data.get("text"))) + return "\n".join(chunk for chunk in chunks if chunk) + if isinstance(value, dict): + if value.get("text") is not None: + return str(value.get("text")) + if value.get("content") is not None: + return _stringify_content(value.get("content")) + return json.dumps(value, ensure_ascii=False, default=str) + return str(value) + + +def _content_to_response_parts(content: Any) -> list[dict[str, Any]]: + if isinstance(content, str): + return [{"type": "input_text", "text": content}] + if not isinstance(content, list): + text = _stringify_content(content) + return [{"type": "input_text", "text": text}] if text else [] + + parts: list[dict[str, Any]] = [] + for item in content: + if isinstance(item, str): + if item: + parts.append({"type": "input_text", "text": item}) + continue + if not isinstance(item, dict): + continue + item_type = str(item.get("type", "")).strip().lower() + if item_type in {"text", "input_text"}: + text_value: Any | None = item.get("text") + if text_value is None: + data = item.get("data") + if isinstance(data, dict): + text_value = data.get("text") + if text_value is not None: + parts.append({"type": "input_text", "text": str(text_value)}) + continue + if item_type == "image_url": + image = item.get("image_url") or {} + if isinstance(image, dict) and image.get("url"): + parts.append( + { + "type": "input_image", + "image_url": str(image.get("url")), + "detail": str(image.get("detail") or "auto"), + } + ) + continue + if item_type.endswith("_url"): + image = item.get(item_type) or {} + if isinstance(image, dict) and image.get("url"): + parts.append( + { + "type": "input_image", + "image_url": str(image.get("url")), + "detail": str(image.get("detail") or "auto"), + } + ) + continue + if item_type == "input_image": + image_url = item.get("image_url") + if image_url: + parts.append( + { + "type": "input_image", + "image_url": str(image_url), + "detail": str(item.get("detail") or "auto"), + } + ) + continue + text = _stringify_content(item) + if text: + parts.append({"type": "input_text", "text": text}) + return parts + + +def _message_to_responses_input( + message: dict[str, Any], + internal_to_api: dict[str, str], +) -> list[dict[str, Any]]: + role = str(message.get("role", "")).strip().lower() + if not role: + return [] + + if role == "tool": + tool_call_id = str(message.get("tool_call_id", "")).strip() + if not tool_call_id: + return [] + return [ + { + "type": "function_call_output", + "call_id": tool_call_id, + "output": _stringify_content(message.get("content")), + } + ] + + items: list[dict[str, Any]] = [] + content_parts = _content_to_response_parts(message.get("content")) + if role in {"user", "assistant", "system", "developer"} and content_parts: + items.append( + { + "type": "message", + "role": role, + "content": content_parts, + } + ) + + if role == "assistant": + tool_calls = message.get("tool_calls") + if isinstance(tool_calls, list): + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + function = tool_call.get("function") or {} + if not isinstance(function, dict): + continue + name = str(function.get("name", "")).strip() + if not name: + continue + api_name = internal_to_api.get(name, name) + call_id = str( + tool_call.get("id") or tool_call.get("call_id") or "" + ).strip() + if not call_id: + continue + items.append( + { + "type": "function_call", + "call_id": call_id, + "name": api_name, + "arguments": str(function.get("arguments", "{}")), + } + ) + return items + + +def _messages_to_instruction_text(messages: list[dict[str, Any]]) -> str: + lines: list[str] = [] + for message in messages: + role = str(message.get("role", "")).strip().lower() + if role not in {"system", "developer"}: + continue + text = _stringify_content(message.get("content")) + if text: + lines.append(text) + return "\n\n".join(line for line in lines if line).strip() + + +def _messages_to_responses_input( + messages: list[dict[str, Any]], + internal_to_api: dict[str, str], + *, + include_system: bool, +) -> list[dict[str, Any]]: + items: list[dict[str, Any]] = [] + for message in messages: + role = str(message.get("role", "")).strip().lower() + if not include_system and role in {"system", "developer"}: + continue + if ( + not include_system + and role == "assistant" + and not message.get("content") + and not message.get("tool_calls") + ): + continue + items.extend(_message_to_responses_input(message, internal_to_api)) + return items + + +def _normalize_responses_tool_choice(tool_choice: Any) -> Any: + if not isinstance(tool_choice, dict): + return tool_choice + choice_type = str(tool_choice.get("type", "")).strip().lower() + if choice_type == "function": + function = tool_choice.get("function") + if isinstance(function, dict): + name = str(function.get("name", "")).strip() + if name: + return {"type": "function", "name": name} + return tool_choice + + +def build_responses_request_body( + model_config: Any, + messages: list[dict[str, Any]], + max_tokens: int, + *, + tools: list[dict[str, Any]] | None, + tool_choice: Any, + extra_kwargs: dict[str, Any], + internal_to_api: dict[str, str], + transport_state: dict[str, Any] | None, +) -> dict[str, Any]: + body: dict[str, Any] = { + "model": getattr(model_config, "model_name"), + "max_output_tokens": max_tokens, + } + reasoning = get_reasoning_payload(model_config) + if reasoning is not None: + body["reasoning"] = reasoning + if tools: + body["tools"] = tools + body["tool_choice"] = _normalize_responses_tool_choice(tool_choice) + + previous_response_id = "" + start_index = 0 + if isinstance(transport_state, dict): + previous_response_id = str( + transport_state.get("previous_response_id") or "" + ).strip() + try: + start_index = int(transport_state.get("tool_result_start_index") or 0) + except Exception: + start_index = 0 + if start_index < 0: + start_index = 0 + + if previous_response_id: + body["previous_response_id"] = previous_response_id + body["input"] = _messages_to_responses_input( + messages[start_index:], internal_to_api, include_system=True + ) + else: + instructions = _messages_to_instruction_text(messages) + if instructions: + body["instructions"] = instructions + body["input"] = _messages_to_responses_input( + messages, internal_to_api, include_system=False + ) + + body.update(extra_kwargs) + return body + + +def _collect_reasoning_text(output: list[dict[str, Any]]) -> str: + chunks: list[str] = [] + for item in output: + if not isinstance(item, dict) or item.get("type") != "reasoning": + continue + content = item.get("content") + if isinstance(content, list): + for part in content: + if not isinstance(part, dict): + continue + if part.get("type") == "reasoning_text" and part.get("text"): + chunks.append(str(part.get("text"))) + summary = item.get("summary") + if isinstance(summary, list): + for part in summary: + if not isinstance(part, dict): + continue + if part.get("type") == "summary_text" and part.get("text"): + chunks.append(str(part.get("text"))) + return "\n".join(chunk for chunk in chunks if chunk).strip() + + +def normalize_responses_result( + result: dict[str, Any], + api_to_internal: dict[str, str] | None = None, +) -> dict[str, Any]: + normalized = dict(result) + output_raw = result.get("output") + output = output_raw if isinstance(output_raw, list) else [] + + assistant_texts: list[str] = [] + tool_calls: list[dict[str, Any]] = [] + for item in output: + if not isinstance(item, dict): + continue + item_type = str(item.get("type", "")).strip().lower() + if ( + item_type == "message" + and str(item.get("role", "")).strip().lower() == "assistant" + ): + content = item.get("content") + if isinstance(content, list): + for part in content: + if not isinstance(part, dict): + continue + part_type = str(part.get("type", "")).strip().lower() + if part_type == "output_text" and part.get("text") is not None: + assistant_texts.append(str(part.get("text"))) + elif part_type == "refusal" and part.get("refusal") is not None: + assistant_texts.append(str(part.get("refusal"))) + elif item_type == "function_call": + function_name = str(item.get("name", "")).strip() + if api_to_internal: + function_name = api_to_internal.get(function_name, function_name) + call_id = str(item.get("call_id") or item.get("id") or "").strip() + if not function_name or not call_id: + continue + tool_calls.append( + { + "id": call_id, + "type": "function", + "function": { + "name": function_name, + "arguments": str(item.get("arguments", "{}")), + }, + } + ) + + message: dict[str, Any] = { + "role": "assistant", + "content": "\n".join(text for text in assistant_texts if text).strip(), + } + reasoning_content = _collect_reasoning_text(output) + if reasoning_content: + message["reasoning_content"] = reasoning_content + if tool_calls: + message["tool_calls"] = tool_calls + + normalized["choices"] = [ + { + "index": 0, + "message": message, + "finish_reason": "tool_calls" if tool_calls else "stop", + } + ] + + usage = result.get("usage") + if isinstance(usage, dict): + normalized["usage"] = { + "prompt_tokens": int(usage.get("input_tokens", 0) or 0), + "completion_tokens": int(usage.get("output_tokens", 0) or 0), + "total_tokens": int(usage.get("total_tokens", 0) or 0), + } + + return normalized diff --git a/src/Undefined/api/app.py b/src/Undefined/api/app.py index 30192a81..3cf11c45 100644 --- a/src/Undefined/api/app.py +++ b/src/Undefined/api/app.py @@ -523,7 +523,13 @@ async def _internal_probe_handler(self, request: web.Request) -> Response: models_info[label] = { "model_name": getattr(mcfg, "model_name", ""), "api_url": _mask_url(getattr(mcfg, "api_url", "")), + "api_mode": getattr(mcfg, "api_mode", "chat_completions"), "thinking_enabled": getattr(mcfg, "thinking_enabled", False), + "thinking_tool_call_compat": getattr( + mcfg, "thinking_tool_call_compat", True + ), + "reasoning_enabled": getattr(mcfg, "reasoning_enabled", False), + "reasoning_effort": getattr(mcfg, "reasoning_effort", "medium"), } for label in ("embedding_model", "rerank_model"): mcfg = getattr(cfg, label, None) diff --git a/src/Undefined/cognitive/historian.py b/src/Undefined/cognitive/historian.py index 105873ec..eb32ca77 100644 --- a/src/Undefined/cognitive/historian.py +++ b/src/Undefined/cognitive/historian.py @@ -858,6 +858,7 @@ async def _merge_profile_target( tools = [_READ_PROFILE_TOOL, _PROFILE_TOOL] result = False max_turns = 100 + transport_state: dict[str, Any] | None = None for turn in range(max_turns): response = await self._ai_client.submit_background_llm_call( @@ -866,6 +867,14 @@ async def _merge_profile_target( tools=tools, tool_choice="auto", call_type="historian_profile_merge", + transport_state=transport_state, + ) + + next_transport_state = ( + response.get("_transport_state") if isinstance(response, dict) else None + ) + transport_state = ( + next_transport_state if isinstance(next_transport_state, dict) else None ) choices = response.get("choices") or [] diff --git a/src/Undefined/config/loader.py b/src/Undefined/config/loader.py index c9081497..85431446 100644 --- a/src/Undefined/config/loader.py +++ b/src/Undefined/config/loader.py @@ -255,6 +255,10 @@ def _get_value( return None +_VALID_API_MODES = {"chat_completions", "responses"} +_VALID_REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} + + def _resolve_thinking_compat_flags( data: dict[str, Any], model_name: str, @@ -280,7 +284,7 @@ def _resolve_thinking_compat_flags( ) include_budget_default = True - tool_call_compat_default = False + tool_call_compat_default = True if legacy_value is not None: legacy_enabled = _coerce_bool(legacy_value, False) include_budget_default = not legacy_enabled @@ -292,6 +296,26 @@ def _resolve_thinking_compat_flags( ) +def _resolve_api_mode( + data: dict[str, Any], + model_name: str, + env_key: str, + default: str = "chat_completions", +) -> str: + raw_value = _get_value(data, ("models", model_name, "api_mode"), env_key) + value = _coerce_str(raw_value, default).strip().lower() + if value not in _VALID_API_MODES: + return default + return value + + +def _resolve_reasoning_effort(value: Any, default: str = "medium") -> str: + effort = _coerce_str(value, default).strip().lower() + if effort not in _VALID_REASONING_EFFORTS: + return default + return effort + + def load_local_admins() -> list[int]: """从本地配置文件加载动态管理员列表""" if not LOCAL_CONFIG_PATH.exists(): @@ -1510,6 +1534,16 @@ def _parse_model_pool( item.get("queue_interval_seconds"), primary_config.queue_interval_seconds, ), + api_mode=( + _coerce_str(item.get("api_mode"), primary_config.api_mode) + .strip() + .lower() + ) + if _coerce_str(item.get("api_mode"), primary_config.api_mode) + .strip() + .lower() + in _VALID_API_MODES + else primary_config.api_mode, thinking_enabled=_coerce_bool( item.get("thinking_enabled"), primary_config.thinking_enabled ), @@ -1525,6 +1559,14 @@ def _parse_model_pool( item.get("thinking_tool_call_compat"), primary_config.thinking_tool_call_compat, ), + reasoning_enabled=_coerce_bool( + item.get("reasoning_enabled"), + primary_config.reasoning_enabled, + ), + reasoning_effort=_resolve_reasoning_effort( + item.get("reasoning_effort"), + primary_config.reasoning_effort, + ), request_params=merge_request_params( primary_config.request_params, item.get("request_params"), @@ -1630,6 +1672,23 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: legacy_env_key="CHAT_MODEL_DEEPSEEK_NEW_COT_SUPPORT", ) ) + api_mode = _resolve_api_mode(data, "chat", "CHAT_MODEL_API_MODE") + reasoning_enabled = _coerce_bool( + _get_value( + data, + ("models", "chat", "reasoning_enabled"), + "CHAT_MODEL_REASONING_ENABLED", + ), + False, + ) + reasoning_effort = _resolve_reasoning_effort( + _get_value( + data, + ("models", "chat", "reasoning_effort"), + "CHAT_MODEL_REASONING_EFFORT", + ), + "medium", + ) config = ChatModelConfig( api_url=_coerce_str( _get_value(data, ("models", "chat", "api_url"), "CHAT_MODEL_API_URL"), @@ -1650,6 +1709,7 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: 8192, ), queue_interval_seconds=queue_interval_seconds, + api_mode=api_mode, thinking_enabled=_coerce_bool( _get_value( data, @@ -1668,6 +1728,8 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + reasoning_enabled=reasoning_enabled, + reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "chat"), ) config.pool = Config._parse_model_pool(data, "chat", config) @@ -1694,6 +1756,23 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: legacy_env_key="VISION_MODEL_DEEPSEEK_NEW_COT_SUPPORT", ) ) + api_mode = _resolve_api_mode(data, "vision", "VISION_MODEL_API_MODE") + reasoning_enabled = _coerce_bool( + _get_value( + data, + ("models", "vision", "reasoning_enabled"), + "VISION_MODEL_REASONING_ENABLED", + ), + False, + ) + reasoning_effort = _resolve_reasoning_effort( + _get_value( + data, + ("models", "vision", "reasoning_effort"), + "VISION_MODEL_REASONING_EFFORT", + ), + "medium", + ) return VisionModelConfig( api_url=_coerce_str( _get_value( @@ -1714,6 +1793,7 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: "", ), queue_interval_seconds=queue_interval_seconds, + api_mode=api_mode, thinking_enabled=_coerce_bool( _get_value( data, @@ -1732,6 +1812,8 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + reasoning_enabled=reasoning_enabled, + reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "vision"), ) @@ -1777,6 +1859,23 @@ def _parse_security_model_config( legacy_env_key="SECURITY_MODEL_DEEPSEEK_NEW_COT_SUPPORT", ) ) + api_mode = _resolve_api_mode(data, "security", "SECURITY_MODEL_API_MODE") + reasoning_enabled = _coerce_bool( + _get_value( + data, + ("models", "security", "reasoning_enabled"), + "SECURITY_MODEL_REASONING_ENABLED", + ), + False, + ) + reasoning_effort = _resolve_reasoning_effort( + _get_value( + data, + ("models", "security", "reasoning_effort"), + "SECURITY_MODEL_REASONING_EFFORT", + ), + "medium", + ) if api_url and api_key and model_name: return SecurityModelConfig( @@ -1792,6 +1891,7 @@ def _parse_security_model_config( 100, ), queue_interval_seconds=queue_interval_seconds, + api_mode=api_mode, thinking_enabled=_coerce_bool( _get_value( data, @@ -1810,6 +1910,8 @@ def _parse_security_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + reasoning_enabled=reasoning_enabled, + reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "security"), ) @@ -1820,10 +1922,13 @@ def _parse_security_model_config( model_name=chat_model.model_name, max_tokens=chat_model.max_tokens, queue_interval_seconds=chat_model.queue_interval_seconds, + api_mode=chat_model.api_mode, thinking_enabled=False, thinking_budget_tokens=0, thinking_include_budget=True, - thinking_tool_call_compat=False, + thinking_tool_call_compat=chat_model.thinking_tool_call_compat, + reasoning_enabled=chat_model.reasoning_enabled, + reasoning_effort=chat_model.reasoning_effort, request_params=merge_request_params(chat_model.request_params), ) @@ -1848,6 +1953,23 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: legacy_env_key="AGENT_MODEL_DEEPSEEK_NEW_COT_SUPPORT", ) ) + api_mode = _resolve_api_mode(data, "agent", "AGENT_MODEL_API_MODE") + reasoning_enabled = _coerce_bool( + _get_value( + data, + ("models", "agent", "reasoning_enabled"), + "AGENT_MODEL_REASONING_ENABLED", + ), + False, + ) + reasoning_effort = _resolve_reasoning_effort( + _get_value( + data, + ("models", "agent", "reasoning_effort"), + "AGENT_MODEL_REASONING_EFFORT", + ), + "medium", + ) config = AgentModelConfig( api_url=_coerce_str( _get_value(data, ("models", "agent", "api_url"), "AGENT_MODEL_API_URL"), @@ -1868,6 +1990,7 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: 4096, ), queue_interval_seconds=queue_interval_seconds, + api_mode=api_mode, thinking_enabled=_coerce_bool( _get_value( data, @@ -1886,6 +2009,8 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + reasoning_enabled=reasoning_enabled, + reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "agent"), ) config.pool = Config._parse_model_pool(data, "agent", config) @@ -1968,12 +2093,15 @@ def _log_debug_info( ] for name, cfg in configs: logger.debug( - "[配置] %s_model=%s api_url=%s api_key_set=%s thinking=%s cot_compat=%s", + "[配置] %s_model=%s api_url=%s api_key_set=%s api_mode=%s thinking=%s reasoning=%s/%s cot_compat=%s", name, cfg.model_name, cfg.api_url, bool(cfg.api_key), + getattr(cfg, "api_mode", "chat_completions"), cfg.thinking_enabled, + getattr(cfg, "reasoning_enabled", False), + getattr(cfg, "reasoning_effort", "medium"), getattr(cfg, "thinking_tool_call_compat", False), ) @@ -2020,12 +2148,19 @@ def _parse_historian_model_config( legacy_env_key="HISTORIAN_MODEL_DEEPSEEK_NEW_COT_SUPPORT", ) ) + api_mode = _resolve_api_mode( + {"models": {"historian": h}}, + "historian", + "HISTORIAN_MODEL_API_MODE", + fallback.api_mode, + ) return AgentModelConfig( api_url=_coerce_str(h.get("api_url"), fallback.api_url), api_key=_coerce_str(h.get("api_key"), fallback.api_key), model_name=_coerce_str(h.get("model_name"), fallback.model_name), max_tokens=_coerce_int(h.get("max_tokens"), fallback.max_tokens), queue_interval_seconds=queue_interval_seconds, + api_mode=api_mode, thinking_enabled=_coerce_bool( h.get("thinking_enabled"), fallback.thinking_enabled ), @@ -2034,6 +2169,22 @@ def _parse_historian_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + reasoning_enabled=_coerce_bool( + _get_value( + {"models": {"historian": h}}, + ("models", "historian", "reasoning_enabled"), + "HISTORIAN_MODEL_REASONING_ENABLED", + ), + fallback.reasoning_enabled, + ), + reasoning_effort=_resolve_reasoning_effort( + _get_value( + {"models": {"historian": h}}, + ("models", "historian", "reasoning_effort"), + "HISTORIAN_MODEL_REASONING_EFFORT", + ), + fallback.reasoning_effort, + ), request_params=merge_request_params( fallback.request_params, h.get("request_params"), diff --git a/src/Undefined/config/models.py b/src/Undefined/config/models.py index 0f0992e0..7269c70b 100644 --- a/src/Undefined/config/models.py +++ b/src/Undefined/config/models.py @@ -15,10 +15,13 @@ class ModelPoolEntry: model_name: str max_tokens: int queue_interval_seconds: float = 1.0 + api_mode: str = "chat_completions" thinking_enabled: bool = False thinking_budget_tokens: int = 0 thinking_include_budget: bool = True - thinking_tool_call_compat: bool = False + thinking_tool_call_compat: bool = True + reasoning_enabled: bool = False + reasoning_effort: str = "medium" request_params: dict[str, Any] = field(default_factory=dict) @@ -40,12 +43,15 @@ class ChatModelConfig: model_name: str max_tokens: int queue_interval_seconds: float = 1.0 + api_mode: str = "chat_completions" # 请求 API 模式 thinking_enabled: bool = False # 是否启用 thinking thinking_budget_tokens: int = 20000 # 思维预算 token 数量 thinking_include_budget: bool = True # 是否在请求中发送 budget_tokens thinking_tool_call_compat: bool = ( - False # 思维链 + 工具调用兼容(回传 reasoning_content) + True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + reasoning_enabled: bool = False # 是否启用 reasoning.effort + reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) pool: ModelPool | None = None # 模型池配置 @@ -58,12 +64,15 @@ class VisionModelConfig: api_key: str model_name: str queue_interval_seconds: float = 1.0 + api_mode: str = "chat_completions" # 请求 API 模式 thinking_enabled: bool = False # 是否启用 thinking thinking_budget_tokens: int = 20000 # 思维预算 token 数量 thinking_include_budget: bool = True # 是否在请求中发送 budget_tokens thinking_tool_call_compat: bool = ( - False # 思维链 + 工具调用兼容(回传 reasoning_content) + True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + reasoning_enabled: bool = False # 是否启用 reasoning.effort + reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -76,12 +85,15 @@ class SecurityModelConfig: model_name: str max_tokens: int queue_interval_seconds: float = 1.0 + api_mode: str = "chat_completions" # 请求 API 模式 thinking_enabled: bool = False # 是否启用 thinking thinking_budget_tokens: int = 0 # 思维预算 token 数量 thinking_include_budget: bool = True # 是否在请求中发送 budget_tokens thinking_tool_call_compat: bool = ( - False # 思维链 + 工具调用兼容(回传 reasoning_content) + True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + reasoning_enabled: bool = False # 是否启用 reasoning.effort + reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -120,12 +132,15 @@ class AgentModelConfig: model_name: str max_tokens: int = 4096 queue_interval_seconds: float = 1.0 + api_mode: str = "chat_completions" # 请求 API 模式 thinking_enabled: bool = False # 是否启用 thinking thinking_budget_tokens: int = 0 # 思维预算 token 数量 thinking_include_budget: bool = True # 是否在请求中发送 budget_tokens thinking_tool_call_compat: bool = ( - False # 思维链 + 工具调用兼容(回传 reasoning_content) + True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + reasoning_enabled: bool = False # 是否启用 reasoning.effort + reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) pool: ModelPool | None = None # 模型池配置 diff --git a/src/Undefined/injection_response_agent.py b/src/Undefined/injection_response_agent.py index 25df1420..f0f4e796 100644 --- a/src/Undefined/injection_response_agent.py +++ b/src/Undefined/injection_response_agent.py @@ -8,6 +8,7 @@ from typing import Any from Undefined.ai.llm import ModelRequester +from Undefined.ai.transports import API_MODE_CHAT_COMPLETIONS, get_api_mode from Undefined.ai.parsing import extract_choices_content from Undefined.config import SecurityModelConfig from Undefined.utils.resources import read_text_resource @@ -58,7 +59,10 @@ async def generate_response(self, user_message: str) -> str: start_time = time.perf_counter() try: request_kwargs: dict[str, Any] = {"temperature": 0.7} - if not self.security_config.thinking_enabled: + if ( + get_api_mode(self.security_config) == API_MODE_CHAT_COMPLETIONS + and not self.security_config.thinking_enabled + ): request_kwargs["thinking"] = {"enabled": False, "budget_tokens": 0} result = await self._requester.request( diff --git a/src/Undefined/services/ai_coordinator.py b/src/Undefined/services/ai_coordinator.py index 1493f163..3ddd78a6 100644 --- a/src/Undefined/services/ai_coordinator.py +++ b/src/Undefined/services/ai_coordinator.py @@ -516,6 +516,7 @@ async def _execute_background_llm_call(self, request: dict[str, Any]) -> None: call_type=request.get("call_type", "background"), max_tokens=request.get("max_tokens") or getattr(request["model_config"], "max_tokens", 4096), + transport_state=request.get("transport_state"), ) self.ai.set_llm_call_result(request_id, result) except Exception as exc: diff --git a/src/Undefined/services/security.py b/src/Undefined/services/security.py index 2d993f0e..50857fd3 100644 --- a/src/Undefined/services/security.py +++ b/src/Undefined/services/security.py @@ -8,6 +8,7 @@ from Undefined.injection_response_agent import InjectionResponseAgent from Undefined.token_usage_storage import TokenUsageStorage from Undefined.ai.llm import ModelRequester +from Undefined.ai.transports import API_MODE_CHAT_COMPLETIONS, get_api_mode from Undefined.ai.parsing import extract_choices_content from Undefined.utils.resources import read_text_resource from Undefined.utils.xml import escape_xml_text, escape_xml_attr @@ -103,7 +104,10 @@ async def detect_injection( # 使用安全模型配置进行注入检测 security_config = self.config.security_model request_kwargs: dict[str, Any] = {} - if not security_config.thinking_enabled: + if ( + get_api_mode(security_config) == API_MODE_CHAT_COMPLETIONS + and not security_config.thinking_enabled + ): request_kwargs["thinking"] = {"enabled": False, "budget_tokens": 0} result = await self._requester.request( diff --git a/src/Undefined/skills/agents/README.md b/src/Undefined/skills/agents/README.md index 96793af3..f04880d0 100644 --- a/src/Undefined/skills/agents/README.md +++ b/src/Undefined/skills/agents/README.md @@ -31,12 +31,21 @@ agent_name/ [models.agent] api_url = "https://api.openai.com/v1" api_key = "..." -model = "gpt-4o-mini" +model_name = "gpt-4o-mini" max_tokens = 4096 +api_mode = "chat_completions" +reasoning_enabled = false +reasoning_effort = "medium" thinking_enabled = false thinking_budget_tokens = 0 +thinking_tool_call_compat = true ``` +说明: +- `api_mode = "chat_completions"` 时,旧 `thinking_*` 仍按原逻辑生效;若开启 `reasoning_enabled`,也会额外发送 `reasoning.effort`。 +- `api_mode = "responses"` 时,Agent 的多轮工具调用会自动使用 `previous_response_id + function_call_output` 续轮;旧 `thinking_*` 不会发到 `responses`。 +- `thinking_tool_call_compat` 默认 `true`,会把 `reasoning_content` 回填到本地消息历史,便于日志、回放和兼容读取。 + 兼容的环境变量(会覆盖 `config.toml`): ```env @@ -44,8 +53,12 @@ AGENT_MODEL_API_URL= AGENT_MODEL_API_KEY= AGENT_MODEL_NAME= AGENT_MODEL_MAX_TOKENS=4096 +AGENT_MODEL_API_MODE=chat_completions +AGENT_MODEL_REASONING_ENABLED=false +AGENT_MODEL_REASONING_EFFORT=medium AGENT_MODEL_THINKING_ENABLED=false AGENT_MODEL_THINKING_BUDGET_TOKENS=0 +AGENT_MODEL_THINKING_TOOL_CALL_COMPAT=true ``` ## 介绍自动生成(推荐) diff --git a/src/Undefined/skills/agents/runner.py b/src/Undefined/skills/agents/runner.py index e9743bc9..7f5fcf82 100644 --- a/src/Undefined/skills/agents/runner.py +++ b/src/Undefined/skills/agents/runner.py @@ -102,6 +102,7 @@ async def run_agent_with_tools( if agent_history: messages.extend(agent_history) messages.append({"role": "user", "content": user_content}) + transport_state: dict[str, Any] | None = None for iteration in range(1, max_iterations + 1): logger.debug("[Agent:%s] iteration=%s", agent_name, iteration) @@ -113,6 +114,7 @@ async def run_agent_with_tools( call_type=f"agent:{agent_name}", tools=tools if tools else None, tool_choice="auto", + transport_state=transport_state, ) tool_name_map = ( @@ -127,6 +129,13 @@ async def run_agent_with_tools( for key, value in raw_api_to_internal.items() } + next_transport_state = ( + result.get("_transport_state") if isinstance(result, dict) else None + ) + transport_state = ( + next_transport_state if isinstance(next_transport_state, dict) else None + ) + choice: dict[str, Any] = result.get("choices", [{}])[0] message: dict[str, Any] = choice.get("message", {}) content: str = message.get("content") or "" diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index 3260f9eb..6fef53b8 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -219,6 +219,16 @@ function isLongText(value) { return typeof value === "string" && (value.length > 80 || value.includes("\n")); } +const FIELD_SELECT_OPTIONS = { + api_mode: ["chat_completions", "responses"], + reasoning_effort: ["none", "minimal", "low", "medium", "high", "xhigh"], +} + +function getFieldSelectOptions(path) { + const key = path.split(".").pop() + return FIELD_SELECT_OPTIONS[key] || null +} + function createField(path, val) { const group = document.createElement("div"); group.className = "form-group"; @@ -264,8 +274,20 @@ function createField(path, val) { const isArray = Array.isArray(val); const isNumber = typeof val === "number"; const isSecret = isSensitiveKey(path); + const selectOptions = getFieldSelectOptions(path); - if (isLongText(val)) { + if (selectOptions) { + input = document.createElement("select"); + input.className = "form-control config-input"; + input.dataset.valueType = "string"; + selectOptions.forEach(optionValue => { + const option = document.createElement("option"); + option.value = optionValue; + option.innerText = optionValue; + option.selected = String(val ?? "") === optionValue; + input.appendChild(option); + }); + } else if (isLongText(val)) { input = document.createElement("textarea"); input.className = "form-control form-textarea config-input"; input.value = val || ""; @@ -293,7 +315,11 @@ function createField(path, val) { input.dataset.path = path; group.appendChild(input); - input.oninput = () => scheduleAutoSave(); + if (selectOptions) { + input.onchange = () => autoSave(); + } else { + input.oninput = () => scheduleAutoSave(); + } } return group; } @@ -732,12 +758,26 @@ function createAotEntry(path, entry) { function buildAotTemplate(path, arr) { if (arr && arr.length > 0) { const template = buildEmptyStructuredValue(arr[0]) - if (AOT_PATHS.has(path) && !Object.prototype.hasOwnProperty.call(template, "request_params")) { - template.request_params = {} + if (AOT_PATHS.has(path)) { + if (!Object.prototype.hasOwnProperty.call(template, "request_params")) { + template.request_params = {} + } + if (!Object.prototype.hasOwnProperty.call(template, "api_mode")) { + template.api_mode = "chat_completions" + } + if (!Object.prototype.hasOwnProperty.call(template, "thinking_tool_call_compat")) { + template.thinking_tool_call_compat = true + } + if (!Object.prototype.hasOwnProperty.call(template, "reasoning_enabled")) { + template.reasoning_enabled = false + } + if (!Object.prototype.hasOwnProperty.call(template, "reasoning_effort")) { + template.reasoning_effort = "medium" + } } return template } - return { model_name: "", api_url: "", api_key: "", request_params: {} } + return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } } function createAotWidget(path, arr) { diff --git a/tests/test_config_request_params.py b/tests/test_config_request_params.py index f52384ac..c36afde8 100644 --- a/tests/test_config_request_params.py +++ b/tests/test_config_request_params.py @@ -10,7 +10,9 @@ def _load_config(path: Path, text: str) -> Config: return Config.load(path, strict=False) -def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: +def test_model_request_params_load_inherit_and_new_transport_fields( + tmp_path: Path, +) -> None: cfg = _load_config( tmp_path / "config.toml", """ @@ -21,6 +23,9 @@ def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: api_url = "https://api.openai.com/v1" api_key = "sk-chat" model_name = "gpt-chat" +api_mode = "responses" +reasoning_enabled = true +reasoning_effort = "high" [models.chat.request_params] temperature = 0.2 @@ -34,6 +39,9 @@ def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: model_name = "gpt-chat-b" api_url = "https://pool.example/v1" api_key = "sk-pool" +api_mode = "chat_completions" +reasoning_enabled = false +reasoning_effort = "low" [models.chat.pool.models.request_params] temperature = 0.6 @@ -43,6 +51,9 @@ def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: api_url = "https://api.openai.com/v1" api_key = "sk-agent" model_name = "gpt-agent" +api_mode = "responses" +reasoning_enabled = true +reasoning_effort = "minimal" [models.agent.request_params] temperature = 0.3 @@ -51,6 +62,8 @@ def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: [models.historian] model_name = "gpt-historian" +api_mode = "chat_completions" +reasoning_effort = "xhigh" [models.historian.request_params] temperature = 0.1 @@ -75,22 +88,47 @@ def test_model_request_params_load_and_inherit(tmp_path: Path) -> None: """, ) + assert cfg.chat_model.api_mode == "responses" + assert cfg.chat_model.reasoning_enabled is True + assert cfg.chat_model.reasoning_effort == "high" + assert cfg.chat_model.thinking_tool_call_compat is True assert cfg.chat_model.request_params == { "temperature": 0.2, "metadata": {"source": "chat"}, } + assert cfg.chat_model.pool is not None + assert cfg.chat_model.pool.models[0].api_mode == "chat_completions" + assert cfg.chat_model.pool.models[0].reasoning_enabled is False + assert cfg.chat_model.pool.models[0].reasoning_effort == "low" + assert cfg.chat_model.pool.models[0].thinking_tool_call_compat is True assert cfg.chat_model.pool.models[0].request_params == { "temperature": 0.6, "metadata": {"source": "chat"}, "provider": {"name": "pool"}, } + + assert cfg.security_model.api_mode == cfg.chat_model.api_mode + assert cfg.security_model.reasoning_enabled == cfg.chat_model.reasoning_enabled + assert cfg.security_model.reasoning_effort == cfg.chat_model.reasoning_effort + assert cfg.security_model.thinking_tool_call_compat is True assert cfg.security_model.request_params == cfg.chat_model.request_params + + assert cfg.agent_model.api_mode == "responses" + assert cfg.agent_model.reasoning_enabled is True + assert cfg.agent_model.reasoning_effort == "minimal" + assert cfg.agent_model.thinking_tool_call_compat is True + + assert cfg.historian_model.api_mode == "chat_completions" + assert cfg.historian_model.reasoning_enabled is True + assert cfg.historian_model.reasoning_effort == "xhigh" + assert cfg.historian_model.thinking_tool_call_compat is True assert cfg.historian_model.request_params == { "temperature": 0.1, "metadata": {"source": "historian"}, "response_format": {"type": "json_object"}, } + assert cfg.embedding_model.request_params == { "encoding_format": "base64", "metadata": {"source": "embed"}, diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index 7cabc398..ea2aed3c 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -7,6 +7,7 @@ from openai import AsyncOpenAI from Undefined.ai.llm import ModelRequester +from Undefined.ai.parsing import extract_choices_content from Undefined.config.models import ChatModelConfig from Undefined.token_usage_storage import TokenUsageStorage @@ -17,31 +18,58 @@ async def record(self, _usage: Any) -> None: class _FakeChatCompletionsAPI: - def __init__(self) -> None: + def __init__(self, responses: list[dict[str, Any]] | None = None) -> None: self.last_kwargs: dict[str, Any] | None = None + self.calls: list[dict[str, Any]] = [] + self._responses = list(responses or []) async def create(self, **kwargs: Any) -> dict[str, Any]: self.last_kwargs = dict(kwargs) + self.calls.append(dict(kwargs)) + if self._responses: + return self._responses.pop(0) return { "choices": [{"message": {"content": "ok"}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, } -class _FakeChatClient: - def __init__(self) -> None: - self.chat = type("_Chat", (), {"completions": _FakeChatCompletionsAPI()})() +class _FakeResponsesAPI: + def __init__(self, responses: list[dict[str, Any]] | None = None) -> None: + self.last_kwargs: dict[str, Any] | None = None + self.calls: list[dict[str, Any]] = [] + self._responses = list(responses or []) + + async def create(self, **kwargs: Any) -> dict[str, Any]: + self.last_kwargs = dict(kwargs) + self.calls.append(dict(kwargs)) + if not self._responses: + raise AssertionError("fake responses exhausted") + return self._responses.pop(0) + + +class _FakeClient: + def __init__( + self, + *, + chat_responses: list[dict[str, Any]] | None = None, + responses: list[dict[str, Any]] | None = None, + ) -> None: + self.chat = type( + "_Chat", (), {"completions": _FakeChatCompletionsAPI(chat_responses)} + )() + self.responses = _FakeResponsesAPI(responses) @pytest.mark.asyncio -async def test_request_uses_model_request_params_and_call_overrides( +async def test_chat_request_uses_model_reasoning_and_request_params( caplog: pytest.LogCaptureFixture, ) -> None: requester = ModelRequester( http_client=httpx.AsyncClient(), token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), ) - fake_client = _FakeChatClient() + fake_client = _FakeClient() setattr( requester, "_get_openai_client_for_model", @@ -52,6 +80,8 @@ async def test_request_uses_model_request_params_and_call_overrides( api_key="sk-test", model_name="gpt-test", max_tokens=512, + reasoning_enabled=True, + reasoning_effort="high", request_params={ "temperature": 0.2, "metadata": {"source": "config"}, @@ -66,7 +96,6 @@ async def test_request_uses_model_request_params_and_call_overrides( max_tokens=128, call_type="chat", temperature=0.7, - reasoning_effort="high", ) assert fake_client.chat.completions.last_kwargs is not None @@ -75,7 +104,7 @@ async def test_request_uses_model_request_params_and_call_overrides( assert fake_client.chat.completions.last_kwargs["temperature"] == 0.7 assert fake_client.chat.completions.last_kwargs["extra_body"] == { "metadata": {"source": "config"}, - "reasoning_effort": "high", + "reasoning": {"effort": "high"}, } assert ( "ignored_keys=model,stream" in caplog.text @@ -83,3 +112,210 @@ async def test_request_uses_model_request_params_and_call_overrides( ) await requester._http_client.aclose() + + +@pytest.mark.asyncio +async def test_responses_request_normalizes_tool_calls_and_usage() -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=[ + { + "id": "resp_1", + "output": [ + { + "type": "reasoning", + "id": "rs_1", + "summary": [{"type": "summary_text", "text": "先想一下"}], + }, + { + "type": "function_call", + "call_id": "call_1", + "name": "lookup", + "arguments": '{"query": "weather"}', + }, + ], + "usage": {"input_tokens": 11, "output_tokens": 7, "total_tokens": 18}, + } + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + reasoning_enabled=True, + reasoning_effort="low", + request_params={ + "metadata": {"source": "config"}, + "custom_flag": "on", + }, + ) + + result = await requester.request( + model_config=cfg, + messages=[{"role": "user", "content": "hello"}], + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + tool_choice=cast(Any, {"type": "function", "function": {"name": "lookup"}}), + thinking={"enabled": False, "budget_tokens": 0}, + ) + + assert fake_client.responses.last_kwargs is not None + assert fake_client.responses.last_kwargs["model"] == "gpt-test" + assert fake_client.responses.last_kwargs["max_output_tokens"] == 128 + assert fake_client.responses.last_kwargs["reasoning"] == {"effort": "low"} + assert fake_client.responses.last_kwargs["tool_choice"] == { + "type": "function", + "name": "lookup", + } + assert fake_client.responses.last_kwargs["metadata"] == {"source": "config"} + assert fake_client.responses.last_kwargs["extra_body"] == {"custom_flag": "on"} + assert "thinking" not in fake_client.responses.last_kwargs + + message = result["choices"][0]["message"] + assert message["tool_calls"][0]["id"] == "call_1" + assert message["tool_calls"][0]["function"]["name"] == "lookup" + assert message["reasoning_content"] == "先想一下" + assert result["usage"] == { + "prompt_tokens": 11, + "completion_tokens": 7, + "total_tokens": 18, + } + assert result["_transport_state"] == { + "api_mode": "responses", + "previous_response_id": "resp_1", + "tool_result_start_index": 2, + } + + await requester._http_client.aclose() + + +@pytest.mark.asyncio +async def test_responses_transport_state_uses_previous_response_id_and_tool_outputs() -> ( + None +): + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=[ + { + "id": "resp_1", + "output": [ + { + "type": "function_call", + "call_id": "call_1", + "name": "lookup", + "arguments": '{"query": "weather"}', + } + ], + "usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5}, + }, + { + "id": "resp_2", + "output": [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "all done"}], + } + ], + "usage": {"input_tokens": 4, "output_tokens": 3, "total_tokens": 7}, + }, + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + reasoning_enabled=True, + reasoning_effort="medium", + ) + tools = [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ] + + first = await requester.request( + model_config=cfg, + messages=[{"role": "user", "content": "hello"}], + max_tokens=128, + call_type="chat", + tools=tools, + ) + first_tool_calls = first["choices"][0]["message"]["tool_calls"] + messages = [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "", "tool_calls": first_tool_calls}, + {"role": "tool", "tool_call_id": "call_1", "content": "done"}, + ] + + second = await requester.request( + model_config=cfg, + messages=messages, + max_tokens=128, + call_type="chat", + tools=tools, + transport_state=first["_transport_state"], + message_count_for_transport=len(messages), + ) + + assert fake_client.responses.calls[1]["previous_response_id"] == "resp_1" + assert fake_client.responses.calls[1]["input"] == [ + { + "type": "function_call_output", + "call_id": "call_1", + "output": "done", + } + ] + assert extract_choices_content(second) == "all done" + assert second["usage"] == { + "prompt_tokens": 4, + "completion_tokens": 3, + "total_tokens": 7, + } + + await requester._http_client.aclose() diff --git a/tests/test_runtime_api_probes.py b/tests/test_runtime_api_probes.py new file mode 100644 index 00000000..da1f6783 --- /dev/null +++ b/tests/test_runtime_api_probes.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import json +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from aiohttp import web + +from Undefined.api import RuntimeAPIContext, RuntimeAPIServer + + +@pytest.mark.asyncio +async def test_runtime_internal_probe_includes_chat_model_transport_fields() -> None: + context = RuntimeAPIContext( + config_getter=lambda: SimpleNamespace( + api=SimpleNamespace( + enabled=True, + host="127.0.0.1", + port=8788, + auth_key="changeme", + openapi_enabled=True, + ), + chat_model=SimpleNamespace( + model_name="gpt-5.4", + api_url="https://api.example.com/v1", + api_mode="responses", + thinking_enabled=False, + thinking_tool_call_compat=True, + reasoning_enabled=True, + reasoning_effort="high", + ), + embedding_model=SimpleNamespace( + model_name="text-embedding-3-small", + api_url="https://api.example.com/v1", + ), + ), + onebot=SimpleNamespace(connection_status=lambda: {}), + ai=SimpleNamespace(memory_storage=None), + command_dispatcher=SimpleNamespace(), + queue_manager=SimpleNamespace(snapshot=lambda: {}), + history_manager=SimpleNamespace(), + ) + server = RuntimeAPIServer(context, host="127.0.0.1", port=8788) + + request = cast(web.Request, cast(Any, SimpleNamespace())) + response = await server._internal_probe_handler(request) + response_text = response.text + assert response_text is not None + payload = json.loads(response_text) + + chat_model = payload["models"]["chat_model"] + assert chat_model == { + "model_name": "gpt-5.4", + "api_url": "https://api.example.com/...", + "api_mode": "responses", + "thinking_enabled": False, + "thinking_tool_call_compat": True, + "reasoning_enabled": True, + "reasoning_effort": "high", + } + assert payload["models"]["embedding_model"] == { + "model_name": "text-embedding-3-small", + "api_url": "https://api.example.com/...", + } diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 6d8665a7..8d853c75 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -69,6 +69,10 @@ def test_pool_model_request_params_roundtrip(self) -> None: model_name = "gpt-5" api_url = "https://api.openai.com/v1" api_key = "sk-a" +api_mode = "responses" +thinking_tool_call_compat = true +reasoning_enabled = true +reasoning_effort = "high" [models.chat.pool.models.request_params] temperature = 0.7 @@ -84,6 +88,10 @@ def test_pool_model_request_params_roundtrip(self) -> None: """ data = _roundtrip(src) model = data["models"]["chat"]["pool"]["models"][0] + assert model["api_mode"] == "responses" + assert model["thinking_tool_call_compat"] is True + assert model["reasoning_enabled"] is True + assert model["reasoning_effort"] == "high" params = model["request_params"] assert params["temperature"] == 0.7 assert params["metadata"]["source"] == "webui" From 60d7b2ac05dce64d508975f54ac0b4136679db07 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 16:29:00 +0800 Subject: [PATCH 08/21] feat(webui): sync config template comments --- docs/configuration.md | 1 + scripts/README.md | 20 ++++ scripts/sync_config_template.py | 67 ++++++++++++ src/Undefined/webui/routes/_auth.py | 6 +- src/Undefined/webui/routes/_config.py | 30 +++++- src/Undefined/webui/static/js/config-form.js | 27 +++++ src/Undefined/webui/static/js/i18n.js | 10 ++ src/Undefined/webui/static/js/main.js | 3 + src/Undefined/webui/templates/index.html | 2 + src/Undefined/webui/utils/__init__.py | 12 ++- src/Undefined/webui/utils/comment.py | 21 ++-- src/Undefined/webui/utils/config_sync.py | 88 ++++++++++++++++ src/Undefined/webui/utils/toml_render.py | 104 +++++++++++++++---- tests/test_config_template_sync.py | 45 ++++++++ tests/test_webui_render_toml.py | 29 +++++- 15 files changed, 430 insertions(+), 35 deletions(-) create mode 100644 scripts/sync_config_template.py create mode 100644 src/Undefined/webui/utils/config_sync.py create mode 100644 tests/test_config_template_sync.py diff --git a/docs/configuration.md b/docs/configuration.md index 4ed6a221..68c5d81e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,6 +17,7 @@ - 主文件:`config.toml` - 示例模板:`config.toml.example` - 启动 WebUI 时,如 `config.toml` 不存在,会自动用示例模板生成。 +- 已有 `config.toml` 想补齐新增配置项/注释时,可用 WebUI 的“同步模板”按钮,或运行 `uv run python scripts/sync_config_template.py`。 ### 1.2 运行时本地文件 - `config.local.json`:运行时维护的本地管理员列表(如 `/addadmin`)。 diff --git a/scripts/README.md b/scripts/README.md index 12a9d319..f9410c3e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,6 +4,26 @@ ## 脚本列表 +### [`sync_config_template.py`](sync_config_template.py) — 同步配置模板与注释 + +保留当前 `config.toml` 已有配置值,同时把 `config.toml.example` 中新增的配置项、默认空表和双语注释同步回来。 + +```bash +# 直接写回 config.toml +uv run python scripts/sync_config_template.py + +# 仅预览,不落盘 +uv run python scripts/sync_config_template.py --dry-run + +# 输出同步后的完整内容 +uv run python scripts/sync_config_template.py --stdout +``` + +**适用场景**: +- 项目升级后想把新增配置项补齐到现有 `config.toml` +- 想恢复 `config.toml.example` 中的最新双语注释 +- 不想手工比对新旧配置文件 + ### [`reembed_cognitive.py`](reembed_cognitive.py) — 认知记忆向量库重嵌入 当更换嵌入模型(维度变化或模型升级)时,对 ChromaDB 中的 `cognitive_events` 和 `cognitive_profiles` 进行全量重嵌入。 diff --git a/scripts/sync_config_template.py b/scripts/sync_config_template.py new file mode 100644 index 00000000..cbf2a3ce --- /dev/null +++ b/scripts/sync_config_template.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from Undefined.webui.utils import sync_config_file + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="同步 config.toml.example 中的新配置项与注释到现有 config.toml,同时保留已有配置值。" + ) + parser.add_argument( + "--config", + type=Path, + default=Path("config.toml"), + help="目标配置文件路径,默认: config.toml", + ) + parser.add_argument( + "--example", + type=Path, + default=Path("config.toml.example"), + help="示例配置文件路径,默认: config.toml.example", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="仅预览同步结果,不写回文件。", + ) + parser.add_argument( + "--stdout", + action="store_true", + help="将同步后的完整 TOML 输出到标准输出。", + ) + return parser + + +def main() -> int: + args = build_parser().parse_args() + try: + result = sync_config_file( + config_path=args.config, + example_path=args.example, + write=not args.dry_run, + ) + except FileNotFoundError as exc: + print(f"[sync-config] 未找到示例配置:{exc}", file=sys.stderr) + return 1 + except ValueError as exc: + print(f"[sync-config] 配置解析失败:{exc}", file=sys.stderr) + return 1 + + action = "预览完成" if args.dry_run else "同步完成" + print(f"[sync-config] {action}: {args.config}") + print(f"[sync-config] 新增路径数量: {len(result.added_paths)}") + for path in result.added_paths: + print(f" + {path}") + + if args.stdout: + print("\n--- merged config.toml ---\n") + print(result.content, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/Undefined/webui/routes/_auth.py b/src/Undefined/webui/routes/_auth.py index cbe363e2..52689781 100644 --- a/src/Undefined/webui/routes/_auth.py +++ b/src/Undefined/webui/routes/_auth.py @@ -19,7 +19,7 @@ _record_login_failure, _clear_login_failures, ) -from ..utils import read_config_source, apply_patch, render_toml +from ..utils import read_config_source, apply_patch, load_comment_map, render_toml @routes.post("/api/login") @@ -171,7 +171,9 @@ async def password_handler(request: web.Request) -> Response: data_dict = {} patched = apply_patch(data_dict, {"webui.password": new_password}) - CONFIG_PATH.write_text(render_toml(patched), encoding="utf-8") + CONFIG_PATH.write_text( + render_toml(patched, comments=load_comment_map()), encoding="utf-8" + ) get_config_manager().reload() request.app["settings"] = load_webui_settings() get_session_store(request).clear() diff --git a/src/Undefined/webui/routes/_config.py b/src/Undefined/webui/routes/_config.py index 26770f05..e5128569 100644 --- a/src/Undefined/webui/routes/_config.py +++ b/src/Undefined/webui/routes/_config.py @@ -16,6 +16,7 @@ apply_patch, render_toml, sort_config, + sync_config_file, ) @@ -85,7 +86,9 @@ async def config_patch_handler(request: web.Request) -> Response: data = {} patched = apply_patch(data, patch) - CONFIG_PATH.write_text(render_toml(patched), encoding="utf-8") + CONFIG_PATH.write_text( + render_toml(patched, comments=load_comment_map()), encoding="utf-8" + ) get_config_manager().reload() validation_ok, validation_msg = validate_required_config() return web.json_response( @@ -95,3 +98,28 @@ async def config_patch_handler(request: web.Request) -> Response: "warning": None if validation_ok else validation_msg, } ) + + +@routes.post("/api/config/sync-template") +async def sync_config_template_handler(request: web.Request) -> Response: + if not check_auth(request): + return web.json_response({"error": "Unauthorized"}, status=401) + try: + result = sync_config_file() + get_config_manager().reload() + validation_ok, validation_msg = validate_required_config() + return web.json_response( + { + "success": True, + "message": "Synced", + "added_paths": result.added_paths, + "added_count": len(result.added_paths), + "warning": None if validation_ok else validation_msg, + } + ) + except FileNotFoundError as exc: + return web.json_response({"success": False, "error": str(exc)}, status=404) + except ValueError as exc: + return web.json_response({"success": False, "error": str(exc)}, status=400) + except Exception as exc: + return web.json_response({"success": False, "error": str(exc)}, status=500) diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index 6fef53b8..59c64350 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -890,6 +890,33 @@ async function autoSave() { } } +async function syncConfigTemplate(button) { + if (!confirm(t("config.sync_confirm"))) return; + setButtonLoading(button, true); + showSaveStatus("saving", t("config.syncing")); + try { + const res = await api("/api/config/sync-template", { method: "POST" }); + const data = await res.json(); + if (!data.success) { + showSaveStatus("error", t("config.save_error")); + showToast(`${t("common.error")}: ${data.error || t("config.sync_error")}`, "error", 5000); + return; + } + await loadConfig(); + showSaveStatus("saved", t("config.saved")); + if (data.warning) { + showToast(`${t("common.warning")}: ${data.warning}`, "warning", 5000); + } + const suffix = Number.isFinite(data.added_count) ? ` (+${data.added_count})` : ""; + showToast(`${t("config.sync_success")}${suffix}`, "info", 4000); + } catch (e) { + showSaveStatus("error", t("config.sync_error")); + showToast(`${t("common.error")}: ${e.message}`, "error", 5000); + } finally { + setButtonLoading(button, false); + } +} + async function resetConfig() { if (!confirm(t("config.reset_confirm"))) return; try { diff --git a/src/Undefined/webui/static/js/i18n.js b/src/Undefined/webui/static/js/i18n.js index 32092056..59c913e3 100644 --- a/src/Undefined/webui/static/js/i18n.js +++ b/src/Undefined/webui/static/js/i18n.js @@ -71,6 +71,11 @@ const I18N = { "config.save": "保存更改", "config.reset": "重置更改", "config.reset_confirm": "确定要撤销所有本地更改吗?这将从服务器重新加载配置。", + "config.sync_template": "同步模板", + "config.sync_confirm": "确定要把 config.toml.example 中的新配置项和注释同步到当前 config.toml 吗?现有配置值会保留。", + "config.syncing": "同步模板中...", + "config.sync_success": "模板同步完成", + "config.sync_error": "模板同步失败", "config.search_placeholder": "搜索配置...", "config.clear_search": "清除搜索", "config.expand_all": "全部展开", @@ -272,6 +277,11 @@ const I18N = { "config.save": "Save Changes", "config.reset": "Revert Changes", "config.reset_confirm": "Are you sure you want to revert all local changes? This will reload the configuration from the server.", + "config.sync_template": "Sync Template", + "config.sync_confirm": "Sync new settings and comments from config.toml.example into the current config.toml? Existing configured values will be kept.", + "config.syncing": "Syncing template...", + "config.sync_success": "Template sync completed", + "config.sync_error": "Template sync failed", "config.search_placeholder": "Search config...", "config.clear_search": "Clear search", "config.expand_all": "Expand all", diff --git a/src/Undefined/webui/static/js/main.js b/src/Undefined/webui/static/js/main.js index dedbc0b0..a9d036bd 100644 --- a/src/Undefined/webui/static/js/main.js +++ b/src/Undefined/webui/static/js/main.js @@ -140,6 +140,9 @@ async function init() { const resetBtn = get("btnResetConfig"); if (resetBtn) resetBtn.onclick = resetConfig; + const syncConfigBtn = get("btnSyncConfigTemplate"); + if (syncConfigBtn) syncConfigBtn.onclick = () => syncConfigTemplate(syncConfigBtn); + const refreshLogsBtn = get("btnRefreshLogs"); if (refreshLogsBtn) { refreshLogsBtn.onclick = async () => { diff --git a/src/Undefined/webui/templates/index.html b/src/Undefined/webui/templates/index.html index 0501c425..98ad198b 100644 --- a/src/Undefined/webui/templates/index.html +++ b/src/Undefined/webui/templates/index.html @@ -301,6 +301,8 @@

配置修改

data-i18n="config.expand_all">全部展开 + diff --git a/src/Undefined/webui/utils/__init__.py b/src/Undefined/webui/utils/__init__.py index 9d3c496b..9e4e2f1c 100644 --- a/src/Undefined/webui/utils/__init__.py +++ b/src/Undefined/webui/utils/__init__.py @@ -8,7 +8,13 @@ CONFIG_EXAMPLE_PATH, TomlData, ) -from .comment import CommentMap, parse_comment_map, load_comment_map +from .comment import ( + CommentMap, + parse_comment_map, + parse_comment_map_text, + load_comment_map, +) +from .config_sync import ConfigTemplateSyncResult, sync_config_file, sync_config_text from .toml_render import ( format_value, render_table, @@ -31,7 +37,11 @@ "TomlData", "CommentMap", "parse_comment_map", + "parse_comment_map_text", "load_comment_map", + "ConfigTemplateSyncResult", + "sync_config_file", + "sync_config_text", "format_value", "render_table", "render_toml", diff --git a/src/Undefined/webui/utils/comment.py b/src/Undefined/webui/utils/comment.py index df7fba71..53bc2353 100644 --- a/src/Undefined/webui/utils/comment.py +++ b/src/Undefined/webui/utils/comment.py @@ -34,17 +34,11 @@ def _normalize_comment_buffer(buffer: list[str]) -> dict[str, str]: return result -def parse_comment_map(path: Path) -> CommentMap: - if not path.exists(): - return {} +def parse_comment_map_text(text: str) -> CommentMap: comments: CommentMap = {} buffer: list[str] = [] current_section = "" - try: - lines = path.read_text(encoding="utf-8").splitlines() - except Exception: - return {} - for raw_line in lines: + for raw_line in text.splitlines(): line = raw_line.strip() if not line: buffer.clear() @@ -53,7 +47,7 @@ def parse_comment_map(path: Path) -> CommentMap: buffer.append(line.lstrip("#").strip()) continue if line.startswith("[") and line.endswith("]"): - section_name = line[1:-1].strip() + section_name = line.strip("[]").strip() if buffer: comment = _normalize_comment_buffer(buffer) if comment: @@ -74,6 +68,15 @@ def parse_comment_map(path: Path) -> CommentMap: return comments +def parse_comment_map(path: Path) -> CommentMap: + if not path.exists(): + return {} + try: + return parse_comment_map_text(path.read_text(encoding="utf-8")) + except Exception: + return {} + + def load_comment_map() -> CommentMap: example_path = _resolve_config_example_path() comments = parse_comment_map(example_path) if example_path else {} diff --git a/src/Undefined/webui/utils/config_sync.py b/src/Undefined/webui/utils/config_sync.py new file mode 100644 index 00000000..d79edc06 --- /dev/null +++ b/src/Undefined/webui/utils/config_sync.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import tomllib +from dataclasses import dataclass +from pathlib import Path +from Undefined.config.loader import CONFIG_PATH + +from .comment import CommentMap, parse_comment_map_text +from .config_io import CONFIG_EXAMPLE_PATH, _resolve_config_example_path +from .toml_render import TomlData, merge_defaults, render_toml + + +@dataclass(frozen=True) +class ConfigTemplateSyncResult: + content: str + added_paths: list[str] + comments: CommentMap + + +def _parse_toml_text(content: str, *, label: str) -> TomlData: + if not content.strip(): + return {} + try: + data = tomllib.loads(content) + except tomllib.TOMLDecodeError as exc: + raise ValueError(f"{label} TOML parse error: {exc}") from exc + if not isinstance(data, dict): + raise ValueError(f"{label} TOML root must be a table") + return data + + +def _collect_added_paths( + defaults: TomlData, current: TomlData, prefix: str = "" +) -> list[str]: + added: list[str] = [] + for key, default_value in defaults.items(): + path = f"{prefix}.{key}" if prefix else key + if key not in current: + added.append(path) + continue + current_value = current[key] + if isinstance(default_value, dict) and isinstance(current_value, dict): + added.extend(_collect_added_paths(default_value, current_value, path)) + return added + + +def _merge_comment_maps(current: CommentMap, example: CommentMap) -> CommentMap: + merged: CommentMap = dict(current) + for key, value in example.items(): + merged[key] = dict(value) + return merged + + +def sync_config_text(current_text: str, example_text: str) -> ConfigTemplateSyncResult: + current_data = _parse_toml_text(current_text, label="current config") + example_data = _parse_toml_text(example_text, label="config example") + added_paths = _collect_added_paths(example_data, current_data) + merged = merge_defaults(example_data, current_data) + comments = _merge_comment_maps( + parse_comment_map_text(current_text), + parse_comment_map_text(example_text), + ) + content = render_toml(merged, comments=comments) + return ConfigTemplateSyncResult( + content=content, + added_paths=added_paths, + comments=comments, + ) + + +def sync_config_file( + config_path: Path = CONFIG_PATH, + example_path: Path = CONFIG_EXAMPLE_PATH, + *, + write: bool = True, +) -> ConfigTemplateSyncResult: + resolved_example = _resolve_config_example_path(example_path) + if resolved_example is None or not resolved_example.exists(): + raise FileNotFoundError(f"config example not found: {example_path}") + + current_text = ( + config_path.read_text(encoding="utf-8") if config_path.exists() else "" + ) + example_text = resolved_example.read_text(encoding="utf-8") + result = sync_config_text(current_text, example_text) + if write: + config_path.write_text(result.content, encoding="utf-8") + return result diff --git a/src/Undefined/webui/utils/toml_render.py b/src/Undefined/webui/utils/toml_render.py index 532b0db6..ef396e91 100644 --- a/src/Undefined/webui/utils/toml_render.py +++ b/src/Undefined/webui/utils/toml_render.py @@ -3,6 +3,7 @@ from functools import lru_cache from typing import Any, cast +from .comment import CommentMap from .config_io import load_default_data TomlData = dict[str, Any] @@ -61,55 +62,116 @@ def _is_array_of_tables(value: Any) -> bool: ) -def _render_scalar_items(path: list[str], table: TomlData) -> list[str]: - return [ - f"{key} = {format_value(table[key])}" - for key in sorted_keys(table, path) - if not isinstance(table[key], dict) and not _is_array_of_tables(table[key]) - ] +def _comment_lines(comments: CommentMap | None, path_key: str) -> list[str]: + if not comments: + return [] + entry = comments.get(path_key) + if not entry: + return [] + lines: list[str] = [] + zh = str(entry.get("zh", "")).strip() + en = str(entry.get("en", "")).strip() + if zh: + lines.append(f"# zh: {zh}") + if en: + lines.append(f"# en: {en}") + if not lines: + for value in entry.values(): + text = str(value).strip() + if text: + lines.append(f"# {text}") + return lines + + +def _append_comment( + lines: list[str], comments: CommentMap | None, path_key: str +) -> None: + comment_lines = _comment_lines(comments, path_key) + if comment_lines: + lines.extend(comment_lines) + + +def _render_scalar_items( + path: list[str], table: TomlData, comments: CommentMap | None = None +) -> list[str]: + lines: list[str] = [] + path_prefix = ".".join(path) if path else "" + for key in sorted_keys(table, path): + value = table[key] + if isinstance(value, dict) or _is_array_of_tables(value): + continue + path_key = f"{path_prefix}.{key}" if path_prefix else key + _append_comment(lines, comments, path_key) + lines.append(f"{key} = {format_value(value)}") + return lines -def _render_nested_items(path: list[str], table: TomlData) -> list[str]: +def _render_nested_items( + path: list[str], table: TomlData, comments: CommentMap | None = None +) -> list[str]: lines: list[str] = [] for key in sorted_keys(table, path): value = table[key] if isinstance(value, dict): - lines.extend(render_table(path + [key], value)) + lines.extend(render_table(path + [key], value, comments=comments)) elif _is_array_of_tables(value): aot_path = path + [key] - for item in value: + for index, item in enumerate(value): lines.extend( - _render_array_of_tables_item(aot_path, cast(TomlData, item)) + _render_array_of_tables_item( + aot_path, + cast(TomlData, item), + comments=comments, + include_collection_comment=index == 0, + ) ) return lines -def _render_array_of_tables_item(path: list[str], table: TomlData) -> list[str]: - lines = [f"[[{'.'.join(path)}]]"] - lines.extend(_render_scalar_items(path, table)) +def _render_array_of_tables_item( + path: list[str], + table: TomlData, + *, + comments: CommentMap | None = None, + include_collection_comment: bool = False, +) -> list[str]: + lines: list[str] = [] + path_key = ".".join(path) + if include_collection_comment: + _append_comment(lines, comments, path_key) + lines.append(f"[[{path_key}]]") + lines.extend(_render_scalar_items(path, table, comments=comments)) lines.append("") - lines.extend(_render_nested_items(path, table)) + lines.extend(_render_nested_items(path, table, comments=comments)) return lines -def render_table(path: list[str], table: TomlData) -> list[str]: +def render_table( + path: list[str], table: TomlData, comments: CommentMap | None = None +) -> list[str]: lines: list[str] = [] - items = _render_scalar_items(path, table) - if items and path: + items = _render_scalar_items(path, table, comments=comments) + nested = _render_nested_items(path, table, comments=comments) + + if path: + _append_comment(lines, comments, ".".join(path)) lines.append(f"[{'.'.join(path)}]") lines.extend(items) lines.append("") - elif items: + lines.extend(nested) + return lines + + if items: lines.extend(items) lines.append("") - lines.extend(_render_nested_items(path, table)) + lines.extend(nested) return lines -def render_toml(data: TomlData) -> str: +def render_toml(data: TomlData, comments: CommentMap | None = None) -> str: if not data: return "" - return "\n".join(render_table([], data)).rstrip() + "\n" + return "\n".join(render_table([], data, comments=comments)).rstrip() + "\n" def apply_patch(data: TomlData, patch: dict[str, Any]) -> TomlData: diff --git a/tests/test_config_template_sync.py b/tests/test_config_template_sync.py new file mode 100644 index 00000000..c37f3b51 --- /dev/null +++ b/tests/test_config_template_sync.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import tomllib + +from Undefined.webui.utils import sync_config_text + + +def test_sync_config_text_preserves_values_and_adds_new_fields_with_comments() -> None: + current = """ +# custom comment should stay for unknown path +[core] +bot_qq = 123456 + +[custom] +flag = true +""" + example = """ +# zh: 机器人小号配置。 +# en: Bot account settings. +[core] +# zh: 机器人QQ号。 +# en: Bot QQ number. +bot_qq = 0 +# zh: 是否处理私聊消息。 +# en: Process private messages. +process_private_message = true + +# zh: 模板新增小节。 +# en: Newly added section from template. +[features] +# zh: 是否启用模型池。 +# en: Enable model pool. +pool_enabled = false +""" + + result = sync_config_text(current, example) + data = tomllib.loads(result.content) + + assert data["core"]["bot_qq"] == 123456 + assert data["core"]["process_private_message"] is True + assert data["features"]["pool_enabled"] is False + assert data["custom"]["flag"] is True + assert "# zh: 是否处理私聊消息。" in result.content + assert "# en: Enable model pool." in result.content + assert result.added_paths == ["core.process_private_message", "features"] diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 8d853c75..11c217ce 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -2,7 +2,7 @@ import tomllib -from Undefined.webui.utils import render_toml +from Undefined.webui.utils import parse_comment_map_text, render_toml def _roundtrip(toml_str: str) -> dict: # type: ignore[type-arg] @@ -119,3 +119,30 @@ def test_nested_aot_child_tables_roundtrip(self) -> None: "child-a", "child-b", ] + + def test_render_comments_and_empty_table(self) -> None: + """带注释的空表应被完整渲染出来""" + src = """ +# zh: 主配置 +# en: Main section +[models.chat.request_params] +""" + comments = parse_comment_map_text(src) + rendered = render_toml( + {"models": {"chat": {"request_params": {}}}}, comments=comments + ) + assert "# zh: 主配置" in rendered + assert "[models.chat.request_params]" in rendered + + def test_render_comments_before_scalar_keys(self) -> None: + """标量字段前应写出示例注释""" + comments = { + "core.bot_qq": { + "zh": "机器人QQ号。", + "en": "Bot QQ number.", + } + } + rendered = render_toml({"core": {"bot_qq": 1}}, comments=comments) + assert "# zh: 机器人QQ号。" in rendered + assert "# en: Bot QQ number." in rendered + assert "bot_qq = 1" in rendered From ab58e7e14d66023606b90e525b500e92d0b82a15 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 16:42:51 +0800 Subject: [PATCH 09/21] docs(scripts): document direct config sync entry --- docs/configuration.md | 2 +- scripts/README.md | 9 ++++++--- scripts/reembed_cognitive.py | 0 scripts/sync_config_template.py | 8 +++++++- 4 files changed, 14 insertions(+), 5 deletions(-) mode change 100644 => 100755 scripts/reembed_cognitive.py mode change 100644 => 100755 scripts/sync_config_template.py diff --git a/docs/configuration.md b/docs/configuration.md index 68c5d81e..0d2dd1b0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,7 +17,7 @@ - 主文件:`config.toml` - 示例模板:`config.toml.example` - 启动 WebUI 时,如 `config.toml` 不存在,会自动用示例模板生成。 -- 已有 `config.toml` 想补齐新增配置项/注释时,可用 WebUI 的“同步模板”按钮,或运行 `uv run python scripts/sync_config_template.py`。 +- 已有 `config.toml` 想补齐新增配置项/注释时,可用 WebUI 的“同步模板”按钮,或运行 `python scripts/sync_config_template.py`(也支持 `uv run python scripts/sync_config_template.py`)。 ### 1.2 运行时本地文件 - `config.local.json`:运行时维护的本地管理员列表(如 `/addadmin`)。 diff --git a/scripts/README.md b/scripts/README.md index f9410c3e..cae7f2dc 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -9,14 +9,17 @@ 保留当前 `config.toml` 已有配置值,同时把 `config.toml.example` 中新增的配置项、默认空表和双语注释同步回来。 ```bash -# 直接写回 config.toml +# 直接用 Python 运行 +python scripts/sync_config_template.py + +# 若希望复用项目虚拟环境,也可这样运行 uv run python scripts/sync_config_template.py # 仅预览,不落盘 -uv run python scripts/sync_config_template.py --dry-run +python scripts/sync_config_template.py --dry-run # 输出同步后的完整内容 -uv run python scripts/sync_config_template.py --stdout +python scripts/sync_config_template.py --stdout ``` **适用场景**: diff --git a/scripts/reembed_cognitive.py b/scripts/reembed_cognitive.py old mode 100644 new mode 100755 diff --git a/scripts/sync_config_template.py b/scripts/sync_config_template.py old mode 100644 new mode 100755 index cbf2a3ce..93cff1bb --- a/scripts/sync_config_template.py +++ b/scripts/sync_config_template.py @@ -1,10 +1,16 @@ +#!/usr/bin/env python3 from __future__ import annotations import argparse import sys from pathlib import Path -from Undefined.webui.utils import sync_config_file +_PROJECT_ROOT = Path(__file__).resolve().parent.parent +_SRC_DIR = _PROJECT_ROOT / "src" +if str(_SRC_DIR) not in sys.path: + sys.path.insert(0, str(_SRC_DIR)) + +from Undefined.webui.utils import sync_config_file # noqa: E402 def build_parser() -> argparse.ArgumentParser: From ed1f6de124be0556010a51d1972c2d94b95fab52 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 17:07:20 +0800 Subject: [PATCH 10/21] fix(webui): escape multiline toml strings --- src/Undefined/webui/utils/toml_render.py | 3 ++- tests/test_config_template_sync.py | 22 ++++++++++++++++++++++ tests/test_webui_render_toml.py | 19 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Undefined/webui/utils/toml_render.py b/src/Undefined/webui/utils/toml_render.py index ef396e91..7bf9e90f 100644 --- a/src/Undefined/webui/utils/toml_render.py +++ b/src/Undefined/webui/utils/toml_render.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from functools import lru_cache from typing import Any, cast @@ -48,7 +49,7 @@ def format_value(value: Any) -> str: if isinstance(value, (int, float)): return str(value) if isinstance(value, str): - return f'"{value.replace(chr(92), chr(92) * 2).replace(chr(34), chr(92) + chr(34))}"' + return json.dumps(value, ensure_ascii=False) if isinstance(value, list): return f"[{', '.join(format_value(item) for item in value)}]" return f'"{str(value)}"' diff --git a/tests/test_config_template_sync.py b/tests/test_config_template_sync.py index c37f3b51..9eed5cc6 100644 --- a/tests/test_config_template_sync.py +++ b/tests/test_config_template_sync.py @@ -43,3 +43,25 @@ def test_sync_config_text_preserves_values_and_adds_new_fields_with_comments() - assert "# zh: 是否处理私聊消息。" in result.content assert "# en: Enable model pool." in result.content assert result.added_paths == ["core.process_private_message", "features"] + + +def test_sync_config_text_preserves_multiline_string_values() -> None: + current = ''' +[models.embedding] +query_instruction = """第一行 +第二行 +第三行""" +''' + example = ''' +[models.embedding] +query_instruction = """默认第一行 +默认第二行""" +document_instruction = """文档前缀 +第二行""" +''' + result = sync_config_text(current, example) + parsed = tomllib.loads(result.content) + assert ( + parsed["models"]["embedding"]["query_instruction"] == "第一行\n第二行\n第三行" + ) + assert parsed["models"]["embedding"]["document_instruction"] == "文档前缀\n第二行" diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 11c217ce..60e4882c 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -146,3 +146,22 @@ def test_render_comments_before_scalar_keys(self) -> None: assert "# zh: 机器人QQ号。" in rendered assert "# en: Bot QQ number." in rendered assert "bot_qq = 1" in rendered + + def test_multiline_string_roundtrip(self) -> None: + """多行字符串应被渲染成合法 TOML,并可完整往返""" + original = "第一行\n第二行\n第三行" + rendered = render_toml( + {"models": {"embedding": {"query_instruction": original}}} + ) + parsed = tomllib.loads(rendered) + assert parsed["models"]["embedding"]["query_instruction"] == original + assert "\n" in rendered + + def test_multiline_string_with_quotes_and_backslashes_roundtrip(self) -> None: + """带引号与反斜杠的多行字符串也必须是合法 TOML""" + original = 'prefix "quoted"\npath\\to\\file\nline3' + rendered = render_toml( + {"models": {"embedding": {"document_instruction": original}}} + ) + parsed = tomllib.loads(rendered) + assert parsed["models"]["embedding"]["document_instruction"] == original From f6ca7cc8437899cb6b36a4472b5bc3b51ee337b6 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 17:38:01 +0800 Subject: [PATCH 11/21] fix(ai): normalize responses function tools --- .../ai/transports/openai_transport.py | 57 +++++++++- tests/test_llm_request_params.py | 100 +++++++++++++++++- 2 files changed, 152 insertions(+), 5 deletions(-) diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py index 70f2b809..da1b8c31 100644 --- a/src/Undefined/ai/transports/openai_transport.py +++ b/src/Undefined/ai/transports/openai_transport.py @@ -250,7 +250,51 @@ def _messages_to_responses_input( return items -def _normalize_responses_tool_choice(tool_choice: Any) -> Any: +def _normalize_responses_tools( + tools: list[dict[str, Any]], + internal_to_api: dict[str, str], +) -> list[dict[str, Any]]: + normalized_tools: list[dict[str, Any]] = [] + for tool in tools: + if not isinstance(tool, dict): + continue + tool_type = str(tool.get("type", "")).strip().lower() + if tool_type != "function": + normalized_tools.append(dict(tool)) + continue + + function = tool.get("function") + if not isinstance(function, dict): + normalized_tools.append(dict(tool)) + continue + + name = str(function.get("name", "")).strip() + if not name: + normalized_tools.append(dict(tool)) + continue + + api_name = internal_to_api.get(name, name) + normalized_tool: dict[str, Any] = { + "type": "function", + "name": api_name, + } + description = function.get("description") + if description is not None: + normalized_tool["description"] = description + parameters = function.get("parameters") + if parameters is not None: + normalized_tool["parameters"] = parameters + strict = function.get("strict") + if strict is not None: + normalized_tool["strict"] = strict + normalized_tools.append(normalized_tool) + return normalized_tools + + +def _normalize_responses_tool_choice( + tool_choice: Any, + internal_to_api: dict[str, str], +) -> Any: if not isinstance(tool_choice, dict): return tool_choice choice_type = str(tool_choice.get("type", "")).strip().lower() @@ -259,7 +303,10 @@ def _normalize_responses_tool_choice(tool_choice: Any) -> Any: if isinstance(function, dict): name = str(function.get("name", "")).strip() if name: - return {"type": "function", "name": name} + return { + "type": "function", + "name": internal_to_api.get(name, name), + } return tool_choice @@ -282,8 +329,10 @@ def build_responses_request_body( if reasoning is not None: body["reasoning"] = reasoning if tools: - body["tools"] = tools - body["tool_choice"] = _normalize_responses_tool_choice(tool_choice) + body["tools"] = _normalize_responses_tools(tools, internal_to_api) + body["tool_choice"] = _normalize_responses_tool_choice( + tool_choice, internal_to_api + ) previous_response_id = "" start_index = 0 diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index ea2aed3c..7bc31186 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -6,7 +6,7 @@ import pytest from openai import AsyncOpenAI -from Undefined.ai.llm import ModelRequester +from Undefined.ai.llm import ModelRequester, _encode_tool_name_for_api from Undefined.ai.parsing import extract_choices_content from Undefined.config.models import ChatModelConfig from Undefined.token_usage_storage import TokenUsageStorage @@ -187,6 +187,18 @@ async def test_responses_request_normalizes_tool_calls_and_usage() -> None: assert fake_client.responses.last_kwargs["model"] == "gpt-test" assert fake_client.responses.last_kwargs["max_output_tokens"] == 128 assert fake_client.responses.last_kwargs["reasoning"] == {"effort": "low"} + assert fake_client.responses.last_kwargs["tools"] == [ + { + "type": "function", + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + } + ] assert fake_client.responses.last_kwargs["tool_choice"] == { "type": "function", "name": "lookup", @@ -319,3 +331,89 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp } await requester._http_client.aclose() + + +@pytest.mark.asyncio +async def test_responses_tools_and_tool_choice_use_sanitized_api_names() -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + expected_api_name = _encode_tool_name_for_api("lookup.weather@bj") + fake_client = _FakeClient( + responses=[ + { + "id": "resp_1", + "output": [ + { + "type": "function_call", + "call_id": "call_1", + "name": expected_api_name, + "arguments": '{"query": "weather"}', + } + ], + "usage": {"input_tokens": 2, "output_tokens": 3, "total_tokens": 5}, + } + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + ) + + result = await requester.request( + model_config=cfg, + messages=[{"role": "user", "content": "hello"}], + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup.weather@bj", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + tool_choice=cast( + Any, + {"type": "function", "function": {"name": "lookup.weather@bj"}}, + ), + ) + + assert fake_client.responses.last_kwargs is not None + tool_payload = fake_client.responses.last_kwargs["tools"][0] + api_tool_name = tool_payload["name"] + assert tool_payload == { + "type": "function", + "name": api_tool_name, + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + } + assert api_tool_name != "lookup.weather@bj" + assert fake_client.responses.last_kwargs["tool_choice"] == { + "type": "function", + "name": api_tool_name, + } + assert result["choices"][0]["message"]["tool_calls"][0]["function"]["name"] == ( + "lookup.weather@bj" + ) + + await requester._http_client.aclose() From 4abef5b474faafe6fb2039a4ad0acf429aad0813 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 17:49:47 +0800 Subject: [PATCH 12/21] fix(ai): compat responses tool choice string --- .../ai/transports/openai_transport.py | 44 +++++++++++++------ tests/test_llm_request_params.py | 10 +---- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py index da1b8c31..3593774d 100644 --- a/src/Undefined/ai/transports/openai_transport.py +++ b/src/Undefined/ai/transports/openai_transport.py @@ -294,20 +294,25 @@ def _normalize_responses_tools( def _normalize_responses_tool_choice( tool_choice: Any, internal_to_api: dict[str, str], -) -> Any: +) -> tuple[Any, str | None]: if not isinstance(tool_choice, dict): - return tool_choice + return tool_choice, None choice_type = str(tool_choice.get("type", "")).strip().lower() - if choice_type == "function": - function = tool_choice.get("function") - if isinstance(function, dict): - name = str(function.get("name", "")).strip() - if name: - return { - "type": "function", - "name": internal_to_api.get(name, name), - } - return tool_choice + if choice_type != "function": + return tool_choice, None + + name = "" + function = tool_choice.get("function") + if isinstance(function, dict): + name = str(function.get("name", "")).strip() + elif tool_choice.get("name") is not None: + name = str(tool_choice.get("name", "")).strip() + + if not name: + return "auto", None + + api_name = internal_to_api.get(name, name) + return "required", api_name def build_responses_request_body( @@ -329,10 +334,21 @@ def build_responses_request_body( if reasoning is not None: body["reasoning"] = reasoning if tools: - body["tools"] = _normalize_responses_tools(tools, internal_to_api) - body["tool_choice"] = _normalize_responses_tool_choice( + normalized_tools = _normalize_responses_tools(tools, internal_to_api) + normalized_tool_choice, selected_tool_name = _normalize_responses_tool_choice( tool_choice, internal_to_api ) + if selected_tool_name: + filtered_tools = [ + tool + for tool in normalized_tools + if str(tool.get("type", "")).strip().lower() == "function" + and str(tool.get("name", "")).strip() == selected_tool_name + ] + if filtered_tools: + normalized_tools = filtered_tools + body["tools"] = normalized_tools + body["tool_choice"] = normalized_tool_choice previous_response_id = "" start_index = 0 diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index 7bc31186..e7ec29db 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -199,10 +199,7 @@ async def test_responses_request_normalizes_tool_calls_and_usage() -> None: }, } ] - assert fake_client.responses.last_kwargs["tool_choice"] == { - "type": "function", - "name": "lookup", - } + assert fake_client.responses.last_kwargs["tool_choice"] == "required" assert fake_client.responses.last_kwargs["metadata"] == {"source": "config"} assert fake_client.responses.last_kwargs["extra_body"] == {"custom_flag": "on"} assert "thinking" not in fake_client.responses.last_kwargs @@ -408,10 +405,7 @@ async def test_responses_tools_and_tool_choice_use_sanitized_api_names() -> None }, } assert api_tool_name != "lookup.weather@bj" - assert fake_client.responses.last_kwargs["tool_choice"] == { - "type": "function", - "name": api_tool_name, - } + assert fake_client.responses.last_kwargs["tool_choice"] == "required" assert result["choices"][0]["message"]["tool_calls"][0]["function"]["name"] == ( "lookup.weather@bj" ) From 456288b9de1e01a98b6123cb299f079c66276e58 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 18:25:14 +0800 Subject: [PATCH 13/21] feat(ai): add responses tool choice compat switch --- config.toml.example | 15 +++ docs/configuration.md | 20 ++-- docs/openapi.md | 3 +- .../ai/transports/openai_transport.py | 12 ++- src/Undefined/api/app.py | 3 + src/Undefined/config/loader.py | 47 +++++++++- src/Undefined/config/models.py | 13 +++ src/Undefined/webui/static/js/config-form.js | 5 +- tests/test_config_request_params.py | 8 ++ tests/test_llm_request_params.py | 94 ++++++++++++++++++- tests/test_runtime_api_probes.py | 2 + tests/test_webui_render_toml.py | 2 + 12 files changed, 211 insertions(+), 13 deletions(-) diff --git a/config.toml.example b/config.toml.example index 4bd8c629..2a661f6d 100644 --- a/config.toml.example +++ b/config.toml.example @@ -110,6 +110,9 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true +# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 +# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -164,6 +167,9 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true +# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 +# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -211,6 +217,9 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true +# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 +# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -255,6 +264,9 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true +# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 +# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -312,6 +324,9 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true +# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 +# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. diff --git a/docs/configuration.md b/docs/configuration.md index 0d2dd1b0..16836232 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,6 +150,7 @@ model_name = "gpt-4o-mini" | `thinking_budget_tokens` | thinking 预算 | | `thinking_include_budget` | 是否发送 `budget_tokens` | | `thinking_tool_call_compat` | Tool Calls 兼容模式:在多轮工具调用中回填 `reasoning_content`;默认 `true` | +| `responses_tool_choice_compat` | `responses` 下的 `tool_choice` 兼容开关:对不支持对象型 `tool_choice` 的代理降级为字符串 `"required"`;默认 `false` | | `request_params` | 额外请求体参数(透传给模型 API,保留字段会忽略) | 请求模式说明: @@ -158,6 +159,8 @@ model_name = "gpt-4o-mini" - `reasoning_enabled=true` 时额外发送 `reasoning={ effort = ... }` - `api_mode="responses"`:走 `client.responses.create(...)` - 仅在 `reasoning_enabled=true` 时发送 `reasoning={ effort = ... }` + - `responses_tool_choice_compat=true` 时,会把指定函数的 `tool_choice` 降级为字符串 `"required"`,并只保留目标工具,用于兼容部分不完整代理 + - 关闭时使用官方对象格式:`{"type":"function","name":"..."}` - 旧式 `thinking_*` 不会下发到 `responses` `request_params` 说明: @@ -179,6 +182,7 @@ model_name = "gpt-4o-mini" - `reasoning_effort="medium"` - `thinking_budget_tokens=20000` - `thinking_tool_call_compat=true` +- `responses_tool_choice_compat=false` 补充: - 若上游只对 `/v1/responses` 识别自定义参数,可将 `api_mode` 切到 `responses`。 @@ -193,16 +197,17 @@ model_name = "gpt-4o-mini" - `reasoning_effort="medium"` - `thinking_budget_tokens=20000` - `thinking_tool_call_compat=true` +- `responses_tool_choice_compat=false` ### 4.4.4 `[models.security]` 安全模型 字段: - 额外开关:`enabled=true` -- 默认:`max_tokens=100`、`api_mode="chat_completions"`、`reasoning_enabled=false`、`reasoning_effort="medium"`、`thinking_budget_tokens=0`、`thinking_tool_call_compat=true` +- 默认:`max_tokens=100`、`api_mode="chat_completions"`、`reasoning_enabled=false`、`reasoning_effort="medium"`、`thinking_budget_tokens=0`、`thinking_tool_call_compat=true`、`responses_tool_choice_compat=false` 关键回退逻辑: - 若 `api_url/api_key/model_name` 任一缺失,会自动回退为 chat 模型(并告警)。 -- 回退时会继承 chat 的 `api_mode`、`reasoning_*` 与 `request_params`;旧 `thinking_*` 仍保持安全模型自身默认值。 +- 回退时会继承 chat 的 `api_mode`、`reasoning_*`、`responses_tool_choice_compat` 与 `request_params`;旧 `thinking_*` 仍保持安全模型自身默认值。 ### 4.4.5 `[models.agent]` Agent 执行模型 @@ -213,12 +218,13 @@ model_name = "gpt-4o-mini" - `reasoning_enabled=false` - `reasoning_effort="medium"` - `thinking_tool_call_compat=true` +- `responses_tool_choice_compat=false` ### 4.4.6 `[models.historian]` 史官模型 - 用于认知记忆后台改写。 - 若整个节缺失或为空:完整回退到 `models.agent`。 -- 若部分字段缺失:逐项继承 agent 配置,包括 `api_mode`、`reasoning_*`、`thinking_*` 与 `request_params`。 +- 若部分字段缺失:逐项继承 agent 配置,包括 `api_mode`、`reasoning_*`、`thinking_*`、`responses_tool_choice_compat` 与 `request_params`。 - `queue_interval_seconds<=0` 时回退到 agent 的间隔。 ### 4.4.7 模型池 @@ -240,7 +246,7 @@ model_name = "gpt-4o-mini" `models` 条目支持字段: - `model_name`(必填) - `api_url` / `api_key` / `max_tokens` / `queue_interval_seconds` -- `api_mode` / `reasoning_enabled` / `reasoning_effort` +- `api_mode` / `reasoning_enabled` / `reasoning_effort` / `responses_tool_choice_compat` - `thinking_*` / `request_params` - 以上可选字段缺省继承主模型 @@ -672,9 +678,9 @@ model_name = "gpt-4o-mini" - `BOT_QQ` / `SUPERADMIN_QQ` - `ONEBOT_WS_URL` / `ONEBOT_TOKEN` - `CHAT_MODEL_API_URL` / `CHAT_MODEL_API_KEY` / `CHAT_MODEL_NAME` -- `CHAT_MODEL_API_MODE` / `CHAT_MODEL_REASONING_ENABLED` / `CHAT_MODEL_REASONING_EFFORT` -- `VISION_MODEL_*` / `AGENT_MODEL_*` / `SECURITY_MODEL_*` -- 上述模型环境变量同样覆盖 `*_THINKING_ENABLED`、`*_THINKING_BUDGET_TOKENS`、`*_THINKING_TOOL_CALL_COMPAT` +- `CHAT_MODEL_API_MODE` / `CHAT_MODEL_REASONING_ENABLED` / `CHAT_MODEL_REASONING_EFFORT` / `CHAT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT` +- `VISION_MODEL_*` / `AGENT_MODEL_*` / `SECURITY_MODEL_*` / `HISTORIAN_MODEL_*` +- 上述模型环境变量同样覆盖 `*_THINKING_ENABLED`、`*_THINKING_BUDGET_TOKENS`、`*_THINKING_TOOL_CALL_COMPAT`、`*_RESPONSES_TOOL_CHOICE_COMPAT` - `EMBEDDING_MODEL_*` / `RERANK_MODEL_*` - `SEARXNG_URL` - `HTTP_PROXY` / `HTTPS_PROXY` diff --git a/docs/openapi.md b/docs/openapi.md index eca5d6df..5ea73def 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -79,7 +79,7 @@ curl http://127.0.0.1:8788/openapi.json | `cognitive` | `object` | 认知服务(`enabled`、`queue`) | | `api` | `object` | Runtime API 配置(`enabled`、`host`、`port`、`openapi_enabled`) | | `skills` | `object` | 技能统计,包含 `tools`、`agents`、`anthropic_skills` 三个子对象 | -| `models` | `object` | 模型配置;聊天类模型包含 `model_name`、脱敏 `api_url`、`api_mode`、`thinking_enabled`、`thinking_tool_call_compat`、`reasoning_enabled`、`reasoning_effort` | +| `models` | `object` | 模型配置;聊天类模型包含 `model_name`、脱敏 `api_url`、`api_mode`、`thinking_enabled`、`thinking_tool_call_compat`、`responses_tool_choice_compat`、`reasoning_enabled`、`reasoning_effort` | `skills` 子对象结构: @@ -103,6 +103,7 @@ curl http://127.0.0.1:8788/openapi.json "api_mode": "responses", "thinking_enabled": false, "thinking_tool_call_compat": true, + "responses_tool_choice_compat": false, "reasoning_enabled": true, "reasoning_effort": "high" }, diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py index 3593774d..10cdc7e1 100644 --- a/src/Undefined/ai/transports/openai_transport.py +++ b/src/Undefined/ai/transports/openai_transport.py @@ -294,6 +294,8 @@ def _normalize_responses_tools( def _normalize_responses_tool_choice( tool_choice: Any, internal_to_api: dict[str, str], + *, + compat_mode: bool = False, ) -> tuple[Any, str | None]: if not isinstance(tool_choice, dict): return tool_choice, None @@ -312,7 +314,9 @@ def _normalize_responses_tool_choice( return "auto", None api_name = internal_to_api.get(name, name) - return "required", api_name + if compat_mode: + return "required", api_name + return {"type": "function", "name": api_name}, None def build_responses_request_body( @@ -336,7 +340,11 @@ def build_responses_request_body( if tools: normalized_tools = _normalize_responses_tools(tools, internal_to_api) normalized_tool_choice, selected_tool_name = _normalize_responses_tool_choice( - tool_choice, internal_to_api + tool_choice, + internal_to_api, + compat_mode=bool( + getattr(model_config, "responses_tool_choice_compat", False) + ), ) if selected_tool_name: filtered_tools = [ diff --git a/src/Undefined/api/app.py b/src/Undefined/api/app.py index 3cf11c45..09618ae8 100644 --- a/src/Undefined/api/app.py +++ b/src/Undefined/api/app.py @@ -528,6 +528,9 @@ async def _internal_probe_handler(self, request: web.Request) -> Response: "thinking_tool_call_compat": getattr( mcfg, "thinking_tool_call_compat", True ), + "responses_tool_choice_compat": getattr( + mcfg, "responses_tool_choice_compat", False + ), "reasoning_enabled": getattr(mcfg, "reasoning_enabled", False), "reasoning_effort": getattr(mcfg, "reasoning_effort", "medium"), } diff --git a/src/Undefined/config/loader.py b/src/Undefined/config/loader.py index 85431446..0548c1f8 100644 --- a/src/Undefined/config/loader.py +++ b/src/Undefined/config/loader.py @@ -316,6 +316,22 @@ def _resolve_reasoning_effort(value: Any, default: str = "medium") -> str: return effort +def _resolve_responses_tool_choice_compat( + data: dict[str, Any], + model_name: str, + env_key: str, + default: bool = False, +) -> bool: + return _coerce_bool( + _get_value( + data, + ("models", model_name, "responses_tool_choice_compat"), + env_key, + ), + default, + ) + + def load_local_admins() -> list[int]: """从本地配置文件加载动态管理员列表""" if not LOCAL_CONFIG_PATH.exists(): @@ -1559,6 +1575,10 @@ def _parse_model_pool( item.get("thinking_tool_call_compat"), primary_config.thinking_tool_call_compat, ), + responses_tool_choice_compat=_coerce_bool( + item.get("responses_tool_choice_compat"), + primary_config.responses_tool_choice_compat, + ), reasoning_enabled=_coerce_bool( item.get("reasoning_enabled"), primary_config.reasoning_enabled, @@ -1673,6 +1693,9 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: ) ) api_mode = _resolve_api_mode(data, "chat", "CHAT_MODEL_API_MODE") + responses_tool_choice_compat = _resolve_responses_tool_choice_compat( + data, "chat", "CHAT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1728,6 +1751,7 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + responses_tool_choice_compat=responses_tool_choice_compat, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "chat"), @@ -1757,6 +1781,9 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: ) ) api_mode = _resolve_api_mode(data, "vision", "VISION_MODEL_API_MODE") + responses_tool_choice_compat = _resolve_responses_tool_choice_compat( + data, "vision", "VISION_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1812,6 +1839,7 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + responses_tool_choice_compat=responses_tool_choice_compat, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "vision"), @@ -1860,6 +1888,9 @@ def _parse_security_model_config( ) ) api_mode = _resolve_api_mode(data, "security", "SECURITY_MODEL_API_MODE") + responses_tool_choice_compat = _resolve_responses_tool_choice_compat( + data, "security", "SECURITY_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1910,6 +1941,7 @@ def _parse_security_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + responses_tool_choice_compat=responses_tool_choice_compat, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "security"), @@ -1927,6 +1959,7 @@ def _parse_security_model_config( thinking_budget_tokens=0, thinking_include_budget=True, thinking_tool_call_compat=chat_model.thinking_tool_call_compat, + responses_tool_choice_compat=chat_model.responses_tool_choice_compat, reasoning_enabled=chat_model.reasoning_enabled, reasoning_effort=chat_model.reasoning_effort, request_params=merge_request_params(chat_model.request_params), @@ -1954,6 +1987,9 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: ) ) api_mode = _resolve_api_mode(data, "agent", "AGENT_MODEL_API_MODE") + responses_tool_choice_compat = _resolve_responses_tool_choice_compat( + data, "agent", "AGENT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -2009,6 +2045,7 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + responses_tool_choice_compat=responses_tool_choice_compat, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "agent"), @@ -2093,7 +2130,7 @@ def _log_debug_info( ] for name, cfg in configs: logger.debug( - "[配置] %s_model=%s api_url=%s api_key_set=%s api_mode=%s thinking=%s reasoning=%s/%s cot_compat=%s", + "[配置] %s_model=%s api_url=%s api_key_set=%s api_mode=%s thinking=%s reasoning=%s/%s cot_compat=%s responses_tool_choice_compat=%s", name, cfg.model_name, cfg.api_url, @@ -2103,6 +2140,7 @@ def _log_debug_info( getattr(cfg, "reasoning_enabled", False), getattr(cfg, "reasoning_effort", "medium"), getattr(cfg, "thinking_tool_call_compat", False), + getattr(cfg, "responses_tool_choice_compat", False), ) def update_from(self, new_config: "Config") -> dict[str, tuple[Any, Any]]: @@ -2154,6 +2192,12 @@ def _parse_historian_model_config( "HISTORIAN_MODEL_API_MODE", fallback.api_mode, ) + responses_tool_choice_compat = _resolve_responses_tool_choice_compat( + {"models": {"historian": h}}, + "historian", + "HISTORIAN_MODEL_RESPONSES_TOOL_CHOICE_COMPAT", + fallback.responses_tool_choice_compat, + ) return AgentModelConfig( api_url=_coerce_str(h.get("api_url"), fallback.api_url), api_key=_coerce_str(h.get("api_key"), fallback.api_key), @@ -2169,6 +2213,7 @@ def _parse_historian_model_config( ), thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, + responses_tool_choice_compat=responses_tool_choice_compat, reasoning_enabled=_coerce_bool( _get_value( {"models": {"historian": h}}, diff --git a/src/Undefined/config/models.py b/src/Undefined/config/models.py index 7269c70b..6b403c5f 100644 --- a/src/Undefined/config/models.py +++ b/src/Undefined/config/models.py @@ -20,6 +20,7 @@ class ModelPoolEntry: thinking_budget_tokens: int = 0 thinking_include_budget: bool = True thinking_tool_call_compat: bool = True + responses_tool_choice_compat: bool = False reasoning_enabled: bool = False reasoning_effort: str = "medium" request_params: dict[str, Any] = field(default_factory=dict) @@ -50,6 +51,9 @@ class ChatModelConfig: thinking_tool_call_compat: bool = ( True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + responses_tool_choice_compat: bool = ( + False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) + ) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -71,6 +75,9 @@ class VisionModelConfig: thinking_tool_call_compat: bool = ( True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + responses_tool_choice_compat: bool = ( + False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) + ) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -92,6 +99,9 @@ class SecurityModelConfig: thinking_tool_call_compat: bool = ( True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + responses_tool_choice_compat: bool = ( + False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) + ) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -139,6 +149,9 @@ class AgentModelConfig: thinking_tool_call_compat: bool = ( True # 思维链 + 工具调用兼容(回传 reasoning_content) ) + responses_tool_choice_compat: bool = ( + False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) + ) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index 59c64350..dfd64421 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -768,6 +768,9 @@ function buildAotTemplate(path, arr) { if (!Object.prototype.hasOwnProperty.call(template, "thinking_tool_call_compat")) { template.thinking_tool_call_compat = true } + if (!Object.prototype.hasOwnProperty.call(template, "responses_tool_choice_compat")) { + template.responses_tool_choice_compat = false + } if (!Object.prototype.hasOwnProperty.call(template, "reasoning_enabled")) { template.reasoning_enabled = false } @@ -777,7 +780,7 @@ function buildAotTemplate(path, arr) { } return template } - return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } + return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, responses_tool_choice_compat: false, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } } function createAotWidget(path, arr) { diff --git a/tests/test_config_request_params.py b/tests/test_config_request_params.py index c36afde8..8a5d077d 100644 --- a/tests/test_config_request_params.py +++ b/tests/test_config_request_params.py @@ -26,6 +26,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( api_mode = "responses" reasoning_enabled = true reasoning_effort = "high" +responses_tool_choice_compat = true [models.chat.request_params] temperature = 0.2 @@ -54,6 +55,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( api_mode = "responses" reasoning_enabled = true reasoning_effort = "minimal" +responses_tool_choice_compat = true [models.agent.request_params] temperature = 0.3 @@ -92,6 +94,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.chat_model.reasoning_enabled is True assert cfg.chat_model.reasoning_effort == "high" assert cfg.chat_model.thinking_tool_call_compat is True + assert cfg.chat_model.responses_tool_choice_compat is True assert cfg.chat_model.request_params == { "temperature": 0.2, "metadata": {"source": "chat"}, @@ -102,6 +105,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.chat_model.pool.models[0].reasoning_enabled is False assert cfg.chat_model.pool.models[0].reasoning_effort == "low" assert cfg.chat_model.pool.models[0].thinking_tool_call_compat is True + assert cfg.chat_model.pool.models[0].responses_tool_choice_compat is True assert cfg.chat_model.pool.models[0].request_params == { "temperature": 0.6, "metadata": {"source": "chat"}, @@ -112,17 +116,20 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.security_model.reasoning_enabled == cfg.chat_model.reasoning_enabled assert cfg.security_model.reasoning_effort == cfg.chat_model.reasoning_effort assert cfg.security_model.thinking_tool_call_compat is True + assert cfg.security_model.responses_tool_choice_compat is True assert cfg.security_model.request_params == cfg.chat_model.request_params assert cfg.agent_model.api_mode == "responses" assert cfg.agent_model.reasoning_enabled is True assert cfg.agent_model.reasoning_effort == "minimal" assert cfg.agent_model.thinking_tool_call_compat is True + assert cfg.agent_model.responses_tool_choice_compat is True assert cfg.historian_model.api_mode == "chat_completions" assert cfg.historian_model.reasoning_enabled is True assert cfg.historian_model.reasoning_effort == "xhigh" assert cfg.historian_model.thinking_tool_call_compat is True + assert cfg.historian_model.responses_tool_choice_compat is True assert cfg.historian_model.request_params == { "temperature": 0.1, "metadata": {"source": "historian"}, @@ -134,3 +141,4 @@ def test_model_request_params_load_inherit_and_new_transport_fields( "metadata": {"source": "embed"}, } assert cfg.rerank_model.request_params == {"priority": "high"} + assert cfg.vision_model.responses_tool_choice_compat is False diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index e7ec29db..971f5acc 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -199,7 +199,10 @@ async def test_responses_request_normalizes_tool_calls_and_usage() -> None: }, } ] - assert fake_client.responses.last_kwargs["tool_choice"] == "required" + assert fake_client.responses.last_kwargs["tool_choice"] == { + "type": "function", + "name": "lookup", + } assert fake_client.responses.last_kwargs["metadata"] == {"source": "config"} assert fake_client.responses.last_kwargs["extra_body"] == {"custom_flag": "on"} assert "thinking" not in fake_client.responses.last_kwargs @@ -222,6 +225,94 @@ async def test_responses_request_normalizes_tool_calls_and_usage() -> None: await requester._http_client.aclose() +@pytest.mark.asyncio +async def test_responses_tool_choice_compat_mode_uses_required_string() -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=[ + { + "id": "resp_compat", + "output": [ + { + "type": "function_call", + "call_id": "call_compat", + "name": "lookup", + "arguments": '{"query": "weather"}', + } + ], + "usage": {"input_tokens": 5, "output_tokens": 4, "total_tokens": 9}, + } + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + responses_tool_choice_compat=True, + ) + + await requester.request( + model_config=cfg, + messages=[{"role": "user", "content": "hello"}], + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "search", + "description": "search docs", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + }, + ], + tool_choice=cast(Any, {"type": "function", "function": {"name": "lookup"}}), + ) + + assert fake_client.responses.last_kwargs is not None + assert fake_client.responses.last_kwargs["tool_choice"] == "required" + assert fake_client.responses.last_kwargs["tools"] == [ + { + "type": "function", + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + } + ] + + await requester._http_client.aclose() + + @pytest.mark.asyncio async def test_responses_transport_state_uses_previous_response_id_and_tool_outputs() -> ( None @@ -364,6 +455,7 @@ async def test_responses_tools_and_tool_choice_use_sanitized_api_names() -> None model_name="gpt-test", max_tokens=512, api_mode="responses", + responses_tool_choice_compat=True, ) result = await requester.request( diff --git a/tests/test_runtime_api_probes.py b/tests/test_runtime_api_probes.py index da1f6783..c39817d6 100644 --- a/tests/test_runtime_api_probes.py +++ b/tests/test_runtime_api_probes.py @@ -27,6 +27,7 @@ async def test_runtime_internal_probe_includes_chat_model_transport_fields() -> api_mode="responses", thinking_enabled=False, thinking_tool_call_compat=True, + responses_tool_choice_compat=False, reasoning_enabled=True, reasoning_effort="high", ), @@ -56,6 +57,7 @@ async def test_runtime_internal_probe_includes_chat_model_transport_fields() -> "api_mode": "responses", "thinking_enabled": False, "thinking_tool_call_compat": True, + "responses_tool_choice_compat": False, "reasoning_enabled": True, "reasoning_effort": "high", } diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 60e4882c..00c329bd 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -71,6 +71,7 @@ def test_pool_model_request_params_roundtrip(self) -> None: api_key = "sk-a" api_mode = "responses" thinking_tool_call_compat = true +responses_tool_choice_compat = true reasoning_enabled = true reasoning_effort = "high" @@ -90,6 +91,7 @@ def test_pool_model_request_params_roundtrip(self) -> None: model = data["models"]["chat"]["pool"]["models"][0] assert model["api_mode"] == "responses" assert model["thinking_tool_call_compat"] is True + assert model["responses_tool_choice_compat"] is True assert model["reasoning_enabled"] is True assert model["reasoning_effort"] == "high" params = model["request_params"] From 66eede2da97852482f28c370ace2da2367fe97f7 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 18:37:30 +0800 Subject: [PATCH 14/21] docs(config): clarify responses compat usage --- config.toml.example | 20 ++++++++++---------- docs/configuration.md | 6 ++++-- docs/openapi.md | 2 ++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/config.toml.example b/config.toml.example index 2a661f6d..ea749aaf 100644 --- a/config.toml.example +++ b/config.toml.example @@ -110,8 +110,8 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true -# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 -# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +# zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 +# en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 @@ -167,8 +167,8 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true -# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 -# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +# zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 +# en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 @@ -217,8 +217,8 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true -# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 -# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +# zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 +# en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 @@ -264,8 +264,8 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true -# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 -# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +# zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 +# en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 @@ -324,8 +324,8 @@ thinking_include_budget = true # zh: 思维链工具调用兼容:启用后在多轮工具调用中回传 reasoning_content,避免部分模型返回 400。 # en: Thinking tool-call compatibility: pass back reasoning_content in multi-turn tool calls to avoid 400 errors from some models. thinking_tool_call_compat = true -# zh: Responses API 的 tool_choice 兼容模式:仅为兼容部分只接受字符串 tool_choice 的代理,上报为 "required" 并只保留目标工具。默认关闭。 -# en: Responses API tool_choice compatibility mode: for proxies that only accept string tool_choice, send "required" and keep only the selected tool. Disabled by default. +# zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 +# en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 diff --git a/docs/configuration.md b/docs/configuration.md index 16836232..ddf937c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,7 +150,7 @@ model_name = "gpt-4o-mini" | `thinking_budget_tokens` | thinking 预算 | | `thinking_include_budget` | 是否发送 `budget_tokens` | | `thinking_tool_call_compat` | Tool Calls 兼容模式:在多轮工具调用中回填 `reasoning_content`;默认 `true` | -| `responses_tool_choice_compat` | `responses` 下的 `tool_choice` 兼容开关:对不支持对象型 `tool_choice` 的代理降级为字符串 `"required"`;默认 `false` | +| `responses_tool_choice_compat` | `responses` 下的 `tool_choice` 兼容开关:仅建议在默认关闭时请求仍返回 500、怀疑上游不兼容对象型 `tool_choice` 时再尝试开启;开启后降级为字符串 `"required"`;默认 `false` | | `request_params` | 额外请求体参数(透传给模型 API,保留字段会忽略) | 请求模式说明: @@ -159,8 +159,10 @@ model_name = "gpt-4o-mini" - `reasoning_enabled=true` 时额外发送 `reasoning={ effort = ... }` - `api_mode="responses"`:走 `client.responses.create(...)` - 仅在 `reasoning_enabled=true` 时发送 `reasoning={ effort = ... }` + - 默认使用官方对象格式:`{"type":"function","name":"..."}` - `responses_tool_choice_compat=true` 时,会把指定函数的 `tool_choice` 降级为字符串 `"required"`,并只保留目标工具,用于兼容部分不完整代理 - - 关闭时使用官方对象格式:`{"type":"function","name":"..."}` + - 仅建议在默认关闭时请求仍返回 500,再尝试开启该兼容开关 + - 当前已知 `new-api v0.11.4-alpha.3` 存在这类兼容问题 - 旧式 `thinking_*` 不会下发到 `responses` `request_params` 说明: diff --git a/docs/openapi.md b/docs/openapi.md index 5ea73def..1360406c 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -111,6 +111,8 @@ curl http://127.0.0.1:8788/openapi.json } ``` +说明:`responses_tool_choice_compat` 通常保持 `false`;仅建议在 `responses` 请求默认配置下仍返回 `500` 时再尝试开启。当前已知 `new-api v0.11.4-alpha.3` 存在该兼容问题。 + #### 外部探针响应字段 | 字段 | 类型 | 说明 | From f137cbd32f57c44e033a11a21af51c5270f1e889 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 19:32:47 +0800 Subject: [PATCH 15/21] fix(ai): fallback responses tool followups to replay --- src/Undefined/ai/llm.py | 79 ++++++++- .../ai/transports/openai_transport.py | 6 +- tests/test_llm_request_params.py | 152 +++++++++++++++++- 3 files changed, 229 insertions(+), 8 deletions(-) diff --git a/src/Undefined/ai/llm.py b/src/Undefined/ai/llm.py index 0b584846..9cec9a34 100644 --- a/src/Undefined/ai/llm.py +++ b/src/Undefined/ai/llm.py @@ -155,6 +155,10 @@ _DEFAULT_TOOLS_DESCRIPTION_MAX_LEN = 1024 _TOOLS_PARAM_INDEX_RE = re.compile(r"Tools\[(\d+)\]", re.IGNORECASE) +_RESPONSES_MISSING_TOOL_CALL_OUTPUT_RE = re.compile( + r"no tool call found for function call output with call_id", + re.IGNORECASE, +) _DEFAULT_TOOLS_DESCRIPTION_PREVIEW_LEN = 160 _DEFAULT_TOOL_NAME_DOT_DELIMITER = "-_-" @@ -227,6 +231,32 @@ def _encode_tool_name_for_api(tool_name: str) -> str: return encoded +def _responses_should_fallback_to_stateless_replay( + exc: APIStatusError, + request_body: dict[str, Any], + *, + stateless_replay: bool, +) -> bool: + if stateless_replay or not request_body.get("previous_response_id"): + return False + input_items = request_body.get("input") + if not isinstance(input_items, list) or not any( + isinstance(item, dict) and item.get("type") == "function_call_output" + for item in input_items + ): + return False + if exc.status_code != 400 or not isinstance(exc.body, dict): + return False + error = exc.body.get("error") + if not isinstance(error, dict): + return False + message = str(error.get("message", "")).strip() + param = str(error.get("param", "")).strip().lower() + return param == "input" and bool( + _RESPONSES_MISSING_TOOL_CALL_OUTPUT_RE.search(message) + ) + + def _sanitize_openai_tool_names_in_request( request_body: dict[str, Any], ) -> tuple[dict[str, str], dict[str, str]]: @@ -960,6 +990,11 @@ async def request( call_type=call_type, overrides=dict(kwargs), ) + responses_stateless_replay = bool( + isinstance(transport_state, dict) + and transport_state.get("stateless_replay") + ) + effective_transport_state = transport_state request_body = build_request_body( model_config=model_config, messages=messages_for_api, @@ -967,7 +1002,7 @@ async def request( tools=tools_for_api, tool_choice=tool_choice, internal_to_api=internal_to_api, - transport_state=transport_state, + transport_state=effective_transport_state, **effective_kwargs, ) @@ -998,7 +1033,45 @@ async def request( ) log_debug_json(logger, "[API请求体]", request_body) - raw_result = await self._request_with_openai(model_config, request_body) + try: + raw_result = await self._request_with_openai(model_config, request_body) + except APIStatusError as exc: + if ( + api_mode == API_MODE_RESPONSES + and _responses_should_fallback_to_stateless_replay( + exc, + request_body, + stateless_replay=responses_stateless_replay, + ) + ): + logger.warning( + "[responses.compat] previous_response_id 续轮失败,自动降级为 stateless replay: model=%s call_type=%s previous_response_id=%s", + model_config.model_name, + call_type, + request_body.get("previous_response_id", ""), + ) + effective_transport_state = dict(effective_transport_state or {}) + effective_transport_state["stateless_replay"] = True + responses_stateless_replay = True + request_body = build_request_body( + model_config=model_config, + messages=messages_for_api, + max_tokens=max_tokens, + tools=tools_for_api, + tool_choice=tool_choice, + internal_to_api=internal_to_api, + transport_state=effective_transport_state, + **effective_kwargs, + ) + if logger.isEnabledFor(logging.DEBUG): + log_debug_json( + logger, "[API请求体][stateless replay]", request_body + ) + raw_result = await self._request_with_openai( + model_config, request_body + ) + else: + raise if api_mode == API_MODE_RESPONSES: result = normalize_responses_result( raw_result, @@ -1023,6 +1096,8 @@ async def request( "tool_result_start_index": transport_message_count + (1 if tool_calls else 0), } + if responses_stateless_replay: + result["_transport_state"]["stateless_replay"] = True else: result = self._normalize_result(raw_result) if api_to_internal: diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py index 10cdc7e1..67e07101 100644 --- a/src/Undefined/ai/transports/openai_transport.py +++ b/src/Undefined/ai/transports/openai_transport.py @@ -94,7 +94,7 @@ def _stringify_content(value: Any) -> str: def _content_to_response_parts(content: Any) -> list[dict[str, Any]]: if isinstance(content, str): - return [{"type": "input_text", "text": content}] + return [{"type": "input_text", "text": content}] if content else [] if not isinstance(content, list): text = _stringify_content(content) return [{"type": "input_text", "text": text}] if text else [] @@ -360,10 +360,12 @@ def build_responses_request_body( previous_response_id = "" start_index = 0 + stateless_replay = False if isinstance(transport_state, dict): previous_response_id = str( transport_state.get("previous_response_id") or "" ).strip() + stateless_replay = bool(transport_state.get("stateless_replay")) try: start_index = int(transport_state.get("tool_result_start_index") or 0) except Exception: @@ -371,7 +373,7 @@ def build_responses_request_body( if start_index < 0: start_index = 0 - if previous_response_id: + if previous_response_id and not stateless_replay: body["previous_response_id"] = previous_response_id body["input"] = _messages_to_responses_input( messages[start_index:], internal_to_api, include_system=True diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index 971f5acc..97a0193d 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -4,7 +4,7 @@ import httpx import pytest -from openai import AsyncOpenAI +from openai import AsyncOpenAI, BadRequestError from Undefined.ai.llm import ModelRequester, _encode_tool_name_for_api from Undefined.ai.parsing import extract_choices_content @@ -35,7 +35,7 @@ async def create(self, **kwargs: Any) -> dict[str, Any]: class _FakeResponsesAPI: - def __init__(self, responses: list[dict[str, Any]] | None = None) -> None: + def __init__(self, responses: list[Any] | None = None) -> None: self.last_kwargs: dict[str, Any] | None = None self.calls: list[dict[str, Any]] = [] self._responses = list(responses or []) @@ -45,7 +45,16 @@ async def create(self, **kwargs: Any) -> dict[str, Any]: self.calls.append(dict(kwargs)) if not self._responses: raise AssertionError("fake responses exhausted") - return self._responses.pop(0) + item = self._responses.pop(0) + if isinstance(item, Exception): + raise item + return cast(dict[str, Any], item) + + +def _make_bad_request_error(message: str, body: dict[str, Any]) -> BadRequestError: + request = httpx.Request("POST", "https://api.example.com/v1/responses") + response = httpx.Response(400, request=request) + return BadRequestError(message, response=response, body=body) class _FakeClient: @@ -387,7 +396,7 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp tools=tools, ) first_tool_calls = first["choices"][0]["message"]["tool_calls"] - messages = [ + messages: list[dict[str, Any]] = [ {"role": "user", "content": "hello"}, {"role": "assistant", "content": "", "tool_calls": first_tool_calls}, {"role": "tool", "tool_call_id": "call_1", "content": "done"}, @@ -421,6 +430,141 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp await requester._http_client.aclose() +@pytest.mark.asyncio +async def test_responses_followup_falls_back_to_stateless_replay_on_missing_call_id() -> ( + None +): + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=cast( + list[dict[str, Any]], + [ + _make_bad_request_error( + "No tool call found for function call output with call_id call_1.", + { + "error": { + "message": "No tool call found for function call output with call_id call_1.", + "type": "invalid_request_error", + "param": "input", + "code": None, + } + }, + ), + { + "id": "resp_2", + "output": [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "all done"}], + } + ], + "usage": {"input_tokens": 6, "output_tokens": 3, "total_tokens": 9}, + }, + ], + ), + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + ) + messages: list[dict[str, Any]] = [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "lookup", + "arguments": '{"query": "weather"}', + }, + } + ], + }, + {"role": "tool", "tool_call_id": "call_1", "content": "done"}, + ] + + result = await requester.request( + model_config=cfg, + messages=messages, + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + transport_state={ + "api_mode": "responses", + "previous_response_id": "resp_1", + "tool_result_start_index": 2, + }, + message_count_for_transport=len(messages), + ) + + assert fake_client.responses.calls[0]["previous_response_id"] == "resp_1" + assert fake_client.responses.calls[0]["input"] == [ + { + "type": "function_call_output", + "call_id": "call_1", + "output": "done", + } + ] + assert "previous_response_id" not in fake_client.responses.calls[1] + replay_input = fake_client.responses.calls[1]["input"] + assert replay_input[0] == { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "hello"}], + } + assert replay_input[1]["type"] == "function_call" + assert replay_input[1]["call_id"] == "call_1" + assert replay_input[1]["name"] == "lookup" + assert replay_input[1]["arguments"] in { + '{"query": "weather"}', + '{"query":"weather"}', + } + assert replay_input[2] == { + "type": "function_call_output", + "call_id": "call_1", + "output": "done", + } + assert extract_choices_content(result) == "all done" + assert result["_transport_state"] == { + "api_mode": "responses", + "previous_response_id": "resp_2", + "tool_result_start_index": 3, + "stateless_replay": True, + } + + await requester._http_client.aclose() + + @pytest.mark.asyncio async def test_responses_tools_and_tool_choice_use_sanitized_api_names() -> None: requester = ModelRequester( From df130c18b79ce1ea0e24c3b3d0b3b62c3cc5272c Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 19:55:38 +0800 Subject: [PATCH 16/21] feat(config): add responses stateless replay switch --- config.toml.example | 15 +++ docs/configuration.md | 21 ++-- docs/openapi.md | 5 +- src/Undefined/ai/llm.py | 9 +- src/Undefined/api/app.py | 3 + src/Undefined/config/loader.py | 47 +++++++- src/Undefined/config/models.py | 5 + src/Undefined/skills/agents/README.md | 4 +- src/Undefined/webui/static/js/config-form.js | 5 +- tests/test_config_request_params.py | 32 +++++- tests/test_llm_request_params.py | 107 +++++++++++++++++++ tests/test_runtime_api_probes.py | 2 + tests/test_webui_render_toml.py | 2 + 13 files changed, 242 insertions(+), 15 deletions(-) diff --git a/config.toml.example b/config.toml.example index ea749aaf..0f9c6083 100644 --- a/config.toml.example +++ b/config.toml.example @@ -113,6 +113,9 @@ thinking_tool_call_compat = true # zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 # en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false +# zh: Responses API 续轮强制降级:启用后,多轮工具调用将始终跳过 previous_response_id,直接使用完整消息重放(stateless replay)。仅在上游不兼容 responses 状态续轮时使用。默认关闭。 +# en: Responses API force stateless replay: when enabled, multi-turn tool follow-ups always skip previous_response_id and replay the full message history instead. Use only when the upstream does not handle stateful responses follow-ups correctly. Disabled by default. +responses_force_stateless_replay = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -170,6 +173,9 @@ thinking_tool_call_compat = true # zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 # en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false +# zh: Responses API 续轮强制降级:启用后,多轮工具调用将始终跳过 previous_response_id,直接使用完整消息重放(stateless replay)。仅在上游不兼容 responses 状态续轮时使用。默认关闭。 +# en: Responses API force stateless replay: when enabled, multi-turn tool follow-ups always skip previous_response_id and replay the full message history instead. Use only when the upstream does not handle stateful responses follow-ups correctly. Disabled by default. +responses_force_stateless_replay = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -220,6 +226,9 @@ thinking_tool_call_compat = true # zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 # en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false +# zh: Responses API 续轮强制降级:启用后,多轮工具调用将始终跳过 previous_response_id,直接使用完整消息重放(stateless replay)。仅在上游不兼容 responses 状态续轮时使用。默认关闭。 +# en: Responses API force stateless replay: when enabled, multi-turn tool follow-ups always skip previous_response_id and replay the full message history instead. Use only when the upstream does not handle stateful responses follow-ups correctly. Disabled by default. +responses_force_stateless_replay = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -267,6 +276,9 @@ thinking_tool_call_compat = true # zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 # en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false +# zh: Responses API 续轮强制降级:启用后,多轮工具调用将始终跳过 previous_response_id,直接使用完整消息重放(stateless replay)。仅在上游不兼容 responses 状态续轮时使用。默认关闭。 +# en: Responses API force stateless replay: when enabled, multi-turn tool follow-ups always skip previous_response_id and replay the full message history instead. Use only when the upstream does not handle stateful responses follow-ups correctly. Disabled by default. +responses_force_stateless_replay = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. @@ -327,6 +339,9 @@ thinking_tool_call_compat = true # zh: Responses API 的 tool_choice 兼容模式:仅在关闭时请求仍返回 500、怀疑上游不兼容对象型 tool_choice 时再尝试开启;开启后上报为 "required" 并只保留目标工具。当前已在 new-api v0.11.4-alpha.3 发现该问题。默认关闭。 # en: Responses API tool_choice compatibility mode: only try enabling this when requests still return 500 with the default setting and you suspect the upstream does not support object-style tool_choice; it sends "required" and keeps only the selected tool. This issue is currently observed on new-api v0.11.4-alpha.3. Disabled by default. responses_tool_choice_compat = false +# zh: Responses API 续轮强制降级:启用后,多轮工具调用将始终跳过 previous_response_id,直接使用完整消息重放(stateless replay)。仅在上游不兼容 responses 状态续轮时使用。默认关闭。 +# en: Responses API force stateless replay: when enabled, multi-turn tool follow-ups always skip previous_response_id and replay the full message history instead. Use only when the upstream does not handle stateful responses follow-ups correctly. Disabled by default. +responses_force_stateless_replay = false # zh: 额外请求体参数(可选),可用于 temperature 或供应商私有参数。 # en: Extra request-body params (optional), e.g. temperature or vendor-specific fields. diff --git a/docs/configuration.md b/docs/configuration.md index ddf937c3..e4244050 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,6 +151,7 @@ model_name = "gpt-4o-mini" | `thinking_include_budget` | 是否发送 `budget_tokens` | | `thinking_tool_call_compat` | Tool Calls 兼容模式:在多轮工具调用中回填 `reasoning_content`;默认 `true` | | `responses_tool_choice_compat` | `responses` 下的 `tool_choice` 兼容开关:仅建议在默认关闭时请求仍返回 500、怀疑上游不兼容对象型 `tool_choice` 时再尝试开启;开启后降级为字符串 `"required"`;默认 `false` | +| `responses_force_stateless_replay` | `responses` 下的续轮强制降级开关:启用后多轮工具调用始终跳过 `previous_response_id`,改为完整消息重放;默认 `false` | | `request_params` | 额外请求体参数(透传给模型 API,保留字段会忽略) | 请求模式说明: @@ -161,7 +162,8 @@ model_name = "gpt-4o-mini" - 仅在 `reasoning_enabled=true` 时发送 `reasoning={ effort = ... }` - 默认使用官方对象格式:`{"type":"function","name":"..."}` - `responses_tool_choice_compat=true` 时,会把指定函数的 `tool_choice` 降级为字符串 `"required"`,并只保留目标工具,用于兼容部分不完整代理 - - 仅建议在默认关闭时请求仍返回 500,再尝试开启该兼容开关 + - `responses_force_stateless_replay=true` 时,多轮工具调用会始终跳过 `previous_response_id`,直接走完整消息重放 + - 仅建议在默认关闭时请求仍返回 500,再尝试开启这些兼容开关 - 当前已知 `new-api v0.11.4-alpha.3` 存在这类兼容问题 - 旧式 `thinking_*` 不会下发到 `responses` @@ -185,6 +187,7 @@ model_name = "gpt-4o-mini" - `thinking_budget_tokens=20000` - `thinking_tool_call_compat=true` - `responses_tool_choice_compat=false` +- `responses_force_stateless_replay=false` 补充: - 若上游只对 `/v1/responses` 识别自定义参数,可将 `api_mode` 切到 `responses`。 @@ -200,16 +203,17 @@ model_name = "gpt-4o-mini" - `thinking_budget_tokens=20000` - `thinking_tool_call_compat=true` - `responses_tool_choice_compat=false` +- `responses_force_stateless_replay=false` ### 4.4.4 `[models.security]` 安全模型 字段: - 额外开关:`enabled=true` -- 默认:`max_tokens=100`、`api_mode="chat_completions"`、`reasoning_enabled=false`、`reasoning_effort="medium"`、`thinking_budget_tokens=0`、`thinking_tool_call_compat=true`、`responses_tool_choice_compat=false` +- 默认:`max_tokens=100`、`api_mode="chat_completions"`、`reasoning_enabled=false`、`reasoning_effort="medium"`、`thinking_budget_tokens=0`、`thinking_tool_call_compat=true`、`responses_tool_choice_compat=false`、`responses_force_stateless_replay=false` 关键回退逻辑: - 若 `api_url/api_key/model_name` 任一缺失,会自动回退为 chat 模型(并告警)。 -- 回退时会继承 chat 的 `api_mode`、`reasoning_*`、`responses_tool_choice_compat` 与 `request_params`;旧 `thinking_*` 仍保持安全模型自身默认值。 +- 回退时会继承 chat 的 `api_mode`、`reasoning_*`、`responses_tool_choice_compat`、`responses_force_stateless_replay` 与 `request_params`;旧 `thinking_*` 仍保持安全模型自身默认值。 ### 4.4.5 `[models.agent]` Agent 执行模型 @@ -221,12 +225,13 @@ model_name = "gpt-4o-mini" - `reasoning_effort="medium"` - `thinking_tool_call_compat=true` - `responses_tool_choice_compat=false` +- `responses_force_stateless_replay=false` ### 4.4.6 `[models.historian]` 史官模型 - 用于认知记忆后台改写。 - 若整个节缺失或为空:完整回退到 `models.agent`。 -- 若部分字段缺失:逐项继承 agent 配置,包括 `api_mode`、`reasoning_*`、`thinking_*`、`responses_tool_choice_compat` 与 `request_params`。 +- 若部分字段缺失:逐项继承 agent 配置,包括 `api_mode`、`reasoning_*`、`thinking_*`、`responses_tool_choice_compat`、`responses_force_stateless_replay` 与 `request_params`。 - `queue_interval_seconds<=0` 时回退到 agent 的间隔。 ### 4.4.7 模型池 @@ -248,7 +253,7 @@ model_name = "gpt-4o-mini" `models` 条目支持字段: - `model_name`(必填) - `api_url` / `api_key` / `max_tokens` / `queue_interval_seconds` -- `api_mode` / `reasoning_enabled` / `reasoning_effort` / `responses_tool_choice_compat` +- `api_mode` / `reasoning_enabled` / `reasoning_effort` / `responses_tool_choice_compat` / `responses_force_stateless_replay` - `thinking_*` / `request_params` - 以上可选字段缺省继承主模型 @@ -288,7 +293,7 @@ model_name = "gpt-4o-mini" `request_params` 说明: - 仅用于**请求体**字段,不包含 `api_key`、`base_url`、`timeout`、`extra_headers` 等 client 选项。 - 聊天类(`chat_completions`)保留字段:`model`、`messages`、`max_tokens`、`tools`、`tool_choice`、`stream`、`stream_options`、`reasoning`、`reasoning_effort`。 -- 聊天类(`responses`)保留字段:`model`、`input`、`instructions`、`max_output_tokens`、`tools`、`tool_choice`、`previous_response_id`、`stream`、`stream_options`、`thinking`、`reasoning`、`reasoning_effort`。 +- 聊天类(`responses`)保留字段:`model`、`input`、`instructions`、`max_output_tokens`、`tools`、`tool_choice`、`previous_response_id`、`stream`、`stream_options`、`thinking`、`reasoning`、`reasoning_effort`。启用 `responses_force_stateless_replay` 时会主动跳过 `previous_response_id`。 - embedding 保留字段:`model`、`input`、`dimensions`。 - rerank 保留字段:`model`、`query`、`documents`、`top_n`、`return_documents`。 @@ -680,9 +685,9 @@ model_name = "gpt-4o-mini" - `BOT_QQ` / `SUPERADMIN_QQ` - `ONEBOT_WS_URL` / `ONEBOT_TOKEN` - `CHAT_MODEL_API_URL` / `CHAT_MODEL_API_KEY` / `CHAT_MODEL_NAME` -- `CHAT_MODEL_API_MODE` / `CHAT_MODEL_REASONING_ENABLED` / `CHAT_MODEL_REASONING_EFFORT` / `CHAT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT` +- `CHAT_MODEL_API_MODE` / `CHAT_MODEL_REASONING_ENABLED` / `CHAT_MODEL_REASONING_EFFORT` / `CHAT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT` / `CHAT_MODEL_RESPONSES_FORCE_STATELESS_REPLAY` - `VISION_MODEL_*` / `AGENT_MODEL_*` / `SECURITY_MODEL_*` / `HISTORIAN_MODEL_*` -- 上述模型环境变量同样覆盖 `*_THINKING_ENABLED`、`*_THINKING_BUDGET_TOKENS`、`*_THINKING_TOOL_CALL_COMPAT`、`*_RESPONSES_TOOL_CHOICE_COMPAT` +- 上述模型环境变量同样覆盖 `*_THINKING_ENABLED`、`*_THINKING_BUDGET_TOKENS`、`*_THINKING_TOOL_CALL_COMPAT`、`*_RESPONSES_TOOL_CHOICE_COMPAT`、`*_RESPONSES_FORCE_STATELESS_REPLAY` - `EMBEDDING_MODEL_*` / `RERANK_MODEL_*` - `SEARXNG_URL` - `HTTP_PROXY` / `HTTPS_PROXY` diff --git a/docs/openapi.md b/docs/openapi.md index 1360406c..db5a6f26 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -79,7 +79,7 @@ curl http://127.0.0.1:8788/openapi.json | `cognitive` | `object` | 认知服务(`enabled`、`queue`) | | `api` | `object` | Runtime API 配置(`enabled`、`host`、`port`、`openapi_enabled`) | | `skills` | `object` | 技能统计,包含 `tools`、`agents`、`anthropic_skills` 三个子对象 | -| `models` | `object` | 模型配置;聊天类模型包含 `model_name`、脱敏 `api_url`、`api_mode`、`thinking_enabled`、`thinking_tool_call_compat`、`responses_tool_choice_compat`、`reasoning_enabled`、`reasoning_effort` | +| `models` | `object` | 模型配置;聊天类模型包含 `model_name`、脱敏 `api_url`、`api_mode`、`thinking_enabled`、`thinking_tool_call_compat`、`responses_tool_choice_compat`、`responses_force_stateless_replay`、`reasoning_enabled`、`reasoning_effort` | `skills` 子对象结构: @@ -104,6 +104,7 @@ curl http://127.0.0.1:8788/openapi.json "thinking_enabled": false, "thinking_tool_call_compat": true, "responses_tool_choice_compat": false, + "responses_force_stateless_replay": false, "reasoning_enabled": true, "reasoning_effort": "high" }, @@ -111,7 +112,7 @@ curl http://127.0.0.1:8788/openapi.json } ``` -说明:`responses_tool_choice_compat` 通常保持 `false`;仅建议在 `responses` 请求默认配置下仍返回 `500` 时再尝试开启。当前已知 `new-api v0.11.4-alpha.3` 存在该兼容问题。 +说明:`responses_tool_choice_compat` 与 `responses_force_stateless_replay` 通常都保持 `false`;仅建议在 `responses` 请求默认配置下仍返回 `500`,且怀疑上游不兼容状态续轮时再尝试开启。当前已知 `new-api v0.11.4-alpha.3` 存在该兼容问题。 #### 外部探针响应字段 diff --git a/src/Undefined/ai/llm.py b/src/Undefined/ai/llm.py index 9cec9a34..41d93b61 100644 --- a/src/Undefined/ai/llm.py +++ b/src/Undefined/ai/llm.py @@ -991,10 +991,17 @@ async def request( overrides=dict(kwargs), ) responses_stateless_replay = bool( + getattr(model_config, "responses_force_stateless_replay", False) + ) or bool( isinstance(transport_state, dict) and transport_state.get("stateless_replay") ) - effective_transport_state = transport_state + effective_transport_state: dict[str, Any] | None + if responses_stateless_replay: + effective_transport_state = dict(transport_state or {}) + effective_transport_state["stateless_replay"] = True + else: + effective_transport_state = transport_state request_body = build_request_body( model_config=model_config, messages=messages_for_api, diff --git a/src/Undefined/api/app.py b/src/Undefined/api/app.py index 09618ae8..0cf5528e 100644 --- a/src/Undefined/api/app.py +++ b/src/Undefined/api/app.py @@ -531,6 +531,9 @@ async def _internal_probe_handler(self, request: web.Request) -> Response: "responses_tool_choice_compat": getattr( mcfg, "responses_tool_choice_compat", False ), + "responses_force_stateless_replay": getattr( + mcfg, "responses_force_stateless_replay", False + ), "reasoning_enabled": getattr(mcfg, "reasoning_enabled", False), "reasoning_effort": getattr(mcfg, "reasoning_effort", "medium"), } diff --git a/src/Undefined/config/loader.py b/src/Undefined/config/loader.py index 0548c1f8..31bc2e56 100644 --- a/src/Undefined/config/loader.py +++ b/src/Undefined/config/loader.py @@ -332,6 +332,22 @@ def _resolve_responses_tool_choice_compat( ) +def _resolve_responses_force_stateless_replay( + data: dict[str, Any], + model_name: str, + env_key: str, + default: bool = False, +) -> bool: + return _coerce_bool( + _get_value( + data, + ("models", model_name, "responses_force_stateless_replay"), + env_key, + ), + default, + ) + + def load_local_admins() -> list[int]: """从本地配置文件加载动态管理员列表""" if not LOCAL_CONFIG_PATH.exists(): @@ -1579,6 +1595,10 @@ def _parse_model_pool( item.get("responses_tool_choice_compat"), primary_config.responses_tool_choice_compat, ), + responses_force_stateless_replay=_coerce_bool( + item.get("responses_force_stateless_replay"), + primary_config.responses_force_stateless_replay, + ), reasoning_enabled=_coerce_bool( item.get("reasoning_enabled"), primary_config.reasoning_enabled, @@ -1696,6 +1716,9 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: responses_tool_choice_compat = _resolve_responses_tool_choice_compat( data, "chat", "CHAT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" ) + responses_force_stateless_replay = _resolve_responses_force_stateless_replay( + data, "chat", "CHAT_MODEL_RESPONSES_FORCE_STATELESS_REPLAY" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1752,6 +1775,7 @@ def _parse_chat_model_config(data: dict[str, Any]) -> ChatModelConfig: thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, responses_tool_choice_compat=responses_tool_choice_compat, + responses_force_stateless_replay=responses_force_stateless_replay, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "chat"), @@ -1784,6 +1808,9 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: responses_tool_choice_compat = _resolve_responses_tool_choice_compat( data, "vision", "VISION_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" ) + responses_force_stateless_replay = _resolve_responses_force_stateless_replay( + data, "vision", "VISION_MODEL_RESPONSES_FORCE_STATELESS_REPLAY" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1840,6 +1867,7 @@ def _parse_vision_model_config(data: dict[str, Any]) -> VisionModelConfig: thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, responses_tool_choice_compat=responses_tool_choice_compat, + responses_force_stateless_replay=responses_force_stateless_replay, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "vision"), @@ -1891,6 +1919,9 @@ def _parse_security_model_config( responses_tool_choice_compat = _resolve_responses_tool_choice_compat( data, "security", "SECURITY_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" ) + responses_force_stateless_replay = _resolve_responses_force_stateless_replay( + data, "security", "SECURITY_MODEL_RESPONSES_FORCE_STATELESS_REPLAY" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -1942,6 +1973,7 @@ def _parse_security_model_config( thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, responses_tool_choice_compat=responses_tool_choice_compat, + responses_force_stateless_replay=responses_force_stateless_replay, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "security"), @@ -1960,6 +1992,7 @@ def _parse_security_model_config( thinking_include_budget=True, thinking_tool_call_compat=chat_model.thinking_tool_call_compat, responses_tool_choice_compat=chat_model.responses_tool_choice_compat, + responses_force_stateless_replay=chat_model.responses_force_stateless_replay, reasoning_enabled=chat_model.reasoning_enabled, reasoning_effort=chat_model.reasoning_effort, request_params=merge_request_params(chat_model.request_params), @@ -1990,6 +2023,9 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: responses_tool_choice_compat = _resolve_responses_tool_choice_compat( data, "agent", "AGENT_MODEL_RESPONSES_TOOL_CHOICE_COMPAT" ) + responses_force_stateless_replay = _resolve_responses_force_stateless_replay( + data, "agent", "AGENT_MODEL_RESPONSES_FORCE_STATELESS_REPLAY" + ) reasoning_enabled = _coerce_bool( _get_value( data, @@ -2046,6 +2082,7 @@ def _parse_agent_model_config(data: dict[str, Any]) -> AgentModelConfig: thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, responses_tool_choice_compat=responses_tool_choice_compat, + responses_force_stateless_replay=responses_force_stateless_replay, reasoning_enabled=reasoning_enabled, reasoning_effort=reasoning_effort, request_params=_get_model_request_params(data, "agent"), @@ -2130,7 +2167,7 @@ def _log_debug_info( ] for name, cfg in configs: logger.debug( - "[配置] %s_model=%s api_url=%s api_key_set=%s api_mode=%s thinking=%s reasoning=%s/%s cot_compat=%s responses_tool_choice_compat=%s", + "[配置] %s_model=%s api_url=%s api_key_set=%s api_mode=%s thinking=%s reasoning=%s/%s cot_compat=%s responses_tool_choice_compat=%s responses_force_stateless_replay=%s", name, cfg.model_name, cfg.api_url, @@ -2141,6 +2178,7 @@ def _log_debug_info( getattr(cfg, "reasoning_effort", "medium"), getattr(cfg, "thinking_tool_call_compat", False), getattr(cfg, "responses_tool_choice_compat", False), + getattr(cfg, "responses_force_stateless_replay", False), ) def update_from(self, new_config: "Config") -> dict[str, tuple[Any, Any]]: @@ -2198,6 +2236,12 @@ def _parse_historian_model_config( "HISTORIAN_MODEL_RESPONSES_TOOL_CHOICE_COMPAT", fallback.responses_tool_choice_compat, ) + responses_force_stateless_replay = _resolve_responses_force_stateless_replay( + {"models": {"historian": h}}, + "historian", + "HISTORIAN_MODEL_RESPONSES_FORCE_STATELESS_REPLAY", + fallback.responses_force_stateless_replay, + ) return AgentModelConfig( api_url=_coerce_str(h.get("api_url"), fallback.api_url), api_key=_coerce_str(h.get("api_key"), fallback.api_key), @@ -2214,6 +2258,7 @@ def _parse_historian_model_config( thinking_include_budget=thinking_include_budget, thinking_tool_call_compat=thinking_tool_call_compat, responses_tool_choice_compat=responses_tool_choice_compat, + responses_force_stateless_replay=responses_force_stateless_replay, reasoning_enabled=_coerce_bool( _get_value( {"models": {"historian": h}}, diff --git a/src/Undefined/config/models.py b/src/Undefined/config/models.py index 6b403c5f..720cfa61 100644 --- a/src/Undefined/config/models.py +++ b/src/Undefined/config/models.py @@ -21,6 +21,7 @@ class ModelPoolEntry: thinking_include_budget: bool = True thinking_tool_call_compat: bool = True responses_tool_choice_compat: bool = False + responses_force_stateless_replay: bool = False reasoning_enabled: bool = False reasoning_effort: str = "medium" request_params: dict[str, Any] = field(default_factory=dict) @@ -54,6 +55,7 @@ class ChatModelConfig: responses_tool_choice_compat: bool = ( False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) ) + responses_force_stateless_replay: bool = False # Responses API 续轮强制降级为 stateless replay(不使用 previous_response_id) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -78,6 +80,7 @@ class VisionModelConfig: responses_tool_choice_compat: bool = ( False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) ) + responses_force_stateless_replay: bool = False # Responses API 续轮强制降级为 stateless replay(不使用 previous_response_id) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -102,6 +105,7 @@ class SecurityModelConfig: responses_tool_choice_compat: bool = ( False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) ) + responses_force_stateless_replay: bool = False # Responses API 续轮强制降级为 stateless replay(不使用 previous_response_id) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) @@ -152,6 +156,7 @@ class AgentModelConfig: responses_tool_choice_compat: bool = ( False # Responses API 的 tool_choice 兼容模式(降级为字符串 required) ) + responses_force_stateless_replay: bool = False # Responses API 续轮强制降级为 stateless replay(不使用 previous_response_id) reasoning_enabled: bool = False # 是否启用 reasoning.effort reasoning_effort: str = "medium" # reasoning effort 档位 request_params: dict[str, Any] = field(default_factory=dict) diff --git a/src/Undefined/skills/agents/README.md b/src/Undefined/skills/agents/README.md index f04880d0..8f46a53a 100644 --- a/src/Undefined/skills/agents/README.md +++ b/src/Undefined/skills/agents/README.md @@ -39,11 +39,13 @@ reasoning_effort = "medium" thinking_enabled = false thinking_budget_tokens = 0 thinking_tool_call_compat = true +responses_tool_choice_compat = false +responses_force_stateless_replay = false ``` 说明: - `api_mode = "chat_completions"` 时,旧 `thinking_*` 仍按原逻辑生效;若开启 `reasoning_enabled`,也会额外发送 `reasoning.effort`。 -- `api_mode = "responses"` 时,Agent 的多轮工具调用会自动使用 `previous_response_id + function_call_output` 续轮;旧 `thinking_*` 不会发到 `responses`。 +- `api_mode = "responses"` 时,Agent 的多轮工具调用默认使用 `previous_response_id + function_call_output` 续轮;若开启 `responses_force_stateless_replay`,则会始终改为完整消息重放;旧 `thinking_*` 不会发到 `responses`。 - `thinking_tool_call_compat` 默认 `true`,会把 `reasoning_content` 回填到本地消息历史,便于日志、回放和兼容读取。 兼容的环境变量(会覆盖 `config.toml`): diff --git a/src/Undefined/webui/static/js/config-form.js b/src/Undefined/webui/static/js/config-form.js index dfd64421..2a89538b 100644 --- a/src/Undefined/webui/static/js/config-form.js +++ b/src/Undefined/webui/static/js/config-form.js @@ -771,6 +771,9 @@ function buildAotTemplate(path, arr) { if (!Object.prototype.hasOwnProperty.call(template, "responses_tool_choice_compat")) { template.responses_tool_choice_compat = false } + if (!Object.prototype.hasOwnProperty.call(template, "responses_force_stateless_replay")) { + template.responses_force_stateless_replay = false + } if (!Object.prototype.hasOwnProperty.call(template, "reasoning_enabled")) { template.reasoning_enabled = false } @@ -780,7 +783,7 @@ function buildAotTemplate(path, arr) { } return template } - return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, responses_tool_choice_compat: false, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } + return { model_name: "", api_url: "", api_key: "", api_mode: "chat_completions", thinking_tool_call_compat: true, responses_tool_choice_compat: false, responses_force_stateless_replay: false, reasoning_enabled: false, reasoning_effort: "medium", request_params: {} } } function createAotWidget(path, arr) { diff --git a/tests/test_config_request_params.py b/tests/test_config_request_params.py index 8a5d077d..4e059065 100644 --- a/tests/test_config_request_params.py +++ b/tests/test_config_request_params.py @@ -27,6 +27,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( reasoning_enabled = true reasoning_effort = "high" responses_tool_choice_compat = true +responses_force_stateless_replay = true [models.chat.request_params] temperature = 0.2 @@ -48,6 +49,20 @@ def test_model_request_params_load_inherit_and_new_transport_fields( temperature = 0.6 provider = { name = "pool" } +[models.vision] +api_url = "https://api.openai.com/v1" +api_key = "sk-vision" +model_name = "gpt-vision" +api_mode = "responses" +reasoning_enabled = true +reasoning_effort = "low" +responses_tool_choice_compat = true +responses_force_stateless_replay = true + +[models.vision.request_params] +temperature = 0.4 +metadata = { source = "vision" } + [models.agent] api_url = "https://api.openai.com/v1" api_key = "sk-agent" @@ -56,6 +71,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( reasoning_enabled = true reasoning_effort = "minimal" responses_tool_choice_compat = true +responses_force_stateless_replay = true [models.agent.request_params] temperature = 0.3 @@ -95,6 +111,7 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.chat_model.reasoning_effort == "high" assert cfg.chat_model.thinking_tool_call_compat is True assert cfg.chat_model.responses_tool_choice_compat is True + assert cfg.chat_model.responses_force_stateless_replay is True assert cfg.chat_model.request_params == { "temperature": 0.2, "metadata": {"source": "chat"}, @@ -106,17 +123,29 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.chat_model.pool.models[0].reasoning_effort == "low" assert cfg.chat_model.pool.models[0].thinking_tool_call_compat is True assert cfg.chat_model.pool.models[0].responses_tool_choice_compat is True + assert cfg.chat_model.pool.models[0].responses_force_stateless_replay is True assert cfg.chat_model.pool.models[0].request_params == { "temperature": 0.6, "metadata": {"source": "chat"}, "provider": {"name": "pool"}, } + assert cfg.vision_model.api_mode == "responses" + assert cfg.vision_model.reasoning_enabled is True + assert cfg.vision_model.reasoning_effort == "low" + assert cfg.vision_model.responses_tool_choice_compat is True + assert cfg.vision_model.responses_force_stateless_replay is True + assert cfg.vision_model.request_params == { + "temperature": 0.4, + "metadata": {"source": "vision"}, + } + assert cfg.security_model.api_mode == cfg.chat_model.api_mode assert cfg.security_model.reasoning_enabled == cfg.chat_model.reasoning_enabled assert cfg.security_model.reasoning_effort == cfg.chat_model.reasoning_effort assert cfg.security_model.thinking_tool_call_compat is True assert cfg.security_model.responses_tool_choice_compat is True + assert cfg.security_model.responses_force_stateless_replay is True assert cfg.security_model.request_params == cfg.chat_model.request_params assert cfg.agent_model.api_mode == "responses" @@ -124,12 +153,14 @@ def test_model_request_params_load_inherit_and_new_transport_fields( assert cfg.agent_model.reasoning_effort == "minimal" assert cfg.agent_model.thinking_tool_call_compat is True assert cfg.agent_model.responses_tool_choice_compat is True + assert cfg.agent_model.responses_force_stateless_replay is True assert cfg.historian_model.api_mode == "chat_completions" assert cfg.historian_model.reasoning_enabled is True assert cfg.historian_model.reasoning_effort == "xhigh" assert cfg.historian_model.thinking_tool_call_compat is True assert cfg.historian_model.responses_tool_choice_compat is True + assert cfg.historian_model.responses_force_stateless_replay is True assert cfg.historian_model.request_params == { "temperature": 0.1, "metadata": {"source": "historian"}, @@ -141,4 +172,3 @@ def test_model_request_params_load_inherit_and_new_transport_fields( "metadata": {"source": "embed"}, } assert cfg.rerank_model.request_params == {"priority": "high"} - assert cfg.vision_model.responses_tool_choice_compat is False diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index 97a0193d..5d0c6cdb 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -430,6 +430,113 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp await requester._http_client.aclose() +@pytest.mark.asyncio +async def test_responses_force_stateless_replay_skips_previous_response_id() -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=[ + { + "id": "resp_2", + "output": [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "all done"}], + } + ], + "usage": {"input_tokens": 6, "output_tokens": 3, "total_tokens": 9}, + } + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + responses_force_stateless_replay=True, + ) + messages: list[dict[str, Any]] = [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "lookup", + "arguments": '{"query": "weather"}', + }, + } + ], + }, + {"role": "tool", "tool_call_id": "call_1", "content": "done"}, + ] + + result = await requester.request( + model_config=cfg, + messages=messages, + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + transport_state={ + "api_mode": "responses", + "previous_response_id": "resp_1", + "tool_result_start_index": 2, + }, + message_count_for_transport=len(messages), + ) + + assert "previous_response_id" not in fake_client.responses.calls[0] + replay_input = fake_client.responses.calls[0]["input"] + assert replay_input[0] == { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "hello"}], + } + assert replay_input[1]["type"] == "function_call" + assert replay_input[1]["call_id"] == "call_1" + assert replay_input[2] == { + "type": "function_call_output", + "call_id": "call_1", + "output": "done", + } + assert extract_choices_content(result) == "all done" + assert result["_transport_state"] == { + "api_mode": "responses", + "previous_response_id": "resp_2", + "tool_result_start_index": 3, + "stateless_replay": True, + } + + await requester._http_client.aclose() + + @pytest.mark.asyncio async def test_responses_followup_falls_back_to_stateless_replay_on_missing_call_id() -> ( None diff --git a/tests/test_runtime_api_probes.py b/tests/test_runtime_api_probes.py index c39817d6..6e48a663 100644 --- a/tests/test_runtime_api_probes.py +++ b/tests/test_runtime_api_probes.py @@ -28,6 +28,7 @@ async def test_runtime_internal_probe_includes_chat_model_transport_fields() -> thinking_enabled=False, thinking_tool_call_compat=True, responses_tool_choice_compat=False, + responses_force_stateless_replay=False, reasoning_enabled=True, reasoning_effort="high", ), @@ -58,6 +59,7 @@ async def test_runtime_internal_probe_includes_chat_model_transport_fields() -> "thinking_enabled": False, "thinking_tool_call_compat": True, "responses_tool_choice_compat": False, + "responses_force_stateless_replay": False, "reasoning_enabled": True, "reasoning_effort": "high", } diff --git a/tests/test_webui_render_toml.py b/tests/test_webui_render_toml.py index 00c329bd..5298b435 100644 --- a/tests/test_webui_render_toml.py +++ b/tests/test_webui_render_toml.py @@ -72,6 +72,7 @@ def test_pool_model_request_params_roundtrip(self) -> None: api_mode = "responses" thinking_tool_call_compat = true responses_tool_choice_compat = true +responses_force_stateless_replay = true reasoning_enabled = true reasoning_effort = "high" @@ -92,6 +93,7 @@ def test_pool_model_request_params_roundtrip(self) -> None: assert model["api_mode"] == "responses" assert model["thinking_tool_call_compat"] is True assert model["responses_tool_choice_compat"] is True + assert model["responses_force_stateless_replay"] is True assert model["reasoning_enabled"] is True assert model["reasoning_effort"] == "high" params = model["request_params"] From bf949ee2c3de20d6e91e384dfce9ab4bfb7d3af5 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sat, 7 Mar 2026 22:34:22 +0800 Subject: [PATCH 17/21] feat: harden python interpreter, fix sender message_id, sync pool model configs - python_interpreter: separate pip install (with network) from code execution (network=none, read-only), add path traversal protection for send_files via _resolve_output_host_path - sender: extract _extract_message_id to handle nested data.message_id envelope from OneBot responses - model_selector: pass responses_tool_choice_compat and responses_force_stateless_replay through pool model selection - config_sync/toml_render: support array-of-tables (pool models) in template merge and comment augmentation - Add tests for python interpreter handler, sender, model pool flags, and config sync pool model merging Co-Authored-By: Claude Opus 4.6 --- src/Undefined/ai/model_selector.py | 4 + .../tools/python_interpreter/handler.py | 189 +++++++++++++----- src/Undefined/utils/sender.py | 38 ++-- src/Undefined/webui/utils/config_sync.py | 120 ++++++++++- src/Undefined/webui/utils/toml_render.py | 15 ++ tests/test_config_template_sync.py | 50 +++++ tests/test_model_pool.py | 73 ++++++- tests/test_python_interpreter_handler.py | 105 ++++++++++ tests/test_sender.py | 61 ++++++ 9 files changed, 586 insertions(+), 69 deletions(-) create mode 100644 tests/test_python_interpreter_handler.py create mode 100644 tests/test_sender.py diff --git a/src/Undefined/ai/model_selector.py b/src/Undefined/ai/model_selector.py index 989da9bb..026dda89 100644 --- a/src/Undefined/ai/model_selector.py +++ b/src/Undefined/ai/model_selector.py @@ -246,6 +246,8 @@ def _entry_to_chat_config( thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + responses_tool_choice_compat=entry.responses_tool_choice_compat, + responses_force_stateless_replay=entry.responses_force_stateless_replay, reasoning_enabled=entry.reasoning_enabled, reasoning_effort=entry.reasoning_effort, request_params=entry.request_params, @@ -268,6 +270,8 @@ def _entry_to_agent_config( thinking_budget_tokens=entry.thinking_budget_tokens, thinking_include_budget=entry.thinking_include_budget, thinking_tool_call_compat=entry.thinking_tool_call_compat, + responses_tool_choice_compat=entry.responses_tool_choice_compat, + responses_force_stateless_replay=entry.responses_force_stateless_replay, reasoning_enabled=entry.reasoning_enabled, reasoning_effort=entry.reasoning_effort, request_params=entry.request_params, diff --git a/src/Undefined/skills/tools/python_interpreter/handler.py b/src/Undefined/skills/tools/python_interpreter/handler.py index 43bc212f..e935c7ec 100644 --- a/src/Undefined/skills/tools/python_interpreter/handler.py +++ b/src/Undefined/skills/tools/python_interpreter/handler.py @@ -4,7 +4,8 @@ import re import shutil import tempfile -from pathlib import Path +import time +from pathlib import Path, PurePosixPath from typing import Any, Dict logger = logging.getLogger(__name__) @@ -24,6 +25,105 @@ _SAFE_LIB_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._\-\[\],<>=!~]*$") +def _resolve_output_host_path(container_path: str, host_tmpdir: str) -> Path | None: + """将容器内 /tmp 路径映射到宿主机临时目录,并拒绝目录穿越/符号链接逃逸。""" + try: + pure_path = PurePosixPath(container_path) + relative = pure_path.relative_to("/tmp") + except (TypeError, ValueError): + return None + + host_tmp_root = Path(host_tmpdir).resolve() + candidate = (host_tmp_root / relative.as_posix()).resolve(strict=False) + + try: + candidate.relative_to(host_tmp_root) + except ValueError: + return None + + return candidate + + +def _build_docker_base_cmd(host_tmpdir: str, memory: str) -> list[str]: + return [ + "docker", + "run", + "--rm", + "--memory", + memory, + "--cpus", + CPU_LIMIT, + "-v", + f"{host_tmpdir}:/tmp", + ] + + +def _build_install_cmd(host_tmpdir: str, memory: str) -> list[str]: + cmd = _build_docker_base_cmd(host_tmpdir, memory) + cmd.append(DOCKER_IMAGE) + cmd.extend( + [ + "sh", + "-c", + "python -m pip install --quiet --disable-pip-version-check " + "--no-cache-dir -r /tmp/_requirements.txt --target /tmp/_site_packages; " + "_e=$?; chmod -R a+rw /tmp 2>/dev/null; exit $_e", + ] + ) + return cmd + + +def _build_exec_cmd( + host_tmpdir: str, + memory: str, + *, + pythonpath: str | None = None, +) -> list[str]: + cmd = _build_docker_base_cmd(host_tmpdir, memory) + cmd.extend(["--network", "none", "--read-only"]) + if pythonpath: + cmd.extend(["-e", f"PYTHONPATH={pythonpath}"]) + cmd.append(DOCKER_IMAGE) + cmd.extend( + [ + "sh", + "-c", + "python /tmp/_script.py; _e=$?; chmod -R a+rw /tmp 2>/dev/null; exit $_e", + ] + ) + return cmd + + +async def _run_docker_command( + cmd: list[str], + *, + timeout: float, +) -> tuple[int, str, str]: + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + try: + stdout_bytes, stderr_bytes = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) + except asyncio.TimeoutError: + try: + process.terminate() + await process.wait() + except Exception as e: + logger.error("[Python解释器] 终止超时进程失败: %s", e) + raise + + return ( + process.returncode if process.returncode is not None else 1, + stdout_bytes.decode("utf-8", errors="replace"), + stderr_bytes.decode("utf-8", errors="replace"), + ) + + async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: """在 Docker 容器中执行 Python 代码,可选安装库和发送输出文件。""" code = args.get("code", "") @@ -38,11 +138,6 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if not _SAFE_LIB_PATTERN.match(lib): return f"错误: 无效的库名 '{lib}'。" - # 验证文件路径必须在 /tmp/ 下 - for fpath in send_files: - if not fpath.startswith("/tmp/"): - return f"错误: 输出文件路径必须在 /tmp/ 目录下: '{fpath}'" - has_libs = bool(libraries) memory = MEMORY_LIMIT_WITH_LIBS if has_libs else MEMORY_LIMIT timeout = TIMEOUT_WITH_LIBS if has_libs else TIMEOUT @@ -51,46 +146,45 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: host_tmpdir = tempfile.mkdtemp(prefix="pyinterp_") try: + # 验证文件路径必须绑定到容器 /tmp,并且不能逃逸宿主机临时目录 + for fpath in send_files: + if _resolve_output_host_path(fpath, host_tmpdir) is None: + return f"错误: 输出文件路径必须位于容器 /tmp 目录内: '{fpath}'" + # 将代码写入脚本文件(避免 shell 引号转义问题) script_path = os.path.join(host_tmpdir, "_script.py") with open(script_path, "w", encoding="utf-8") as f: f.write(code) - # 构建 docker 命令 - cmd: list[str] = [ - "docker", - "run", - "--rm", - "--memory", - memory, - "--cpus", - CPU_LIMIT, - "-v", - f"{host_tmpdir}:/tmp", - ] + deadline = time.monotonic() + timeout if has_libs: - # 需要网络下载包、需要写权限安装包 + # 单独安装依赖,再以无网络/只读根文件系统执行用户代码。 req_path = os.path.join(host_tmpdir, "_requirements.txt") with open(req_path, "w", encoding="utf-8") as f: for lib in libraries: f.write(lib + "\n") - cmd.append(DOCKER_IMAGE) - # pip install → 运行代码 → 修正文件权限以便宿主机清理 - cmd.extend( - [ - "sh", - "-c", - "pip install --quiet -r /tmp/_requirements.txt " - "&& python /tmp/_script.py; " - "_e=$?; chmod -R a+rw /tmp 2>/dev/null; exit $_e", - ] + install_timeout = max(deadline - time.monotonic(), 1.0) + install_cmd = _build_install_cmd(host_tmpdir, memory) + install_code, install_stdout, install_stderr = await _run_docker_command( + install_cmd, + timeout=install_timeout, + ) + if install_code != 0: + return ( + f"依赖安装失败 (退出代码: {install_code}):\n" + f"{install_stderr}\n{install_stdout}" + ) + + exec_timeout = max(deadline - time.monotonic(), 1.0) + cmd = _build_exec_cmd( + host_tmpdir, + memory, + pythonpath="/tmp/_site_packages", ) else: - # 无需网络、只读文件系统 - cmd.extend(["--network", "none", "--read-only"]) - cmd.append(DOCKER_IMAGE) - cmd.extend(["python", "/tmp/_script.py"]) + cmd = _build_exec_cmd(host_tmpdir, memory) + exec_timeout = timeout logger.info( "[Python解释器] 开始执行, 超时: %ss, 库: %s, 输出文件: %s", @@ -100,21 +194,12 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: ) logger.debug("[Python解释器] 代码内容:\n%s", code) - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - try: - stdout_bytes, stderr_bytes = await asyncio.wait_for( - process.communicate(), timeout=timeout + exit_code, response, error_output = await _run_docker_command( + cmd, + timeout=exec_timeout, ) - exit_code = process.returncode - response = stdout_bytes.decode("utf-8", errors="replace") - error_output = stderr_bytes.decode("utf-8", errors="replace") - # 构建结果 parts: list[str] = [] if exit_code == 0: @@ -135,11 +220,6 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: return "\n".join(parts) except asyncio.TimeoutError: - try: - process.terminate() - await process.wait() - except Exception as e: - logger.error("[Python解释器] 终止超时进程失败: %s", e) return f"错误: 代码执行超时 ({timeout}s)。" except Exception as e: @@ -165,9 +245,12 @@ async def _send_output_files( results: list[str] = [] for container_path in send_files: - # /tmp/output.png → {host_tmpdir}/output.png - relative = container_path.removeprefix("/tmp/") - host_path = os.path.join(host_tmpdir, relative) + resolved_host_path = _resolve_output_host_path(container_path, host_tmpdir) + if resolved_host_path is None: + results.append(f"文件路径非法: {container_path}") + continue + + host_path = str(resolved_host_path) if not os.path.isfile(host_path): results.append(f"文件未找到: {container_path}") diff --git a/src/Undefined/utils/sender.py b/src/Undefined/utils/sender.py index cd92bb56..1eeaaa6c 100644 --- a/src/Undefined/utils/sender.py +++ b/src/Undefined/utils/sender.py @@ -19,6 +19,22 @@ MAX_MESSAGE_LENGTH = 4000 +def _extract_message_id(result: object) -> int | None: + if not isinstance(result, dict): + return None + + message_id = result.get("message_id") + if message_id is None: + data = result.get("data") + if isinstance(data, dict): + message_id = data.get("message_id") + + try: + return int(message_id) if message_id is not None else None + except (TypeError, ValueError): + return None + + def _format_size(size_bytes: int | None) -> str: if size_bytes is None or size_bytes < 0: return "未知大小" @@ -105,8 +121,7 @@ async def send_group_message( result = await self.onebot.send_group_message( group_id, segments, mark_sent=mark_sent ) - if isinstance(result, dict): - bot_message_id = result.get("message_id") + bot_message_id = _extract_message_id(result) else: bot_message_id = await self._send_chunked_group( group_id, message, mark_sent=mark_sent, reply_to=reply_to @@ -153,8 +168,8 @@ async def _send_chunked_group( result = await self.onebot.send_group_message( group_id, segments, mark_sent=mark_sent ) - if chunk_count == 1 and isinstance(result, dict): - first_message_id = result.get("message_id") + if chunk_count == 1: + first_message_id = _extract_message_id(result) current_chunk = [] current_length = 0 @@ -171,8 +186,8 @@ async def _send_chunked_group( result = await self.onebot.send_group_message( group_id, segments, mark_sent=mark_sent ) - if chunk_count == 1 and isinstance(result, dict): - first_message_id = result.get("message_id") + if chunk_count == 1: + first_message_id = _extract_message_id(result) logger.info(f"[消息分段] 已完成 {chunk_count} 段消息的发送") return first_message_id @@ -219,8 +234,7 @@ async def send_private_message( result = await self.onebot.send_private_message( user_id, segments, mark_sent=mark_sent ) - if isinstance(result, dict): - bot_message_id = result.get("message_id") + bot_message_id = _extract_message_id(result) else: bot_message_id = await self._send_chunked_private( user_id, message, mark_sent=mark_sent, reply_to=reply_to @@ -266,8 +280,8 @@ async def _send_chunked_private( result = await self.onebot.send_private_message( user_id, segments, mark_sent=mark_sent ) - if chunk_count == 1 and isinstance(result, dict): - first_message_id = result.get("message_id") + if chunk_count == 1: + first_message_id = _extract_message_id(result) current_chunk = [] current_length = 0 @@ -284,8 +298,8 @@ async def _send_chunked_private( result = await self.onebot.send_private_message( user_id, segments, mark_sent=mark_sent ) - if chunk_count == 1 and isinstance(result, dict): - first_message_id = result.get("message_id") + if chunk_count == 1: + first_message_id = _extract_message_id(result) logger.info(f"[消息分段] 已完成 {chunk_count} 段消息的发送") return first_message_id diff --git a/src/Undefined/webui/utils/config_sync.py b/src/Undefined/webui/utils/config_sync.py index d79edc06..61a88626 100644 --- a/src/Undefined/webui/utils/config_sync.py +++ b/src/Undefined/webui/utils/config_sync.py @@ -1,8 +1,11 @@ from __future__ import annotations +import copy import tomllib from dataclasses import dataclass from pathlib import Path +from typing import Any + from Undefined.config.loader import CONFIG_PATH from .comment import CommentMap, parse_comment_map_text @@ -41,9 +44,118 @@ def _collect_added_paths( current_value = current[key] if isinstance(default_value, dict) and isinstance(current_value, dict): added.extend(_collect_added_paths(default_value, current_value, path)) + elif _is_array_of_tables(default_value) and _is_array_of_tables(current_value): + if not default_value or not current_value: + continue + template_item = default_value[0] + for index, current_item in enumerate(current_value): + default_item = ( + default_value[index] + if index < len(default_value) + else template_item + ) + added.extend( + _collect_added_paths( + default_item, + current_item, + f"{path}[{index}]", + ) + ) return added +def _is_array_of_tables(value: Any) -> bool: + return ( + isinstance(value, list) + and bool(value) + and all(isinstance(item, dict) for item in value) + ) + + +def _get_nested_value(data: TomlData, path: tuple[str, ...]) -> Any: + node: Any = data + for key in path: + if not isinstance(node, dict): + return None + node = node.get(key) + if node is None: + return None + return node + + +def _prepare_pool_model_templates( + example_data: TomlData, + current_data: TomlData, +) -> TomlData: + prepared = copy.deepcopy(example_data) + + for model_kind in ("chat", "agent"): + current_models = _get_nested_value( + current_data, + ("models", model_kind, "pool", "models"), + ) + example_models = _get_nested_value( + prepared, + ("models", model_kind, "pool", "models"), + ) + model_table = _get_nested_value(prepared, ("models", model_kind)) + + if not (_is_array_of_tables(current_models) and current_models): + continue + if example_models != []: + continue + if not isinstance(model_table, dict): + continue + + template = { + key: copy.deepcopy(value) + for key, value in model_table.items() + if key != "pool" + } + pool_table = model_table.get("pool") + if not isinstance(pool_table, dict): + continue + pool_table["models"] = [template] + + return prepared + + +def _augment_pool_model_comments( + example_comments: CommentMap, + example_data: TomlData, + current_data: TomlData, +) -> CommentMap: + merged: CommentMap = dict(example_comments) + for model_kind in ("chat", "agent"): + current_models = _get_nested_value( + current_data, + ("models", model_kind, "pool", "models"), + ) + example_models = _get_nested_value( + example_data, + ("models", model_kind, "pool", "models"), + ) + if not (_is_array_of_tables(current_models) and current_models): + continue + if example_models != []: + continue + + source_prefix = f"models.{model_kind}" + target_prefix = f"models.{model_kind}.pool.models" + for path, value in example_comments.items(): + if path == source_prefix: + merged[target_prefix] = dict(value) + continue + if not path.startswith(f"{source_prefix}."): + continue + suffix = path.removeprefix(source_prefix) + if suffix.startswith(".pool"): + continue + merged[f"{target_prefix}{suffix}"] = dict(value) + + return merged + + def _merge_comment_maps(current: CommentMap, example: CommentMap) -> CommentMap: merged: CommentMap = dict(current) for key, value in example.items(): @@ -54,11 +166,13 @@ def _merge_comment_maps(current: CommentMap, example: CommentMap) -> CommentMap: def sync_config_text(current_text: str, example_text: str) -> ConfigTemplateSyncResult: current_data = _parse_toml_text(current_text, label="current config") example_data = _parse_toml_text(example_text, label="config example") - added_paths = _collect_added_paths(example_data, current_data) - merged = merge_defaults(example_data, current_data) + prepared_example_data = _prepare_pool_model_templates(example_data, current_data) + added_paths = _collect_added_paths(prepared_example_data, current_data) + merged = merge_defaults(prepared_example_data, current_data) + example_comments = parse_comment_map_text(example_text) comments = _merge_comment_maps( parse_comment_map_text(current_text), - parse_comment_map_text(example_text), + _augment_pool_model_comments(example_comments, example_data, current_data), ) content = render_toml(merged, comments=comments) return ConfigTemplateSyncResult( diff --git a/src/Undefined/webui/utils/toml_render.py b/src/Undefined/webui/utils/toml_render.py index 7bf9e90f..ed2538a3 100644 --- a/src/Undefined/webui/utils/toml_render.py +++ b/src/Undefined/webui/utils/toml_render.py @@ -194,6 +194,21 @@ def merge_defaults(defaults: TomlData, data: TomlData) -> TomlData: for key, value in data.items(): if isinstance(value, dict) and isinstance(merged.get(key), dict): merged[key] = merge_defaults(merged[key], value) + elif _is_array_of_tables(value) and _is_array_of_tables(merged.get(key)): + default_items = cast(list[TomlData], merged[key]) + current_items = cast(list[TomlData], value) + if not default_items: + merged[key] = list(current_items) + continue + + template_item = default_items[0] + merged[key] = [ + merge_defaults( + default_items[idx] if idx < len(default_items) else template_item, + item, + ) + for idx, item in enumerate(current_items) + ] else: merged[key] = value return merged diff --git a/tests/test_config_template_sync.py b/tests/test_config_template_sync.py index 9eed5cc6..e57c2189 100644 --- a/tests/test_config_template_sync.py +++ b/tests/test_config_template_sync.py @@ -65,3 +65,53 @@ def test_sync_config_text_preserves_multiline_string_values() -> None: parsed["models"]["embedding"]["query_instruction"] == "第一行\n第二行\n第三行" ) assert parsed["models"]["embedding"]["document_instruction"] == "文档前缀\n第二行" + + +def test_sync_config_text_merges_new_fields_into_existing_pool_model_entries() -> None: + current = """ +[models.chat] +api_url = "https://primary.example/v1" +api_key = "primary-key" +model_name = "primary-model" +max_tokens = 4096 + +[models.chat.pool] +enabled = true +strategy = "round_robin" + +[[models.chat.pool.models]] +model_name = "pool-a" +api_url = "https://pool.example/v1" +api_key = "pool-key" +max_tokens = 2048 +""" + example = """ +[models.chat] +api_url = "" +api_key = "" +model_name = "" +max_tokens = 4096 +api_mode = "responses" +responses_tool_choice_compat = true +responses_force_stateless_replay = true + +[models.chat.request_params] +temperature = 0.2 + +[models.chat.pool] +enabled = false +strategy = "default" +models = [] +""" + + result = sync_config_text(current, example) + parsed = tomllib.loads(result.content) + model = parsed["models"]["chat"]["pool"]["models"][0] + + assert model["model_name"] == "pool-a" + assert model["api_mode"] == "responses" + assert model["responses_tool_choice_compat"] is True + assert model["responses_force_stateless_replay"] is True + assert model["request_params"]["temperature"] == 0.2 + assert "models.chat.pool.models[0].api_mode" in result.added_paths + assert "models.chat.pool.models[0].request_params" in result.added_paths diff --git a/tests/test_model_pool.py b/tests/test_model_pool.py index ff6deec8..92c9e4f8 100644 --- a/tests/test_model_pool.py +++ b/tests/test_model_pool.py @@ -8,7 +8,12 @@ import pytest from Undefined.ai.model_selector import ModelSelector -from Undefined.config.models import ChatModelConfig, ModelPool, ModelPoolEntry +from Undefined.config.models import ( + AgentModelConfig, + ChatModelConfig, + ModelPool, + ModelPoolEntry, +) from Undefined.services.model_pool import ModelPoolService @@ -295,6 +300,72 @@ def test_get_all_chat_models( assert models[1][0] == "model-a" assert models[2][0] == "model-b" + def test_select_chat_config_preserves_responses_flags( + self, + model_selector: ModelSelector, + ) -> None: + primary = ChatModelConfig( + api_url="https://api.example.com/v1", + api_key="primary-key", + model_name="primary-model", + max_tokens=4096, + pool=ModelPool( + enabled=True, + strategy="round_robin", + models=[ + ModelPoolEntry( + api_url="https://pool.example.com/v1", + api_key="pool-key", + model_name="pool-model", + max_tokens=2048, + api_mode="responses", + responses_tool_choice_compat=True, + responses_force_stateless_replay=True, + ) + ], + ), + ) + + result = model_selector.select_chat_config(primary, global_enabled=True) + + assert result.model_name == "pool-model" + assert result.api_mode == "responses" + assert result.responses_tool_choice_compat is True + assert result.responses_force_stateless_replay is True + + def test_select_agent_config_preserves_responses_flags( + self, + model_selector: ModelSelector, + ) -> None: + primary = AgentModelConfig( + api_url="https://api.example.com/v1", + api_key="primary-key", + model_name="primary-agent", + max_tokens=4096, + pool=ModelPool( + enabled=True, + strategy="round_robin", + models=[ + ModelPoolEntry( + api_url="https://pool.example.com/v1", + api_key="pool-key", + model_name="pool-agent", + max_tokens=2048, + api_mode="responses", + responses_tool_choice_compat=True, + responses_force_stateless_replay=True, + ) + ], + ), + ) + + result = model_selector.select_agent_config(primary, global_enabled=True) + + assert result.model_name == "pool-agent" + assert result.api_mode == "responses" + assert result.responses_tool_choice_compat is True + assert result.responses_force_stateless_replay is True + class TestModelPoolServiceHandleMessage: """测试 ModelPoolService 的消息处理""" diff --git a/tests/test_python_interpreter_handler.py b/tests/test_python_interpreter_handler.py new file mode 100644 index 00000000..5a2aeee0 --- /dev/null +++ b/tests/test_python_interpreter_handler.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import asyncio +from pathlib import Path + +import pytest + +from Undefined.skills.tools.python_interpreter import handler as python_handler + + +class _FakeProcess: + def __init__(self, *, returncode: int = 0) -> None: + self.returncode = returncode + + async def communicate(self) -> tuple[bytes, bytes]: + return b"", b"" + + def terminate(self) -> None: + return None + + async def wait(self) -> int: + return self.returncode + + +def test_resolve_output_host_path_rejects_symlink_escape(tmp_path: Path) -> None: + host_tmpdir = tmp_path / "mounted" + host_tmpdir.mkdir() + outside_file = tmp_path / "secret.txt" + outside_file.write_text("secret", encoding="utf-8") + (host_tmpdir / "leak.txt").symlink_to(outside_file) + + assert ( + python_handler._resolve_output_host_path( + "/tmp/leak.txt", + str(host_tmpdir), + ) + is None + ) + + +@pytest.mark.asyncio +async def test_execute_rejects_send_files_that_escape_tmp_mount( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _unexpected_subprocess(*args: object, **kwargs: object) -> None: + raise AssertionError("subprocess should not be called") + + monkeypatch.setattr( + asyncio, + "create_subprocess_exec", + _unexpected_subprocess, + ) + + result = await python_handler.execute( + { + "code": "print('hello')", + "send_files": ["/tmp/../../data0/Undefined/config.toml"], + }, + {}, + ) + + assert "错误: 输出文件路径必须位于容器 /tmp 目录内" in result + + +@pytest.mark.asyncio +async def test_execute_with_libraries_runs_user_code_in_network_isolated_container( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[tuple[str, ...]] = [] + + async def _fake_create_subprocess_exec( + *args: str, **kwargs: object + ) -> _FakeProcess: + calls.append(tuple(args)) + return _FakeProcess() + + monkeypatch.setattr( + asyncio, + "create_subprocess_exec", + _fake_create_subprocess_exec, + ) + + result = await python_handler.execute( + { + "code": "print('hello')", + "libraries": ["requests"], + }, + {}, + ) + + assert result == "代码执行成功 (无输出)。" + assert len(calls) == 2 + + install_cmd = calls[0] + exec_cmd = calls[1] + + assert "--network" not in install_cmd + assert "python /tmp/_script.py" not in " ".join(install_cmd) + assert "pip install" in " ".join(install_cmd) + + assert "--network" in exec_cmd + assert "none" in exec_cmd + assert "--read-only" in exec_cmd + assert "PYTHONPATH=/tmp/_site_packages" in exec_cmd + assert "python /tmp/_script.py" in " ".join(exec_cmd) diff --git a/tests/test_sender.py b/tests/test_sender.py new file mode 100644 index 00000000..221d458f --- /dev/null +++ b/tests/test_sender.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from typing import cast +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from Undefined.utils.sender import MAX_MESSAGE_LENGTH, MessageSender + + +@pytest.fixture +def sender() -> MessageSender: + onebot = MagicMock() + history_manager = MagicMock() + history_manager.add_group_message = AsyncMock() + history_manager.add_private_message = AsyncMock() + + config = MagicMock() + config.is_group_allowed.return_value = True + config.is_private_allowed.return_value = True + config.access_control_enabled.return_value = False + config.group_access_denied_reason.return_value = None + config.private_access_denied_reason.return_value = None + + return MessageSender(onebot, history_manager, bot_qq=10000, config=config) + + +@pytest.mark.asyncio +async def test_send_group_message_reads_message_id_from_onebot_envelope( + sender: MessageSender, +) -> None: + sender.onebot.send_group_message = AsyncMock( # type: ignore[method-assign] + return_value={"data": {"message_id": 123456}} + ) + + await sender.send_group_message(12345, "hello group") + + mock = cast(AsyncMock, sender.history_manager.add_group_message) + assert mock.await_count == 1 + assert mock.await_args is not None + assert mock.await_args.kwargs["message_id"] == 123456 + + +@pytest.mark.asyncio +async def test_send_private_message_reads_message_id_from_chunked_envelope( + sender: MessageSender, +) -> None: + sender.onebot.send_private_message = AsyncMock( # type: ignore[method-assign] + side_effect=[ + {"data": {"message_id": "223344"}}, + {"data": {"message_id": "223345"}}, + ] + ) + long_message = f"{'a' * (MAX_MESSAGE_LENGTH - 500)}\n{'b' * 700}" + + await sender.send_private_message(54321, long_message) + + mock = cast(AsyncMock, sender.history_manager.add_private_message) + assert mock.await_count == 1 + assert mock.await_args is not None + assert mock.await_args.kwargs["message_id"] == 223344 From a61161734cf0d9c3b203bb6abfd57d8d3fde6858 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sun, 8 Mar 2026 00:10:39 +0800 Subject: [PATCH 18/21] fix: address review issues in responses and python interpreter Handle Responses replies from gateways that return top-level output_text, preserve tool replay offsets after prefetch injection, and avoid racing temp cleanup with OneBot file reads while lowering container privileges. Co-Authored-By: Claude Opus 4.6 --- src/Undefined/ai/client.py | 2 +- .../ai/transports/openai_transport.py | 10 +- .../tools/python_interpreter/handler.py | 43 +++++++- tests/test_llm_request_params.py | 99 ++++++++++++++++++- tests/test_python_interpreter_handler.py | 51 ++++++++++ 5 files changed, 194 insertions(+), 11 deletions(-) diff --git a/src/Undefined/ai/client.py b/src/Undefined/ai/client.py index 86153ddc..7df04e19 100644 --- a/src/Undefined/ai/client.py +++ b/src/Undefined/ai/client.py @@ -648,7 +648,6 @@ async def request_model( transport_state: dict[str, Any] | None = None, **kwargs: Any, ) -> dict[str, Any]: - message_count_for_transport = len(messages) tools = self.tool_manager.maybe_merge_agent_tools(call_type, tools) if not ( isinstance(transport_state, dict) @@ -657,6 +656,7 @@ async def request_model( messages, tools = await self._maybe_prefetch_tools( messages, tools, call_type ) + message_count_for_transport = len(messages) return await self._requester.request( model_config=model_config, messages=messages, diff --git a/src/Undefined/ai/transports/openai_transport.py b/src/Undefined/ai/transports/openai_transport.py index 67e07101..5c6d7e7e 100644 --- a/src/Undefined/ai/transports/openai_transport.py +++ b/src/Undefined/ai/transports/openai_transport.py @@ -440,6 +440,10 @@ def normalize_responses_result( assistant_texts.append(str(part.get("text"))) elif part_type == "refusal" and part.get("refusal") is not None: assistant_texts.append(str(part.get("refusal"))) + else: + text = _stringify_content(content) + if text: + assistant_texts.append(text) elif item_type == "function_call": function_name = str(item.get("name", "")).strip() if api_to_internal: @@ -458,9 +462,13 @@ def normalize_responses_result( } ) + content = "\n".join(text for text in assistant_texts if text).strip() + if not content and "output_text" in result: + content = str(result["output_text"]).strip() + message: dict[str, Any] = { "role": "assistant", - "content": "\n".join(text for text in assistant_texts if text).strip(), + "content": content, } reasoning_content = _collect_reasoning_text(output) if reasoning_content: diff --git a/src/Undefined/skills/tools/python_interpreter/handler.py b/src/Undefined/skills/tools/python_interpreter/handler.py index e935c7ec..2262dc74 100644 --- a/src/Undefined/skills/tools/python_interpreter/handler.py +++ b/src/Undefined/skills/tools/python_interpreter/handler.py @@ -12,26 +12,35 @@ # Docker 执行配置 DOCKER_IMAGE = "python:3.11-slim" +DOCKER_USER = "65534:65534" MEMORY_LIMIT = "128m" MEMORY_LIMIT_WITH_LIBS = "512m" CPU_LIMIT = "0.5" TIMEOUT = 480 # 8 分钟 TIMEOUT_WITH_LIBS = 600 # 10 分钟(pip 安装需要更多时间) +OUTPUT_FILE_RETENTION_SECONDS = 30.0 # 图片扩展名(内联发送而非文件附件) _IMAGE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}) # 安全:库名仅允许 PyPI 合法字符,必须以字母/数字开头(防止 -r/-e/--index-url 注入) _SAFE_LIB_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._\-\[\],<>=!~]*$") +_PENDING_CLEANUP_TASKS: set[asyncio.Task[None]] = set() -def _resolve_output_host_path(container_path: str, host_tmpdir: str) -> Path | None: - """将容器内 /tmp 路径映射到宿主机临时目录,并拒绝目录穿越/符号链接逃逸。""" +def _get_output_relative_path(container_path: str) -> PurePosixPath | None: try: - pure_path = PurePosixPath(container_path) - relative = pure_path.relative_to("/tmp") + relative = PurePosixPath(container_path).relative_to("/tmp") except (TypeError, ValueError): return None + return relative if relative.parts else None + + +def _resolve_output_host_path(container_path: str, host_tmpdir: str) -> Path | None: + """将容器内 /tmp 路径映射到宿主机临时目录,并拒绝目录穿越/符号链接逃逸。""" + relative = _get_output_relative_path(container_path) + if relative is None: + return None host_tmp_root = Path(host_tmpdir).resolve() candidate = (host_tmp_root / relative.as_posix()).resolve(strict=False) @@ -44,6 +53,23 @@ def _resolve_output_host_path(container_path: str, host_tmpdir: str) -> Path | N return candidate +async def _cleanup_output_dir_later(host_tmpdir: str, delay_seconds: float) -> None: + try: + await asyncio.sleep(delay_seconds) + await asyncio.to_thread(shutil.rmtree, host_tmpdir, True) + finally: + task = asyncio.current_task() + if task is not None: + _PENDING_CLEANUP_TASKS.discard(task) + + +def _schedule_output_dir_cleanup(host_tmpdir: str) -> None: + task = asyncio.create_task( + _cleanup_output_dir_later(host_tmpdir, OUTPUT_FILE_RETENTION_SECONDS) + ) + _PENDING_CLEANUP_TASKS.add(task) + + def _build_docker_base_cmd(host_tmpdir: str, memory: str) -> list[str]: return [ "docker", @@ -53,6 +79,8 @@ def _build_docker_base_cmd(host_tmpdir: str, memory: str) -> list[str]: memory, "--cpus", CPU_LIMIT, + "--user", + DOCKER_USER, "-v", f"{host_tmpdir}:/tmp", ] @@ -144,6 +172,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: # 创建宿主机临时目录,绑定挂载到容器 /tmp host_tmpdir = tempfile.mkdtemp(prefix="pyinterp_") + defer_cleanup = False try: # 验证文件路径必须绑定到容器 /tmp,并且不能逃逸宿主机临时目录 @@ -216,6 +245,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: file_result = await _send_output_files(send_files, host_tmpdir, context) if file_result: parts.append(file_result) + defer_cleanup = True return "\n".join(parts) @@ -226,7 +256,10 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: logger.exception("[Python解释器] 执行出错: %s", e) return "执行出错,请检查代码或重试" finally: - shutil.rmtree(host_tmpdir, ignore_errors=True) + if defer_cleanup: + _schedule_output_dir_cleanup(host_tmpdir) + else: + shutil.rmtree(host_tmpdir, ignore_errors=True) async def _send_output_files( diff --git a/tests/test_llm_request_params.py b/tests/test_llm_request_params.py index 5d0c6cdb..f68c46ed 100644 --- a/tests/test_llm_request_params.py +++ b/tests/test_llm_request_params.py @@ -7,6 +7,7 @@ from openai import AsyncOpenAI, BadRequestError from Undefined.ai.llm import ModelRequester, _encode_tool_name_for_api +from Undefined.ai.transports.openai_transport import normalize_responses_result from Undefined.ai.parsing import extract_choices_content from Undefined.config.models import ChatModelConfig from Undefined.token_usage_storage import TokenUsageStorage @@ -231,7 +232,34 @@ async def test_responses_request_normalizes_tool_calls_and_usage() -> None: "tool_result_start_index": 2, } - await requester._http_client.aclose() + +def test_normalize_responses_result_falls_back_to_output_text_and_scalar_content() -> ( + None +): + top_level = normalize_responses_result( + { + "id": "resp_top_level", + "output": [], + "output_text": "hello from gateway", + } + ) + assert top_level["choices"][0]["message"]["content"] == "hello from gateway" + + scalar_content = normalize_responses_result( + { + "id": "resp_scalar", + "output": [ + { + "type": "message", + "role": "assistant", + "content": {"text": "hello from content object"}, + } + ], + } + ) + assert scalar_content["choices"][0]["message"]["content"] == ( + "hello from content object" + ) @pytest.mark.asyncio @@ -427,11 +455,74 @@ async def test_responses_transport_state_uses_previous_response_id_and_tool_outp "total_tokens": 7, } - await requester._http_client.aclose() - @pytest.mark.asyncio -async def test_responses_force_stateless_replay_skips_previous_response_id() -> None: +async def test_responses_transport_state_uses_prefetched_message_count() -> None: + requester = ModelRequester( + http_client=httpx.AsyncClient(), + token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), + ) + fake_client = _FakeClient( + responses=[ + { + "id": "resp_prefetch", + "output": [ + { + "type": "function_call", + "call_id": "call_prefetch", + "name": "lookup", + "arguments": '{"query": "weather"}', + } + ], + "usage": {"input_tokens": 3, "output_tokens": 2, "total_tokens": 5}, + } + ] + ) + setattr( + requester, + "_get_openai_client_for_model", + lambda _cfg: cast(AsyncOpenAI, fake_client), + ) + cfg = ChatModelConfig( + api_url="https://api.openai.com/v1", + api_key="sk-test", + model_name="gpt-test", + max_tokens=512, + api_mode="responses", + ) + + result = await requester.request( + model_config=cfg, + messages=[ + {"role": "system", "content": "prefetch result"}, + {"role": "user", "content": "hello"}, + ], + max_tokens=128, + call_type="chat", + tools=[ + { + "type": "function", + "function": { + "name": "lookup", + "description": "lookup weather", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + }, + } + ], + message_count_for_transport=2, + ) + + assert result["_transport_state"] == { + "api_mode": "responses", + "previous_response_id": "resp_prefetch", + "tool_result_start_index": 3, + } + + await requester._http_client.aclose() requester = ModelRequester( http_client=httpx.AsyncClient(), token_usage_storage=cast(TokenUsageStorage, _FakeUsageStorage()), diff --git a/tests/test_python_interpreter_handler.py b/tests/test_python_interpreter_handler.py index 5a2aeee0..8072c326 100644 --- a/tests/test_python_interpreter_handler.py +++ b/tests/test_python_interpreter_handler.py @@ -1,7 +1,9 @@ from __future__ import annotations import asyncio +import tempfile from pathlib import Path +from unittest.mock import AsyncMock, MagicMock import pytest @@ -97,9 +99,58 @@ async def _fake_create_subprocess_exec( assert "--network" not in install_cmd assert "python /tmp/_script.py" not in " ".join(install_cmd) assert "pip install" in " ".join(install_cmd) + assert "--user" in install_cmd + assert python_handler.DOCKER_USER in install_cmd assert "--network" in exec_cmd assert "none" in exec_cmd assert "--read-only" in exec_cmd assert "PYTHONPATH=/tmp/_site_packages" in exec_cmd assert "python /tmp/_script.py" in " ".join(exec_cmd) + assert "--user" in exec_cmd + assert python_handler.DOCKER_USER in exec_cmd + + +@pytest.mark.asyncio +async def test_execute_defers_cleanup_after_sending_files( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + host_tmpdir = tmp_path / "mounted" + host_tmpdir.mkdir() + output_file = host_tmpdir / "result.txt" + output_file.write_text("hello", encoding="utf-8") + + monkeypatch.setattr(tempfile, "mkdtemp", lambda prefix: str(host_tmpdir)) + monkeypatch.setattr( + python_handler, + "_run_docker_command", + AsyncMock(return_value=(0, "", "")), + ) + send_files_mock = AsyncMock(return_value="已发送: result.txt") + monkeypatch.setattr(python_handler, "_send_output_files", send_files_mock) + cleanup_mock = MagicMock() + monkeypatch.setattr(python_handler, "_schedule_output_dir_cleanup", cleanup_mock) + + result = await python_handler.execute( + { + "code": "print('hello')", + "send_files": ["/tmp/result.txt"], + }, + {}, + ) + + assert result == "代码执行成功 (无输出)。\n已发送: result.txt" + cleanup_mock.assert_called_once_with(str(host_tmpdir)) + assert output_file.exists() + + +@pytest.mark.asyncio +async def test_cleanup_output_dir_later_removes_directory(tmp_path: Path) -> None: + host_tmpdir = tmp_path / "mounted" + host_tmpdir.mkdir() + (host_tmpdir / "result.txt").write_text("hello", encoding="utf-8") + + await python_handler._cleanup_output_dir_later(str(host_tmpdir), 0) + + assert not host_tmpdir.exists() From 9ec403d46fc7423d369f1fc26e852c7429a3abf7 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sun, 8 Mar 2026 10:18:13 +0800 Subject: [PATCH 19/21] fix: delay writing python script until offline execution --- .../skills/tools/python_interpreter/handler.py | 11 +++++++---- tests/test_python_interpreter_handler.py | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Undefined/skills/tools/python_interpreter/handler.py b/src/Undefined/skills/tools/python_interpreter/handler.py index 2262dc74..6ad09231 100644 --- a/src/Undefined/skills/tools/python_interpreter/handler.py +++ b/src/Undefined/skills/tools/python_interpreter/handler.py @@ -180,11 +180,7 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: if _resolve_output_host_path(fpath, host_tmpdir) is None: return f"错误: 输出文件路径必须位于容器 /tmp 目录内: '{fpath}'" - # 将代码写入脚本文件(避免 shell 引号转义问题) script_path = os.path.join(host_tmpdir, "_script.py") - with open(script_path, "w", encoding="utf-8") as f: - f.write(code) - deadline = time.monotonic() + timeout if has_libs: @@ -205,6 +201,10 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: f"{install_stderr}\n{install_stdout}" ) + # 避免在有网络的安装阶段暴露用户脚本给依赖安装代码。 + with open(script_path, "w", encoding="utf-8") as f: + f.write(code) + exec_timeout = max(deadline - time.monotonic(), 1.0) cmd = _build_exec_cmd( host_tmpdir, @@ -212,6 +212,9 @@ async def execute(args: Dict[str, Any], context: Dict[str, Any]) -> str: pythonpath="/tmp/_site_packages", ) else: + # 将代码写入脚本文件(避免 shell 引号转义问题) + with open(script_path, "w", encoding="utf-8") as f: + f.write(code) cmd = _build_exec_cmd(host_tmpdir, memory) exec_timeout = timeout diff --git a/tests/test_python_interpreter_handler.py b/tests/test_python_interpreter_handler.py index 8072c326..a50ba072 100644 --- a/tests/test_python_interpreter_handler.py +++ b/tests/test_python_interpreter_handler.py @@ -69,6 +69,7 @@ async def test_execute_with_libraries_runs_user_code_in_network_isolated_contain monkeypatch: pytest.MonkeyPatch, ) -> None: calls: list[tuple[str, ...]] = [] + script_paths: list[str] = [] async def _fake_create_subprocess_exec( *args: str, **kwargs: object @@ -76,11 +77,15 @@ async def _fake_create_subprocess_exec( calls.append(tuple(args)) return _FakeProcess() - monkeypatch.setattr( - asyncio, - "create_subprocess_exec", - _fake_create_subprocess_exec, - ) + real_mkdtemp = tempfile.mkdtemp + + def _tracked_mkdtemp(prefix: str) -> str: + path = real_mkdtemp(prefix=prefix) + script_paths.append(str(Path(path) / "_script.py")) + return path + + monkeypatch.setattr(asyncio, "create_subprocess_exec", _fake_create_subprocess_exec) + monkeypatch.setattr(tempfile, "mkdtemp", _tracked_mkdtemp) result = await python_handler.execute( { @@ -101,6 +106,8 @@ async def _fake_create_subprocess_exec( assert "pip install" in " ".join(install_cmd) assert "--user" in install_cmd assert python_handler.DOCKER_USER in install_cmd + assert len(script_paths) == 1 + assert not Path(script_paths[0]).exists() assert "--network" in exec_cmd assert "none" in exec_cmd From f9170052692450a87fd99914260e6641a783ebe8 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sun, 8 Mar 2026 10:40:14 +0800 Subject: [PATCH 20/21] fix: run python interpreter container as host user Use the current host uid/gid for Docker bind mounts so the mounted temp directory remains accessible without opening it to world-writable permissions. Co-Authored-By: Claude Opus 4.6 --- src/Undefined/skills/tools/python_interpreter/handler.py | 7 +++++-- tests/test_python_interpreter_handler.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Undefined/skills/tools/python_interpreter/handler.py b/src/Undefined/skills/tools/python_interpreter/handler.py index 6ad09231..002bf560 100644 --- a/src/Undefined/skills/tools/python_interpreter/handler.py +++ b/src/Undefined/skills/tools/python_interpreter/handler.py @@ -12,7 +12,6 @@ # Docker 执行配置 DOCKER_IMAGE = "python:3.11-slim" -DOCKER_USER = "65534:65534" MEMORY_LIMIT = "128m" MEMORY_LIMIT_WITH_LIBS = "512m" CPU_LIMIT = "0.5" @@ -70,6 +69,10 @@ def _schedule_output_dir_cleanup(host_tmpdir: str) -> None: _PENDING_CLEANUP_TASKS.add(task) +def _get_docker_user() -> str: + return f"{os.getuid()}:{os.getgid()}" + + def _build_docker_base_cmd(host_tmpdir: str, memory: str) -> list[str]: return [ "docker", @@ -80,7 +83,7 @@ def _build_docker_base_cmd(host_tmpdir: str, memory: str) -> list[str]: "--cpus", CPU_LIMIT, "--user", - DOCKER_USER, + _get_docker_user(), "-v", f"{host_tmpdir}:/tmp", ] diff --git a/tests/test_python_interpreter_handler.py b/tests/test_python_interpreter_handler.py index a50ba072..6683eb8d 100644 --- a/tests/test_python_interpreter_handler.py +++ b/tests/test_python_interpreter_handler.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import os import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock @@ -105,7 +106,7 @@ def _tracked_mkdtemp(prefix: str) -> str: assert "python /tmp/_script.py" not in " ".join(install_cmd) assert "pip install" in " ".join(install_cmd) assert "--user" in install_cmd - assert python_handler.DOCKER_USER in install_cmd + assert f"{os.getuid()}:{os.getgid()}" in install_cmd assert len(script_paths) == 1 assert not Path(script_paths[0]).exists() @@ -115,7 +116,7 @@ def _tracked_mkdtemp(prefix: str) -> str: assert "PYTHONPATH=/tmp/_site_packages" in exec_cmd assert "python /tmp/_script.py" in " ".join(exec_cmd) assert "--user" in exec_cmd - assert python_handler.DOCKER_USER in exec_cmd + assert f"{os.getuid()}:{os.getgid()}" in exec_cmd @pytest.mark.asyncio From cc772cb8182eec317a468d56530ab96001b86ac1 Mon Sep 17 00:00:00 2001 From: Null <1708213363@qq.com> Date: Sun, 8 Mar 2026 10:44:14 +0800 Subject: [PATCH 21/21] chore(version): bump version to 3.1.2 --- pyproject.toml | 2 +- src/Undefined/__init__.py | 2 +- uv.lock | 1012 +++++++++++++++++++------------------ 3 files changed, 521 insertions(+), 495 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2ca0c342..e9d13689 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Undefined-bot" -version = "3.1.1" +version = "3.1.2" description = "QQ bot platform with cognitive memory architecture and multi-agent Skills, via OneBot V11." readme = "README.md" authors = [ diff --git a/src/Undefined/__init__.py b/src/Undefined/__init__.py index a5bbca4f..44e62cdb 100644 --- a/src/Undefined/__init__.py +++ b/src/Undefined/__init__.py @@ -1,3 +1,3 @@ """Undefined - A high-performance, highly scalable QQ group and private chat robot based on a self-developed architecture.""" -__version__ = "3.1.1" +__version__ = "3.1.2" diff --git a/uv.lock b/uv.lock index 79e23dc8..e66861fa 100644 --- a/uv.lock +++ b/uv.lock @@ -199,14 +199,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.8" +version = "1.6.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/6c/c88eac87468c607f88bc24df1f3b31445ee6fc9ba123b09e666adf687cd9/authlib-1.6.8.tar.gz", hash = "sha256:41ae180a17cf672bc784e4a518e5c82687f1fe1e98b0cafaeda80c8e4ab2d1cb", size = 165074, upload-time = "2026-02-14T04:02:17.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/73/f7084bf12755113cd535ae586782ff3a6e710bfbe6a0d13d1c2f81ffbbfa/authlib-1.6.8-py2.py3-none-any.whl", hash = "sha256:97286fd7a15e6cfefc32771c8ef9c54f0ed58028f1322de6a2a7c969c3817888", size = 244116, upload-time = "2026-02-14T04:02:15.579Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, ] [[package]] @@ -428,22 +428,22 @@ wheels = [ [[package]] name = "brotlicffi" -version = "1.2.0.0" +version = "1.2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" }, - { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ec/52488a0563f1663e2ccc75834b470650f4b8bcdea3132aef3bf67219c661/brotlicffi-1.2.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fa102a60e50ddbd08de86a63431a722ea216d9bc903b000bf544149cc9b823dc", size = 402002, upload-time = "2025-11-21T18:17:51.76Z" }, - { url = "https://files.pythonhosted.org/packages/e4/63/d4aea4835fd97da1401d798d9b8ba77227974de565faea402f520b37b10f/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d3c4332fc808a94e8c1035950a10d04b681b03ab585ce897ae2a360d479037c", size = 406447, upload-time = "2025-11-21T18:17:53.614Z" }, - { url = "https://files.pythonhosted.org/packages/62/4e/5554ecb2615ff035ef8678d4e419549a0f7a28b3f096b272174d656749fb/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb4eb5830026b79a93bf503ad32b2c5257315e9ffc49e76b2715cffd07c8e3db", size = 402521, upload-time = "2025-11-21T18:17:54.875Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/b07f8f125ac52bbee5dc00ef0d526f820f67321bf4184f915f17f50a4657/brotlicffi-1.2.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3832c66e00d6d82087f20a972b2fc03e21cd99ef22705225a6f8f418a9158ecc", size = 374730, upload-time = "2025-11-21T18:17:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/7f/53/6262c2256513e6f530d81642477cb19367270922063eaa2d7b781d8c723d/brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851", size = 402265, upload-time = "2026-03-05T19:54:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d9/d5340b43cf5fbe7fe5a083d237e5338cc1caa73bea523be1c5e452c26290/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf", size = 406710, upload-time = "2026-03-05T19:54:07.272Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/dbced4c1e0792efdf23fd90ff6d2a320c64ff4dfef7aacc85c04fde9ddd2/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4", size = 402787, upload-time = "2026-03-05T19:54:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6f/534205ba7590c9a8716a614f270c5c2ec419b5b7079b3f9cd31b7b5580de/brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1", size = 375108, upload-time = "2026-03-05T19:54:10.079Z" }, ] [[package]] @@ -462,11 +462,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.0.1" +version = "7.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/5c/3b882b82e9af737906539a2eafb62f96a229f1fa80255bede0c7b554cbc4/cachetools-7.0.3.tar.gz", hash = "sha256:8c246313b95849964e54a909c03b327a87ab0428b068fac10da7b105ca275ef6", size = 37187, upload-time = "2026-03-05T21:00:57.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/573185481c50a8841331f54ddae44e4a3469c46aa0b397731c53a004369a/cachetools-7.0.3-py3-none-any.whl", hash = "sha256:c128ffca156eef344c25fcd08a96a5952803786fa33097f5f2d49edf76f79d53", size = 13907, upload-time = "2026-03-05T21:00:56.486Z" }, ] [[package]] @@ -477,10 +477,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db wheels = [ { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" }, { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" }, + { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" }, { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, ] @@ -543,73 +549,88 @@ wheels = [ [[package]] name = "chardet" -version = "6.0.0.post1" +version = "7.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/42/fb9436c103a881a377e34b9f58d77b5f503461c702ff654ebe86151bcfe9/chardet-6.0.0.post1.tar.gz", hash = "sha256:6b78048c3c97c7b2ed1fbad7a18f76f5a6547f7d34dbab536cc13887c9a92fa4", size = 12521798, upload-time = "2026-02-22T15:09:17.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/80/4684035f1a2a3096506bc377276a815ccf0be3c3316eab35d589e82d9f3c/chardet-7.0.1.tar.gz", hash = "sha256:6fce895c12c5495bb598e59ae3cd89306969b4464ec7b6dd609b9c86e3397fe3", size = 490240, upload-time = "2026-03-04T21:25:26.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/42/5de54f632c2de53cd3415b3703383d5fff43a94cbc0567ef362515261a21/chardet-6.0.0.post1-py3-none-any.whl", hash = "sha256:c894a36800549adf7bb5f2af47033281b75fdfcd2aa0f0243be0ad22a52e2dcb", size = 627245, upload-time = "2026-02-22T15:09:15.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/fb/a90b4510aa9080966c65321db2084bcfa184518ee1ed15570d351649ecb2/chardet-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3f59dc3e148b54813ec5c7b4b2e025d37f5dc221ee28a06d1a62f169cfaedf5", size = 540100, upload-time = "2026-03-04T21:24:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/24/fa/3ad0b454a55376b7971fe64c2f225dfe56a491d8d8728fbfba63f8ff416d/chardet-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3355a3c8453d673e7c1664fdd24a0c6ef39964c3d41befc4849250f7eb1de3b5", size = 533202, upload-time = "2026-03-04T21:24:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/ad/53/a57a8a6be34379e55c8bdbf2b988c145d3b7675577bd152e73bff7c4ba3c/chardet-7.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5333f9967863ea7d8642df0e00cf4d33e8ed7e99fe7b6464b40ba969a2808544", size = 552994, upload-time = "2026-03-04T21:24:53.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/3d4ba1650e3eb3e7431a054e3bf1b5eaea25b84c72afabf5ef6fc33305d1/chardet-7.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:265cb3b5dafc0411c0949800a0692f07e986fb663b6ae1ecfba32ad193a55a03", size = 555605, upload-time = "2026-03-04T21:24:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/73/64/9c5c450ba18359a8e8ab2943e6c3a0b100bd394799bc73a844e3c5cd9c7c/chardet-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:26186f0ea03c4c1f9be20c088b127c71b0e9d487676930fab77625ddec2a4ef2", size = 524098, upload-time = "2026-03-04T21:24:57.3Z" }, + { url = "https://files.pythonhosted.org/packages/f6/88/4c6fe7dcd5d36a2cfd7030084fbd79264083f329faaf96038c23888a8e05/chardet-7.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f661edbfa77b8683a503043ddc9b9fe9036cf28af13064200e11fa1844ded79c", size = 541828, upload-time = "2026-03-04T21:24:58.726Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fb/3b92a2433eadef83ae131fa720a17857cfbf7687c5f188bfb2f9eee2d3dd/chardet-7.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:169951fa88d449e72e0c6194cec1c5e405fd36a6cfbe74c7dab5494cc35f1700", size = 533571, upload-time = "2026-03-04T21:25:00.703Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/37bee6900183ea08a3a0ae04b9f018f9e64c6b10716e1f7b423db0c4356c/chardet-7.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd6db7505556ae8f9e2a3bf6d689c2b86aa6b459cf39552645d2c4d3fdbf489c", size = 554182, upload-time = "2026-03-04T21:25:02.168Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/2fe5ea435ae480bd3a76be1415920ce52b3ff6e188d8eab6a635d6a2a1d1/chardet-7.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f907962b18df78d5ca87a7484e4034354408d2c97cec6f53634b0ea0424c594", size = 557933, upload-time = "2026-03-04T21:25:03.694Z" }, + { url = "https://files.pythonhosted.org/packages/07/ba/7ca89301e492ac4184ba7f4736565d954ba3125acf6bf02c66a38a802bda/chardet-7.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:302798e1e62008ca34a216dd04ecc5e240993b2090628e2a35d4c0754313ea9a", size = 524256, upload-time = "2026-03-04T21:25:05.581Z" }, + { url = "https://files.pythonhosted.org/packages/56/26/1a22b9a19b4ca167ca462eaf91d0fc31285874d80b0381c55fdc5bc5f066/chardet-7.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67fe3f453416ed9343057dcf06583b36aae6d8bdb013370b3ff46bc37b7e30ac", size = 541652, upload-time = "2026-03-04T21:25:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/2f2425f3b0801e897653723ee827bc87e5a0feacf826ab268a9216680615/chardet-7.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:63bc210ce73f8a1b87430b949f84d086cb326d67eb259305862e7c8861b73374", size = 533333, upload-time = "2026-03-04T21:25:08.886Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/6b5f4b49c471b396bdbddad55b569e05d686ea65d91795dae6c774b285f0/chardet-7.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f51985946b49739968b6dc2fa70e7d8f490bb15574377c5ee114f33d19ef7e", size = 553815, upload-time = "2026-03-04T21:25:10.861Z" }, + { url = "https://files.pythonhosted.org/packages/b9/45/860a82d618e5c3930faef0a0fe205b752323e5d10ce0c18fe5016fd4f8d2/chardet-7.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8714f0013c208452a98e23595d99cef53c5364565454425f431446eb586e2591", size = 557506, upload-time = "2026-03-04T21:25:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/ed/44/7acb8f84fc7b5ad3c977ac31865b308881da1c0a6ca58be35554d2473dd7/chardet-7.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:c12abc65830068ad05bd257fb953aaaf63a551446688e03e145522086be5738c", size = 524145, upload-time = "2026-03-04T21:25:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1f/c1a089db6333b1283409cad3714b8935e7e56722c9c60f9299726a1e57c2/chardet-7.0.1-py3-none-any.whl", hash = "sha256:e51e1ff2c51b2d622d97c9737bd5ee9d9b9038f05b7dd8f9ea10b9e2d9674c24", size = 408292, upload-time = "2026-03-04T21:25:25.214Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, ] [[package]] name = "chromadb" -version = "1.5.1" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, @@ -640,13 +661,13 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b6/b7bd96a44a94698d10bb61a7714439108f06900f6c89e005e66b5f64ccb9/chromadb-1.5.1.tar.gz", hash = "sha256:1ebf53664f6d2064c07681741016c80f5f47e7d61d1eba0d654d01823842a516", size = 2379368, upload-time = "2026-02-19T19:59:32.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/48/aa5906f9f817b73c9e87e085d3a64705d91b7bb4f76f4649b9379baea980/chromadb-1.5.2.tar.gz", hash = "sha256:4fc3535a0fcd45343f93d298591882f68e659f24ed319aef14094b168105f956", size = 2386235, upload-time = "2026-02-27T19:49:34.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/c3/598e28a67db38ffc377f30c49f37cad865be2fe261d719fa84641b07ff72/chromadb-1.5.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0ca6e9f8110e848eeb2807994184b50380b35a59bce09d7acff850ec35c735f9", size = 20732567, upload-time = "2026-02-19T19:59:30.269Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/e219be6a44ffc6d7f8012cc6987e1618561a20a8673341f696f9feb93890/chromadb-1.5.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8f4c06709e5bd8f6af1a2196db8500dc728697aef4a8cb4f8f37b47338582032", size = 19993506, upload-time = "2026-02-19T19:59:26.734Z" }, - { url = "https://files.pythonhosted.org/packages/21/25/b4dbc81e174bb6e661c5aa48d03598f0d5c0e8267461b608e861dcb841d4/chromadb-1.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa1a63c757c2a9a61820aab81d6ad4921e7394daf4f0cf04c8690d30274530f2", size = 20643281, upload-time = "2026-02-19T19:59:18.496Z" }, - { url = "https://files.pythonhosted.org/packages/24/6b/051e4684966599991d9fc6fe10cf2fd8d84e08bfe8752485c74111167543/chromadb-1.5.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89ff9f7185238b352c498181b3cfa9e28f7f3336c2b8d7ab8cdfe4f3d76e5e96", size = 21516981, upload-time = "2026-02-19T19:59:22.439Z" }, - { url = "https://files.pythonhosted.org/packages/84/a2/023696860162c59ed7d5d2a589d701bf5c54233d82a0f808c69956204c10/chromadb-1.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:7ec9dc47841cf3fecc475ca07a0aacfc9a347b3460881051636755618d6250c6", size = 21856118, upload-time = "2026-02-19T19:59:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3b/36989e7ebfa2ee10a85deacd423989b07f9e3bd176846863ace1305e9460/chromadb-1.5.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a898ab200f9a22a16751eed5444dac330f1f82184264e16d5420e41e0afe63e4", size = 20733964, upload-time = "2026-02-27T19:49:31.683Z" }, + { url = "https://files.pythonhosted.org/packages/85/b3/db3e5a8a47106d339c3e109e73859647969a81e9c54ca15bc6dde6685c1e/chromadb-1.5.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e6a12adb34bf441f8cc368b6460fbc9e14bee5cf926f34e752da759d68dec56", size = 19993688, upload-time = "2026-02-27T19:49:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/0a9b6dddac3097f321ec8b057d09a61b4edb2b42b891fce7c2bfd01cd4c3/chromadb-1.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b533db30303ce5a82856ded8897c3cafd3160e1f2dccf5473d0bfdee49a159b3", size = 20642212, upload-time = "2026-02-27T19:49:22.467Z" }, + { url = "https://files.pythonhosted.org/packages/ae/74/b8cd9d9bc72c545a579fd1f7bb44558a801ed5f5bab164a25eea16d51ad9/chromadb-1.5.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48e5b0f300d6f709446a5d9299614e3b6bca997772d810e1298b76b0c4e7dbb", size = 21529269, upload-time = "2026-02-27T19:49:25.699Z" }, + { url = "https://files.pythonhosted.org/packages/e8/96/fa83f81f8b618ffca7527915f99cf054c6f8bd272bf3cf5c0616757083ba/chromadb-1.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:042e746ee0c9db34eef2723c4dca30197ded3bf9d27846d996fd51715ec7b0e3", size = 21863829, upload-time = "2026-02-27T19:49:36.422Z" }, ] [[package]] @@ -938,7 +959,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.6.0" +version = "4.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -946,9 +967,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/5c/88a4068c660a096bbe87efc5b7c190080c9e86919c36ec5f092cb08d852f/cyclopts-4.6.0.tar.gz", hash = "sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5", size = 162724, upload-time = "2026-02-23T15:44:49.286Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/7a/3c3623755561c7f283dd769470e99ae36c46810bf3b3f264d69006f6c97a/cyclopts-4.8.0.tar.gz", hash = "sha256:92cc292d18d8be372e58d8bce1aa966d30f819a5fb3fee02bd2ad4a6bb403f29", size = 164066, upload-time = "2026-03-07T19:39:18.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/eb/1e8337755a70dc7d7ff10a73dc8f20e9352c9ad6c2256ed863ac95cd3539/cyclopts-4.6.0-py3-none-any.whl", hash = "sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2", size = 200518, upload-time = "2026-02-23T15:44:47.854Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/6ec7210775ea5e4989a10d89eda6c5ea7ff06caa614231ad533d74fecac8/cyclopts-4.8.0-py3-none-any.whl", hash = "sha256:ef353da05fec36587d4ebce7a6e4b27515d775d184a23bab4b01426f93ddc8d4", size = 201948, upload-time = "2026-03-07T19:39:19.307Z" }, ] [[package]] @@ -1062,7 +1083,7 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.0.2" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -1082,13 +1103,14 @@ dependencies = [ { name = "python-dotenv" }, { name = "pyyaml" }, { name = "rich" }, + { name = "uncalled-for" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" }, + { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, ] [[package]] @@ -1134,11 +1156,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.24.3" +version = "3.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/92/a8e2479937ff39185d20dd6a851c1a63e55849e447a55e798cc2e1f49c65/filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa", size = 37935, upload-time = "2026-02-19T00:48:20.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/0f/5d0c71a1aefeb08efff26272149e07ab922b64f46c63363756224bd6872e/filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d", size = 24331, upload-time = "2026-02-19T00:48:18.465Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, ] [[package]] @@ -1266,14 +1288,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.72.0" +version = "1.73.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, ] [[package]] @@ -1313,43 +1335,43 @@ wheels = [ [[package]] name = "grpcio" -version = "1.78.1" +version = "1.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/de/de568532d9907552700f80dcec38219d8d298ad9e71f5e0a095abaf2761e/grpcio-1.78.1.tar.gz", hash = "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", size = 12835760, upload-time = "2026-02-20T01:16:10.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/1e/ad774af3b2c84f49c6d8c4a7bea4c40f02268ea8380630c28777edda463b/grpcio-1.78.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:3a8aa79bc6e004394c0abefd4b034c14affda7b66480085d87f5fbadf43b593b", size = 5951132, upload-time = "2026-02-20T01:13:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/48/9d/ad3c284bedd88c545e20675d98ae904114d8517a71b0efc0901e9166628f/grpcio-1.78.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8e1fcb419da5811deb47b7749b8049f7c62b993ba17822e3c7231e3e0ba65b79", size = 11831052, upload-time = "2026-02-20T01:13:09.604Z" }, - { url = "https://files.pythonhosted.org/packages/6d/08/20d12865e47242d03c3ade9bb2127f5b4aded964f373284cfb357d47c5ac/grpcio-1.78.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b071dccac245c32cd6b1dd96b722283b855881ca0bf1c685cf843185f5d5d51e", size = 6524749, upload-time = "2026-02-20T01:13:21.692Z" }, - { url = "https://files.pythonhosted.org/packages/c6/53/a8b72f52b253ec0cfdf88a13e9236a9d717c332b8aa5f0ba9e4699e94b55/grpcio-1.78.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d6fb962947e4fe321eeef3be1ba5ba49d32dea9233c825fcbade8e858c14aaf4", size = 7198995, upload-time = "2026-02-20T01:13:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/13/3c/ac769c8ded1bcb26bb119fb472d3374b481b3cf059a0875db9fc77139c17/grpcio-1.78.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6afd191551fd72e632367dfb083e33cd185bf9ead565f2476bba8ab864ae496", size = 6730770, upload-time = "2026-02-20T01:13:26.522Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c3/2275ef4cc5b942314321f77d66179be4097ff484e82ca34bf7baa5b1ddbc/grpcio-1.78.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b2acd83186305c0802dbc4d81ed0ec2f3e8658d7fde97cfba2f78d7372f05b89", size = 7305036, upload-time = "2026-02-20T01:13:30.923Z" }, - { url = "https://files.pythonhosted.org/packages/91/cb/3c2aa99e12cbbfc72c2ed8aa328e6041709d607d668860380e6cd00ba17d/grpcio-1.78.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5380268ab8513445740f1f77bd966d13043d07e2793487e61fd5b5d0935071eb", size = 8288641, upload-time = "2026-02-20T01:13:39.42Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b2/21b89f492260ac645775d9973752ca873acfd0609d6998e9d3065a21ea2f/grpcio-1.78.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:389b77484959bdaad6a2b7dda44d7d1228381dd669a03f5660392aa0e9385b22", size = 7730967, upload-time = "2026-02-20T01:13:41.697Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6b89eddf87fdffb8fa9d37375d44d3a798f4b8116ac363a5f7ca84caa327/grpcio-1.78.1-cp311-cp311-win32.whl", hash = "sha256:9dee66d142f4a8cca36b5b98a38f006419138c3c89e72071747f8fca415a6d8f", size = 4076680, upload-time = "2026-02-20T01:13:43.781Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a8/204460b1bc1dff9862e98f56a2d14be3c4171f929f8eaf8c4517174b4270/grpcio-1.78.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b930cf4f9c4a2262bb3e5d5bc40df426a72538b4f98e46f158b7eb112d2d70", size = 4801074, upload-time = "2026-02-20T01:13:46.315Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ed/d2eb9d27fded1a76b2a80eb9aa8b12101da7e41ce2bac0ad3651e88a14ae/grpcio-1.78.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", size = 5913389, upload-time = "2026-02-20T01:13:49.005Z" }, - { url = "https://files.pythonhosted.org/packages/69/1b/40034e9ab010eeb3fa41ec61d8398c6dbf7062f3872c866b8f72700e2522/grpcio-1.78.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", size = 11811839, upload-time = "2026-02-20T01:13:51.839Z" }, - { url = "https://files.pythonhosted.org/packages/b4/69/fe16ef2979ea62b8aceb3a3f1e7a8bbb8b717ae2a44b5899d5d426073273/grpcio-1.78.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", size = 6475805, upload-time = "2026-02-20T01:13:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/1e/069e0a9062167db18446917d7c00ae2e91029f96078a072bedc30aaaa8c3/grpcio-1.78.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", size = 7169955, upload-time = "2026-02-20T01:13:59.553Z" }, - { url = "https://files.pythonhosted.org/packages/38/fc/44a57e2bb4a755e309ee4e9ed2b85c9af93450b6d3118de7e69410ee05fa/grpcio-1.78.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", size = 6690767, upload-time = "2026-02-20T01:14:02.31Z" }, - { url = "https://files.pythonhosted.org/packages/b8/87/21e16345d4c75046d453916166bc72a3309a382c8e97381ec4b8c1a54729/grpcio-1.78.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", size = 7266846, upload-time = "2026-02-20T01:14:12.974Z" }, - { url = "https://files.pythonhosted.org/packages/11/df/d6261983f9ca9ef4d69893765007a9a3211b91d9faf85a2591063df381c7/grpcio-1.78.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", size = 8253522, upload-time = "2026-02-20T01:14:17.407Z" }, - { url = "https://files.pythonhosted.org/packages/de/7c/4f96a0ff113c5d853a27084d7590cd53fdb05169b596ea9f5f27f17e021e/grpcio-1.78.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", size = 7698070, upload-time = "2026-02-20T01:14:20.032Z" }, - { url = "https://files.pythonhosted.org/packages/17/3c/7b55c0b5af88fbeb3d0c13e25492d3ace41ac9dbd0f5f8f6c0fb613b6706/grpcio-1.78.1-cp312-cp312-win32.whl", hash = "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", size = 4066474, upload-time = "2026-02-20T01:14:22.602Z" }, - { url = "https://files.pythonhosted.org/packages/5d/17/388c12d298901b0acf10b612b650692bfed60e541672b1d8965acbf2d722/grpcio-1.78.1-cp312-cp312-win_amd64.whl", hash = "sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", size = 4797537, upload-time = "2026-02-20T01:14:25.444Z" }, - { url = "https://files.pythonhosted.org/packages/df/72/754754639cfd16ad04619e1435a518124b2d858e5752225376f9285d4c51/grpcio-1.78.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", size = 5919437, upload-time = "2026-02-20T01:14:29.403Z" }, - { url = "https://files.pythonhosted.org/packages/5c/84/6267d1266f8bc335d3a8b7ccf981be7de41e3ed8bd3a49e57e588212b437/grpcio-1.78.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", size = 11803701, upload-time = "2026-02-20T01:14:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/f3/56/c9098e8b920a54261cd605bbb040de0cde1ca4406102db0aa2c0b11d1fb4/grpcio-1.78.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", size = 6479416, upload-time = "2026-02-20T01:14:35.926Z" }, - { url = "https://files.pythonhosted.org/packages/86/cf/5d52024371ee62658b7ed72480200524087528844ec1b65265bbcd31c974/grpcio-1.78.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", size = 7174087, upload-time = "2026-02-20T01:14:39.98Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/5e59551afad4279e27335a6d60813b8aa3ae7b14fb62cea1d329a459c118/grpcio-1.78.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", size = 6692881, upload-time = "2026-02-20T01:14:42.466Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/940062de2d14013c02f51b079eb717964d67d46f5d44f22038975c9d9576/grpcio-1.78.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", size = 7269092, upload-time = "2026-02-20T01:14:45.826Z" }, - { url = "https://files.pythonhosted.org/packages/09/87/9db657a4b5f3b15560ec591db950bc75a1a2f9e07832578d7e2b23d1a7bd/grpcio-1.78.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", size = 8252037, upload-time = "2026-02-20T01:14:48.57Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/b980e0265479ec65e26b6e300a39ceac33ecb3f762c2861d4bac990317cf/grpcio-1.78.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28", size = 7695243, upload-time = "2026-02-20T01:14:51.376Z" }, - { url = "https://files.pythonhosted.org/packages/98/46/5fc42c100ab702fa1ea41a75c890c563c3f96432b4a287d5a6369654f323/grpcio-1.78.1-cp313-cp313-win32.whl", hash = "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", size = 4065329, upload-time = "2026-02-20T01:14:53.952Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/806d60bb6611dfc16cf463d982bd92bd8b6bd5f87dfac66b0a44dfe20995/grpcio-1.78.1-cp313-cp313-win_amd64.whl", hash = "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", size = 4797637, upload-time = "2026-02-20T01:14:57.276Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, ] [[package]] @@ -1376,26 +1398,26 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.3.1" +version = "1.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/d0/73454ef7ca885598a3194d07d5c517d91a840753c5b35d272600d7907f64/hf_xet-1.3.1.tar.gz", hash = "sha256:513aa75f8dc39a63cc44dbc8d635ccf6b449e07cdbd8b2e2d006320d2e4be9bb", size = 641393, upload-time = "2026-02-25T00:57:56.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/79/9b6a5614230d7a871442d8d8e1c270496821638ba3a9baac16a5b9166200/hf_xet-1.3.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:08b231260c68172c866f7aa7257c165d0c87887491aafc5efeee782731725366", size = 3759716, upload-time = "2026-02-25T00:57:41.052Z" }, - { url = "https://files.pythonhosted.org/packages/d4/de/72acb8d7702b3cf9b36a68e8380f3114bf04f9f21cf9e25317457fe31f00/hf_xet-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0810b69c64e96dee849036193848007f665dca2311879c9ea8693f4fc37f1795", size = 3518075, upload-time = "2026-02-25T00:57:39.605Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/ed728d8530fec28da88ee882b522fccf00dc98e9d7bae4cdb0493070cb17/hf_xet-1.3.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ecd38f98e7f0f41108e30fd4a9a5553ec30cf726df7473dd3e75a1b6d56728c2", size = 4174369, upload-time = "2026-02-25T00:57:32.697Z" }, - { url = "https://files.pythonhosted.org/packages/3c/db/785a0e20aa3086948a26573f1d4ff5c090e63564bf0a52d32eb5b4d82e8d/hf_xet-1.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65411867d46700765018b1990eb1604c3bf0bf576d9e65fc57fdcc10797a2eb9", size = 3953249, upload-time = "2026-02-25T00:57:30.096Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6a/51b669c1e3dbd9374b61356f554e8726b9e1c1d6a7bee5d727d3913b10ad/hf_xet-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1684c840c60da12d76c2a031ba40e4b154fdbf9593836fcf5ff090d95a033c61", size = 4152989, upload-time = "2026-02-25T00:57:48.308Z" }, - { url = "https://files.pythonhosted.org/packages/df/31/de07e26e396f46d13a09251df69df9444190e93e06a9d30d639e96c8a0ed/hf_xet-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3012c0f2ce1f0863338491a2bc0fd3f84aded0e147ab25f230da1f5249547fd", size = 4390709, upload-time = "2026-02-25T00:57:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c1/fcb010b54488c2c112224f55b71f80e44d1706d9b764a0966310b283f86e/hf_xet-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:4eb432e1aa707a65a7e1f8455e40c5b47431d44fe0fb1b0c5d53848c27469398", size = 3634142, upload-time = "2026-02-25T00:57:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/9ef49cc601c68209979661b3e0b6659fc5a47bfb40f3ebf29eae9ee09e5c/hf_xet-1.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:e56104c84b2a88b9c7b23ba11a2d7ed0ccbe96886b3f985a50cedd2f0e99853f", size = 3494918, upload-time = "2026-02-25T00:57:57.654Z" }, - { url = "https://files.pythonhosted.org/packages/75/f8/c2da4352c0335df6ae41750cf5bab09fdbfc30d3b4deeed9d621811aa835/hf_xet-1.3.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:581d1809a016f7881069d86a072168a8199a46c839cf394ff53970a47e4f1ca1", size = 3761755, upload-time = "2026-02-25T00:57:43.621Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e5/a2f3eaae09da57deceb16a96ebe9ae1f6f7b9b94145a9cd3c3f994e7782a/hf_xet-1.3.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:329c80c86f2dda776bafd2e4813a46a3ee648dce3ac0c84625902c70d7a6ddba", size = 3523677, upload-time = "2026-02-25T00:57:42.3Z" }, - { url = "https://files.pythonhosted.org/packages/61/cd/acbbf9e51f17d8cef2630e61741228e12d4050716619353efc1ac119f902/hf_xet-1.3.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2973c3ff594c3a8da890836308cae1444c8af113c6f10fe6824575ddbc37eca7", size = 4178557, upload-time = "2026-02-25T00:57:35.399Z" }, - { url = "https://files.pythonhosted.org/packages/df/4f/014c14c4ae3461d9919008d0bed2f6f35ba1741e28b31e095746e8dac66f/hf_xet-1.3.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ed4bfd2e6d10cb86c9b0f3483df1d7dd2d0220f75f27166925253bacbc1c2dbe", size = 3958975, upload-time = "2026-02-25T00:57:34.004Z" }, - { url = "https://files.pythonhosted.org/packages/86/50/043f5c5a26f3831c3fa2509c17fcd468fd02f1f24d363adc7745fbe661cb/hf_xet-1.3.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:713913387cc76e300116030705d843a9f15aee86158337eeffb9eb8d26f47fcd", size = 4158298, upload-time = "2026-02-25T00:57:51.14Z" }, - { url = "https://files.pythonhosted.org/packages/08/9c/b667098a636a88358dbeb2caf90e3cb9e4b961f61f6c55bb312793424def/hf_xet-1.3.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5063789c9d21f51e9ed4edbee8539655d3486e9cad37e96b7af967da20e8b16", size = 4395743, upload-time = "2026-02-25T00:57:52.783Z" }, - { url = "https://files.pythonhosted.org/packages/70/37/4db0e4e1534270800cfffd5a7e0b338f2137f8ceb5768000147650d34ea9/hf_xet-1.3.1-cp37-abi3-win_amd64.whl", hash = "sha256:607d5bbc2730274516714e2e442a26e40e3330673ac0d0173004461409147dee", size = 3638145, upload-time = "2026-02-25T00:58:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/4e/46/1ba8d36f8290a4b98f78898bdce2b0e8fe6d9a59df34a1399eb61a8d877f/hf_xet-1.3.1-cp37-abi3-win_arm64.whl", hash = "sha256:851b1be6597a87036fe7258ce7578d5df3c08176283b989c3b165f94125c5097", size = 3500490, upload-time = "2026-02-25T00:58:00.667Z" }, + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, ] [[package]] @@ -1480,7 +1502,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.4.1" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1489,14 +1511,13 @@ dependencies = [ { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "shellingham" }, { name = "tqdm" }, - { name = "typer-slim" }, + { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/fc/eb9bc06130e8bbda6a616e1b80a7aa127681c448d6b49806f61db2670b61/huggingface_hub-1.4.1.tar.gz", hash = "sha256:b41131ec35e631e7383ab26d6146b8d8972abc8b6309b963b306fbcca87f5ed5", size = 642156, upload-time = "2026-02-06T09:20:03.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7a/304cec37112382c4fe29a43bcb0d5891f922785d18745883d2aa4eb74e4b/huggingface_hub-1.6.0.tar.gz", hash = "sha256:d931ddad8ba8dfc1e816bf254810eb6f38e5c32f60d4184b5885662a3b167325", size = 717071, upload-time = "2026-03-06T14:19:18.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/ae/2f6d96b4e6c5478d87d606a1934b5d436c4a2bce6bb7c6fdece891c128e3/huggingface_hub-1.4.1-py3-none-any.whl", hash = "sha256:9931d075fb7a79af5abc487106414ec5fba2c0ae86104c0c62fd6cae38873d18", size = 553326, upload-time = "2026-02-06T09:20:00.728Z" }, + { url = "https://files.pythonhosted.org/packages/92/e3/e3a44f54c8e2f28983fcf07f13d4260b37bd6a0d3a081041bc60b91d230e/huggingface_hub-1.6.0-py3-none-any.whl", hash = "sha256:ef40e2d5cb85e48b2c067020fa5142168342d5108a1b267478ed384ecbf18961", size = 612874, upload-time = "2026-03-06T14:19:16.844Z" }, ] [[package]] @@ -1620,14 +1641,14 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.1.0" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, ] [[package]] @@ -1779,16 +1800,16 @@ wheels = [ [[package]] name = "jsonschema-path" -version = "0.4.2" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/da/1ebeb1c0ff579c330e200e8b06e6200653e3d0758136d8bd86762d63e7de/jsonschema_path-0.4.2.tar.gz", hash = "sha256:5f5ff183150030ea24bb51cf1ddac9bf5dbf030272e2792a7ffe8262f7eea2a5", size = 13417, upload-time = "2026-02-23T16:21:36.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/10/96f8fe82137979fcd1e46fff243ce7d80cd03b9e1cee8f22476ce780f38c/jsonschema_path-0.4.2-py3-none-any.whl", hash = "sha256:9c3d88e727cc4f1a88e51dbbed4211dbcd815d27799d2685efd904435c3d39e7", size = 16702, upload-time = "2026-02-23T16:21:35.119Z" }, + { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, ] [[package]] @@ -1907,7 +1928,7 @@ wheels = [ [[package]] name = "langchain-classic" -version = "1.0.1" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1918,9 +1939,9 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/4b/bd03518418ece4c13192a504449b58c28afee915dc4a6f4b02622458cb1b/langchain_classic-1.0.1.tar.gz", hash = "sha256:40a499684df36b005a1213735dc7f8dca8f5eb67978d6ec763e7a49780864fdc", size = 10516020, upload-time = "2025-12-23T22:55:22.615Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/6f/59da67274d8ceea16d0610142af33e348a24750894f08c0688de01504ff2/langchain_classic-1.0.2.tar.gz", hash = "sha256:bbf686613d0051905794f2646ecb6a79fa398db399750a4af039107d93054335", size = 10533928, upload-time = "2026-03-06T20:19:46.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0f/eab87f017d7fe28e8c11fff614f4cdbfae32baadb77d0f79e9f922af1df2/langchain_classic-1.0.1-py3-none-any.whl", hash = "sha256:131d83a02bb80044c68fedc1ab4ae885d5b8f8c2c742d8ab9e7534ad9cda8e80", size = 1040666, upload-time = "2025-12-23T22:55:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4b/20f040567a0e97b28649b7190967190f3df5188984c80873bcbe69585168/langchain_classic-1.0.2-py3-none-any.whl", hash = "sha256:ae3480f488d2cf778f75e59ce0316b336fe2bcd4f8828aeb1aef1e2f26987d7f", size = 1041471, upload-time = "2026-03-06T20:19:44.568Z" }, ] [[package]] @@ -1948,7 +1969,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.16" +version = "1.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1960,9 +1981,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/a7/4c992456dae89a8704afec03e3c2a0149ccc5f29c1cbdd5f4aa77628e921/langchain_core-1.2.16.tar.gz", hash = "sha256:055a4bfe7d62f4ac45ed49fd759ee2e6bdd15abf998fbeea695fda5da2de6413", size = 835286, upload-time = "2026-02-25T16:27:30.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/93/36226f593df52b871fc24d494c274f3a6b2ac76763a2806e7d35611634a1/langchain_core-1.2.17.tar.gz", hash = "sha256:54aa267f3311e347fb2e50951fe08e53761cebfb999ab80e6748d70525bbe872", size = 836130, upload-time = "2026-03-02T22:47:55.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/a1/57d5feaa11dc2ebb40f3bc3d7bf4294b6703e152e56edea9d4c622475a6a/langchain_core-1.2.16-py3-none-any.whl", hash = "sha256:2768add9aa97232a7712580f678e0ba045ee1036c71fe471355be0434fcb6e30", size = 502219, upload-time = "2026-02-25T16:27:29.379Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/073f33ab383a62908eca7ea699586dfea280e77182176e33199c80ddf22a/langchain_core-1.2.17-py3-none-any.whl", hash = "sha256:bf6bd6ce503874e9c2da1669a69383e967c3de1ea808921d19a9a6bff1a9fbbe", size = 502727, upload-time = "2026-03-02T22:47:54.537Z" }, ] [[package]] @@ -1979,7 +2000,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.7.7" +version = "0.7.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1992,9 +2013,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/bd/1f11fd1203ced8aee16b24fe8709edef60e692fdfbec50a81fb9fd530d53/langsmith-0.7.7.tar.gz", hash = "sha256:2294d3c4a5a8205ef38880c1c412d85322e6055858ae999ef6641c815995d437", size = 1058459, upload-time = "2026-02-25T19:21:35.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/32/b3931027ff7d635a66a0edbeec9f8a285fe77b04f1f0cbbc58fd20f2555a/langsmith-0.7.14.tar.gz", hash = "sha256:95606314a8dea0ea1ff3650da4cf0433737b14c4c296579c6b770b43cb5e0b37", size = 1113666, upload-time = "2026-03-06T20:13:17.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/a6/6025ccbdffe3533d444cdd0e989ff29bd273cb0ea6701c91dbbfeecc657a/langsmith-0.7.7-py3-none-any.whl", hash = "sha256:ef3d0aff77917bf3776368e90f387df5ffd7cb7cff11ece0ec4fd227e433b5de", size = 339539, upload-time = "2026-02-25T19:21:33.719Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4f/b81ee2d06e1d69aa689b43d2b777901c060d257507806cad7cd9035d5ca4/langsmith-0.7.14-py3-none-any.whl", hash = "sha256:754dcb474a3f3f83cfefbd9694b897bce2a1a0b412bf75e256f85a64206ddcb7", size = 347350, upload-time = "2026-03-06T20:13:15.706Z" }, ] [[package]] @@ -2055,19 +2076,19 @@ wheels = [ [[package]] name = "linkify-it-py" -version = "2.0.3" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uc-micro-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] [[package]] name = "litellm" -version = "1.81.15" +version = "1.82.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2083,9 +2104,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/0c/62a0fdc5adae6d205338f9239175aa6a93818e58b75cf000a9c7214a3d9f/litellm-1.81.15.tar.gz", hash = "sha256:a8a6277a53280762051c5818ebc76dd5f036368b9426c6f21795ae7f1ac6ebdc", size = 16597039, upload-time = "2026-02-24T06:52:50.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/00/49bb5c28e0dea0f5086229a2a08d5fdc6c8dc0d8e2acb2a2d1f7dd9f4b70/litellm-1.82.0.tar.gz", hash = "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22", size = 16800355, upload-time = "2026-03-01T02:35:30.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/fd/da11826dda0d332e360b9ead6c0c992d612ecb85b00df494823843cfcda3/litellm-1.81.15-py3-none-any.whl", hash = "sha256:2fa253658702509ce09fe0e172e5a47baaadf697fb0f784c7fd4ff665ae76ae1", size = 14682123, upload-time = "2026-02-24T06:52:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/28/89/eb28bfcf97d6b045c400e72eb047c381594467048c237dbb6c227764084c/litellm-1.82.0-py3-none-any.whl", hash = "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", size = 14911978, upload-time = "2026-03-01T02:35:26.844Z" }, ] [[package]] @@ -2361,63 +2382,63 @@ wheels = [ [[package]] name = "mmh3" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107, upload-time = "2025-07-29T07:41:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635, upload-time = "2025-07-29T07:41:57.903Z" }, - { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078, upload-time = "2025-07-29T07:41:58.772Z" }, - { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262, upload-time = "2025-07-29T07:41:59.678Z" }, - { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118, upload-time = "2025-07-29T07:42:01.197Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072, upload-time = "2025-07-29T07:42:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925, upload-time = "2025-07-29T07:42:03.632Z" }, - { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583, upload-time = "2025-07-29T07:42:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127, upload-time = "2025-07-29T07:42:05.929Z" }, - { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544, upload-time = "2025-07-29T07:42:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262, upload-time = "2025-07-29T07:42:07.804Z" }, - { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824, upload-time = "2025-07-29T07:42:08.735Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255, upload-time = "2025-07-29T07:42:09.706Z" }, - { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779, upload-time = "2025-07-29T07:42:10.546Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549, upload-time = "2025-07-29T07:42:11.399Z" }, - { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336, upload-time = "2025-07-29T07:42:12.209Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, - { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, - { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, - { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, - { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, - { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, - { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, - { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, - { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, - { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, - { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, - { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, - { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, - { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, - { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, - { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, - { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, - { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, - { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, - { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/d7/3312a59df3c1cdd783f4cf0c4ee8e9decff9c5466937182e4cc7dbbfe6c5/mmh3-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dae0f0bd7d30c0ad61b9a504e8e272cb8391eed3f1587edf933f4f6b33437450", size = 56082, upload-time = "2026-03-05T15:53:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/6f617baa098ca0d2989bfec6d28b5719532cd8d8848782662f5b755f657f/mmh3-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aeaf53eaa075dd63e81512522fd180097312fb2c9f476333309184285c49ce0", size = 40458, upload-time = "2026-03-05T15:54:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/9cd284bd6062d711e13d26c04d4778ab3f690c1c38a4563e3c767ec8802e/mmh3-5.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0634581290e6714c068f4aa24020acf7880927d1f0084fa753d9799ae9610082", size = 40079, upload-time = "2026-03-05T15:54:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/f6/09/a806334ce1d3d50bf782b95fcee8b3648e1e170327d4bb7b4bad2ad7d956/mmh3-5.2.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080c0637aea036f35507e803a4778f119a9b436617694ae1c5c366805f1e997", size = 97242, upload-time = "2026-03-05T15:54:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/ee/93/723e317dd9e041c4dc4566a2eb53b01ad94de31750e0b834f1643905e97c/mmh3-5.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db0562c5f71d18596dcd45e854cf2eeba27d7543e1a3acdafb7eef728f7fe85d", size = 103082, upload-time = "2026-03-05T15:54:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/61/b5/f96121e69cc48696075071531cf574f112e1ffd08059f4bffb41210e6fc5/mmh3-5.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d9f9a3ce559a5267014b04b82956993270f63ec91765e13e9fd73daf2d2738e", size = 106054, upload-time = "2026-03-05T15:54:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/192b987ec48d0b2aecf8ac285a9b11fbc00030f6b9c694664ae923458dde/mmh3-5.2.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:960b1b3efa39872ac8b6cc3a556edd6fb90ed74f08c9c45e028f1005b26aa55d", size = 112910, upload-time = "2026-03-05T15:54:09.403Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/03e91fd334ed0144b83343a76eb11f17434cd08f746401488cfeafb2d241/mmh3-5.2.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d30b650595fdbe32366b94cb14f30bb2b625e512bd4e1df00611f99dc5c27fd4", size = 120551, upload-time = "2026-03-05T15:54:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/b89a71d2ff35c3a764d1c066c7313fc62c7cc48fa48a4b3b0304a4a0146f/mmh3-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82f3802bfc4751f420d591c5c864de538b71cea117fce67e4595c2afede08a15", size = 99096, upload-time = "2026-03-05T15:54:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/b5/613772c1c6ed5f7b63df55eb131e887cc43720fec392777b95a79d34e640/mmh3-5.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:915e7a2418f10bd1151b1953df06d896db9783c9cfdb9a8ee1f9b3a4331ab503", size = 98524, upload-time = "2026-03-05T15:54:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/1524566fe8eaf871e4f7bc44095929fcd2620488f402822d848df19d679c/mmh3-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fc78739b5ec6e4fb02301984a3d442a91406e7700efbe305071e7fd1c78278f2", size = 106239, upload-time = "2026-03-05T15:54:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/21adfa7d90a7a697137ad6de33eeff6445420ca55e433a5d4919c79bc3b5/mmh3-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:41aac7002a749f08727cb91babff1daf8deac317c0b1f317adc69be0e6c375d1", size = 109797, upload-time = "2026-03-05T15:54:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e6/1aacc3a219e1aa62fa65669995d4a3562b35be5200ec03680c7e4bec9676/mmh3-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9d8089d853c7963a8ce87fff93e2a67075c0bc08684a08ea6ad13577c38ffc38", size = 97228, upload-time = "2026-03-05T15:54:16.992Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b9/5e4cca8dcccf298add0a27f3c357bc8cf8baf821d35cdc6165e4bd5a48b0/mmh3-5.2.1-cp311-cp311-win32.whl", hash = "sha256:baeb47635cb33375dee4924cd93d7f5dcaa786c740b08423b0209b824a1ee728", size = 40751, upload-time = "2026-03-05T15:54:18.714Z" }, + { url = "https://files.pythonhosted.org/packages/72/fc/5b11d49247f499bcda591171e9cf3b6ee422b19e70aa2cef2e0ae65ca3b9/mmh3-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e4ecee40ba19e6975e1120829796770325841c2f153c0e9aecca927194c6a2a", size = 41517, upload-time = "2026-03-05T15:54:19.764Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/2a511ee8a1c2a527c77726d5231685b72312c5a1a1b7639ad66a9652aa84/mmh3-5.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:c302245fd6c33d96bd169c7ccf2513c20f4c1e417c07ce9dce107c8bc3f8411f", size = 39287, upload-time = "2026-03-05T15:54:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" }, + { url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" }, + { url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" }, + { url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" }, + { url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" }, + { url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" }, ] [[package]] @@ -2699,7 +2720,7 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.24.2" +version = "1.24.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, @@ -2709,28 +2730,28 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/4e/050c947924ffd8ff856d219d8f83ee3d4e7dc52d5a6770ff34a15675c437/onnxruntime-1.24.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:69d1c75997276106d24e65da2e69ec4302af1b117fef414e2154740cde0f6214", size = 17217298, upload-time = "2026-02-19T17:15:09.891Z" }, - { url = "https://files.pythonhosted.org/packages/30/17/c814121dff4de962476ced979c402c3cce72d5d46e87099610b47a1f2622/onnxruntime-1.24.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:670d7e671af2dbd17638472f9b9ff98041889efd7150718406b9ea989312d064", size = 15027128, upload-time = "2026-02-19T17:13:19.367Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/4e5921ba8b82ac37cad45f1108ca6effd430f49c7f20577d53f317d166ed/onnxruntime-1.24.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93fe190ee555ae8e9c1214bcfcf13af85cd06dd835e8d835ce5a8d01056844fe", size = 17107440, upload-time = "2026-02-19T17:14:02.932Z" }, - { url = "https://files.pythonhosted.org/packages/48/55/9d13c97d912db81e81c9b369a49b36f2804fa3bb8de64462e5e6bd412d0b/onnxruntime-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:04a3a80b28dd39739463cb1e34081eed668929ba0b8e1bc861885dcdf66b7601", size = 12506375, upload-time = "2026-02-19T17:14:57.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d4/cf0e0b3bd84e7b68fe911810f7098f414936d1ffb612faa569a3fb8a76a5/onnxruntime-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:a845096277444670b0b52855bb4aad706003540bd34986b50868e9f29606c142", size = 12167758, upload-time = "2026-02-19T17:14:47.386Z" }, - { url = "https://files.pythonhosted.org/packages/23/1c/38af1cfe82c75d2b205eb5019834b0f2b0b6647ec8a20a3086168e413570/onnxruntime-1.24.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d8a50b422d45c0144864c0977d04ad4fa50a8a48e5153056ab1f7d06ea9fc3e2", size = 17217857, upload-time = "2026-02-19T17:15:14.297Z" }, - { url = "https://files.pythonhosted.org/packages/01/8a/e2d4332ae18d6383376e75141cd914256bee12c3cc439f42260eb176ceb9/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c44fc9a89dcefcd5a4ab5c6bbbb9ff1604325ab2d5d0bc9ff5a9cba7b37f4a", size = 15027167, upload-time = "2026-02-19T17:13:21.92Z" }, - { url = "https://files.pythonhosted.org/packages/35/af/ad86cfbfd65d5a86204b3a30893e92c0cf3f1a56280efc5a12e69d81f52d/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09aa6f8d766b4afc3cfba68dd10be39586b49f9462fbd1386c5d5644239461ca", size = 17106547, upload-time = "2026-02-19T17:14:05.758Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/9d725326f933bf8323e309956a17e52d33fb59d35bb5dda1886f94352938/onnxruntime-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:ebcee9276420a65e5fa08b05f18379c2271b5992617e5bdc0d0d6c5ea395c1a1", size = 12506161, upload-time = "2026-02-19T17:14:59.377Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a9/7b06efd5802db881860d961a7cb4efacb058ed694c1c8f096c0c1499d017/onnxruntime-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:8d770a934513f6e17937baf3438eaaec5983a23cdaedb81c9fc0dfcf26831c24", size = 12169884, upload-time = "2026-02-19T17:14:49.962Z" }, - { url = "https://files.pythonhosted.org/packages/9c/98/8f5b9ae63f7f6dd5fb2d192454b915ec966a421fdd0effeeef5be7f7221f/onnxruntime-1.24.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:038ebcd8363c3835ea83eed66129e1d11d8219438892dfb7dc7656c4d4dfa1f9", size = 17217884, upload-time = "2026-02-19T17:13:36.193Z" }, - { url = "https://files.pythonhosted.org/packages/55/e6/dc4dc59565c93506c45017c0dd3f536f6d1b7bc97047821af13fba2e3def/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8235cc11e118ad749c497ba93288c04073eccd8cc6cc508c8a7988ae36ab52d8", size = 15026995, upload-time = "2026-02-19T17:13:25.029Z" }, - { url = "https://files.pythonhosted.org/packages/ac/62/6f2851cf3237a91bc04cdb35434293a623d4f6369f79836929600da574ba/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92b46cc6d8be4286436a05382a881c88d85a2ae1ea9cfe5e6fab89f2c3e89cc", size = 17106308, upload-time = "2026-02-19T17:14:09.817Z" }, - { url = "https://files.pythonhosted.org/packages/62/5a/1e2b874daf24f26e98af14281fdbdd6ae1ed548ba471c01ea2a3084c55bb/onnxruntime-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:1fd824ee4f6fb811bc47ffec2b25f129f31a087214ca91c8b4f6fda32962b78f", size = 12506095, upload-time = "2026-02-19T17:15:02.434Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/8fac5eecb94f861d56a43ede3c2ebcdce60132952d3b72003f3e3d91483c/onnxruntime-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:d8cf0acbf90771fff012c33eb2749e8aca2a8b4c66c672f30ee77c140a6fba5b", size = 12168564, upload-time = "2026-02-19T17:14:52.28Z" }, - { url = "https://files.pythonhosted.org/packages/35/e4/7dfed3f445f7289a0abff709d012439c6c901915390704dd918e5f47aad3/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e22fb5d9ac51b61f50cca155ce2927576cc2c42501ede6c0df23a1aeb070bdd5", size = 15036844, upload-time = "2026-02-19T17:13:27.928Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/9d52397e30b0d8c1692afcec5184ca9372ff4d6b0f6039bba9ad479a2563/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2956f5220e7be8b09482ae5726caabf78eb549142cdb28523191a38e57fb6119", size = 17117779, upload-time = "2026-02-19T17:14:13.862Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/3253db975a90c3ce1d475e2a230773a21cd7998537f0657947df6fb79861/onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c", size = 17332766, upload-time = "2026-03-05T17:18:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/3af6b325f1492d691b23844d88ed26844c1164620860c5efe95c0e22782d/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978", size = 15130330, upload-time = "2026-03-05T16:34:53.831Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/f96b46c1866a293ed23ca2cf5e5a63d413ad3a951da60dd877e3c56cbbca/onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d", size = 17213247, upload-time = "2026-03-05T17:17:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/27cf4d8df2578747584e8758aeb0b673b60274048510257f1f084b15e80e/onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39", size = 12595530, upload-time = "2026-03-05T17:18:49.356Z" }, + { url = "https://files.pythonhosted.org/packages/19/8c/6d9f31e6bae72a8079be12ed8ba36c4126a571fad38ded0a1b96f60f6896/onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723", size = 12261715, upload-time = "2026-03-05T17:18:39.699Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/dfdc4e52600fde4c02d59bfe98c4b057931c1114b701e175aee311a9bc11/onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91", size = 17342578, upload-time = "2026-03-05T17:19:02.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/dc/1f5489f7b21817d4ad352bf7a92a252bd5b438bcbaa7ad20ea50814edc79/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452", size = 15150105, upload-time = "2026-03-05T16:34:56.897Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/fd253da53594ab8efbefdc85b3638620ab1a6aab6eb7028a513c853559ce/onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d", size = 17237101, upload-time = "2026-03-05T17:18:02.561Z" }, + { url = "https://files.pythonhosted.org/packages/71/5f/eaabc5699eeed6a9188c5c055ac1948ae50138697a0428d562ac970d7db5/onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a", size = 12597638, upload-time = "2026-03-05T17:18:52.141Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/d8066c320b90610dbeb489a483b132c3b3879b2f93f949fb5d30cfa9b119/onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8", size = 12270943, upload-time = "2026-03-05T17:18:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/51/8d/487ece554119e2991242d4de55de7019ac6e47ee8dfafa69fcf41d37f8ed/onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7", size = 17342706, upload-time = "2026-03-05T16:35:10.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/25/8b444f463c1ac6106b889f6235c84f01eec001eaf689c3eff8c69cf48fae/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7", size = 15149956, upload-time = "2026-03-05T16:34:59.264Z" }, + { url = "https://files.pythonhosted.org/packages/34/fc/c9182a3e1ab46940dd4f30e61071f59eee8804c1f641f37ce6e173633fb6/onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9", size = 17237370, upload-time = "2026-03-05T17:18:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/3b549e1f4538514118bff98a1bcd6481dd9a17067f8c9af77151621c9a5c/onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b", size = 12597939, upload-time = "2026-03-05T17:18:54.772Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/9696a5c4631a0caa75cc8bc4efd30938fd483694aa614898d087c3ee6d29/onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b", size = 12270705, upload-time = "2026-03-05T17:18:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/b7/65/a26c5e59e3b210852ee04248cf8843c81fe7d40d94cf95343b66efe7eec9/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34", size = 15161796, upload-time = "2026-03-05T16:35:02.871Z" }, + { url = "https://files.pythonhosted.org/packages/f3/25/2035b4aa2ccb5be6acf139397731ec507c5f09e199ab39d3262b22ffa1ac/onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d", size = 17240936, upload-time = "2026-03-05T17:18:09.534Z" }, ] [[package]] name = "openai" -version = "2.24.0" +version = "2.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2742,9 +2763,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, + { 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" }, ] [[package]] @@ -2773,32 +2794,32 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2809,48 +2830,48 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b1" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, ] [[package]] @@ -2926,21 +2947,21 @@ wheels = [ [[package]] name = "patchright" -version = "1.58.0" +version = "1.58.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/61/c6/b1d685ccce237e280d8549454a8b5760e58ab5ee88af9ef875fad2282845/patchright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:caadeec5b4812f12db5e245e78b7c1bdd9c6b38d2c15a59fa3047b04e33a3e60", size = 42229561, upload-time = "2026-01-30T15:26:54.532Z" }, - { url = "https://files.pythonhosted.org/packages/61/13/e5726d38be9ecf9ed714346433f2536eb6423748836f4a22a6701b992ba0/patchright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:af567d94d2d735be8fa88c6ff9418e46361d823f7b28c10c2823e51942739507", size = 41018089, upload-time = "2026-01-30T15:26:58.097Z" }, - { url = "https://files.pythonhosted.org/packages/6c/33/db35661268edc03381bbf61dcb3119f427591562ce45dce90d17e116ffb5/patchright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:ccf8116a97dcef6e3865c9823f51965db069c931346afe5253e25d9486160a92", size = 42229561, upload-time = "2026-01-30T15:27:02.073Z" }, - { url = "https://files.pythonhosted.org/packages/ea/86/98d8f42d5186b6864144fb25e21da8aa7cffa5b9d1d76752276610b9ea58/patchright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:832bee2fe48cf9dc07bb3b0f0d05eee923203f348cd98b14c2c515eece326734", size = 46213732, upload-time = "2026-01-30T15:27:06.187Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b1/7094545c805a31235ef69316ccc910aa5ff5e940c41e85df588ca660f00d/patchright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:431b1df8967b4919d326a3121445c47f15769bc6a10dcebaa699073eb7d125f9", size = 45942677, upload-time = "2026-01-30T15:27:09.981Z" }, - { url = "https://files.pythonhosted.org/packages/4a/11/e21a51c42969473237c92a47d5433b2c58db1ec2bbd3b340ddeb33ac718f/patchright-1.58.0-py3-none-win32.whl", hash = "sha256:5529f66d296e2894789c309a13750b1a20f468daeb7de511f91bbf54cac95d95", size = 36794461, upload-time = "2026-01-30T15:27:13.409Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a1/b7dff0669ce8814c690c67eee1b44b3cdb422593efbbbbc4bfe3bf10f9fa/patchright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:e37109834056feb8e4e4918fb259d497dbfc37e03f9391c0d3cf1532f5fa9b7f", size = 36794467, upload-time = "2026-01-30T15:27:16.613Z" }, - { url = "https://files.pythonhosted.org/packages/91/2a/81ef2b079bbc925a935f2fd73dc1285c46c7eb35c5032a0d63b48d753c4a/patchright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:b044efea1774beac8ee033583eac7181b86ea450da3a36d3039d7a1a428ac098", size = 33064382, upload-time = "2026-01-30T15:27:19.725Z" }, + { url = "https://files.pythonhosted.org/packages/41/2f/afacd242f1ac8265275531c2e1be387f0c3b87ed14accff118c1e824695e/patchright-1.58.2-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:3930464552e52f4d5283998db5797e1797c1869206bce25c065b2d84a69e6bfb", size = 42237382, upload-time = "2026-03-07T07:42:41.261Z" }, + { url = "https://files.pythonhosted.org/packages/9b/38/e8f173299b05bbf5fd0278fbee5ceaf25eab93fece203bb5b08ae924d604/patchright-1.58.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:be76fa83f5b36219375fc0ed52f76de800eb2388844c185bb857a2e107caea13", size = 41025905, upload-time = "2026-03-07T07:42:44.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/08/5c97f3f3300a93c62b417b5dac86d22ad771e0941cd5b59c6054d7716197/patchright-1.58.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:8dc1005c5683c8661de461e5ee85f857b43758f1e2599a7d8a44c50c6ad9c5d7", size = 42237381, upload-time = "2026-03-07T07:42:48.156Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/cb8b7053f2ede3586d89cb7e45f7b643751f8d97b4dfa9af7f4188aac3f9/patchright-1.58.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:13aef416c59f23f0fb552658281890ef349db2bee2e449c159560867c2e6cb61", size = 46221550, upload-time = "2026-03-07T07:42:51.984Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d9/33f3c4839ddbc3255ab012457220d56d7a910174a0a41424f6424a8b156f/patchright-1.58.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e68d0c538b5bd2bd6ef0b1327e9e766c3919d5aeade8b7bd4b29ecd3adfc0b4", size = 45950498, upload-time = "2026-03-07T07:42:55.814Z" }, + { url = "https://files.pythonhosted.org/packages/bb/63/3b054f25a44721b9a530ec12de33d6b5d94cd9952748c2586b2a64ef62ba/patchright-1.58.2-py3-none-win32.whl", hash = "sha256:7dac724893fde90d726b125f7c35507a2afb5480c23cb57f88a31484d131de98", size = 36802278, upload-time = "2026-03-07T07:42:59.362Z" }, + { url = "https://files.pythonhosted.org/packages/c4/11/f06d2f6ae8e0c1aea4b17b18a105dc2ad28e358217896eb3720e80e2d297/patchright-1.58.2-py3-none-win_amd64.whl", hash = "sha256:9b740c13343a6e412efe052d0c17a65910cc4e3fd0fd6b62c1ac8dc1eec4c158", size = 36802282, upload-time = "2026-03-07T07:43:02.775Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ae/a85dca1ebcdfc63e5838783c0929d82066dacd7448e29911d052bbd286cb/patchright-1.58.2-py3-none-win_arm64.whl", hash = "sha256:958cd884787d140dd464ec2901ea85b9634aad5e8444a267f407ee648de04667", size = 33072202, upload-time = "2026-03-07T07:43:06.344Z" }, ] [[package]] @@ -3025,11 +3046,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.2" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] @@ -3739,11 +3760,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { 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" }, ] [[package]] @@ -3784,11 +3805,11 @@ wheels = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -3890,74 +3911,74 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/c0/d8079d4f6342e4cec5c3e7d7415b5cd3e633d5f4124f7a4626908dbe84c7/regex-2026.2.19.tar.gz", hash = "sha256:6fb8cb09b10e38f3ae17cc6dc04a1df77762bd0351b6ba9041438e7cc85ec310", size = 414973, upload-time = "2026-02-19T19:03:47.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/93/43f405a98f54cc59c786efb4fc0b644615ed2392fc89d57d30da11f35b5b/regex-2026.2.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:93b16a18cadb938f0f2306267161d57eb33081a861cee9ffcd71e60941eb5dfc", size = 488365, upload-time = "2026-02-19T19:00:17.857Z" }, - { url = "https://files.pythonhosted.org/packages/66/46/da0efce22cd8f5ae28eeb25ac69703f49edcad3331ac22440776f4ea0867/regex-2026.2.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78af1e499cab704131f6f4e2f155b7f54ce396ca2acb6ef21a49507e4752e0be", size = 290737, upload-time = "2026-02-19T19:00:19.869Z" }, - { url = "https://files.pythonhosted.org/packages/fb/19/f735078448132c1c974974d30d5306337bc297fe6b6f126164bff72c1019/regex-2026.2.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eb20c11aa4c3793c9ad04c19a972078cdadb261b8429380364be28e867a843f2", size = 288654, upload-time = "2026-02-19T19:00:21.307Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/6d7c24a2f423c03ad03e3fbddefa431057186ac1c4cb4fa98b03c7f39808/regex-2026.2.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db5fd91eec71e7b08de10011a2223d0faa20448d4e1380b9daa179fa7bf58906", size = 793785, upload-time = "2026-02-19T19:00:22.926Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/fdb8107504b3122a79bde6705ac1f9d495ed1fe35b87d7cfc1864471999a/regex-2026.2.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fdbade8acba71bb45057c2b72f477f0b527c4895f9c83e6cfc30d4a006c21726", size = 860731, upload-time = "2026-02-19T19:00:25.196Z" }, - { url = "https://files.pythonhosted.org/packages/9a/fd/cc8c6f05868defd840be6e75919b1c3f462357969ac2c2a0958363b4dc23/regex-2026.2.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:31a5f561eb111d6aae14202e7043fb0b406d3c8dddbbb9e60851725c9b38ab1d", size = 907350, upload-time = "2026-02-19T19:00:27.093Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1b/4590db9caa8db3d5a3fe31197c4e42c15aab3643b549ef6a454525fa3a61/regex-2026.2.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4584a3ee5f257b71e4b693cc9be3a5104249399f4116fe518c3f79b0c6fc7083", size = 800628, upload-time = "2026-02-19T19:00:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/76/05/513eaa5b96fa579fd0b813e19ec047baaaf573d7374ff010fa139b384bf7/regex-2026.2.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:196553ba2a2f47904e5dc272d948a746352e2644005627467e055be19d73b39e", size = 773711, upload-time = "2026-02-19T19:00:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/95/65/5aed06d8c54563d37fea496cf888be504879a3981a7c8e12c24b2c92c209/regex-2026.2.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0c10869d18abb759a3317c757746cc913d6324ce128b8bcec99350df10419f18", size = 783186, upload-time = "2026-02-19T19:00:34.598Z" }, - { url = "https://files.pythonhosted.org/packages/2c/57/79a633ad90f2371b4ef9cd72ba3a69a1a67d0cfaab4fe6fa8586d46044ef/regex-2026.2.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e689fed279cbe797a6b570bd18ff535b284d057202692c73420cb93cca41aa32", size = 854854, upload-time = "2026-02-19T19:00:37.306Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2d/0f113d477d9e91ec4545ec36c82e58be25038d06788229c91ad52da2b7f5/regex-2026.2.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0782bd983f19ac7594039c9277cd6f75c89598c1d72f417e4d30d874105eb0c7", size = 762279, upload-time = "2026-02-19T19:00:39.793Z" }, - { url = "https://files.pythonhosted.org/packages/39/cb/237e9fa4f61469fd4f037164dbe8e675a376c88cf73aaaa0aedfd305601c/regex-2026.2.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:dbb240c81cfed5d4a67cb86d7676d9f7ec9c3f186310bec37d8a1415210e111e", size = 846172, upload-time = "2026-02-19T19:00:42.134Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7c/104779c5915cc4eb557a33590f8a3f68089269c64287dd769afd76c7ce61/regex-2026.2.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80d31c3f1fe7e4c6cd1831cd4478a0609903044dfcdc4660abfe6fb307add7f0", size = 789078, upload-time = "2026-02-19T19:00:43.908Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4a/eae4e88b1317fb2ff57794915e0099198f51e760f6280b320adfa0ad396d/regex-2026.2.19-cp311-cp311-win32.whl", hash = "sha256:66e6a43225ff1064f8926adbafe0922b370d381c3330edaf9891cade52daa790", size = 266013, upload-time = "2026-02-19T19:00:47.274Z" }, - { url = "https://files.pythonhosted.org/packages/f9/29/ba89eb8fae79705e07ad1bd69e568f776159d2a8093c9dbc5303ee618298/regex-2026.2.19-cp311-cp311-win_amd64.whl", hash = "sha256:59a7a5216485a1896c5800e9feb8ff9213e11967b482633b6195d7da11450013", size = 277906, upload-time = "2026-02-19T19:00:49.011Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1a/042d8f04b28e318df92df69d8becb0f42221eb3dd4fe5e976522f4337c76/regex-2026.2.19-cp311-cp311-win_arm64.whl", hash = "sha256:ec661807ffc14c8d14bb0b8c1bb3d5906e476bc96f98b565b709d03962ee4dd4", size = 270463, upload-time = "2026-02-19T19:00:50.988Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/13b39c7c9356f333e564ab4790b6cb0df125b8e64e8d6474e73da49b1955/regex-2026.2.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c1665138776e4ac1aa75146669236f7a8a696433ec4e525abf092ca9189247cc", size = 489541, upload-time = "2026-02-19T19:00:52.728Z" }, - { url = "https://files.pythonhosted.org/packages/15/77/fcc7bd9a67000d07fbcc11ed226077287a40d5c84544e62171d29d3ef59c/regex-2026.2.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d792b84709021945597e05656aac059526df4e0c9ef60a0eaebb306f8fafcaa8", size = 291414, upload-time = "2026-02-19T19:00:54.51Z" }, - { url = "https://files.pythonhosted.org/packages/f9/87/3997fc72dc59233426ef2e18dfdd105bb123812fff740ee9cc348f1a3243/regex-2026.2.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db970bcce4d63b37b3f9eb8c893f0db980bbf1d404a1d8d2b17aa8189de92c53", size = 289140, upload-time = "2026-02-19T19:00:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d0/b7dd3883ed1cff8ee0c0c9462d828aaf12be63bf5dc55453cbf423523b13/regex-2026.2.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03d706fbe7dfec503c8c3cb76f9352b3e3b53b623672aa49f18a251a6c71b8e6", size = 798767, upload-time = "2026-02-19T19:00:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7e/8e2d09103832891b2b735a2515abf377db21144c6dd5ede1fb03c619bf09/regex-2026.2.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dbff048c042beef60aa1848961384572c5afb9e8b290b0f1203a5c42cf5af65", size = 864436, upload-time = "2026-02-19T19:01:00.772Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2e/afea8d23a6db1f67f45e3a0da3057104ce32e154f57dd0c8997274d45fcd/regex-2026.2.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccaaf9b907ea6b4223d5cbf5fa5dff5f33dc66f4907a25b967b8a81339a6e332", size = 912391, upload-time = "2026-02-19T19:01:02.865Z" }, - { url = "https://files.pythonhosted.org/packages/59/3c/ea5a4687adaba5e125b9bd6190153d0037325a0ba3757cc1537cc2c8dd90/regex-2026.2.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75472631eee7898e16a8a20998d15106cb31cfde21cdf96ab40b432a7082af06", size = 803702, upload-time = "2026-02-19T19:01:05.298Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c5/624a0705e8473a26488ec1a3a4e0b8763ecfc682a185c302dfec71daea35/regex-2026.2.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d89f85a5ccc0cec125c24be75610d433d65295827ebaf0d884cbe56df82d4774", size = 775980, upload-time = "2026-02-19T19:01:07.047Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/ed776642533232b5599b7c1f9d817fe11faf597e8a92b7a44b841daaae76/regex-2026.2.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9f81806abdca3234c3dd582b8a97492e93de3602c8772013cb4affa12d1668", size = 788122, upload-time = "2026-02-19T19:01:08.744Z" }, - { url = "https://files.pythonhosted.org/packages/8c/58/e93e093921d13b9784b4f69896b6e2a9e09580a265c59d9eb95e87d288f2/regex-2026.2.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9dadc10d1c2bbb1326e572a226d2ec56474ab8aab26fdb8cf19419b372c349a9", size = 858910, upload-time = "2026-02-19T19:01:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/85/77/ff1d25a0c56cd546e0455cbc93235beb33474899690e6a361fa6b52d265b/regex-2026.2.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6bc25d7e15f80c9dc7853cbb490b91c1ec7310808b09d56bd278fe03d776f4f6", size = 764153, upload-time = "2026-02-19T19:01:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ef/8ec58df26d52d04443b1dc56f9be4b409f43ed5ae6c0248a287f52311fc4/regex-2026.2.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:965d59792f5037d9138da6fed50ba943162160443b43d4895b182551805aff9c", size = 850348, upload-time = "2026-02-19T19:01:14.147Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b3/c42fd5ed91639ce5a4225b9df909180fc95586db071f2bf7c68d2ccbfbe6/regex-2026.2.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:38d88c6ed4a09ed61403dbdf515d969ccba34669af3961ceb7311ecd0cef504a", size = 789977, upload-time = "2026-02-19T19:01:15.838Z" }, - { url = "https://files.pythonhosted.org/packages/b6/22/bc3b58ebddbfd6ca5633e71fd41829ee931963aad1ebeec55aad0c23044e/regex-2026.2.19-cp312-cp312-win32.whl", hash = "sha256:5df947cabab4b643d4791af5e28aecf6bf62e6160e525651a12eba3d03755e6b", size = 266381, upload-time = "2026-02-19T19:01:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4a/6ff550b63e67603ee60e69dc6bd2d5694e85046a558f663b2434bdaeb285/regex-2026.2.19-cp312-cp312-win_amd64.whl", hash = "sha256:4146dc576ea99634ae9c15587d0c43273b4023a10702998edf0fa68ccb60237a", size = 277274, upload-time = "2026-02-19T19:01:19.826Z" }, - { url = "https://files.pythonhosted.org/packages/cc/29/9ec48b679b1e87e7bc8517dff45351eab38f74fbbda1fbcf0e9e6d4e8174/regex-2026.2.19-cp312-cp312-win_arm64.whl", hash = "sha256:cdc0a80f679353bd68450d2a42996090c30b2e15ca90ded6156c31f1a3b63f3b", size = 270509, upload-time = "2026-02-19T19:01:22.075Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2d/a849835e76ac88fcf9e8784e642d3ea635d183c4112150ca91499d6703af/regex-2026.2.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8df08decd339e8b3f6a2eb5c05c687fe9d963ae91f352bc57beb05f5b2ac6879", size = 489329, upload-time = "2026-02-19T19:01:23.841Z" }, - { url = "https://files.pythonhosted.org/packages/da/aa/78ff4666d3855490bae87845a5983485e765e1f970da20adffa2937b241d/regex-2026.2.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3aa0944f1dc6e92f91f3b306ba7f851e1009398c84bfd370633182ee4fc26a64", size = 291308, upload-time = "2026-02-19T19:01:25.605Z" }, - { url = "https://files.pythonhosted.org/packages/cd/58/714384efcc07ae6beba528a541f6e99188c5cc1bc0295337f4e8a868296d/regex-2026.2.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c13228fbecb03eadbfd8f521732c5fda09ef761af02e920a3148e18ad0e09968", size = 289033, upload-time = "2026-02-19T19:01:27.243Z" }, - { url = "https://files.pythonhosted.org/packages/75/ec/6438a9344d2869cf5265236a06af1ca6d885e5848b6561e10629bc8e5a11/regex-2026.2.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d0e72703c60d68b18b27cde7cdb65ed2570ae29fb37231aa3076bfb6b1d1c13", size = 798798, upload-time = "2026-02-19T19:01:28.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/be/b1ce2d395e3fd2ce5f2fde2522f76cade4297cfe84cd61990ff48308749c/regex-2026.2.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46e69a4bf552e30e74a8aa73f473c87efcb7f6e8c8ece60d9fd7bf13d5c86f02", size = 864444, upload-time = "2026-02-19T19:01:30.933Z" }, - { url = "https://files.pythonhosted.org/packages/d5/97/a3406460c504f7136f140d9461960c25f058b0240e4424d6fb73c7a067ab/regex-2026.2.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8edda06079bd770f7f0cf7f3bba1a0b447b96b4a543c91fe0c142d034c166161", size = 912633, upload-time = "2026-02-19T19:01:32.744Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d9/e5dbef95008d84e9af1dc0faabbc34a7fbc8daa05bc5807c5cf86c2bec49/regex-2026.2.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cbc69eae834afbf634f7c902fc72ff3e993f1c699156dd1af1adab5d06b7fe7", size = 803718, upload-time = "2026-02-19T19:01:34.61Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e5/61d80132690a1ef8dc48e0f44248036877aebf94235d43f63a20d1598888/regex-2026.2.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bcf57d30659996ee5c7937999874504c11b5a068edc9515e6a59221cc2744dd1", size = 775975, upload-time = "2026-02-19T19:01:36.525Z" }, - { url = "https://files.pythonhosted.org/packages/05/32/ae828b3b312c972cf228b634447de27237d593d61505e6ad84723f8eabba/regex-2026.2.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8e6e77cd92216eb489e21e5652a11b186afe9bdefca8a2db739fd6b205a9e0a4", size = 788129, upload-time = "2026-02-19T19:01:38.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/25/d74f34676f22bec401eddf0e5e457296941e10cbb2a49a571ca7a2c16e5a/regex-2026.2.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b9ab8dec42afefa6314ea9b31b188259ffdd93f433d77cad454cd0b8d235ce1c", size = 858818, upload-time = "2026-02-19T19:01:40.409Z" }, - { url = "https://files.pythonhosted.org/packages/1e/eb/0bc2b01a6b0b264e1406e5ef11cae3f634c3bd1a6e61206fd3227ce8e89c/regex-2026.2.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:294c0fb2e87c6bcc5f577c8f609210f5700b993151913352ed6c6af42f30f95f", size = 764186, upload-time = "2026-02-19T19:01:43.009Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/5fe5a630d0d99ecf0c3570f8905dafbc160443a2d80181607770086c9812/regex-2026.2.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c0924c64b082d4512b923ac016d6e1dcf647a3560b8a4c7e55cbbd13656cb4ed", size = 850363, upload-time = "2026-02-19T19:01:45.015Z" }, - { url = "https://files.pythonhosted.org/packages/c3/45/ef68d805294b01ec030cfd388724ba76a5a21a67f32af05b17924520cb0b/regex-2026.2.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:790dbf87b0361606cb0d79b393c3e8f4436a14ee56568a7463014565d97da02a", size = 790026, upload-time = "2026-02-19T19:01:47.51Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3a/40d3b66923dfc5aeba182f194f0ca35d09afe8c031a193e6ae46971a0a0e/regex-2026.2.19-cp313-cp313-win32.whl", hash = "sha256:43cdde87006271be6963896ed816733b10967baaf0e271d529c82e93da66675b", size = 266372, upload-time = "2026-02-19T19:01:49.469Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f2/39082e8739bfd553497689e74f9d5e5bb531d6f8936d0b94f43e18f219c0/regex-2026.2.19-cp313-cp313-win_amd64.whl", hash = "sha256:127ea69273485348a126ebbf3d6052604d3c7da284f797bba781f364c0947d47", size = 277253, upload-time = "2026-02-19T19:01:51.208Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c2/852b9600d53fb47e47080c203e2cdc0ac7e84e37032a57e0eaa37446033a/regex-2026.2.19-cp313-cp313-win_arm64.whl", hash = "sha256:5e56c669535ac59cbf96ca1ece0ef26cb66809990cda4fa45e1e32c3b146599e", size = 270505, upload-time = "2026-02-19T19:01:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/a9/a2/e0b4575b93bc84db3b1fab24183e008691cd2db5c0ef14ed52681fbd94dd/regex-2026.2.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93d881cab5afdc41a005dba1524a40947d6f7a525057aa64aaf16065cf62faa9", size = 492202, upload-time = "2026-02-19T19:01:54.816Z" }, - { url = "https://files.pythonhosted.org/packages/24/b5/b84fec8cbb5f92a7eed2b6b5353a6a9eed9670fee31817c2da9eb85dc797/regex-2026.2.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:80caaa1ddcc942ec7be18427354f9d58a79cee82dea2a6b3d4fd83302e1240d7", size = 292884, upload-time = "2026-02-19T19:01:58.254Z" }, - { url = "https://files.pythonhosted.org/packages/70/0c/fe89966dfae43da46f475362401f03e4d7dc3a3c955b54f632abc52669e0/regex-2026.2.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d793c5b4d2b4c668524cd1651404cfc798d40694c759aec997e196fe9729ec60", size = 291236, upload-time = "2026-02-19T19:01:59.966Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f7/bda2695134f3e63eb5cccbbf608c2a12aab93d261ff4e2fe49b47fabc948/regex-2026.2.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5100acb20648d9efd3f4e7e91f51187f95f22a741dcd719548a6cf4e1b34b3f", size = 807660, upload-time = "2026-02-19T19:02:01.632Z" }, - { url = "https://files.pythonhosted.org/packages/11/56/6e3a4bf5e60d17326b7003d91bbde8938e439256dec211d835597a44972d/regex-2026.2.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e3a31e94d10e52a896adaa3adf3621bd526ad2b45b8c2d23d1bbe74c7423007", size = 873585, upload-time = "2026-02-19T19:02:03.522Z" }, - { url = "https://files.pythonhosted.org/packages/35/5e/c90c6aa4d1317cc11839359479cfdd2662608f339e84e81ba751c8a4e461/regex-2026.2.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8497421099b981f67c99eba4154cf0dfd8e47159431427a11cfb6487f7791d9e", size = 915243, upload-time = "2026-02-19T19:02:05.608Z" }, - { url = "https://files.pythonhosted.org/packages/90/7c/981ea0694116793001496aaf9524e5c99e122ec3952d9e7f1878af3a6bf1/regex-2026.2.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e7a08622f7d51d7a068f7e4052a38739c412a3e74f55817073d2e2418149619", size = 812922, upload-time = "2026-02-19T19:02:08.115Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/9eda82afa425370ffdb3fa9f3ea42450b9ae4da3ff0a4ec20466f69e371b/regex-2026.2.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8abe671cf0f15c26b1ad389bf4043b068ce7d3b1c5d9313e12895f57d6738555", size = 781318, upload-time = "2026-02-19T19:02:10.072Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d5/50f0bbe56a8199f60a7b6c714e06e54b76b33d31806a69d0703b23ce2a9e/regex-2026.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5a8f28dd32a4ce9c41758d43b5b9115c1c497b4b1f50c457602c1d571fa98ce1", size = 795649, upload-time = "2026-02-19T19:02:11.96Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/d039f081e44a8b0134d0bb2dd805b0ddf390b69d0b58297ae098847c572f/regex-2026.2.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:654dc41a5ba9b8cc8432b3f1aa8906d8b45f3e9502442a07c2f27f6c63f85db5", size = 868844, upload-time = "2026-02-19T19:02:14.043Z" }, - { url = "https://files.pythonhosted.org/packages/ef/53/e2903b79a19ec8557fe7cd21cd093956ff2dbc2e0e33969e3adbe5b184dd/regex-2026.2.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4a02faea614e7fdd6ba8b3bec6c8e79529d356b100381cec76e638f45d12ca04", size = 770113, upload-time = "2026-02-19T19:02:16.161Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e2/784667767b55714ebb4e59bf106362327476b882c0b2f93c25e84cc99b1a/regex-2026.2.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d96162140bb819814428800934c7b71b7bffe81fb6da2d6abc1dcca31741eca3", size = 854922, upload-time = "2026-02-19T19:02:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/59/78/9ef4356bd4aed752775bd18071034979b85f035fec51f3a4f9dea497a254/regex-2026.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c227f2922153ee42bbeb355fd6d009f8c81d9d7bdd666e2276ce41f53ed9a743", size = 799636, upload-time = "2026-02-19T19:02:20.04Z" }, - { url = "https://files.pythonhosted.org/packages/cf/54/fcfc9287f20c5c9bd8db755aafe3e8cf4d99a6a3f1c7162ee182e0ca9374/regex-2026.2.19-cp313-cp313t-win32.whl", hash = "sha256:a178df8ec03011153fbcd2c70cb961bc98cbbd9694b28f706c318bee8927c3db", size = 268968, upload-time = "2026-02-19T19:02:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/1e/a0/ff24c6cb1273e42472706d277147fc38e1f9074a280fb6034b0fc9b69415/regex-2026.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:2c1693ca6f444d554aa246b592355b5cec030ace5a2729eae1b04ab6e853e768", size = 280390, upload-time = "2026-02-19T19:02:25.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b6/a3f6ad89d780ffdeebb4d5e2e3e30bd2ef1f70f6a94d1760e03dd1e12c60/regex-2026.2.19-cp313-cp313t-win_arm64.whl", hash = "sha256:c0761d7ae8d65773e01515ebb0b304df1bf37a0a79546caad9cbe79a42c12af7", size = 271643, upload-time = "2026-02-19T19:02:27.175Z" }, +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, ] [[package]] @@ -4123,27 +4144,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, - { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, - { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, - { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, - { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, - { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, - { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +version = "0.15.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, + { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, ] [[package]] @@ -4300,55 +4321,55 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.47" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/13/886338d3e8ab5ddcfe84d54302c749b1793e16c4bba63d7004e3f7baa8ec/sqlalchemy-2.0.47-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a1dbf0913879c443617d6b64403cf2801c941651db8c60e96d204ed9388d6b0", size = 2157124, upload-time = "2026-02-24T16:43:54.706Z" }, - { url = "https://files.pythonhosted.org/packages/b6/bb/a897f6a66c9986aa9f27f5cf8550637d8a5ea368fd7fb42f6dac3105b4dc/sqlalchemy-2.0.47-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:775effbb97ea3b00c4dd3aeaf3ba8acba6e3e2b4b41d17d67a27e696843dbc95", size = 3313513, upload-time = "2026-02-24T17:29:00.527Z" }, - { url = "https://files.pythonhosted.org/packages/59/fb/69bfae022b681507565ab0d34f0c80aa1e9f954a5a7cbfb0ed054966ac8d/sqlalchemy-2.0.47-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56cc834a3ffac34270cc2a41875e0f40e97aa651f4f3ca1cfbbf421c044cb62b", size = 3313014, upload-time = "2026-02-24T17:27:11.679Z" }, - { url = "https://files.pythonhosted.org/packages/04/f3/0eba329f7c182d53205a228c4fd24651b95489b431ea2bd830887b4c13c4/sqlalchemy-2.0.47-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49b5e0c7244262f39e767c018e4fdb5e5dbc23cd54c5ddac8eea8f0ba32ef890", size = 3265389, upload-time = "2026-02-24T17:29:02.497Z" }, - { url = "https://files.pythonhosted.org/packages/5c/06/654edc084b3b46ac79e04200d7c46467ae80c759c4ee41c897f9272b036f/sqlalchemy-2.0.47-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cd822a3f1f6f77b5b841a30c1a07a07f7dee3385f17e638e1722de9ab683be", size = 3287604, upload-time = "2026-02-24T17:27:13.295Z" }, - { url = "https://files.pythonhosted.org/packages/78/33/c18c8f63b61981219d3aa12321bb7ccee605034d195e868ed94f9727b27c/sqlalchemy-2.0.47-cp311-cp311-win32.whl", hash = "sha256:9847a19548cd283a65e1ce0afd54016598d55ff72682d6fd3e493af6fc044064", size = 2116916, upload-time = "2026-02-24T17:14:37.392Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a59e3f9796fff844e16afbd821db9abfd6e12698db9441a231a96193a100/sqlalchemy-2.0.47-cp311-cp311-win_amd64.whl", hash = "sha256:722abf1c82aeca46a1a0803711244a48a298279eeaec9e02f7bfee9e064182e5", size = 2141587, upload-time = "2026-02-24T17:14:39.746Z" }, - { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687, upload-time = "2026-02-24T17:05:46.451Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978, upload-time = "2026-02-24T17:18:04.597Z" }, - { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939, upload-time = "2026-02-24T17:27:18.937Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648, upload-time = "2026-02-24T17:18:07.038Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695, upload-time = "2026-02-24T17:27:20.93Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502, upload-time = "2026-02-24T17:22:52.546Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435, upload-time = "2026-02-24T17:22:54.268Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143", size = 2152551, upload-time = "2026-02-24T17:05:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6", size = 3278782, upload-time = "2026-02-24T17:18:10.012Z" }, - { url = "https://files.pythonhosted.org/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5", size = 3295155, upload-time = "2026-02-24T17:27:22.827Z" }, - { url = "https://files.pythonhosted.org/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092", size = 3226834, upload-time = "2026-02-24T17:18:11.465Z" }, - { url = "https://files.pythonhosted.org/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01", size = 3265001, upload-time = "2026-02-24T17:27:24.813Z" }, - { url = "https://files.pythonhosted.org/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476", size = 2113647, upload-time = "2026-02-24T17:22:55.747Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66", size = 2139425, upload-time = "2026-02-24T17:22:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003", size = 3558809, upload-time = "2026-02-24T17:12:15.232Z" }, - { url = "https://files.pythonhosted.org/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487", size = 3525480, upload-time = "2026-02-24T17:27:59.602Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb", size = 3466569, upload-time = "2026-02-24T17:12:16.94Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e", size = 3475770, upload-time = "2026-02-24T17:28:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc", size = 2141300, upload-time = "2026-02-24T17:14:37.024Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42", size = 2173053, upload-time = "2026-02-24T17:14:38.688Z" }, - { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] name = "sse-starlette" -version = "3.2.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, ] [[package]] @@ -4523,14 +4544,14 @@ wheels = [ [[package]] name = "trimesh" -version = "4.11.2" +version = "4.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/41/de14e2fa9b2d99214c60402fc57d2efb201f2925b16d6bee289565901d83/trimesh-4.11.2.tar.gz", hash = "sha256:30fbde5b8dd7c157e7ff4d54286cb35291844fd3f4d0364e8b2727f1b308fb06", size = 835044, upload-time = "2026-02-10T16:00:27.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/63/a0766634bd34127ca9dac672fb45d6525924ba4fcbbbff23af2a59742bcb/trimesh-4.11.3.tar.gz", hash = "sha256:fe9b6bbd68d8e6c0f7d93313a5409d02d3da0bf4fd3d7e7c039b386bc5ce04f3", size = 835722, upload-time = "2026-03-06T01:16:14.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/b9/da09903ea53b677a58ba770112de6fe8b2acb8b4cd9bffae4ff6cfe7c072/trimesh-4.11.2-py3-none-any.whl", hash = "sha256:25e3ab2620f9eca5c9376168c67aabdd32205dad1c4eea09cd45cd4a3edf775a", size = 740328, upload-time = "2026-02-10T16:00:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5a/bed8d057a11019224be9f0b06380df2b39390be1f40196973a54f1013931/trimesh-4.11.3-py3-none-any.whl", hash = "sha256:8549c6cb95326aaf61759c7a9517b8342ae49a5bd360290b7b1e565902a85bad", size = 740519, upload-time = "2026-03-06T01:16:12.555Z" }, ] [[package]] @@ -4548,18 +4569,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] -[[package]] -name = "typer-slim" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, -] - [[package]] name = "types-aiofiles" version = "25.1.0.20251011" @@ -4644,16 +4653,25 @@ wheels = [ [[package]] name = "uc-micro-py" -version = "1.0.3" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "uncalled-for" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, ] [[package]] name = "undefined-bot" -version = "3.1.1" +version = "3.1.2" source = { editable = "." } dependencies = [ { name = "aiofiles" }, @@ -5077,80 +5095,88 @@ wheels = [ [[package]] name = "yarl" -version = "1.22.0" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]]