Skip to content

vllm_mlx.models.mllm

MLX Multimodal Language Model (MLLM) wrapper.

View the complete module source at #L1-L2944.

API details

Each callable below includes its exact signature, type annotations, inputs, defaults, return contract, documented exceptions, implementation source, and parsed docstring sections when the source provides them.

vllm_mlx.models.mllm

MLX Multimodal Language Model (MLLM) wrapper.

This module provides a wrapper around mlx-vlm for multimodal inference, supporting vision, audio, and video understanding on Apple Silicon.

Features: - OpenAI-compatible API format for images and video - Smart video frame extraction with configurable FPS - Base64 and URL image support - Streaming generation - MLLM KV cache for repeated image/video+prompt combinations

vllm_mlx.models.mllm.logger module-attribute

logger = logging.getLogger(__name__)

vllm_mlx.models.mllm._temp_manager module-attribute

_temp_manager = TempFileManager()

vllm_mlx.models.mllm.FRAME_FACTOR module-attribute

FRAME_FACTOR = 2

vllm_mlx.models.mllm.DEFAULT_FPS module-attribute

DEFAULT_FPS = 2.0

vllm_mlx.models.mllm.MIN_FRAMES module-attribute

MIN_FRAMES = 4

vllm_mlx.models.mllm.MAX_FRAMES module-attribute

MAX_FRAMES = 128

vllm_mlx.models.mllm.IMAGE_FACTOR module-attribute

IMAGE_FACTOR = 28

vllm_mlx.models.mllm.MAX_IMAGE_SIZE module-attribute

MAX_IMAGE_SIZE = 20 * 1024 * 1024

vllm_mlx.models.mllm.MAX_VIDEO_SIZE module-attribute

MAX_VIDEO_SIZE = 500 * 1024 * 1024

vllm_mlx.models.mllm.MAX_AUDIO_SIZE module-attribute

MAX_AUDIO_SIZE = 100 * 1024 * 1024

vllm_mlx.models.mllm.MAX_BASE64_IMAGE_LENGTH module-attribute

MAX_BASE64_IMAGE_LENGTH = 30 * 1024 * 1024

vllm_mlx.models.mllm.MAX_BASE64_VIDEO_LENGTH module-attribute

MAX_BASE64_VIDEO_LENGTH = 700 * 1024 * 1024

vllm_mlx.models.mllm.MAX_BASE64_AUDIO_LENGTH module-attribute

MAX_BASE64_AUDIO_LENGTH = 140 * 1024 * 1024

vllm_mlx.models.mllm._DRAFT_KWARG_NAMES module-attribute

_DRAFT_KWARG_NAMES = ('draft_model', 'draft_kind', 'draft_block_size')

vllm_mlx.models.mllm._VIDEO_EXT_MAP module-attribute

_VIDEO_EXT_MAP: dict[str, str] = {'mp4': '.mp4', 'webm': '.webm', 'avi': '.avi', 'mov': '.mov', 'quicktime': '.mov', 'mkv': '.mkv'}

vllm_mlx.models.mllm._AUDIO_EXT_MAP module-attribute

_AUDIO_EXT_MAP: dict[str, str] = {'wav': '.wav', 'mpeg': '.mp3', 'mp3': '.mp3', 'flac': '.flac', 'ogg': '.ogg', 'webm': '.webm', 'mp4': '.m4a', 'm4a': '.m4a', 'aac': '.m4a'}

vllm_mlx.models.mllm._base64_image_cache module-attribute

_base64_image_cache: dict[str, str] = {}

vllm_mlx.models.mllm.MLXVisionLanguageModel module-attribute

MLXVisionLanguageModel = MLXMultimodalLM

vllm_mlx.models.mllm.VLMOutput module-attribute

VLMOutput = MLLMOutput

vllm_mlx.models.mllm.is_vlm_model module-attribute

vllm_mlx.models.mllm.TempFileManager

TempFileManager()

Thread-safe manager for tracking and cleaning up temporary files.

Source code in vllm_mlx/models/mllm.py
def __init__(self):
    self._files: set[str] = set()
    self._lock = threading.Lock()
    atexit.register(self.cleanup_all)

vllm_mlx.models.mllm.TempFileManager._files instance-attribute

_files: set[str] = set()

vllm_mlx.models.mllm.TempFileManager._lock instance-attribute

_lock = threading.Lock()

vllm_mlx.models.mllm.TempFileManager.register

register(path: str) -> str

Register a temp file for tracking. Returns the path for convenience.

Source code in vllm_mlx/models/mllm.py
def register(self, path: str) -> str:
    """Register a temp file for tracking. Returns the path for convenience."""
    with self._lock:
        self._files.add(path)
    return path

vllm_mlx.models.mllm.TempFileManager.cleanup

cleanup(path: str) -> bool

Clean up a specific temp file. Returns True if successful.

Source code in vllm_mlx/models/mllm.py
def cleanup(self, path: str) -> bool:
    """Clean up a specific temp file. Returns True if successful."""
    with self._lock:
        if path in self._files:
            self._files.discard(path)
    try:
        if os.path.exists(path):
            os.unlink(path)
            logger.debug(f"Cleaned up temp file: {path}")
            return True
    except OSError as e:
        logger.warning(f"Failed to clean up temp file {path}: {e}")
    return False

vllm_mlx.models.mllm.TempFileManager.cleanup_all

cleanup_all() -> int

Clean up all tracked temp files. Returns count of cleaned files.

Source code in vllm_mlx/models/mllm.py
def cleanup_all(self) -> int:
    """Clean up all tracked temp files. Returns count of cleaned files."""
    with self._lock:
        files_to_clean = list(self._files)
        self._files.clear()

    cleaned = 0
    for path in files_to_clean:
        try:
            if os.path.exists(path):
                os.unlink(path)
                cleaned += 1
        except OSError:
            pass

    if cleaned:
        logger.info(f"Cleaned up {cleaned} temp files")
    return cleaned

vllm_mlx.models.mllm.FileSizeExceededError

Bases: Exception

Raised when a downloaded file exceeds the size limit.

vllm_mlx.models.mllm.UnsafeRemoteURLError

UnsafeRemoteURLError(message: str, *, public_message: str = 'Remote media URL is not allowed')

Bases: ValueError

Raised when a remote media URL targets an unsafe destination.

Source code in vllm_mlx/models/mllm.py
def __init__(
    self,
    message: str,
    *,
    public_message: str = "Remote media URL is not allowed",
) -> None:
    super().__init__(message)
    self.public_message = public_message

vllm_mlx.models.mllm.UnsafeRemoteURLError.public_message instance-attribute

public_message = public_message

vllm_mlx.models.mllm.MultimodalInput dataclass

MultimodalInput(prompt: str, images: list[str] = list(), videos: list[str] = list(), audio: list[str] = list())

Input for multimodal generation.

vllm_mlx.models.mllm.MultimodalInput.prompt instance-attribute

prompt: str

vllm_mlx.models.mllm.MultimodalInput.images class-attribute instance-attribute

images: list[str] = field(default_factory=list)

vllm_mlx.models.mllm.MultimodalInput.videos class-attribute instance-attribute

videos: list[str] = field(default_factory=list)

vllm_mlx.models.mllm.MultimodalInput.audio class-attribute instance-attribute

audio: list[str] = field(default_factory=list)

vllm_mlx.models.mllm.MLLMOutput dataclass

MLLMOutput(text: str, finish_reason: str | None = None, prompt_tokens: int = 0, completion_tokens: int = 0, mtp_drafts: int = 0, mtp_accepted: int = 0)

Output from multimodal language model.

vllm_mlx.models.mllm.MLLMOutput.text instance-attribute

text: str

vllm_mlx.models.mllm.MLLMOutput.finish_reason class-attribute instance-attribute

finish_reason: str | None = None

vllm_mlx.models.mllm.MLLMOutput.prompt_tokens class-attribute instance-attribute

prompt_tokens: int = 0

vllm_mlx.models.mllm.MLLMOutput.completion_tokens class-attribute instance-attribute

completion_tokens: int = 0

vllm_mlx.models.mllm.MLLMOutput.mtp_drafts class-attribute instance-attribute

mtp_drafts: int = 0

vllm_mlx.models.mllm.MLLMOutput.mtp_accepted class-attribute instance-attribute

mtp_accepted: int = 0

vllm_mlx.models.mllm.MLXMultimodalLM

MLXMultimodalLM(model_name: str, trust_remote_code: bool = False, enable_cache: bool = True, cache_size: int = 50, max_kv_size: int = 0, draft_model: str | None = None, draft_kind: str | None = None, draft_block_size: int | None = None)

Wrapper around mlx-vlm for multimodal inference.

This class provides a unified interface for multimodal language models using Apple's MLX framework. Supports: - Image understanding (single and multi-image) - Video understanding (smart frame extraction) - Audio understanding (for supported models) - OpenAI-compatible API format

Supported models include: - Qwen2-VL / Qwen2.5-VL / Qwen3-VL - LLaVA - Idefics3 - PaliGemma - And more via mlx-vlm

Example

model = MLXMultimodalLM("mlx-community/Qwen2-VL-2B-Instruct-4bit") model.load() output = model.generate( ... prompt="What's in this image?", ... images=["photo.jpg"] ... ) print(output.text)

Initialize the MLX multimodal language model.

Parameters:

  • model_name (str) –

    HuggingFace model name or local path

  • trust_remote_code (bool, default: False ) –

    Whether to trust remote code

  • enable_cache (bool, default: True ) –

    Enable KV cache for repeated image/video+prompt (default: True)

  • cache_size (int, default: 50 ) –

    Maximum cache entries (default: 50)

  • max_kv_size (int, default: 0 ) –

    Maximum KV cache size per sequence (0 = unbounded)

  • draft_model (str | None, default: None ) –

    Optional MLLM speculative draft/assistant model path.

  • draft_kind (str | None, default: None ) –

    Optional mlx-vlm draft kind, for example "mtp".

  • draft_block_size (int | None, default: None ) –

    Optional speculative block size passed to mlx-vlm.

Source code in vllm_mlx/models/mllm.py
def __init__(
    self,
    model_name: str,
    trust_remote_code: bool = False,
    enable_cache: bool = True,
    cache_size: int = 50,
    max_kv_size: int = 0,
    draft_model: str | None = None,
    draft_kind: str | None = None,
    draft_block_size: int | None = None,
):
    """
    Initialize the MLX multimodal language model.

    Args:
        model_name: HuggingFace model name or local path
        trust_remote_code: Whether to trust remote code
        enable_cache: Enable KV cache for repeated image/video+prompt (default: True)
        cache_size: Maximum cache entries (default: 50)
        max_kv_size: Maximum KV cache size per sequence (0 = unbounded)
        draft_model: Optional MLLM speculative draft/assistant model path.
        draft_kind: Optional mlx-vlm draft kind, for example "mtp".
        draft_block_size: Optional speculative block size passed to mlx-vlm.
    """
    self.model_name = model_name
    self.trust_remote_code = trust_remote_code
    self.enable_cache = enable_cache
    self.max_kv_size = max_kv_size
    self.draft_model_path = draft_model
    self.draft_kind = draft_kind
    self.draft_block_size = draft_block_size

    self.model = None
    self.processor = None
    self.config = None
    self._draft_model = None
    self._loaded = False
    self._video_native = False
    self._video_native_with_audio = False

    # Initialize MLLM prefix cache manager (with vision embedding caching)
    self._cache_manager: MLLMPrefixCacheManager | None = None
    if enable_cache:
        self._cache_manager = MLLMPrefixCacheManager(max_entries=cache_size)

vllm_mlx.models.mllm.MLXMultimodalLM.model_name instance-attribute

model_name = model_name

vllm_mlx.models.mllm.MLXMultimodalLM.trust_remote_code instance-attribute

trust_remote_code = trust_remote_code

vllm_mlx.models.mllm.MLXMultimodalLM.enable_cache instance-attribute

enable_cache = enable_cache

vllm_mlx.models.mllm.MLXMultimodalLM.max_kv_size instance-attribute

max_kv_size = max_kv_size

vllm_mlx.models.mllm.MLXMultimodalLM.draft_model_path instance-attribute

draft_model_path = draft_model

vllm_mlx.models.mllm.MLXMultimodalLM.draft_kind instance-attribute

draft_kind = draft_kind

vllm_mlx.models.mllm.MLXMultimodalLM.draft_block_size instance-attribute

draft_block_size = draft_block_size

vllm_mlx.models.mllm.MLXMultimodalLM.model instance-attribute

model = None

vllm_mlx.models.mllm.MLXMultimodalLM.processor instance-attribute

processor = None

vllm_mlx.models.mllm.MLXMultimodalLM.config instance-attribute

config = None

vllm_mlx.models.mllm.MLXMultimodalLM._draft_model instance-attribute

_draft_model = None

vllm_mlx.models.mllm.MLXMultimodalLM._loaded instance-attribute

_loaded = False

vllm_mlx.models.mllm.MLXMultimodalLM._video_native instance-attribute

_video_native = False

vllm_mlx.models.mllm.MLXMultimodalLM._video_native_with_audio instance-attribute

_video_native_with_audio = False

vllm_mlx.models.mllm.MLXMultimodalLM._cache_manager instance-attribute

_cache_manager: MLLMPrefixCacheManager | None = None

vllm_mlx.models.mllm.MLXMultimodalLM.load

load() -> None

Load the model and processor.

Source code in vllm_mlx/models/mllm.py
def load(self) -> None:
    """Load the model and processor."""
    if self._loaded:
        return

    try:
        from mlx_vlm import load
        from mlx_vlm.utils import load_config

        logger.info(f"Loading MLLM: {self.model_name}")

        self.model, self.processor = load(self.model_name)
        self.config = load_config(self.model_name)
        if self.draft_model_path:
            self._draft_model = self._load_draft_model()
            _install_draft_metrics_hooks(self._draft_model)

        self._loaded = True
        self._video_native = hasattr(
            self.model.config, "video_token_id"
        ) or hasattr(self.model.config, "video_token_index")
        # Omni models expose a sound_encoder; for these, a video_url
        # without a paired audio_url should auto-extract the video's
        # audio track so the model can fuse A/V in one forward pass.
        # Decoupled from _video_native because some omni models (e.g.
        # Nemotron-H Omni) don't expose video_token_id at config level
        # and run through the frames-as-images fallback path.
        self._video_native_with_audio = _model_has_sound_encoder(self.model)
        logger.info(f"MLLM loaded successfully: {self.model_name}")
        if self._video_native:
            logger.info("Native video pipeline enabled (temporal 3D conv + M-RoPE)")
        if self._video_native_with_audio:
            logger.info(
                "Omni model detected: video_url will auto-extract audio for A/V fusion"
            )

    except ImportError:
        raise ImportError(
            "mlx-vlm is required for multimodal inference. "
            "Install with: pip install mlx-vlm"
        )
    except Exception as e:
        logger.error(f"Failed to load MLLM: {e}")
        raise

vllm_mlx.models.mllm.MLXMultimodalLM._load_draft_model

_load_draft_model()
Source code in vllm_mlx/models/mllm.py
def _load_draft_model(self):
    if self.draft_kind == "mtp":
        return load_gemma4_assistant_drafter(self.draft_model_path)

    from mlx_vlm.utils import load

    draft_model, _ = load(self.draft_model_path)
    return draft_model

vllm_mlx.models.mllm.MLXMultimodalLM._draft_generation_kwargs

_draft_generation_kwargs(call_kwargs: dict | None = None) -> dict

Return mlx-vlm drafter kwargs when the request explicitly opts in.

call_kwargs is the outbound mlx-vlm kwargs dict. This method removes vllm-mlx drafter control keys before the dict is forwarded so caller passthrough values cannot conflict with the configured server drafter.

Source code in vllm_mlx/models/mllm.py
def _draft_generation_kwargs(self, call_kwargs: dict | None = None) -> dict:
    """Return mlx-vlm drafter kwargs when the request explicitly opts in.

    ``call_kwargs`` is the outbound mlx-vlm kwargs dict. This method removes
    vllm-mlx drafter control keys before the dict is forwarded so caller
    passthrough values cannot conflict with the configured server drafter.
    """
    draft_requested = False
    if call_kwargs is not None:
        draft_requested = bool(call_kwargs.pop("mllm_draft", False))
        for key in _DRAFT_KWARG_NAMES:
            call_kwargs.pop(key, None)
    if not draft_requested or self._draft_model is None:
        return {}
    # Tests may install the draft model after load(); the hook is idempotent.
    _install_draft_metrics_hooks(self._draft_model)
    kwargs = {"draft_model": self._draft_model}
    if self.draft_kind:
        kwargs["draft_kind"] = self.draft_kind
    if self.draft_block_size is not None:
        kwargs["draft_block_size"] = self.draft_block_size
    return kwargs

vllm_mlx.models.mllm.MLXMultimodalLM._reset_draft_metrics

_reset_draft_metrics() -> int
Source code in vllm_mlx/models/mllm.py
def _reset_draft_metrics(self) -> int:
    if self._draft_model is None:
        return 0
    if hasattr(self._draft_model, "accept_lens"):
        self._draft_model.accept_lens = []
    if hasattr(self._draft_model, "_vllm_mlx_draft_counts"):
        self._draft_model._vllm_mlx_draft_counts = []
    return 0

vllm_mlx.models.mllm.MLXMultimodalLM._draft_metrics_since

_draft_metrics_since(start_accept_lens: int) -> dict[str, int]
Source code in vllm_mlx/models/mllm.py
def _draft_metrics_since(self, start_accept_lens: int) -> dict[str, int]:
    if self._draft_model is None:
        return {"mtp_drafts": 0, "mtp_accepted": 0}
    accept_lens = list(getattr(self._draft_model, "accept_lens", []))
    if start_accept_lens > len(accept_lens):
        new_accept_lens = accept_lens
    else:
        new_accept_lens = accept_lens[start_accept_lens:]
    draft_counts = list(getattr(self._draft_model, "_vllm_mlx_draft_counts", []))
    if start_accept_lens > len(draft_counts):
        new_draft_counts = draft_counts
    else:
        new_draft_counts = draft_counts[start_accept_lens:]
    block_size = (
        int(self.draft_block_size)
        if self.draft_block_size is not None
        else int(
            getattr(getattr(self._draft_model, "config", None), "block_size", 0)
        )
    )
    drafted_per_round = max(block_size - 1, 0)
    mtp_drafts = (
        sum(max(int(value), 0) for value in new_draft_counts)
        if new_draft_counts
        else drafted_per_round * len(new_accept_lens)
    )
    return {
        "mtp_drafts": mtp_drafts,
        "mtp_accepted": sum(int(value) for value in new_accept_lens),
    }

vllm_mlx.models.mllm.MLXMultimodalLM.get_language_model

get_language_model()

Extract the underlying language model for mlx_lm TextModel construction.

Source code in vllm_mlx/models/mllm.py
def get_language_model(self):
    """Extract the underlying language model for mlx_lm TextModel construction."""
    return self.model.language_model

vllm_mlx.models.mllm.MLXMultimodalLM.get_tokenizer

get_tokenizer()

Get the text tokenizer (not the multimodal processor).

Source code in vllm_mlx/models/mllm.py
def get_tokenizer(self):
    """Get the text tokenizer (not the multimodal processor)."""
    return self.processor.tokenizer

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_images

_prepare_images(images: list) -> list[str]

Process remote/base64 image inputs into local temp file paths.

Source code in vllm_mlx/models/mllm.py
def _prepare_images(self, images: list) -> list[str]:
    """Process remote/base64 image inputs into local temp file paths."""
    processed = []
    for img in images:
        try:
            path = process_image_input(img)
            processed.append(path)
        except Exception as e:
            logger.warning(f"Failed to process image: {e}")
    return processed

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_audio

_prepare_audio(audio_inputs: list) -> list[str]

Process audio inputs and return local file paths.

Source code in vllm_mlx/models/mllm.py
def _prepare_audio(self, audio_inputs: list) -> list[str]:
    """Process audio inputs and return local file paths."""
    processed = []
    for audio_input in audio_inputs:
        try:
            path = process_audio_input(audio_input)
            processed.append(path)
        except Exception as e:
            logger.warning(f"Failed to process audio: {e}")
    return processed

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_video

_prepare_video(video_input: str | dict, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resolved_path: str | None = None) -> list[str]

Process video input and extract frames.

Supports: - URLs (http/https) - will be downloaded - Base64 encoded videos (data:video/mp4;base64,...) - OpenAI format dicts: {"url": "..."} or {"video_url": {"url": "..."}}

Parameters:

  • video_input (str | dict) –

    Video in any supported format

  • fps (float, default: DEFAULT_FPS ) –

    Frames per second to extract

  • max_frames (int, default: MAX_FRAMES ) –

    Maximum frames to extract

  • resolved_path (str | None, default: None ) –

    Optional pre-resolved local path. Callers that already ran process_video_input (e.g. for parallel audio extraction) pass it here to avoid re-downloading / re-decoding.

Returns:

  • list[str]

    List of paths to extracted frame images

Source code in vllm_mlx/models/mllm.py
def _prepare_video(
    self,
    video_input: str | dict,
    fps: float = DEFAULT_FPS,
    max_frames: int = MAX_FRAMES,
    resolved_path: str | None = None,
) -> list[str]:
    """
    Process video input and extract frames.

    Supports:
    - URLs (http/https) - will be downloaded
    - Base64 encoded videos (data:video/mp4;base64,...)
    - OpenAI format dicts: {"url": "..."} or {"video_url": {"url": "..."}}

    Args:
        video_input: Video in any supported format
        fps: Frames per second to extract
        max_frames: Maximum frames to extract
        resolved_path: Optional pre-resolved local path. Callers that
            already ran process_video_input (e.g. for parallel audio
            extraction) pass it here to avoid re-downloading / re-decoding.

    Returns:
        List of paths to extracted frame images
    """
    # Reuse caller's resolved path when supplied; otherwise resolve here
    # (downloads if URL, decodes if base64).
    video_path = resolved_path or process_video_input(video_input)

    # Extract frames
    frames = extract_video_frames_smart(
        video_path,
        fps=fps,
        max_frames=max_frames,
    )
    return save_frames_to_temp(frames)

vllm_mlx.models.mllm.MLXMultimodalLM._collect_video_inputs

_collect_video_inputs(messages: list[dict]) -> dict[int, list]

Collect video inputs from messages, keyed by message index.

Handles both 'video' and 'video_url' content types, including Pydantic model conversion.

Source code in vllm_mlx/models/mllm.py
def _collect_video_inputs(self, messages: list[dict]) -> dict[int, list]:
    """Collect video inputs from messages, keyed by message index.

    Handles both 'video' and 'video_url' content types, including
    Pydantic model conversion.
    """
    video_inputs: dict[int, list] = {}
    for msg_idx, msg in enumerate(messages):
        content = msg.get("content", "")
        if not isinstance(content, list):
            continue
        for item in content:
            if hasattr(item, "model_dump"):
                item = item.model_dump(exclude_none=True)
            elif hasattr(item, "dict"):
                item = {k: v for k, v in item.dict().items() if v is not None}

            if not isinstance(item, dict):
                continue
            item_type = item.get("type", "")
            if item_type == "video":
                video_inputs.setdefault(msg_idx, []).append(
                    item.get("video", item.get("url", ""))
                )
            elif item_type == "video_url":
                vid_url = item.get("video_url", {})
                if isinstance(vid_url, str):
                    video_inputs.setdefault(msg_idx, []).append(vid_url)
                elif isinstance(vid_url, dict):
                    url = vid_url.get("url", "")
                    if url:
                        video_inputs.setdefault(msg_idx, []).append(url)
    return video_inputs

vllm_mlx.models.mllm.MLXMultimodalLM._collect_audio_inputs

_collect_audio_inputs(messages: list[dict]) -> dict[int, list]

