Review · updated OCT 11

Mistral Small 3.2 24B review: not one we'd recommend right now

It scored 44 out of 100, #36 of 56. It solved 12 of 30 coding jobs and scored 47 on reading documents. Runs on a 24 GB graphics card or a Mac with 32 GB.

The short version
  • Mistral Small 3.2 24B is a free model from Mistral AI that you can run on your own computer. In our tests it's not one we'd recommend right now: 44 out of 100, #36 of 56.
  • It solved 12 of 30 coding jobs and scored 47 on reading documents. On our hardest tasks it scored 21.
  • Runs on a 24 GB graphics card or a Mac with 32 GB.

Coding

Our coding test is 30 programming jobs, from small ones like reading time durations or cleaning up messy data to harder ones like a config-file parser or a double-entry ledger. We run each answer against tests the model never sees, and a job only counts if everything passes. Mistral Small 3.2 24B got 12 of 30 right. The best local coders solved 29 of 30.

Reading documents

The second test hands the model things like an expense claim thread, a pay stub or an insurance statement, and asks for specific numbers and dates. Many questions need a bit of math, or noticing a correction further down the email. Mistral Small 3.2 24B scored 47; the best model scored 100.

TestScorePublic questionsSecret questions
Coding404339
Reading documents476044
Decisions838283

On the 18 hardest tasks (included in the scores above) it scored 21. This number separates the top models.

This model did much better on our public questions than on the secret ones. Sometimes that's luck; it can also mean the model has seen the public questions before.

We tested the full-size version online. The smaller versions you download usually score a little lower.

Can your computer run it?

You can download Mistral Small 3.2 24B in several sizes. Smaller versions fit on more computers and lose a little quality, so pick the biggest one that fits.

Your computerRuns it?Version to download
Graphics card, 8 GBNo–
Graphics card, 12 GBNo–
Graphics card, 16 GBNo–
Graphics card, 24 GBYesQ6
Graphics card, 32 GBYesQ8
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBYesQ5
Mac, 48 GBYesQ8
Mac, 64 GBYesQ8
Mac, 96 GBYesQ8
Mac, 128 GBYesQ8

For normal-length chats. Long documents need more memory. Q4 or Q8? · How we worked this out

How to try it

Search for "Mistral Small 3.2 24B GGUF" in LM Studio or on Hugging Face and pick the version that fits your computer. New to this? Start with our Ollama guide.

Want the strongest all-rounder instead? Qwen3.6 27B scored 97.

Its actual answers

What Mistral Small 3.2 24B wrote for our public questions, word for word.

