# Qwen OAuth And HTTP Flow Этот документ фиксирует рабочий поток авторизации Qwen OAuth и последующего общения с моделью в `new-qwen` без вызова `qwen -p`. ## Компоненты - `bot/app.py` принимает сообщения Telegram и ходит в `serv` по локальному HTTP API - `serv/app.py` поднимает HTTP server, управляет OAuth, сессиями, jobs и вызовами моделей - `serv/oauth.py` реализует Qwen OAuth Device Flow - `serv/model_router.py` формирует рабочий OpenAI-compatible HTTP запрос в Qwen Текущая конфигурация: - `serv` слушает `127.0.0.1:8081` - `bot` ходит в `http://127.0.0.1:8081` ## 1. OAuth Device Flow Источник истины по OAuth endpoints: - `QWEN_OAUTH_BASE_URL = https://chat.qwen.ai` - `device code endpoint = https://chat.qwen.ai/api/v1/oauth2/device/code` - `token endpoint = https://chat.qwen.ai/api/v1/oauth2/token` - `client_id = f0304373b74a44d2b584a3fb70ca9e56` - `scope = openid profile email model.completion` - `grant_type = urn:ietf:params:oauth:grant-type:device_code` ### 1.1 Запуск device flow Telegram пользователь отправляет `/auth`. `bot` инициирует: - `POST /api/v1/auth/device/start` `serv` вызывает `QwenOAuthManager.start_device_flow()`, который делает: ```http POST https://chat.qwen.ai/api/v1/oauth2/device/code Content-Type: application/x-www-form-urlencoded; charset=utf-8 Accept: application/json x-request-id: ``` Body: ```x-www-form-urlencoded client_id=f0304373b74a44d2b584a3fb70ca9e56 scope=openid profile email model.completion code_challenge= code_challenge_method=S256 ``` Ответ содержит: - `device_code` - `user_code` - `verification_uri` - `verification_uri_complete` - `expires_in` Бот показывает пользователю ссылку вида: ```text https://chat.qwen.ai/authorize?user_code=ORRNFVIN&client=qwen-code ``` ### 1.2 Polling device flow После подтверждения в браузере `bot` циклически вызывает: - `POST /api/v1/auth/device/poll` `serv` делает: ```http POST https://chat.qwen.ai/api/v1/oauth2/token Content-Type: application/x-www-form-urlencoded; charset=utf-8 Accept: application/json x-request-id: ``` Body: ```x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:device_code client_id=f0304373b74a44d2b584a3fb70ca9e56 device_code= code_verifier= ``` Поведение: - `authorization_pending` => продолжаем polling - `slow_down` => увеличиваем интервал polling - success => сохраняем credentials ### 1.3 Сохраняемые credentials Файл: - `~/.qwen/oauth_creds.json` Формат: ```json { "access_token": "...", "refresh_token": "...", "token_type": "Bearer", "resource_url": "portal.qwen.ai", "expiry_date": 1775689403149 } ``` Поля: - `access_token` используется в `Authorization: Bearer ...` - `refresh_token` нужен для refresh - `resource_url` приходит от Qwen OAuth и используется как inference host - `expiry_date` хранится в миллисекундах Unix epoch ## 2. Refresh Token Flow Когда access token истекает, `serv/oauth.py` вызывает: ```http POST https://chat.qwen.ai/api/v1/oauth2/token Content-Type: application/x-www-form-urlencoded; charset=utf-8 Accept: application/json ``` Body: ```x-www-form-urlencoded grant_type=refresh_token refresh_token= client_id=f0304373b74a44d2b584a3fb70ca9e56 ``` На практике возможен WAF challenge на этом endpoint. Поэтому главный рабочий сценарий сейчас такой: - логин делается через device flow - inference использует уже сохранённый `access_token` - `resource_url` берётся из credentials ## 3. Как bot ходит в serv Основной пользовательский запрос: - пользователь отправляет обычный текст в Telegram - `bot/app.py` вызывает `POST /api/v1/chat/start` - затем `bot` polling-ом вызывает `POST /api/v1/chat/poll` Если OAuth ещё не завершён: - бот кладёт сообщение в локальную очередь - после успешного `/auth` автоматически отправляет queued message в `serv` ## 4. Как serv ходит в Qwen model endpoint ### 4.1 Базовый URL модели `serv/oauth.py -> get_openai_base_url()`: - берёт `resource_url` из `~/.qwen/oauth_creds.json` - если нет схемы, добавляет `https://` - если нет suffix `/v1`, добавляет `/v1` При текущем Qwen OAuth это обычно даёт: ```text https://portal.qwen.ai/v1 ``` ### 4.2 Реальный рабочий endpoint `serv/model_router.py` делает: ```http POST https://portal.qwen.ai/v1/chat/completions ``` Headers: ```http Content-Type: application/json Authorization: Bearer User-Agent: QwenCode/unknown (linux; x64) X-DashScope-CacheControl: enable X-DashScope-UserAgent: QwenCode/unknown (linux; x64) X-DashScope-AuthType: qwen-oauth Accept: application/json ``` ## 5. Критически важный формат request body Главная причина, почему ранние попытки ломались с: ```json {"error":{"code":"invalid_parameter_error","message":"bad request"}} ``` была в неверном формате `messages[].content`. Qwen Portal ожидает не строку, а массив content-blocks. ### 5.1 Неправильный формат Это не работало: ```json { "model": "coder-model", "messages": [ { "role": "user", "content": "Ку" } ] } ``` ### 5.2 Рабочий формат Это рабочий формат: ```json { "model": "coder-model", "messages": [ { "role": "system", "content": [ { "type": "text", "text": "You are a helpful assistant." } ] }, { "role": "user", "content": [ { "type": "text", "text": "Reply with exactly pong" } ] } ], "max_tokens": 8000, "metadata": { "sessionId": "8ad7c4ca-184f-41dd-85d5-5ab3b3d3d4a7", "promptId": "6d8d4a7be1c2" }, "vl_high_resolution_images": true } ``` ### 5.3 Что именно обязательно Для рабочего HTTP path критично оказалось: - `model = "coder-model"` - `messages[*].content` в виде массива блоков - наличие `system` и `user` сообщений - `max_tokens` - `metadata.sessionId` - `metadata.promptId` - `vl_high_resolution_images = true` - правильные DashScope/Qwen headers ### 5.4 Что оказалось лишним или вредным - строковый `content` ломал запрос - `tool_choice = "auto"` в ранней реализации не использовался в эталонном рабочем request body - fallback на другие DashScope endpoints не заменял правильный body format - вызов через `qwen -p` для runtime работы больше не нужен ## 6. Tool calling `serv` передаёт tools в OpenAI-compatible формате: ```json { "type": "function", "function": { "name": "read_file", "description": "...", "parameters": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } } } ``` Список инструментов формируется в `serv/tools.py`. После ответа модели: - `assistant.tool_calls` разбираются в `serv/llm.py` - инструмент исполняется на стороне `serv` - tool result добавляется обратно в историю как `role = "tool"` - затем выполняется следующий `chat/completions` Внутренне `serv/model_router.py` тоже нормализует `tool` message content в content-block array. ## 7. Диагностика, которая реально помогла ### 7.1 Что не сработало - смена только `X-DashScope-AuthType` - смена только fallback URL - прямые запросы на `dashscope.aliyuncs.com` и `coding-intl.dashscope.aliyuncs.com` - минимальный body со строковым `content` ### 7.2 Что сработало Нужно было снять эталонный request body из реально работающего `qwen-code` и повторить его. Практический метод: 1. Была сделана локальная копия `qwen` bundle 2. В `buildRequest(...)` был вставлен лог final request body 3. Инструментированная копия `qwen` была запущена локально 4. Из её лога был взят точный JSON request body 5. `serv/model_router.py` был приведён к этому формату Именно это включило чистый HTTP path. ## 8. Последовательность end-to-end ### 8.1 Авторизация 1. Telegram user -> `/auth` 2. `bot` -> `POST /api/v1/auth/device/start` 3. `serv` -> `POST https://chat.qwen.ai/api/v1/oauth2/device/code` 4. user подтверждает login в браузере 5. `bot` -> `POST /api/v1/auth/device/poll` 6. `serv` -> `POST https://chat.qwen.ai/api/v1/oauth2/token` 7. `serv` сохраняет `~/.qwen/oauth_creds.json` ### 8.2 Обычный chat request 1. Telegram user -> `"Ку"` 2. `bot` -> `POST /api/v1/chat/start` 3. `serv/llm.py` собирает историю и системный prompt 4. `serv/model_router.py` нормализует messages в content-block arrays 5. `serv/model_router.py` -> `POST https://portal.qwen.ai/v1/chat/completions` 6. Qwen возвращает `choices[0].message` 7. `serv` либо завершает ответ, либо выполняет tool calls и делает следующий round 8. `bot` через `/api/v1/chat/poll` получает итог и отправляет его в Telegram ## 9. Текущие инварианты реализации - inference path идёт через чистый HTTP из `serv` - runtime path не зависит от `qwen -p` - credentials хранятся в `~/.qwen/oauth_creds.json` - основной inference host берётся из `resource_url` - текущий рабочий `resource_url` для OAuth path: `portal.qwen.ai` - `serv` слушает `127.0.0.1:8081` - `bot` ходит в `http://127.0.0.1:8081` ## 10. Файлы, которые надо смотреть при будущих изменениях - `serv/oauth.py` - `serv/model_router.py` - `serv/llm.py` - `serv/app.py` - `bot/app.py` - `bot/config.py` Если Qwen снова начнёт возвращать `invalid_parameter_error`, первым делом нужно сравнивать не только headers и endpoint, а точный JSON body с рабочим `qwen-code`.