11 KiB
Qwen OAuth And HTTP Flow
Этот документ фиксирует рабочий поток авторизации Qwen OAuth и последующего общения с моделью в new-qwen без вызова qwen -p.
Компоненты
bot/app.pyпринимает сообщения Telegram и ходит вservпо локальному HTTP APIserv/app.pyподнимает HTTP server, управляет OAuth, сессиями, jobs и вызовами моделейserv/oauth.pyреализует Qwen OAuth Device Flowserv/model_router.pyформирует рабочий OpenAI-compatible HTTP запрос в Qwen
Текущая конфигурация:
servслушает127.0.0.1:8081botходит вhttp://127.0.0.1:8081
1. OAuth Device Flow
Источник истины по OAuth endpoints:
QWEN_OAUTH_BASE_URL = https://chat.qwen.aidevice code endpoint = https://chat.qwen.ai/api/v1/oauth2/device/codetoken endpoint = https://chat.qwen.ai/api/v1/oauth2/tokenclient_id = f0304373b74a44d2b584a3fb70ca9e56scope = openid profile email model.completiongrant_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(), который делает:
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: <uuid>
Body:
client_id=f0304373b74a44d2b584a3fb70ca9e56
scope=openid profile email model.completion
code_challenge=<pkce_sha256>
code_challenge_method=S256
Ответ содержит:
device_codeuser_codeverification_uriverification_uri_completeexpires_in
Бот показывает пользователю ссылку вида:
https://chat.qwen.ai/authorize?user_code=ORRNFVIN&client=qwen-code
1.2 Polling device flow
После подтверждения в браузере bot циклически вызывает:
POST /api/v1/auth/device/poll
serv делает:
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: <uuid>
Body:
grant_type=urn:ietf:params:oauth:grant-type:device_code
client_id=f0304373b74a44d2b584a3fb70ca9e56
device_code=<device_code>
code_verifier=<pkce_verifier>
Поведение:
authorization_pending=> продолжаем pollingslow_down=> увеличиваем интервал polling- success => сохраняем credentials
1.3 Сохраняемые credentials
Файл:
~/.qwen/oauth_creds.json
Формат:
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"resource_url": "portal.qwen.ai",
"expiry_date": 1775689403149
}
Поля:
access_tokenиспользуется вAuthorization: Bearer ...refresh_tokenнужен для refreshresource_urlприходит от Qwen OAuth и используется как inference hostexpiry_dateхранится в миллисекундах Unix epoch
2. Refresh Token Flow
Когда access token истекает, serv/oauth.py вызывает:
POST https://chat.qwen.ai/api/v1/oauth2/token
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Accept: application/json
Body:
grant_type=refresh_token
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- затем
botpolling-ом вызывает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 это обычно даёт:
https://portal.qwen.ai/v1
4.2 Реальный рабочий endpoint
serv/model_router.py делает:
POST https://portal.qwen.ai/v1/chat/completions
Headers:
Content-Type: application/json
Authorization: Bearer <access_token>
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
Главная причина, почему ранние попытки ломались с:
{"error":{"code":"invalid_parameter_error","message":"bad request"}}
была в неверном формате messages[].content.
Qwen Portal ожидает не строку, а массив content-blocks.
5.1 Неправильный формат
Это не работало:
{
"model": "coder-model",
"messages": [
{ "role": "user", "content": "Ку" }
]
}
5.2 Рабочий формат
Это рабочий формат:
{
"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_tokensmetadata.sessionIdmetadata.promptIdvl_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 формате:
{
"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 и повторить его.
Практический метод:
- Была сделана локальная копия
qwenbundle - В
buildRequest(...)был вставлен лог final request body - Инструментированная копия
qwenбыла запущена локально - Из её лога был взят точный JSON request body
serv/model_router.pyбыл приведён к этому формату
Именно это включило чистый HTTP path.
8. Последовательность end-to-end
8.1 Авторизация
- Telegram user ->
/auth bot->POST /api/v1/auth/device/startserv->POST https://chat.qwen.ai/api/v1/oauth2/device/code- user подтверждает login в браузере
bot->POST /api/v1/auth/device/pollserv->POST https://chat.qwen.ai/api/v1/oauth2/tokenservсохраняет~/.qwen/oauth_creds.json
8.2 Обычный chat request
- Telegram user ->
"Ку" bot->POST /api/v1/chat/startserv/llm.pyсобирает историю и системный promptserv/model_router.pyнормализует messages в content-block arraysserv/model_router.py->POST https://portal.qwen.ai/v1/chat/completions- Qwen возвращает
choices[0].message servлибо завершает ответ, либо выполняет tool calls и делает следующий roundbotчерез/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:8081botходит вhttp://127.0.0.1:8081
10. Файлы, которые надо смотреть при будущих изменениях
serv/oauth.pyserv/model_router.pyserv/llm.pyserv/app.pybot/app.pybot/config.py
Если Qwen снова начнёт возвращать invalid_parameter_error, первым делом нужно сравнивать не только headers и endpoint, а точный JSON body с рабочим qwen-code.