| """Serve the System-2 backbone (Qwen3.5-9B) as an OpenAI-compatible endpoint. |
| |
| Preferred path - vLLM (recommended by the Qwen3.5 recipes, single GPU): |
| vllm serve Qwen/Qwen3.5-9B --port 8000 |
| |
| This script is the fallback when vLLM is unavailable: it wraps transformers |
| in a minimal /v1/chat/completions server. Both expose the same contract the |
| System2Controller expects (mode="openai"). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import base64 |
| import io |
| import re |
|
|
|
|
| def extract_messages(payload: dict) -> tuple[list[dict], dict]: |
| messages = payload.get("messages", []) |
| gen_kwargs = { |
| "temperature": payload.get("temperature", 0.2), |
| "max_new_tokens": payload.get("max_tokens", 2048), |
| } |
| return messages, gen_kwargs |
|
|
|
|
| def decode_images(messages: list[dict]) -> list: |
| """Pull base64 PNGs out of OpenAI-style image_url content blocks.""" |
| images = [] |
| for msg in messages: |
| content = msg.get("content") |
| if isinstance(content, list): |
| for part in content: |
| if part.get("type") == "image_url": |
| url = part["image_url"]["url"] |
| b64 = re.sub("^data:image/\\w+;base64,", "", url) |
| from PIL import Image |
|
|
| images.append(Image.open(io.BytesIO(base64.b64decode(b64)))) |
| return images |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--model", default="Qwen/Qwen3.5-9B") |
| ap.add_argument("--port", type=int, default=8000) |
| args = ap.parse_args() |
|
|
| import torch |
| from flask import Flask, jsonify, request |
| from transformers import AutoProcessor, AutoModelForImageTextToText |
|
|
| processor = AutoProcessor.from_pretrained(args.model) |
| model = AutoModelForImageTextToText.from_pretrained( |
| args.model, torch_dtype=torch.bfloat16, device_map="auto" |
| ) |
|
|
| app = Flask(__name__) |
|
|
| @app.post("/v1/chat/completions") |
| def completions(): |
| payload = request.get_json(force=True) |
| messages, gen = extract_messages(payload) |
| text = processor.apply_chat_template( |
| messages, add_generation_prompt=True, tokenize=False |
| ) |
| inputs = processor(text=text, images=decode_images(messages) or None, |
| return_tensors="pt").to(model.device) |
| out = model.generate(**inputs, **gen) |
| completion = processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], |
| skip_special_tokens=True)[0] |
| return jsonify({ |
| "id": "qwenjev-s2", |
| "choices": [{"message": {"role": "assistant", "content": completion}, |
| "finish_reason": "stop"}], |
| }) |
|
|
| app.run(host="127.0.0.1", port=args.port) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|