Qwen-Reverse
Reverse-engineered async Python client for chat.qwen.ai — text chat, streaming with real-time reasoning, tool calling, image and video generation. No official API key required (works anonymously).
pip install qwen-reverse
Features
- Chat — one-shot, streaming (SSE incremental), multi-turn with conversation memory (
conversation_id/parent_idchaining) - Real-time reasoning —
reasoningevents streamed token-by-token before the answer, in both plain and multi-turn mode - Tool calling — OpenAI-style function definitions; either let the SDK execute them (JSON-stringified follow-up) or handle them yourself with
emit_tool_calls=True - Vision — image upload + chat about it (pass files/URLs/bytes)
- Image editing (i2i) —
chat_type="image_edit": edit an uploaded image (add/remove/modify) and get back CDN URLs - File upload —
upload()uploads local files (or URLs/bytes) into the web API for vision/editing chats - Image / video generation — t2i and t2v returning CDN URLs (
cdn.qwenlm.ai) - No account required — the web API works without a token; OAuth device-flow login (
chat.qwen.aiaccount) is also implemented - Optional FastAPI server — OpenAI-compatible
/v1/chat/completionsand/v1/models(seeserver/)
Quickstart
import asyncio
from qwen_reverse import Generative
async def main():
gen = Generative()
print(await gen.generate("Explain what a kernel is in 2 lines"))
asyncio.run(main())
Streaming with real-time thinking
import asyncio
from qwen_reverse import Generative
async def main():
gen = Generative()
async for event in gen.stream("Explain in 2 sentences what an OS kernel is"):
if event["type"] == "reasoning":
print(f"\r\033[90m{event['data']}\033[0m", end="", flush=True)
elif event["type"] == "content":
print(event["data"], end="", flush=True)
asyncio.run(main())
Multi-turn conversation
import asyncio
from qwen_reverse import Conversation
async def main():
conv = Conversation()
r1 = await conv.send("My name is Popbob and I work with kernels in C.")
r2 = await conv.send("What is my name?") # remembers turn 1
print(r2) # "Popbob"
print(conv.conversation_id, conv.parent_id)
asyncio.run(main())
Tool calling (automatic execution)
import asyncio
from qwen_reverse import Conversation
WEATHER_TOOL = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
async def main():
conv = Conversation(tools=[WEATHER_TOOL])
async for event in conv.stream("What's the weather in Buenos Aires?"):
if event["type"] == "tool_calls":
print("[tool_calls]", event["data"])
elif event["type"] == "content":
print(event["data"], end="", flush=True)
asyncio.run(main())
For raw tool-call JSON instead of execution, pass emit_tool_calls=True to stream()/create_chat().
Image generation
import asyncio
from qwen_reverse import Image
async def main():
img = Image()
urls = await img.generate(
"A cyberpunk dragon flying over a neon city, anime style",
aspect_ratio="16:9",
)
print(urls[0]) # https://cdn.qwenlm.ai/output/...
asyncio.run(main())
Upload a file + chat with vision
import asyncio
from qwen_reverse import Conversation
async def main():
conv = Conversation(model="qwen3-vl-plus") # a vision-capable model
reply = await conv.send(
"What is written on the whiteboard?",
files=["photo.jpg"], # path | bytes | URL | already-uploaded dict
)
print(reply)
asyncio.run(main())
files accepts local paths, raw bytes, http(s) URLs (downloaded then uploaded) or a pre-uploaded file dict. Use upload() to upload manually:
import asyncio
from qwen_reverse import upload
async def main():
file_dict = await upload("diagram.png") # -> {"id", "url", "type", ...}
# reuse file_dict in multiple chats without re-uploading
asyncio.run(main())
Edit an image (image-to-image)
import asyncio
from qwen_reverse import ImageEdit
async def main():
img = ImageEdit() # chat_type="image_edit"
urls = await img.edit(
"Add a bold arrow crossing through the logo from top-left to bottom-right, "
"keep the original violet symbol exactly the same",
"logo.png", # path | bytes | URL | uploaded dict
aspect_ratio="1:1",
)
print(urls[0]) # edited image CDN URL
asyncio.run(main())
You can also call Generative(...).edit_image(prompt, image, aspect_ratio=...) directly.
API
| Symbol | Description |
|---|---|
Generative(model=..., token=...) |
.generate(), .stream() (events: reasoning, content, usage, tool_calls, done) |
Conversation(token=..., tools=...) |
.send(), .stream(), .reset() — persists conversation_id/parent_id across turns |
Image(model=...), Video(model=...), ImageEdit(model=...) |
.generate(prompt, aspect_ratio=...) → list of CDN URLs; ImageEdit.edit(prompt, image, aspect_ratio=...) for image-to-image editing |
create_chat(model, messages, ...) |
low-level async generator; conversation_id/parent_id for multi-turn |
fetch_models() |
available models (qwen3.8-max, qwen3.7-plus, qwen3-vl-plus, ...) |
upload(data, filename=...) |
upload a local path/bytes (or fetch+upload a URL) → file payload dict; returns the web-API file object |
resolve_files(files) |
normalize a list of paths/bytes/URLs/dicts into upload-ready payload dicts |
upload_file(...) |
low-level file upload (advanced: pass your own session/headers) |
start_device_login() / complete_device_login(...) |
OAuth device flow |
QwenOAuth2Client, SharedTokenManager |
token management |
generate_cookies() / generate_fingerprint() / BXUAGenerator |
anti-bot primitives (cookies ssxmod_itna, bx-umidtoken, fingerprint) |
Events emitted by stream()
| Event | Data | When |
|---|---|---|
reasoning |
str (chunk) |
during phase: "think" — the model's chain of thought |
content |
str (chunk) |
during phase: "answer" — the actual reply |
tool_calls |
list[dict] |
when the model requests functions |
image / image_done |
{"url", "extra"} / None |
image generation progress |
usage |
dict |
token usage (input_tokens, output_tokens, ...) |
done |
— | final event; carries conversation_id and parent_id |
Authentication
Anonymous mode works — no token required for most features (lower rate limits). For higher limits, log in with a chat.qwen.ai account:
import asyncio
from qwen_reverse import start_device_login, complete_device_login
async def main():
client, data = await start_device_login()
print(data["verification_uri_complete"]) # open in a logged-in browser
# after authorizing:
token = await complete_device_login(client, data)
print(token)
Models
Defaults to qwen3.8-max. Others: qwen3.7-plus, qwen3.7-max, plus vision / image / video models — see qwen_reverse/models.py and fetch_models().
Running the tests
pip install -e ".[dev]"
pytest
Optional server (OpenAI-compatible)
pip install "qwen-reverse[server]"
cd server
uvicorn app.main:app --port 8000
# GET /v1/models
# POST /v1/chat/completions (OpenAI-style, streaming SSE)
How it works
The client replicates what the official web app does against chat.qwen.ai:
- Create a chat via
POST /api/v2/chats/new(gets achat_id) - Stream the response via
POST /api/v2/chat/completions?chat_id=...withversion: 2.1,incremental_outputand afeature_configthat enables the reasoning stream - Multi-turn chaining uses the server's
response_id(assistant message fid) as the next turn'sparent_id - Anti-bot headers are regenerated per request:
ssxmod_itnacookies (custom LZW + custom base64),bx-umidtoken, fingerprint
Disclaimer
This project is for educational and research purposes. It is not affiliated with or endorsed by Alibaba/Qwen. Use at your own risk — the endpoints may change or the service may rate-limit or block unofficial clients. MIT licensed; reverse-engineering references based on g4f (MIT).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qwen_reverse-0.1.3.tar.gz.
File metadata
- Download URL: qwen_reverse-0.1.3.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d61a19744b819ec81b64e129e48397cbe02812c024369e6a183ee23e4241612
|
|
| MD5 |
3159c3d4c497c276933ed59622cc5ab9
|
|
| BLAKE2b-256 |
560671191b6ddf79e06f8d1db0b9e48ea9495869694f8d336de502fac1ae5385
|
File details
Details for the file qwen_reverse-0.1.3-py3-none-any.whl.
File metadata
- Download URL: qwen_reverse-0.1.3-py3-none-any.whl
- Upload date:
- Size: 26.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37343c58272556a37f526659e1698f3eb0e0975b433d62e48a3bea653d78585f
|
|
| MD5 |
da12c93f1561e9eb6786d04908c6dca8
|
|
| BLAKE2b-256 |
38f006e1995e3aba127bdbcaeacb0231df79a2564f9541a429a63f8243cb1a28
|