Coding: INI parser (hard) Didn't work
```python
import re
from collections import defaultdict

def parse_ini(text: str) -> dict[str, dict[str, str]]:
    sections = defaultdict(dict)
    current_section = None
    current_key = None
    lines = text.splitlines()
    default_section = {}

    for line in lines:
        stripped_line = line.strip()
        if not stripped_line or stripped_line.startswith((';', '#')):
            continue

        # Check for section header
        section_match = re.match(r'^\s*\[([^\]]+)\]\s*$', line)
        if section_match:
            current_section = section_match.group(1).strip()
            if current_section.lower() == 'default':
                current_section = None
            else:
                sections[current_section] = {}
            continue

        # Check for key-value pair
        if '=' in line and ':' in line:
            sep = min(line.find('='), line.find(':'))
        elif '=' in line:
            sep = line.find('=')
        elif ':' in line:
            sep = line.find(':')
        else:
            raise ValueError(f"Malformed line: {line}")

        key_part = line[:sep].strip().lower()
        value_part = line[sep+1:].strip()

        # Check for inline comment
        inline_comment_match = re.search(r'(?<!\\)[\s]*(;|#).*$', value_part)
        if inline_comment_match:
            value_part = value_part[:inline_comment_match.start()].strip()

        # Check for quoted value
        if value_part.startswith('"') and value_part.endswith('"'):
            value_part = value_part[1:-1]
        else:
            value_part = value_part.strip()

        # Handle continuation lines
        if current_key is not None:
            if line.startswith((' ', '\t')):
                value_part = sections[current_section][current_key] + '\n' + value_part
                sections[current_section][current_key] = value_part
                continue
            else:
                current_key = None

        current_key = key_part
        sections[current_section][current_key] = value_part

    # Process DEFAULT section
    if None in sections:
        default_section = sections.pop(None)

    # Apply interpolation
    for section_name, section in sections.items():
        section.update(default_section)
        for key in list(section.keys()):
            section[key] = interpolate_value(section, key, sections)

    return sections

def interpolate_value(section: dict[str, str], key: str, all_sections: dict[str, dict[str, str]]) -> str:
    value = section[key]
    pattern = re.compile(r'\$\{(?:([^:]+):)?([^}]+)\}')
    while True:
        match = pattern.search(value)
        if not match:
            break
        section_name, inner_key = match.groups()
        if section_name is None:
            if inner_key not in section:
                raise KeyError(f"Missing key: {inner_key}")
            replacement = section[inner_key]
        else:
            if section_name not in all_sections:
                raise KeyError(f"Missing section: {section_name}")
            if inner_key not in all_sections[section_name]:
                raise KeyError(f"Missing key: {inner_key} in section {section_name}")
            replacement = all_sections[section_name][inner_key]
        value = value[:match.start()] + replacement + value[match.end():]

    return value
```
Coding: TTL LRU (hard) Didn't work
```python
from collections import OrderedDict

class TTLCache:
    def __init__(self, capacity: int, ttl: float, clock):
        self.capacity = capacity
        self.ttl = ttl
        self.clock = clock
        self.cache = OrderedDict()
        self.stats = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "expirations": 0
        }

    def _clean_expired(self):
        now = self.clock()
        expired_keys = []
        for key, (value, expiry) in list(self.cache.items()):
            if now >= expiry:
                expired_keys.append(key)
        for key in expired_keys:
            del self.cache[key]
            self.stats["expirations"] += 1

    def put(self, key, value):
        if self.capacity == 0:
            return

        now = self.clock()
        expiry = now + self.ttl

        if key in self.cache:
            # Update existing entry
            del self.cache[key]
            self.cache[key] = (value, expiry)
            self.cache.move_to_end(key)
        else:
            # Insert new entry
            if len(self.cache) >= self.capacity:
                self._clean_expired()
                if len(self.cache) >= self.capacity:
                    evicted_key, _ = self.cache.popitem(last=False)
                    self.stats["evictions"] += 1
            self.cache[key] = (value, expiry)
            self.cache.move_to_end(key)

    def get(self, key, default=None):
        if self.capacity == 0:
            self.stats["misses"] += 1
            return default

        now = self.clock()
        if key in self.cache:
            value, expiry = self.cache[key]
            if now >= expiry:
                del self.cache[key]
                self.stats["expirations"] += 1
                self.stats["misses"] += 1
                return default
            else:
                self.cache.move_to_end(key)
                self.stats["hits"] += 1
                return value
        else:
            self.stats["misses"] += 1
            return default

    def __len__(self):
        now = self.clock()
        return sum(1 for _, (_, expiry) in self.cache.items() if now < expiry)

    def stats(self):
        return self.stats.copy()
```
Coding: Parse duration Didn't work
```python
def parse_duration(s: str) -> int:
    if not s or s.isspace():
        raise ValueError("Empty or blank string")

    parts = []
    for part in s.split():
        if not part:
            continue
        if 'h' in part:
            if part.lower() != 'h':
                raise ValueError("Invalid format for hours")
            parts.append(('h', 1))
        elif 'm' in part:
            if part.lower() != 'm':
                raise ValueError("Invalid format for minutes")
            parts.append(('m', 2))
        elif 's' in part:
            if part.lower() != 's':
                raise ValueError("Invalid format for seconds")
            parts.append(('s', 3))
        else:
            raise ValueError("Unknown unit")

    if not parts:
        raise ValueError("No valid parts found")

    # Check for repeated units
    units = [unit for unit, _ in parts]
    if len(units) != len(set(units)):
        raise ValueError("Repeated units")

    # Check order of units
    expected_order = ['h', 'm', 's']
    for i, (unit, _) in enumerate(parts):
        if i < len(expected_order) and unit != expected_order[i]:
            raise ValueError("Units out of order")

    # Parse numbers
    total_seconds = 0
    for i, (unit, multiplier) in enumerate(parts):
        try:
            num = int(part.split(unit)[0])
        except ValueError:
            raise ValueError("Invalid number format")
        if num < 0:
            raise ValueError("Negative number")
        if unit == 'h':
            total_seconds += num * 3600
        elif unit == 'm':
            total_seconds += num * 60
        elif unit == 's':
            total_seconds += num
    return total_seconds
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""

    unique_sorted = sorted(set(nums))
    result = []
    start = unique_sorted[0]

    for i in range(1, len(unique_sorted)):
        if unique_sorted[i] != unique_sorted[i - 1] + 1:
            if unique_sorted[i - 1] - start >= 2:
                result.append(f"{start}..{unique_sorted[i - 1]}")
            else:
                for num in range(start, unique_sorted[i - 1] + 1):
                    result.append(str(num))
            start = unique_sorted[i]

    # Handle the last range
    if unique_sorted[-1] - start >= 2:
        result.append(f"{start}..{unique_sorted[-1]}")
    else:
        for num in range(start, unique_sorted[-1] + 1):
            result.append(str(num))

    return ",".join(result)
```
Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    customer_totals = {}

    for row in rows:
        customer = row.get("customer", "").strip()
        amount_str = row.get("amount", "")

        if not customer:
            continue

        # Parse the amount
        try:
            amount = 0.0
            # Handle negative amounts
            if amount_str.startswith("-"):
                amount_str = amount_str[1:]
                negative = True
            else:
                negative = False

            # Remove any non-digit characters except decimal point
            cleaned = ''.join(c for c in amount_str if c.isdigit() or c == '.')
            if not cleaned:
                continue

            # Parse the cleaned amount
            amount = float(cleaned)
            if negative:
                amount = -amount
        except (ValueError, TypeError):
            continue

        # Get the first valid customer name (case-insensitive but stored as-is)
        if customer.lower() not in customer_totals:
            customer_totals[customer.lower()] = (customer, 0.0)

        # Add to the total
        name, total = customer_totals[customer.lower()]
        customer_totals[customer.lower()] = (name, total + amount)

    # Prepare the list of (name, total) tuples
    customer_list = [(name, round(total, 2)) for name, total in customer_totals.values()]

    # Sort by total descending, then name ascending
    customer_list.sort(key=lambda x: (-x[1], x[0]))

    # Return the top n customers
    return customer_list[:n]