Collect audio inputs from messages, keyed by message index.

Source code in vllm_mlx/models/mllm.py
def _collect_audio_inputs(self, messages: list[dict]) -> dict[int, list]:
    """Collect audio inputs from messages, keyed by message index."""
    audio_inputs: dict[int, list] = {}
    for msg_idx, msg in enumerate(messages):
        content = msg.get("content", "")
        if not isinstance(content, list):
            continue
        for item in content:
            if hasattr(item, "model_dump"):
                item = item.model_dump(exclude_none=True)
            elif hasattr(item, "dict"):
                item = {k: v for k, v in item.dict().items() if v is not None}

            if not isinstance(item, dict):
                continue

            item_type = item.get("type", "")
            if item_type == "audio":
                audio_value = item.get("audio", item.get("url", ""))
                if audio_value:
                    audio_inputs.setdefault(msg_idx, []).append(audio_value)
            elif item_type == "audio_url":
                audio_url = item.get("audio_url", {})
                if isinstance(audio_url, str):
                    audio_inputs.setdefault(msg_idx, []).append(audio_url)
                elif isinstance(audio_url, dict):
                    url = audio_url.get("url", "")
                    if url:
                        audio_inputs.setdefault(msg_idx, []).append(url)
    return audio_inputs

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_native_video_inputs

_prepare_native_video_inputs(messages: list[dict], video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list | None = None) -> tuple[str, dict]

Preprocess messages into prompt + generation kwargs for native video.

Mirrors the preprocessing in mlx_vlm.video_generate.main() so that upstream improvements are easy to adopt. Returns the formatted prompt text and a dict of kwargs ready to pass to video_generate.generate().

Currently Qwen-family-specific (video_token_id / video_token_index).

Source code in vllm_mlx/models/mllm.py
def _prepare_native_video_inputs(
    self,
    messages: list[dict],
    video_fps: float = DEFAULT_FPS,
    video_max_frames: int = MAX_FRAMES,
    tools: list | None = None,
) -> tuple[str, dict]:
    """Preprocess messages into prompt + generation kwargs for native video.

    Mirrors the preprocessing in mlx_vlm.video_generate.main() so that
    upstream improvements are easy to adopt. Returns the formatted prompt
    text and a dict of kwargs ready to pass to video_generate.generate().

    Currently Qwen-family-specific (video_token_id / video_token_index).
    """
    import mlx.core as mx

    try:
        from mlx_vlm.video_generate import process_vision_info
    except ImportError:
        raise ImportError(
            "mlx_vlm.video_generate is required for native video support. "
            "Upgrade with: pip install --upgrade mlx-vlm"
        )

    # Translate OpenAI API messages into process_vision_info format
    native_messages = self._translate_messages_for_native_video(
        messages, video_fps, video_max_frames
    )

    # Use HF processor's chat template (handles timestamp interleaving)
    template_kwargs: dict = {}
    if tools:
        template_kwargs["tools"] = tools

    text = self.processor.apply_chat_template(
        native_messages,
        tokenize=False,
        add_generation_prompt=True,
        **template_kwargs,
    )

    # Extract vision inputs via mlx-vlm's process_vision_info
    image_inputs, video_inputs, fps_info = process_vision_info(
        native_messages, return_video_kwargs=True
    )

    # Collect audio paths emitted by the translation step
    # (explicit audio_url, or auto-extracted from video_url for omni
    # models).
    audio_inputs: list[str] = []
    for nmsg in native_messages:
        ncontent = nmsg.get("content", [])
        if not isinstance(ncontent, list):
            continue
        for nitem in ncontent:
            if isinstance(nitem, dict) and nitem.get("type") == "audio":
                apath = nitem.get("audio")
                if apath:
                    audio_inputs.append(apath)

    # Process through HF processor to get input_ids, pixel_values, grid_thw
    # and (for omni models) sound_clips / input_features.
    processor_kwargs: dict = {
        "text": [text],
        "images": image_inputs,
        "videos": video_inputs,
        "padding": True,
        "return_tensors": "pt",
    }
    if audio_inputs:
        processor_kwargs["audio"] = audio_inputs
    inputs = self.processor(**processor_kwargs)

    input_ids = mx.array(inputs["input_ids"])
    pixel_values = inputs.get(
        "pixel_values_videos", inputs.get("pixel_values", None)
    )
    if pixel_values is not None:
        pixel_values = mx.array(pixel_values)
    mask = mx.array(inputs["attention_mask"])

    gen_kwargs: dict = {}
    if inputs.get("video_grid_thw", None) is not None:
        gen_kwargs["video_grid_thw"] = mx.array(inputs["video_grid_thw"])
    if inputs.get("image_grid_thw", None) is not None:
        gen_kwargs["image_grid_thw"] = mx.array(inputs["image_grid_thw"])

    # Forward audio embeddings/clips from the processor so the omni
    # model's sound encoder gets fed alongside the visual stream.
    for audio_key in (
        "sound_clips",
        "input_features",
        "feature_attention_mask",
        "audio_feature_lengths",
        "sound_feature_lengths",
        "sound_attention_mask",
    ):
        val = inputs.get(audio_key, None)
        if val is not None:
            gen_kwargs[audio_key] = val
    if audio_inputs:
        logger.info(
            f"Native video: forwarding audio ({len(audio_inputs)} clip(s)) "
            f"to omni model via "
            f"{[k for k in gen_kwargs if k in ('sound_clips', 'input_features')]}"
        )

    gen_kwargs["input_ids"] = input_ids
    gen_kwargs["pixel_values"] = pixel_values
    gen_kwargs["mask"] = mask

    grid_thw_info = gen_kwargs.get("video_grid_thw")
    logger.info(
        f"Native video: {input_ids.size} input tokens, "
        f"video_grid_thw={grid_thw_info.tolist() if grid_thw_info is not None else None}"
    )

    return text, gen_kwargs

vllm_mlx.models.mllm.MLXMultimodalLM._generate_native_video

_generate_native_video(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list | None = None, **kwargs) -> MLLMOutput

Generate using native video pipeline (Qwen-family models).

Delegates preprocessing to _prepare_native_video_inputs and generation to mlx_vlm.video_generate.generate(), keeping our code aligned with upstream's video pipeline so improvements are easy to adopt.

Source code in vllm_mlx/models/mllm.py
def _generate_native_video(
    self,
    messages: list[dict],
    max_tokens: int = 256,
    temperature: float = 0.7,
    video_fps: float = DEFAULT_FPS,
    video_max_frames: int = MAX_FRAMES,
    tools: list | None = None,
    **kwargs,
) -> MLLMOutput:
    """Generate using native video pipeline (Qwen-family models).

    Delegates preprocessing to _prepare_native_video_inputs and generation
    to mlx_vlm.video_generate.generate(), keeping our code aligned with
    upstream's video pipeline so improvements are easy to adopt.
    """
    try:
        from mlx_vlm.video_generate import generate
    except ImportError:
        raise ImportError(
            "mlx_vlm.video_generate is required for native video support. "
            "Upgrade with: pip install --upgrade mlx-vlm"
        )

    text, gen_kwargs = self._prepare_native_video_inputs(
        messages, video_fps, video_max_frames, tools
    )
    gen_kwargs["temperature"] = temperature

    result = generate(
        self.model,
        self.processor,
        prompt=text,
        max_tokens=max_tokens,
        verbose=False,
        **gen_kwargs,
    )

    if hasattr(result, "text"):
        return MLLMOutput(
            text=result.text,
            finish_reason="stop",
            prompt_tokens=getattr(result, "prompt_tokens", 0),
            completion_tokens=getattr(result, "generation_tokens", 0),
        )
    return MLLMOutput(text=str(result), finish_reason="stop")

vllm_mlx.models.mllm.MLXMultimodalLM._translate_messages_for_native_video

_translate_messages_for_native_video(messages: list[dict], video_fps: float, video_max_frames: int) -> list[dict]

Translate OpenAI API format messages to process_vision_info format.

Converts video_url/video types and resolves remote/base64 inputs to local paths. Images are preserved as-is (process_vision_info handles them).

Source code in vllm_mlx/models/mllm.py
def _translate_messages_for_native_video(
    self,
    messages: list[dict],
    video_fps: float,
    video_max_frames: int,
) -> list[dict]:
    """Translate OpenAI API format messages to process_vision_info format.

    Converts video_url/video types and resolves remote/base64 inputs to local paths.
    Images are preserved as-is (process_vision_info handles them).
    """
    translated = []
    for msg in messages:
        role = msg.get("role", "user")
        content = msg.get("content", "")

        if isinstance(content, str):
            translated.append({"role": role, "content": content})
            continue

        if not isinstance(content, list):
            translated.append({"role": role, "content": str(content)})
            continue

        # Pre-pass: does this message have an explicit audio_url/audio
        # block? If so, we skip auto-extracting audio from a video_url to
        # honor the caller's explicit choice.
        has_explicit_audio = False
        for item in content:
            if hasattr(item, "model_dump"):
                probe = item.model_dump(exclude_none=True)
            elif hasattr(item, "dict"):
                probe = {k: v for k, v in item.dict().items() if v is not None}
            else:
                probe = item
            if isinstance(probe, dict) and probe.get("type", "") in (
                "audio",
                "audio_url",
            ):
                has_explicit_audio = True
                break

        new_content = []
        for item in content:
            if hasattr(item, "model_dump"):
                item = item.model_dump(exclude_none=True)
            elif hasattr(item, "dict"):
                item = {k: v for k, v in item.dict().items() if v is not None}

            if not isinstance(item, dict):
                new_content.append({"type": "text", "text": str(item)})
                continue

            item_type = item.get("type", "")

            if item_type == "text":
                new_content.append(item)

            elif item_type == "image_url":
                img_url = item.get("image_url", {})
                url = (
                    img_url.get("url", img_url)
                    if isinstance(img_url, dict)
                    else img_url
                )
                # Resolve to local path for process_vision_info
                local_path = process_image_input(url)
                new_content.append({"type": "image", "image": local_path})

            elif item_type == "image":
                img = item.get("image", item.get("url", ""))
                local_path = process_image_input(img)
                new_content.append({"type": "image", "image": local_path})

            elif item_type in ("video", "video_url"):
                # Extract video path/URL from various formats
                if item_type == "video_url":
                    vid_url = item.get("video_url", {})
                    if isinstance(vid_url, str):
                        video_source = vid_url
                    elif isinstance(vid_url, dict):
                        video_source = vid_url.get("url", "")
                    else:
                        continue
                else:
                    video_source = item.get("video", item.get("url", ""))

                if not video_source:
                    continue

                # Resolve to local path
                video_path = process_video_input(video_source)
                new_content.append(
                    {
                        "type": "video",
                        "video": video_path,
                        "fps": video_fps,
                        "max_frames": video_max_frames,
                    }
                )
                # For omni-capable models, pull the video's audio track
                # alongside frames so the model can fuse A/V in one
                # forward pass. We extract from the already-resolved local
                # path (no raw user URL handed to ffmpeg → avoids URL-
                # protocol SSRF via ffmpeg's network demuxers).
                if not has_explicit_audio and getattr(
                    self, "_video_native_with_audio", False
                ):
                    extracted = extract_audio_from_video(video_path)
                    if extracted is not None:
                        new_content.append({"type": "audio", "audio": extracted})

            elif item_type in ("audio", "audio_url"):
                if item_type == "audio_url":
                    aud_url = item.get("audio_url", {})
                    if isinstance(aud_url, str):
                        audio_source = aud_url
                    elif isinstance(aud_url, dict):
                        audio_source = aud_url.get("url", "")
                    else:
                        continue
                else:
                    audio_source = item.get("audio", item.get("url", ""))

                if not audio_source:
                    continue

                audio_path = process_audio_input(audio_source)
                new_content.append({"type": "audio", "audio": audio_path})

            else:
                new_content.append(item)

        translated.append({"role": role, "content": new_content})

    return translated

vllm_mlx.models.mllm.MLXMultimodalLM.generate

generate(prompt: str, images: list | None = None, videos: list | None = None, audio: list[str] | None = None, max_tokens: int = 256, temperature: float = 0.7, top_p: float = 0.9, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, use_cache: bool = True, **kwargs) -> MLLMOutput

Generate text from multimodal input.

Parameters:

  • prompt (str) –

    Text prompt/question

  • images (list | None, default: None ) –

    List of image URLs or base64 strings

  • videos (list | None, default: None ) –

    List of video inputs (URLs, base64, or OpenAI format dicts)

  • audio (list[str] | None, default: None ) –

    List of audio file paths

  • max_tokens (int, default: 256 ) –

    Maximum tokens to generate

  • temperature (float, default: 0.7 ) –

    Sampling temperature

  • top_p (float, default: 0.9 ) –

    Top-p sampling parameter

  • video_fps (float, default: DEFAULT_FPS ) –

    FPS for video frame extraction (default: 2.0)

  • video_max_frames (int, default: MAX_FRAMES ) –

    Max frames to extract from video

  • use_cache (bool, default: True ) –

    Whether to use KV cache (default: True)

  • **kwargs

    Additional generation parameters

Returns:

Example
With local video

output = model.generate("Describe this video", videos=["video.mp4"])

With video URL

output = model.generate("What happens?", videos=["https://example.com/video.mp4"])

With base64 video

output = model.generate("Describe", videos=["data:video/mp4;base64,AAAA..."])

Source code in vllm_mlx/models/mllm.py
def generate(
    self,
    prompt: str,
    images: list | None = None,
    videos: list | None = None,
    audio: list[str] | None = None,
    max_tokens: int = 256,
    temperature: float = 0.7,
    top_p: float = 0.9,
    video_fps: float = DEFAULT_FPS,
    video_max_frames: int = MAX_FRAMES,
    use_cache: bool = True,
    **kwargs,
) -> MLLMOutput:
    """
    Generate text from multimodal input.

    Args:
        prompt: Text prompt/question
        images: List of image URLs or base64 strings
        videos: List of video inputs (URLs, base64, or OpenAI format dicts)
        audio: List of audio file paths
        max_tokens: Maximum tokens to generate
        temperature: Sampling temperature
        top_p: Top-p sampling parameter
        video_fps: FPS for video frame extraction (default: 2.0)
        video_max_frames: Max frames to extract from video
        use_cache: Whether to use KV cache (default: True)
        **kwargs: Additional generation parameters

    Returns:
        MLLMOutput with generated text

    Example:
        # With local video
        output = model.generate("Describe this video", videos=["video.mp4"])

        # With video URL
        output = model.generate("What happens?", videos=["https://example.com/video.mp4"])

        # With base64 video
        output = model.generate("Describe", videos=["data:video/mp4;base64,AAAA..."])
    """
    if not self._loaded:
        self.load()

    from mlx_vlm import generate
    from mlx_vlm.models import cache as vlm_cache
    from mlx_vlm.prompt_utils import apply_chat_template

    images = images or []
    videos = videos or []
    audio = audio or []

    # Process all images (including frames from videos) and audio inputs
    all_images = []
    all_audio = []
    all_sources = []  # Track original sources for cache key

    # Process image inputs
    if images:
        all_images.extend(self._prepare_images(images))
        all_sources.extend(images)

    # Extract frames from videos
    for video_path in videos:
        frames = self._prepare_video(
            video_path,
            fps=video_fps,
            max_frames=video_max_frames,
        )
        all_images.extend(frames)
        # Include video params in cache key
        video_str = video_path if isinstance(video_path, str) else str(video_path)
        all_sources.append(
            f"video:{video_str}:fps{video_fps}:max{video_max_frames}"
        )
        logger.info(f"Added {len(frames)} frames from video: {video_path}")

    if audio:
        all_audio.extend(self._prepare_audio(audio))

    # Apply chat template if needed
    if (all_images or all_audio) and hasattr(self.processor, "apply_chat_template"):
        try:
            formatted_prompt = apply_chat_template(
                self.processor,
                self.config,
                prompt,
                num_images=len(all_images),
                num_audios=len(all_audio),
            )
        except Exception:
            formatted_prompt = prompt
    else:
        formatted_prompt = prompt

    # Check cache for existing KV state
    prompt_cache = None
    cache_hit = False

    if use_cache and all_audio:
        logger.info("MLLM cache disabled for audio inputs")
        use_cache = False

    if use_cache and self._cache_manager is not None and all_sources:
        prompt_cache, cache_hit = self._cache_manager.fetch_cache(
            all_sources, formatted_prompt
        )
        if cache_hit:
            logger.info(f"MLLM cache hit for {len(all_sources)} source(s)")

    # Create new cache if needed
    if prompt_cache is None and self.model is not None:
        try:
            prompt_cache = vlm_cache.make_prompt_cache(
                self.model.language_model,
                max_kv_size=self.max_kv_size or None,
            )
        except Exception:
            prompt_cache = None

    # Generate with cache
    draft_accept_start = self._reset_draft_metrics()
    result = generate(
        self.model,
        self.processor,
        formatted_prompt,
        all_images if all_images else None,
        audio=all_audio if all_audio else None,
        max_tokens=max_tokens,
        temp=temperature,
        top_p=top_p,
        verbose=False,
        prompt_cache=prompt_cache,
        **self._draft_generation_kwargs(kwargs),
        **kwargs,
    )
    draft_metrics = self._draft_metrics_since(draft_accept_start)

    # Store cache for future reuse (only on miss)
    if use_cache and self._cache_manager and all_sources and not cache_hit:
        if prompt_cache is not None:
            try:
                num_tokens = getattr(result, "prompt_tokens", 0)
                self._cache_manager.store_cache(
                    all_sources, formatted_prompt, prompt_cache, num_tokens
                )
                logger.info(f"MLLM cache stored for {len(all_sources)} source(s)")
            except Exception as e:
                logger.debug(f"Failed to store MLLM cache: {e}")

    # Handle GenerationResult object or plain string
    if hasattr(result, "text"):
        output_text = result.text
        prompt_tokens = getattr(result, "prompt_tokens", 0)
        generation_tokens = getattr(result, "generation_tokens", 0)
    else:
        output_text = str(result)
        prompt_tokens = 0
        generation_tokens = 0

    return MLLMOutput(
        text=output_text,
        finish_reason="stop",
        prompt_tokens=prompt_tokens,
        completion_tokens=generation_tokens,
        **draft_metrics,
    )

vllm_mlx.models.mllm.MLXMultimodalLM.stream_generate

stream_generate(prompt: str, images: list | None = None, videos: list[str] | None = None, audio: list[str] | None = None, max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, **kwargs) -> Iterator[str]

Stream text generation for multimodal input.

Parameters:

  • prompt (str) –

    Text prompt

  • images (list | None, default: None ) –

    List of image inputs

  • videos (list[str] | None, default: None ) –

    List of video paths

  • audio (list[str] | None, default: None ) –

    List of audio inputs

  • max_tokens (int, default: 256 ) –

    Maximum tokens to generate

  • temperature (float, default: 0.7 ) –

    Sampling temperature

  • video_fps (float, default: DEFAULT_FPS ) –

    FPS for video frame extraction

  • **kwargs

    Additional parameters

Yields:

  • str

    Generated text chunks

Source code in vllm_mlx/models/mllm.py
def stream_generate(
    self,
    prompt: str,
    images: list | None = None,
    videos: list[str] | None = None,
    audio: list[str] | None = None,
    max_tokens: int = 256,
    temperature: float = 0.7,
    video_fps: float = DEFAULT_FPS,
    **kwargs,
) -> Iterator[str]:
    """
    Stream text generation for multimodal input.

    Args:
        prompt: Text prompt
        images: List of image inputs
        videos: List of video paths
        audio: List of audio inputs
        max_tokens: Maximum tokens to generate
        temperature: Sampling temperature
        video_fps: FPS for video frame extraction
        **kwargs: Additional parameters

    Yields:
        Generated text chunks
    """
    if not self._loaded:
        self.load()

    try:
        from mlx_vlm import stream_generate
        from mlx_vlm.prompt_utils import apply_chat_template
    except ImportError:
        # Fallback to non-streaming
        output = self.generate(
            prompt=prompt,
            images=images,
            videos=videos,
            audio=audio,
            max_tokens=max_tokens,
            temperature=temperature,
            video_fps=video_fps,
            **kwargs,
        )
        yield output.text
        return

    images = images or []
    videos = videos or []
    audio = audio or []

    # Process images
    all_images = []
    all_audio = []
    if images:
        all_images.extend(self._prepare_images(images))
    for video_path in videos:
        frames = self._prepare_video(video_path, fps=video_fps)
        all_images.extend(frames)
    if audio:
        all_audio.extend(self._prepare_audio(audio))

    # Apply chat template
    if all_images or all_audio:
        try:
            formatted_prompt = apply_chat_template(
                self.processor,
                self.config,
                prompt,
                num_images=len(all_images),
                num_audios=len(all_audio),
            )
        except Exception:
            formatted_prompt = prompt
    else:
        formatted_prompt = prompt

    for chunk in stream_generate(
        self.model,
        self.processor,
        formatted_prompt,
        all_images if all_images else None,
        audio=all_audio if all_audio else None,
        max_tokens=max_tokens,
        temp=temperature,
        **self._draft_generation_kwargs(kwargs),
        **kwargs,
    ):
        yield chunk

vllm_mlx.models.mllm.MLXMultimodalLM.chat

chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> MLLMOutput

Chat with OpenAI-compatible message format.

Supports multimodal content in messages: - {"type": "text", "text": "..."} - {"type": "image_url", "image_url": {"url": "..."}} - {"type": "image_url", "image_url": {"url": "data:image/...;base64,..."}}

Parameters:

  • messages (list[dict]) –

    List of chat messages (OpenAI format)

  • max_tokens (int, default: 256 ) –

    Maximum tokens to generate

  • temperature (float, default: 0.7 ) –

    Sampling temperature

  • **kwargs

    Additional parameters

Returns:

  • MLLMOutput

    MLLMOutput with assistant's response

