Review · updated OCT 11

GLM 4.6V review: decent, with trade-offs

It scored 73 out of 100, #19 of 56. It solved 22 of 30 coding jobs and scored 73 on reading documents. Runs on a Mac with 96 GB.

The short version
  • GLM 4.6V is a free model from Zhipu AI that you can run on your own computer. In our tests it's decent, with trade-offs: 73 out of 100, #19 of 56.
  • It solved 22 of 30 coding jobs and scored 73 on reading documents. On our hardest tasks it scored 53.
  • Runs on a Mac with 96 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. GLM 4.6V got 22 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. GLM 4.6V scored 73; the best model scored 100.

TestScorePublic questionsSecret questions
Coding738670
Reading documents737871
Decisions949693

On the 18 hardest tasks (included in the scores above) it scored 53. 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 GLM 4.6V 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 GBNo–
Graphics card, 32 GBNo–
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBNo–
Mac, 48 GBNo–
Mac, 64 GBNo–
Mac, 96 GBYesQ4
Mac, 128 GBYesQ6

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

How to try it

Search for "GLM 4.6V 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 GLM 4.6V wrote for our public questions, word for word.

Coding: INI parser (hard) Didn't work
```python
import re
from typing import Dict, Tuple, Set

def parse_ini(text: str) -> dict[str, dict[str, str]]:
    sections: dict[str, dict[str, str]] = {}
    default_section: dict[str, str] = {}
    current_section: str | None = None
    current_key: str | None = None
    current_value: str = ""
    last_line_was_key: bool = False

    def remove_inline_comments(line: str) -> str:
        in_quotes = False
        result = []
        i = 0
        while i < len(line):
            if line[i] == '"':
                in_quotes = not in_quotes
                result.append(line[i])
                i += 1
            elif not in_quotes and line[i].isspace():
                # Check for comment start
                if i + 1 < len(line) and line[i+1] in (';', '#'):
                    # Comment starts here, cut the line
                    break
                result.append(line[i])
                i += 1
            else:
                result.append(line[i])
                i += 1
        return ''.join(result)

    lines = text.splitlines()
    for line_num, raw_line in enumerate(lines, 1):
        line = raw_line.rstrip('
')
        # Remove inline comments
        line = remove_inline_comments(line)
        # Skip empty lines
        if not line.strip():
            last_line_was_key = False
            continue
        # Check for continuation line
        if last_line_was_key and line.startswith((' ', '\t')):
            # Append to current value
            current_value += '
' + line.lstrip()
            continue
        # Not a continuation line, process as new line
        last_line_was_key = False
        # Section header?
        if line.startswith('[') and line.endswith(']'):
            section_name = line[1:-1].strip()
            current_section = section_name
            current_key = None
            current_value = ""
            sections[section_name] = {}
            continue
        # Key-value line?
        # Find first '=' or ':' that is not inside quotes
        in_quotes = False
        sep_pos = -1
        for i, ch in enumerate(line):
            if ch == '"':
                in_quotes = not in_quotes
            elif not in_quotes and ch in ('=', ':'):
                sep_pos = i
                break
        if sep_pos != -1:
            key_part = line[:sep_pos].strip()
            value_part = line[sep_pos+1:].strip()
            if not key_part:
                raise ValueError(f"Malformed key-value line at line {line_num}")
            key = key_part.lower()
            # Check for quoted value
            if value_part.startswith('"') and value_part.endswith('"'):
                value = value_part[1:-1]
            else:
                value = value_part
            # Store in current section or default
            if current_section is None:
                raise ValueError(f"Key line before any section at line {line_num}")
            if current_section == "DEFAULT":
                default_section[key] = value
            else:
                sections[current_section][key] = value
            current_key = key
            current_value = value
            last_line_was_key = True
            continue
        # If we get here, it's an invalid line
        raise ValueError(f"Malformed line at line {line_num}")

    # Interpolation
    def get_value(section_name: str, key: str) -> str:
        if key in sections.get(section_name, {}):
            return sections[section_name][key]
        if key in default_section:
            return default_section[key]
        raise KeyError(f"Key '{key}' not found in section '{section_name}'")

    def resolve_placeholder(placeholder: str, section_name: str, visited: Set[Tuple[str, str]]) -> str:
        if ':' in placeholder:
            parts = placeholder.split(':', 1)
            ref_section = parts[0]
            ref_key = parts[1]
            if ref_section == "DEFAULT":
                raise KeyError(f"Section 'DEFAULT' not found")
            if ref_section not in sections:
                raise KeyError(f"Section '{ref_section}' not found")
            try:
                ref_value = get_value(ref_section, ref_key)
            except KeyError as e:
                raise KeyError(f"Key '{ref_key}' not found in section '{ref_section}'") from e
        else:
            ref_key = placeholder
            ref_section = section_name
            try:
                ref_value = get_value(section_name, ref_key)
            except KeyError as e:
                raise KeyError(f"Key '{ref_key}' not found in section '{section_name}'") from e
        if (ref_section, ref_key) in visited:
            raise ValueError(f"Circular reference: ${placeholder}")
        visited.add((ref_section, ref_key))
        return interpolate_value(ref_value, ref_section, visited)

    def interpolate_value(value: str, section_name: str, visited: Set[Tuple[str, str]]) -> str:
        # Find all placeholders outside quotes
        in_quotes = False
        result = []
        i = 0
        while i < len(value):
            if value[i] == '"':
                in_quotes = not in_quotes
                result.append(value[i])
                i += 1
            elif not in_quotes and value.startswith('${', i):
                # Extract placeholder
                j = i + 2
                while j < len(value) and value[j] != '}':
                    j += 1
                if j == len(value):
                    raise ValueError(f"Malformed placeholder at line (placeholder not closed)")
                placeholder = value[i+2:j]
                resolved = resolve_placeholder(placeholder, section_name, visited)
                result.append(resolved)
                i = j + 1
            else:
                result.append(value[i])
                i += 1
        return ''.join(result)

    # Apply interpolation to all values
    for section_name, section_dict in sections.items():
        new_section_dict = {}
        for key, value in section_dict.items():
            visited: Set[Tuple[str, str]] = set()
            interpolated = interpolate_value(value, section_name, visited)
            new_section_dict[key] = interpolated
        sections[section_name] = new_section_dict

    return sections
```
Coding: TTL LRU (hard) Worked
```python
from collections import OrderedDict
from typing import Callable, Dict, Any, Tuple

class TTLCache:
    """
    An LRU cache with time-to-live (TTL) expiration.
    
    Parameters
    ----------
    capacity : int
        Maximum number of non-expired entries the cache can hold.
    ttl : float
        Time-to-live in seconds for each entry.
    clock : Callable[[], float]
        Zero-argument callable that returns the current time in seconds.
    """
    
    def __init__(self, capacity: int, ttl: float, clock: Callable[[], float]) -> None:
        if capacity < 0:
            raise ValueError("capacity must be non-negative")
        self.capacity: int = capacity
        self.ttl: float = ttl
        self.clock: Callable[[], float] = clock
        self._cache: "OrderedDict[Any, Tuple[Any, float]]" = OrderedDict()
        self._stats: Dict[str, int] = {"hits": 0, "misses": 0, "evictions": 0, "expirations": 0}
    
    def _now(self) -> float:
        """Return the current time using the provided clock."""
        return self.clock()
    
    def _is_expired(self, expiry: float) -> bool:
        """Check if an entry has expired."""
        return expiry <= self._now()
    
    def _remove_expired(self) -> None:
        """Remove all expired entries from the cache, updating expirations count."""
        now = self._now()
        expired_keys = [key for key, (_, expiry) in self._cache.items() if expiry <= now]
        for key in expired_keys:
            del self._cache[key]
            self._stats["expirations"] += 1
    
    def _evict_lru(self) -> None:
        """Evict the least recently used entry, updating evictions count."""
        if self._cache:
            self._cache.popitem(last=False)
            self._stats["evictions"] += 1
    
    def put(self, key: Any, value: Any) -> None:
        """
        Insert or replace an entry.
        
        If the key already exists, its expiry and recency are refreshed.
        If inserting a new key exceeds capacity, expired entries are removed first;
        if still full, the least recently used entry is evicted.
        """
        if self.capacity == 0:
            return
        
        now = self._now()
        if key in self._cache:
            # Update existing entry: refresh expiry and recency.
            self._cache[key] = (value, now + self.ttl)
            self._cache.move_to_end(key)
        else:
            # New key: first remove expired entries.
            self._remove_expired()
            # If still at capacity, evict LRU.
            if len(self._cache) >= self.capacity:
                self._evict_lru()
            # Insert new entry.
            self._cache[key] = (value, now + self.ttl)
    
    def get(self, key: Any, default: Any = None) -> Any:
        """
        Retrieve an entry if present and not expired.
        
        Marks the entry as most recently used if found and not expired.
        Expired entries are removed and counted as expirations and misses.
        """
        if self.capacity == 0:
            return default
        
        now = self._now()
        if key in self._cache:
            value, expiry = self._cache[key]
            if self._is_expired(expiry):
                # Entry expired: remove and count as expiration and miss.
                del self._cache[key]
                self._stats["expirations"] += 1
                self._stats["misses"] += 1
                return default
            else:
                # Valid entry: count as hit and mark as most recent.
                self._stats["hits"] += 1
                self._cache.move_to_end(key)
                return value
        else:
            self._stats["misses"] += 1
            return default
    
    def __len__(self) -> int:
        """
        Return the number of non-expired entries at the current time.
        Does not modify the cache or stats.
        """
        now = self._now()
        count = 0
        for _, expiry in self._cache.values():
            if expiry > now:
                count += 1
        return count
    
    def stats(self) -> Dict[str, int]:
        """
        Return a copy of the statistics dictionary.
        """
        return self._stats.copy()
```
Coding: Parse duration Worked
```python
import re

def parse_duration(s: str) -> int:
    """
    Convert a duration string into a total number of seconds.

    Supported units: h (hours), m (minutes), s (seconds). Units are case-insensitive.
    Each part is a non-negative integer followed by a unit, e.g. "1h30m", "45s", "2H",
    "1h 5m 10s", "90m". Whitespace is allowed before, after and between parts.
    Units must appear in the order h, m, s and each at most once.

    Raises:
        ValueError: for empty/blank strings, numbers without a unit, unknown units,
                    decimals, repeated units, or units out of order.
    """
    if not isinstance(s, str):
        raise ValueError("Input must be a string")

    # Strip leading/trailing whitespace
    s = s.strip()
    if not s:
        raise ValueError("Empty or blank string")

    # Regular expression to capture integer-unit pairs
    pattern = r'(\d+)([hmsHMS])'
    matches = list(re.finditer(pattern, s))

    # Ensure the entire string (ignoring whitespace) is covered by matches
    # Reconstruct the string from matches and compare with the stripped input
    reconstructed = ''.join(f"{m.group(1)}{m.group(2)}" for m in matches)
    if reconstructed != s.replace(' ', ''):
        raise ValueError("Invalid format: contains characters without a unit or unknown units")

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

    total_seconds = 0
    last_unit = None
    seen_units = set()

    for match in matches:
        num_str, unit = match.group(1), match.group(2).lower()

        # Validate integer part (non-negative, no decimals)
        if not num_str.isdigit():
            raise ValueError(f"Invalid number '{num_str}' in part '{match.group(0)}'")

        num = int(num_str)
        if num < 0:
            raise ValueError(f"Negative number '{num}' in part '{match.group(0)}'")

        # Validate unit order and uniqueness
        if unit in seen_units:
            raise ValueError(f"Repeated unit '{unit}'")
        if last_unit is not None and unit < last_unit:
            raise ValueError(f"Units out of order: '{last_unit}' before '{unit}'")
        seen_units.add(unit)
        last_unit = unit

        # Convert to seconds
        if unit == 'h':
            total_seconds += num * 3600
        elif unit == 'm':
            total_seconds += num * 60
        elif unit == 's':
            total_seconds += num
        else:
            # This should not happen due to regex, but kept for safety
            raise ValueError(f"Unknown unit '{unit}'")

    return total_seconds
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""
    sorted_nums = sorted(set(nums))
    n = len(sorted_nums)
    i = 0
    result = []
    while i < n:
        start = sorted_nums[i]
        j = i
        while j + 1 < n and sorted_nums[j + 1] == sorted_nums[j] + 1:
            j += 1
        end = sorted_nums[j]
        if end - start >= 2:
            result.append(f"{start}..{end}")
        else:
            result.append(str(start))
            if start != end:
                result.append(str(end))
        i = j + 1
    return ",".join(result)
```
Coding: Top customers Worked
```python
from typing import List, Dict, Tuple

def top_customers(rows: List[Dict[str, str]], n: int) -> List[Tuple[str, float]]:
    """
    Compute the top `n` customers by total amount.

    Parameters
    ----------
    rows : list[dict]
        Each dict must contain "customer" and "amount" keys.
    n : int
        Number of top customers to return.

    Returns
    -------
    list[tuple[str, float]]
        List of (customer_name, total_amount) tuples sorted by total descending,
        then name ascending.
    """
    if n <= 0:
        return []

    totals: Dict[str, float] = {}
    original_names: Dict[str, str] = {}

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

        # Skip rows with missing or empty customer/amount
        if not customer or not amount:
            continue

        customer_clean = customer.strip()
        if not customer_clean:
            continue

        # Parse amount: remove $ and commas, then convert to float
        try:
            amount_clean = amount.strip().replace("$", "").replace(",", "")
            amount_val = float(amount_clean)
        except (ValueError, AttributeError):
            continue

        norm_name = customer_clean.lower()
        if norm_name not in totals:
            totals[norm_name] = amount_val
            original_names[norm_name] = customer_clean
        else:
            totals[norm_name] += amount_val

    # Round totals to 2 decimal places
    for name in totals:
        totals[name] = round(totals[name], 2)

    # Build result list
    result = [(original_names[name], totals[name]) for name in totals]

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

    # Return top n entries
    return result[:n]
```
Coding: Slugify Worked
```python
import re
import unicodedata

def slugify(title: str, max_len: int = 50) -> str:
    if not title:
        return ""

    # 1. Transliterate accented characters to ASCII
    normalized = unicodedata.normalize('NFKD', title)
    ascii_str = ''.join(c for c in normalized if unicodedata.category(c) != 'Mn')

    # 2. Lowercase
    ascii_str = ascii_str.lower()

    # 3. Replace non-alphanumeric runs with a single hyphen and strip hyphens
    slug = re.sub(r'[^a-z0-9]+', '-', ascii_str)
    slug = slug.strip('-')

    # 4. Truncate if needed
    if max_len <= 0:
        return ""
    if len(slug) <= max_len:
        return slug

    words = slug.split('-')
    # If the first word alone exceeds max_len, hard‑cut it
    if words and len(words[0]) > max_len:
        return words[0][:max_len]

    # Build the longest prefix of whole words that fits within max_len
    prefix = []
    current_len = 0
    for i, word in enumerate(words):
        word_len = len(word)
        # Adding a hyphen before this word (if not the first) may exceed max_len
        if i > 0 and current_len + 1 + word_len > max_len:
            break
        if i > 0:
            current_len += 1  # hyphen
        current_len += word_len
        prefix.append(word)

    # Ensure no trailing hyphen
    result = '-'.join(prefix)
    return result.rstrip('-')
```
Coding: Token bucket Worked
```python
from __future__ import annotations
from typing import Callable

class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock: Callable[[], float]) -> None:
        if capacity <= 0:
            raise ValueError("capacity must be positive")
        if refill_per_sec < 0:
            raise ValueError("refill_per_sec must be non-negative")
        self._capacity = float(capacity)
        self._refill_per_sec = float(refill_per_sec)
        self._clock = clock
        self._tokens = float(capacity)
        self._last_refill_time = float(clock())

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

    def allow(self, cost: float = 1.0) -> 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) 100% right
{
  "outcome": {
    "full_refund": 0.0,
    "partial_refund": 0.0,
    "store_credit": 1.0,
    "nothing": 0.0
  },
  "defective": {
    "true": 0.0,
    "false": 1.0
  }
}
Decisions: Incident matrix (hard) 100% right
{
  "severity": {
    "0": 0.0,
    "1": 0.95,
    "2": 0.04,
    "3": 0.01
  },
  "page": {
    "true": 0.01,
    "false": 0.99
  }
}
Decisions: Tool followup (hard) 100% right
{
  "tool": {
    "web_search": 0.0,
    "calculator": 0.0,
    "calendar": 1.0,
    "email": 0.0,
    "none": 0.0
  },
  "confirm": {
    "true": 1.0,
    "false": 0.0
  }
}
Decisions: Legit security alert (hard) 100% right
{
  "phishing": {
    "true": 0.01,
    "false": 0.99
  },
  "action_needed": {
    "true": 0.01,
    "false": 0.99
  }
}
Decisions: Meeting slot (hard) 50% right
```json
{
  "slot": {
    "A": 0.5,
    "B": 0.5,
    "C": 0.0,
    "D": 0.0
  },
  "raj_last": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Review mixed (hard) 100% right
{
  "hardware": {
    "true": 0.95,
    "false": 0.05
  },
  "support": {
    "true": 0.95,
    "false": 0.05
  }
}
Decisions: Support checkout down 100% right
{
  "department": {
    "billing": 0.05,
    "technical": 0.90,
    "account": 0.03,
    "shipping": 0.01,
    "sales": 0.01
  },
  "urgency": {
    "0": 0.01,
    "1": 0.01,
    "2": 0.08,
    "3": 0.90
  },
  "outage": {
    "true": 0.95,
    "false": 0.05
  }
}
Decisions: Refund wrong plan 100% right
{
  "department": {
    "billing": 0.95,
    "technical": 0.01,
    "account": 0.01,
    "shipping": 0.01,
    "sales": 0.02
  },
  "refund": {
    "true": 1.0,
    "false": 0.0
  },
  "tone": {
    "frustrated": 0.05,
    "calm": 0.95
  }
}
Decisions: Moderation doxxing 100% right
{
  "policy": {
    "none": 0.05,
    "harassment": 0.95,
    "hate": 0.0,
    "spam": 0.0,
    "self_harm": 0.0
  },
  "personal_info": {
    "true": 0.99,
    "false": 0.01
  }
}
Decisions: Route calendar 100% right
{"tool": {"web_search": 0, "calculator": 0, "calendar": 1, "email": 0, "none": 0}, "confirm": {"true": 1, "false": 0}}
Decisions: Doc invoice missing due 100% right
{
  "doc_type": {
    "invoice": 0.95,
    "resume": 0.01,
    "contract": 0.03,
    "bank_statement": 0.01,
    "other": 0.00
  },
  "missing_due_date": {
    "true": 0.99,
    "false": 0.01
  }
}
Decisions: Phishing paypal 100% right
{
  "phishing": {
    "true": 0.99,
    "false": 0.01
  },
  "risk": {
    "0": 0.01,
    "1": 0.01,
    "2": 0.08,
    "3": 0.90
  }
}
Decisions: Pii ssn email 100% right
{
  "data_kind": {
    "none": 0.01,
    "contact": 0.05,
    "financial": 0.04,
    "government_id": 0.9,
    "health": 0.0
  },
  "sensitive": {
    "true": 0.99,
    "false": 0.01
  }
}
Decisions: Review mixed 100% right
{
  "sentiment": {"positive": 0, "neutral": 0, "negative": 1},
  "defect": {"true": 1, "false": 0},
  "recommend": {"true": 0, "false": 1}
}
Documents: Saas escalator (hard) 60% right
```json
{
  "year2_price_per_seat_month": 47.25,
  "year3_price_per_seat_month": 47.25,
  "year1_invoice": 58320.00,
  "year2_invoice": 61164.00,
  "addon_months_billed": 6,
  "addon_invoice": 45360.00,
  "year3_invoice": 135408.00,
  "year3_discount_percent": 15,
  "total_contract_value": 300252.00,
  "contract_end_date": "2027-02-28"
}
```
Documents: Expense thread 88% 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": 41.04
    },
    {
      "date": "2025-02-25",
      "category": "meals",
      "amount_usd": 229.39
    },
    {
      "date": "2025-02-26",
      "category": "lodging",
      "amount_usd": 466.56
    },
    {
      "date": "2025-02-27",
      "category": "ground_transport",
      "amount_usd": 38.88
    }
  ],
  "rejected_item_count": 1,
  "per_diem_days": 3,
  "per_diem_usd": 195.00,
  "total_reimbursable_usd": 2155.47,
  "approver_email": "priya.raman@corvane.com"
}
```
Documents: Lease amendment 100% right
```json
{
  "tenants": ["Marcus Lin", "Sofia Lin"],
  "landlord": "Ridgeline Property Group LLC",
  "zip": "97205",
  "lease_end": "2025-11-30",
  "original_monthly_rent": 2150,
  "monthly_rent_from_2025_06_01": 2236,
  "late_fee_from_2025_06_01": 111.8,
  "security_deposit": 2150,
  "total_pet_deposits": 800,
  "total_monthly_payment_july_2025": 2306,
  "move_in_payment": 4700
}
```
Documents: Ticket SLA 100% 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-15T15:30",
  "sla_due_utc": "2025-09-15T20:30:00Z",
  "reissued_invoice": "INV-2025-0812"
}
```
Documents: Sales footnotes 44% right
```json
{
  "q3_total_usd": 15346,
  "q2_total_usd": 14464,
  "q2_central_originally_reported_usd": 3047,
  "q2_to_q3_change_pct": 6.1,
  "top_region_q3": "East",
  "fastest_growing_region_q1_to_q3": "International",
  "regions_declining_q2_to_q3": ["East"],
  "international_q3_organic_usd": 1731,
  "west_excluding_mountain_q3_usd": 4201
}
```

Size: 108B parameters. First tested OCT 11.

Models that scored about the same

Comments

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