```
Coding: Slugify Didn't work
```python
import unicodedata
import re

def slugify(title: str, max_len: int = 50) -> str:
    # Step 1: Transliterate accented characters to ASCII and drop non-ASCII
    normalized = unicodedata.normalize('NFKD', title)
    ascii_chars = []
    for c in normalized:
        if unicodedata.category(c) == 'Mn':
            continue
        if ord(c) < 128:
            ascii_chars.append(c)
    ascii_str = ''.join(ascii_chars)

    # Step 2: Lowercase
    lower_str = ascii_str.lower()

    # Step 3: Replace non-alphanumeric runs with "-"
    slug = re.sub(r'[^a-z0-9]+', '-', lower_str)
    slug = slug.strip('-')

    # Step 4: Truncate to max_len if needed
    if len(slug) > max_len:
        words = slug.split('-')
        truncated = []
        remaining = max_len
        for word in words:
            if remaining >= len(word) + 1:  # +1 for the potential "-"
                truncated.append(word)
                remaining -= len(word) + 1
            else:
                break
        if not truncated:
            # First word is too long, hard-cut it
            truncated = [slug[:max_len]]
        slug = '-'.join(truncated)

    return slug
```
Coding: Token bucket Worked
```python
import time
from typing import Callable

class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock: Callable[[], float]):
        if capacity <= 0 or refill_per_sec < 0:
            raise ValueError("Capacity must be positive and refill rate must be non-negative")
        self._capacity = capacity
        self._refill_rate = refill_per_sec
        self._clock = clock
        self._last_refill_time = clock()
        self._tokens = capacity

    def _refill(self) -> None:
        now = self._clock()
        elapsed = now - self._last_refill_time
        if elapsed > 0:
            refill_amount = elapsed * self._refill_rate
            self._tokens = min(self._tokens + refill_amount, self._capacity)
            self._last_refill_time = now

    def allow(self, cost: float = 1) -> bool:
        self._refill()
        if self._tokens >= cost:
            self._tokens -= cost
            return True
        return False

    @property
    def tokens(self) -> float:
        self._refill()
        return self._tokens