Source code in vllm_mlx/models/mllm.py
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
def chat(
    self,
    messages: list[dict],
    max_tokens: int = 256,
    temperature: float = 0.7,
    **kwargs,
) -> MLLMOutput:
    """
    Chat with OpenAI-compatible message format.

    Supports multimodal content in messages:
    - {"type": "text", "text": "..."}
    - {"type": "image_url", "image_url": {"url": "..."}}
    - {"type": "image_url", "image_url": {"url": "data:image/...;base64,..."}}

    Args:
        messages: List of chat messages (OpenAI format)
        max_tokens: Maximum tokens to generate
        temperature: Sampling temperature
        **kwargs: Additional parameters

    Returns:
        MLLMOutput with assistant's response
    """
    if not self._loaded:
        self.load()

    from mlx_vlm import generate
    from mlx_vlm.prompt_utils import get_chat_template

    # Extract text, images and audio from messages
    # Build chat_messages for multi-turn support WITH proper image/audio tokens per message
    all_image_urls = []  # Raw URLs/paths to process later
    chat_messages = []  # List of properly formatted messages for chat template

    logger.info(f"MLLM.chat() called with {len(messages)} messages")

    # Pop params early so they don't leak into mlx_vlm.generate()
    video_fps = kwargs.pop("video_fps", DEFAULT_FPS)
    video_max_frames = kwargs.pop("video_max_frames", MAX_FRAMES)
    tools = kwargs.pop("tools", None)
    use_cache = kwargs.pop("use_cache", True)
    enable_thinking = kwargs.pop("enable_thinking", True)
    # Honor chat_template_kwargs on the MLLM path (parity with the text path in
    # llm.py). enable_thinking is commonly nested here via
    # --default-chat-template-kwargs {"enable_thinking": false}; it was previously
    # dropped for MLLM models, so reasoning could not be disabled and leaked into
    # the response.
    chat_template_kwargs = kwargs.pop("chat_template_kwargs", None) or {}
    if "enable_thinking" in chat_template_kwargs:
        enable_thinking = chat_template_kwargs.pop("enable_thinking")

    # Collect video and audio inputs from messages
    _msg_video_inputs = self._collect_video_inputs(messages)
    _msg_audio_inputs = self._collect_audio_inputs(messages)

    # Use native video pipeline for supported models
    if self._video_native and _msg_video_inputs:
        return self._generate_native_video(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            video_fps=video_fps,
            video_max_frames=video_max_frames,
            tools=tools,
            **kwargs,
        )

    # Fallback: extract frames and treat as individual images
    _msg_video_frame_counts: dict[int, int] = {}
    _msg_extra_audio: dict[int, list[str]] = {}
    all_video_frames: list[str] = []
    all_audio_inputs: list[str] = []
    for msg_idx, vid_inputs in _msg_video_inputs.items():
        total_frames = 0
        has_explicit_audio = bool(_msg_audio_inputs.get(msg_idx))
        for vid_input in vid_inputs:
            # Resolve the video to a local path ONCE per input. Both
            # audio extraction (when this is an omni model with no
            # explicit audio block) and frame extraction need a local
            # file; resolving twice would re-download remote URLs and
            # re-decode base64. Resolving up front also keeps user-
            # supplied raw URLs out of ffmpeg's URL-protocol demuxers
            # (avoids SSRF via http://, rtsp://, etc.).
            try:
                resolved_video_path = process_video_input(vid_input)
            except Exception as exc:
                logger.warning(f"Could not resolve video: {exc}")
                resolved_video_path = None

            if (
                resolved_video_path
                and self._video_native_with_audio
                and not has_explicit_audio
            ):
                extracted_audio = extract_audio_from_video(resolved_video_path)
                if extracted_audio:
                    _msg_extra_audio.setdefault(msg_idx, []).append(extracted_audio)

            frames = self._prepare_video(
                vid_input,
                fps=video_fps,
                max_frames=video_max_frames,
                resolved_path=resolved_video_path,
            )
            all_video_frames.extend(frames)
            total_frames += len(frames)
            logger.info(f"Added {len(frames)} frames from video: {vid_input}")
        _msg_video_frame_counts[msg_idx] = total_frames

    # Merge auto-extracted audio into the per-message audio map so the
    # chat-template token-counting loop downstream sees the right count.
    for msg_idx, extra in _msg_extra_audio.items():
        _msg_audio_inputs.setdefault(msg_idx, []).extend(extra)

    for aud_inputs in _msg_audio_inputs.values():
        all_audio_inputs.extend(aud_inputs)

    chat_messages = _build_mllm_chat_messages(
        messages,
        all_image_urls=all_image_urls,
        video_frame_counts=_msg_video_frame_counts,
    )

    # Process images
    all_images = []
    if all_image_urls:
        all_images.extend(self._prepare_images(all_image_urls))
    # Append pre-processed video frames
    all_images.extend(all_video_frames)
    all_audio = self._prepare_audio(all_audio_inputs) if all_audio_inputs else []

    # Apply chat template directly - messages are already properly structured
    logger.info(
        f"Applying chat template with {len(chat_messages)} messages, {len(all_images)} images, {len(all_audio)} audios"
    )
    for i, cm in enumerate(chat_messages):
        content_preview = str(cm.get("content", ""))[:80]
        logger.info(
            f"  Chat msg {i}: role={cm['role']}, content={content_preview}..."
        )

    # Build template kwargs for tool definitions (tools already popped above)
    template_extra_kwargs = {}
    if tools:
        template_extra_kwargs["tools"] = tools
    # Forward any remaining chat_template_kwargs (parity with the text path)
    template_extra_kwargs.update(chat_template_kwargs)

    try:
        formatted_prompt = get_chat_template(
            self.processor,
            chat_messages,
            add_generation_prompt=True,
            enable_thinking=enable_thinking,
            **template_extra_kwargs,
        )
    except TypeError:
        # The processor's chat template doesn't accept some forwarded
        # chat_template_kwargs — drop them and retry (mirror llm.py).
        for key in chat_template_kwargs:
            template_extra_kwargs.pop(key, None)
        formatted_prompt = get_chat_template(
            self.processor,
            chat_messages,
            add_generation_prompt=True,
            enable_thinking=enable_thinking,
            **template_extra_kwargs,
        )
    except Exception as e:
        logger.warning(
            f"Failed to apply chat template: {e}, using last user message"
        )
        # Fallback to last user message if template fails
        last_user_msg = ""
        for m in reversed(chat_messages):
            if m["role"] == "user":
                content = m.get("content", "")
                if isinstance(content, list):
                    for item in content:
                        if isinstance(item, dict) and item.get("type") == "text":
                            last_user_msg = item.get("text", "")
                            break
                else:
                    last_user_msg = content
                break
        formatted_prompt = last_user_msg

    # Prefix caching with vision embedding support
    # Following LMCache approach: cache vision embeddings to skip encoder on hit
    import time

    from mlx_vlm.models import cache as vlm_cache

    cache_entry = None
    prefix_match_len = 0
    vision_embeddings = None
    cache_hit = False

    # Tokenize prompt for cache lookup
    tokenizer = (
        self.processor.tokenizer
        if hasattr(self.processor, "tokenizer")
        else self.processor
    )
    token_ids = tokenizer.encode(formatted_prompt)

    # Check prefix cache
    if use_cache and all_audio:
        logger.info("Prefix cache disabled for audio inputs")
        use_cache = False

    if use_cache and self._cache_manager is not None and all_images:
        try:
            cache_entry, prefix_match_len = self._cache_manager.fetch(
                all_images, formatted_prompt, token_ids
            )
            if cache_entry:
                cache_hit = True
                vision_embeddings = cache_entry.vision_embeddings
                if vision_embeddings is not None:
                    logger.info(
                        "[PREFIX CACHE] Vision embeddings cached - would skip encoder!"
                    )
                if prefix_match_len > 0:
                    logger.info(
                        f"[PREFIX CACHE] {prefix_match_len} prefix tokens match"
                    )
        except Exception as e:
            logger.warning(f"Cache fetch failed: {e}")

    # Generate - use KV cache if available from previous identical request
    start_time = time.time()

    # Create or reuse prompt cache for prefix caching speedup
    prompt_cache = None
    skip_prompt_processing = False

    if cache_hit and cache_entry and cache_entry.kv_cache:
        # NOTE: mlx-vlm's generate_step() has its own multimodal KV cache with prefix matching
        # (MULTIMODAL_KV_CACHE_ENABLED in mlx_vlm/utils.py). Let it handle caching.
        # We only use vllm-mlx's cache for text-only requests (no images).
        if all_images:
            # Let mlx-vlm's multimodal cache handle this - don't interfere
            logger.info(
                "[PREFIX CACHE] Images present - delegating to mlx-vlm multimodal cache"
            )
            prompt_cache = None  # Fresh cache, mlx-vlm will handle prefix matching
            skip_prompt_processing = False
        else:
            # Text-only: can use skip_prompt_processing for maximum speedup
            logger.info(
                "[PREFIX CACHE] Text-only cache hit - using skip_prompt_processing speedup"
            )
            cached_prompt_cache = cache_entry.kv_cache
            try:
                import copy

                prompt_cache = []
                for layer_cache in cached_prompt_cache:
                    new_cache = copy.copy(layer_cache)
                    if hasattr(layer_cache, "state"):
                        state = layer_cache.state
                        if state is not None:
                            import mlx.core as mx

                            if len(state) >= 2 and state[0] is not None:
                                new_cache.keys = mx.array(state[0])
                                new_cache.values = mx.array(state[1])
                                if len(state) >= 3:
                                    new_cache.offset = state[2]
                                elif hasattr(layer_cache, "offset"):
                                    new_cache.offset = layer_cache.offset
                    prompt_cache.append(new_cache)
                skip_prompt_processing = True
                logger.info(
                    f"[PREFIX CACHE] Skipping {prefix_match_len} token forward pass"
                )
            except Exception as e:
                logger.warning(f"[PREFIX CACHE] Failed to copy cache: {e}")
                prompt_cache = None
                skip_prompt_processing = False

    if prompt_cache is None and self.model is not None:
        # Create fresh cache
        try:
            prompt_cache = vlm_cache.make_prompt_cache(
                self.model.language_model,
                max_kv_size=self.max_kv_size or None,
            )
        except Exception:
            prompt_cache = None

    draft_accept_start = self._reset_draft_metrics()
    result = generate(
        self.model,
        self.processor,
        formatted_prompt,
        all_images if all_images else None,
        audio=all_audio if all_audio else None,
        max_tokens=max_tokens,
        temp=temperature,
        verbose=False,
        prompt_cache=prompt_cache,
        skip_prompt_processing=skip_prompt_processing,
        **self._draft_generation_kwargs(kwargs),
        **kwargs,
    )
    draft_metrics = self._draft_metrics_since(draft_accept_start)

    # Store KV cache for future reuse (on cache miss)
    # IMPORTANT: We need to store only the prompt portion, not generated tokens
    if (
        use_cache
        and self._cache_manager is not None
        and all_images
        and not cache_hit
        and prompt_cache
    ):
        try:
            import copy

            import mlx.core as mx

            # Get prompt token count (before generation)
            prompt_tokens_count = getattr(result, "prompt_tokens", 0)

            # Deep copy the cache and trim to prompt tokens only
            cache_to_store = []
            for layer_cache in prompt_cache:
                new_cache = copy.copy(layer_cache)
                if hasattr(layer_cache, "state"):
                    state = layer_cache.state
                    if (
                        state is not None
                        and len(state) >= 2
                        and state[0] is not None
                    ):
                        # Copy arrays
                        keys = mx.array(state[0])
                        values = mx.array(state[1])
                        # Trim to prompt tokens only (not generated tokens)
                        if (
                            hasattr(layer_cache, "offset")
                            and layer_cache.offset > prompt_tokens_count
                        ):
                            # For caches with offset tracking, slice to prompt length
                            new_cache.keys = keys[:, :, :prompt_tokens_count, :]
                            new_cache.values = values[:, :, :prompt_tokens_count, :]
                            new_cache.offset = prompt_tokens_count
                        else:
                            new_cache.keys = keys
                            new_cache.values = values
                            if len(state) >= 3:
                                new_cache.offset = state[2]
                            elif hasattr(layer_cache, "offset"):
                                new_cache.offset = min(
                                    layer_cache.offset, prompt_tokens_count
                                )
                cache_to_store.append(new_cache)

            self._cache_manager.store(
                images=all_images,
                prompt=formatted_prompt,
                vision_embeddings=None,
                kv_cache=cache_to_store,
                token_ids=token_ids,
                num_image_tokens=256,
                model_name=self.model_name,
            )
            logger.info(
                f"[PREFIX CACHE] Stored KV cache for {len(all_images)} image(s) ({prompt_tokens_count} prompt tokens)"
            )
        except Exception as e:
            logger.warning(f"Failed to cache: {e}")

    # Handle GenerationResult object or plain string
    if hasattr(result, "text"):
        output_text = result.text
        prompt_tokens = getattr(result, "prompt_tokens", 0)
        generation_tokens = getattr(result, "generation_tokens", 0)
    else:
        output_text = str(result)
        prompt_tokens = 0
        generation_tokens = 0

    return MLLMOutput(
        text=output_text,
        finish_reason="stop",
        prompt_tokens=prompt_tokens,
        completion_tokens=generation_tokens,
        **draft_metrics,
    )

vllm_mlx.models.mllm.MLXMultimodalLM.stream_chat

stream_chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> Iterator[MLLMOutput]

Stream chat with OpenAI-compatible message format.

Supports multimodal content in messages: - {"type": "text", "text": "..."} - {"type": "image_url", "image_url": {"url": "..."}} - {"type": "image_url", "image_url": {"url": "data:image/...;base64,..."}}

Parameters:

  • messages (list[dict]) –

    List of chat messages (OpenAI format)

  • max_tokens (int, default: 256 ) –

    Maximum tokens to generate

  • temperature (float, default: 0.7 ) –

    Sampling temperature

  • **kwargs

    Additional parameters

Yields:

  • MLLMOutput

    MLLMOutput with incremental text chunks

Source code in vllm_mlx/models/mllm.py
def stream_chat(
    self,
    messages: list[dict],
    max_tokens: int = 256,
    temperature: float = 0.7,
    **kwargs,
) -> Iterator[MLLMOutput]:
    """
    Stream chat with OpenAI-compatible message format.

    Supports multimodal content in messages:
    - {"type": "text", "text": "..."}
    - {"type": "image_url", "image_url": {"url": "..."}}
    - {"type": "image_url", "image_url": {"url": "data:image/...;base64,..."}}

    Args:
        messages: List of chat messages (OpenAI format)
        max_tokens: Maximum tokens to generate
        temperature: Sampling temperature
        **kwargs: Additional parameters

    Yields:
        MLLMOutput with incremental text chunks
    """
    if not self._loaded:
        self.load()

    try:
        from mlx_vlm import stream_generate
        from mlx_vlm.prompt_utils import get_chat_template
    except ImportError:
        # Fallback to non-streaming if stream_generate not available
        output = self.chat(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            **kwargs,
        )
        yield output
        return

    # Extract text and images from messages
    # Build chat_messages for multi-turn support WITH proper image tokens per message
    all_image_urls = []  # Raw URLs/paths to process later
    chat_messages = []  # List of properly formatted messages for chat template

    # Pop params early so they don't leak into mlx_vlm.generate()
    video_fps = kwargs.pop("video_fps", DEFAULT_FPS)
    video_max_frames = kwargs.pop("video_max_frames", MAX_FRAMES)
    tools = kwargs.pop("tools", None)
    use_cache = kwargs.pop("use_cache", True)
    enable_thinking = kwargs.pop("enable_thinking", True)
    # Honor chat_template_kwargs on the MLLM path (parity with the text path in
    # llm.py). enable_thinking is commonly nested here via
    # --default-chat-template-kwargs {"enable_thinking": false}; it was previously
    # dropped for MLLM models, so reasoning could not be disabled and leaked into
    # the response.
    chat_template_kwargs = kwargs.pop("chat_template_kwargs", None) or {}
    if "enable_thinking" in chat_template_kwargs:
        enable_thinking = chat_template_kwargs.pop("enable_thinking")

    # Collect video and audio inputs from messages
    _msg_video_inputs = self._collect_video_inputs(messages)
    _msg_audio_inputs = self._collect_audio_inputs(messages)

    # Use native video pipeline for supported models.
    # NOTE: Native video yields a single chunk (not incremental streaming)
    # because mlx_vlm.video_generate has no streaming API. The event loop
    # is NOT blocked at the server level — SimpleEngine wraps this in
    # asyncio.to_thread(). True token-level streaming requires upstream
    # mlx-vlm support for video stream_generate.
    if self._video_native and _msg_video_inputs:
        output = self._generate_native_video(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            video_fps=video_fps,
            video_max_frames=video_max_frames,
            tools=tools,
            **kwargs,
        )
        yield output
        return

    # Fallback: frames as images
    _msg_video_frame_counts: dict[int, int] = {}
    _msg_extra_audio: dict[int, list[str]] = {}
    all_video_frames: list[str] = []
    all_audio_inputs: list[str] = []
    for msg_idx, vid_inputs in _msg_video_inputs.items():
        total_frames = 0
        has_explicit_audio = bool(_msg_audio_inputs.get(msg_idx))
        for vid_input in vid_inputs:
            # Resolve once; reused for audio extraction and frame prep.
            # See the matching block in chat() for rationale.
            try:
                resolved_video_path = process_video_input(vid_input)
            except Exception as exc:
                logger.warning(f"Could not resolve video: {exc}")
                resolved_video_path = None

            if (
                resolved_video_path
                and self._video_native_with_audio
                and not has_explicit_audio
            ):
                extracted_audio = extract_audio_from_video(resolved_video_path)
                if extracted_audio:
                    _msg_extra_audio.setdefault(msg_idx, []).append(extracted_audio)

            frames = self._prepare_video(
                vid_input,
                fps=video_fps,
                max_frames=video_max_frames,
                resolved_path=resolved_video_path,
            )
            all_video_frames.extend(frames)
            total_frames += len(frames)
            logger.info(f"Added {len(frames)} frames from video: {vid_input}")
        _msg_video_frame_counts[msg_idx] = total_frames

    for msg_idx, extra in _msg_extra_audio.items():
        _msg_audio_inputs.setdefault(msg_idx, []).extend(extra)

    for aud_inputs in _msg_audio_inputs.values():
        all_audio_inputs.extend(aud_inputs)

    chat_messages = _build_mllm_chat_messages(
        messages,
        all_image_urls=all_image_urls,
        video_frame_counts=_msg_video_frame_counts,
    )

    all_images = []
    if all_image_urls:
        all_images.extend(self._prepare_images(all_image_urls))
    all_images.extend(all_video_frames)
    all_audio = self._prepare_audio(all_audio_inputs) if all_audio_inputs else []

    # Build template kwargs for tool definitions (tools already popped above)
    template_extra_kwargs = {}
    if tools:
        template_extra_kwargs["tools"] = tools
    # Forward any remaining chat_template_kwargs (parity with the text path)
    template_extra_kwargs.update(chat_template_kwargs)

    try:
        formatted_prompt = get_chat_template(
            self.processor,
            chat_messages,
            add_generation_prompt=True,
            enable_thinking=enable_thinking,
            **template_extra_kwargs,
        )
    except TypeError:
        # The processor's chat template doesn't accept some forwarded
        # chat_template_kwargs — drop them and retry (mirror llm.py).
        for key in chat_template_kwargs:
            template_extra_kwargs.pop(key, None)
        formatted_prompt = get_chat_template(
            self.processor,
            chat_messages,
            add_generation_prompt=True,
            enable_thinking=enable_thinking,
            **template_extra_kwargs,
        )
    except Exception as e:
        logger.warning(
            f"Failed to apply chat template: {e}, using last user message"
        )
        # Fallback to last user message if template fails
        last_user_msg = ""
        for m in reversed(chat_messages):
            if m["role"] == "user":
                content = m.get("content", "")
                if isinstance(content, list):
                    for item in content:
                        if isinstance(item, dict) and item.get("type") == "text":
                            last_user_msg = item.get("text", "")
                            break
                else:
                    last_user_msg = content
                break
        formatted_prompt = last_user_msg

    # Check cache for existing KV state (uses images as cache key)
    from mlx_vlm.models import cache as vlm_cache

    prompt_cache = None
    cache_hit = False

    if use_cache and all_audio:
        logger.info("Stream chat cache disabled for audio inputs")
        use_cache = False

    if use_cache and self._cache_manager is not None and all_images:
        prompt_cache, cache_hit = self._cache_manager.fetch_cache(
            all_images, formatted_prompt
        )
        if cache_hit:
            logger.debug(f"Stream chat cache hit for {len(all_images)} image(s)")

    # Create new cache if needed
    if prompt_cache is None and self.model is not None:
        try:
            prompt_cache = vlm_cache.make_prompt_cache(
                self.model.language_model,
                max_kv_size=self.max_kv_size or None,
            )
        except Exception:
            prompt_cache = None

    # Stream generate tokens with cache
    accumulated_text = ""
    token_count = 0
    draft_accept_start = self._reset_draft_metrics()

    for chunk in stream_generate(
        self.model,
        self.processor,
        formatted_prompt,
        all_images if all_images else None,
        audio=all_audio if all_audio else None,
        max_tokens=max_tokens,
        temp=temperature,
        prompt_cache=prompt_cache,
        **self._draft_generation_kwargs(kwargs),
        **kwargs,
    ):
        token_count += 1
        # chunk is a GenerationResult with .text attribute containing the new token
        new_text = chunk.text if hasattr(chunk, "text") else str(chunk)
        accumulated_text += new_text

        yield MLLMOutput(
            text=new_text,  # Just the new token for streaming
            finish_reason=None,
            prompt_tokens=getattr(chunk, "prompt_tokens", 0),
            completion_tokens=token_count,
        )

    # Final yield with finish_reason
    yield MLLMOutput(
        text="",
        finish_reason="stop",
        prompt_tokens=getattr(chunk, "prompt_tokens", 0) if "chunk" in dir() else 0,
        completion_tokens=token_count,
        **self._draft_metrics_since(draft_accept_start),
    )

vllm_mlx.models.mllm.MLXMultimodalLM.describe_image

describe_image(image: str, prompt: str = 'Describe this image in detail.', max_tokens: int = 512, **kwargs) -> str

Convenience method to describe an image.

Parameters:

  • image (str) –

    Image path, URL, or base64 string

  • prompt (str, default: 'Describe this image in detail.' ) –

    Description prompt

  • max_tokens (int, default: 512 ) –

    Maximum tokens

  • **kwargs

    Additional parameters

Returns:

  • str

    Image description text

Source code in vllm_mlx/models/mllm.py
def describe_image(
    self,
    image: str,
    prompt: str = "Describe this image in detail.",
    max_tokens: int = 512,
    **kwargs,
) -> str:
    """
    Convenience method to describe an image.

    Args:
        image: Image path, URL, or base64 string
        prompt: Description prompt
        max_tokens: Maximum tokens
        **kwargs: Additional parameters

    Returns:
        Image description text
    """
    output = self.generate(
        prompt=prompt,
        images=[image],
        max_tokens=max_tokens,
        **kwargs,
    )
    return output.text

vllm_mlx.models.mllm.MLXMultimodalLM.answer_about_image

answer_about_image(image: str, question: str, max_tokens: int = 256, **kwargs) -> str

Answer a question about an image.

Parameters:

  • image (str) –

    Image path, URL, or base64 string

  • question (str) –

    Question about the image

  • max_tokens (int, default: 256 ) –

    Maximum tokens

  • **kwargs

    Additional parameters

Returns:

  • str

    Answer text

Source code in vllm_mlx/models/mllm.py
def answer_about_image(
    self,
    image: str,
    question: str,
    max_tokens: int = 256,
    **kwargs,
) -> str:
    """
    Answer a question about an image.

    Args:
        image: Image path, URL, or base64 string
        question: Question about the image
        max_tokens: Maximum tokens
        **kwargs: Additional parameters

    Returns:
        Answer text
    """
    output = self.generate(
        prompt=question,
        images=[image],
        max_tokens=max_tokens,
        **kwargs,
    )
    return output.text

vllm_mlx.models.mllm.MLXMultimodalLM.describe_video

describe_video(video: str | dict, prompt: str = 'Describe what happens in this video.', fps: float = 2.0, max_frames: int = 32, max_tokens: int = 512, **kwargs) -> str

Describe a video using frame extraction.

Parameters:

  • video (str | dict) –

    Video file path, URL, base64, or OpenAI format dict

  • prompt (str, default: 'Describe what happens in this video.' ) –

    Description prompt

  • fps (float, default: 2.0 ) –

    Frames per second to extract

  • max_frames (int, default: 32 ) –

    Maximum frames to extract

  • max_tokens (int, default: 512 ) –

    Maximum tokens to generate

Returns:

  • str

    Video description text

Example
URL

model.describe_video("https://example.com/video.mp4")

OpenAI format

model.describe_video({"url": "https://example.com/video.mp4"})

Source code in vllm_mlx/models/mllm.py
def describe_video(
    self,
    video: str | dict,
    prompt: str = "Describe what happens in this video.",
    fps: float = 2.0,
    max_frames: int = 32,
    max_tokens: int = 512,
    **kwargs,
) -> str:
    """
    Describe a video using frame extraction.

    Args:
        video: Video file path, URL, base64, or OpenAI format dict
        prompt: Description prompt
        fps: Frames per second to extract
        max_frames: Maximum frames to extract
        max_tokens: Maximum tokens to generate

    Returns:
        Video description text

    Example:
        # URL
        model.describe_video("https://example.com/video.mp4")

        # OpenAI format
        model.describe_video({"url": "https://example.com/video.mp4"})
    """
    output = self.generate(
        prompt=prompt,
        videos=[video],
        video_fps=fps,
        video_max_frames=max_frames,
        max_tokens=max_tokens,
        **kwargs,
    )
    return output.text

