commit e6ab2d453438ede77d3606d5593d8fc711568eea Author: Pine Date: Mon May 25 16:37:47 2026 +0800 init(all):一个让codex调用deepseek模型的项目 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..62f74f9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Git +.git +.gitignore + +# Python +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +.venv/ +.python-version + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Runtime +proxy_debug.log +debug_request_body.txt + +# Docker (避免层级嵌套) +docker/ +Dockerfile +docker-compose*.yml + +# Build / Dist +dist/ +build/ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eb8967a --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# ==== codex_proxy 配置文件 ==== +# 复制此文件为 .env 并填入你的配置 + +# 必填:DeepSeek API Key(从 https://platform.deepseek.com/api_keys 获取) +DEEPSEEK_API_KEY=********* + +# 本服务的API Key,用于代理请求 +MY_API_KEY=sk-1234567890 + +# 默认模型 +MODEL_DEFAULT = deepseek-v4-flash + +## codex模型对应,为空则使用默认模型 +MODEL_GTP5_5 = deepseek-v4-pro +MODEL_GTP5_4 = deepseek-v4-flash +MODEl_GPT5_4_MINI = deepseek-v4-flash +MODEL_GTP5_3_CODEX = deepseek-v4-flash +MODEL_GTP5_2 = deepseek-v4-flash + +# 可选:DeepSeek API 地址(默认官方地址,一般无需修改) +# DEEPSEEK_URL=https://api.deepseek.com/v1/chat/completions + +# 可选:调试日志,设为 1 开启 +# DEEPSEEK_DEBUG=1 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92f9c20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# API Key +.env + +# 调试日志 +proxy_debug.log + +# Python +__pycache__/ +*.pyc +*.pyo + +# 虚拟环境 +venv/ +.venv/ +env/ +/.claude \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00d18a5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# ============================================================ +# Codex2DeepSeek — 跨平台多阶段构建 +# ============================================================ +# 构建: +# docker build -t codex2deepseek:latest . +# docker buildx build --platform linux/amd64,linux/arm64 -t codex2deepseek:latest . +# +# 运行: +# docker compose -f docker/docker-compose.yml up -d +# ============================================================ + +# ---- 构建阶段 ---- +# 使用与运行时相同的 slim 基镜像,避免 musl/glibc 不兼容问题 +FROM python:3.12-slim AS builder + +WORKDIR /build + +# 安装 uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# 利用 Docker 层缓存:先复制依赖声明 +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen --no-dev --no-install-project + +# 复制源码 +COPY app/ ./app/ +COPY routers/ ./routers/ +COPY services/ ./services/ +COPY main.py ./ + +# 重新 sync 以链接本地包 +RUN uv sync --frozen --no-dev + + +# ---- 运行时阶段 ---- +FROM python:3.12-slim + +WORKDIR /app + +# 安装 ca-certificates 确保 HTTPS 请求正常 +RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# 从 builder 复制已安装的 venv 和源码 +COPY --from=builder /build/.venv /app/.venv +COPY --from=builder /build/main.py /app/main.py +COPY --from=builder /build/app/ /app/app/ +COPY --from=builder /build/routers/ /app/routers/ +COPY --from=builder /build/services/ /app/services/ + +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +EXPOSE 12345 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import socket; socket.create_connection(('localhost', 12345), timeout=5).close()" || exit 1 + +ENTRYPOINT ["python", "main.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..131d45d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PineKings + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..73832cd --- /dev/null +++ b/README.md @@ -0,0 +1,274 @@ +# Codex2DeepSeek + +OpenAI Responses API ↔ DeepSeek Chat API 流式转发代理。 + +将 OpenAI Codex 等客户端发出的 Responses API 请求,实时转换为 DeepSeek Chat Completions 格式,实现 SSE 流式透传。 + +本项目配合 `CC Switch`使用,可实现 Codex IDE/CLI 与 DeepSeek Chat 的对接中转。 + +> 妈的,使用Codex,想用DeepSeek接入,找了半天工具,就没发现什么非常好用的工具,协议都不匹配,找到 [Nigel211/codex_deepseek_proxy](https://github.com/Nigel211/codex_deepseek_proxy.git) 但是单文件模式不习惯,使用flask框架也不喜欢,同时在使用中发现模型的对应问题,很头大,没工夫去慢慢研究了,所以重构了fastAPI的版本,后续有空的话会增加其他各路国产模型的适配。 + + +## 特性 + +- **协议转换** — 将 OpenAI Responses API 请求(`input`/`instructions`/`tools` 等字段)自动映射为 DeepSeek Chat Completions 格式 +- **流式透传** — 基于 SSE (Server-Sent Events) 实时转发 DeepSeek 响应,完整保留 `reasoning_content`(思考链) +- **工具调用** — 支持 Function Calling 的双向格式转换与消息重排,兼容并行工具调用 +- **模型映射** — 通过环境变量灵活配置客户端模型名到 DeepSeek 模型的对应关系 +- **身份验证** — 支持 Authorization Bearer Token 鉴权 +- **轻量部署** — 仅依赖 FastAPI + Uvicorn + Requests + +## 流程 + +``` +Codex IDE/CLI Proxy Server DeepSeek API +───────────── ───────────── ──────────── +Responses API ────→ 格式转换 (Responses→Chat) ────→ /v1/chat/completions +SSE Stream ←──── 格式转换 (Chat→Responses) ←──── SSE Stream +``` + +### 项目结构 + +``` +├── main.py # 应用入口:CLI 参数、FastAPI 初始化、启动 +├── pyproject.toml # 项目配置与依赖声明 +├── .env # 环境变量(API Key、模型映射等) +├── app/ +│ ├── __init__.py +│ └── config.py # 配置管理:.env 加载、API Key 交互式输入、模型映射表 +├── routers/ +│ ├── __init__.py # 路由统一注册 +│ ├── proxy.py # API 端点:/responses, /v1/responses, /v1/chat/completions +│ ├── lifespan.py # 应用生命周期管理 +│ └── middleware.py # 中间件:CORS、Auth 鉴权、模型名称解析 +└── services/ + ├── __init__.py + ├── converter.py # 协议转换:Responses API → Chat Completions + └── stream.py # SSE 流式生成器 +``` + +## 快速启动 + +> 前置条件:Python 3.8+、uv(uv 未安装请自行百度uv安装) + +### 1. 安装依赖 + +```bash +git clone https://github.com/PineKings/Code2DeepSeek.git +cd Code2DeepSeek +uv sync +``` + +### 2. 配置 + +创建 `.env` 文件: + +```ini +# DeepSeek API Key(必填) +DEEPSEEK_API_KEY=sk-your-deepseek-key + +# 本服务鉴权密钥(必填,客户端请求需携带) +MY_API_KEY=sk-your-proxy-key + +# 默认模型(请求中未指定模型时使用) +MODEL_DEFAULT=deepseek-v4-flash + +# 模型映射:客户端模型名 → DeepSeek 模型名(留空则使用 MODEL_DEFAULT) +MODEL_GTP5_5=deepseek-v4-pro +MODEL_GTP5_4=deepseek-v4-flash +MODEl_GPT5_4_MINI=deepseek-v4-flash +MODEL_GTP5_3_CODEX=deepseek-v4-flash +MODEL_GTP5_2=deepseek-v4-flash +``` + +首次启动时若未配置 `DEEPSEEK_API_KEY` 或 `MY_API_KEY`,程序会交互式提示输入并自动写入 `.env`,该方式docker启动不可用❌。 + +### 3. 运行 + +```bash +uv run main.py +``` + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `--port` | `12345` | 服务端口 | +| `--host` | `0.0.0.0` | 绑定地址 | +| `--debug` | — | 启用调试日志 | + + +### 4. Docker 方式构建后部署 + +确保已安装 Docker 和 Docker Compose(Docker Desktop 已内置 compose)。 + +#### 构建镜像 + +```bash +# 从 Dockerfile 构建(根目录) +docker build -t codex2deepseek:latest . + +# 跨平台构建 +docker buildx build --platform linux/amd64,linux/arm64 -t codex2deepseek:latest . +``` + +#### 启动服务 + +```bash +# 使用 docker-compose.yml 后台启动 +docker compose -f docker/docker-compose.yml up -d + +# 查看日志 +docker compose -f docker/docker-compose.yml logs -f + +# 重启 +docker compose -f docker/docker-compose.yml restart + +# 停止并删除容器 +docker compose -f docker/docker-compose.yml down +``` + +服务将在 `http://127.0.0.1:12345` 监听。 + +> ⚠️ `.env` 文件**必须**在启动前已配置好 `DEEPSEEK_API_KEY` 和 `MY_API_KEY`。Docker 模式下不支持交互式输入,请确认 `.env` 文件内容完整。 + +#### 单独使用 Docker(不依赖 Compose) + +```bash +# 构建 +docker build -t codex2deepseek:latest . + +# 运行 +docker run -d -p 127.0.0.1:12345:12345 \ + --name codex2deepseek \ + --env-file .env \ + --restart unless-stopped \ + codex2deepseek:latest +``` + +#### 文件说明 + +``` +├── Dockerfile # 多阶段构建,builder 使用 python:3.12-slim + uv +├── docker/ +│ ├── docker-compose.yml # Compose 编排文件(构建 + 启动) +│ └── .env.example # 环境变量模板 +``` + +- `Dockerfile` 位于项目根目录,使用 python:3.12-slim 作为构建和运行时基镜像,避免 musl/glibc 不兼容问题 +- `docker/docker-compose.yml` 位于 `docker/` 目录下,通过 `context: ..` 引用项目根目录的 `Dockerfile` 和 `.env` + +### 5. Docker 部署 +#### 拉取镜像 +```bash +docker pull pinekings/codex2deepseek:latest +``` + +#### 创建.env文件 +```bash +touch .env +``` + +#### 编辑.env文件(`vi .env`) +```bash +# ==== codex_proxy 配置文件 ==== +# 复制此文件为 .env 并填入你的配置 + +# 必填:DeepSeek API Key(从 https://platform.deepseek.com/api_keys 获取) +DEEPSEEK_API_KEY=********* + +# 本服务的API Key,用于代理请求 +MY_API_KEY=sk-1234567890 + +# 默认模型 +MODEL_DEFAULT = deepseek-v4-flash + +## codex模型对应,为空则使用默认模型 +MODEL_GTP5_5 = deepseek-v4-pro +MODEL_GTP5_4 = deepseek-v4-flash +MODEl_GPT5_4_MINI = deepseek-v4-flash +MODEL_GTP5_3_CODEX = deepseek-v4-flash +MODEL_GTP5_2 = deepseek-v4-flash + +# 可选:DeepSeek API 地址(默认官方地址,一般无需修改) +# DEEPSEEK_URL=https://api.deepseek.com/v1/chat/completions + +# 可选:调试日志,设为 1 开启 +# DEEPSEEK_DEBUG=1 +``` + +#### 运行容器 +```bash +docker run -d \ + --name codex2deepseek \ + -p 12345:12345 \ + --env-file ./.env \ + pinekings/codex2deepseek:latest +``` + +## 用法 + +### API Endpoints + +本项目无法直接实现codex的对接,需要配合CCSwitch等工具实现对接。 + +打开ccswitch,点击添加新供应商,选择自定义,填写代理地址和端口即可。 + +``` +供应商名称: DeepSeek +API Key: sk-your-proxy-key +API 请求地址:http://127.0.0.1:12345(本地跑这个服务就本地,远程就改成你的ip和端口号) +``` + +注意: +- 点击“获取模型列表”按钮,报错“未找到可用的模型列表端点,请检查 Base URL 或确认供应商是否开放该接口”非常正常!因为本项目就没有中转模型列表获取的接口! +- 测试是否对接成功,在供应商列表,然后点击“测试模型”,如果返回了类似这样`DeepSeek 运行正常 (4ms)`的信息就说明成功了 + +![alt text](image.png) + + +## 模型映射 + +通过环境变量为每个 Codex 模型名指定对应的 DeepSeek 模型: + +| 环境变量 | 默认值 | 客户端请求 model | +|---------|--------|-----------------| +| `MODEL_DEFAULT` | `deepseek-v4-flash` | 未指定或未匹配时使用 | +| `MODEL_GTP5_5` | `MODEL_DEFAULT` | `gpt-5.5` | +| `MODEL_GTP5_4` | `MODEL_DEFAULT` | `gpt-5.4` | +| `MODEl_GPT5_4_MINI` | `MODEL_DEFAULT` | `gpt-5.4-mini` | +| `MODEL_GTP5_3_CODEX` | `MODEL_DEFAULT` | `gpt-5.3-codex` | +| `MODEL_GTP5_2` | `MODEL_DEFAULT` | `gpt-5.2` | + +模型解析优先级:`中间件解析值 > 请求体中的 model > MODEL_DEFAULT > 代码默认值` + +## 环境变量 + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `DEEPSEEK_API_KEY` | 是 | — | DeepSeek API 密钥 | +| `MY_API_KEY` | 是 | — | 本服务鉴权密钥 | +| `DEEPSEEK_URL` | 否 | `https://api.deepseek.com/v1/chat/completions` | DeepSeek API 地址 | +| `DEFAULT_MODEL` | 否 | `deepseek-v4-flash` | 无模型指定时的默认模型(旧配置) | +| `MODEL_DEFAULT` | 否 | `deepseek-v4-flash` | 模型映射默认值 | +| `DEEPSEEK_DEBUG` | 否 | `0` | 设为 `1` 启用调试日志到 `proxy_debug.log` | +| `MODEL_GTP5_5` | 否 | `MODEL_DEFAULT` | `gpt-5.5` 映射目标 | +| `MODEL_GTP5_4` | 否 | `MODEL_DEFAULT` | `gpt-5.4` 映射目标 | +| `MODEl_GPT5_4_MINI` | 否 | `MODEL_DEFAULT` | `gpt-5.4-mini` 映射目标 | +| `MODEL_GTP5_3_CODEX` | 否 | `MODEL_DEFAULT` | `gpt-5.3-codex` 映射目标 | +| `MODEL_GTP5_2` | 否 | `MODEL_DEFAULT` | `gpt-5.2` 映射目标 | + +## Tool Calling 兼容性 + +代理处理了 DeepSeek Chat Completions API 与 OpenAI Responses API 之间的以下差异: + +| 问题 | 处理方式 | +|------|----------| +| 工具定义格式不同 | 将 Responses API 扁平格式转为 Chat API 嵌套 `function` 格式,清除 `strict` 和 `additionalProperties` 字段 | +| 并行工具调用 | 将 Codex 的多个连续 `function_call` 项合并为单个 assistant 消息(含多个 `tool_calls`) | +| 消息顺序 | DeepSeek 要求 tool 消息紧跟对应的 assistant 消息;代理自动重排插入的 system 消息 | +| 思考模式 | 默认禁用(`thinking: {type: "disabled"}`),避免 `reasoning_content` 回传兼容问题 | +| `reasoning_content` | 仅在流结束事件中随 `item.done` 回传,避免破坏 Responses API 事件结构 | + + +## 归属权 + +本项目重构自 [Nigel211/codex_deepseek_proxy](https://github.com/Nigel211/codex_deepseek_proxy.git),该项目以MIT协议开源,本项目亦同。 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..41d8130 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""codex_deepseek_proxy 配置模块""" diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..ccb4490 --- /dev/null +++ b/app/config.py @@ -0,0 +1,190 @@ +""" +配置管理模块 + +职责: + - .env 文件加载(不覆盖已有系统环境变量) + - DEEPSEEK_API_KEY 交互式输入与持久化 + - 导出全局配置常量 +""" + +import os +import sys +import getpass + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def load_dotenv(): + """加载 .env 文件到 os.environ(不覆盖已有的系统环境变量)""" + env_file = os.path.join(BASE_DIR, ".env") + if not os.path.exists(env_file): + return + with open(env_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, val = line.partition("=") + key, val = key.strip(), val.strip().strip("\"'") + if key and key not in os.environ: + os.environ[key] = val + + +def ensure_api_key() -> str: + """确保 DEEPSEEK_API_KEY 已设置:系统环境变量 > .env > 交互输入 + + Returns: + API Key 字符串,若用户取消输入则退出进程。 + """ + key = os.environ.get("DEEPSEEK_API_KEY", "").strip() + if key: + return key + + print("=" * 60) + print(" 未检测到 DEEPSEEK_API_KEY") + print("=" * 60) + print() + print(" 从 https://platform.deepseek.com/api_keys 获取 API Key") + print() + print(" 你也可以设置系统环境变量 DEEPSEEK_API_KEY 后重启") + print() + + try: + key = getpass.getpass(" 请输入你的 DeepSeek API Key: ").strip() + except (EOFError, KeyboardInterrupt): + key = "" + + if not key: + print() + print(" ERROR: 未输入 API Key,程序退出。") + print() + print(" 支持以下方式设置 API Key(按优先级排列):") + print(" 1. 系统环境变量: DEEPSEEK_API_KEY=sk-your-key") + print(" 2. 脚本同目录 .env 文件: DEEPSEEK_API_KEY=sk-your-key") + print() + input(" 按 Enter 退出...") + sys.exit(1) + + env_file = os.path.join(BASE_DIR, ".env") + existing = {} + if os.path.exists(env_file): + with open(env_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + existing[k.strip()] = line + existing["DEEPSEEK_API_KEY"] = f"DEEPSEEK_API_KEY={key}" + + with open(env_file, "w", encoding="utf-8") as f: + for line in existing.values(): + f.write(line + "\n") + if "DEFAULT_MODEL" not in existing: + f.write("DEFAULT_MODEL=deepseek-v4-pro\n") + + os.environ["DEEPSEEK_API_KEY"] = key + print() + print(f" API Key 已保存到: {env_file}") + print() + return key + + +# ===================== 模块导入时自动加载 ===================== +load_dotenv() + +# ===================== 全局配置常量 ===================== +DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "").strip() +DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "deepseek-v4-flash").strip() +DEEPSEEK_URL = os.environ.get( + "DEEPSEEK_URL", "https://api.deepseek.com/v1/chat/completions" +).strip() +DEEPSEEK_DEBUG = os.environ.get("DEEPSEEK_DEBUG", "0").strip() in ( + "1", + "true", + "True", + "yes", +) +DEBUG_LOG = os.path.join(BASE_DIR, "proxy_debug.log") + +# ===================== 模型映射表 ===================== +# 将客户端请求中的模型名称映射为 DeepSeek 实际模型名称 +# 默认模型名称(可被环境变量覆盖),当请求中未指定模型或指定的模型不在 MODEL_MAP 中时使用 +MODEL_DEFAULT = os.environ.get("MODEL_DEFAULT", "deepseek-v4-flash") + +## codex模型对应,为空则使用默认模型 +MODEL_GTP5_5 = os.environ.get("MODEL_GTP5_5", "").strip() or MODEL_DEFAULT +MODEL_GTP5_4 = os.environ.get("MODEL_GTP5_4", "").strip() or MODEL_DEFAULT +MODEl_GPT5_4_MINI = os.environ.get("MODEl_GPT5_4_MINI", "").strip() or MODEL_DEFAULT +MODEL_GTP5_3_CODEX = os.environ.get("MODEL_GTP5_3_CODEX", "").strip() or MODEL_DEFAULT +MODEL_GTP5_2 = os.environ.get("MODEL_GTP5_2", "").strip() or MODEL_DEFAULT + +MODEL_MAP = { + "gpt-5.5": MODEL_GTP5_5, + "gpt-5.4": MODEL_GTP5_4, + "gpt-5.4-mini": MODEl_GPT5_4_MINI, + "gpt-5.3-codex": MODEL_GTP5_3_CODEX, + "gpt-5.2": MODEL_GTP5_2, + +} + +def ensure_my_api_key() -> str: + """确保 MY_API_KEY 已设置:系统环境变量 > .env > 交互输入 + + Returns: + 授权密钥字符串,若用户取消输入则退出进程。 + """ + key = os.environ.get("MY_API_KEY", "").strip() + if key: + return key + + print("=" * 60) + print(" 未检测到 MY_API_KEY(授权密钥)") + print("=" * 60) + print() + print(" MY_API_KEY 用于验证请求来源是否合法,请设置一个自定义密钥。") + print() + + try: + key = getpass.getpass(" 请输入你的 MY_API_KEY: ").strip() + except (EOFError, KeyboardInterrupt): + key = "" + + if not key: + print() + print(" ERROR: 未输入 MY_API_KEY,程序退出。") + print() + print(" 你可以在 .env 文件中设置: MY_API_KEY=你的密钥") + print() + input(" 按 Enter 退出...") + sys.exit(1) + + env_file = os.path.join(BASE_DIR, ".env") + existing = {} + if os.path.exists(env_file): + with open(env_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, _, v = line.partition("=") + existing[k.strip()] = line + existing["MY_API_KEY"] = f"MY_API_KEY={key}" + + with open(env_file, "w", encoding="utf-8") as f: + for line in existing.values(): + f.write(line + "\n") + + os.environ["MY_API_KEY"] = key + # 更新模块级变量,使后续 import 能拿到最新值 + globals()["MY_API_KEY"] = key + print() + print(f" MY_API_KEY 已保存到: {env_file}") + print() + return key + + +# ===================== 全局配置常量 ===================== +# 简单防护,使用固定API Key 验证请求来源是否合法 +MY_API_KEY = os.environ.get("MY_API_KEY", "").strip() + diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..eb8967a --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,24 @@ +# ==== codex_proxy 配置文件 ==== +# 复制此文件为 .env 并填入你的配置 + +# 必填:DeepSeek API Key(从 https://platform.deepseek.com/api_keys 获取) +DEEPSEEK_API_KEY=********* + +# 本服务的API Key,用于代理请求 +MY_API_KEY=sk-1234567890 + +# 默认模型 +MODEL_DEFAULT = deepseek-v4-flash + +## codex模型对应,为空则使用默认模型 +MODEL_GTP5_5 = deepseek-v4-pro +MODEL_GTP5_4 = deepseek-v4-flash +MODEl_GPT5_4_MINI = deepseek-v4-flash +MODEL_GTP5_3_CODEX = deepseek-v4-flash +MODEL_GTP5_2 = deepseek-v4-flash + +# 可选:DeepSeek API 地址(默认官方地址,一般无需修改) +# DEEPSEEK_URL=https://api.deepseek.com/v1/chat/completions + +# 可选:调试日志,设为 1 开启 +# DEEPSEEK_DEBUG=1 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..6d3e774 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,38 @@ +# ============================================================ +# Codex DeepSeek Proxy — Docker Compose 编排 +# ============================================================ +# 使用: +# docker compose -f docker/docker-compose.yml up -d # 启动 +# docker compose -f docker/docker-compose.yml logs -f # 日志 +# docker compose -f docker/docker-compose.yml down # 停止 +# docker compose -f docker/docker-compose.yml restart # 重启 +# ============================================================ + +services: + proxy: + build: + context: .. + dockerfile: Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + image: codex2deepseek:latest + container_name: codex2deepseek + ports: + - "127.0.0.1:12345:12345" + env_file: + - ../.env + restart: unless-stopped + user: "1000:1000" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 128M + healthcheck: + test: ["CMD", "python", "-c", "import socket; socket.create_connection(('localhost', 12345), timeout=5).close()"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s diff --git a/image.png b/image.png new file mode 100644 index 0000000..ae8902b Binary files /dev/null and b/image.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..31f3819 --- /dev/null +++ b/main.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +""" +OpenAI Responses API ↔ DeepSeek Chat API 流式转发代理 +启动: + python main.py --port 12345 +""" + +import argparse +import sys + +# ===================== 命令行参数 ===================== +parser = argparse.ArgumentParser(description="Codex DeepSeek Proxy") +parser.add_argument( + "--port", type=int, default=12345, help="服务运行端口,默认 12345" +) +parser.add_argument( + "--host", type=str, default="0.0.0.0", help="绑定地址,默认 0.0.0.0" +) +parser.add_argument( + "--debug", + action="store_true", + help="启用调试日志(覆盖 DEEPSEEK_DEBUG 环境变量)", +) +args = parser.parse_args() + +# ===================== 初始化 API Key ===================== +# 必须在 FastAPI 应用创建前完成,因为 config 模块在 import 时加载 .env +from app.config import ensure_api_key, ensure_my_api_key + +key = ensure_api_key() +if not key: + sys.exit(1) + +my_key = ensure_my_api_key() +if not my_key: + sys.exit(1) + +# ===================== FastAPI 应用创建 ===================== +from fastapi import FastAPI + +from routers import register_routes +from routers.lifespan import create_lifespan +from routers.middleware import setup_middleware + +app = FastAPI( + title="Codex DeepSeek Proxy", + description="OpenAI Responses API ↔ DeepSeek Chat API 流式转发代理", + version="0.2.0", + lifespan=create_lifespan, +) + +# 配置中间件 +setup_middleware(app) + +# 注册路由 +register_routes(app) + +# ===================== 服务启动 ===================== +if __name__ == "__main__": + import uvicorn + uvicorn.run( + app, + host=args.host, + port=args.port, + access_log=True, + ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..de9b4f2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "codex2deepseek" +version = "0.2.0" +description = "OpenAI Responses API ↔ DeepSeek Chat API 流式转发代理" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.136.0", + "uvicorn>=0.44.0", + "requests>=2.28", +] diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..8737418 --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1,11 @@ +"""路由注册入口 + +参照 PineSoundDesktop 项目结构,通过 register_routes() 统一注册所有路由。 +""" + +from .proxy import router + + +def register_routes(app): + """注册所有 API 路由""" + app.include_router(router) diff --git a/routers/lifespan.py b/routers/lifespan.py new file mode 100644 index 0000000..826d459 --- /dev/null +++ b/routers/lifespan.py @@ -0,0 +1,19 @@ +""" +应用生命周期管理 + +通过 FastAPI lifespan 机制处理启动和关闭事件。 +""" + +from contextlib import asynccontextmanager + + +@asynccontextmanager +async def create_lifespan(app): + """应用生命周期上下文管理器""" + # ==================== 启动事件 ==================== + print("codex_deepseek_proxy starting ...") + + yield + + # ==================== 关闭事件 ==================== + print("codex_deepseek_proxy shutting down ...") diff --git a/routers/middleware.py b/routers/middleware.py new file mode 100644 index 0000000..8fd3893 --- /dev/null +++ b/routers/middleware.py @@ -0,0 +1,68 @@ +""" +中间件配置 + +参照 PineSoundDesktop 项目结构集中管理中间件。 +""" + +import json + +from fastapi import Response +from fastapi.middleware.cors import CORSMiddleware + +from app.config import MODEL_MAP, DEFAULT_MODEL, MY_API_KEY + + +def setup_middleware(app): + """配置所有中间件 + + Args: + app: FastAPI 应用实例 + """ + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @app.middleware("http") + async def auth_check(request, call_next): + """验证请求携带的 API Key 是否合法""" + # OPTIONS 预检请求跳过验证 + if request.method == "OPTIONS": + return await call_next(request) + + auth = request.headers.get("Authorization", "") + if auth.startswith("Bearer "): + token = auth[7:] + else: + token = "" + + if token != MY_API_KEY: + return Response( + content=json.dumps({ + "error": {"message": "Invalid API Key", "type": "auth_error"}, + }), + status_code=401, + media_type="application/json", + ) + + return await call_next(request) + + @app.middleware("http") + async def resolve_model(request, call_next): + """拦截请求中的模型名称,按 MODEL_MAP 进行替换""" + if request.method == "POST": + body = await request.body() + if body: + try: + data = json.loads(body) + original = data.get("model") + if original: + resolved = MODEL_MAP.get(original, DEFAULT_MODEL) + request.state.resolved_model = resolved + except (json.JSONDecodeError, UnicodeDecodeError): + pass + response = await call_next(request) + return response diff --git a/routers/proxy.py b/routers/proxy.py new file mode 100644 index 0000000..8307719 --- /dev/null +++ b/routers/proxy.py @@ -0,0 +1,60 @@ +""" +代理路由模块 + +提供 OpenAI Responses API ↔ DeepSeek Chat API 的流式转发端点。 +""" + +import uuid + +from fastapi import APIRouter, Request +from fastapi.responses import StreamingResponse + +from app.config import MODEL_DEFAULT, DEEPSEEK_DEBUG +from services.converter import extract_messages +from services.stream import create_sse_generator, _log_debug + +router = APIRouter() + + +async def _handle_proxy(request: Request): + """处理 /responses 系列请求的核心逻辑""" + req_data = await request.json() + messages, tools, tool_choice = extract_messages(req_data) + # 中间件 resolve_model 已解析的模型优先级最高 + effective_model = ( + getattr(request.state, "resolved_model", None) + or req_data.get("model") + or MODEL_DEFAULT + ) + response_id = f"resp_{uuid.uuid4().hex[:12]}" + + if DEEPSEEK_DEBUG: + _log_debug(req_data, messages, tools, tool_choice, request.url.path) + + stream_gen = create_sse_generator( + messages, tools, tool_choice, effective_model, response_id, debug_path=request.url.path + ) + + return StreamingResponse( + stream_gen(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +@router.post("/responses") +async def route_responses(request: Request): + """OpenAI Responses API 格式的流式代理端点""" + return await _handle_proxy(request) + + +@router.post("/v1/responses") +async def route_v1_responses(request: Request): + """OpenAI Responses API (v1) 格式的流式代理端点""" + return await _handle_proxy(request) + + +@router.post("/v1/chat/completions") +async def route_v1_chat(request: Request): + """Chat Completions API 格式的流式代理端点(兼容模式)""" + return await _handle_proxy(request) diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..24f9b37 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ +"""代理业务逻辑服务模块""" diff --git a/services/converter.py b/services/converter.py new file mode 100644 index 0000000..3563167 --- /dev/null +++ b/services/converter.py @@ -0,0 +1,205 @@ +""" +消息格式转换模块 + +将 OpenAI Responses API 请求格式转换为 DeepSeek Chat Completions API 格式。 +""" + + +def _clean_schema(obj): + """递归清除 JSON Schema 中 DeepSeek 不支持的字段""" + if not isinstance(obj, dict): + return obj + cleaned = {} + for k, v in obj.items(): + if k in ("additionalProperties", "strict"): + continue + if isinstance(v, dict): + cleaned[k] = _clean_schema(v) + elif isinstance(v, list): + cleaned[k] = [_clean_schema(i) if isinstance(i, dict) else i for i in v] + else: + cleaned[k] = v + return cleaned + + +def _convert_tools(tools: list) -> list: + """将工具定义从 Responses API 格式转换为 Chat Completions API 格式""" + result = [] + for tool in tools: + if not isinstance(tool, dict): + continue + if tool.get("type") != "function": + continue + func = { + "name": tool.get("name", ""), + "description": tool.get("description", ""), + } + if "parameters" in tool: + func["parameters"] = _clean_schema(tool["parameters"]) + result.append({"type": "function", "function": func}) + return result + + +def _convert_tool_choice(tc): + """将 tool_choice 从 Responses API 格式转换为 Chat Completions 格式""" + if tc is None: + return "auto" + if isinstance(tc, str): + return tc + if isinstance(tc, dict) and tc.get("type") == "function": + return {"type": "function", "function": {"name": tc.get("name", "")}} + return "auto" + + +def extract_messages(data: dict): + """ + 从 Responses API 请求中提取 messages 列表、tools 列表和 tool_choice。 + + 支持两种输入格式: + - Responses API(input/instructions 字段) + - Chat Completions API(messages 字段) + + Returns: + (messages, tools, tool_choice) + """ + ROLE_MAP = {"developer": "system"} + raw_tools = data.get("tools", []) + tools = _convert_tools(raw_tools) + tool_choice = _convert_tool_choice(data.get("tool_choice")) + + if "input" not in data: + if "messages" in data: + return data["messages"], tools, tool_choice + return [], tools, tool_choice + + inp = data["input"] + if isinstance(inp, str): + messages = [] + if "instructions" in data and data["instructions"]: + messages.append({"role": "system", "content": data["instructions"]}) + messages.append({"role": "user", "content": inp}) + return messages, tools, tool_choice + + if not isinstance(inp, list): + return [], tools, tool_choice + + messages = [] + if "instructions" in data and data["instructions"]: + messages.append({"role": "system", "content": data["instructions"]}) + + pending_tool_calls = [] + pending_reasoning = "" + + def _flush_tool_calls(): + nonlocal pending_tool_calls, pending_reasoning + if pending_tool_calls: + msg = { + "role": "assistant", + "content": "", + "tool_calls": pending_tool_calls, + } + if pending_reasoning: + msg["reasoning_content"] = pending_reasoning + messages.append(msg) + pending_tool_calls = [] + pending_reasoning = "" + + for item in inp: + if not isinstance(item, dict): + continue + item_type = item.get("type") + + if item_type == "message": + _flush_tool_calls() + role = item.get("role", "user") + role = ROLE_MAP.get(role, role) + content = item.get("content", "") + if isinstance(content, list): + texts = [] + tool_calls = [] + for c in content: + if not isinstance(c, dict): + continue + c_type = c.get("type") + if c_type in ("text", "input_text", "output_text"): + t = c.get("text", "") + if t.strip(): + texts.append(t) + elif c_type == "tool_call": + tool_calls.append({ + "id": c.get("id", ""), + "type": "function", + "function": { + "name": c.get("name", ""), + "arguments": c.get("arguments", ""), + }, + }) + text_content = "\n".join(texts) + if tool_calls: + msg = {"role": role, "content": text_content or ""} + msg["tool_calls"] = tool_calls + if item.get("reasoning_content"): + msg["reasoning_content"] = item["reasoning_content"] + messages.append(msg) + elif text_content: + msg = {"role": role, "content": text_content} + if item.get("reasoning_content"): + msg["reasoning_content"] = item["reasoning_content"] + messages.append(msg) + elif isinstance(content, str) and content.strip(): + msg = {"role": role, "content": content.strip()} + if item.get("reasoning_content"): + msg["reasoning_content"] = item["reasoning_content"] + messages.append(msg) + + elif item_type == "function_call": + pending_tool_calls.append({ + "id": item.get("call_id", ""), + "type": "function", + "function": { + "name": item.get("name", ""), + "arguments": item.get("arguments", ""), + }, + }) + if item.get("reasoning_content") and not pending_reasoning: + pending_reasoning = item["reasoning_content"] + + elif item_type == "function_call_output": + _flush_tool_calls() + messages.append({ + "role": "tool", + "tool_call_id": item.get("call_id", ""), + "content": item.get("output", ""), + }) + + _flush_tool_calls() + + # ---- 重排消息:确保 tool 消息紧跟对应的 assistant 消息 ---- + reordered = [] + i = 0 + while i < len(messages): + msg = messages[i] + if msg.get("role") == "assistant" and msg.get("tool_calls"): + expected_ids = {tc["id"] for tc in msg["tool_calls"]} + tool_msgs = [] + non_tool_msgs = [] + j = i + 1 + while j < len(messages) and expected_ids: + nxt = messages[j] + if nxt.get("role") == "tool" and nxt.get("tool_call_id") in expected_ids: + expected_ids.remove(nxt["tool_call_id"]) + tool_msgs.append(nxt) + elif nxt.get("role") in ("system", "developer"): + non_tool_msgs.append(nxt) + else: + break + j += 1 + reordered.extend(non_tool_msgs) + reordered.append(msg) + reordered.extend(tool_msgs) + i = j + else: + reordered.append(msg) + i += 1 + + return reordered, tools, tool_choice diff --git a/services/stream.py b/services/stream.py new file mode 100644 index 0000000..736a368 --- /dev/null +++ b/services/stream.py @@ -0,0 +1,551 @@ +""" +SSE 流式生成模块 + +将 DeepSeek Chat Completions API 的流式响应转换为 +OpenAI Responses API 格式的 SSE 事件流。 +""" + +import json +import uuid +from datetime import datetime + +import requests + +from app.config import DEEPSEEK_API_KEY, DEEPSEEK_URL, DEEPSEEK_DEBUG, DEBUG_LOG + + +def _log_debug(req_data, messages, tools, tool_choice, debug_path): + """记录调试日志到文件""" + with open(DEBUG_LOG, "a", encoding="utf-8") as f: + f.write(f"\n--- [{datetime.now()}] PATH={debug_path} ---\n") + f.write(f"Request body:\n{json.dumps(req_data, indent=2, ensure_ascii=False)}\n") + f.write(f"Messages:\n{json.dumps(messages, indent=2, ensure_ascii=False)}\n") + if tools: + f.write(f"Tools count: {len(tools)}\n") + f.write(f"Tool choice: {tool_choice}\n") + + +def _log_debug_error(payload, messages, tools, err_msg, status_code, body): + """记录错误调试日志""" + with open(DEBUG_LOG, "a", encoding="utf-8") as f: + f.write(f"ERROR: {err_msg}\n") + f.write(f"Payload sent (tools={len(tools)}, msgs={len(messages)}):\n") + payload_copy = dict(payload) + payload_copy.pop("messages", None) + payload_copy.pop("tools", None) + f.write(json.dumps(payload_copy, indent=2, ensure_ascii=False) + "\n") + f.write(f"Messages ({len(messages)}):\n") + f.write(json.dumps(messages, indent=2, ensure_ascii=False)[:3000] + "\n") + f.write(f"Tools ({len(tools)}):\n") + tools_str = json.dumps(tools, indent=2, ensure_ascii=False) + f.write(tools_str[:5000] + ("...(truncated)" if len(tools_str) > 5000 else "") + "\n") + total_size = len(json.dumps(payload, ensure_ascii=False)) + f.write(f"Total payload size: {total_size} bytes ({total_size/1024:.1f} KB)\n") + + +def create_sse_generator(messages, tools, tool_choice, effective_model, response_id, debug_path=""): + """创建 SSE 流式事件生成器 + + Args: + messages: 转换后的 Chat Completions 格式消息列表 + tools: 转换后的工具定义列表 + tool_choice: 工具选择策略 + effective_model: 使用的模型名称 + response_id: 响应 ID + debug_path: 请求路径(用于调试日志) + + Returns: + 生成 SSE 事件字符串的生成器函数 + """ + + def generate(): + if not messages: + yield "event: response.completed\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.completed", + "response": { + "id": response_id, + "object": "response", + "status": "completed", + "model": effective_model, + "output": [], + "usage": { + "input_tokens": 0, + "output_tokens": 0, + "total_tokens": 0, + }, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + return + + # response.created + yield "event: response.created\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.created", + "response": { + "id": response_id, + "object": "response", + "status": "in_progress", + "model": effective_model, + "output": [], + "usage": None, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + # response.in_progress + yield "event: response.in_progress\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.in_progress", + "response": { + "id": response_id, + "object": "response", + "status": "in_progress", + "model": effective_model, + "output": [], + "usage": None, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + # 构建 DeepSeek 请求 + headers = { + "Authorization": f"Bearer {DEEPSEEK_API_KEY}", + "Content-Type": "application/json", + } + payload = { + "model": effective_model, + "messages": messages, + "stream": True, + "stream_options": {"include_usage": True}, + "thinking": {"type": "disabled"}, + } + if tools: + payload["tools"] = tools + if tool_choice != "auto": + payload["tool_choice"] = tool_choice + + # 状态跟踪 + text_item_id = f"item_{uuid.uuid4().hex[:12]}" + full_text = "" + full_reasoning = "" + has_text = False + text_started = False + + # 工具调用累积: index → {id, name, arguments, item_id, started} + tool_calls_acc = {} + + input_tokens = 0 + output_tokens = 0 + seq = 0 + + upstream = None + try: + upstream = requests.post( + DEEPSEEK_URL, + headers=headers, + json=payload, + stream=True, + timeout=120, + ) + upstream.raise_for_status() + for line in upstream.iter_lines(): + if not line: + continue + line = line.decode("utf-8") + if not line.startswith("data: "): + continue + raw = line[6:].strip() + if raw == "[DONE]": + continue + try: + chunk = json.loads(raw) + except json.JSONDecodeError: + continue + + usage = chunk.get("usage") + if usage: + input_tokens = usage.get("prompt_tokens", 0) + output_tokens = usage.get("completion_tokens", 0) + + if "error" in chunk: + err = chunk["error"] + raise Exception( + f"DeepSeek API error: {err.get('message', str(err))}" + ) + + if "choices" not in chunk or not chunk["choices"]: + continue + + delta = chunk["choices"][0].get("delta", {}) + + # ---- 捕获 reasoning_content ---- + reasoning_delta = delta.get("reasoning_content", "") + if reasoning_delta: + full_reasoning += reasoning_delta + + # ---- 处理文本内容 ---- + content = delta.get("content", "") + if content: + if not text_started: + text_started = True + has_text = True + yield "event: response.output_item.added\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_item.added", + "output_index": 0, + "item": { + "id": text_item_id, + "type": "message", + "status": "in_progress", + "role": "assistant", + "content": [], + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + yield "event: response.content_part.added\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.content_part.added", + "item_id": text_item_id, + "output_index": 0, + "content_index": 0, + "part": {"type": "text", "text": ""}, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + full_text += content + seq += 1 + yield "event: response.output_text.delta\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_text.delta", + "delta": content, + "item_id": text_item_id, + "output_index": 0, + "content_index": 0, + "sequence_number": seq, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + # ---- 处理工具调用 ---- + for tc in delta.get("tool_calls", []): + idx = tc.get("index", 0) + if idx not in tool_calls_acc: + item_id = f"item_{uuid.uuid4().hex[:12]}" + tool_calls_acc[idx] = { + "id": "", + "name": "", + "arguments": "", + "item_id": item_id, + "started": False, + } + + acc = tool_calls_acc[idx] + if tc.get("id"): + acc["id"] = tc["id"] + func = tc.get("function", {}) + if func.get("name"): + acc["name"] = func["name"] + + args_delta = func.get("arguments", "") + if args_delta: + acc["arguments"] += args_delta + out_idx = ( + 1 if has_text else 0 + ) + sorted(tool_calls_acc.keys()).index(idx) + + if not acc["started"]: + acc["started"] = True + yield "event: response.output_item.added\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_item.added", + "output_index": out_idx, + "item": { + "id": acc["item_id"], + "type": "function_call", + "status": "in_progress", + "call_id": acc["id"], + "name": acc["name"], + "arguments": "", + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + yield "event: response.function_call_arguments.delta\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.function_call_arguments.delta", + "item_id": acc["item_id"], + "output_index": out_idx, + "delta": args_delta, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + # ===== 流结束后发出完成事件 ===== + + # 文本完成 + if has_text: + yield "event: response.output_text.done\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_text.done", + "text": full_text, + "item_id": text_item_id, + "output_index": 0, + "content_index": 0, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + yield "event: response.content_part.done\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.content_part.done", + "item_id": text_item_id, + "output_index": 0, + "content_index": 0, + "part": {"type": "text", "text": full_text}, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + text_output_item = { + "id": text_item_id, + "type": "message", + "status": "completed", + "role": "assistant", + "content": [{"type": "text", "text": full_text}], + } + if full_reasoning: + text_output_item["reasoning_content"] = full_reasoning + yield "event: response.output_item.done\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_item.done", + "output_index": 0, + "item": text_output_item, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + # 工具调用完成 + output_items = [] + if has_text: + output_items.append({ + "id": text_item_id, + "type": "message", + "status": "completed", + "role": "assistant", + "content": [{"type": "text", "text": full_text}], + **({"reasoning_content": full_reasoning} if full_reasoning else {}), + }) + + for idx in sorted(tool_calls_acc.keys()): + acc = tool_calls_acc[idx] + out_idx = (1 if has_text else 0) + sorted(tool_calls_acc.keys()).index( + idx + ) + + yield "event: response.function_call_arguments.done\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.function_call_arguments.done", + "item_id": acc["item_id"], + "output_index": out_idx, + "arguments": acc["arguments"], + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + func_item = { + "id": acc["item_id"], + "type": "function_call", + "status": "completed", + "call_id": acc["id"], + "name": acc["name"], + "arguments": acc["arguments"], + } + if full_reasoning: + func_item["reasoning_content"] = full_reasoning + yield "event: response.output_item.done\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.output_item.done", + "output_index": out_idx, + "item": func_item, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + output_items.append({ + "id": acc["item_id"], + "type": "function_call", + "status": "completed", + "call_id": acc["id"], + "name": acc["name"], + "arguments": acc["arguments"], + **({"reasoning_content": full_reasoning} if full_reasoning else {}), + }) + + # response.completed + yield "event: response.completed\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.completed", + "response": { + "id": response_id, + "object": "response", + "status": "completed", + "model": effective_model, + "output": output_items, + "usage": { + "input_tokens": input_tokens + or max(1, len(json.dumps(messages)) // 4), + "output_tokens": output_tokens + or max(1, len(full_text) // 4), + "total_tokens": (input_tokens + output_tokens) + or max( + 1, + len(json.dumps(messages)) // 4 + len(full_text) // 4, + ), + }, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + except requests.exceptions.HTTPError as e: + body = "" + try: + if upstream is not None: + body = upstream.text[:2000] + except Exception: + body = "(unable to read error body)" + err_msg = f"DeepSeek API {e.response.status_code}: {body}" + if DEEPSEEK_DEBUG: + _log_debug_error(payload, messages, tools, err_msg, e.response.status_code, body) + yield "event: response.failed\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.failed", + "response": { + "id": response_id, + "object": "response", + "status": "failed", + "model": effective_model, + "error": { + "message": err_msg, + "type": "upstream_error", + }, + "output": [], + "usage": None, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + except requests.exceptions.RequestException as e: + yield "event: response.failed\n" + yield ( + "data: " + + json.dumps( + { + "type": "response.failed", + "response": { + "id": response_id, + "object": "response", + "status": "failed", + "model": effective_model, + "error": { + "message": str(e), + "type": "upstream_error", + }, + "output": [], + "usage": None, + }, + }, + ensure_ascii=False, + ) + + "\n\n" + ) + + finally: + if upstream is not None: + try: + upstream.close() + except Exception: + pass + + return generate diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1773fe9 --- /dev/null +++ b/uv.lock @@ -0,0 +1,349 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "codex2deepseek" +version = "0.2.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "requests" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.0" }, + { name = "requests", specifier = ">=2.28" }, + { name = "uvicorn", specifier = ">=0.44.0" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "starlette" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/66/4d20cdf39a8d6a51e663b7038e3b828ff211d3891a43a713fe7e4643f3a8/starlette-1.1.0.tar.gz", hash = "sha256:e83c7fe0ddecd8719c5b840080325aec0260acec86e9832899e377b91d65e90f", size = 2660060, upload-time = "2026-05-23T16:55:41.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/79/920b8e0a8b20f793e8d64855095cb8febabf6175b8550b6f7a547d813891/starlette-1.1.0-py3-none-any.whl", hash = "sha256:7f0dfd38e428aad5cb6f9f667f0ca1d2d8ca3f3385dccac8305f79ec98458382", size = 72899, upload-time = "2026-05-23T16:55:39.201Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" }, +]