```
Decisions: Refund window (hard) 50% right
```json
{
  "outcome": {
    "full_refund": 0.0,
    "partial_refund": 0.9,
    "store_credit": 0.1,
    "nothing": 0.0
  },
  "defective": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Decisions: Incident matrix (hard) 0% right
```json
{
  "severity": {
    "0": 0.0,
    "1": 0.1,
    "2": 0.9,
    "3": 0.0
  },
  "page": {
    "true": 0.9,
    "false": 0.1
  }
}
```
Decisions: Tool followup (hard) 100% right
```json
{
  "tool": {
    "calendar": 1.0,
    "web_search": 0.0,
    "calculator": 0.0,
    "email": 0.0,
    "none": 0.0
  },
  "confirm": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Legit security alert (hard) 100% right
```json
{
  "phishing": {
    "true": 0.05,
    "false": 0.95
  },
  "action_needed": {
    "true": 0.1,
    "false": 0.9
  }
}
```
Decisions: Meeting slot (hard) 0% right
```json
{
  "slot": {
    "A": 0,
    "B": 0.25,
    "C": 0.75,
    "D": 0
  },
  "raj_last": {
    "true": 0,
    "false": 1
  }
}
```
Decisions: Review mixed (hard) 100% right
```json
{
  "hardware": {
    "true": 1.0,
    "false": 0.0
  },
  "support": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Support checkout down 100% right
```json
{
  "department": {
    "billing": 0.1,
    "technical": 0.8,
    "account": 0.05,
    "shipping": 0.02,
    "sales": 0.03
  },
  "urgency": {
    "0": 0,
    "1": 0,
    "2": 0.1,
    "3": 0.9
  },
  "outage": {
    "true": 0.9,
    "false": 0.1
  }
}
```
Decisions: Refund wrong plan 100% right
```json
{
  "department": {
    "billing": 0.9,
    "technical": 0.05,
    "account": 0.05,
    "shipping": 0,
    "sales": 0
  },
  "refund": {
    "true": 1,
    "false": 0
  },
  "tone": {
    "frustrated": 0.1,
    "calm": 0.9
  }
}
```
Decisions: Moderation doxxing 100% right
```json
{
  "policy": {
    "none": 0.0,
    "harassment": 0.9,
    "hate": 0.0,
    "spam": 0.0,
    "self_harm": 0.1
  },
  "personal_info": {
    "true": 0.9,
    "false": 0.1
  }
}
```
Decisions: Route calendar 100% right
```json
{
  "tool": {
    "web_search": 0.0,
    "calculator": 0.0,
    "calendar": 0.95,
    "email": 0.05,
    "none": 0.0
  },
  "confirm": {
    "true": 0.8,
    "false": 0.2
  }
}
```
Decisions: Doc invoice missing due 100% right
```json
{
  "doc_type": {
    "invoice": 0.95,
    "resume": 0.01,
    "contract": 0.02,
    "bank_statement": 0.01,
    "other": 0.01
  },
  "missing_due_date": {
    "true": 0.9,
    "false": 0.1
  }
}
```
Decisions: Phishing paypal 100% right
```json
{
  "phishing": {
    "true": 1.0,
    "false": 0.0
  },
  "risk": {
    "0": 0.0,
    "1": 0.0,
    "2": 0.0,
    "3": 1.0
  }
}
```
Decisions: Pii ssn email 100% right
```json
{
  "data_kind": {
    "none": 0.0,
    "contact": 0.2,
    "financial": 0.1,
    "government_id": 0.7,
    "health": 0.0
  },
  "sensitive": {
    "true": 0.7,
    "false": 0.3
  }
}
```
Decisions: Review mixed 100% right
```json
{
  "sentiment": {
    "positive": 0.0,
    "neutral": 0.0,
    "negative": 1.0
  },
  "defect": {
    "true": 1.0,
    "false": 0.0
  },
  "recommend": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Documents: Saas escalator (hard) 30% right
```json
{
  "year2_price_per_seat_month": 46.47,
  "year3_price_per_seat_month": 46.47,
  "year1_invoice": 64800.00,
  "year2_invoice": 67764.00,
  "addon_months_billed": 6,
  "addon_invoice": 11808.00,
  "year3_invoice": 67764.00,
  "year3_discount_percent": 15,
  "total_contract_value": 211336.00,
  "contract_end_date": "2027-02-28"
}
```
Documents: Expense thread 75% right
```json
{
  "employee_id": "EMP-20417",
  "destination_city": "Lisbon",
  "trip_start": "2025-02-24",
  "trip_end": "2025-02-27",
  "approved_items": [
    {
      "date": "2025-02-24",
      "category": "airfare",
      "amount_usd": 1184.60
    },
    {
      "date": "2025-02-24",
      "category": "ground_transport",
      "amount_usd": 38.88
    },
    {
      "date": "2025-02-25",
      "category": "meals",
      "amount_usd": 228.19
    },
    {
      "date": "2025-02-26",
      "category": "lodging",
      "amount_usd": 432.00
    },
    {
      "date": "2025-02-27",
      "category": "ground_transport",
      "amount_usd": 44.77
    }
  ],
  "rejected_item_count": 1,
  "per_diem_days": 2,
  "per_diem_usd": 130.00,
  "total_reimbursable_usd": 2068.44,
  "approver_email": "priya.raman@corvane.com"
}
```
Documents: Lease amendment 92% right
```json
{
  "tenants": ["Marcus Lin", "Sofia Lin"],
  "landlord": "Ridgeline Property Group LLC",
  "zip": "97205",
  "lease_end": "2025-11-30",
  "original_monthly_rent": 2150.00,
  "monthly_rent_from_2025_06_01": 2236.00,
  "late_fee_from_2025_06_01": 107.80,
  "security_deposit": 2150.00,
  "total_pet_deposits": 800.00,
  "total_monthly_payment_july_2025": 2306.00,
  "move_in_payment": 4700.00
}
```
Documents: Ticket SLA 82% right
```json
{
  "ticket_id": "48213",
  "account_id": "ACC-7731",
  "open_issue": "inventory_sync",
  "resolved_issues": ["billing_address"],
  "affected_orders": ["SO-99812", "SO-99820", "SO-99827"],
  "priority": "P2",
  "sla_due_local": "2025-09-12T23:30",
  "sla_due_utc": "2025-09-13T04:30:00Z",
  "reissued_invoice": "INV-2025-0812"
}
```
Documents: Sales footnotes 22% right
```json
{
  "q3_total_usd": 15346,
  "q2_total_usd": 14464,
  "q2_central_originally_reported_usd": 3047,
  "q2_to_q3_change_pct": 5.4,
  "top_region_q3": "East",
  "fastest_growing_region_q1_to_q3": "International",
  "regions_declining_q2_to_q3": ["Central"],
  "international_q3_organic_usd": 1731,
  "west_excluding_mountain_q3_usd": 4201
}
```

Size: 24B parameters. First tested OCT 10.

Models that scored about the same

Comments

Sign in with GitHub to comment. Spam and abuse are hidden automatically.