vllm_mlx.models.mllm.MLXMultimodalLM.get_cache_stats

get_cache_stats() -> dict

Get MLLM cache statistics.

Returns:

  • dict

    Dictionary with cache stats (hits, misses, hit_rate, tokens_saved, etc.)

Source code in vllm_mlx/models/mllm.py
def get_cache_stats(self) -> dict:
    """
    Get MLLM cache statistics.

    Returns:
        Dictionary with cache stats (hits, misses, hit_rate, tokens_saved, etc.)
    """
    if self._cache_manager is None:
        return {"enabled": False}

    stats = self._cache_manager.get_stats()
    stats["enabled"] = True
    stats["cache_entries"] = len(self._cache_manager)
    stats["max_entries"] = self._cache_manager.max_size
    return stats

vllm_mlx.models.mllm.MLXMultimodalLM.clear_cache

clear_cache() -> None

Clear the MLLM KV cache.

Source code in vllm_mlx/models/mllm.py
def clear_cache(self) -> None:
    """Clear the MLLM KV cache."""
    if self._cache_manager is not None:
        self._cache_manager.clear()
        logger.info("MLLM cache cleared")

vllm_mlx.models.mllm.MLXMultimodalLM.get_model_info

get_model_info() -> dict

Get information about the loaded model.

Source code in vllm_mlx/models/mllm.py
def get_model_info(self) -> dict:
    """Get information about the loaded model."""
    if not self._loaded:
        return {"loaded": False, "model_name": self.model_name}

    info = {
        "loaded": True,
        "model_name": self.model_name,
        "type": "multimodal-language-model",
        "supports_video": True,
        "supports_streaming": True,
        "cache_enabled": self.enable_cache,
    }

    if self.config:
        info["model_type"] = getattr(self.config, "model_type", "unknown")

    if self._cache_manager is not None:
        info["cache_stats"] = self._cache_manager.get_stats()

    return info

vllm_mlx.models.mllm.MLXMultimodalLM.list_supported_model_families staticmethod

list_supported_model_families() -> dict[str, str]

List supported model families and their patterns.

Any model on HuggingFace containing these patterns in the name is likely compatible with mlx-vlm.

Source code in vllm_mlx/models/mllm.py
@staticmethod
def list_supported_model_families() -> dict[str, str]:
    """
    List supported model families and their patterns.

    Any model on HuggingFace containing these patterns in the name
    is likely compatible with mlx-vlm.
    """
    return {
        "Qwen-VL": "Qwen VL models (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, etc.)",
        "LLaVA": "LLaVA vision-language models",
        "Idefics": "Idefics vision-language models",
        "PaliGemma": "PaliGemma multimodal models",
        "Pixtral": "Mistral's Pixtral vision models",
        "Molmo": "Allen AI's Molmo models",
        "Phi-3-Vision": "Microsoft's Phi-3 Vision models",
        "CogVLM": "Tsinghua's CogVLM models",
        "InternVL": "InternVL models",
        "MiniCPM-V": "OpenBMB's MiniCPM-V models",
        "Florence": "Microsoft Florence vision models",
        "DeepSeek-VL": "DeepSeek's vision-language models (DeepSeek-VL, DeepSeek-VL2)",
    }

vllm_mlx.models.mllm.MLXMultimodalLM.is_mllm_model staticmethod

is_mllm_model(model_name: str) -> bool

Check if a model name indicates an MLLM model.

Source code in vllm_mlx/models/mllm.py
@staticmethod
def is_mllm_model(model_name: str) -> bool:
    """Check if a model name indicates an MLLM model."""
    mllm_patterns = [
        "-VL-",
        "-VL/",
        "VL-",
        "llava",
        "LLaVA",
        "idefics",
        "Idefics",
        "paligemma",
        "PaliGemma",
        "gemma-3",
        "gemma3",  # Gemma 3 (multimodal)
        "medgemma",
        "MedGemma",  # MedGemma (medical multimodal)
        "pixtral",
        "Pixtral",
        "molmo",
        "Molmo",
        "phi3-vision",
        "phi-3-vision",
        "cogvlm",
        "CogVLM",
        "internvl",
        "InternVL",
        "minicpm-v",
        "MiniCPM-V",
        "florence",
        "Florence",
        "deepseek-vl",
        "DeepSeek-VL",
    ]
    model_lower = model_name.lower()
    return any(pattern.lower() in model_lower for pattern in mllm_patterns)

vllm_mlx.models.mllm.MLXMultimodalLM.__repr__

__repr__() -> str
Source code in vllm_mlx/models/mllm.py
def __repr__(self) -> str:
    status = "loaded" if self._loaded else "not loaded"
    return f"<MLXMultimodalLM model={self.model_name} status={status}>"

vllm_mlx.models.mllm.cleanup_temp_file

cleanup_temp_file(path: str) -> bool

Clean up a specific temporary file.

Source code in vllm_mlx/models/mllm.py
def cleanup_temp_file(path: str) -> bool:
    """Clean up a specific temporary file."""
    return _temp_manager.cleanup(path)

vllm_mlx.models.mllm.cleanup_all_temp_files

cleanup_all_temp_files() -> int

Clean up all tracked temporary files. Returns count of cleaned files.

Source code in vllm_mlx/models/mllm.py
def cleanup_all_temp_files() -> int:
    """Clean up all tracked temporary files. Returns count of cleaned files."""
    return _temp_manager.cleanup_all()

vllm_mlx.models.mllm._normalize_content_part

_normalize_content_part(item: object) -> object

Convert Pydantic content parts into plain Python objects.

Source code in vllm_mlx/models/mllm.py
def _normalize_content_part(item: object) -> object:
    """Convert Pydantic content parts into plain Python objects."""
    if hasattr(item, "model_dump"):
        return item.model_dump(exclude_none=True)
    if hasattr(item, "dict"):
        return {k: v for k, v in item.dict().items() if v is not None}
    return item

vllm_mlx.models.mllm._extract_media_url

_extract_media_url(item: dict, item_type: str) -> str
Source code in vllm_mlx/models/mllm.py
def _extract_media_url(item: dict, item_type: str) -> str:
    if item_type == "image_url":
        media_value = item.get("image_url", {})
    elif item_type == "video_url":
        media_value = item.get("video_url", {})
    elif item_type == "audio_url":
        media_value = item.get("audio_url", {})
    elif item_type in {"image", "video", "audio"}:
        media_value = item.get(item_type, item.get("url", ""))
    else:
        return ""

    if isinstance(media_value, dict):
        media_value = media_value.get("url", "")
    return media_value if isinstance(media_value, str) else ""

vllm_mlx.models.mllm._text_content_part

_text_content_part(text: str) -> dict[str, str]
Source code in vllm_mlx/models/mllm.py
def _text_content_part(text: str) -> dict[str, str]:
    return {"type": "text", "text": text, "content": text}

vllm_mlx.models.mllm._append_text_content_part

_append_text_content_part(built_parts: list[dict[str, str]], text_parts: list[str], text: str) -> None
Source code in vllm_mlx/models/mllm.py
def _append_text_content_part(
    built_parts: list[dict[str, str]], text_parts: list[str], text: str
) -> None:
    if not text:
        return
    built_parts.append(_text_content_part(text))
    text_parts.append(text)

vllm_mlx.models.mllm._build_string_mllm_message_content

_build_string_mllm_message_content(content: str, role: str) -> tuple[object, bool]
Source code in vllm_mlx/models/mllm.py
def _build_string_mllm_message_content(content: str, role: str) -> tuple[object, bool]:
    if not content:
        return "", False
    if role == "assistant":
        return content, True
    return [_text_content_part(content)], True

vllm_mlx.models.mllm._append_ordered_mllm_content_part

_append_ordered_mllm_content_part(raw_item: object, *, built_parts: list[dict[str, str]], text_parts: list[str], all_image_urls: list[str], video_frame_count: int) -> int
Source code in vllm_mlx/models/mllm.py
def _append_ordered_mllm_content_part(
    raw_item: object,
    *,
    built_parts: list[dict[str, str]],
    text_parts: list[str],
    all_image_urls: list[str],
    video_frame_count: int,
) -> int:
    item = _normalize_content_part(raw_item)
    if isinstance(item, str):
        _append_text_content_part(built_parts, text_parts, item)
        return video_frame_count

    if not isinstance(item, dict):
        return video_frame_count

    item_type = item.get("type", "")
    if item_type in {"text", "input_text"}:
        _append_text_content_part(
            built_parts, text_parts, item.get("text", "") or item.get("content", "")
        )
    elif item_type in {"image_url", "image"}:
        media_url = _extract_media_url(item, item_type)
        if media_url:
            all_image_urls.append(media_url)
        built_parts.append({"type": "image"})
    elif item_type in {"audio_url", "audio"}:
        # Audio inputs are collected once by _collect_audio_inputs before
        # message reconstruction; this helper only preserves placeholder order.
        built_parts.append({"type": "audio"})
    elif item_type in {"video", "video_url"}:
        # Native video models bypass this helper. For fallback frame extraction,
        # preserve the video position by inserting that message's frames here.
        built_parts.extend({"type": "image"} for _ in range(video_frame_count))
        return 0
    return video_frame_count

vllm_mlx.models.mllm._build_ordered_mllm_message_content

_build_ordered_mllm_message_content(content: object, *, role: str, all_image_urls: list[str], video_frame_count: int = 0) -> tuple[object, bool]

Build template content while preserving OpenAI media/text part order.

Source code in vllm_mlx/models/mllm.py
def _build_ordered_mllm_message_content(
    content: object,
    *,
    role: str,
    all_image_urls: list[str],
    video_frame_count: int = 0,
) -> tuple[object, bool]:
    """Build template content while preserving OpenAI media/text part order."""
    if isinstance(content, str):
        return _build_string_mllm_message_content(content, role)

    if not isinstance(content, list):
        return "", False

    built_parts: list[dict[str, str]] = []
    text_parts: list[str] = []
    remaining_video_frames = video_frame_count

    for raw_item in content:
        remaining_video_frames = _append_ordered_mllm_content_part(
            raw_item,
            built_parts=built_parts,
            text_parts=text_parts,
            all_image_urls=all_image_urls,
            video_frame_count=remaining_video_frames,
        )

    if role == "assistant":
        text = "".join(text_parts)
        return text, bool(text)

    return built_parts, bool(built_parts)

vllm_mlx.models.mllm._normalize_mllm_tool_calls

_normalize_mllm_tool_calls(tool_calls: list) -> list

Normalize replayed assistant tool calls for chat templates.

Mirrors _normalize_tool_call_arguments_for_template in vllm_mlx/engine/batched.py: JSON argument strings become mappings so templates that iterate argument keys render correctly.

Source code in vllm_mlx/models/mllm.py
def _normalize_mllm_tool_calls(tool_calls: list) -> list:
    """Normalize replayed assistant tool calls for chat templates.

    Mirrors ``_normalize_tool_call_arguments_for_template`` in
    ``vllm_mlx/engine/batched.py``: JSON argument strings become mappings so
    templates that iterate argument keys render correctly.
    """
    plain_calls = [_normalize_content_part(call) for call in tool_calls]
    normalized = normalize_messages_for_chat_template(
        [{"role": "assistant", "tool_calls": plain_calls}]
    )
    return normalized[0].get("tool_calls", plain_calls)

vllm_mlx.models.mllm._build_mllm_chat_messages

_build_mllm_chat_messages(messages: list[dict], *, all_image_urls: list[str], video_frame_counts: dict[int, int]) -> list[dict]

Build chat-template messages without reordering multimodal content parts.

Source code in vllm_mlx/models/mllm.py
def _build_mllm_chat_messages(
    messages: list[dict],
    *,
    all_image_urls: list[str],
    video_frame_counts: dict[int, int],
) -> list[dict]:
    """Build chat-template messages without reordering multimodal content parts."""
    chat_messages: list[dict] = []
    for msg_idx, msg in enumerate(messages):
        role = msg.get("role", "user")
        if not isinstance(role, str):
            role = str(role)

        content, has_content = _build_ordered_mllm_message_content(
            msg.get("content", ""),
            role=role,
            all_image_urls=all_image_urls,
            video_frame_count=video_frame_counts.get(msg_idx, 0),
        )
        chat_message = {"role": role, "content": content}

        if role == "assistant":
            tool_calls = msg.get("tool_calls")
            if isinstance(tool_calls, list) and tool_calls:
                # Keep tool-call turns even when text content is empty so
                # templates render the assistant -> tool exchange (issue #608).
                chat_message["tool_calls"] = _normalize_mllm_tool_calls(tool_calls)
                reasoning_content = msg.get("reasoning_content")
                if reasoning_content:
                    chat_message["reasoning_content"] = reasoning_content
                has_content = True
        elif role == "tool":
            tool_call_id = msg.get("tool_call_id")
            if tool_call_id:
                chat_message["tool_call_id"] = tool_call_id
                # Tools may legitimately return empty output; keep the message
                # anyway so the assistant tool_call still has its anchor and
                # template forward-scans pair calls to responses (issue #608).
                if not has_content:
                    chat_message["content"] = ""
                    has_content = True

        if has_content:
            chat_messages.append(chat_message)
    return chat_messages

vllm_mlx.models.mllm.load_gemma4_assistant_drafter

load_gemma4_assistant_drafter(model_path: str)

Load a Gemma 4 assistant drafter for mlx-vlm speculative decoding.

Source code in vllm_mlx/models/mllm.py
def load_gemma4_assistant_drafter(model_path: str):
    """Load a Gemma 4 assistant drafter for mlx-vlm speculative decoding."""
    try:
        import mlx.core as mx
        from mlx_vlm.speculative.drafters import gemma4_assistant as arch
    except ImportError as exc:
        raise ImportError(
            "Gemma 4 assistant drafter support requires an mlx-vlm build that "
            "provides mlx_vlm.speculative.drafters.gemma4_assistant."
        ) from exc

    try:
        mlx_vlm_version = version("mlx-vlm")
    except PackageNotFoundError:
        mlx_vlm_version = "unknown"
    logger.info(
        "Loading Gemma 4 assistant drafter from %s using mlx-vlm %s",
        model_path,
        mlx_vlm_version,
    )

    path = Path(model_path)
    config_path = path / "config.json"
    weight_paths = sorted(path.glob("*.safetensors"))
    if not config_path.exists():
        raise FileNotFoundError(f"Gemma 4 assistant config not found: {config_path}")
    if not weight_paths:
        raise FileNotFoundError(f"Gemma 4 assistant weights not found: {path}")

    config = arch.ModelConfig.from_dict(
        json.loads(config_path.read_text(encoding="utf-8"))
    )
    model = arch.Model(config)
    weights = {}
    for weight_path in weight_paths:
        weights.update(mx.load(str(weight_path)))
    if hasattr(model, "sanitize"):
        weights = model.sanitize(weights)
    model.load_weights(list(weights.items()))
    mx.eval(model.parameters())
    model.eval()
    return model

vllm_mlx.models.mllm._count_draft_tokens

_count_draft_tokens(draft_tokens) -> int

Best-effort drafted-token count for an mlx-vlm drafter output.

Source code in vllm_mlx/models/mllm.py
def _count_draft_tokens(draft_tokens) -> int:
    """Best-effort drafted-token count for an mlx-vlm drafter output."""
    shape = getattr(draft_tokens, "shape", None)
    if shape:
        try:
            return max(int(shape[-1]), 0)
        except (TypeError, ValueError):
            pass
    try:
        return max(len(draft_tokens), 0)
    except TypeError:
        return 0

vllm_mlx.models.mllm._install_draft_metrics_hooks

_install_draft_metrics_hooks(draft_model) -> None

Record actual drafted token counts from mlx-vlm assistant drafters.

Source code in vllm_mlx/models/mllm.py
def _install_draft_metrics_hooks(draft_model) -> None:
    """Record actual drafted token counts from mlx-vlm assistant drafters."""
    if draft_model is None or getattr(draft_model, "_vllm_mlx_metrics_hooked", False):
        return

    if not hasattr(draft_model, "_vllm_mlx_draft_counts"):
        draft_model._vllm_mlx_draft_counts = []

    draft_block = getattr(draft_model, "draft_block", None)
    if callable(draft_block):

        def draft_block_with_metrics(*args, **kwargs):
            draft_tokens = draft_block(*args, **kwargs)
            draft_model._vllm_mlx_draft_counts.append(_count_draft_tokens(draft_tokens))
            return draft_tokens

        draft_model.draft_block = draft_block_with_metrics

    reset = getattr(draft_model, "reset", None)
    if callable(reset):

        def reset_with_metrics(*args, **kwargs):
            draft_model._vllm_mlx_draft_counts = []
            return reset(*args, **kwargs)

        draft_model.reset = reset_with_metrics

    draft_model._vllm_mlx_metrics_hooked = True

vllm_mlx.models.mllm.is_base64_image

is_base64_image(s: str) -> bool

Check if string is base64-encoded image data.

Source code in vllm_mlx/models/mllm.py
def is_base64_image(s: str) -> bool:
    """Check if string is base64-encoded image data."""
    return s.startswith("data:image/") or (
        len(s) > 100 and not s.startswith(("http://", "https://", "/"))
    )

vllm_mlx.models.mllm.is_url

is_url(s: str) -> bool

Check if string is a URL.

Source code in vllm_mlx/models/mllm.py
def is_url(s: str) -> bool:
    """Check if string is a URL."""
    return s.startswith(("http://", "https://"))

vllm_mlx.models.mllm.is_base64_video

is_base64_video(s: str) -> bool

Check if string is base64-encoded video data.

Source code in vllm_mlx/models/mllm.py
def is_base64_video(s: str) -> bool:
    """Check if string is base64-encoded video data."""
    return s.startswith("data:video/")

vllm_mlx.models.mllm.is_base64_audio

is_base64_audio(s: str) -> bool

Check if string is base64-encoded audio data.

Source code in vllm_mlx/models/mllm.py
def is_base64_audio(s: str) -> bool:
    """Check if string is base64-encoded audio data."""
    return s.startswith("data:audio/")

vllm_mlx.models.mllm.decode_base64_image

decode_base64_image(base64_string: str, max_length: int = MAX_BASE64_IMAGE_LENGTH) -> bytes

Decode base64 image to bytes.

Parameters:

  • base64_string (str) –

    Base64 encoded image (optionally with data URL prefix)

  • max_length (int, default: MAX_BASE64_IMAGE_LENGTH ) –

    Maximum allowed length of base64 string

Returns:

  • bytes

    Decoded image bytes

Raises:

Source code in vllm_mlx/models/mllm.py
def decode_base64_image(
    base64_string: str, max_length: int = MAX_BASE64_IMAGE_LENGTH
) -> bytes:
    """
    Decode base64 image to bytes.

    Args:
        base64_string: Base64 encoded image (optionally with data URL prefix)
        max_length: Maximum allowed length of base64 string

    Returns:
        Decoded image bytes

    Raises:
        FileSizeExceededError: If base64 string exceeds max_length
    """
    if len(base64_string) > max_length:
        raise FileSizeExceededError(
            f"Base64 image data exceeds maximum size: {len(base64_string) / 1024 / 1024:.1f} MB > "
            f"{max_length / 1024 / 1024:.1f} MB limit"
        )

    # Handle data URL format: data:image/jpeg;base64,/9j/4AAQ...
    if base64_string.startswith("data:"):
        # Extract the base64 part after the comma
        _, data = base64_string.split(",", 1)
        return base64.b64decode(data)
    return base64.b64decode(base64_string)

vllm_mlx.models.mllm._validate_url_safety

_validate_url_safety(url: str) -> None

Reject remote URLs that target local or private network resources.

Source code in vllm_mlx/models/mllm.py
def _validate_url_safety(url: str) -> None:
    """Reject remote URLs that target local or private network resources."""
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"}:
        raise UnsafeRemoteURLError(
            f"Unsupported remote media URL scheme: {parsed.scheme or '<missing>'}"
        )

    hostname = parsed.hostname
    if not hostname:
        raise UnsafeRemoteURLError("Remote media URL must include a hostname")

    if hostname == "localhost" or hostname.endswith(".localhost"):
        raise UnsafeRemoteURLError(
            f"Remote media URL targets a blocked host: {hostname}"
        )

    try:
        resolved_ips = [ipaddress.ip_address(hostname)]
    except ValueError:
        try:
            addrinfo = socket.getaddrinfo(
                hostname,
                parsed.port or (443 if parsed.scheme == "https" else 80),
                type=socket.SOCK_STREAM,
            )
        except socket.gaierror as exc:
            raise UnsafeRemoteURLError(
                f"Failed to resolve remote media host {hostname}: {exc}"
            ) from exc
        resolved_ips = [ipaddress.ip_address(info[4][0]) for info in addrinfo]

    blocked_ips = [str(ip) for ip in resolved_ips if not ip.is_global]
    if blocked_ips:
        raise UnsafeRemoteURLError(
            f"Remote media URL resolves to blocked address(es): {', '.join(sorted(set(blocked_ips)))}"
        )

vllm_mlx.models.mllm._request_with_safe_redirects

_request_with_safe_redirects(method: str, url: str, *, timeout: int, headers: dict[str, str], stream: bool = False, max_redirects: int = 5)

Issue a requests call while validating every redirect target.

Source code in vllm_mlx/models/mllm.py
def _request_with_safe_redirects(
    method: str,
    url: str,
    *,
    timeout: int,
    headers: dict[str, str],
    stream: bool = False,
    max_redirects: int = 5,
):
    """Issue a requests call while validating every redirect target."""
    current_url = url
    for _ in range(max_redirects + 1):
        _validate_url_safety(current_url)
        response = requests.request(
            method,
            current_url,
            timeout=timeout,
            headers=headers,
            allow_redirects=False,
            verify=True,
            stream=stream,
        )
        if not response.is_redirect and not response.is_permanent_redirect:
            return response

        location = response.headers.get("location")
        response.close()
        if not location:
            raise UnsafeRemoteURLError(
                f"Remote media URL redirect missing Location header: {current_url}"
            )
        current_url = urljoin(current_url, location)

    raise UnsafeRemoteURLError(
        f"Remote media URL exceeded redirect limit ({max_redirects}): {url}"
    )

vllm_mlx.models.mllm.download_image

download_image(url: str, timeout: int = 30, max_size: int = MAX_IMAGE_SIZE) -> str

Download image from URL and return local path.

Parameters:

  • url (str) –

    Image URL

  • timeout (int, default: 30 ) –

    Download timeout in seconds

  • max_size (int, default: MAX_IMAGE_SIZE ) –

    Maximum allowed file size in bytes

Returns:

  • str

    Local file path to downloaded image

Raises:

