← back to Exo
Add DeepSeek V4 Flash/Pro (#1978)
37f6f4f6c22f28ae8cc97702e7d5a2f750d5b5af · 2026-04-27 15:20:50 +0100 · rltakashige
Wait for upstream merge.
---------
Co-authored-by: Evan <evanev7@gmail.com>
Files touched
M .mlx_typings/mlx_lm/generate.pyiA .mlx_typings/mlx_lm/models/deepseek_v4.pyiM bench/exo_bench.pyM dashboard/src/lib/stores/app.svelte.tsM dashboard/src/routes/integrations/+page.svelteM pyproject.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.tomlM resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.tomlA resources/inference_model_cards/mlx-community--DeepSeek-V4-Flash.tomlA resources/inference_model_cards/mlx-community--DeepSeek-V4-Pro.tomlM resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-4bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-6bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.tomlM resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-5-8bit.tomlM resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--GLM-5-bf16.tomlM resources/inference_model_cards/mlx-community--GLM-5.1-DQ4plus-q8.tomlM resources/inference_model_cards/mlx-community--GLM-5.1-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--GLM-5.1.tomlM resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.tomlM resources/inference_model_cards/mlx-community--Kimi-K2.5.tomlM resources/inference_model_cards/mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit-mxfp4.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7-5bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7-6bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7-8bit.tomlM resources/inference_model_cards/mlx-community--MiniMax-M2.7.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-27B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-27B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-27B-bf16.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-4bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-5bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-8bit.tomlM resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-bf16.tomlM resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.tomlM resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.tomlM src/exo/api/adapters/chat_completions.pyM src/exo/api/main.pyM src/exo/api/types/api.pyM src/exo/master/placement.pyM src/exo/shared/models/model_cards.pyM src/exo/shared/types/mlx.pyM src/exo/shared/types/text_generation.pyM src/exo/worker/engines/mlx/auto_parallel.pyM src/exo/worker/engines/mlx/cache.pyM src/exo/worker/engines/mlx/generator/generate.pyM src/exo/worker/engines/mlx/patches/opt_batch_gen.pyM src/exo/worker/engines/mlx/utils_mlx.pyA src/exo/worker/engines/mlx/vendor/__init__.pyA src/exo/worker/engines/mlx/vendor/deepseek_v4_encoding.pyR091 src/exo/worker/engines/mlx/dsml_encoding.py src/exo/worker/engines/mlx/vendor/dsml_encoding.pyM src/exo/worker/runner/llm_inference/model_output_parsers.pyM src/exo/worker/tests/unittests/test_mlx/test_prefix_cache_architectures.pyM src/exo/worker/tests/unittests/test_mlx/test_tp_bit_exact.pyM src/exo/worker/tests/unittests/test_runner/test_dsml_e2e.pyM src/exo/worker/tests/unittests/test_runner/test_finish_reason_sse.pyM uv.lock
Diff
commit 37f6f4f6c22f28ae8cc97702e7d5a2f750d5b5af
Author: rltakashige <rl.takashige@gmail.com>
Date: Mon Apr 27 15:20:50 2026 +0100
Add DeepSeek V4 Flash/Pro (#1978)
Wait for upstream merge.
---------
Co-authored-by: Evan <evanev7@gmail.com>
---
.mlx_typings/mlx_lm/generate.pyi | 9 +-
.mlx_typings/mlx_lm/models/deepseek_v4.pyi | 280 +++++++
bench/exo_bench.py | 60 +-
dashboard/src/lib/stores/app.svelte.ts | 46 +-
dashboard/src/routes/integrations/+page.svelte | 34 +-
pyproject.toml | 15 +-
.../mlx-community--DeepSeek-V3.1-4bit.toml | 1 +
.../mlx-community--DeepSeek-V3.1-8bit.toml | 1 +
.../mlx-community--DeepSeek-V3.2-4bit.toml | 1 +
.../mlx-community--DeepSeek-V3.2-8bit.toml | 1 +
.../mlx-community--DeepSeek-V4-Flash.toml | 21 +
.../mlx-community--DeepSeek-V4-Pro.toml | 21 +
.../mlx-community--GLM-4.5-Air-8bit.toml | 2 +-
.../mlx-community--GLM-4.5-Air-bf16.toml | 2 +-
.../mlx-community--GLM-4.7-4bit.toml | 2 +-
.../mlx-community--GLM-4.7-6bit.toml | 2 +-
.../mlx-community--GLM-4.7-8bit-gs32.toml | 2 +-
.../mlx-community--GLM-4.7-Flash-4bit.toml | 2 +-
.../mlx-community--GLM-4.7-Flash-5bit.toml | 2 +-
.../mlx-community--GLM-4.7-Flash-6bit.toml | 2 +-
.../mlx-community--GLM-4.7-Flash-8bit.toml | 2 +-
.../mlx-community--GLM-5-8bit.toml | 2 +-
.../mlx-community--GLM-5-MXFP4-Q8.toml | 2 +-
.../mlx-community--GLM-5-bf16.toml | 2 +-
.../mlx-community--GLM-5.1-DQ4plus-q8.toml | 2 +-
.../mlx-community--GLM-5.1-MXFP4-Q8.toml | 2 +-
.../mlx-community--GLM-5.1.toml | 2 +-
.../mlx-community--Kimi-K2-Thinking.toml | 2 +-
.../mlx-community--Kimi-K2.5.toml | 2 +-
.../mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.toml | 2 +-
.../mlx-community--MiniMax-M2.1-3bit.toml | 2 +-
.../mlx-community--MiniMax-M2.1-8bit.toml | 2 +-
.../mlx-community--MiniMax-M2.5-4bit.toml | 2 +-
.../mlx-community--MiniMax-M2.5-6bit.toml | 2 +-
.../mlx-community--MiniMax-M2.5-8bit.toml | 2 +-
.../mlx-community--MiniMax-M2.7-4bit-mxfp4.toml | 2 +-
.../mlx-community--MiniMax-M2.7-4bit.toml | 2 +-
.../mlx-community--MiniMax-M2.7-5bit.toml | 2 +-
.../mlx-community--MiniMax-M2.7-6bit.toml | 2 +-
.../mlx-community--MiniMax-M2.7-8bit.toml | 2 +-
.../mlx-community--MiniMax-M2.7.toml | 2 +-
...ommunity--Qwen3-Next-80B-A3B-Thinking-4bit.toml | 2 +-
...ommunity--Qwen3-Next-80B-A3B-Thinking-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-122B-A10B-4bit.toml | 2 +-
.../mlx-community--Qwen3.5-122B-A10B-6bit.toml | 2 +-
.../mlx-community--Qwen3.5-122B-A10B-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-122B-A10B-bf16.toml | 2 +-
.../mlx-community--Qwen3.5-27B-4bit.toml | 2 +-
.../mlx-community--Qwen3.5-27B-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-2B-MLX-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-35B-A3B-4bit.toml | 2 +-
.../mlx-community--Qwen3.5-35B-A3B-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-397B-A17B-4bit.toml | 2 +-
.../mlx-community--Qwen3.5-397B-A17B-6bit.toml | 2 +-
.../mlx-community--Qwen3.5-397B-A17B-8bit.toml | 2 +-
.../mlx-community--Qwen3.5-9B-4bit.toml | 2 +-
.../mlx-community--Qwen3.5-9B-8bit.toml | 2 +-
.../mlx-community--Qwen3.6-27B-4bit.toml | 2 +-
.../mlx-community--Qwen3.6-27B-8bit.toml | 2 +-
.../mlx-community--Qwen3.6-27B-bf16.toml | 2 +-
.../mlx-community--Qwen3.6-35B-A3B-4bit.toml | 2 +-
.../mlx-community--Qwen3.6-35B-A3B-5bit.toml | 2 +-
.../mlx-community--Qwen3.6-35B-A3B-8bit.toml | 2 +-
.../mlx-community--Qwen3.6-35B-A3B-bf16.toml | 2 +-
.../mlx-community--gpt-oss-120b-MXFP4-Q8.toml | 2 +-
.../mlx-community--gpt-oss-20b-MXFP4-Q8.toml | 2 +-
src/exo/api/adapters/chat_completions.py | 10 +-
src/exo/api/main.py | 1 +
src/exo/api/types/api.py | 3 +-
src/exo/master/placement.py | 6 +-
src/exo/shared/models/model_cards.py | 3 +
src/exo/shared/types/mlx.py | 8 +-
src/exo/shared/types/text_generation.py | 17 +
src/exo/worker/engines/mlx/auto_parallel.py | 200 ++++-
src/exo/worker/engines/mlx/cache.py | 156 +++-
src/exo/worker/engines/mlx/generator/generate.py | 16 +-
.../worker/engines/mlx/patches/opt_batch_gen.py | 21 +-
src/exo/worker/engines/mlx/utils_mlx.py | 72 +-
src/exo/worker/engines/mlx/vendor/__init__.py | 0
.../engines/mlx/vendor/deepseek_v4_encoding.py | 836 +++++++++++++++++++++
.../engines/mlx/{ => vendor}/dsml_encoding.py | 8 +-
.../runner/llm_inference/model_output_parsers.py | 84 ++-
.../test_mlx/test_prefix_cache_architectures.py | 7 +
.../tests/unittests/test_mlx/test_tp_bit_exact.py | 36 +
.../tests/unittests/test_runner/test_dsml_e2e.py | 130 +++-
.../test_runner/test_finish_reason_sse.py | 2 +-
uv.lock | 130 ++--
87 files changed, 2113 insertions(+), 231 deletions(-)
diff --git a/.mlx_typings/mlx_lm/generate.pyi b/.mlx_typings/mlx_lm/generate.pyi
index aa38c027..22ddc558 100644
--- a/.mlx_typings/mlx_lm/generate.pyi
+++ b/.mlx_typings/mlx_lm/generate.pyi
@@ -383,11 +383,12 @@ class GenerationBatch:
state_machines: List[SequenceStateMachine]
max_tokens: List[int]
_current_tokens: Optional[mx.array]
- _current_logprobs: List[mx.array]
- _next_tokens: mx.array
- _next_logprobs: List[mx.array]
- _token_context: List[mx.array]
+ _current_logprobs: mx.array | List[mx.array]
+ _next_tokens: Optional[mx.array]
+ _next_logprobs: mx.array | List[mx.array]
+ _token_context: List[Any]
_num_tokens: List[int]
+ _matcher_states: List[Any]
def __init__(
self,
diff --git a/.mlx_typings/mlx_lm/models/deepseek_v4.pyi b/.mlx_typings/mlx_lm/models/deepseek_v4.pyi
new file mode 100644
index 00000000..0f753826
--- /dev/null
+++ b/.mlx_typings/mlx_lm/models/deepseek_v4.pyi
@@ -0,0 +1,280 @@
+"""Type stubs for mlx_lm.models.deepseek_v4"""
+
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional
+
+import mlx.core as mx
+import mlx.nn as nn
+
+from .base import BaseModelArgs
+from .cache import ArraysCache, RotatingKVCache
+from .switch_layers import SwitchGLU
+
+@dataclass
+class ModelArgs(BaseModelArgs):
+ model_type: str
+ vocab_size: int
+ hidden_size: int
+ intermediate_size: int
+ moe_intermediate_size: int
+ num_hidden_layers: int
+ num_attention_heads: int
+ num_key_value_heads: int
+ n_shared_experts: Optional[int]
+ n_routed_experts: int
+ num_experts_per_tok: int
+ head_dim: int
+ qk_rope_head_dim: int
+ q_lora_rank: int
+ o_lora_rank: int
+ o_groups: int
+ sliding_window: int
+ hc_mult: int
+ hc_sinkhorn_iters: int
+ hc_eps: float
+ compress_ratios: Optional[List[int]]
+ compress_rope_theta: float
+ rope_theta: float
+ rope_scaling: Optional[Dict[str, Any]]
+ rms_norm_eps: float
+ swiglu_limit: float
+ attention_bias: bool
+ max_position_embeddings: int
+
+class DeepseekV4RoPE(nn.Module):
+ dims: int
+ freqs: mx.array
+
+ def __init__(
+ self,
+ dims: int,
+ base: float,
+ scaling_config: Optional[Dict[str, Any]] = None,
+ ) -> None: ...
+ def __call__(
+ self,
+ x: mx.array,
+ offset: int = 0,
+ inverse: bool = False,
+ ) -> mx.array: ...
+
+class HyperConnection(nn.Module):
+ dim: int
+ hc_mult: int
+ norm_eps: float
+
+ def __init__(
+ self,
+ dim: int,
+ hc_mult: int,
+ norm_eps: float,
+ sinkhorn_iters: int,
+ hc_eps: float,
+ ) -> None: ...
+
+class HyperHead(nn.Module):
+ dim: int
+ hc_mult: int
+
+ def __init__(
+ self,
+ dim: int,
+ hc_mult: int,
+ norm_eps: float,
+ hc_eps: float,
+ ) -> None: ...
+ def __call__(self, x: mx.array) -> mx.array: ...
+
+class Compressor(nn.Module):
+ dim: int
+ head_dim: int
+ rope_head_dim: int
+ compress_ratio: int
+ overlap: bool
+ wkv_gate: nn.Linear
+ ape: mx.array
+ norm: nn.RMSNorm
+ rope: DeepseekV4RoPE
+
+ def __init__(
+ self,
+ dim: int,
+ compress_ratio: int,
+ head_dim: int,
+ rope_head_dim: int,
+ rms_norm_eps: float,
+ rope: DeepseekV4RoPE,
+ ) -> None: ...
+ def __call__(
+ self,
+ x: mx.array,
+ cache: "DeepseekV4Cache",
+ offset: Any,
+ key: str = ...,
+ ) -> mx.array: ...
+
+class Indexer(nn.Module):
+ def __init__(
+ self,
+ args: ModelArgs,
+ compress_ratio: int,
+ rope: DeepseekV4RoPE,
+ ) -> None: ...
+
+class _CompressorBranch:
+ buffer_kv: Optional[mx.array]
+ buffer_gate: Optional[mx.array]
+ prev_kv: Optional[mx.array]
+ prev_gate: Optional[mx.array]
+ pool: Optional[mx.array]
+ buffer_lengths: Optional[List[int]]
+ pool_lengths: Optional[List[int]]
+ buffer_count: int
+ _new_pool_lengths: Optional[List[int]]
+
+ def __init__(self) -> None: ...
+
+class DeepseekV4Cache:
+ local: RotatingKVCache
+ offset: int
+ keys: Optional[mx.array]
+ values: Optional[mx.array]
+ state: Any
+ meta_state: Any
+ nbytes: int
+ _branches: Dict[str, _CompressorBranch]
+ _pending_lengths: Optional[List[int]]
+
+ def __init__(self, sliding_window: int) -> None: ...
+ def update_and_fetch(
+ self, keys: mx.array, values: mx.array
+ ) -> tuple[mx.array, mx.array]: ...
+ def is_trimmable(self) -> bool: ...
+ def trim(self, n: int) -> int: ...
+ def empty(self) -> bool: ...
+ def size(self) -> int: ...
+ def prepare(
+ self,
+ *,
+ left_padding: Optional[List[int]] = None,
+ lengths: Optional[List[int]] = None,
+ right_padding: Optional[List[int]] = None,
+ ) -> None: ...
+ def finalize(self) -> None: ...
+ def filter(self, batch_indices: mx.array) -> None: ...
+ def extend(self, other: "DeepseekV4Cache") -> None: ...
+ def extract(self, idx: int) -> "DeepseekV4Cache": ...
+ @classmethod
+ def merge(cls, caches: List["DeepseekV4Cache"]) -> "DeepseekV4Cache": ...
+
+class V4Attention(nn.Module):
+ args: ModelArgs
+ layer_id: int
+ dim: int
+ n_heads: int
+ head_dim: int
+ rope_head_dim: int
+ nope_head_dim: int
+ n_groups: int
+ q_lora_rank: int
+ o_lora_rank: int
+ window: int
+ eps: float
+ scale: float
+ compress_ratio: int
+ wqkv_a: nn.Linear
+ q_norm: nn.RMSNorm
+ wq_b: nn.Linear
+ kv_norm: nn.RMSNorm
+ attn_sink: mx.array
+ wo_a: nn.Linear
+ wo_b: nn.Linear
+ rope: DeepseekV4RoPE
+ compressor: Compressor
+ indexer: Indexer
+
+ def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
+ def __call__(
+ self,
+ x: mx.array,
+ mask: Optional[mx.array] = None,
+ cache: Optional[Any] = None,
+ ) -> mx.array: ...
+
+class DeepseekV4MLP(nn.Module):
+ gate_proj: nn.Linear
+ up_proj: nn.Linear
+ down_proj: nn.Linear
+
+ def __init__(
+ self,
+ hidden_size: int,
+ intermediate_size: int,
+ swiglu_limit: float = 0.0,
+ ) -> None: ...
+ def __call__(self, x: mx.array) -> mx.array: ...
+
+class MoEGate(nn.Module):
+ weight: mx.array
+
+ def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
+ def __call__(
+ self, x: mx.array, input_ids: mx.array
+ ) -> tuple[mx.array, mx.array]: ...
+
+class DeepseekV4MoE(nn.Module):
+ num_experts_per_tok: int
+ switch_mlp: SwitchGLU
+ gate: MoEGate
+ shared_experts: DeepseekV4MLP
+
+ def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
+ def __call__(self, x: mx.array, input_ids: mx.array) -> mx.array: ...
+
+class DeepseekV4Block(nn.Module):
+ attn_norm: nn.RMSNorm
+ attn: V4Attention
+ hc_attn: HyperConnection
+ ffn_norm: nn.RMSNorm
+ ffn: DeepseekV4MoE
+ hc_ffn: HyperConnection
+
+ def __init__(self, args: ModelArgs, layer_id: int) -> None: ...
+ def __call__(
+ self,
+ h: mx.array,
+ cache: Optional[Any],
+ input_ids: mx.array,
+ ) -> mx.array: ...
+
+class DeepseekV4Model(nn.Module):
+ args: ModelArgs
+ vocab_size: int
+ embed_tokens: nn.Embedding
+ layers: list[DeepseekV4Block]
+ norm: nn.RMSNorm
+ hc_head: HyperHead
+
+ def __init__(self, args: ModelArgs) -> None: ...
+ def __call__(
+ self,
+ inputs: mx.array,
+ cache: Optional[List[Any]] = None,
+ ) -> mx.array: ...
+
+class Model(nn.Module):
+ args: ModelArgs
+ model_type: str
+ model: DeepseekV4Model
+ lm_head: nn.Linear
+
+ def __init__(self, args: ModelArgs) -> None: ...
+ def __call__(
+ self,
+ inputs: mx.array,
+ cache: Optional[List[Any]] = None,
+ ) -> mx.array: ...
+ def sanitize(self, weights: dict[str, Any]) -> dict[str, Any]: ...
+ def make_cache(self) -> list[RotatingKVCache | DeepseekV4Cache]: ...
+ @property
+ def layers(self) -> list[DeepseekV4Block]: ...
diff --git a/bench/exo_bench.py b/bench/exo_bench.py
index b64b4c2a..e2248f72 100644
--- a/bench/exo_bench.py
+++ b/bench/exo_bench.py
@@ -122,8 +122,48 @@ def load_tokenizer_for_bench(model_id: str) -> Any:
return hf_tokenizer
- # Default: use AutoTokenizer
- return AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
+ # TODO: Change back to using only transformers
+ try:
+ return AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
+ except (AttributeError, ValueError):
+ from huggingface_hub import snapshot_download
+ from transformers import PretrainedConfig
+
+ model_path = Path(
+ snapshot_download(
+ model_id,
+ allow_patterns=[
+ "*.json",
+ "*.py",
+ "tokenizer.model",
+ "*.tiktoken",
+ "tiktoken.model",
+ "*.txt",
+ "*.jsonl",
+ "*.jinja",
+ ],
+ )
+ )
+ stub_kwargs: dict[str, Any] = {}
+ config_file = model_path / "config.json"
+ if config_file.exists():
+ with open(config_file) as f:
+ raw = json.load(f)
+ for key in (
+ "model_type",
+ "max_position_embeddings",
+ "vocab_size",
+ "bos_token_id",
+ "eos_token_id",
+ "pad_token_id",
+ ):
+ if key in raw:
+ stub_kwargs[key] = raw[key]
+ return AutoTokenizer.from_pretrained(
+ str(model_path),
+ config=PretrainedConfig(**stub_kwargs),
+ trust_remote_code=True,
+ )
def format_peak_memory(b: float) -> str:
@@ -278,9 +318,19 @@ class PromptSizer:
def _make_counter(tokenizer: Any) -> Callable[[str], int]:
def count_fn(user_content: str) -> int:
messages = [{"role": "user", "content": user_content}]
- ids = tokenizer.apply_chat_template(
- messages, tokenize=True, add_generation_prompt=True
- )
+ try:
+ ids = tokenizer.apply_chat_template(
+ messages, tokenize=True, add_generation_prompt=True
+ )
+ except ValueError:
+ # Models without a Jinja chat template (e.g. DeepSeek V4 which
+ # ships its own Python encoder). Use the exo-side V4 encoder.
+ from exo.worker.engines.mlx.deepseek_v4_encoding import (
+ encode_messages as encode_v4,
+ )
+
+ prompt = encode_v4(messages, thinking_mode="thinking")
+ ids = tokenizer.encode(prompt, add_special_tokens=False)
# Fix for transformers 5.x
if hasattr(ids, "input_ids"):
ids = ids.input_ids
diff --git a/dashboard/src/lib/stores/app.svelte.ts b/dashboard/src/lib/stores/app.svelte.ts
index c5a54104..dd6bd854 100644
--- a/dashboard/src/lib/stores/app.svelte.ts
+++ b/dashboard/src/lib/stores/app.svelte.ts
@@ -1670,7 +1670,15 @@ class AppStore {
}
}
}
- return { role: m.role, content: msgContent };
+ const out: {
+ role: string;
+ content: string;
+ reasoning_content?: string;
+ } = { role: m.role, content: msgContent };
+ if (m.role === "assistant" && m.thinking) {
+ out.reasoning_content = m.thinking;
+ }
+ return out;
}),
];
@@ -1877,7 +1885,15 @@ class AppStore {
const apiMessages = [
systemPrompt,
...targetConversation.messages.slice(0, -1).map((m) => {
- return { role: m.role, content: m.content };
+ const out: {
+ role: string;
+ content: string;
+ reasoning_content?: string;
+ } = { role: m.role, content: m.content };
+ if (m.role === "assistant" && m.thinking) {
+ out.reasoning_content = m.thinking;
+ }
+ return out;
}),
];
@@ -2408,10 +2424,15 @@ class AppStore {
contentParts.push({ type: "text", text: textContent });
}
- return {
- role: m.role,
- content: contentParts,
- };
+ const out: {
+ role: string;
+ content: typeof contentParts;
+ reasoning_content?: string;
+ } = { role: m.role, content: contentParts };
+ if (m.role === "assistant" && m.thinking) {
+ out.reasoning_content = m.thinking;
+ }
+ return out;
}
// Text-only message (original path)
@@ -2429,10 +2450,15 @@ class AppStore {
}
}
- return {
- role: m.role,
- content: msgContent,
- };
+ const out: {
+ role: string;
+ content: string;
+ reasoning_content?: string;
+ } = { role: m.role, content: msgContent };
+ if (m.role === "assistant" && m.thinking) {
+ out.reasoning_content = m.thinking;
+ }
+ return out;
}),
];
diff --git a/dashboard/src/routes/integrations/+page.svelte b/dashboard/src/routes/integrations/+page.svelte
index 95a646e1..b8e6665d 100644
--- a/dashboard/src/routes/integrations/+page.svelte
+++ b/dashboard/src/routes/integrations/+page.svelte
@@ -14,6 +14,7 @@
let modelCapabilities = $state<Record<string, string[]>>({});
let modelContextLengths = $state<Record<string, number>>({});
+ let modelReasoningDialects = $state<Record<string, string>>({});
const runningModels = $derived.by(() => {
const models: string[] = [];
@@ -132,6 +133,7 @@
for (const modelId of runningModels) {
const caps = modelCapabilities[modelId] || [];
const ctxLen = modelContextLengths[modelId] || 0;
+ const dialect = modelReasoningDialects[modelId];
const entry: Record<string, unknown> = { name: modelId };
if (ctxLen > 0) {
entry.limit = { context: ctxLen, output: Math.min(ctxLen, 16384) };
@@ -139,6 +141,27 @@
if (caps.includes("vision")) {
entry.modalities = { input: ["text", "image"], output: ["text"] };
}
+ // Reasoning round-trip: opencode's `interleaved` field tells the
+ // openai-compatible adapter to send the assistant's prior
+ // reasoning_content back in subsequent turns. Emit it for dialects
+ // whose chat templates use prior reasoning:
+ // - `tool_conditional` (DeepSeek V3.2 / V4): wrapper preserves all
+ // reasoning when tools are present.
+ // - `post_last_user` (Qwen3-Thinking, GLM 4.5+, MiniMax M2.x):
+ // Jinja template reads reasoning_content for assistant turns since
+ // the last user message — exactly the tool-chain window.
+ // - `channel` (gpt-oss / Harmony): the model's Jinja template reads
+ // `message.thinking` rather than `message.reasoning_content`, but
+ // the server bridges `reasoning_content` → `thinking` before
+ // rendering, so the round-trip works through the standard field.
+ // `suffix` (Kimi): reasoning lives in content; no separate field path.
+ if (
+ dialect === "tool_conditional" ||
+ dialect === "post_last_user" ||
+ dialect === "channel"
+ ) {
+ entry.interleaved = { field: "reasoning_content" };
+ }
models[modelId] = entry;
}
if (Object.keys(models).length === 0) {
@@ -350,16 +373,25 @@
try {
const resp = await fetch("/v1/models");
const data = (await resp.json()) as {
- data: { id: string; capabilities: string[]; context_length: number }[];
+ data: {
+ id: string;
+ capabilities: string[];
+ context_length: number;
+ reasoning_dialect?: string;
+ }[];
};
const caps: Record<string, string[]> = {};
const ctxs: Record<string, number> = {};
+ const dialects: Record<string, string> = {};
for (const model of data.data) {
caps[model.id] = model.capabilities || [];
if (model.context_length > 0) ctxs[model.id] = model.context_length;
+ if (model.reasoning_dialect)
+ dialects[model.id] = model.reasoning_dialect;
}
modelCapabilities = caps;
modelContextLengths = ctxs;
+ modelReasoningDialects = dialects;
} catch {
/* ignore */
}
diff --git a/pyproject.toml b/pyproject.toml
index 65541027..c6a0f37f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,11 +15,11 @@ dependencies = [
"huggingface-hub>=1.8.0",
"psutil>=7.0.0",
"loguru>=0.7.3",
- "exo-pyo3-bindings", # rust bindings
+ "exo-pyo3-bindings", # rust bindings
"anyio==4.11.0",
"mlx==0.31.2; sys_platform == 'darwin'",
"mlx-lm; sys_platform=='darwin'",
- "tiktoken>=0.12.0", # required for kimi k2 tokenizer
+ "tiktoken>=0.12.0", # required for kimi k2 tokenizer
"hypercorn>=0.18.0",
"openai-harmony>=0.0.8",
"httpx>=0.28.1",
@@ -28,8 +28,8 @@ dependencies = [
"python-multipart>=0.0.21",
"msgspec>=0.19.0",
"zstandard>=0.23.0",
- "mlx-vlm>=0.3.11",
- "transformers>=5.0.0,<5.4.0",
+ "mlx-vlm>=0.3.11; sys_platform == 'darwin'",
+ "transformers>=5.6.2",
]
[project.scripts]
@@ -52,18 +52,21 @@ cpu = [
"mlx==0.31.1; sys_platform == 'linux'",
"mlx-cpu==0.31.1; sys_platform == 'linux'",
"mlx-lm; sys_platform == 'linux'",
+ "mlx-vlm>=0.3.11; sys_platform== 'linux'",
"torch>=2.10.0; sys_platform == 'linux'",
]
cuda12 = [
"mlx==0.31.1; sys_platform == 'linux'",
"mlx-cuda-12==0.31.1; sys_platform == 'linux'",
"mlx-lm; sys_platform == 'linux'",
+ "mlx-vlm>=0.3.11; sys_platform== 'linux'",
"torch>=2.10.0; sys_platform == 'linux'",
]
cuda13 = [
"mlx==0.31.1; sys_platform == 'linux'",
"mlx-cuda-13==0.31.1; sys_platform == 'linux'",
"mlx-lm; sys_platform == 'linux'",
+ "mlx-vlm>=0.3.11; sys_platform== 'linux'",
"torch>=2.10.0; sys_platform == 'linux'",
]
@@ -77,7 +80,7 @@ members = ["rust/exo_pyo3_bindings", "bench"]
[tool.uv.sources]
exo-pyo3-bindings = { workspace = true }
mlx = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git", branch = "address-rdma-gpu-locks", marker = "sys_platform == 'darwin'" }
-mlx-lm = { git = "https://github.com/rltakashige/mlx-lm", branch = "leo/fix-arrayscache-leak" }
+mlx-lm = { git = "https://github.com/rltakashige/mlx-lm", branch = "leo/deepseek-v4" }
torch = [
{ index = "pytorch-cu130", marker = "sys_platform == 'linux' and extra == 'cuda13' and extra != 'cpu' and extra != 'cuda12'" },
{ index = "pytorch-cu120", marker = "sys_platform == 'linux' and extra == 'cuda12' and extra != 'cpu' and extra != 'cuda13'" },
@@ -154,7 +157,7 @@ required-version = ">=0.8.6"
prerelease = "allow"
environments = ["sys_platform == 'darwin'", "sys_platform == 'linux'"]
conflicts = [[{ extra = "cuda12" }, { extra = "cuda13" }, { extra = "cpu" }]]
-constraint-dependencies = ["transformers>=5.0.0,<5.4.0"]
+constraint-dependencies = ["transformers>=5.6.2"]
override-dependencies = [
"mlx==0.31.1; sys_platform=='linux'",
"mlx; sys_platform=='darwin'",
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
index 5fbea636..a8d578f6 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-4bit.toml
@@ -8,6 +8,7 @@ family = "deepseek"
quantization = "4bit"
base_model = "DeepSeek V3.1"
capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "post_last_user"
context_length = 131072
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
index 3ebc0cee..4989cb3a 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.1-8bit.toml
@@ -8,6 +8,7 @@ family = "deepseek"
quantization = "8bit"
base_model = "DeepSeek V3.1"
capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "post_last_user"
context_length = 131072
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
index 025a53df..d5d24d22 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-4bit.toml
@@ -8,6 +8,7 @@ family = "deepseek"
quantization = "4bit"
base_model = "DeepSeek V3.2"
capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "tool_conditional"
context_length = 131072
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
index a9b064b3..600639cd 100644
--- a/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V3.2-8bit.toml
@@ -8,6 +8,7 @@ family = "deepseek"
quantization = "8bit"
base_model = "DeepSeek V3.2"
capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "tool_conditional"
context_length = 131072
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V4-Flash.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V4-Flash.toml
new file mode 100644
index 00000000..e1b43f39
--- /dev/null
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V4-Flash.toml
@@ -0,0 +1,21 @@
+model_id = "mlx-community/DeepSeek-V4-Flash"
+n_layers = 43
+hidden_size = 4096
+num_key_value_heads = 1
+supports_tensor = true
+tasks = ["TextGeneration"]
+family = "deepseek"
+quantization = "8bit"
+base_model = "DeepSeek V4 Flash"
+capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "tool_conditional"
+
+context_length = 1048576
+
+[storage_size]
+in_bytes = 155095760030
+
+# Source: https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash
+[sampling_defaults]
+temperature = 1.0
+top_p = 1.0
diff --git a/resources/inference_model_cards/mlx-community--DeepSeek-V4-Pro.toml b/resources/inference_model_cards/mlx-community--DeepSeek-V4-Pro.toml
new file mode 100644
index 00000000..80dbf55c
--- /dev/null
+++ b/resources/inference_model_cards/mlx-community--DeepSeek-V4-Pro.toml
@@ -0,0 +1,21 @@
+model_id = "mlx-community/DeepSeek-V4-Pro"
+n_layers = 61
+hidden_size = 7168
+num_key_value_heads = 1
+supports_tensor = true
+tasks = ["TextGeneration"]
+family = "deepseek"
+quantization = "8bit"
+base_model = "DeepSeek V4 Pro"
+capabilities = ["text", "thinking", "thinking_toggle"]
+reasoning_dialect = "tool_conditional"
+
+context_length = 1048576
+
+[storage_size]
+in_bytes = 849681803879
+
+# Source: https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro
+[sampling_defaults]
+temperature = 1.0
+top_p = 1.0
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
index 35a4529f..0c9f5e08 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-8bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "8bit"
base_model = "GLM 4.5 Air"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 131072
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
index 499754f3..d92a102a 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.5-Air-bf16.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "bf16"
base_model = "GLM 4.5 Air"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 131072
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
index 88b5b001..ce75f1c2 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-4bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "4bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
index 3296b8eb..247dcb0b 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-6bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "6bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
index 705b7871..314a0888 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-8bit-gs32.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "8bit"
base_model = "GLM 4.7"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
index fb203c7a..be6ccf92 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-4bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "4bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
index d6dbfa16..5f1c8dea 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-5bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "5bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
index 153e7f12..36e96616 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-6bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "6bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
index 2eef1c3f..233d54c2 100644
--- a/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-4.7-Flash-8bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "8bit"
base_model = "GLM 4.7 Flash"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml b/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
index 222fbd7b..6a5bda1c 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-8bit.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "8bit"
base_model = "GLM-5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
index fc4100a3..1e1ac0a0 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-MXFP4-Q8.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "MXFP4-Q8"
base_model = "GLM-5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml b/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
index 9b9549e8..035388c6 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5-bf16.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "bf16"
base_model = "GLM-5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5.1-DQ4plus-q8.toml b/resources/inference_model_cards/mlx-community--GLM-5.1-DQ4plus-q8.toml
index 3912a7b7..09749554 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5.1-DQ4plus-q8.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5.1-DQ4plus-q8.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "8bit"
base_model = "GLM-5.1"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5.1-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--GLM-5.1-MXFP4-Q8.toml
index fc644788..81392a6a 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5.1-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5.1-MXFP4-Q8.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "MXFP4-Q8"
base_model = "GLM-5.1"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--GLM-5.1.toml b/resources/inference_model_cards/mlx-community--GLM-5.1.toml
index dec2122e..1239794e 100644
--- a/resources/inference_model_cards/mlx-community--GLM-5.1.toml
+++ b/resources/inference_model_cards/mlx-community--GLM-5.1.toml
@@ -8,7 +8,7 @@ family = "glm"
quantization = "bf16"
base_model = "GLM-5.1"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 202752
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml b/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
index 2c314e8e..15206c88 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2-Thinking.toml
@@ -8,7 +8,7 @@ family = "kimi"
quantization = ""
base_model = "Kimi K2"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "suffix"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml b/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
index 1008b389..7460d91e 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2.5.toml
@@ -8,7 +8,7 @@ family = "kimi"
quantization = ""
base_model = "Kimi K2.5"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "suffix"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.toml b/resources/inference_model_cards/mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.toml
index 4bc0b97c..2571a660 100644
--- a/resources/inference_model_cards/mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.toml
+++ b/resources/inference_model_cards/mlx-community--Kimi-K2.6-mlx-DQ3_K_M-q8.toml
@@ -8,7 +8,7 @@ family = "kimi"
quantization = "3bit"
base_model = "Kimi K2.6"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "suffix"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
index 268e1da6..d0351b24 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-3bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "3bit"
base_model = "MiniMax M2.1"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
index 96ad2c18..86d6de2d 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.1-8bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "8bit"
base_model = "MiniMax M2.1"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
index 08648ad7..f57b4d74 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-4bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "4bit"
base_model = "MiniMax M2.5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
index 6b5e2b74..824801b9 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-6bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "6bit"
base_model = "MiniMax M2.5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
index 7fb1204d..9588abc9 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.5-8bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "8bit"
base_model = "MiniMax M2.5"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit-mxfp4.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit-mxfp4.toml
index bc7281f6..7dd6e942 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit-mxfp4.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit-mxfp4.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "4bit-mxfp4"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit.toml
index 44c9d0d2..d06cfa7b 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-4bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "4bit"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-5bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-5bit.toml
index a39096ce..e0f09cf6 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-5bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-5bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "5bit"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-6bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-6bit.toml
index 7026062c..49c4685d 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-6bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "6bit"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-8bit.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-8bit.toml
index 0067fa4e..6d62934a 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7-8bit.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "8bit"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--MiniMax-M2.7.toml b/resources/inference_model_cards/mlx-community--MiniMax-M2.7.toml
index 3262d55d..478df79b 100644
--- a/resources/inference_model_cards/mlx-community--MiniMax-M2.7.toml
+++ b/resources/inference_model_cards/mlx-community--MiniMax-M2.7.toml
@@ -8,7 +8,7 @@ family = "minimax"
quantization = "bf16"
base_model = "MiniMax M2.7"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "post_last_user"
context_length = 196608
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
index 0d8f8952..c15735b5 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3 Next 80B"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
index 37cc5635..3e57c5b4 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3-Next-80B-A3B-Thinking-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3 Next 80B"
capabilities = ["text", "thinking", "thinking_toggle"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
index de047902..6f937b4a 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.5 122B A10B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
index 25caa21d..6973380c 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-6bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "6bit"
base_model = "Qwen3.5 122B A10B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
index fd8e201c..2558dd70 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 122B A10B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
index c65812cb..e504ad65 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-122B-A10B-bf16.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "bf16"
base_model = "Qwen3.5 122B A10B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
index 9a086fd2..45bb673e 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.5 27B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
index 4c5622e4..0e6f8f02 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-27B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 27B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
index a7ddeec6..5e484a99 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-2B-MLX-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 2B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
index 63cb64e2..393a0776 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.5 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
index 55fd0860..bb6742ec 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-35B-A3B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
index 08cd2709..8404cb7a 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.5 397B A17B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
index 441200a5..c0d3093c 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-6bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "6bit"
base_model = "Qwen3.5 397B A17B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
index 166d9213..988839d8 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-397B-A17B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 397B A17B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
index 867739d3..e864300b 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.5 9B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
index 04773d42..fc3b412d 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.5-9B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.5 9B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-4bit.toml
index d8188395..25bd0d81 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.6 27B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-8bit.toml
index 91d5ec9e..ee62ba29 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.6 27B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-bf16.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-bf16.toml
index f688f3a6..f820559d 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-27B-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-27B-bf16.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "bf16"
base_model = "Qwen3.6 27B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-4bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-4bit.toml
index 0f329cb1..b857d8d8 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-4bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-4bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "4bit"
base_model = "Qwen3.6 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-5bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-5bit.toml
index 6d7a60db..db445870 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-5bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-5bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "5bit"
base_model = "Qwen3.6 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-8bit.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-8bit.toml
index b0edb2e0..3d142553 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-8bit.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-8bit.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "8bit"
base_model = "Qwen3.6 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-bf16.toml b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-bf16.toml
index 366e1b7b..ef459ca3 100644
--- a/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-bf16.toml
+++ b/resources/inference_model_cards/mlx-community--Qwen3.6-35B-A3B-bf16.toml
@@ -8,7 +8,7 @@ family = "qwen"
quantization = "bf16"
base_model = "Qwen3.6 35B A3B"
capabilities = ["text", "thinking", "thinking_toggle", "vision"]
-
+reasoning_dialect = "post_last_user"
context_length = 262144
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
index bdc8f2a9..1a8a028f 100644
--- a/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--gpt-oss-120b-MXFP4-Q8.toml
@@ -8,7 +8,7 @@ family = "gpt-oss"
quantization = "MXFP4-Q8"
base_model = "GPT-OSS 120B"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "channel"
context_length = 131072
[storage_size]
diff --git a/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml b/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
index a93d2b1d..b8bb29ec 100644
--- a/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
+++ b/resources/inference_model_cards/mlx-community--gpt-oss-20b-MXFP4-Q8.toml
@@ -8,7 +8,7 @@ family = "gpt-oss"
quantization = "MXFP4-Q8"
base_model = "GPT-OSS 20B"
capabilities = ["text", "thinking"]
-
+reasoning_dialect = "channel"
context_length = 131072
[storage_size]
diff --git a/src/exo/api/adapters/chat_completions.py b/src/exo/api/adapters/chat_completions.py
index 3797bd5b..d10cfb61 100644
--- a/src/exo/api/adapters/chat_completions.py
+++ b/src/exo/api/adapters/chat_completions.py
@@ -131,9 +131,13 @@ async def chat_request_to_text_generation(
multimodal_content.append({"type": "text", "text": part.text})
else:
multimodal_content.append({"type": "image"})
- chat_template_messages.append(
- {"role": msg.role, "content": multimodal_content}
- )
+ multimodal_msg: dict[str, Any] = {
+ "role": msg.role,
+ "content": multimodal_content,
+ }
+ if msg.reasoning_content is not None:
+ multimodal_msg["reasoning_content"] = msg.reasoning_content
+ chat_template_messages.append(multimodal_msg)
continue
msg_copy = msg.model_copy(update={"content": content})
diff --git a/src/exo/api/main.py b/src/exo/api/main.py
index 37557f5b..0b96e924 100644
--- a/src/exo/api/main.py
+++ b/src/exo/api/main.py
@@ -1663,6 +1663,7 @@ class API:
quantization=card.quantization,
base_model=card.base_model,
capabilities=card.capabilities,
+ reasoning_dialect=card.reasoning_dialect,
context_length=card.context_length,
)
for card in cards
diff --git a/src/exo/api/types/api.py b/src/exo/api/types/api.py
index ddf07f78..859eca77 100644
--- a/src/exo/api/types/api.py
+++ b/src/exo/api/types/api.py
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, field_validator
from exo.shared.models.model_cards import ModelCard, ModelId
from exo.shared.types.common import CommandId, NodeId
from exo.shared.types.memory import Memory
-from exo.shared.types.text_generation import ReasoningEffort
+from exo.shared.types.text_generation import ReasoningDialect, ReasoningEffort
from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta
from exo.shared.types.worker.shards import Sharding, ShardMetadata
from exo.utils.pydantic_ext import FrozenModel
@@ -48,6 +48,7 @@ class ModelListModel(BaseModel):
quantization: str = Field(default="")
base_model: str = Field(default="")
capabilities: list[str] = Field(default_factory=list)
+ reasoning_dialect: ReasoningDialect = "none"
class ModelList(BaseModel):
diff --git a/src/exo/master/placement.py b/src/exo/master/placement.py
index 160a010f..65b97831 100644
--- a/src/exo/master/placement.py
+++ b/src/exo/master/placement.py
@@ -133,12 +133,16 @@ def place_instance(
f"Requested Tensor sharding but this model does not support tensor parallelism: {command.model_card.model_id}"
)
# TODO: the condition here for tensor parallel is not correct, but it works good enough for now.
+ # DeepSeek V4 is MQA (num_key_value_heads=1) but its sharding strategy
+ # head-parallelises wq_b/wo_a and shards MoE experts instead of splitting
+ # KV heads, so the kv-head divisibility check doesn't apply.
+ is_deepseek_v4 = command.model_card.base_model.startswith("DeepSeek V4")
kv_heads = command.model_card.num_key_value_heads
cycles_with_sufficient_memory = [
cycle
for cycle in cycles_with_sufficient_memory
if command.model_card.hidden_size % len(cycle) == 0
- and (kv_heads is None or kv_heads % len(cycle) == 0)
+ and (is_deepseek_v4 or kv_heads is None or kv_heads % len(cycle) == 0)
]
if not cycles_with_sufficient_memory:
raise ValueError(
diff --git a/src/exo/shared/models/model_cards.py b/src/exo/shared/models/model_cards.py
index d5a8e172..0d9acc1d 100644
--- a/src/exo/shared/models/model_cards.py
+++ b/src/exo/shared/models/model_cards.py
@@ -28,6 +28,7 @@ from exo.shared.constants import (
)
from exo.shared.types.common import ModelId
from exo.shared.types.memory import Memory
+from exo.shared.types.text_generation import ReasoningDialect
from exo.utils.pydantic_ext import FrozenModel
# kinda ugly...
@@ -145,6 +146,7 @@ class ModelCard(FrozenModel):
quantization: str = ""
base_model: str = ""
capabilities: list[str] = []
+ reasoning_dialect: ReasoningDialect = "none"
context_length: int = 0
uses_cfg: bool = False
trust_remote_code: bool = True
@@ -270,6 +272,7 @@ class ConfigData(BaseModel):
return self.architectures in [
["Glm4MoeLiteForCausalLM"],
["GlmMoeDsaForCausalLM"],
+ ["DeepseekV4ForCausalLM"],
["DeepseekV32ForCausalLM"],
["DeepseekV3ForCausalLM"],
["Qwen3NextForCausalLM"],
diff --git a/src/exo/shared/types/mlx.py b/src/exo/shared/types/mlx.py
index f15fed42..0b70c1aa 100644
--- a/src/exo/shared/types/mlx.py
+++ b/src/exo/shared/types/mlx.py
@@ -11,10 +11,16 @@ from mlx_lm.models.cache import (
QuantizedKVCache,
RotatingKVCache,
)
+from mlx_lm.models.deepseek_v4 import DeepseekV4Cache
# This list contains one cache entry per transformer layer
KVCacheType = Sequence[
- KVCache | RotatingKVCache | QuantizedKVCache | ArraysCache | CacheList
+ KVCache
+ | RotatingKVCache
+ | QuantizedKVCache
+ | ArraysCache
+ | CacheList
+ | DeepseekV4Cache
]
diff --git a/src/exo/shared/types/text_generation.py b/src/exo/shared/types/text_generation.py
index 1d3fd526..116a5ec2 100644
--- a/src/exo/shared/types/text_generation.py
+++ b/src/exo/shared/types/text_generation.py
@@ -13,6 +13,23 @@ from exo.shared.types.common import ModelId, TruncatingString
MessageRole = Literal["user", "assistant", "system", "developer", "tool"]
ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"]
+# How a model wants prior-turn reasoning content handled. Drives both the
+# server-side encoder (drop vs keep) and the integration configs we emit
+# (e.g. opencode's per-model `interleaved` flag).
+# - "none": model has no reasoning channel.
+# - "post_last_user": reasoning is only meaningful for the latest assistant
+# turn; older turns can drop it (drop_thinking=True).
+# - "suffix": reasoning is embedded in the assistant content as a
+# suffix/prefix; round-tripping content already covers
+# it (no separate `reasoning_content` round-trip).
+# - "channel": reasoning lives on a dedicated channel (Harmony, etc.)
+# and must be sent back verbatim every turn.
+# - "tool_conditional": always round-trip when the conversation has tools;
+# the model relies on prior reasoning to chain tool
+# calls (DeepSeek V3.2 / V4).
+ReasoningDialect = Literal[
+ "none", "post_last_user", "suffix", "channel", "tool_conditional"
+]
def resolve_reasoning_params(
diff --git a/src/exo/worker/engines/mlx/auto_parallel.py b/src/exo/worker/engines/mlx/auto_parallel.py
index 9df66b77..14603f4f 100644
--- a/src/exo/worker/engines/mlx/auto_parallel.py
+++ b/src/exo/worker/engines/mlx/auto_parallel.py
@@ -17,6 +17,8 @@ from mlx_lm.models.base import (
from mlx_lm.models.cache import ArraysCache, KVCache
from mlx_lm.models.deepseek_v3 import DeepseekV3MLP
from mlx_lm.models.deepseek_v3 import Model as DeepseekV3Model
+from mlx_lm.models.deepseek_v4 import DeepseekV4MoE, V4Attention
+from mlx_lm.models.deepseek_v4 import Model as DeepseekV4Model
from mlx_lm.models.deepseek_v32 import DeepseekV32MLP
from mlx_lm.models.deepseek_v32 import Model as DeepseekV32Model
from mlx_lm.models.gemma4 import Model as Gemma4Model
@@ -295,6 +297,7 @@ def pipeline_auto_parallel(
total = len(layers)
for i, layer in enumerate(layers):
mx.eval(layer) # type: ignore
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
layers[0] = PipelineFirstLayer(layers[0], device_rank, group=group)
@@ -510,6 +513,14 @@ def tensor_auto_parallel(
all_to_sharded_linear_in_place,
sharded_to_all_linear_in_place,
)
+ elif isinstance(model, DeepseekV4Model):
+ tensor_parallel_sharding_strategy = DeepseekV4ShardingStrategy(
+ group,
+ all_to_sharded_linear,
+ sharded_to_all_linear,
+ all_to_sharded_linear_in_place,
+ sharded_to_all_linear_in_place,
+ )
elif isinstance(model, MiniMaxModel):
tensor_parallel_sharding_strategy = MiniMaxShardingStrategy(
group,
@@ -648,7 +659,14 @@ def _set_layers(model: nn.Module, layers: list[_LayerCallable]) -> None:
# Update DeepSeek V3 specific parameters when layers are shrunk
if isinstance(
- model, (DeepseekV3Model, DeepseekV32Model, Glm4MoeModel, KimiK25Model)
+ model,
+ (
+ DeepseekV3Model,
+ DeepseekV32Model,
+ DeepseekV4Model,
+ Glm4MoeModel,
+ KimiK25Model,
+ ),
) and hasattr(inner_model_instance, "num_layers"):
logger.info(
f"Setting num_layers to {len(layers)} for model {model.model.__class__.__name__}"
@@ -749,6 +767,178 @@ class ShardedMoE(CustomMlxLayer):
return y
+class ShardedMoEV4(CustomMlxLayer):
+ """Same as ShardedMoE but for DeepseekV4MoE which takes (x, input_ids)."""
+
+ def __init__(self, layer: DeepseekV4MoE):
+ super().__init__(cast(_LayerCallable, cast(object, layer)))
+ self._v4_inner = layer
+ self.sharding_group: mx.distributed.Group | None = None
+
+ def __call__(self, x: mx.array, input_ids: mx.array) -> mx.array:
+ if self.sharding_group is not None:
+ x = sum_gradients(self.sharding_group)(x)
+ y = self._v4_inner(x, input_ids)
+ if self.sharding_group is not None:
+ y = mx.distributed.all_sum(y, group=self.sharding_group)
+ return y
+
+
+def _shard_quantized_rows(
+ q: nn.QuantizedLinear,
+ head_dim: int,
+ slicer: Callable[[mx.array, int], mx.array],
+) -> None:
+ weight = q["weight"]
+ scales = q["scales"]
+ assert isinstance(weight, mx.array)
+ assert isinstance(scales, mx.array)
+ q.weight = slicer(weight, head_dim)
+ q.scales = slicer(scales, head_dim)
+ biases = q.get("biases")
+ if isinstance(biases, mx.array):
+ q.biases = slicer(biases, head_dim)
+
+
+class _AllSumLinear(nn.Module):
+ """Wraps an unsharded wo_b that takes a head-sharded partial wo_a output.
+
+ Flow per rank:
+ 1. all_sum the incoming partial wo_a output (summed across the head
+ input shards → full wo_a_out on every rank)
+ 2. apply the unsharded wo_b → full hidden on every rank
+
+ One collective per layer on the smaller of (n_groups * o_lora_rank) vs
+ hidden. wo_b compute is replicated, but at decode B=1 it's only ~30M FLOPs
+ per layer and 61 extra all_gathers/token cost more than running wo_b on
+ every rank.
+ """
+
+ def __init__(self, inner: nn.Module, group: mx.distributed.Group):
+ super().__init__()
+ self.inner = inner
+ self._group = group
+
+ def __call__(self, x: mx.array) -> mx.array:
+ x = mx.distributed.all_sum(x, group=self._group)
+ return cast(Callable[[mx.array], mx.array], self.inner)(x)
+
+
+def _shard_v4_attention_heads(
+ attn: V4Attention,
+ world_size: int,
+ rank: int,
+) -> None:
+ """Interleaved-per-group head sharding for V4Attention.
+
+ V4 uses a grouped low-rank output projection: `_grouped_output_projection`
+ reshapes the flat `n_heads * head_dim` dim into `(o_groups, heads_per_group,
+ head_dim)`, so group g owns heads `[g * heads_per_group : (g+1) * heads_per_group]`.
+
+ A naive contiguous `shard_linear("all-to-sharded")` on wq_b puts whole
+ original groups on each rank — the per-rank "group g" ends up containing
+ heads that don't belong to original group g. That breaks the wo_a grouped
+ weight mapping. We instead slice heads interleaved-by-group: each rank
+ owns `heads_per_group / N` heads *from every original group*, kept in
+ group-major order so SDPA → reshape → wo_a preserves the group mapping.
+
+ Affects `wq_b.weight` / `wq_b.bias`, `attn_sink`. wo_a is sharded via a
+ normal input-dim block split (the default axis-(-1) behavior of
+ shard_inplace), which now correctly aligns with the interleaved head
+ layout because the last dim of out after reshape is `heads_per_group/N *
+ head_dim` per group.
+ """
+ n_heads: int = attn.n_heads
+ head_dim: int = attn.head_dim
+ o_groups: int = attn.n_groups
+ assert n_heads % o_groups == 0, "n_heads must be divisible by o_groups"
+ heads_per_group = n_heads // o_groups
+ assert heads_per_group % world_size == 0, (
+ f"heads_per_group ({heads_per_group}) must be divisible by world_size "
+ f"({world_size}) for interleaved per-group head sharding"
+ )
+ hpg_per_rank = heads_per_group // world_size
+ start = rank * hpg_per_rank
+ end = start + hpg_per_rank
+
+ def _slice_head_major_flat(arr: mx.array, stride: int) -> mx.array:
+ """Slice arr on axis 0 where the flat 0-axis is (o_groups *
+ heads_per_group * stride), returning a fresh contiguous allocation
+ so the full unsharded array can be freed. Without the contiguous
+ copy the slice is a view and the original weight stays resident —
+ OOM on large V4. Quantized packed weights don't round-trip through
+ numpy so we use mx.contiguous directly."""
+ rest = arr.shape[1:]
+ reshaped = arr.reshape(o_groups, heads_per_group, stride, *rest)
+ sliced = reshaped[:, start:end].reshape(o_groups * hpg_per_rank * stride, *rest)
+ detached = mx.contiguous(sliced)
+ mx.eval(detached)
+ return detached
+
+ wq_b: nn.Module = attn.wq_b
+ if isinstance(wq_b, nn.QuantizedLinear):
+ # Packed weight: (n_heads*head_dim, q_lora_rank/el_per_int).
+ # scales/biases: (n_heads*head_dim, q_lora_rank/group_size).
+ # Slice axis 0 interleaved-by-group with head_dim stride.
+ _shard_quantized_rows(wq_b, head_dim, _slice_head_major_flat)
+ else:
+ dense = wq_b
+ assert isinstance(dense, nn.Linear)
+ w = dense.weight
+ q_lora_rank = w.shape[-1]
+ w_sharded = _slice_head_major_flat(w, head_dim)
+ has_bias = "bias" in dense
+ new_wq_b = nn.Linear(q_lora_rank, w_sharded.shape[0], bias=has_bias)
+ new_wq_b.weight = w_sharded
+ if has_bias:
+ b = dense.bias
+ assert b is not None
+ new_wq_b.bias = _slice_head_major_flat(b[:, None], head_dim).reshape(-1)
+ attn.wq_b = new_wq_b
+
+ sink = attn.attn_sink
+ reshaped = sink.reshape(o_groups, heads_per_group)[:, start:end].reshape(-1)
+ detached_sink = mx.contiguous(reshaped)
+ mx.eval(detached_sink)
+ attn.attn_sink = detached_sink
+ attn.n_heads = o_groups * hpg_per_rank
+
+
+class DeepseekV4ShardingStrategy(TensorParallelShardingStrategy):
+ def shard_model(
+ self,
+ model: nn.Module,
+ ) -> Generator[ModelLoadingResponse, None, nn.Module]:
+ model = cast(DeepseekV4Model, model)
+ total = len(model.layers)
+
+ for i, layer in enumerate(model.layers):
+ mx.eval(layer.parameters())
+
+ # Head-parallel attention with interleaved-per-group sharding.
+ _shard_v4_attention_heads(layer.attn, self.N, self.group.rank())
+ self.sharded_to_all_linear_in_place(layer.attn.wo_a)
+ layer.attn.wo_b = _AllSumLinear(layer.attn.wo_b, self.group) # type: ignore[assignment]
+
+ ffn = layer.ffn
+ if getattr(ffn, "shared_experts", None) is not None:
+ self.all_to_sharded_linear_in_place(ffn.shared_experts.gate_proj)
+ self.sharded_to_all_linear_in_place(ffn.shared_experts.down_proj)
+ self.all_to_sharded_linear_in_place(ffn.shared_experts.up_proj)
+ self.all_to_sharded_linear_in_place(ffn.switch_mlp.gate_proj)
+ self.sharded_to_all_linear_in_place(ffn.switch_mlp.down_proj)
+ self.all_to_sharded_linear_in_place(ffn.switch_mlp.up_proj)
+ wrapped = ShardedMoEV4(ffn)
+ wrapped.sharding_group = self.group
+ layer.ffn = wrapped # type: ignore[assignment]
+
+ mx.eval(layer)
+ mx.clear_cache()
+ yield ModelLoadingResponse(layers_loaded=i, total=total)
+
+ return model
+
+
class GLM4MoeLiteShardingStrategy(TensorParallelShardingStrategy):
def shard_model(
self,
@@ -804,6 +994,7 @@ class GLM4MoeLiteShardingStrategy(TensorParallelShardingStrategy):
layer.mlp = ShardedMoE(layer.mlp) # type: ignore
layer.mlp.sharding_group = self.group # type: ignore
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
@@ -921,6 +1112,7 @@ class MiniMaxShardingStrategy(TensorParallelShardingStrategy):
layer.block_sparse_moe = ShardedMoE(layer.block_sparse_moe) # type: ignore
layer.block_sparse_moe.sharding_group = self.group
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1085,6 +1277,7 @@ class QwenShardingStrategy(TensorParallelShardingStrategy):
layer.mlp.up_proj = self.all_to_sharded_linear(layer.mlp.up_proj)
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1130,6 +1323,7 @@ class Glm4MoeShardingStrategy(TensorParallelShardingStrategy):
layer.mlp.up_proj = self.all_to_sharded_linear(layer.mlp.up_proj)
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1170,6 +1364,7 @@ class GptOssShardingStrategy(TensorParallelShardingStrategy):
layer.mlp = ShardedMoE(layer.mlp) # type: ignore
layer.mlp.sharding_group = self.group
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1212,6 +1407,7 @@ class Step35ShardingStrategy(TensorParallelShardingStrategy):
self.sharded_to_all_linear_in_place(layer.mlp.switch_mlp.down_proj)
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1254,6 +1450,7 @@ class NemotronHShardingStrategy(TensorParallelShardingStrategy):
layer.mixer = mixer # pyright: ignore[reportAttributeAccessIssue]
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
@@ -1391,5 +1588,6 @@ class Gemma4ShardingStrategy(TensorParallelShardingStrategy):
layer.experts.sharding_group = self.group
mx.eval(layer)
+ mx.clear_cache()
yield ModelLoadingResponse(layers_loaded=i, total=total)
return model
diff --git a/src/exo/worker/engines/mlx/cache.py b/src/exo/worker/engines/mlx/cache.py
index c66693fc..30100db8 100644
--- a/src/exo/worker/engines/mlx/cache.py
+++ b/src/exo/worker/engines/mlx/cache.py
@@ -13,6 +13,12 @@ from mlx_lm.models.cache import (
QuantizedKVCache,
RotatingKVCache,
)
+from mlx_lm.models.deepseek_v4 import (
+ DeepseekV4Cache,
+)
+from mlx_lm.models.deepseek_v4 import (
+ _CompressorBranch as CompressorBranch, # type: ignore
+)
from mlx_lm.tokenizer_utils import TokenizerWrapper
from exo.shared.types.memory import Memory
@@ -46,7 +52,11 @@ class CacheSnapshot:
"""Snapshot of states at a known token position."""
def __init__(
- self, states: list[RotatingKVCache | ArraysCache | None], token_count: int
+ self,
+ states: list[
+ RotatingKVCache | ArraysCache | CacheList | DeepseekV4Cache | None
+ ],
+ token_count: int,
):
self.states = states
self.token_count = token_count
@@ -83,13 +93,107 @@ def copy_rotating_kv_cache(cache: RotatingKVCache) -> RotatingKVCache | None:
return snap
+def _copy_arrays_cache(ac: ArraysCache) -> ArraysCache:
+ entries: list[mx.array | None] = []
+ for entry in ac.cache: # type: ignore[reportUnknownMemberType]
+ if entry is None:
+ entries.append(None)
+ continue
+ assert isinstance(entry, mx.array)
+ entries.append(_detached_copy(entry))
+ copy = ArraysCache(len(entries))
+ copy.cache = entries # type: ignore[reportUnknownMemberType]
+ return copy
+
+
+def _copy_cache_list(cl: CacheList) -> CacheList:
+ inners: list[object] = list(cl) # type: ignore[reportUnknownArgumentType]
+ copied: list[object] = []
+ for inner in inners:
+ if isinstance(inner, RotatingKVCache):
+ snap = copy_rotating_kv_cache(inner)
+ copied.append(snap if snap is not None else deepcopy(inner))
+ elif isinstance(inner, ArraysCache):
+ copied.append(_copy_arrays_cache(inner))
+ else:
+ copied.append(deepcopy(inner))
+ return CacheList(*copied)
+
+
+def _detached_copy_or_none(a: mx.array | None) -> mx.array | None:
+ if a is None:
+ return None
+ out = _detached_copy(a)
+ mx.eval(out)
+ return out
+
+
+def _copy_compressor_branch(b: CompressorBranch) -> CompressorBranch:
+ out = CompressorBranch.__new__(CompressorBranch)
+ out.buffer_kv = _detached_copy_or_none(b.buffer_kv)
+ out.buffer_gate = _detached_copy_or_none(b.buffer_gate)
+ out.prev_kv = _detached_copy_or_none(b.prev_kv)
+ out.prev_gate = _detached_copy_or_none(b.prev_gate)
+ out.pool = _detached_copy_or_none(b.pool)
+ out.buffer_lengths = deepcopy(b.buffer_lengths)
+ out.pool_lengths = deepcopy(b.pool_lengths)
+ out.buffer_count = deepcopy(b.buffer_count)
+ out._new_pool_lengths = deepcopy(b._new_pool_lengths)
+ return out
+
+
+def _copy_v4_cache(c: DeepseekV4Cache) -> DeepseekV4Cache:
+ snap = DeepseekV4Cache.__new__(DeepseekV4Cache)
+
+ local: RotatingKVCache = c.local
+ local_snap = copy_rotating_kv_cache(local)
+ if local_snap is None:
+ local_snap = RotatingKVCache.__new__(RotatingKVCache)
+ local_snap.keys = None
+ local_snap.values = None
+ local_snap.offset = local.offset
+ local_snap._idx = 0
+ local_snap.keep = local.keep
+ local_snap.max_size = local.max_size
+ snap.local = local_snap
+
+ snap._branches = {
+ key: _copy_compressor_branch(branch) for key, branch in c._branches.items()
+ }
+ snap._pending_lengths = deepcopy(c._pending_lengths)
+ return snap
+
+
+def copy_snapshot_entry(
+ entry: ArraysCache | RotatingKVCache | CacheList | DeepseekV4Cache | None,
+) -> ArraysCache | RotatingKVCache | CacheList | DeepseekV4Cache | None:
+ match entry:
+ case None:
+ return None
+ case RotatingKVCache():
+ snap = copy_rotating_kv_cache(entry)
+ return snap if snap is not None else deepcopy(entry)
+ case ArraysCache():
+ return _copy_arrays_cache(entry)
+ case CacheList():
+ return _copy_cache_list(entry)
+ case DeepseekV4Cache():
+ return _copy_v4_cache(entry)
+
+
def snapshot_ssm_states(cache: KVCacheType) -> CacheSnapshot:
- states: list[ArraysCache | RotatingKVCache | None] = []
+ states: list[
+ RotatingKVCache | ArraysCache | CacheList | DeepseekV4Cache | None
+ ] = []
for c in cache:
if isinstance(c, ArraysCache):
- states.append(deepcopy(c))
+ states.append(_copy_arrays_cache(c))
elif isinstance(c, RotatingKVCache):
states.append(copy_rotating_kv_cache(c))
+ elif isinstance(c, CacheList) and not bool(c.is_trimmable()): # type: ignore[reportUnknownMemberType]
+ states.append(_copy_cache_list(c))
+ elif isinstance(c, DeepseekV4Cache):
+ states.append(_copy_v4_cache(c))
else:
states.append(None)
token_count = cache_length(cache)
@@ -109,9 +213,20 @@ def _find_nearest_snapshot(
return best
+def is_non_trimmable_cache_entry(c: object) -> bool:
+ """A cache entry is non-trimmable if `trim(n)` can't roll back its full
+ state — meaning the prefill +2 rollback must snapshot+restore it instead.
+ """
+ if isinstance(c, (ArraysCache, RotatingKVCache)):
+ return True
+ if isinstance(c, CacheList):
+ return not bool(c.is_trimmable()) # type: ignore[reportUnknownMemberType]
+ return isinstance(c, DeepseekV4Cache)
+
+
def has_non_kv_caches(cache: KVCacheType) -> bool:
"""Check if a cache contains any ArraysCache (SSM) entries."""
- return any(isinstance(c, (ArraysCache, RotatingKVCache)) for c in cache)
+ return any(is_non_trimmable_cache_entry(c) for c in cache)
class KVPrefixCache:
@@ -267,6 +382,10 @@ class KVPrefixCache:
trim_cache(prompt_cache, tokens_to_trim, restore_snap)
# Reset cache offset to match trimmed length
for c in prompt_cache:
+ if isinstance(c, (ArraysCache, RotatingKVCache)):
+ continue
+ if isinstance(c, DeepseekV4Cache):
+ continue
if hasattr(c, "offset"):
c.offset = restore_pos
@@ -357,11 +476,27 @@ def trim_cache(
snapshot: CacheSnapshot | None = None,
) -> None:
for i, c in enumerate(cache):
- if isinstance(c, (ArraysCache, RotatingKVCache)):
+ non_trimmable = isinstance(c, (ArraysCache, RotatingKVCache)) or (
+ isinstance(c, CacheList) and not bool(c.is_trimmable()) # type: ignore[reportUnknownMemberType]
+ )
+ if non_trimmable:
if snapshot is not None and snapshot.states[i] is not None:
- cache[i] = deepcopy(snapshot.states[i]) # type: ignore
- else:
+ restored = copy_snapshot_entry(snapshot.states[i])
+ if restored is not None:
+ cache[i] = restored # type: ignore
+ elif isinstance(c, (ArraysCache, RotatingKVCache)):
c.state = [None] * len(c.state)
+ if isinstance(c, RotatingKVCache):
+ c.offset = 0
+ c._idx = 0
+ else:
+ # CacheList without a snapshot — zero each inner cache's state
+ for inner in c: # type: ignore[reportUnknownVariableType]
+ if isinstance(inner, (ArraysCache, RotatingKVCache)):
+ inner.state = [None] * len(inner.state)
+ if isinstance(inner, RotatingKVCache):
+ inner.offset = 0
+ inner._idx = 0
else:
c.trim(num_tokens)
@@ -379,7 +514,12 @@ def encode_prompt(tokenizer: TokenizerWrapper, prompt: str) -> mx.array:
def _entry_length(
- c: KVCache | RotatingKVCache | QuantizedKVCache | ArraysCache | CacheList,
+ c: KVCache
+ | RotatingKVCache
+ | QuantizedKVCache
+ | ArraysCache
+ | CacheList
+ | DeepseekV4Cache,
) -> int:
# Use .offset attribute which KVCache types have (len() not implemented in older QuantizedKVCache).
if hasattr(c, "offset"):
diff --git a/src/exo/worker/engines/mlx/generator/generate.py b/src/exo/worker/engines/mlx/generator/generate.py
index da1e958b..47b32a1e 100644
--- a/src/exo/worker/engines/mlx/generator/generate.py
+++ b/src/exo/worker/engines/mlx/generator/generate.py
@@ -2,7 +2,6 @@ import contextlib
import functools
import math
import time
-from copy import deepcopy
from typing import Callable, Generator, cast, get_args
import mlx.core as mx
@@ -10,7 +9,6 @@ from mlx_lm.generate import (
maybe_quantize_kv_cache,
stream_generate,
)
-from mlx_lm.models.cache import ArraysCache, RotatingKVCache
from mlx_lm.sample_utils import make_logits_processors, make_sampler
from mlx_lm.tokenizer_utils import TokenizerWrapper
@@ -44,8 +42,10 @@ from exo.worker.engines.mlx.auto_parallel import (
from exo.worker.engines.mlx.cache import (
CacheSnapshot,
KVPrefixCache,
+ copy_snapshot_entry,
encode_prompt,
has_non_kv_caches,
+ is_non_trimmable_cache_entry,
make_kv_cache,
snapshot_ssm_states,
)
@@ -370,14 +370,16 @@ def prefill(
# stream_generate added 1 extra generated token to the cache, so we should trim it.
# Because of needing to roll back arrays cache, we will generate on 2 tokens so trim 1 more.
- pre_gen = deepcopy(snapshots[-2]) if has_ssm else None
+ pre_gen = snapshots[-2] if has_ssm else None
for i, c in enumerate(cache):
- if has_ssm and isinstance(c, (ArraysCache, RotatingKVCache)):
+ non_trimmable = is_non_trimmable_cache_entry(c)
+ if has_ssm and non_trimmable:
assert pre_gen is not None
- if pre_gen.states[i] is not None:
- cache[i] = deepcopy(pre_gen.states[i]) # type: ignore
+ restored = copy_snapshot_entry(pre_gen.states[i])
+ if restored is not None:
+ cache[i] = restored # type: ignore
else:
- assert not isinstance(c, (ArraysCache, RotatingKVCache))
+ assert not non_trimmable
c.trim(2)
elapsed = time.perf_counter() - start_time
diff --git a/src/exo/worker/engines/mlx/patches/opt_batch_gen.py b/src/exo/worker/engines/mlx/patches/opt_batch_gen.py
index 7b07412b..e7bff419 100644
--- a/src/exo/worker/engines/mlx/patches/opt_batch_gen.py
+++ b/src/exo/worker/engines/mlx/patches/opt_batch_gen.py
@@ -58,6 +58,7 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
self._current_tokens = self._next_tokens
self._current_logprobs = self._next_logprobs
inputs = self._current_tokens
+ assert inputs is not None, "_step requires initialized _next_tokens"
buf = _get_buffer(self)
buf.ready = buf.pending
@@ -87,7 +88,7 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
sampled = self.fallback_sampler(logprobs)
self._next_tokens = sampled
- self._next_logprobs = list(logprobs)
+ self._next_logprobs = logprobs
if buf.needs_topk:
batch_size = len(self.uids)
@@ -106,19 +107,29 @@ def _patched_step(self: GenerationBatch) -> tuple[list[int], list[mx.array]]:
)
mx.async_eval(
self._next_tokens,
- *self._next_logprobs,
+ self._next_logprobs,
pending_indices,
pending_values,
pending_selected,
)
else:
- mx.async_eval(self._next_tokens, *self._next_logprobs)
+ mx.async_eval(self._next_tokens, self._next_logprobs)
+
+ current_lp = self._current_logprobs
+ if isinstance(current_lp, mx.array):
+ mx.eval(inputs, current_lp)
+ elif current_lp:
+ mx.eval(inputs, *current_lp)
+ else:
+ mx.eval(inputs)
- mx.eval(inputs, *self._current_logprobs)
token_list = cast(list[int], inputs.tolist())
for sti, ti in zip(self.tokens, token_list, strict=True):
sti.append(ti)
- return token_list, self._current_logprobs
+
+ if isinstance(current_lp, mx.array):
+ current_lp = list(current_lp)
+ return token_list, current_lp
def apply_batch_gen_patch() -> None:
diff --git a/src/exo/worker/engines/mlx/utils_mlx.py b/src/exo/worker/engines/mlx/utils_mlx.py
index c884c106..a016bbf2 100644
--- a/src/exo/worker/engines/mlx/utils_mlx.py
+++ b/src/exo/worker/engines/mlx/utils_mlx.py
@@ -165,6 +165,8 @@ def load_mlx_items(
) -> Generator[
ModelLoadingResponse, None, tuple[Model, TokenizerWrapper, "VisionProcessor | None"]
]:
+ set_wired_limit_for_model(get_weights_size(bound_instance.bound_shard))
+
if group is None:
logger.info(f"Single device used for {bound_instance.instance}")
model_path = build_model_path(bound_instance.bound_shard.model_card.model_id)
@@ -199,8 +201,6 @@ def load_mlx_items(
f"Time taken to shard and load model: {(end_time - start_time):.2f}s"
)
- set_wired_limit_for_model(get_weights_size(bound_instance.bound_shard))
-
mx.clear_cache()
vision_config = bound_instance.bound_shard.model_card.vision
@@ -486,6 +486,32 @@ def _needs_dsml_encoding(task_params: TextGenerationTaskParams) -> bool:
return "deepseek-v3.2" in task_params.model.lower()
+def _needs_v4_encoding(task_params: TextGenerationTaskParams) -> bool:
+ return "deepseek-v4" in task_params.model.lower()
+
+
+def _v4_reasoning_effort(task_params: TextGenerationTaskParams) -> str | None:
+ effort = task_params.reasoning_effort
+ if effort == "xhigh":
+ return "max"
+ if effort == "high":
+ return "high"
+ return None
+
+
+def _strip_v4_thinking_markers(content: str) -> str:
+ """Remove `<think>…</think>` blocks and any stray `<think>`/`</think>` tags
+ from prior-turn assistant content.
+
+ The V4 encoder drops `reasoning_content` for older turns when
+ `drop_thinking=True`"""
+ block = re.compile(r"<think>.*?</think>", re.DOTALL)
+ if not content:
+ return content
+ cleaned = block.sub("", content)
+ return cleaned.replace("<think>", "").replace("</think>", "")
+
+
def consolidate_system_messages(
messages: list[dict[str, Any]],
) -> list[dict[str, Any]]:
@@ -535,7 +561,7 @@ def render_chat_template(
formatted_messages = formatted_messages[:-1]
if _needs_dsml_encoding(task_params):
- from exo.worker.engines.mlx.dsml_encoding import encode_messages
+ from exo.worker.engines.mlx.vendor.dsml_encoding import encode_messages
prompt = encode_messages(
messages=formatted_messages,
@@ -549,9 +575,49 @@ def render_chat_template(
prompt += partial_assistant_content
return prompt
+ if _needs_v4_encoding(task_params):
+ from exo.worker.engines.mlx.vendor.deepseek_v4_encoding import (
+ encode_messages as encode_messages_v4,
+ )
+
+ v4_messages = [dict(m) for m in formatted_messages]
+ for msg in v4_messages:
+ if msg.get("role") == "assistant":
+ content = msg.get("content")
+ if isinstance(content, str):
+ msg["content"] = _strip_v4_thinking_markers(content)
+ if task_params.tools:
+ for msg in v4_messages:
+ if msg.get("role") in ("system", "developer"):
+ msg["tools"] = task_params.tools
+ break
+ else:
+ v4_messages.insert(
+ 0, {"role": "system", "content": "", "tools": task_params.tools}
+ )
+
+ prompt = encode_messages_v4(
+ messages=v4_messages,
+ thinking_mode="chat"
+ if task_params.enable_thinking is False
+ else "thinking",
+ reasoning_effort=_v4_reasoning_effort(task_params),
+ )
+ if partial_assistant_content:
+ prompt += partial_assistant_content
+ return prompt
+
for msg in formatted_messages:
_normalize_tool_calls(msg)
+ # Put reasoning content in thinking block for GPT OSS
+ if "gpt-oss" in task_params.model.lower():
+ for msg in formatted_messages:
+ if msg.get("role") == "assistant" and "thinking" not in msg:
+ rc = msg.get("reasoning_content")
+ if isinstance(rc, str) and rc:
+ msg["thinking"] = rc
+
extra_kwargs: dict[str, Any] = {}
if task_params.enable_thinking is not None:
# Qwen3 and GLM use "enable_thinking"; DeepSeek uses "thinking".
diff --git a/src/exo/worker/engines/mlx/vendor/__init__.py b/src/exo/worker/engines/mlx/vendor/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/exo/worker/engines/mlx/vendor/deepseek_v4_encoding.py b/src/exo/worker/engines/mlx/vendor/deepseek_v4_encoding.py
new file mode 100644
index 00000000..4eb100c3
--- /dev/null
+++ b/src/exo/worker/engines/mlx/vendor/deepseek_v4_encoding.py
@@ -0,0 +1,836 @@
+# type: ignore
+"""
+DeepSeek-V4 Encoding
+
+From upstream
+"""
+
+import copy
+import json
+import re
+from typing import Any, Dict, List, Optional, Tuple, Union
+
+# ============================================================
+# Special Tokens
+# ============================================================
+
+bos_token: str = "<|begin▁of▁sentence|>"
+eos_token: str = "<|end▁of▁sentence|>"
+thinking_start_token: str = "<think>"
+thinking_end_token: str = "</think>"
+dsml_token: str = "|DSML|"
+
+USER_SP_TOKEN = "<|User|>"
+ASSISTANT_SP_TOKEN = "<|Assistant|>"
+LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
+
+# Task special tokens for internal classification tasks
+DS_TASK_SP_TOKENS = {
+ "action": "<|action|>",
+ "query": "<|query|>",
+ "authority": "<|authority|>",
+ "domain": "<|domain|>",
+ "title": "<|title|>",
+ "read_url": "<|read_url|>",
+}
+VALID_TASKS = set(DS_TASK_SP_TOKENS.keys())
+
+# ============================================================
+# Templates
+# ============================================================
+
+system_msg_template: str = "{content}"
+user_msg_template: str = "{content}"
+latest_reminder_msg_template: str = "{content}"
+assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
+assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
+thinking_template: str = "{reasoning_content}"
+
+response_format_template: str = "## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
+tool_call_template: str = (
+ '<{dsml_token}invoke name="{name}">\n{arguments}\n</{dsml_token}invoke>'
+)
+tool_calls_template = (
+ "<{dsml_token}{tc_block_name}>\n{tool_calls}\n</{dsml_token}{tc_block_name}>"
+)
+tool_calls_block_name: str = "tool_calls"
+
+tool_output_template: str = "<tool_result>{content}</tool_result>"
+
+REASONING_EFFORT_MAX = (
+ "Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
+ "You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
+ "Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n"
+)
+
+TOOLS_TEMPLATE = """## Tools
+
+You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
+
+<{dsml_token}tool_calls>
+<{dsml_token}invoke name="$TOOL_NAME">
+<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
+...
+</{dsml_token}invoke>
+<{dsml_token}invoke name="$TOOL_NAME2">
+...
+</{dsml_token}invoke>
+</{dsml_token}tool_calls>
+
+String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
+
+If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
+
+Otherwise, output directly after {thinking_end_token} with tool calls or final response.
+
+### Available Tool Schemas
+
+{tool_schemas}
+
+You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
+"""
+
+# ============================================================
+# Utility Functions
+# ============================================================
+
+
+def to_json(value: Any) -> str:
+ """Serialize a value to JSON string."""
+ try:
+ return json.dumps(value, ensure_ascii=False)
+ except: # noqa: E722
+ return json.dumps(value, ensure_ascii=True)
+
+
+def tools_from_openai_format(tools):
+ """Extract function definitions from OpenAI-format tool list."""
+ return [tool["function"] for tool in tools]
+
+
+def tool_calls_from_openai_format(tool_calls):
+ """Convert OpenAI-format tool calls to internal format."""
+ return [
+ {
+ "name": tool_call["function"]["name"],
+ "arguments": tool_call["function"]["arguments"],
+ }
+ for tool_call in tool_calls
+ ]
+
+
+def tool_calls_to_openai_format(tool_calls):
+ """Convert internal tool calls to OpenAI format."""
+ return [
+ {
+ "type": "function",
+ "function": {
+ "name": tool_call["name"],
+ "arguments": tool_call["arguments"],
+ },
+ }
+ for tool_call in tool_calls
+ ]
+
+
+def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
+ """
+ Encode tool call arguments into DSML parameter format.
+
+ Args:
+ tool_call: Dict with "name" and "arguments" (JSON string) keys.
+
+ Returns:
+ DSML-formatted parameter string.
+ """
+ p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
+ P_dsml_strs = [] # noqa: N806
+
+ try:
+ arguments = json.loads(tool_call["arguments"])
+ except Exception:
+ arguments = {"arguments": tool_call["arguments"]}
+
+ for k, v in arguments.items():
+ p_dsml_str = p_dsml_template.format(
+ dsml_token=dsml_token,
+ key=k,
+ is_str="true" if isinstance(v, str) else "false",
+ value=v if isinstance(v, str) else to_json(v),
+ )
+ P_dsml_strs.append(p_dsml_str)
+
+ return "\n".join(P_dsml_strs)
+
+
+def decode_dsml_to_arguments(
+ tool_name: str, tool_args: Dict[str, Tuple[str, str]]
+) -> Dict[str, str]:
+ """
+ Decode DSML parameters back to a tool call dict.
+
+ Args:
+ tool_name: Name of the tool.
+ tool_args: Dict mapping param_name -> (value, is_string_flag).
+
+ Returns:
+ Dict with "name" and "arguments" (JSON string) keys.
+ """
+
+ def _decode_value(key: str, value: str, string: str):
+ if string == "true":
+ value = to_json(value)
+ return f"{to_json(key)}: {value}"
+
+ tool_args_json = (
+ "{"
+ + ", ".join(
+ [_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]
+ )
+ + "}"
+ )
+ return dict(name=tool_name, arguments=tool_args_json)
+
+
+def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
+ """
+ Render tool schemas into the system prompt format.
+
+ Args:
+ tools: List of tool schema dicts (each with name, description, parameters).
+
+ Returns:
+ Formatted tools section string.
+ """
+ tools_json = [to_json(t) for t in tools]
+
+ return TOOLS_TEMPLATE.format(
+ tool_schemas="\n".join(tools_json),
+ dsml_token=dsml_token,
+ thinking_start_token=thinking_start_token,
+ thinking_end_token=thinking_end_token,
+ )
+
+
+def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
+ """Find the index of the last user/developer message."""
+ last_user_index = -1
+ for idx in range(len(messages) - 1, -1, -1):
+ if messages[idx].get("role") in ["user", "developer"]:
+ last_user_index = idx
+ break
+ return last_user_index
+
+
+# ============================================================
+# Message Rendering
+# ============================================================
+
+
+def render_message(
+ index: int,
+ messages: List[Dict[str, Any]],
+ thinking_mode: str,
+ drop_thinking: bool = True,
+ reasoning_effort: Optional[str] = None,
+) -> str:
+ """
+ Render a single message at the given index into its encoded string form.
+
+ This is the core function that converts each message in the conversation
+ into the DeepSeek-V4 format.
+
+ Args:
+ index: Index of the message to render.
+ messages: Full list of messages in the conversation.
+ thinking_mode: Either "chat" or "thinking".
+ drop_thinking: Whether to drop reasoning content from earlier turns.
+ reasoning_effort: Optional reasoning effort level ("max", "high", or None).
+
+ Returns:
+ Encoded string for this message.
+ """
+ assert 0 <= index < len(messages)
+ assert thinking_mode in ["chat", "thinking"], (
+ f"Invalid thinking_mode `{thinking_mode}`"
+ )
+
+ prompt = ""
+ msg = messages[index]
+ last_user_idx = find_last_user_index(messages)
+
+ role = msg.get("role")
+ content = msg.get("content")
+ tools = msg.get("tools")
+ response_format = msg.get("response_format")
+ tool_calls = msg.get("tool_calls")
+ reasoning_content = msg.get("reasoning_content")
+ wo_eos = msg.get("wo_eos", False)
+
+ if tools:
+ tools = tools_from_openai_format(tools)
+ if tool_calls:
+ tool_calls = tool_calls_from_openai_format(tool_calls)
+
+ # Reasoning effort prefix (only at index 0 in thinking mode with max effort)
+ assert reasoning_effort in ["max", None, "high"], (
+ f"Invalid reasoning effort: {reasoning_effort}"
+ )
+ if index == 0 and thinking_mode == "thinking" and reasoning_effort == "max":
+ prompt += REASONING_EFFORT_MAX
+
+ if role == "system":
+ prompt += system_msg_template.format(content=content or "")
+ if tools:
+ prompt += "\n\n" + render_tools(tools)
+ if response_format:
+ prompt += "\n\n" + response_format_template.format(
+ schema=to_json(response_format)
+ )
+
+ elif role == "developer":
+ assert content, f"Invalid message for role `{role}`: {msg}"
+
+ content_developer = USER_SP_TOKEN
+ content_developer += content
+
+ if tools:
+ content_developer += "\n\n" + render_tools(tools)
+ if response_format:
+ content_developer += "\n\n" + response_format_template.format(
+ schema=to_json(response_format)
+ )
+
+ prompt += user_msg_template.format(content=content_developer)
+
+ elif role == "user":
+ prompt += USER_SP_TOKEN
+
+ # Handle content blocks (tool results mixed with text)
+ content_blocks = msg.get("content_blocks")
+ if content_blocks:
+ parts = []
+ for block in content_blocks:
+ block_type = block.get("type")
+ if block_type == "text":
+ parts.append(block.get("text", ""))
+ elif block_type == "tool_result":
+ tool_content = block.get("content", "")
+ if isinstance(tool_content, list):
+ text_parts = []
+ for b in tool_content:
+ if b.get("type") == "text":
+ text_parts.append(b.get("text", ""))
+ else:
+ text_parts.append(f"[Unsupported {b.get('type')}]")
+ tool_content = "\n\n".join(text_parts)
+ parts.append(tool_output_template.format(content=tool_content))
+ else:
+ parts.append(f"[Unsupported {block_type}]")
+ prompt += "\n\n".join(parts)
+ else:
+ prompt += content or ""
+
+ elif role == "latest_reminder":
+ prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(
+ content=content
+ )
+
+ elif role == "tool":
+ raise NotImplementedError(
+ "deepseek_v4 merges tool messages into user; please preprocess with merge_tool_messages()"
+ )
+
+ elif role == "assistant":
+ thinking_part = ""
+ tc_content = ""
+
+ if tool_calls:
+ tc_list = [
+ tool_call_template.format(
+ dsml_token=dsml_token,
+ name=tc.get("name"),
+ arguments=encode_arguments_to_dsml(tc),
+ )
+ for tc in tool_calls
+ ]
+ tc_content += "\n\n" + tool_calls_template.format(
+ dsml_token=dsml_token,
+ tool_calls="\n".join(tc_list),
+ tc_block_name=tool_calls_block_name,
+ )
+
+ summary_content = content or ""
+ rc = reasoning_content or ""
+
+ # Check if previous message has a task - if so, this is a task output (no thinking)
+ prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
+
+ if thinking_mode == "thinking" and not prev_has_task:
+ if not drop_thinking or index > last_user_idx:
+ thinking_part = (
+ thinking_template.format(reasoning_content=rc) + thinking_end_token
+ )
+ else:
+ thinking_part = ""
+
+ if wo_eos:
+ prompt += assistant_msg_wo_eos_template.format(
+ reasoning=thinking_part,
+ content=summary_content,
+ tool_calls=tc_content,
+ )
+ else:
+ prompt += assistant_msg_template.format(
+ reasoning=thinking_part,
+ content=summary_content,
+ tool_calls=tc_content,
+ )
+ else:
+ raise NotImplementedError(f"Unknown role: {role}")
+
+ # Append transition tokens based on what follows
+ if index + 1 < len(messages) and messages[index + 1].get("role") not in [
+ "assistant",
+ "latest_reminder",
+ ]:
+ return prompt
+
+ task = messages[index].get("task")
+ if task is not None:
+ # Task special token for internal classification tasks
+ assert task in VALID_TASKS, (
+ f"Invalid task: '{task}'. Valid tasks are: {list(VALID_TASKS)}"
+ )
+ task_sp_token = DS_TASK_SP_TOKENS[task]
+
+ if task != "action":
+ # Non-action tasks: append task sp token directly after the message
+ prompt += task_sp_token
+ else:
+ # Action task: append Assistant + thinking token + action sp token
+ prompt += ASSISTANT_SP_TOKEN
+ prompt += (
+ thinking_end_token
+ if thinking_mode != "thinking"
+ else thinking_start_token
+ )
+ prompt += task_sp_token
+
+ elif messages[index].get("role") in ["user", "developer"]:
+ # Normal generation: append Assistant + thinking token
+ prompt += ASSISTANT_SP_TOKEN
+ if (
+ not drop_thinking
+ and thinking_mode == "thinking"
+ or drop_thinking
+ and thinking_mode == "thinking"
+ and index >= last_user_idx
+ ):
+ prompt += thinking_start_token
+ else:
+ prompt += thinking_end_token
+
+ return prompt
+
+
+# ============================================================
+# Preprocessing
+# ============================================================
+
+
+def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Merge tool messages into the preceding user message using content_blocks format.
+
+ DeepSeek-V4 does not have a standalone "tool" role; instead, tool results
+ are encoded as <tool_result> blocks within user messages.
+
+ This function converts a standard OpenAI-format conversation (with separate
+ "tool" role messages) into V4 format where tool results are merged into
+ user messages.
+
+ Args:
+ messages: List of message dicts in OpenAI format.
+
+ Returns:
+ Processed message list with tool messages merged into user messages.
+ """
+ merged: List[Dict[str, Any]] = []
+
+ for msg in messages:
+ msg = copy.deepcopy(msg)
+ role = msg.get("role")
+
+ if role == "tool":
+ # Convert tool message to a user message with tool_result block
+ tool_block = {
+ "type": "tool_result",
+ "tool_use_id": msg.get("tool_call_id", ""),
+ "content": msg.get("content", ""),
+ }
+ # Merge into previous message if it's already a user (merged tool)
+ if (
+ merged
+ and merged[-1].get("role") == "user"
+ and "content_blocks" in merged[-1]
+ ):
+ merged[-1]["content_blocks"].append(tool_block)
+ else:
+ merged.append(
+ {
+ "role": "user",
+ "content_blocks": [tool_block],
+ }
+ )
+ elif role == "user":
+ text_block = {"type": "text", "text": msg.get("content", "")}
+ if (
+ merged
+ and merged[-1].get("role") == "user"
+ and "content_blocks" in merged[-1]
+ and merged[-1].get("task") is None
+ ):
+ merged[-1]["content_blocks"].append(text_block)
+ else:
+ new_msg = {
+ "role": "user",
+ "content": msg.get("content", ""),
+ "content_blocks": [text_block],
+ }
+ # Preserve extra fields (task, wo_eos, mask, etc.)
+ for key in ("task", "wo_eos", "mask"):
+ if key in msg:
+ new_msg[key] = msg[key]
+ merged.append(new_msg)
+ else:
+ merged.append(msg)
+
+ return merged
+
+
+def sort_tool_results_by_call_order(
+ messages: List[Dict[str, Any]],
+) -> List[Dict[str, Any]]:
+ """
+ Sort tool_result blocks within user messages by the order of tool_calls
+ in the preceding assistant message.
+
+ Args:
+ messages: Preprocessed message list (after merge_tool_messages).
+
+ Returns:
+ Message list with sorted tool result blocks.
+ """
+ last_tool_call_order: Dict[str, int] = {}
+
+ for msg in messages:
+ role = msg.get("role")
+ if role == "assistant" and msg.get("tool_calls"):
+ last_tool_call_order = {}
+ for idx, tc in enumerate(msg["tool_calls"]):
+ tc_id = tc.get("id") or tc.get("function", {}).get("id", "")
+ if tc_id:
+ last_tool_call_order[tc_id] = idx
+
+ elif role == "user" and msg.get("content_blocks"):
+ tool_blocks = [
+ b for b in msg["content_blocks"] if b.get("type") == "tool_result"
+ ]
+ if len(tool_blocks) > 1 and last_tool_call_order:
+ sorted_blocks = sorted(
+ tool_blocks,
+ key=lambda b: last_tool_call_order.get(b.get("tool_use_id", ""), 0),
+ )
+ sorted_idx = 0
+ new_blocks = []
+ for block in msg["content_blocks"]:
+ if block.get("type") == "tool_result":
+ new_blocks.append(sorted_blocks[sorted_idx])
+ sorted_idx += 1
+ else:
+ new_blocks.append(block)
+ msg["content_blocks"] = new_blocks
+
+ return messages
+
+
+# ============================================================
+# Main Encoding Function
+# ============================================================
+
+
+def encode_messages(
+ messages: List[Dict[str, Any]],
+ thinking_mode: str,
+ context: Optional[List[Dict[str, Any]]] = None,
+ drop_thinking: bool = True,
+ add_default_bos_token: bool = True,
+ reasoning_effort: Optional[str] = None,
+) -> str:
+ """
+ Encode a list of messages into the DeepSeek-V4 prompt format.
+
+ This is the main entry point for encoding conversations. It handles:
+ - BOS token insertion
+ - Thinking mode with optional reasoning content dropping
+ - Tool message merging into user messages
+ - Multi-turn conversation context
+
+ Args:
+ messages: List of message dicts to encode.
+ thinking_mode: Either "chat" or "thinking".
+ context: Optional preceding context messages (already encoded prefix).
+ drop_thinking: If True, drop reasoning_content from earlier assistant turns
+ (only keep reasoning for messages after the last user message).
+ add_default_bos_token: Whether to prepend BOS token at conversation start.
+ reasoning_effort: Optional reasoning effort level ("max", "high", or None).
+
+ Returns:
+ The encoded prompt string.
+ """
+ context = context if context else []
+
+ # Preprocess: merge tool messages and sort tool results
+ messages = merge_tool_messages(messages)
+ messages = sort_tool_results_by_call_order(context + messages)[len(context) :]
+ if context:
+ context = merge_tool_messages(context)
+ context = sort_tool_results_by_call_order(context)
+
+ full_messages = context + messages
+
+ prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
+
+ effective_drop_thinking = drop_thinking
+ if any(m.get("tools") for m in full_messages):
+ effective_drop_thinking = False
+
+ if thinking_mode == "thinking" and effective_drop_thinking:
+ full_messages = _drop_thinking_messages(full_messages)
+ # After dropping, recalculate how many messages to render
+ # (context may have shrunk too)
+ num_to_render = len(full_messages) - len(_drop_thinking_messages(context))
+ context_len = len(full_messages) - num_to_render
+ else:
+ num_to_render = len(messages)
+ context_len = len(context)
+
+ for idx in range(num_to_render):
+ prompt += render_message(
+ idx + context_len,
+ full_messages,
+ thinking_mode=thinking_mode,
+ drop_thinking=effective_drop_thinking,
+ reasoning_effort=reasoning_effort,
+ )
+
+ return prompt
+
+
+def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """
+ Drop reasoning_content and non-essential messages before the last user message.
+
+ Behavior:
+ - Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
+ - Messages at or after the last user index are always kept.
+ - Assistant messages before the last user get reasoning_content removed.
+ - Developer messages before the last user are dropped entirely.
+ """
+ last_user_idx = find_last_user_index(messages)
+ result = []
+ keep_roles = {"user", "system", "tool", "latest_reminder", "direct_search_results"}
+
+ for idx, msg in enumerate(messages):
+ role = msg.get("role")
+ if role in keep_roles or idx >= last_user_idx:
+ result.append(msg)
+ elif role == "assistant":
+ msg = copy.copy(msg)
+ msg.pop("reasoning_content", None)
+ result.append(msg)
+ # developer and other roles before last_user_idx are dropped
+
+ return result
+
+
+# ============================================================
+# Parsing (Decoding model output)
+# ============================================================
+
+
+def _read_until_stop(
+ index: int, text: str, stop: List[str]
+) -> Tuple[int, str, Optional[str]]:
+ """
+ Read text from index until one of the stop strings is found.
+
+ Returns:
+ Tuple of (new_index, content_before_stop, matched_stop_string_or_None).
+ """
+ min_pos = len(text)
+ matched_stop = None
+
+ for s in stop:
+ pos = text.find(s, index)
+ if pos != -1 and pos < min_pos:
+ min_pos = pos
+ matched_stop = s
+
+ if matched_stop:
+ content = text[index:min_pos]
+ return min_pos + len(matched_stop), content, matched_stop
+ else:
+ content = text[index:]
+ return len(text), content, None
+
+
+def parse_tool_calls(
+ index: int, text: str
+) -> Tuple[int, Optional[str], List[Dict[str, str]]]:
+ """
+ Parse DSML tool calls from text starting at the given index.
+
+ Args:
+ index: Starting position in text.
+ text: The full text to parse.
+
+ Returns:
+ Tuple of (new_index, last_stop_token, list_of_tool_call_dicts).
+ Each tool call dict has "name" and "arguments" keys.
+ """
+ tool_calls: List[Dict[str, Any]] = []
+ stop_token = None
+ tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
+
+ while index < len(text):
+ index, _, stop_token = _read_until_stop(
+ index, text, [f"<{dsml_token}invoke", tool_calls_end_token]
+ )
+ if _ != ">\n":
+ raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
+
+ if stop_token == tool_calls_end_token:
+ break
+
+ if stop_token is None:
+ raise ValueError("Missing special token in tool calls")
+
+ index, tool_name_content, stop_token = _read_until_stop(
+ index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
+ )
+
+ p_tool_name = re.findall(
+ r'^\s*name="(.*?)">\n$', tool_name_content, flags=re.DOTALL
+ )
+ if len(p_tool_name) != 1:
+ raise ValueError(f"Tool name format error: '{tool_name_content}'")
+ tool_name = p_tool_name[0]
+
+ tool_args: Dict[str, Tuple[str, str]] = {}
+ while stop_token == f"<{dsml_token}parameter":
+ index, param_content, stop_token = _read_until_stop(
+ index, text, [f"/{dsml_token}parameter"]
+ )
+
+ param_kv = re.findall(
+ r'^ name="(.*?)" string="(true|false)">(.*?)<$',
+ param_content,
+ flags=re.DOTALL,
+ )
+ if len(param_kv) != 1:
+ raise ValueError(f"Parameter format error: '{param_content}'")
+ param_name, string, param_value = param_kv[0]
+
+ if param_name in tool_args:
+ raise ValueError(f"Duplicate parameter name: '{param_name}'")
+ tool_args[param_name] = (param_value, string)
+
+ index, content, stop_token = _read_until_stop(
+ index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
+ )
+ if content != ">\n":
+ raise ValueError(
+ f"Parameter format error: expected '>\\n' but got '{content}'"
+ )
+
+ tool_call = decode_dsml_to_arguments(tool_name=tool_name, tool_args=tool_args)
+ tool_calls.append(tool_call)
+
+ return index, stop_token, tool_calls
+
+
+def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[str, Any]:
+ """
+ Parse a model completion text into a structured assistant message.
+
+ This function takes the raw text output from the model (a single assistant turn)
+ and extracts:
+ - reasoning_content (thinking block)
+ - content (summary/response)
+ - tool_calls (if any)
+
+ NOTE: This function is designed to parse only correctly formatted strings and
+ will raise ValueError for malformed output.
+
+ Args:
+ text: The raw completion text (including EOS token).
+ thinking_mode: Either "chat" or "thinking".
+
+ Returns:
+ Dict with keys: "role", "content", "reasoning_content", "tool_calls".
+ tool_calls are in OpenAI format.
+ """
+ summary_content, reasoning_content, tool_calls = "", "", []
+ index, stop_token = 0, None
+ tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
+
+ is_thinking = thinking_mode == "thinking"
+ is_tool_calling = False
+
+ if is_thinking:
+ index, content_delta, stop_token = _read_until_stop(
+ index, text, [thinking_end_token, tool_calls_start_token]
+ )
+ reasoning_content = content_delta
+ assert stop_token == thinking_end_token, (
+ "Invalid thinking format: missing </think>"
+ )
+
+ index, content_delta, stop_token = _read_until_stop(
+ index, text, [eos_token, tool_calls_start_token]
+ )
+ summary_content = content_delta
+ if stop_token == tool_calls_start_token:
+ is_tool_calling = True
+ else:
+ assert stop_token == eos_token, "Invalid format: missing EOS token"
+
+ if is_tool_calling:
+ index, stop_token, tool_calls = parse_tool_calls(index, text)
+
+ index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
+ assert not tool_ends_text, "Unexpected content after tool calls"
+
+ assert len(text) == index and stop_token in [eos_token, None], (
+ "Unexpected content at end"
+ )
+
+ for sp_token in [
+ bos_token,
+ eos_token,
+ thinking_start_token,
+ thinking_end_token,
+ dsml_token,
+ ]:
+ assert sp_token not in summary_content and sp_token not in reasoning_content, (
+ f"Unexpected special token '{sp_token}' in content"
+ )
+
+ return {
+ "role": "assistant",
+ "content": summary_content,
+ "reasoning_content": reasoning_content,
+ "tool_calls": tool_calls_to_openai_format(tool_calls),
+ }
diff --git a/src/exo/worker/engines/mlx/dsml_encoding.py b/src/exo/worker/engines/mlx/vendor/dsml_encoding.py
similarity index 91%
rename from src/exo/worker/engines/mlx/dsml_encoding.py
rename to src/exo/worker/engines/mlx/vendor/dsml_encoding.py
index 66de78c8..893df06b 100644
--- a/src/exo/worker/engines/mlx/dsml_encoding.py
+++ b/src/exo/worker/engines/mlx/vendor/dsml_encoding.py
@@ -34,11 +34,17 @@ def encode_messages(
add_default_bos_token: bool = True,
tools: Any = None, # pyright: ignore[reportAny]
) -> str:
+ # V3.2 (like V4) is `tool_conditional`: when tools are in play, prior-turn
+ # reasoning_content must be retained so multi-step tool chains stay
+ # coherent.
+ effective_drop_thinking = drop_thinking
+ if tools:
+ effective_drop_thinking = False
prompt: str = deepseek_v32.encode_messages(
messages,
thinking_mode=thinking_mode,
context=context,
- drop_thinking=drop_thinking,
+ drop_thinking=effective_drop_thinking,
add_default_bos_token=add_default_bos_token,
tools=tools,
)
diff --git a/src/exo/worker/runner/llm_inference/model_output_parsers.py b/src/exo/worker/runner/llm_inference/model_output_parsers.py
index 944b536b..0e75a96d 100644
--- a/src/exo/worker/runner/llm_inference/model_output_parsers.py
+++ b/src/exo/worker/runner/llm_inference/model_output_parsers.py
@@ -1,7 +1,8 @@
-from collections.abc import Generator, Iterator
+from collections.abc import Callable, Generator, Iterator
from functools import cache
from typing import Any
+from mlx_lm.models.deepseek_v4 import Model as DeepseekV4Model
from mlx_lm.models.deepseek_v32 import Model as DeepseekV32Model
from mlx_lm.models.gpt_oss import Model as GptOssModel
from mlx_lm.tokenizer_utils import TokenizerWrapper
@@ -26,6 +27,7 @@ from exo.shared.types.worker.runner_response import GenerationResponse, ToolCall
from exo.worker.engines.mlx.utils_mlx import (
detect_thinking_prompt_suffix,
)
+from exo.worker.engines.mlx.vendor.dsml_encoding import parse_dsml_output
from exo.worker.runner.bootstrap import logger
from exo.worker.runner.llm_inference.tool_parsers import ToolParser
@@ -73,12 +75,10 @@ def apply_all_parsers(
) -> Iterator[GenerationChunk | None]:
generator = receiver
+ normalized_id = model_id.normalize().lower()
if issubclass(model_type, GptOssModel):
generator = parse_gpt_oss(generator)
- elif (
- issubclass(model_type, DeepseekV32Model)
- and "deepseek" in model_id.normalize().lower()
- ):
+ elif issubclass(model_type, DeepseekV32Model) and "deepseek" in normalized_id:
if tokenizer.has_thinking:
generator = parse_thinking_models(
generator,
@@ -87,6 +87,15 @@ def apply_all_parsers(
starts_in_thinking=detect_thinking_prompt_suffix(prompt, tokenizer),
)
generator = parse_deepseek_v32(generator)
+ elif issubclass(model_type, DeepseekV4Model) and "deepseek-v4" in normalized_id:
+ if tokenizer.has_thinking:
+ generator = parse_thinking_models(
+ generator,
+ tokenizer.think_start,
+ tokenizer.think_end,
+ starts_in_thinking=detect_thinking_prompt_suffix(prompt, tokenizer),
+ )
+ generator = parse_deepseek_v4(generator)
else:
if tokenizer.has_thinking:
generator = parse_thinking_models(
@@ -220,12 +229,32 @@ def parse_deepseek_v32(
Thinking tag handling is delegated to parse_thinking_models, which
wraps this parser in apply_all_parsers.
"""
- from exo.worker.engines.mlx.dsml_encoding import (
+ from exo.worker.engines.mlx.vendor.dsml_encoding import (
TOOL_CALLS_END,
TOOL_CALLS_START,
parse_dsml_output,
)
+ return _parse_dsml_stream(
+ responses, TOOL_CALLS_START, TOOL_CALLS_END, parse_dsml_output
+ )
+
+
+def parse_deepseek_v4(
+ responses: Generator[GenerationResponse | None],
+) -> Generator[GenerationResponse | ToolCallResponse | None]:
+ dsml_token = "|DSML|"
+ start = f"<{dsml_token}tool_calls>"
+ end = f"</{dsml_token}tool_calls>"
+ return _parse_dsml_stream(responses, start, end, parse_dsml_output)
+
+
+def _parse_dsml_stream(
+ responses: Generator[GenerationResponse | None],
+ tool_calls_start: str,
+ tool_calls_end: str,
+ parse_body: Callable[[str], list[ToolCallItem] | None],
+) -> Generator[GenerationResponse | ToolCallResponse | None]:
accumulated = ""
in_tool_call = False
# Tokens buffered while we detect the start of a DSML block
@@ -236,7 +265,7 @@ def parse_deepseek_v32(
def _try_parse_tool_call(
text: str, response: GenerationResponse
) -> ToolCallResponse | GenerationResponse:
- parsed = parse_dsml_output(text)
+ parsed = parse_body(text)
if parsed is not None:
return ToolCallResponse(
tool_calls=parsed, usage=response.usage, stats=response.stats
@@ -256,11 +285,11 @@ def parse_deepseek_v32(
tool_call_text += response.text
yield (
_try_parse_tool_call(tool_call_text, response)
- if TOOL_CALLS_END in tool_call_text
+ if tool_calls_end in tool_call_text
else response.model_copy(update={"text": tool_call_text})
)
- elif TOOL_CALLS_START in response.text and TOOL_CALLS_END in response.text:
- dsml_start = response.text.index(TOOL_CALLS_START)
+ elif tool_calls_start in response.text and tool_calls_end in response.text:
+ dsml_start = response.text.index(tool_calls_start)
before = response.text[:dsml_start]
if before:
yield response.model_copy(update={"text": before})
@@ -269,25 +298,21 @@ def parse_deepseek_v32(
yield response
break
- # ── Handle tool call accumulation ──
if in_tool_call:
tool_call_text += response.text
- if TOOL_CALLS_END in tool_call_text:
+ if tool_calls_end in tool_call_text:
yield _try_parse_tool_call(tool_call_text, response)
in_tool_call = False
tool_call_text = ""
continue
- # ── Detect start of tool call block ──
accumulated += response.text
- if TOOL_CALLS_START in accumulated:
- # The start marker might be split across pending_buffer + current token
- start_idx = accumulated.index(TOOL_CALLS_START)
- # Yield any pending tokens that are purely before the marker
+ if tool_calls_start in accumulated:
+ start_idx = accumulated.index(tool_calls_start)
pre_text = accumulated[:start_idx]
+ # Flush pending buffer tokens that contributed text before the marker
if pre_text:
- # Flush pending buffer tokens that contributed text before the marker
for buf_resp in pending_buffer:
if not pre_text:
break
@@ -302,17 +327,14 @@ def parse_deepseek_v32(
tool_call_text = accumulated[start_idx:]
accumulated = ""
- # Check if the end marker is already present (entire tool call in one token)
- if TOOL_CALLS_END in tool_call_text:
+ if tool_calls_end in tool_call_text:
yield _try_parse_tool_call(tool_call_text, response)
tool_call_text = ""
else:
in_tool_call = True
continue
- # Check if accumulated text might be the start of a DSML marker
- # Buffer tokens if we see a partial match at the end
- if _could_be_dsml_prefix(accumulated):
+ if _could_be_marker_prefix(accumulated, tool_calls_start):
pending_buffer.append(response)
continue
@@ -326,22 +348,12 @@ def parse_deepseek_v32(
yield from pending_buffer
-def _could_be_dsml_prefix(text: str) -> bool:
- """Check if the end of text could be the start of a DSML function_calls marker.
-
- We look for suffixes of text that are prefixes of the TOOL_CALLS_START pattern.
- This allows us to buffer tokens until we can determine if a tool call is starting.
- """
- from exo.worker.engines.mlx.dsml_encoding import TOOL_CALLS_START
-
- # Only check the last portion of text that could overlap with the marker
- max_check = len(TOOL_CALLS_START)
+def _could_be_marker_prefix(text: str, marker: str) -> bool:
+ max_check = len(marker)
tail = text[-max_check:] if len(text) > max_check else text
-
- # Check if any suffix of tail is a prefix of TOOL_CALLS_START
for i in range(len(tail)):
suffix = tail[i:]
- if TOOL_CALLS_START.startswith(suffix):
+ if marker.startswith(suffix):
return True
return False
diff --git a/src/exo/worker/tests/unittests/test_mlx/test_prefix_cache_architectures.py b/src/exo/worker/tests/unittests/test_mlx/test_prefix_cache_architectures.py
index bfaba83b..0a67f0c3 100644
--- a/src/exo/worker/tests/unittests/test_mlx/test_prefix_cache_architectures.py
+++ b/src/exo/worker/tests/unittests/test_mlx/test_prefix_cache_architectures.py
@@ -70,6 +70,13 @@ def _reduce_config(cfg: dict[str, Any]) -> dict[str, Any]:
tc: dict[str, Any] = result["text_config"]
if "num_nextn_predict_layers" in tc:
tc["num_nextn_predict_layers"] = 0
+ tc_n_layers = cast(int, tc.get("num_hidden_layers", n_layers))
+ if "layer_types" in tc and isinstance(tc["layer_types"], list):
+ tc["layer_types"] = cast(list[Any], tc["layer_types"])[:tc_n_layers]
+ if "mlp_only_layers" in tc and isinstance(tc["mlp_only_layers"], list):
+ tc["mlp_only_layers"] = [
+ i for i in cast(list[int], tc["mlp_only_layers"]) if i < tc_n_layers
+ ]
if "layer_types" in result and isinstance(result["layer_types"], list):
result["layer_types"] = result["layer_types"][:n_layers]
diff --git a/src/exo/worker/tests/unittests/test_mlx/test_tp_bit_exact.py b/src/exo/worker/tests/unittests/test_mlx/test_tp_bit_exact.py
index f0d12847..470a98a1 100644
--- a/src/exo/worker/tests/unittests/test_mlx/test_tp_bit_exact.py
+++ b/src/exo/worker/tests/unittests/test_mlx/test_tp_bit_exact.py
@@ -234,6 +234,42 @@ MODEL_CONFIGS = {
rope_theta=10000.0,
),
),
+ "deepseek_v4": dict(
+ module="mlx_lm.models.deepseek_v4",
+ args=dict(
+ model_type="deepseek_v4",
+ vocab_size=256,
+ hidden_size=64,
+ num_hidden_layers=4,
+ num_attention_heads=4,
+ num_key_value_heads=1,
+ q_lora_rank=32,
+ o_lora_rank=32,
+ o_groups=1,
+ head_dim=16,
+ qk_rope_head_dim=8,
+ sliding_window=32,
+ compress_ratios=[0, 0, 4, 0, 0],
+ index_n_heads=4,
+ index_head_dim=16,
+ index_topk=16,
+ moe_intermediate_size=32,
+ n_routed_experts=4,
+ n_shared_experts=1,
+ num_experts_per_tok=2,
+ num_hash_layers=1,
+ hc_mult=1,
+ num_nextn_predict_layers=0,
+ max_position_embeddings=2048,
+ rope_scaling={
+ "beta_fast": 32,
+ "beta_slow": 1,
+ "factor": 2,
+ "original_max_position_embeddings": 1024,
+ "type": "yarn",
+ },
+ ),
+ ),
"gemma4": dict(
module="mlx_lm.models.gemma4",
args=dict(
diff --git a/src/exo/worker/tests/unittests/test_runner/test_dsml_e2e.py b/src/exo/worker/tests/unittests/test_runner/test_dsml_e2e.py
index 8b6c6ded..fe508ae3 100644
--- a/src/exo/worker/tests/unittests/test_runner/test_dsml_e2e.py
+++ b/src/exo/worker/tests/unittests/test_runner/test_dsml_e2e.py
@@ -1,13 +1,13 @@
import json
from collections.abc import Generator
-from typing import Any
+from typing import Any, cast
from exo.shared.types.common import ModelId
from exo.shared.types.worker.runner_response import (
GenerationResponse,
ToolCallResponse,
)
-from exo.worker.engines.mlx.dsml_encoding import (
+from exo.worker.engines.mlx.vendor.dsml_encoding import (
ASSISTANT_TOKEN,
BOS_TOKEN,
DSML_TOKEN,
@@ -21,6 +21,7 @@ from exo.worker.engines.mlx.dsml_encoding import (
parse_dsml_output,
)
from exo.worker.runner.llm_inference.model_output_parsers import (
+ parse_deepseek_v4,
parse_deepseek_v32,
parse_thinking_models,
)
@@ -1054,3 +1055,128 @@ class TestApplyChatTemplateWithToolCalls:
assert "get_weather" in prompt
assert "Tokyo" in prompt
assert "Sunny" in prompt
+
+
+class TestE2EDeepseekV4ToolCallParsing:
+ """V4 emits `<|DSML|tool_calls>` (outer) wrapping `<|DSML|invoke …>` calls
+ (the V4-Flash chat template promises this exact structure). Parser must
+ extract the tool name + parameters back out."""
+
+ def test_v4_tool_call_extracted_from_clean_output(self):
+ """Clean V4 DSML output should yield a ToolCallResponse with the
+ invoked tool name and parameter values — not bleed through as text."""
+ # Realistic token splits matching the V4 tokenizer's known behavior:
+ # `<|DSML|tool_calls>` -> ['<', '|DSML|', 'tool', '_c', 'alls', '>']
+ # The model emits tokens one-by-one in this multi-token pattern.
+ model_tokens = [
+ "<",
+ DSML_TOKEN,
+ "tool",
+ "_c",
+ "alls",
+ ">",
+ "\n<",
+ DSML_TOKEN,
+ "invoke",
+ ' name="read"',
+ ">\n<",
+ DSML_TOKEN,
+ "parameter",
+ ' name="filePath" string="true"',
+ ">",
+ "/Users/l2/PycharmProjects/exo",
+ "</",
+ DSML_TOKEN,
+ "parameter",
+ ">\n</",
+ DSML_TOKEN,
+ "invoke",
+ ">\n</",
+ DSML_TOKEN,
+ "tool",
+ "_c",
+ "alls",
+ ">",
+ ]
+
+ results = list(parse_deepseek_v4(_simulate_tokens(model_tokens)))
+
+ tool_results = [r for r in results if isinstance(r, ToolCallResponse)]
+ text_results = [r for r in results if isinstance(r, GenerationResponse)]
+
+ assert len(tool_results) == 1, (
+ f"expected one ToolCallResponse, got {len(tool_results)} tool + "
+ f"{len(text_results)} text results: text="
+ f"{''.join(r.text for r in text_results)!r}"
+ )
+ tool_calls = tool_results[0].tool_calls
+ assert len(tool_calls) == 1
+ assert tool_calls[0].name == "read"
+ args = cast(dict[str, str], json.loads(tool_calls[0].arguments))
+ assert args == {"filePath": "/Users/l2/PycharmProjects/exo"}
+
+ def test_v4_tool_call_after_thinking_block(self):
+ """V4 reasoning models start in `<think>` and emit DSML tool calls
+ after `</think>`. The thinking parser must hand a complete tool-call
+ block off to `parse_deepseek_v4` without dropping markers."""
+ # `</think>` token-splits into ['</think>'] in V4's tokenizer, so the
+ # thinking parser sees it as a single token. Emit thinking, then the
+ # DSML tool call.
+ model_tokens = [
+ "<think>",
+ "The user wants me to explore the codebase.",
+ "</think>",
+ "<",
+ DSML_TOKEN,
+ "tool",
+ "_c",
+ "alls",
+ ">",
+ "\n<",
+ DSML_TOKEN,
+ "invoke",
+ ' name="read"',
+ ">\n<",
+ DSML_TOKEN,
+ "parameter",
+ ' name="filePath" string="true"',
+ ">",
+ "/Users/l2/PycharmProjects/exo",
+ "</",
+ DSML_TOKEN,
+ "parameter",
+ ">\n</",
+ DSML_TOKEN,
+ "invoke",
+ ">\n</",
+ DSML_TOKEN,
+ "tool",
+ "_c",
+ "alls",
+ ">",
+ ]
+
+ results = list(
+ parse_deepseek_v4(
+ parse_thinking_models(
+ _simulate_tokens(model_tokens),
+ think_start="<think>",
+ think_end="</think>",
+ starts_in_thinking=True,
+ )
+ )
+ )
+
+ tool_results = [r for r in results if isinstance(r, ToolCallResponse)]
+ text_results = [r for r in results if isinstance(r, GenerationResponse)]
+ non_thinking_text = "".join(r.text for r in text_results if not r.is_thinking)
+
+ assert len(tool_results) == 1, (
+ f"expected ToolCallResponse, got {len(tool_results)} tool + "
+ f"non-thinking text {non_thinking_text!r}"
+ )
+ tool_calls = tool_results[0].tool_calls
+ assert len(tool_calls) == 1
+ assert tool_calls[0].name == "read"
+ args = cast(dict[str, str], json.loads(tool_calls[0].arguments))
+ assert args == {"filePath": "/Users/l2/PycharmProjects/exo"}
diff --git a/src/exo/worker/tests/unittests/test_runner/test_finish_reason_sse.py b/src/exo/worker/tests/unittests/test_runner/test_finish_reason_sse.py
index 6771b3d8..8eb4b443 100644
--- a/src/exo/worker/tests/unittests/test_runner/test_finish_reason_sse.py
+++ b/src/exo/worker/tests/unittests/test_runner/test_finish_reason_sse.py
@@ -8,7 +8,7 @@ from exo.shared.types.worker.runner_response import (
GenerationResponse,
ToolCallResponse,
)
-from exo.worker.engines.mlx.dsml_encoding import (
+from exo.worker.engines.mlx.vendor.dsml_encoding import (
DSML_TOKEN,
THINKING_END,
THINKING_START,
diff --git a/uv.lock b/uv.lock
index f0b432b9..9a13fcdc 100644
--- a/uv.lock
+++ b/uv.lock
@@ -24,7 +24,7 @@ members = [
"exo-bench",
"exo-pyo3-bindings",
]
-constraints = [{ name = "transformers", specifier = ">=5.0.0,<5.4.0" }]
+constraints = [{ name = "transformers", specifier = ">=5.6.2" }]
overrides = [
{ name = "mlx", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks" },
{ name = "mlx", marker = "sys_platform == 'linux'", specifier = "==0.31.1" },
@@ -187,7 +187,7 @@ name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (implementation_name != 'PyPy' and sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (implementation_name != 'PyPy' and sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform != 'darwin' and sys_platform != 'linux' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
@@ -394,9 +394,9 @@ dependencies = [
{ name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "mflux", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx-vlm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx-lm", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx-vlm", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "msgspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "openai-harmony", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
@@ -416,21 +416,24 @@ build = [
]
cpu = [
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cpu') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "mlx-cpu", marker = "sys_platform == 'linux'" },
- { name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux'" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux'" },
]
cuda12 = [
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
{ name = "mlx-cuda-12", marker = "sys_platform == 'linux'" },
- { name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux'" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux'" },
]
cuda13 = [
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "(sys_platform == 'darwin' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13')" },
{ name = "mlx-cuda-13", marker = "sys_platform == 'linux'" },
- { name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'linux'" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux'" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux'" },
]
[package.dev-dependencies]
@@ -463,11 +466,14 @@ requires-dist = [
{ name = "mlx-cpu", marker = "sys_platform == 'linux' and extra == 'cpu'", specifier = "==0.31.1" },
{ name = "mlx-cuda-12", marker = "sys_platform == 'linux' and extra == 'cuda12'", specifier = "==0.31.1" },
{ name = "mlx-cuda-13", marker = "sys_platform == 'linux' and extra == 'cuda13'", specifier = "==0.31.1" },
- { name = "mlx-lm", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
- { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cpu'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
- { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda12'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
- { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda13'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak" },
- { name = "mlx-vlm", specifier = ">=0.3.11" },
+ { name = "mlx-lm", marker = "sys_platform == 'darwin'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cpu'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda12'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
+ { name = "mlx-lm", marker = "sys_platform == 'linux' and extra == 'cuda13'", git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4" },
+ { name = "mlx-vlm", marker = "sys_platform == 'darwin'", specifier = ">=0.3.11" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux' and extra == 'cpu'", specifier = ">=0.3.11" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux' and extra == 'cuda12'", specifier = ">=0.3.11" },
+ { name = "mlx-vlm", marker = "sys_platform == 'linux' and extra == 'cuda13'", specifier = ">=0.3.11" },
{ name = "msgspec", specifier = ">=0.19.0" },
{ name = "nanobind", marker = "extra == 'build'" },
{ name = "openai-harmony", specifier = ">=0.0.8" },
@@ -483,7 +489,7 @@ requires-dist = [
{ name = "torch", marker = "(sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda12') or (sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda13')", specifier = ">=2.10.0" },
{ name = "torch", marker = "sys_platform == 'linux' and extra == 'cpu' and extra != 'cuda12' and extra != 'cuda13'", specifier = ">=2.10.0", index = "https://download.pytorch.org/whl/cpu" },
{ name = "torch", marker = "(sys_platform == 'linux' and extra == 'cpu' and extra == 'cuda13') or (sys_platform == 'linux' and extra == 'cuda12' and extra == 'cuda13')", specifier = ">=2.10.0" },
- { name = "transformers", specifier = ">=5.0.0,<5.4.0" },
+ { name = "transformers", specifier = ">=5.6.2" },
{ name = "types-aiofiles", specifier = ">=24.1.0.20250708" },
{ name = "zstandard", specifier = ">=0.23.0" },
]
@@ -1207,7 +1213,7 @@ dependencies = [
{ name = "hf-transfer", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "matplotlib", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "numpy", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "opencv-python", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "piexif", marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
@@ -1236,7 +1242,7 @@ name = "miniaudio"
version = "1.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "cffi", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/fa/96d4cc7ada283357117f7890418ac065a0a6d81ec59e681cd965a403aba3/miniaudio-1.61.tar.gz", hash = "sha256:e88e97837d031f0fb6982394218b6487de02eaa382ad273b8fca37791a2b4b15", size = 1103527, upload-time = "2024-07-24T18:13:10.037Z" }
@@ -1257,8 +1263,8 @@ wheels = [
[[package]]
name = "mlx"
-version = "0.31.2.dev20260422+ec49d18e"
-source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }
+version = "0.32.0.dev20260427+cc3f3e60"
+source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }
resolution-markers = [
"sys_platform == 'darwin'",
]
@@ -1305,36 +1311,11 @@ wheels = [
[[package]]
name = "mlx-lm"
version = "0.31.3"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "sys_platform == 'linux'",
-]
-dependencies = [
- { name = "jinja2", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "numpy", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "protobuf", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "pyyaml", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "sentencepiece", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "transformers", marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/84/94/9a38d6b0c6fcca995b9136c94eb7da1e9c5165652edf228b96b29960fa7a/mlx_lm-0.31.3.tar.gz", hash = "sha256:61eb0e3ba09444f77f874aff295401d7ccd20b39495cbbce0c782a15474ce733", size = 304318, upload-time = "2026-04-22T07:37:27.922Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/02/9a67b8e4f87e3e2e5cd7b1ad79304b93c09a0db6af34bee75e6551c06c60/mlx_lm-0.31.3-py3-none-any.whl", hash = "sha256:758cfddf1180053b7613db76fad3d246a331a2a905808e1164a275621fc983b8", size = 408890, upload-time = "2026-04-22T07:37:25.965Z" },
-]
-
-[[package]]
-name = "mlx-lm"
-version = "0.31.3"
-source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }
-resolution-markers = [
- "sys_platform == 'darwin'",
- "sys_platform == 'linux'",
-]
+source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Fdeepseek-v4#6a3df6cd6b00a347ee40f12d97a182aaf86ea599" }
dependencies = [
{ name = "jinja2", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "numpy", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "protobuf", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "pyyaml", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
@@ -1347,20 +1328,19 @@ name = "mlx-vlm"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "datasets", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "miniaudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx", version = "0.31.2.dev20260422+ec49d18e", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#ec49d18ec4cfba0e0c7a37f20d1cf4d75fe56731" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx-lm", version = "0.31.3", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra != 'extra-3-exo-cpu' and extra != 'extra-3-exo-cuda12' and extra != 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "mlx-lm", version = "0.31.3", source = { git = "https://github.com/rltakashige/mlx-lm?branch=leo%2Ffix-arrayscache-leak#c7010341e1f41ac15815feb5dc55134f44e3b044" }, marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "opencv-python", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "datasets", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "fastapi", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "miniaudio", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.31.1", source = { registry = "https://pypi.org/simple" }, marker = "(sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx", version = "0.32.0.dev20260427+cc3f3e60", source = { git = "https://github.com/rltakashige/mlx-jaccl-fix-small-recv.git?branch=address-rdma-gpu-locks#cc3f3e60be1289506125f2fa19b73b05aa770df8" }, marker = "sys_platform == 'darwin' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "mlx-lm", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "numpy", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "opencv-python", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "pillow", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "requests", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "tqdm", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "transformers", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "uvicorn", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/ec/108aec30efb159940ea29d133d5d8ec14840edbec914869b46eaafac5552/mlx_vlm-0.4.4.tar.gz", hash = "sha256:3197e277c1be9ed1712ea04624df029e486f7747ad93e40e7bd1c9c771f8b179", size = 836370, upload-time = "2026-04-04T15:19:01.087Z" }
wheels = [
@@ -1679,7 +1659,7 @@ name = "opencv-python"
version = "4.13.0.90"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "numpy", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/d7/133d5756aef78090f4d8dd4895793aed24942dec6064a15375cfac9175fc/opencv_python-4.13.0.90-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:58803f8b05b51d8a785e2306d83b44173b32536f980342f3bc76d8c122b5938d", size = 46020278, upload-time = "2026-01-18T08:57:42.539Z" },
@@ -2634,7 +2614,7 @@ wheels = [
[[package]]
name = "transformers"
-version = "5.2.0"
+version = "5.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
@@ -2645,11 +2625,11 @@ dependencies = [
{ name = "safetensors", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "typer-slim", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bd/7e/8a0c57d562015e5b16c97c1f0b8e0e92ead2c7c20513225dc12c2043ba9f/transformers-5.2.0.tar.gz", hash = "sha256:0088b8b46ccc9eff1a1dca72b5d618a5ee3b1befc3e418c9512b35dea9f9a650", size = 8618176, upload-time = "2026-02-16T18:54:02.867Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a4/e9/c6c80a07690142a7d05444271f47b9f3c8aac7dea01d52e1137ee480ad78/transformers-5.6.2.tar.gz", hash = "sha256:e657134c3e5a6bc00a3c35f4e2674bb51adfcd89898495b788a18552bac2b91a", size = 8311867, upload-time = "2026-04-23T18:33:29.332Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4e/93/79754b0ca486e556c2b95d4f5afc66aaf4b260694f3d6e1b51da2d036691/transformers-5.2.0-py3-none-any.whl", hash = "sha256:9ecaf243dc45bee11a7d93f8caf03746accc0cb069181bbf4ad8566c53e854b4", size = 10403304, upload-time = "2026-02-16T18:53:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/95/0b0218149b0d6f14df35f5b8f676fa83df4f19ed253c3cc447107ef86eca/transformers-5.6.2-py3-none-any.whl", hash = "sha256:f8d3a1bb96778fed9b8aabfd0dd6e19843e4b0f2bb6b59f32b8a92051b0f348f", size = 10364898, upload-time = "2026-04-23T18:33:26.081Z" },
]
[[package]]
@@ -2706,18 +2686,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" },
]
-[[package]]
-name = "typer-slim"
-version = "0.24.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typer", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" },
-]
-
[[package]]
name = "types-aiofiles"
version = "25.1.0.20251011"
@@ -2762,8 +2730,8 @@ name = "uvicorn"
version = "0.42.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
- { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "click", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
+ { name = "h11", marker = "sys_platform == 'darwin' or (sys_platform == 'linux' and extra == 'extra-3-exo-cpu') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda12') or (sys_platform == 'linux' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda12') or (extra == 'extra-3-exo-cpu' and extra == 'extra-3-exo-cuda13') or (extra == 'extra-3-exo-cuda12' and extra == 'extra-3-exo-cuda13')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" }
wheels = [
← 48a922fd fix: map presence_penalty and frequency_penalty from ChatCom
·
back to Exo
·
Extend bench/eval tooling (#1905) f2a0db4e →