Quinn Qwen3.5 Omni Plus Realtime
GET/v1/realtime
OpenAI Realtime-style WebSocket for qwen3.5-omni-plus-realtime. Video is sent as JPEG frames, not a raw video stream.
Snapshot alias qwen3.5-omni-plus-realtime-2026-03-15 has the same capability.
Use a platform-issued API token. Do not send OpenAI Sec-WebSocket-Protocol (openai-insecure-api-key.*); this gateway and the upstream both use Bearer.
Connect
This is a WebSocket endpoint (wss://), not an HTTP GET. Do not call it with a normal HTTPS curl.
wss://open-api.fancyai.com/v1/realtime?model=qwen3.5-omni-plus-realtime
Authorization: Bearer YOUR_API_KEY
curl (needs curl 8+ with WebSocket support):
curl --no-buffer \
-H "Authorization: Bearer $API_KEY" \
"wss://open-api.fancyai.com/v1/realtime?model=qwen3.5-omni-plus-realtime"
After the handshake, the server sends session.created.
Python:
import os
import json
import websocket
WS_URL = "wss://open-api.fancyai.com/v1/realtime?model=qwen3.5-omni-plus-realtime"
API_KEY = os.environ["API_KEY"]
def on_open(ws):
print("connected")
def on_message(ws, message):
print(json.dumps(json.loads(message), ensure_ascii=False, indent=2))
ws = websocket.WebSocketApp(
WS_URL,
header=["Authorization: Bearer " + API_KEY],
on_open=on_open,
on_message=on_message,
)
ws.run_forever()
Request
Responses
- 101
- 400
- 401
- 429
Switching Protocols — WebSocket established
Invalid request parameters
Unauthorized
Rate limit exceeded
Configure the session
Send session.update after connect:
{
"event_id": "event_001",
"type": "session.update",
"session": {
"modalities": ["text", "audio"],
"voice": "Tina",
"instructions": "You are a voice assistant. Answer briefly in spoken language.",
"audio": {
"input": { "format": { "type": "pcm", "sample_rate": 16000 } },
"output": { "format": { "type": "pcm", "sample_rate": 24000 } }
},
"turn_detection": {
"type": "semantic_vad",
"threshold": 0.5,
"silence_duration_ms": 800
}
}
}
| Field | Notes |
|---|---|
modalities | ["text"] text only; ["text","audio"] text + speech |
voice | Output voice, default Tina |
audio.input/output.format | Qwen 3.5 Realtime only. Input default pcm/16000, output default pcm/24000. Sample rate: 8000 / 16000 / 24000 / 48000 |
input_audio_format / output_audio_format | Legacy fields, still accepted |
turn_detection | semantic_vad (recommended) or server_vad. null = manual mode |
enable_search | Web search, off by default. Cannot be used together with tools |
tools | Function tools. Cannot be used together with enable_search |
Server replies with session.updated.
End the session with session.finish, or close the WebSocket. Leaving it open keeps accumulating context and billing.
Send audio
Base64-encode PCM/WAV and send:
{
"type": "input_audio_buffer.append",
"audio": "<base64>"
}
About 100ms per packet is recommended.
VAD mode (semantic_vad / server_vad): the server auto-commits and replies after it detects you finished speaking.
Manual mode (turn_detection is null): after audio, submit and trigger a reply:
{ "type": "input_audio_buffer.commit" }
{ "type": "response.create" }
Cancel the current reply:
{ "type": "response.cancel" }
Send images (video frames)
The model does not accept raw video. Extract JPEG frames and send Base64:
{
"type": "input_image_buffer.append",
"image": "<base64 jpeg>"
}
Constraints:
- Send
input_audio_buffer.appendat least once first - JPG/JPEG; 480P or 720P recommended, max 1080P
- Base64 payload ≤ 256KB per frame; raw size ≤ 190KB recommended
- Live camera: 1 fps (max 2 fps)
- In VAD mode, finish the current frame before
input_audio_buffer.speech_stopped
Audio is the timeline; images are inserted at send time. Camera can be turned on or off at any time.
Text-only input
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "What's the weather outside?" }]
}
}
In manual mode, follow with response.create.
Receive replies
Text only:
| Event | Meaning |
|---|---|
response.text.delta | Incremental text |
response.text.done | Full text |
Text + audio:
| Event | Meaning |
|---|---|
response.audio_transcript.delta | Incremental transcript of speech |
response.audio_transcript.done | Full transcript |
response.audio.delta | Base64 audio chunk; decode and play |
response.audio.done | This audio segment ended |
response.done | Turn finished, includes usage |
Other events:
| Event | Meaning |
|---|---|
session.created | Connection ready |
session.updated | Session config applied |
input_audio_buffer.speech_started | User started speaking (you may cancel the current reply) |
input_audio_buffer.speech_stopped | User finished speaking |
conversation.item.input_audio_transcription.completed | User speech transcribed (if input transcription is on) |
error | Error |
Billing
Billed from usage on each response.done, not by connection duration. In multi-turn chats, audio / image / text still in the window is counted again as this turn's input.
When the reply includes speech, output text is not billed separately (matches gateway settlement).
Singapore list price converted at CNY/USD 6.8:
| Item | plus-realtime |
|---|---|
| Input: text / image / video frame | $2.315 / M |
| Input: audio | $18.184 / M |
| Output: text only | $13.666 / M |
| Output: text + audio (output text not billed) | $68.329 / M |
Audio conversion (round up to 1 second):
- Input: seconds × 7
- Output: seconds × 12.5
Images / video frames: about 1 token per 32×32 pixels, min 4 / max 1280 per image.
Web search is billed extra and is off by default. tools and enable_search cannot both be on.
Limits
| plus-realtime | |
|---|---|
| Max audio turns | 100 |
| Max video (frame) turns | 50 |
| Max audio duration | 600 s |
| Max video (frame) duration | 240 s |
| Single session | 120 minutes |
The server disconnects on timeout. Older history is dropped.
Choose a model
Prefer plus for stronger understanding and longer context. For lower latency, use qwen3.5-omni-flash-realtime. This API does not provide WebRTC / AOQ.
Official capability notes: Qwen-Omni realtime. Clients must connect only to this platform /v1/realtime.