Source code in vllm_mlx/models/mllm.py
def download_image(url: str, timeout: int = 30, max_size: int = MAX_IMAGE_SIZE) -> str:
    """
    Download image from URL and return local path.

    Args:
        url: Image URL
        timeout: Download timeout in seconds
        max_size: Maximum allowed file size in bytes

    Returns:
        Local file path to downloaded image

    Raises:
        FileSizeExceededError: If image exceeds max_size
    """
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
    }

    # First, make a HEAD request to check Content-Length
    try:
        head_response = _request_with_safe_redirects(
            "HEAD", url, timeout=timeout, headers=headers
        )
        content_length = head_response.headers.get("content-length")
        if content_length and int(content_length) > max_size:
            raise FileSizeExceededError(
                f"Image at {url} exceeds maximum size: {int(content_length) / 1024 / 1024:.1f} MB > "
                f"{max_size / 1024 / 1024:.1f} MB limit"
            )
    except requests.RequestException:
        # HEAD request failed, proceed with GET and check during download
        pass

    response = _request_with_safe_redirects(
        "GET", url, timeout=timeout, headers=headers, stream=True
    )
    response.raise_for_status()

    # Check Content-Length header from GET response
    content_length = response.headers.get("content-length")
    if content_length and int(content_length) > max_size:
        raise FileSizeExceededError(
            f"Image at {url} exceeds maximum size: {int(content_length) / 1024 / 1024:.1f} MB > "
            f"{max_size / 1024 / 1024:.1f} MB limit"
        )

    # Determine extension from content type or URL
    content_type = response.headers.get("content-type", "")
    if "jpeg" in content_type or "jpg" in content_type:
        ext = ".jpg"
    elif "png" in content_type:
        ext = ".png"
    elif "gif" in content_type:
        ext = ".gif"
    elif "webp" in content_type:
        ext = ".webp"
    else:
        # Try to get from URL
        path = urlparse(response.url).path
        ext = Path(path).suffix or ".jpg"

    # Save to temp file with size checking during download
    temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    downloaded_size = 0
    try:
        for chunk in response.iter_content(chunk_size=8192):
            downloaded_size += len(chunk)
            if downloaded_size > max_size:
                temp_file.close()
                os.unlink(temp_file.name)
                raise FileSizeExceededError(
                    f"Image at {url} exceeds maximum size during download: "
                    f"{downloaded_size / 1024 / 1024:.1f} MB > {max_size / 1024 / 1024:.1f} MB limit"
                )
            temp_file.write(chunk)
        temp_file.close()
    except FileSizeExceededError:
        raise
    except Exception:
        temp_file.close()
        if os.path.exists(temp_file.name):
            os.unlink(temp_file.name)
        raise

    return _temp_manager.register(temp_file.name)

vllm_mlx.models.mllm._download_media

_download_media(url: str, media_type: str, ext_map: dict[str, str], default_ext: str, timeout: int, max_size: int) -> str

Download media from URL, enforce size limits, and return a local temp path.

Source code in vllm_mlx/models/mllm.py
def _download_media(
    url: str,
    media_type: str,
    ext_map: dict[str, str],
    default_ext: str,
    timeout: int,
    max_size: int,
) -> str:
    """Download media from URL, enforce size limits, and return a local temp path."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
    }

    logger.info(f"Downloading {media_type} from: {url}")

    try:
        head_response = _request_with_safe_redirects(
            "HEAD", url, timeout=timeout, headers=headers
        )
        content_length = head_response.headers.get("content-length")
        if content_length and int(content_length) > max_size:
            raise FileSizeExceededError(
                f"{media_type.capitalize()} at {url} exceeds maximum size: "
                f"{int(content_length) / 1024 / 1024:.1f} MB > {max_size / 1024 / 1024:.1f} MB limit"
            )
    except requests.RequestException:
        pass

    response = _request_with_safe_redirects(
        "GET", url, timeout=timeout, headers=headers, stream=True
    )
    response.raise_for_status()

    content_length = response.headers.get("content-length")
    if content_length and int(content_length) > max_size:
        raise FileSizeExceededError(
            f"{media_type.capitalize()} at {url} exceeds maximum size: "
            f"{int(content_length) / 1024 / 1024:.1f} MB > {max_size / 1024 / 1024:.1f} MB limit"
        )

    content_type = response.headers.get("content-type", "").lower()
    ext = default_ext
    for key, mapped_ext in ext_map.items():
        if key in content_type:
            ext = mapped_ext
            break
    else:
        path_ext = Path(urlparse(response.url).path).suffix
        if path_ext:
            ext = path_ext

    temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    downloaded_size = 0
    try:
        for chunk in response.iter_content(chunk_size=8192):
            downloaded_size += len(chunk)
            if downloaded_size > max_size:
                temp_file.close()
                os.unlink(temp_file.name)
                raise FileSizeExceededError(
                    f"{media_type.capitalize()} at {url} exceeds maximum size during download: "
                    f"{downloaded_size / 1024 / 1024:.1f} MB > {max_size / 1024 / 1024:.1f} MB limit"
                )
            temp_file.write(chunk)
        temp_file.close()
    except FileSizeExceededError:
        raise
    except Exception:
        temp_file.close()
        if os.path.exists(temp_file.name):
            os.unlink(temp_file.name)
        raise

    file_size = Path(temp_file.name).stat().st_size
    logger.info(
        f"{media_type.capitalize()} downloaded: {temp_file.name} ({file_size / 1024 / 1024:.1f} MB)"
    )

    return _temp_manager.register(temp_file.name)

vllm_mlx.models.mllm.download_video

download_video(url: str, timeout: int = 120, max_size: int = MAX_VIDEO_SIZE) -> str

Download video from URL and return local path.

Source code in vllm_mlx/models/mllm.py
def download_video(url: str, timeout: int = 120, max_size: int = MAX_VIDEO_SIZE) -> str:
    """Download video from URL and return local path."""
    return _download_media(url, "video", _VIDEO_EXT_MAP, ".mp4", timeout, max_size)

vllm_mlx.models.mllm.download_audio

download_audio(url: str, timeout: int = 120, max_size: int = MAX_AUDIO_SIZE) -> str

Download audio from URL and return local path.

Source code in vllm_mlx/models/mllm.py
def download_audio(url: str, timeout: int = 120, max_size: int = MAX_AUDIO_SIZE) -> str:
    """Download audio from URL and return local path."""
    return _download_media(url, "audio", _AUDIO_EXT_MAP, ".wav", timeout, max_size)

vllm_mlx.models.mllm.decode_base64_video

decode_base64_video(base64_string: str, max_length: int = MAX_BASE64_VIDEO_LENGTH) -> str

Decode base64 video to temp file and return path.

Supports format: data:video/mp4;base64,AAAA...

Parameters:

  • base64_string (str) –

    Base64-encoded video with data URL prefix

  • max_length (int, default: MAX_BASE64_VIDEO_LENGTH ) –

    Maximum allowed length of base64 string

Returns:

  • str

    Local file path to decoded video

Raises:

Source code in vllm_mlx/models/mllm.py
def decode_base64_video(
    base64_string: str, max_length: int = MAX_BASE64_VIDEO_LENGTH
) -> str:
    """
    Decode base64 video to temp file and return path.

    Supports format: data:video/mp4;base64,AAAA...

    Args:
        base64_string: Base64-encoded video with data URL prefix
        max_length: Maximum allowed length of base64 string

    Returns:
        Local file path to decoded video

    Raises:
        FileSizeExceededError: If base64 string exceeds max_length
    """
    if len(base64_string) > max_length:
        raise FileSizeExceededError(
            f"Base64 video data exceeds maximum size: {len(base64_string) / 1024 / 1024:.1f} MB > "
            f"{max_length / 1024 / 1024:.1f} MB limit"
        )

    # Extract format and data
    if base64_string.startswith("data:video/"):
        # Format: data:video/mp4;base64,AAAA...
        header, data = base64_string.split(",", 1)
        # Extract extension from header (e.g., "data:video/mp4;base64" -> "mp4")
        format_part = header.split(";")[0]  # "data:video/mp4"
        ext = "." + format_part.split("/")[-1]  # ".mp4"
    else:
        # Assume mp4 if no header
        data = base64_string
        ext = ".mp4"

    # Decode, save, and register for cleanup
    video_bytes = base64.b64decode(data)
    temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    temp_file.write(video_bytes)
    temp_file.close()

    logger.info(
        f"Base64 video decoded: {temp_file.name} ({len(video_bytes) / 1024 / 1024:.1f} MB)"
    )

    return _temp_manager.register(temp_file.name)

vllm_mlx.models.mllm.decode_base64_audio

decode_base64_audio(base64_string: str, max_length: int = MAX_BASE64_AUDIO_LENGTH) -> str

Decode base64 audio to temp file and return path.

Supports format: data:audio/wav;base64,AAAA...

Source code in vllm_mlx/models/mllm.py
def decode_base64_audio(
    base64_string: str, max_length: int = MAX_BASE64_AUDIO_LENGTH
) -> str:
    """
    Decode base64 audio to temp file and return path.

    Supports format: data:audio/wav;base64,AAAA...
    """
    if len(base64_string) > max_length:
        raise FileSizeExceededError(
            f"Base64 audio data exceeds maximum size: {len(base64_string) / 1024 / 1024:.1f} MB > "
            f"{max_length / 1024 / 1024:.1f} MB limit"
        )

    if base64_string.startswith("data:audio/"):
        header, data = base64_string.split(",", 1)
        format_part = header.split(";")[0]
        ext = "." + format_part.split("/")[-1]
    else:
        data = base64_string
        ext = ".wav"

    audio_bytes = base64.b64decode(data)
    temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    temp_file.write(audio_bytes)
    temp_file.close()
    return _temp_manager.register(temp_file.name)

vllm_mlx.models.mllm.process_video_input

process_video_input(video: str | dict) -> str

Process video input in various formats and return local path.

Supports: - URL (http/https) - Base64 encoded string (data:video/mp4;base64,...) - OpenAI format dict: {"url": "..."} or {"url": "data:video/...;base64,..."}

Parameters:

  • video (str | dict) –

    Video input in any supported format

Returns:

  • str

    Local file path to video

Source code in vllm_mlx/models/mllm.py
def process_video_input(video: str | dict) -> str:
    """
    Process video input in various formats and return local path.

    Supports:
    - URL (http/https)
    - Base64 encoded string (data:video/mp4;base64,...)
    - OpenAI format dict: {"url": "..."} or {"url": "data:video/...;base64,..."}

    Args:
        video: Video input in any supported format

    Returns:
        Local file path to video
    """
    # Handle dict format (OpenAI style)
    if isinstance(video, dict):
        url = video.get("url", video.get("video_url", ""))
        if isinstance(url, dict):
            url = url.get("url", "")
        video = url

    if not video:
        raise ValueError("Empty video input")

    # Check if it's a URL
    if is_url(video):
        return download_video(video)

    # Check if it's base64
    if is_base64_video(video):
        return decode_base64_video(video)

    raise ValueError(
        "Unsupported video input. Only http(s) URLs and data:video base64 payloads are allowed."
    )

vllm_mlx.models.mllm.process_audio_input

process_audio_input(audio: str | dict) -> str

Process audio input in various formats and return local path.

Supports: - Local file path - URL (http/https) - Base64 encoded string (data:audio/wav;base64,...) - OpenAI format dict: {"url": "..."} or {"audio_url": {"url": "..."}}

Source code in vllm_mlx/models/mllm.py
def process_audio_input(audio: str | dict) -> str:
    """
    Process audio input in various formats and return local path.

    Supports:
    - Local file path
    - URL (http/https)
    - Base64 encoded string (data:audio/wav;base64,...)
    - OpenAI format dict: {"url": "..."} or {"audio_url": {"url": "..."}}
    """
    if isinstance(audio, dict):
        url = audio.get("url", audio.get("audio_url", ""))
        if isinstance(url, dict):
            url = url.get("url", "")
        audio = url

    if not audio:
        raise ValueError("Empty audio input")

    if is_base64_audio(audio):
        return decode_base64_audio(audio)

    if is_url(audio):
        return download_audio(audio)

    if len(audio) < 4096 and Path(audio).exists():
        return audio

    raise ValueError(f"Cannot process audio: {audio[:50]}...")

vllm_mlx.models.mllm._video_has_audio_track

_video_has_audio_track(video_path: str) -> bool

Return True if ffprobe finds an audio stream in the video.

Source code in vllm_mlx/models/mllm.py
def _video_has_audio_track(video_path: str) -> bool:
    """Return True if ffprobe finds an audio stream in the video."""
    import shutil
    import subprocess

    if not shutil.which("ffprobe"):
        return True  # assume yes; extraction will fail loudly if not
    try:
        r = subprocess.run(
            [
                "ffprobe",
                "-loglevel",
                "error",
                "-select_streams",
                "a",
                "-show_entries",
                "stream=codec_type",
                "-of",
                "csv=p=0",
                video_path,
            ],
            capture_output=True,
            timeout=30,
            text=True,
        )
        return bool(r.stdout.strip())
    except (subprocess.SubprocessError, OSError):
        return True

vllm_mlx.models.mllm._model_has_sound_encoder

_model_has_sound_encoder(model) -> bool

Whether a loaded model exposes a usable sound encoder.

Uses getattr(..., None) is not None rather than hasattr so model wrappers that declare sound_encoder in __init__ but leave it as None until the first encoder pass are correctly treated as not yet enabled. A bare hasattr check would spuriously enable A/V fusion against a missing encoder and crash the processor downstream.

Source code in vllm_mlx/models/mllm.py
def _model_has_sound_encoder(model) -> bool:
    """Whether a loaded model exposes a usable sound encoder.

    Uses ``getattr(..., None) is not None`` rather than ``hasattr`` so model
    wrappers that declare ``sound_encoder`` in ``__init__`` but leave it as
    ``None`` until the first encoder pass are correctly treated as not yet
    enabled. A bare ``hasattr`` check would spuriously enable A/V fusion
    against a missing encoder and crash the processor downstream.
    """
    return getattr(model, "sound_encoder", None) is not None

vllm_mlx.models.mllm.extract_audio_from_video

extract_audio_from_video(video_path: str) -> str | None

Extract the audio track from a video file as 16 kHz mono WAV.

Returns the path to the WAV (registered with the temp manager so it's cleaned up automatically), or None if the video has no audio or ffmpeg is unavailable.

Source code in vllm_mlx/models/mllm.py
def extract_audio_from_video(video_path: str) -> str | None:
    """Extract the audio track from a video file as 16 kHz mono WAV.

    Returns the path to the WAV (registered with the temp manager so it's
    cleaned up automatically), or None if the video has no audio or ffmpeg
    is unavailable.
    """
    import os
    import shutil
    import subprocess

    if not shutil.which("ffmpeg"):
        logger.warning(
            "ffmpeg not found; cannot fuse audio from video_url. "
            "Install ffmpeg to enable A/V fusion on omni models."
        )
        return None
    if not _video_has_audio_track(video_path):
        return None

    fd, out_path = tempfile.mkstemp(suffix=".wav", prefix="vllmmlx_va_")
    os.close(fd)
    try:
        r = subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-i",
                video_path,
                "-vn",
                "-ac",
                "1",
                "-ar",
                "16000",
                "-c:a",
                "pcm_s16le",
                out_path,
            ],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=600,
        )
        if r.returncode != 0 or os.path.getsize(out_path) == 0:
            try:
                os.unlink(out_path)
            except OSError:
                pass
            return None
        return _temp_manager.register(out_path)
    except (subprocess.SubprocessError, OSError) as e:
        logger.warning(f"Audio extraction from video failed: {e}")
        try:
            os.unlink(out_path)
        except OSError:
            pass
        return None

vllm_mlx.models.mllm.save_base64_image

save_base64_image(base64_string: str) -> str

Save base64 image to temp file and return path. Caches identical images.

Source code in vllm_mlx/models/mllm.py
def save_base64_image(base64_string: str) -> str:
    """Save base64 image to temp file and return path. Caches identical images."""
    import hashlib

    # Hash the FULL base64 string — not just a prefix.
    # Using only the first 1000 chars caused cache collisions between
    # different images with identical JPEG headers (e.g. invoices from
    # the same PDF renderer), returning a previous request's image.
    image_hash = hashlib.sha256(base64_string.encode()).hexdigest()

    # Return cached path if available and file still exists
    if image_hash in _base64_image_cache:
        cached_path = _base64_image_cache[image_hash]
        if Path(cached_path).exists():
            return cached_path

    image_bytes = decode_base64_image(base64_string)

    # Detect format from magic bytes
    if image_bytes[:8] == b"\x89PNG\r\n\x1a\n":
        ext = ".png"
    elif image_bytes[:2] == b"\xff\xd8":
        ext = ".jpg"
    elif image_bytes[:6] in (b"GIF87a", b"GIF89a"):
        ext = ".gif"
    elif image_bytes[:4] == b"RIFF" and image_bytes[8:12] == b"WEBP":
        ext = ".webp"
    else:
        ext = ".jpg"  # Default

    temp_file = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    temp_file.write(image_bytes)
    temp_file.close()

    path = _temp_manager.register(temp_file.name)
    _base64_image_cache[image_hash] = path
    return path

vllm_mlx.models.mllm.process_image_input

process_image_input(image: str | dict) -> str

Process image input in various formats and return local path.

Supports: - URL (http/https) - Base64 encoded string - OpenAI format dict: {"url": "..."} or {"url": "data:image/...;base64,..."}

Source code in vllm_mlx/models/mllm.py
def process_image_input(image: str | dict) -> str:
    """
    Process image input in various formats and return local path.

    Supports:
    - URL (http/https)
    - Base64 encoded string
    - OpenAI format dict: {"url": "..."} or {"url": "data:image/...;base64,..."}
    """
    # Handle dict format (OpenAI style)
    if isinstance(image, dict):
        url = image.get("url", image.get("image_url", ""))
        if isinstance(url, dict):
            url = url.get("url", "")
        image = url

    if not image:
        raise ValueError("Empty image input")

    # Check if it's base64 FIRST (before Path.exists() which fails on long strings)
    if is_base64_image(image):
        return save_base64_image(image)

    # Check if it's a URL
    if is_url(image):
        return download_image(image)

    raise ValueError(
        "Unsupported image input. Only http(s) URLs and data:image base64 payloads are allowed."
    )

vllm_mlx.models.mllm.round_by_factor

round_by_factor(x: int, factor: int) -> int

Round to nearest multiple of factor.

Source code in vllm_mlx/models/mllm.py
def round_by_factor(x: int, factor: int) -> int:
    """Round to nearest multiple of factor."""
    return round(x / factor) * factor

vllm_mlx.models.mllm.ceil_by_factor

ceil_by_factor(x: float, factor: int) -> int

Ceiling to next multiple of factor.

Source code in vllm_mlx/models/mllm.py
def ceil_by_factor(x: float, factor: int) -> int:
    """Ceiling to next multiple of factor."""
    return math.ceil(x / factor) * factor

vllm_mlx.models.mllm.floor_by_factor

floor_by_factor(x: float, factor: int) -> int

Floor to previous multiple of factor.

Source code in vllm_mlx/models/mllm.py
def floor_by_factor(x: float, factor: int) -> int:
    """Floor to previous multiple of factor."""
    return math.floor(x / factor) * factor

vllm_mlx.models.mllm.smart_nframes

smart_nframes(total_frames: int, video_fps: float, target_fps: float = DEFAULT_FPS, min_frames: int = MIN_FRAMES, max_frames: int = MAX_FRAMES) -> int

Calculate optimal number of frames to extract from video.

Uses smart sampling based on video length and target FPS.

Source code in vllm_mlx/models/mllm.py
def smart_nframes(
    total_frames: int,
    video_fps: float,
    target_fps: float = DEFAULT_FPS,
    min_frames: int = MIN_FRAMES,
    max_frames: int = MAX_FRAMES,
) -> int:
    """
    Calculate optimal number of frames to extract from video.

    Uses smart sampling based on video length and target FPS.
    """
    # Calculate duration-based frame count
    duration = total_frames / video_fps if video_fps > 0 else 0
    nframes = duration * target_fps

    # Clamp to min/max
    nframes = max(min_frames, min(nframes, max_frames, total_frames))

    # Round to factor
    nframes = max(FRAME_FACTOR, floor_by_factor(nframes, FRAME_FACTOR))

    return int(nframes)

vllm_mlx.models.mllm.extract_video_frames_smart

extract_video_frames_smart(video_path: str, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resize: tuple[int, int] | None = None) -> list[ndarray]

Extract frames from video with smart sampling.

Parameters:

  • video_path (str) –

    Path to video file

  • fps (float, default: DEFAULT_FPS ) –

    Target frames per second (default: 2.0)

  • max_frames (int, default: MAX_FRAMES ) –

    Maximum frames to extract

  • resize (tuple[int, int] | None, default: None ) –

    Optional (width, height) to resize frames

Returns:

  • list[ndarray]

    List of frame arrays (RGB format)

Source code in vllm_mlx/models/mllm.py
def extract_video_frames_smart(
    video_path: str,
    fps: float = DEFAULT_FPS,
    max_frames: int = MAX_FRAMES,
    resize: tuple[int, int] | None = None,
) -> list[np.ndarray]:
    """
    Extract frames from video with smart sampling.

    Args:
        video_path: Path to video file
        fps: Target frames per second (default: 2.0)
        max_frames: Maximum frames to extract
        resize: Optional (width, height) to resize frames

    Returns:
        List of frame arrays (RGB format)
    """
    try:
        import cv2
    except ImportError:
        raise ImportError("opencv-python is required for video processing")

    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        raise ValueError(f"Cannot open video: {video_path}")

    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    video_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0

    # Calculate number of frames to extract
    nframes = smart_nframes(
        total_frames=total_frames,
        video_fps=video_fps,
        target_fps=fps,
        max_frames=max_frames,
    )

    # Calculate frame indices (evenly spaced)
    indices = np.linspace(0, total_frames - 1, nframes).round().astype(int)

    logger.info(
        f"Video: {total_frames} total frames @ {video_fps:.1f} fps, "
        f"extracting {nframes} frames"
    )

    frames = []
    for idx in indices:
        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
        ret, frame = cap.read()
        if not ret:
            continue

        # Convert BGR to RGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # Resize if specified
        if resize:
            frame = cv2.resize(frame, resize)

        frames.append(frame)

    cap.release()

    return frames

vllm_mlx.models.mllm.save_frames_to_temp

save_frames_to_temp(frames: list[ndarray]) -> list[str]

Save frame arrays to temporary files and return paths.

Source code in vllm_mlx/models/mllm.py
def save_frames_to_temp(frames: list[np.ndarray]) -> list[str]:
    """Save frame arrays to temporary files and return paths."""
    try:
        from PIL import Image
    except ImportError:
        raise ImportError("Pillow is required for frame processing")

    paths = []
    for i, frame in enumerate(frames):
        img = Image.fromarray(frame)
        temp_file = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
        img.save(temp_file.name, "JPEG", quality=85)
        paths.append(_temp_manager.register(temp_file.name))

    return paths

Complete contract reference

Expand any definition for its exact inputs, annotations, defaults, return contract, directly raised exceptions, source-grounded behavior, and immutable line link. This section includes private and nested definitions that ordinary API generators omit.

vllm_mlx.models.mllm.TempFileManager · class
vllm_mlx.models.mllm.TempFileManager()

Thread-safe manager for tracking and cleaning up temporary files.

Parameters

This callable has no explicit inputs.

Returns

  • Constructs: vllm_mlx.models.mllm.TempFileManager

Exceptions and behavior

Class TempFileManager declares 4 direct member(s). No direct raise statement appears in this definition.

View source #L41-L86.

vllm_mlx.models.mllm.TempFileManager.__init__ · method
vllm_mlx.models.mllm.TempFileManager.__init__() -> not annotated

Method TempFileManager.__init__ updates self._files, self._lock; calls set, threading.Lock, atexit.register.

Parameters

This callable has no explicit inputs.

Returns

  • Type: not annotated

Exceptions and behavior

Method TempFileManager.__init__ updates self._files, self._lock; calls set, threading.Lock, atexit.register. No direct raise statement appears in this definition.

View source #L44-L47.

vllm_mlx.models.mllm.TempFileManager.register · method
vllm_mlx.models.mllm.TempFileManager.register(path: str) -> str

Register a temp file for tracking.

Parameters

Name Type Required Default Description
path str yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: path

Exceptions and behavior

Method TempFileManager.register calls self._files.add; returns path. No direct raise statement appears in this definition.

View source #L49-L53.

vllm_mlx.models.mllm.TempFileManager.cleanup · method
vllm_mlx.models.mllm.TempFileManager.cleanup(path: str) -> bool

Clean up a specific temp file.

Parameters

Name Type Required Default Description
path str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: True; False

Exceptions and behavior

Method TempFileManager.cleanup calls self._files.discard, os.path.exists, os.unlink, logger.debug; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L55-L67.

vllm_mlx.models.mllm.TempFileManager.cleanup_all · method
vllm_mlx.models.mllm.TempFileManager.cleanup_all() -> int

Clean up all tracked temp files.

Parameters

This callable has no explicit inputs.

Returns

  • Type: int
  • Direct return expressions: cleaned

Exceptions and behavior

Method TempFileManager.cleanup_all calls list, self._files.clear, os.path.exists, os.unlink; returns cleaned. No direct raise statement appears in this definition.

View source #L69-L86.

vllm_mlx.models.mllm.cleanup_temp_file · function
vllm_mlx.models.mllm.cleanup_temp_file(path: str) -> bool

Clean up a specific temporary file.

Parameters

Name Type Required Default Description
path str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: _temp_manager.cleanup(path)

Exceptions and behavior

Function cleanup_temp_file calls _temp_manager.cleanup; returns _temp_manager.cleanup(path). No direct raise statement appears in this definition.

View source #L93-L95.

vllm_mlx.models.mllm.cleanup_all_temp_files · function
vllm_mlx.models.mllm.cleanup_all_temp_files() -> int

Clean up all tracked temporary files.

Parameters

This callable has no explicit inputs.

Returns

  • Type: int
  • Direct return expressions: _temp_manager.cleanup_all()

Exceptions and behavior

Function cleanup_all_temp_files calls _temp_manager.cleanup_all; returns _temp_manager.cleanup_all(). No direct raise statement appears in this definition.

View source #L98-L100.

vllm_mlx.models.mllm.FileSizeExceededError · class
vllm_mlx.models.mllm.FileSizeExceededError()

Raised when a downloaded file exceeds the size limit.

Parameters

This callable has no explicit inputs.

Returns

  • Constructs: vllm_mlx.models.mllm.FileSizeExceededError

Exceptions and behavior

Class FileSizeExceededError derives from Exception and declares 0 direct member(s). No direct raise statement appears in this definition.

View source #L119-L122.

vllm_mlx.models.mllm.UnsafeRemoteURLError · class
vllm_mlx.models.mllm.UnsafeRemoteURLError(message: str, *, public_message: str = 'Remote media URL is not allowed')

Raised when a remote media URL targets an unsafe destination.

Parameters

Name Type Required Default Description
message str yes none Required positional or keyword input.
public_message str no 'Remote media URL is not allowed' Optional keyword-only input; defaults to 'Remote media URL is not allowed'.

Returns

  • Constructs: vllm_mlx.models.mllm.UnsafeRemoteURLError

Exceptions and behavior

Class UnsafeRemoteURLError derives from ValueError and declares 1 direct member(s). No direct raise statement appears in this definition.

View source #L125-L135.

vllm_mlx.models.mllm.UnsafeRemoteURLError.__init__ · method
vllm_mlx.models.mllm.UnsafeRemoteURLError.__init__(message: str, *, public_message: str = 'Remote media URL is not allowed') -> None

Method UnsafeRemoteURLError.__init__ updates self.public_message; calls super().__init__, super.

Parameters

Name Type Required Default Description
message str yes none Required positional or keyword input.
public_message str no 'Remote media URL is not allowed' Optional keyword-only input; defaults to 'Remote media URL is not allowed'.

Returns

  • Type: None

Exceptions and behavior

Method UnsafeRemoteURLError.__init__ updates self.public_message; calls super().__init__, super. No direct raise statement appears in this definition.

View source #L128-L135.

vllm_mlx.models.mllm._normalize_content_part · function
vllm_mlx.models.mllm._normalize_content_part(item: object) -> object

Convert Pydantic content parts into plain Python objects.

Parameters

Name Type Required Default Description
item object yes none Required positional or keyword input.

Returns

  • Type: object
  • Direct return expressions: item.model_dump(exclude_none=True); {k: v for k, v in item.dict().items() if v is not None}; item

Exceptions and behavior

Function _normalize_content_part calls hasattr, item.model_dump, item.dict().items, item.dict; has 3 explicit return paths. No direct raise statement appears in this definition.

View source #L138-L144.

vllm_mlx.models.mllm._extract_media_url · function
vllm_mlx.models.mllm._extract_media_url(item: dict, item_type: str) -> str

Function _extract_media_url calls item.get, isinstance, media_value.get; has 2 explicit return paths.

Parameters

Name Type Required Default Description
item dict yes none Required positional or keyword input.
item_type str yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: ''; media_value if isinstance(media_value, str) else ''

Exceptions and behavior

Function _extract_media_url calls item.get, isinstance, media_value.get; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L147-L161.

vllm_mlx.models.mllm._text_content_part · function
vllm_mlx.models.mllm._text_content_part(text: str) -> dict[str, str]

Function _text_content_part returns {'type': 'text', 'text': text, 'content': text}.

Parameters

Name Type Required Default Description
text str yes none Required positional or keyword input.

Returns

  • Type: dict[str, str]
  • Direct return expressions: {'type': 'text', 'text': text, 'content': text}

Exceptions and behavior

Function _text_content_part returns {'type': 'text', 'text': text, 'content': text}. No direct raise statement appears in this definition.

View source #L164-L165.

vllm_mlx.models.mllm._append_text_content_part · function
vllm_mlx.models.mllm._append_text_content_part(built_parts: list[dict[str, str]], text_parts: list[str], text: str) -> None

Function _append_text_content_part calls built_parts.append, _text_content_part, text_parts.append; returns None.

Parameters

Name Type Required Default Description
built_parts list[dict[str, str]] yes none Required positional or keyword input.
text_parts list[str] yes none Required positional or keyword input.
text str yes none Required positional or keyword input.

Returns

  • Type: None
  • Direct return expressions: None

Exceptions and behavior

Function _append_text_content_part calls built_parts.append, _text_content_part, text_parts.append; returns None. No direct raise statement appears in this definition.

View source #L168-L174.

vllm_mlx.models.mllm._build_string_mllm_message_content · function
vllm_mlx.models.mllm._build_string_mllm_message_content(content: str, role: str) -> tuple[object, bool]

Function _build_string_mllm_message_content calls _text_content_part; has 3 explicit return paths.

Parameters

Name Type Required Default Description
content str yes none Required positional or keyword input.
role str yes none Required positional or keyword input.

Returns

  • Type: tuple[object, bool]
  • Direct return expressions: ('', False); (content, True); ([_text_content_part(content)], True)

Exceptions and behavior

Function _build_string_mllm_message_content calls _text_content_part; has 3 explicit return paths. No direct raise statement appears in this definition.

View source #L177-L182.

vllm_mlx.models.mllm._append_ordered_mllm_content_part · function
vllm_mlx.models.mllm._append_ordered_mllm_content_part(raw_item: object, *, built_parts: list[dict[str, str]], text_parts: list[str], all_image_urls: list[str], video_frame_count: int) -> int

Function _append_ordered_mllm_content_part calls _normalize_content_part, isinstance, _append_text_content_part, item.get; has 2 explicit return paths.

Parameters

Name Type Required Default Description
raw_item object yes none Required positional or keyword input.
built_parts list[dict[str, str]] yes none Required keyword-only input.
text_parts list[str] yes none Required keyword-only input.
all_image_urls list[str] yes none Required keyword-only input.
video_frame_count int yes none Required keyword-only input.

Returns

  • Type: int
  • Direct return expressions: video_frame_count; 0

Exceptions and behavior

Function _append_ordered_mllm_content_part calls _normalize_content_part, isinstance, _append_text_content_part, item.get; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L185-L220.

vllm_mlx.models.mllm._build_ordered_mllm_message_content · function
vllm_mlx.models.mllm._build_ordered_mllm_message_content(content: object, *, role: str, all_image_urls: list[str], video_frame_count: int = 0) -> tuple[object, bool]

Build template content while preserving OpenAI media/text part order.

Parameters

Name Type Required Default Description
content object yes none Required positional or keyword input.
role str yes none Required keyword-only input.
all_image_urls list[str] yes none Required keyword-only input.
video_frame_count int no 0 Optional keyword-only input; defaults to 0.

Returns

  • Type: tuple[object, bool]
  • Direct return expressions: _build_string_mllm_message_content(content, role); ('', False); (text, bool(text)); (built_parts, bool(built_parts))

Exceptions and behavior

Function _build_ordered_mllm_message_content calls isinstance, _build_string_mllm_message_content, _append_ordered_mllm_content_part, ''.join; has 4 explicit return paths. No direct raise statement appears in this definition.

View source #L223-L254.

vllm_mlx.models.mllm._normalize_mllm_tool_calls · function
vllm_mlx.models.mllm._normalize_mllm_tool_calls(tool_calls: list) -> list

Normalize replayed assistant tool calls for chat templates.

Parameters

Name Type Required Default Description
tool_calls list yes none Required positional or keyword input.

Returns

  • Type: list
  • Direct return expressions: normalized[0].get('tool_calls', plain_calls)

Exceptions and behavior

Function _normalize_mllm_tool_calls calls _normalize_content_part, normalize_messages_for_chat_template, normalized[0].get; returns normalized[0].get('tool_calls', plain_calls). No direct raise statement appears in this definition.

View source #L257-L268.

vllm_mlx.models.mllm._build_mllm_chat_messages · function
vllm_mlx.models.mllm._build_mllm_chat_messages(messages: list[dict], *, all_image_urls: list[str], video_frame_counts: dict[int, int]) -> list[dict]

Build chat-template messages without reordering multimodal content parts.

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.
all_image_urls list[str] yes none Required keyword-only input.
video_frame_counts dict[int, int] yes none Required keyword-only input.

Returns

  • Type: list[dict]
  • Direct return expressions: chat_messages

Exceptions and behavior

Function _build_mllm_chat_messages calls enumerate, msg.get, isinstance, str; returns chat_messages. No direct raise statement appears in this definition.

View source #L271-L315.

vllm_mlx.models.mllm.MultimodalInput · class
vllm_mlx.models.mllm.MultimodalInput(prompt: str, images: list[str] = field(default_factory=list), videos: list[str] = field(default_factory=list), audio: list[str] = field(default_factory=list))

Input for multimodal generation.

Parameters

Name Type Required Default Description
prompt str yes none Required constructor field.
images list[str] no field(default_factory=list) Optional constructor field; defaults to field(default_factory=list).
videos list[str] no field(default_factory=list) Optional constructor field; defaults to field(default_factory=list).
audio list[str] no field(default_factory=list) Optional constructor field; defaults to field(default_factory=list).

Returns

  • Constructs: vllm_mlx.models.mllm.MultimodalInput

Exceptions and behavior

Class MultimodalInput declares 0 direct member(s). No direct raise statement appears in this definition.

View source #L319-L325.

vllm_mlx.models.mllm.MLLMOutput · class
vllm_mlx.models.mllm.MLLMOutput(text: str, finish_reason: str | None = None, prompt_tokens: int = 0, completion_tokens: int = 0, mtp_drafts: int = 0, mtp_accepted: int = 0)

Output from multimodal language model.

Parameters

Name Type Required Default Description
text str yes none Required constructor field.
finish_reason str \| None no None Optional constructor field; defaults to None.
prompt_tokens int no 0 Optional constructor field; defaults to 0.
completion_tokens int no 0 Optional constructor field; defaults to 0.
mtp_drafts int no 0 Optional constructor field; defaults to 0.
mtp_accepted int no 0 Optional constructor field; defaults to 0.

Returns

  • Constructs: vllm_mlx.models.mllm.MLLMOutput

Exceptions and behavior

Class MLLMOutput declares 0 direct member(s). No direct raise statement appears in this definition.

View source #L329-L337.

vllm_mlx.models.mllm.load_gemma4_assistant_drafter · function
vllm_mlx.models.mllm.load_gemma4_assistant_drafter(model_path: str) -> not annotated

Load a Gemma 4 assistant drafter for mlx-vlm speculative decoding.

Parameters

Name Type Required Default Description
model_path str yes none Required positional or keyword input.

Returns

  • Type: not annotated
  • Direct return expressions: model

Exceptions and behavior

Function load_gemma4_assistant_drafter calls ImportError, version, logger.info, Path; can raise ImportError, FileNotFoundError; returns model. Directly raised exceptions: ImportError, FileNotFoundError.

View source #L340-L381.

vllm_mlx.models.mllm._count_draft_tokens · function
vllm_mlx.models.mllm._count_draft_tokens(draft_tokens) -> int

Best-effort drafted-token count for an mlx-vlm drafter output.

Parameters

Name Type Required Default Description
draft_tokens not annotated yes none Required positional or keyword input.

Returns

  • Type: int
  • Direct return expressions: max(int(shape[-1]), 0); max(len(draft_tokens), 0); 0

Exceptions and behavior

Function _count_draft_tokens calls getattr, max, int, len; has 3 explicit return paths. No direct raise statement appears in this definition.

View source #L387-L398.

vllm_mlx.models.mllm._install_draft_metrics_hooks · function
vllm_mlx.models.mllm._install_draft_metrics_hooks(draft_model) -> None

Record actual drafted token counts from mlx-vlm assistant drafters.

Parameters

Name Type Required Default Description
draft_model not annotated yes none Required positional or keyword input.

Returns

  • Type: None
  • Direct return expressions: None

Exceptions and behavior

Function _install_draft_metrics_hooks calls getattr, hasattr, callable; returns None. No direct raise statement appears in this definition.

View source #L401-L428.

vllm_mlx.models.mllm._install_draft_metrics_hooks.draft_block_with_metrics · nested function
vllm_mlx.models.mllm._install_draft_metrics_hooks.draft_block_with_metrics(*args, **kwargs) -> not annotated

Nested Function _install_draft_metrics_hooks.draft_block_with_metrics calls draft_block, draft_model._vllm_mlx_draft_counts.append, _count_draft_tokens; returns draft_tokens.

Parameters

Name Type Required Default Description
*args not annotated no none Additional variadic positional inputs accepted by this callable.
**kwargs not annotated no none Additional variadic keyword inputs accepted by this callable.

Returns

  • Type: not annotated
  • Direct return expressions: draft_tokens

Exceptions and behavior

Nested Function _install_draft_metrics_hooks.draft_block_with_metrics calls draft_block, draft_model._vllm_mlx_draft_counts.append, _count_draft_tokens; returns draft_tokens. No direct raise statement appears in this definition.

View source #L412-L415.

vllm_mlx.models.mllm._install_draft_metrics_hooks.reset_with_metrics · nested function
vllm_mlx.models.mllm._install_draft_metrics_hooks.reset_with_metrics(*args, **kwargs) -> not annotated

Nested Function _install_draft_metrics_hooks.reset_with_metrics calls reset; returns reset(*args, **kwargs).

Parameters

Name Type Required Default Description
*args not annotated no none Additional variadic positional inputs accepted by this callable.
**kwargs not annotated no none Additional variadic keyword inputs accepted by this callable.

Returns

  • Type: not annotated
  • Direct return expressions: reset(*args, **kwargs)

Exceptions and behavior

Nested Function _install_draft_metrics_hooks.reset_with_metrics calls reset; returns reset(*args, **kwargs). No direct raise statement appears in this definition.

View source #L422-L424.

vllm_mlx.models.mllm.is_base64_image · function
vllm_mlx.models.mllm.is_base64_image(s: str) -> bool

Check if string is base64-encoded image data.

Parameters

Name Type Required Default Description
s str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: s.startswith('data:image/') or (len(s) > 100 and (not s.startswith(('http://', 'https://', '/'))))

Exceptions and behavior

Function is_base64_image calls s.startswith, len; returns s.startswith('data:image/') or (len(s) > 100 and (not s.startswith(('http://', 'https://', '/')))). No direct raise statement appears in this definition.

View source #L431-L435.

vllm_mlx.models.mllm.is_url · function
vllm_mlx.models.mllm.is_url(s: str) -> bool

Check if string is a URL.

Parameters

Name Type Required Default Description
s str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: s.startswith(('http://', 'https://'))

Exceptions and behavior

Function is_url calls s.startswith; returns s.startswith(('http://', 'https://')). No direct raise statement appears in this definition.

View source #L438-L440.

vllm_mlx.models.mllm.is_base64_video · function
vllm_mlx.models.mllm.is_base64_video(s: str) -> bool

Check if string is base64-encoded video data.

Parameters

Name Type Required Default Description
s str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: s.startswith('data:video/')

Exceptions and behavior

Function is_base64_video calls s.startswith; returns s.startswith('data:video/'). No direct raise statement appears in this definition.

View source #L443-L445.

vllm_mlx.models.mllm.is_base64_audio · function
vllm_mlx.models.mllm.is_base64_audio(s: str) -> bool

Check if string is base64-encoded audio data.

Parameters

Name Type Required Default Description
s str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: s.startswith('data:audio/')

Exceptions and behavior

Function is_base64_audio calls s.startswith; returns s.startswith('data:audio/'). No direct raise statement appears in this definition.

View source #L448-L450.

vllm_mlx.models.mllm.decode_base64_image · function
vllm_mlx.models.mllm.decode_base64_image(base64_string: str, max_length: int = MAX_BASE64_IMAGE_LENGTH) -> bytes

Decode base64 image to bytes.

Parameters

Name Type Required Default Description
base64_string str yes none Base64 encoded image (optionally with data URL prefix)
max_length int no MAX_BASE64_IMAGE_LENGTH Maximum allowed length of base64 string

Returns

  • Type: bytes
  • Direct return expressions: base64.b64decode(data); base64.b64decode(base64_string)

Exceptions and behavior

Function decode_base64_image calls len, FileSizeExceededError, base64_string.startswith, base64_string.split; can raise FileSizeExceededError; has 2 explicit return paths. Directly raised exceptions: FileSizeExceededError.

View source #L453-L480.

vllm_mlx.models.mllm._validate_url_safety · function
vllm_mlx.models.mllm._validate_url_safety(url: str) -> None

Reject remote URLs that target local or private network resources.

Parameters

Name Type Required Default Description
url str yes none Required positional or keyword input.

Returns

  • Type: None

Exceptions and behavior

Function _validate_url_safety calls urlparse, UnsafeRemoteURLError, hostname.endswith, ipaddress.ip_address; can raise UnsafeRemoteURLError. Directly raised exceptions: UnsafeRemoteURLError.

View source #L483-L519.

vllm_mlx.models.mllm._request_with_safe_redirects · function
vllm_mlx.models.mllm._request_with_safe_redirects(method: str, url: str, *, timeout: int, headers: dict[str, str], stream: bool = False, max_redirects: int = 5) -> not annotated

Issue a requests call while validating every redirect target.

Parameters

Name Type Required Default Description
method str yes none Required positional or keyword input.
url str yes none Required positional or keyword input.
timeout int yes none Required keyword-only input.
headers dict[str, str] yes none Required keyword-only input.
stream bool no False Optional keyword-only input; defaults to False.
max_redirects int no 5 Optional keyword-only input; defaults to 5.

Returns

  • Type: not annotated
  • Direct return expressions: response

Exceptions and behavior

Function _request_with_safe_redirects calls range, _validate_url_safety, requests.request, response.headers.get; can raise UnsafeRemoteURLError; returns response. Directly raised exceptions: UnsafeRemoteURLError.

View source #L522-L557.

vllm_mlx.models.mllm.download_image · function
vllm_mlx.models.mllm.download_image(url: str, timeout: int = 30, max_size: int = MAX_IMAGE_SIZE) -> str

Download image from URL and return local path.

Parameters

Name Type Required Default Description
url str yes none Image URL
timeout int no 30 Download timeout in seconds
max_size int no MAX_IMAGE_SIZE Maximum allowed file size in bytes

Returns

  • Type: str
  • Direct return expressions: _temp_manager.register(temp_file.name)

Exceptions and behavior

Function download_image calls _request_with_safe_redirects, head_response.headers.get, int, FileSizeExceededError; can raise FileSizeExceededError; returns _temp_manager.register(temp_file.name). Directly raised exceptions: FileSizeExceededError.

View source #L560-L645.

vllm_mlx.models.mllm._download_media · function
vllm_mlx.models.mllm._download_media(url: str, media_type: str, ext_map: dict[str, str], default_ext: str, timeout: int, max_size: int) -> str

Download media from URL, enforce size limits, and return a local temp path.

Parameters

Name Type Required Default Description
url str yes none Required positional or keyword input.
media_type str yes none Required positional or keyword input.
ext_map dict[str, str] yes none Required positional or keyword input.
default_ext str yes none Required positional or keyword input.
timeout int yes none Required positional or keyword input.
max_size int yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: _temp_manager.register(temp_file.name)

Exceptions and behavior

Function _download_media calls logger.info, _request_with_safe_redirects, head_response.headers.get, int; can raise FileSizeExceededError; returns _temp_manager.register(temp_file.name). Directly raised exceptions: FileSizeExceededError.

View source #L670-L748.

vllm_mlx.models.mllm.download_video · function
vllm_mlx.models.mllm.download_video(url: str, timeout: int = 120, max_size: int = MAX_VIDEO_SIZE) -> str

Download video from URL and return local path.

Parameters

Name Type Required Default Description
url str yes none Required positional or keyword input.
timeout int no 120 Optional positional or keyword input; defaults to 120.
max_size int no MAX_VIDEO_SIZE Optional positional or keyword input; defaults to MAX_VIDEO_SIZE.

Returns

  • Type: str
  • Direct return expressions: _download_media(url, 'video', _VIDEO_EXT_MAP, '.mp4', timeout, max_size)

Exceptions and behavior

Function download_video calls _download_media; returns _download_media(url, 'video', _VIDEO_EXT_MAP, '.mp4', timeout, max_size). No direct raise statement appears in this definition.

View source #L751-L753.

vllm_mlx.models.mllm.download_audio · function
vllm_mlx.models.mllm.download_audio(url: str, timeout: int = 120, max_size: int = MAX_AUDIO_SIZE) -> str

Download audio from URL and return local path.

Parameters

Name Type Required Default Description
url str yes none Required positional or keyword input.
timeout int no 120 Optional positional or keyword input; defaults to 120.
max_size int no MAX_AUDIO_SIZE Optional positional or keyword input; defaults to MAX_AUDIO_SIZE.

Returns

  • Type: str
  • Direct return expressions: _download_media(url, 'audio', _AUDIO_EXT_MAP, '.wav', timeout, max_size)

Exceptions and behavior

Function download_audio calls _download_media; returns _download_media(url, 'audio', _AUDIO_EXT_MAP, '.wav', timeout, max_size). No direct raise statement appears in this definition.

View source #L756-L758.

vllm_mlx.models.mllm.decode_base64_video · function
vllm_mlx.models.mllm.decode_base64_video(base64_string: str, max_length: int = MAX_BASE64_VIDEO_LENGTH) -> str

Decode base64 video to temp file and return path.

Parameters

Name Type Required Default Description
base64_string str yes none Base64-encoded video with data URL prefix
max_length int no MAX_BASE64_VIDEO_LENGTH Maximum allowed length of base64 string

Returns

  • Type: str
  • Direct return expressions: _temp_manager.register(temp_file.name)

Exceptions and behavior

Function decode_base64_video calls len, FileSizeExceededError, base64_string.startswith, base64_string.split; can raise FileSizeExceededError; returns _temp_manager.register(temp_file.name). Directly raised exceptions: FileSizeExceededError.

View source #L761-L807.

vllm_mlx.models.mllm.decode_base64_audio · function
vllm_mlx.models.mllm.decode_base64_audio(base64_string: str, max_length: int = MAX_BASE64_AUDIO_LENGTH) -> str

Decode base64 audio to temp file and return path.

Parameters

Name Type Required Default Description
base64_string str yes none Required positional or keyword input.
max_length int no MAX_BASE64_AUDIO_LENGTH Optional positional or keyword input; defaults to MAX_BASE64_AUDIO_LENGTH.

Returns

  • Type: str
  • Direct return expressions: _temp_manager.register(temp_file.name)

Exceptions and behavior

Function decode_base64_audio calls len, FileSizeExceededError, base64_string.startswith, base64_string.split; can raise FileSizeExceededError; returns _temp_manager.register(temp_file.name). Directly raised exceptions: FileSizeExceededError.

View source #L810-L836.

vllm_mlx.models.mllm.process_video_input · function
vllm_mlx.models.mllm.process_video_input(video: str | dict) -> str

Process video input in various formats and return local path.

Parameters

Name Type Required Default Description
video str \| dict yes none Video input in any supported format

Returns

  • Type: str
  • Direct return expressions: download_video(video); decode_base64_video(video)

Exceptions and behavior

Function process_video_input calls isinstance, video.get, url.get, ValueError; can raise ValueError; has 2 explicit return paths. Directly raised exceptions: ValueError.

View source #L839-L874.

vllm_mlx.models.mllm.process_audio_input · function
vllm_mlx.models.mllm.process_audio_input(audio: str | dict) -> str

Process audio input in various formats and return local path.

Parameters

Name Type Required Default Description
audio str \| dict yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: decode_base64_audio(audio); download_audio(audio); audio

Exceptions and behavior

Function process_audio_input calls isinstance, audio.get, url.get, ValueError; can raise ValueError; has 3 explicit return paths. Directly raised exceptions: ValueError.

View source #L877-L905.

vllm_mlx.models.mllm._video_has_audio_track · function
vllm_mlx.models.mllm._video_has_audio_track(video_path: str) -> bool

Return True if ffprobe finds an audio stream in the video.

Parameters

Name Type Required Default Description
video_path str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: True; bool(r.stdout.strip())

Exceptions and behavior

Function _video_has_audio_track calls shutil.which, subprocess.run, bool, r.stdout.strip; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L908-L935.

vllm_mlx.models.mllm._model_has_sound_encoder · function
vllm_mlx.models.mllm._model_has_sound_encoder(model) -> bool

Whether a loaded model exposes a usable sound encoder.

Parameters

Name Type Required Default Description
model not annotated yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: getattr(model, 'sound_encoder', None) is not None

Exceptions and behavior

Function _model_has_sound_encoder calls getattr; returns getattr(model, 'sound_encoder', None) is not None. No direct raise statement appears in this definition.

View source #L938-L947.

vllm_mlx.models.mllm.extract_audio_from_video · function
vllm_mlx.models.mllm.extract_audio_from_video(video_path: str) -> str | None

Extract the audio track from a video file as 16 kHz mono WAV.

Parameters

Name Type Required Default Description
video_path str yes none Required positional or keyword input.

Returns

  • Type: str | None
  • Direct return expressions: None; _temp_manager.register(out_path)

Exceptions and behavior

Function extract_audio_from_video calls shutil.which, logger.warning, _video_has_audio_track, tempfile.mkstemp; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L950-L1005.

vllm_mlx.models.mllm.save_base64_image · function
vllm_mlx.models.mllm.save_base64_image(base64_string: str) -> str

Save base64 image to temp file and return path.

Parameters

Name Type Required Default Description
base64_string str yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: cached_path; path

Exceptions and behavior

Function save_base64_image calls hashlib.sha256(base64_string.encode()).hexdigest, hashlib.sha256, base64_string.encode, Path(cached_path).exists; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L1012-L1048.

vllm_mlx.models.mllm.process_image_input · function
vllm_mlx.models.mllm.process_image_input(image: str | dict) -> str

Process image input in various formats and return local path.

Parameters

Name Type Required Default Description
image str \| dict yes none Required positional or keyword input.

Returns

  • Type: str
  • Direct return expressions: save_base64_image(image); download_image(image)

Exceptions and behavior

Function process_image_input calls isinstance, image.get, url.get, ValueError; can raise ValueError; has 2 explicit return paths. Directly raised exceptions: ValueError.

View source #L1051-L1080.

vllm_mlx.models.mllm.round_by_factor · function
vllm_mlx.models.mllm.round_by_factor(x: int, factor: int) -> int

Round to nearest multiple of factor.

Parameters

Name Type Required Default Description
x int yes none Required positional or keyword input.
factor int yes none Required positional or keyword input.

Returns

  • Type: int
  • Direct return expressions: round(x / factor) * factor

Exceptions and behavior

Function round_by_factor calls round; returns round(x / factor) * factor. No direct raise statement appears in this definition.

View source #L1083-L1085.

vllm_mlx.models.mllm.ceil_by_factor · function
vllm_mlx.models.mllm.ceil_by_factor(x: float, factor: int) -> int

Ceiling to next multiple of factor.

Parameters

Name Type Required Default Description
x float yes none Required positional or keyword input.
factor int yes none Required positional or keyword input.

Returns

  • Type: int
  • Direct return expressions: math.ceil(x / factor) * factor

Exceptions and behavior

Function ceil_by_factor calls math.ceil; returns math.ceil(x / factor) * factor. No direct raise statement appears in this definition.

View source #L1088-L1090.

vllm_mlx.models.mllm.floor_by_factor · function
vllm_mlx.models.mllm.floor_by_factor(x: float, factor: int) -> int

Floor to previous multiple of factor.

Parameters

Name Type Required Default Description
x float yes none Required positional or keyword input.
factor int yes none Required positional or keyword input.

Returns

  • Type: int
  • Direct return expressions: math.floor(x / factor) * factor

Exceptions and behavior

Function floor_by_factor calls math.floor; returns math.floor(x / factor) * factor. No direct raise statement appears in this definition.

View source #L1093-L1095.

vllm_mlx.models.mllm.smart_nframes · function
vllm_mlx.models.mllm.smart_nframes(total_frames: int, video_fps: float, target_fps: float = DEFAULT_FPS, min_frames: int = MIN_FRAMES, max_frames: int = MAX_FRAMES) -> int

Calculate optimal number of frames to extract from video.

Parameters

Name Type Required Default Description
total_frames int yes none Required positional or keyword input.
video_fps float yes none Required positional or keyword input.
target_fps float no DEFAULT_FPS Optional positional or keyword input; defaults to DEFAULT_FPS.
min_frames int no MIN_FRAMES Optional positional or keyword input; defaults to MIN_FRAMES.
max_frames int no MAX_FRAMES Optional positional or keyword input; defaults to MAX_FRAMES.

Returns

  • Type: int
  • Direct return expressions: int(nframes)

Exceptions and behavior

Function smart_nframes calls max, min, floor_by_factor, int; returns int(nframes). No direct raise statement appears in this definition.

View source #L1098-L1120.

vllm_mlx.models.mllm.extract_video_frames_smart · function
vllm_mlx.models.mllm.extract_video_frames_smart(video_path: str, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resize: tuple[int, int] | None = None) -> list[np.ndarray]

Extract frames from video with smart sampling.

Parameters

Name Type Required Default Description
video_path str yes none Path to video file
fps float no DEFAULT_FPS Target frames per second (default: 2.0)
max_frames int no MAX_FRAMES Maximum frames to extract
resize tuple[int, int] \| None no None Optional (width, height) to resize frames

Returns

  • Type: list[np.ndarray]
  • Direct return expressions: frames

Exceptions and behavior

Function extract_video_frames_smart calls ImportError, cv2.VideoCapture, cap.isOpened, ValueError; can raise ImportError, ValueError; returns frames. Directly raised exceptions: ImportError, ValueError.

View source #L1123-L1187.

vllm_mlx.models.mllm.save_frames_to_temp · function
vllm_mlx.models.mllm.save_frames_to_temp(frames: list[np.ndarray]) -> list[str]

Save frame arrays to temporary files and return paths.

Parameters

Name Type Required Default Description
frames list[np.ndarray] yes none Required positional or keyword input.

Returns

  • Type: list[str]
  • Direct return expressions: paths

Exceptions and behavior

Function save_frames_to_temp calls ImportError, enumerate, Image.fromarray, tempfile.NamedTemporaryFile; can raise ImportError; returns paths. Directly raised exceptions: ImportError.

View source #L1190-L1204.

vllm_mlx.models.mllm.MLXMultimodalLM · class
vllm_mlx.models.mllm.MLXMultimodalLM(model_name: str, trust_remote_code: bool = False, enable_cache: bool = True, cache_size: int = 50, max_kv_size: int = 0, draft_model: str | None = None, draft_kind: str | None = None, draft_block_size: int | None = None)

Wrapper around mlx-vlm for multimodal inference.

Parameters

Name Type Required Default Description
model_name str yes none HuggingFace model name or local path
trust_remote_code bool no False Whether to trust remote code
enable_cache bool no True Enable KV cache for repeated image/video+prompt (default: True)
cache_size int no 50 Maximum cache entries (default: 50)
max_kv_size int no 0 Maximum KV cache size per sequence (0 = unbounded)
draft_model str \| None no None Optional MLLM speculative draft/assistant model path.
draft_kind str \| None no None Optional mlx-vlm draft kind, for example "mtp".
draft_block_size int \| None no None Optional speculative block size passed to mlx-vlm.

Returns

  • Constructs: vllm_mlx.models.mllm.MLXMultimodalLM

Exceptions and behavior

Class MLXMultimodalLM declares 29 direct member(s). No direct raise statement appears in this definition.

View source #L1207-L2938.

vllm_mlx.models.mllm.MLXMultimodalLM.__init__ · method
vllm_mlx.models.mllm.MLXMultimodalLM.__init__(model_name: str, trust_remote_code: bool = False, enable_cache: bool = True, cache_size: int = 50, max_kv_size: int = 0, draft_model: str | None = None, draft_kind: str | None = None, draft_block_size: int | None = None) -> not annotated

Initialize the MLX multimodal language model.

Parameters

Name Type Required Default Description
model_name str yes none HuggingFace model name or local path
trust_remote_code bool no False Whether to trust remote code
enable_cache bool no True Enable KV cache for repeated image/video+prompt (default: True)
cache_size int no 50 Maximum cache entries (default: 50)
max_kv_size int no 0 Maximum KV cache size per sequence (0 = unbounded)
draft_model str \| None no None Optional MLLM speculative draft/assistant model path.
draft_kind str \| None no None Optional mlx-vlm draft kind, for example "mtp".
draft_block_size int \| None no None Optional speculative block size passed to mlx-vlm.

Returns

  • Type: not annotated

Exceptions and behavior

Method MLXMultimodalLM.__init__ updates self.model_name, self.trust_remote_code, self.enable_cache, self.max_kv_size; calls MLLMPrefixCacheManager. No direct raise statement appears in this definition.

View source #L1235-L1278.

vllm_mlx.models.mllm.MLXMultimodalLM.load · method
vllm_mlx.models.mllm.MLXMultimodalLM.load() -> None

Load the model and processor.

Parameters

This callable has no explicit inputs.

Returns

  • Type: None
  • Direct return expressions: None

Exceptions and behavior

Method MLXMultimodalLM.load updates self.model, self.processor, self.config, self._draft_model; calls logger.info, load, load_config, self._load_draft_model; can raise ImportError; returns None. Directly raised exceptions: ImportError.

View source #L1280-L1323.

vllm_mlx.models.mllm.MLXMultimodalLM._load_draft_model · method
vllm_mlx.models.mllm.MLXMultimodalLM._load_draft_model() -> not annotated

Method MLXMultimodalLM._load_draft_model calls load_gemma4_assistant_drafter, load; has 2 explicit return paths.

Parameters

This callable has no explicit inputs.

Returns

  • Type: not annotated
  • Direct return expressions: load_gemma4_assistant_drafter(self.draft_model_path); draft_model

Exceptions and behavior

Method MLXMultimodalLM._load_draft_model calls load_gemma4_assistant_drafter, load; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L1325-L1332.

vllm_mlx.models.mllm.MLXMultimodalLM._draft_generation_kwargs · method
vllm_mlx.models.mllm.MLXMultimodalLM._draft_generation_kwargs(call_kwargs: dict | None = None) -> dict

Return mlx-vlm drafter kwargs when the request explicitly opts in.

Parameters

Name Type Required Default Description
call_kwargs dict \| None no None Optional positional or keyword input; defaults to None.

Returns

  • Type: dict
  • Direct return expressions: {}; kwargs

Exceptions and behavior

Method MLXMultimodalLM._draft_generation_kwargs calls bool, call_kwargs.pop, _install_draft_metrics_hooks; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L1334-L1355.

vllm_mlx.models.mllm.MLXMultimodalLM._reset_draft_metrics · method
vllm_mlx.models.mllm.MLXMultimodalLM._reset_draft_metrics() -> int

Method MLXMultimodalLM._reset_draft_metrics updates self._draft_model.accept_lens, self._draft_model._vllm_mlx_draft_counts; calls hasattr; returns 0.

Parameters

This callable has no explicit inputs.

Returns

  • Type: int
  • Direct return expressions: 0

Exceptions and behavior

Method MLXMultimodalLM._reset_draft_metrics updates self._draft_model.accept_lens, self._draft_model._vllm_mlx_draft_counts; calls hasattr; returns 0. No direct raise statement appears in this definition.

View source #L1357-L1364.

vllm_mlx.models.mllm.MLXMultimodalLM._draft_metrics_since · method
vllm_mlx.models.mllm.MLXMultimodalLM._draft_metrics_since(start_accept_lens: int) -> dict[str, int]

Method MLXMultimodalLM._draft_metrics_since calls list, getattr, len, int; has 2 explicit return paths.

Parameters

Name Type Required Default Description
start_accept_lens int yes none Required positional or keyword input.

Returns

  • Type: dict[str, int]
  • Direct return expressions: {'mtp_drafts': 0, 'mtp_accepted': 0}; {'mtp_drafts': mtp_drafts, 'mtp_accepted': sum((int(value) for value in new_accept_lens))}

Exceptions and behavior

Method MLXMultimodalLM._draft_metrics_since calls list, getattr, len, int; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L1366-L1395.

vllm_mlx.models.mllm.MLXMultimodalLM.get_language_model · method
vllm_mlx.models.mllm.MLXMultimodalLM.get_language_model() -> not annotated

Extract the underlying language model for mlx_lm TextModel construction.

Parameters

This callable has no explicit inputs.

Returns

  • Type: not annotated
  • Direct return expressions: self.model.language_model

Exceptions and behavior

Method MLXMultimodalLM.get_language_model returns self.model.language_model. No direct raise statement appears in this definition.

View source #L1397-L1399.

vllm_mlx.models.mllm.MLXMultimodalLM.get_tokenizer · method
vllm_mlx.models.mllm.MLXMultimodalLM.get_tokenizer() -> not annotated

Get the text tokenizer (not the multimodal processor).

Parameters

This callable has no explicit inputs.

Returns

  • Type: not annotated
  • Direct return expressions: self.processor.tokenizer

Exceptions and behavior

Method MLXMultimodalLM.get_tokenizer returns self.processor.tokenizer. No direct raise statement appears in this definition.

View source #L1401-L1403.

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_images · method
vllm_mlx.models.mllm.MLXMultimodalLM._prepare_images(images: list) -> list[str]

Process remote/base64 image inputs into local temp file paths.

Parameters

Name Type Required Default Description
images list yes none Required positional or keyword input.

Returns

  • Type: list[str]
  • Direct return expressions: processed

Exceptions and behavior

Method MLXMultimodalLM._prepare_images calls process_image_input, processed.append, logger.warning; returns processed. No direct raise statement appears in this definition.

View source #L1405-L1414.

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_audio · method
vllm_mlx.models.mllm.MLXMultimodalLM._prepare_audio(audio_inputs: list) -> list[str]

Process audio inputs and return local file paths.

Parameters

Name Type Required Default Description
audio_inputs list yes none Required positional or keyword input.

Returns

  • Type: list[str]
  • Direct return expressions: processed

Exceptions and behavior

Method MLXMultimodalLM._prepare_audio calls process_audio_input, processed.append, logger.warning; returns processed. No direct raise statement appears in this definition.

View source #L1416-L1425.

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_video · method
vllm_mlx.models.mllm.MLXMultimodalLM._prepare_video(video_input: str | dict, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resolved_path: str | None = None) -> list[str]

Process video input and extract frames.

Parameters

Name Type Required Default Description
video_input str \| dict yes none Video in any supported format
fps float no DEFAULT_FPS Frames per second to extract
max_frames int no MAX_FRAMES Maximum frames to extract
resolved_path str \| None no None Optional pre-resolved local path. Callers that already ran process_video_input (e.g. for parallel audio extraction) pass it here to avoid re-downloading / re-decoding.

Returns

  • Type: list[str]
  • Direct return expressions: save_frames_to_temp(frames)

Exceptions and behavior

Method MLXMultimodalLM._prepare_video calls process_video_input, extract_video_frames_smart, save_frames_to_temp; returns save_frames_to_temp(frames). No direct raise statement appears in this definition.

View source #L1427-L1463.

vllm_mlx.models.mllm.MLXMultimodalLM._collect_video_inputs · method
vllm_mlx.models.mllm.MLXMultimodalLM._collect_video_inputs(messages: list[dict]) -> dict[int, list]

Collect video inputs from messages, keyed by message index.

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.

Returns

  • Type: dict[int, list]
  • Direct return expressions: video_inputs

Exceptions and behavior

Method MLXMultimodalLM._collect_video_inputs calls enumerate, msg.get, isinstance, hasattr; returns video_inputs. No direct raise statement appears in this definition.

View source #L1465-L1497.

vllm_mlx.models.mllm.MLXMultimodalLM._collect_audio_inputs · method
vllm_mlx.models.mllm.MLXMultimodalLM._collect_audio_inputs(messages: list[dict]) -> dict[int, list]

Collect audio inputs from messages, keyed by message index.

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.

Returns

  • Type: dict[int, list]
  • Direct return expressions: audio_inputs

Exceptions and behavior

Method MLXMultimodalLM._collect_audio_inputs calls enumerate, msg.get, isinstance, hasattr; returns audio_inputs. No direct raise statement appears in this definition.

View source #L1499-L1528.

vllm_mlx.models.mllm.MLXMultimodalLM._prepare_native_video_inputs · method
vllm_mlx.models.mllm.MLXMultimodalLM._prepare_native_video_inputs(messages: list[dict], video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list | None = None) -> tuple[str, dict]

Preprocess messages into prompt + generation kwargs for native video.

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.
video_fps float no DEFAULT_FPS Optional positional or keyword input; defaults to DEFAULT_FPS.
video_max_frames int no MAX_FRAMES Optional positional or keyword input; defaults to MAX_FRAMES.
tools list \| None no None Optional positional or keyword input; defaults to None.

Returns

  • Type: tuple[str, dict]
  • Direct return expressions: (text, gen_kwargs)

Exceptions and behavior

Method MLXMultimodalLM._prepare_native_video_inputs calls ImportError, self._translate_messages_for_native_video, self.processor.apply_chat_template, process_vision_info; can raise ImportError; returns (text, gen_kwargs). Directly raised exceptions: ImportError.

View source #L1530-L1648.

vllm_mlx.models.mllm.MLXMultimodalLM._generate_native_video · method
vllm_mlx.models.mllm.MLXMultimodalLM._generate_native_video(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list | None = None, **kwargs) -> MLLMOutput

Generate using native video pipeline (Qwen-family models).

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.
max_tokens int no 256 Optional positional or keyword input; defaults to 256.
temperature float no 0.7 Optional positional or keyword input; defaults to 0.7.
video_fps float no DEFAULT_FPS Optional positional or keyword input; defaults to DEFAULT_FPS.
video_max_frames int no MAX_FRAMES Optional positional or keyword input; defaults to MAX_FRAMES.
tools list \| None no None Optional positional or keyword input; defaults to None.
**kwargs not annotated no none Additional variadic keyword inputs accepted by this callable.

Returns

  • Type: MLLMOutput
  • Direct return expressions: MLLMOutput(text=result.text, finish_reason='stop', prompt_tokens=getattr(result, 'prompt_tokens', 0), completion_tokens…; MLLMOutput(text=str(result), finish_reason='stop')

Exceptions and behavior

Method MLXMultimodalLM._generate_native_video calls ImportError, self._prepare_native_video_inputs, generate, hasattr; can raise ImportError; has 2 explicit return paths. Directly raised exceptions: ImportError.

View source #L1650-L1695.

vllm_mlx.models.mllm.MLXMultimodalLM._translate_messages_for_native_video · method
vllm_mlx.models.mllm.MLXMultimodalLM._translate_messages_for_native_video(messages: list[dict], video_fps: float, video_max_frames: int) -> list[dict]

Translate OpenAI API format messages to process_vision_info format.

Parameters

Name Type Required Default Description
messages list[dict] yes none Required positional or keyword input.
video_fps float yes none Required positional or keyword input.
video_max_frames int yes none Required positional or keyword input.

Returns

  • Type: list[dict]
  • Direct return expressions: translated

Exceptions and behavior

Method MLXMultimodalLM._translate_messages_for_native_video calls msg.get, isinstance, translated.append, str; returns translated. No direct raise statement appears in this definition.

View source #L1697-L1832.

vllm_mlx.models.mllm.MLXMultimodalLM.generate · method
vllm_mlx.models.mllm.MLXMultimodalLM.generate(prompt: str, images: list | None = None, videos: list | None = None, audio: list[str] | None = None, max_tokens: int = 256, temperature: float = 0.7, top_p: float = 0.9, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, use_cache: bool = True, **kwargs) -> MLLMOutput

Generate text from multimodal input.

Parameters

Name Type Required Default Description
prompt str yes none Text prompt/question
images list \| None no None List of image URLs or base64 strings
videos list \| None no None List of video inputs (URLs, base64, or OpenAI format dicts)
audio list[str] \| None no None List of audio file paths
max_tokens int no 256 Maximum tokens to generate
temperature float no 0.7 Sampling temperature
top_p float no 0.9 Top-p sampling parameter
video_fps float no DEFAULT_FPS FPS for video frame extraction (default: 2.0)
video_max_frames int no MAX_FRAMES Max frames to extract from video
use_cache bool no True Whether to use KV cache (default: True)
**kwargs not annotated no none Additional generation parameters

Returns

  • Type: MLLMOutput
  • Direct return expressions: MLLMOutput(text=output_text, finish_reason='stop', prompt_tokens=prompt_tokens, completion_tokens=generation_tokens, **…

Exceptions and behavior

Method MLXMultimodalLM.generate calls self.load, all_images.extend, self._prepare_images, all_sources.extend; returns MLLMOutput(text=output_text, finish_reason='stop', prompt_tokens=prompt_tokens, completion_tokens=generation_tokens, **…. No direct raise statement appears in this definition.

View source #L1834-L2002.

vllm_mlx.models.mllm.MLXMultimodalLM.stream_generate · method
vllm_mlx.models.mllm.MLXMultimodalLM.stream_generate(prompt: str, images: list | None = None, videos: list[str] | None = None, audio: list[str] | None = None, max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, **kwargs) -> Iterator[str]

Stream text generation for multimodal input.

Parameters

Name Type Required Default Description
prompt str yes none Text prompt
images list \| None no None List of image inputs
videos list[str] \| None no None List of video paths
audio list[str] \| None no None List of audio inputs
max_tokens int no 256 Maximum tokens to generate
temperature float no 0.7 Sampling temperature
video_fps float no DEFAULT_FPS FPS for video frame extraction
**kwargs not annotated no none Additional parameters

Returns

  • Type: Iterator[str]
  • Direct return expressions: None
  • Yields values incrementally.

Exceptions and behavior

Method MLXMultimodalLM.stream_generate calls self.load, self.generate, all_images.extend, self._prepare_images; yields values incrementally; returns None. No direct raise statement appears in this definition.

View source #L2004-L2093.

vllm_mlx.models.mllm.MLXMultimodalLM.chat · method
vllm_mlx.models.mllm.MLXMultimodalLM.chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> MLLMOutput

Chat with OpenAI-compatible message format.

Parameters

Name Type Required Default Description
messages list[dict] yes none List of chat messages (OpenAI format)
max_tokens int no 256 Maximum tokens to generate
temperature float no 0.7 Sampling temperature
**kwargs not annotated no none Additional parameters

Returns

  • Type: MLLMOutput
  • Direct return expressions: self._generate_native_video(messages=messages, max_tokens=max_tokens, temperature=temperature, video_fps=video_fps, vid…; MLLMOutput(text=output_text, finish_reason='stop', prompt_tokens=prompt_tokens, completion_tokens=generation_tokens, **…

Exceptions and behavior

Method MLXMultimodalLM.chat calls self.load, logger.info, len, kwargs.pop; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L2095-L2487.

vllm_mlx.models.mllm.MLXMultimodalLM.stream_chat · method
vllm_mlx.models.mllm.MLXMultimodalLM.stream_chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> Iterator[MLLMOutput]

Stream chat with OpenAI-compatible message format.

Parameters

Name Type Required Default Description
messages list[dict] yes none List of chat messages (OpenAI format)
max_tokens int no 256 Maximum tokens to generate
temperature float no 0.7 Sampling temperature
**kwargs not annotated no none Additional parameters

Returns

  • Type: Iterator[MLLMOutput]
  • Direct return expressions: None
  • Yields values incrementally.

Exceptions and behavior

Method MLXMultimodalLM.stream_chat calls self.load, self.chat, kwargs.pop, chat_template_kwargs.pop; yields values incrementally; returns None. No direct raise statement appears in this definition.

View source #L2489-L2737.

vllm_mlx.models.mllm.MLXMultimodalLM.describe_image · method
vllm_mlx.models.mllm.MLXMultimodalLM.describe_image(image: str, prompt: str = 'Describe this image in detail.', max_tokens: int = 512, **kwargs) -> str

Convenience method to describe an image.

Parameters

Name Type Required Default Description
image str yes none Image path, URL, or base64 string
prompt str no 'Describe this image in detail.' Description prompt
max_tokens int no 512 Maximum tokens
**kwargs not annotated no none Additional parameters

Returns

  • Type: str
  • Direct return expressions: output.text

Exceptions and behavior

Method MLXMultimodalLM.describe_image calls self.generate; returns output.text. No direct raise statement appears in this definition.

View source #L2739-L2764.

vllm_mlx.models.mllm.MLXMultimodalLM.answer_about_image · method
vllm_mlx.models.mllm.MLXMultimodalLM.answer_about_image(image: str, question: str, max_tokens: int = 256, **kwargs) -> str

Answer a question about an image.

Parameters

Name Type Required Default Description
image str yes none Image path, URL, or base64 string
question str yes none Question about the image
max_tokens int no 256 Maximum tokens
**kwargs not annotated no none Additional parameters

Returns

  • Type: str
  • Direct return expressions: output.text

Exceptions and behavior

Method MLXMultimodalLM.answer_about_image calls self.generate; returns output.text. No direct raise statement appears in this definition.

View source #L2766-L2791.

vllm_mlx.models.mllm.MLXMultimodalLM.describe_video · method
vllm_mlx.models.mllm.MLXMultimodalLM.describe_video(video: str | dict, prompt: str = 'Describe what happens in this video.', fps: float = 2.0, max_frames: int = 32, max_tokens: int = 512, **kwargs) -> str

Describe a video using frame extraction.

Parameters

Name Type Required Default Description
video str \| dict yes none Video file path, URL, base64, or OpenAI format dict
prompt str no 'Describe what happens in this video.' Description prompt
fps float no 2.0 Frames per second to extract
max_frames int no 32 Maximum frames to extract
max_tokens int no 512 Maximum tokens to generate
**kwargs not annotated no none Additional variadic keyword inputs accepted by this callable.

Returns

  • Type: str
  • Direct return expressions: output.text

Exceptions and behavior

Method MLXMultimodalLM.describe_video calls self.generate; returns output.text. No direct raise statement appears in this definition.

View source #L2793-L2830.

vllm_mlx.models.mllm.MLXMultimodalLM.get_cache_stats · method
vllm_mlx.models.mllm.MLXMultimodalLM.get_cache_stats() -> dict

Get MLLM cache statistics.

Parameters

This callable has no explicit inputs.

Returns

  • Type: dict
  • Direct return expressions: {'enabled': False}; stats

Exceptions and behavior

Method MLXMultimodalLM.get_cache_stats calls self._cache_manager.get_stats, len; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L2832-L2846.

vllm_mlx.models.mllm.MLXMultimodalLM.clear_cache · method
vllm_mlx.models.mllm.MLXMultimodalLM.clear_cache() -> None

Clear the MLLM KV cache.

Parameters

This callable has no explicit inputs.

Returns

  • Type: None

Exceptions and behavior

Method MLXMultimodalLM.clear_cache calls self._cache_manager.clear, logger.info. No direct raise statement appears in this definition.

View source #L2848-L2852.

vllm_mlx.models.mllm.MLXMultimodalLM.get_model_info · method
vllm_mlx.models.mllm.MLXMultimodalLM.get_model_info() -> dict

Get information about the loaded model.

Parameters

This callable has no explicit inputs.

Returns

  • Type: dict
  • Direct return expressions: {'loaded': False, 'model_name': self.model_name}; info

Exceptions and behavior

Method MLXMultimodalLM.get_model_info calls getattr, self._cache_manager.get_stats; has 2 explicit return paths. No direct raise statement appears in this definition.

View source #L2854-L2874.

vllm_mlx.models.mllm.MLXMultimodalLM.list_supported_model_families · method
vllm_mlx.models.mllm.MLXMultimodalLM.list_supported_model_families() -> dict[str, str]

List supported model families and their patterns.

Parameters

This callable has no explicit inputs.

Returns

  • Type: dict[str, str]
  • Direct return expressions: {'Qwen-VL': 'Qwen VL models (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, etc.)', 'LLaVA': 'LLaVA vision-language models', 'Idefics'…

Exceptions and behavior

Method MLXMultimodalLM.list_supported_model_families returns {'Qwen-VL': 'Qwen VL models (Qwen2-VL, Qwen2.5-VL, Qwen3-VL, etc.)', 'LLaVA': 'LLaVA vision-language models', 'Idefics'…. No direct raise statement appears in this definition.

View source #L2877-L2897.

vllm_mlx.models.mllm.MLXMultimodalLM.is_mllm_model · method
vllm_mlx.models.mllm.MLXMultimodalLM.is_mllm_model(model_name: str) -> bool

Check if a model name indicates an MLLM model.

Parameters

Name Type Required Default Description
model_name str yes none Required positional or keyword input.

Returns

  • Type: bool
  • Direct return expressions: any((pattern.lower() in model_lower for pattern in mllm_patterns))

Exceptions and behavior

Method MLXMultimodalLM.is_mllm_model calls model_name.lower, any, pattern.lower; returns any((pattern.lower() in model_lower for pattern in mllm_patterns)). No direct raise statement appears in this definition.

View source #L2900-L2934.

vllm_mlx.models.mllm.MLXMultimodalLM.__repr__ · method
vllm_mlx.models.mllm.MLXMultimodalLM.__repr__() -> str

Method MLXMultimodalLM.__repr__ returns f'<MLXMultimodalLM model={self.model_name} status={status}>'.

Parameters

This callable has no explicit inputs.

Returns

  • Type: str
  • Direct return expressions: f'<MLXMultimodalLM model={self.model_name} status={status}>'

Exceptions and behavior

Method MLXMultimodalLM.__repr__ returns f'<MLXMultimodalLM model={self.model_name} status={status}>'. No direct raise statement appears in this definition.

View source #L2936-L2938.

Complete symbol map

This map also includes private definitions and nested helpers. The signature column exposes every explicit input even when an internal helper has no dedicated parameter prose.

Symbol Kind Signature and inputs What it does Source
TempFileManager class TempFileManager() Thread-safe manager for tracking and cleaning up temporary files. #L41-L86
TempFileManager.__init__ method TempFileManager.__init__() -> not annotated Method TempFileManager.__init__ updates self._files, self._lock; calls set, threading.Lock, atexit.register. #L44-L47
TempFileManager.register method TempFileManager.register(path: str) -> str Register a temp file for tracking. #L49-L53
TempFileManager.cleanup method TempFileManager.cleanup(path: str) -> bool Clean up a specific temp file. #L55-L67
TempFileManager.cleanup_all method TempFileManager.cleanup_all() -> int Clean up all tracked temp files. #L69-L86
cleanup_temp_file function cleanup_temp_file(path: str) -> bool Clean up a specific temporary file. #L93-L95
cleanup_all_temp_files function cleanup_all_temp_files() -> int Clean up all tracked temporary files. #L98-L100
FileSizeExceededError class FileSizeExceededError() Raised when a downloaded file exceeds the size limit. #L119-L122
UnsafeRemoteURLError class UnsafeRemoteURLError(message: str, *, public_message: str = 'Remote media URL is not allowed') Raised when a remote media URL targets an unsafe destination. #L125-L135
UnsafeRemoteURLError.__init__ method UnsafeRemoteURLError.__init__(message: str, *, public_message: str = 'Remote media URL is not allowed') -> None Method UnsafeRemoteURLError.__init__ updates self.public_message; calls super().__init__, super. #L128-L135
_normalize_content_part function _normalize_content_part(item: object) -> object Convert Pydantic content parts into plain Python objects. #L138-L144
_extract_media_url function _extract_media_url(item: dict, item_type: str) -> str Function _extract_media_url calls item.get, isinstance, media_value.get; has 2 explicit return paths. #L147-L161
_text_content_part function _text_content_part(text: str) -> dict[str, str] Function _text_content_part returns {'type': 'text', 'text': text, 'content': text}. #L164-L165
_append_text_content_part function _append_text_content_part(built_parts: list[dict[str, str]], text_parts: list[str], text: str) -> None Function _append_text_content_part calls built_parts.append, _text_content_part, text_parts.append; returns None. #L168-L174
_build_string_mllm_message_content function _build_string_mllm_message_content(content: str, role: str) -> tuple[object, bool] Function _build_string_mllm_message_content calls _text_content_part; has 3 explicit return paths. #L177-L182
_append_ordered_mllm_content_part function _append_ordered_mllm_content_part(raw_item: object, *, built_parts: list[dict[str, str]], text_parts: list[str], all_image_urls: list[str], video_frame_count: int) -> int Function _append_ordered_mllm_content_part calls _normalize_content_part, isinstance, _append_text_content_part, item.get; has 2 explicit return paths. #L185-L220
_build_ordered_mllm_message_content function _build_ordered_mllm_message_content(content: object, *, role: str, all_image_urls: list[str], video_frame_count: int = 0) -> tuple[object, bool] Build template content while preserving OpenAI media/text part order. #L223-L254
_normalize_mllm_tool_calls function _normalize_mllm_tool_calls(tool_calls: list) -> list Normalize replayed assistant tool calls for chat templates. #L257-L268
_build_mllm_chat_messages function _build_mllm_chat_messages(messages: list[dict], *, all_image_urls: list[str], video_frame_counts: dict[int, int]) -> list[dict] Build chat-template messages without reordering multimodal content parts. #L271-L315
MultimodalInput class MultimodalInput(prompt: str, images: list[str] = field(default_factory=list), videos: list[str] = field(default_factory=list), audio: list[str] = field(default_factory=list)) Input for multimodal generation. #L319-L325
MLLMOutput class MLLMOutput(text: str, finish_reason: str \| None = None, prompt_tokens: int = 0, completion_tokens: int = 0, mtp_drafts: int = 0, mtp_accepted: int = 0) Output from multimodal language model. #L329-L337
load_gemma4_assistant_drafter function load_gemma4_assistant_drafter(model_path: str) -> not annotated Load a Gemma 4 assistant drafter for mlx-vlm speculative decoding. #L340-L381
_count_draft_tokens function _count_draft_tokens(draft_tokens) -> int Best-effort drafted-token count for an mlx-vlm drafter output. #L387-L398
_install_draft_metrics_hooks function _install_draft_metrics_hooks(draft_model) -> None Record actual drafted token counts from mlx-vlm assistant drafters. #L401-L428
_install_draft_metrics_hooks.draft_block_with_metrics nested function _install_draft_metrics_hooks.draft_block_with_metrics(*args, **kwargs) -> not annotated Nested Function _install_draft_metrics_hooks.draft_block_with_metrics calls draft_block, draft_model._vllm_mlx_draft_counts.append, _count_draft_tokens; returns draft_tokens. #L412-L415
_install_draft_metrics_hooks.reset_with_metrics nested function _install_draft_metrics_hooks.reset_with_metrics(*args, **kwargs) -> not annotated Nested Function _install_draft_metrics_hooks.reset_with_metrics calls reset; returns reset(*args, **kwargs). #L422-L424
is_base64_image function is_base64_image(s: str) -> bool Check if string is base64-encoded image data. #L431-L435
is_url function is_url(s: str) -> bool Check if string is a URL. #L438-L440
is_base64_video function is_base64_video(s: str) -> bool Check if string is base64-encoded video data. #L443-L445
is_base64_audio function is_base64_audio(s: str) -> bool Check if string is base64-encoded audio data. #L448-L450
decode_base64_image function decode_base64_image(base64_string: str, max_length: int = MAX_BASE64_IMAGE_LENGTH) -> bytes Decode base64 image to bytes. #L453-L480
_validate_url_safety function _validate_url_safety(url: str) -> None Reject remote URLs that target local or private network resources. #L483-L519
_request_with_safe_redirects function _request_with_safe_redirects(method: str, url: str, *, timeout: int, headers: dict[str, str], stream: bool = False, max_redirects: int = 5) -> not annotated Issue a requests call while validating every redirect target. #L522-L557
download_image function download_image(url: str, timeout: int = 30, max_size: int = MAX_IMAGE_SIZE) -> str Download image from URL and return local path. #L560-L645
_download_media function _download_media(url: str, media_type: str, ext_map: dict[str, str], default_ext: str, timeout: int, max_size: int) -> str Download media from URL, enforce size limits, and return a local temp path. #L670-L748
download_video function download_video(url: str, timeout: int = 120, max_size: int = MAX_VIDEO_SIZE) -> str Download video from URL and return local path. #L751-L753
download_audio function download_audio(url: str, timeout: int = 120, max_size: int = MAX_AUDIO_SIZE) -> str Download audio from URL and return local path. #L756-L758
decode_base64_video function decode_base64_video(base64_string: str, max_length: int = MAX_BASE64_VIDEO_LENGTH) -> str Decode base64 video to temp file and return path. #L761-L807
decode_base64_audio function decode_base64_audio(base64_string: str, max_length: int = MAX_BASE64_AUDIO_LENGTH) -> str Decode base64 audio to temp file and return path. #L810-L836
process_video_input function process_video_input(video: str \| dict) -> str Process video input in various formats and return local path. #L839-L874
process_audio_input function process_audio_input(audio: str \| dict) -> str Process audio input in various formats and return local path. #L877-L905
_video_has_audio_track function _video_has_audio_track(video_path: str) -> bool Return True if ffprobe finds an audio stream in the video. #L908-L935
_model_has_sound_encoder function _model_has_sound_encoder(model) -> bool Whether a loaded model exposes a usable sound encoder. #L938-L947
extract_audio_from_video function extract_audio_from_video(video_path: str) -> str \| None Extract the audio track from a video file as 16 kHz mono WAV. #L950-L1005
save_base64_image function save_base64_image(base64_string: str) -> str Save base64 image to temp file and return path. #L1012-L1048
process_image_input function process_image_input(image: str \| dict) -> str Process image input in various formats and return local path. #L1051-L1080
round_by_factor function round_by_factor(x: int, factor: int) -> int Round to nearest multiple of factor. #L1083-L1085
ceil_by_factor function ceil_by_factor(x: float, factor: int) -> int Ceiling to next multiple of factor. #L1088-L1090
floor_by_factor function floor_by_factor(x: float, factor: int) -> int Floor to previous multiple of factor. #L1093-L1095
smart_nframes function smart_nframes(total_frames: int, video_fps: float, target_fps: float = DEFAULT_FPS, min_frames: int = MIN_FRAMES, max_frames: int = MAX_FRAMES) -> int Calculate optimal number of frames to extract from video. #L1098-L1120
extract_video_frames_smart function extract_video_frames_smart(video_path: str, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resize: tuple[int, int] \| None = None) -> list[np.ndarray] Extract frames from video with smart sampling. #L1123-L1187
save_frames_to_temp function save_frames_to_temp(frames: list[np.ndarray]) -> list[str] Save frame arrays to temporary files and return paths. #L1190-L1204
MLXMultimodalLM class MLXMultimodalLM(model_name: str, trust_remote_code: bool = False, enable_cache: bool = True, cache_size: int = 50, max_kv_size: int = 0, draft_model: str \| None = None, draft_kind: str \| None = None, draft_block_size: int \| None = None) Wrapper around mlx-vlm for multimodal inference. #L1207-L2938
MLXMultimodalLM.__init__ method MLXMultimodalLM.__init__(model_name: str, trust_remote_code: bool = False, enable_cache: bool = True, cache_size: int = 50, max_kv_size: int = 0, draft_model: str \| None = None, draft_kind: str \| None = None, draft_block_size: int \| None = None) -> not annotated Initialize the MLX multimodal language model. #L1235-L1278
MLXMultimodalLM.load method MLXMultimodalLM.load() -> None Load the model and processor. #L1280-L1323
MLXMultimodalLM._load_draft_model method MLXMultimodalLM._load_draft_model() -> not annotated Method MLXMultimodalLM._load_draft_model calls load_gemma4_assistant_drafter, load; has 2 explicit return paths. #L1325-L1332
MLXMultimodalLM._draft_generation_kwargs method MLXMultimodalLM._draft_generation_kwargs(call_kwargs: dict \| None = None) -> dict Return mlx-vlm drafter kwargs when the request explicitly opts in. #L1334-L1355
MLXMultimodalLM._reset_draft_metrics method MLXMultimodalLM._reset_draft_metrics() -> int Method MLXMultimodalLM._reset_draft_metrics updates self._draft_model.accept_lens, self._draft_model._vllm_mlx_draft_counts; calls hasattr; returns 0. #L1357-L1364
MLXMultimodalLM._draft_metrics_since method MLXMultimodalLM._draft_metrics_since(start_accept_lens: int) -> dict[str, int] Method MLXMultimodalLM._draft_metrics_since calls list, getattr, len, int; has 2 explicit return paths. #L1366-L1395
MLXMultimodalLM.get_language_model method MLXMultimodalLM.get_language_model() -> not annotated Extract the underlying language model for mlx_lm TextModel construction. #L1397-L1399
MLXMultimodalLM.get_tokenizer method MLXMultimodalLM.get_tokenizer() -> not annotated Get the text tokenizer (not the multimodal processor). #L1401-L1403
MLXMultimodalLM._prepare_images method MLXMultimodalLM._prepare_images(images: list) -> list[str] Process remote/base64 image inputs into local temp file paths. #L1405-L1414
MLXMultimodalLM._prepare_audio method MLXMultimodalLM._prepare_audio(audio_inputs: list) -> list[str] Process audio inputs and return local file paths. #L1416-L1425
MLXMultimodalLM._prepare_video method MLXMultimodalLM._prepare_video(video_input: str \| dict, fps: float = DEFAULT_FPS, max_frames: int = MAX_FRAMES, resolved_path: str \| None = None) -> list[str] Process video input and extract frames. #L1427-L1463
MLXMultimodalLM._collect_video_inputs method MLXMultimodalLM._collect_video_inputs(messages: list[dict]) -> dict[int, list] Collect video inputs from messages, keyed by message index. #L1465-L1497
MLXMultimodalLM._collect_audio_inputs method MLXMultimodalLM._collect_audio_inputs(messages: list[dict]) -> dict[int, list] Collect audio inputs from messages, keyed by message index. #L1499-L1528
MLXMultimodalLM._prepare_native_video_inputs method MLXMultimodalLM._prepare_native_video_inputs(messages: list[dict], video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list \| None = None) -> tuple[str, dict] Preprocess messages into prompt + generation kwargs for native video. #L1530-L1648
MLXMultimodalLM._generate_native_video method MLXMultimodalLM._generate_native_video(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, tools: list \| None = None, **kwargs) -> MLLMOutput Generate using native video pipeline (Qwen-family models). #L1650-L1695
MLXMultimodalLM._translate_messages_for_native_video method MLXMultimodalLM._translate_messages_for_native_video(messages: list[dict], video_fps: float, video_max_frames: int) -> list[dict] Translate OpenAI API format messages to process_vision_info format. #L1697-L1832
MLXMultimodalLM.generate method MLXMultimodalLM.generate(prompt: str, images: list \| None = None, videos: list \| None = None, audio: list[str] \| None = None, max_tokens: int = 256, temperature: float = 0.7, top_p: float = 0.9, video_fps: float = DEFAULT_FPS, video_max_frames: int = MAX_FRAMES, use_cache: bool = True, **kwargs) -> MLLMOutput Generate text from multimodal input. #L1834-L2002
MLXMultimodalLM.stream_generate method MLXMultimodalLM.stream_generate(prompt: str, images: list \| None = None, videos: list[str] \| None = None, audio: list[str] \| None = None, max_tokens: int = 256, temperature: float = 0.7, video_fps: float = DEFAULT_FPS, **kwargs) -> Iterator[str] Stream text generation for multimodal input. #L2004-L2093
MLXMultimodalLM.chat method MLXMultimodalLM.chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> MLLMOutput Chat with OpenAI-compatible message format. #L2095-L2487
MLXMultimodalLM.stream_chat method MLXMultimodalLM.stream_chat(messages: list[dict], max_tokens: int = 256, temperature: float = 0.7, **kwargs) -> Iterator[MLLMOutput] Stream chat with OpenAI-compatible message format. #L2489-L2737
MLXMultimodalLM.describe_image method MLXMultimodalLM.describe_image(image: str, prompt: str = 'Describe this image in detail.', max_tokens: int = 512, **kwargs) -> str Convenience method to describe an image. #L2739-L2764
MLXMultimodalLM.answer_about_image method MLXMultimodalLM.answer_about_image(image: str, question: str, max_tokens: int = 256, **kwargs) -> str Answer a question about an image. #L2766-L2791
MLXMultimodalLM.describe_video method MLXMultimodalLM.describe_video(video: str \| dict, prompt: str = 'Describe what happens in this video.', fps: float = 2.0, max_frames: int = 32, max_tokens: int = 512, **kwargs) -> str Describe a video using frame extraction. #L2793-L2830
MLXMultimodalLM.get_cache_stats method MLXMultimodalLM.get_cache_stats() -> dict Get MLLM cache statistics. #L2832-L2846
MLXMultimodalLM.clear_cache method MLXMultimodalLM.clear_cache() -> None Clear the MLLM KV cache. #L2848-L2852
MLXMultimodalLM.get_model_info method MLXMultimodalLM.get_model_info() -> dict Get information about the loaded model. #L2854-L2874
MLXMultimodalLM.list_supported_model_families method MLXMultimodalLM.list_supported_model_families() -> dict[str, str] List supported model families and their patterns. #L2877-L2897
MLXMultimodalLM.is_mllm_model method MLXMultimodalLM.is_mllm_model(model_name: str) -> bool Check if a model name indicates an MLLM model. #L2900-L2934
MLXMultimodalLM.__repr__ method MLXMultimodalLM.__repr__() -> str Method MLXMultimodalLM.__repr__ returns f'<MLXMultimodalLM model={self.model_name} status={status}>'. #L2936-L2938