Review · updated OCT 11

GLM 4.5V review: a solid all-rounder

It scored 93 out of 100, #5 of 56. It solved 26 of 30 coding jobs and scored 99 on reading documents. Runs on a Mac with 96 GB.

The short version
  • GLM 4.5V is a free model from Zhipu AI that you can run on your own computer. In our tests it's a solid all-rounder: 93 out of 100, #5 of 56.
  • It solved 26 of 30 coding jobs and scored 99 on reading documents. On our hardest tasks it scored 89.
  • 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.5V got 26 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.5V scored 99; the best model scored 100.

TestScorePublic questionsSecret questions
Coding878687
Reading documents9910099
Decisions9810098

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

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.5V 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.5V 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.5V wrote for our public questions, word for word.

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


def parse_ini(text: str) -> dict[str, dict[str, str]]:
    """
    Parse INI-formatted text according to the specified dialect rules.
    
    Returns a dictionary mapping section names to dictionaries of key-value pairs.
    The DEFAULT section is not included in the result but its values are inherited.
    """
    sections: dict[str, dict[str, str]] = {}
    current_section: str | None = None
    current_key: str | None = None

    for line in text.split('
'):
        left_stripped = line.lstrip()

        # Rule 3: Ignore empty lines and comment lines (; or # as first non-space char)
        if not left_stripped or left_stripped[0] in (';', '#'):
            current_key = None
            continue

        # Rule 1: Section headers [name] with optional surrounding whitespace
        sec_match = re.match(r'\s*\[\s*(.*?)\s*\]\s*$', line)
        if sec_match:
            section_name = sec_match.group(1)
            current_section = '__DEFAULT__' if section_name == 'DEFAULT' else section_name
            sections.setdefault(current_section, {})
            current_key = None
            continue

        # Rule 10: Key-value pair before any section header is an error
        if current_section is None:
            raise ValueError("Key-value pair encountered before any section header")

        # Rule 6: Continuation line (starts with whitespace, follows a key line)
        if line and line[0].isspace() and current_key is not None:
            sections[current_section][current_key] += '
' + line.strip()
            continue

        # Rule 2: Parse key = value or key : value (split on first = or :)
        eq_pos = line.find('=')
        colon_pos = line.find(':')

        if eq_pos == -1 and colon_pos == -1:
            raise ValueError(f"Malformed line (no '=' or ':' found): {line!r}")

        if eq_pos == -1:
            split_at = colon_pos
        elif colon_pos == -1:
            split_at = eq_pos
        else:
            split_at = min(eq_pos, colon_pos)

        key = line[:split_at].strip().lower()
        if not key:
            raise ValueError(f"Malformed line (empty key): {line!r}")

        raw_value = line[split_at + 1:]
        value = _parse_value(raw_value)

        # Rule 7: Later values overwrite earlier ones
        sections[current_section][key] = value
        current_key = key

    # Rule 8: Handle DEFAULT section inheritance
    default_keys = sections.pop('__DEFAULT__', {})

    result: dict[str, dict[str, str]] = {}
    for section_name, section_data in sections.items():
        merged = dict(default_keys)
        merged.update(section_data)
        result[section_name] = merged

    # Rule 9: Perform interpolation
    final_result: dict[str, dict[str, str]] = {}
    for section_name, section_data in result.items():
        final_result[section_name] = {}
        for key, value in section_data.items():
            final_result[section_name][key] = _interpolate(
                result, section_name, value, frozenset()
            )

    return final_result


def _parse_value(value_str: str) -> str:
    """
    Parse a value string handling quotes and inline comments.
    
    Rule 5: Double-quoted values keep contents exactly (quotes removed).
    Rule 4: Inline comments start at ' ;' or ' #' (not inside quotes).
    """
    value_str = value_str.lstrip()

    if not value_str:
        return ''

    # Rule 5: Quoted value - keep everything inside exactly
    if value_str[0] == '"':
        close_quote = value_str.find('"', 1)
        if close_quote == -1:
            return value_str[1:]
        return value_str[1:close_quote]

    # Rule 4: Unquoted value - look for inline comment (whitespace + ; or #)
    for i, ch in enumerate(value_str):
        if ch in (' ', '\t') and i + 1 < len(value_str) and value_str[i + 1] in (';', '#'):
            return value_str[:i].rstrip()

    return value_str.rstrip()


def _interpolate(
    sections: dict[str, dict[str, str]],
    current_section: str,
    value: str,
    visiting: frozenset
) -> str:
    """
    Recursively resolve ${...} interpolations.
    
    Rule 9: ${key} for same section, ${section:key} for cross-section.
    Raises KeyError for missing references, ValueError for cycles.
    """
    def replace_ref(match: re.Match) -> str:
        ref = match.group(1)

        if ':' in ref:
            target_section, target_key = ref.split(':', 1)
        else:
            target_section = current_section
            target_key = ref

        if target_section not in sections:
            raise KeyError(f"Referenced section '{target_section}' does not exist")
        if target_key not in sections[target_section]:
            raise KeyError(
                f"Referenced key '{target_key}' does not exist in section '{target_section}'"
            )

        ref_node = (target_section, target_key)
        if ref_node in visiting:
            raise ValueError(f"Circular interpolation reference detected: {ref_node}")

        return _interpolate(
            sections,
            target_section,
            sections[target_section][target_key],
            visiting | {ref_node}
        )

    return re.sub(r'\$\{([^}]+)\}', replace_ref, value)
```
Coding: TTL LRU (hard) Worked
```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()  # key -> (value, expiry_time)
        self._hits = 0
        self._misses = 0
        self._evictions = 0
        self._expirations = 0

    def _purge_expired(self):
        """Remove all expired entries, incrementing expiration count."""
        now = self.clock()
        expired_keys = [
            k for k, (_, exp) in self._cache.items() if now >= exp
        ]
        for k in expired_keys:
            del self._cache[k]
            self._expirations += 1

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

        now = self.clock()

        if key in self._cache:
            # Replace existing: refresh expiry and move to most-recent end
            self._cache.move_to_end(key)
            self._cache[key] = (value, now + self.ttl)
        else:
            # New key: first drop any expired entries
            self._purge_expired()

            # If still at capacity, evict LRU (first item)
            if len(self._cache) >= self.capacity:
                lru_key = next(iter(self._cache))
                del self._cache[lru_key]
                self._evictions += 1

            # Insert the new entry at the most-recent end
            self._cache[key] = (value, now + self.ttl)

    def get(self, key, default=None):
        if self.capacity == 0:
            self._misses += 1
            return default

        if key not in self._cache:
            self._misses += 1
            return default

        value, expiry = self._cache[key]
        now = self.clock()

        if now >= expiry:
            # Entry has expired – remove and report as expiration + miss
            del self._cache[key]
            self._expirations += 1
            self._misses += 1
            return default

        # Cache hit – promote to most-recently-used
        self._cache.move_to_end(key)
        self._hits += 1
        return value

    def __len__(self):
        now = self.clock()
        return sum(1 for _, exp in self._cache.values() if now < exp)

    def stats(self):
        return {
            "hits": self._hits,
            "misses": self._misses,
            "evictions": self._evictions,
            "expirations": self._expirations,
        }
```
Coding: Parse duration Worked
```python
import re

def parse_duration(s: str) -> int:
    """Convert a duration string to total seconds."""
    s = s.strip()
    if not s:
        raise ValueError("empty or blank duration string")

    pattern = re.compile(r'(\d+)\s*([hHmMsS])')
    matches = list(pattern.finditer(s))
    if not matches:
        raise ValueError("no valid duration parts found")

    total = 0
    last_order = -1
    order_map = {'h': 0, 'm': 1, 's': 2}

    for m in matches:
        num_str = m.group(1)
        unit = m.group(2).lower()
        num = int(num_str)
        order = order_map[unit]

        if order <= last_order:
            raise ValueError(f"invalid unit order or duplicate unit: {unit}")

        last_order = order

        if unit == 'h':
            total += num * 3600
        elif unit == 'm':
            total += num * 60
        else:  # 's'
            total += num

    # Verify that the entire string consists only of the matched parts and whitespace
    matched_text = ''.join(m.group(0) for m in matches)
    if re.sub(r'\s+', '', s) != re.sub(r'\s+', '', matched_text):
        raise ValueError("invalid characters in duration string")

    return total
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""
    
    # Sort and remove duplicates
    sorted_nums = sorted(set(nums))
    
    result = []
    i = 0
    n = len(sorted_nums)
    
    while i < n:
        start = sorted_nums[i]
        # Find the end of the current run of consecutive numbers
        j = i
        while j + 1 < n and sorted_nums[j + 1] == sorted_nums[j] + 1:
            j += 1
        
        run_length = j - i + 1
        
        if run_length >= 3:
            result.append(f"{start}..{sorted_nums[j]}")
        elif run_length == 2:
            result.append(f"{start},{sorted_nums[j]}")
        else:  # run_length == 1
            result.append(f"{start}")
        
        i = j + 1
    
    return ",".join(result)
```
Coding: Top customers Worked
```python
import re
from collections import defaultdict

def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    def parse_amount(amount_str: str) -> float | None:
        """Parse amount string to float. Returns None if invalid."""
        if not amount_str:
            return None
        
        s = amount_str.strip()
        if not s:
            return None
        
        # Determine sign
        negative = False
        if s.startswith('-'):
            negative = True
            s = s[1:]
        
        # Remove dollar sign
        if s.startswith('$'):
            s = s[1:]
        
        # Remove commas (thousand separators)
        s = s.replace(',', '')
        
        try:
            value = float(s)
            return -value if negative else value
        except ValueError:
            return None
    
    # Store: lowercase_key -> [display_name, running_total]
    customers = {}
    
    for row in rows:
        customer = row.get('customer', '')
        amount_str = row.get('amount', '')
        
        # Trim customer name
        customer = customer.strip()
        
        # Skip empty customer
        if not customer:
            continue
        
        # Parse amount, skip if invalid
        amount = parse_amount(amount_str)
        if amount is None:
            continue
        
        # Case-insensitive key
        key = customer.lower()
        
        if key not in customers:
            # First valid occurrence determines display name
            customers[key] = [customer, 0.0]
        
        customers[key][1] += amount
    
    # Build results with rounded totals
    results = [(name, round(total, 2)) for name, total in customers.values()]
    
    # Sort by total descending, then name ascending
    results.sort(key=lambda x: (-x[1], x[0]))
    
    return results[:n]
```
Coding: Slugify Worked
```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_str = ''.join(
        c for c in normalized
        if not unicodedata.combining(c) and ord(c) < 128
    )

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

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

    # Step 4 & 5: Handle length constraint
    if len(slug) > max_len:
        words = slug.split('-')
        result_words = []
        current_length = 0

        for word in words:
            if result_words:
                potential_length = current_length + 1 + len(word)
            else:
                potential_length = len(word)

            if potential_length <= max_len:
                result_words.append(word)
                current_length = potential_length
            else:
                break

        if not result_words:
            # First word exceeds max_len, hard-cut it
            slug = words[0][:max_len]
        else:
            slug = '-'.join(result_words)

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

class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock: Callable[[], float]) -> None:
        self._capacity = capacity
        self._refill_rate = refill_per_sec
        self._clock = clock
        self._tokens = capacity          # start full
        self._last_time = self._clock()  # initial timestamp

    def _refill(self) -> None:
        """Refill tokens based on elapsed time."""
        now = self._clock()
        elapsed = now - self._last_time
        if elapsed > 0:
            # add tokens, but never exceed capacity
            self._tokens = min(self._capacity,
                               self._tokens + elapsed * self._refill_rate)
            self._last_time = now

    def allow(self, cost: float = 1) -> bool:
        """Try to consume `cost` tokens. Returns True if successful."""
        self._refill()
        if self._tokens >= cost:
            self._tokens -= cost
            return True
        return False

    @property
    def tokens(self) -> float:
        """Current number of tokens in the bucket after refilling."""
        self._refill()
        return self._tokens
```
Decisions: Refund window (hard) 100% right
```json
{
  "outcome": {
    "full_refund": 0,
    "partial_refund": 0,
    "store_credit": 1,
    "nothing": 0
  },
  "defective": {
    "true": 0,
    "false": 1
  }
}
```
Decisions: Incident matrix (hard) 100% right
```json
{
  "severity": {
    "0": 0.01,
    "1": 0.97,
    "2": 0.02,
    "3": 0.0
  },
  "page": {
    "true": 0.02,
    "false": 0.98
    }
}
```
Decisions: Tool followup (hard) 100% right
```json
{
  "tool": {
    "web_search": 0,
    "calculator": 0,
    "calendar": 1,
    "email": 0,
    "none": 0
  },
  "confirm": {
    "true": 1,
    "false": 0
  }
}
```
Decisions: Legit security alert (hard) 100% right
```json
{
  "phishing": {
    "true": 0.01,
    "false": 0.99
  },
  "action_needed": {
    "true": 0.01,
    "false": 0.99
  }
}
```
Decisions: Meeting slot (hard) 100% right
```json
{
  "slot": {
    "A": 0,
    "B": 1,
    "C": 0,
    "D": 0
  },
  "raj_last": {
    "true": 1,
    "false": 0
  }
}
```
Decisions: Review mixed (hard) 100% right
{
  "hardware": {
    "true": 1.0,
    "false": 0.0
  },
  "support": {
    "true": 1.0,
    "false": 0.0
  }
}
Decisions: Support checkout down 100% right
```json
{
  "department": {
    "billing": 0.05,
    "technical": 0.95,
    "account": 0.0,
    "shipping": 0.0,
    "sales": 0.0
  },
  "urgency": {
    "0": 0.0,
    "1": 0.0,
    "2": 0.05,
    "3": 0.95
  },
  "outage": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Refund wrong plan 100% right
```json
{
  "department": {
    "billing": 1.0,
    "technical": 0.0,
    "account": 0.0,
    "shipping": 0.0,
    "sales": 0.0
  },
  "refund": {
    "true": 1.0,
    "false": 0.0
  },
  "tone": {
    "frustrated": 0.0,
    "calm": 1.0
  }
}
```
Decisions: Moderation doxxing 100% right
```json
{
  "policy": {
    "none": 0.01,
    "harassment": 0.99,
    "hate": 0.0,
    "spam": 0.0,
    "self_harm": 0.0
  },
  "personal_info": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Route calendar 100% right
```json
{
  "tool": {
    "web_search": 0.01,
    "calculator": 0.01,
    "calendar": 0.97,
    "email": 0.01,
    "none": 0.0
  },
  "confirm": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Doc invoice missing due 100% right
```json
{
  "doc_type": {
    "invoice": 1.0,
    "resume": 0.0,
    "contract": 0.0,
    "bank_statement": 0.0,
    "other": 0.0
  },
  "missing_due_date": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Phishing paypal 100% right
{
  "phishing": {
    "true": 0.99,
    "false": 0.01
  },
  "risk": {
    "0": 0.0,
    "1": 0.0,
    "2": 0.01,
    "3": 0.99
  }
}
Decisions: Pii ssn email 100% right
```json
{
  "data_kind": {
    "none": 0,
    "contact": 0,
    "financial": 0,
    "government_id": 1,
    "health": 0
  },
  "sensitive": {
    "true": 1,
    "false": 0
  }
}
```
Decisions: Review mixed 100% right
```json
{
  "sentiment": {
    "positive": 0.05,
    "neutral": 0.05,
    "negative": 0.9
  },
  "defect": {
    "true": 1.0,
    "false": 0.0
  },
  "recommend": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Documents: Saas escalator (hard) 100% right
{
  "year2_price_per_seat_month": 47.25,
  "year3_price_per_seat_month": 47.25,
  "year1_invoice": 58320.00,
  "year2_invoice": 61236.00,
  "addon_months_billed": 6,
  "addon_invoice": 38556.00,
  "year3_invoice": 134946.00,
  "year3_discount_percent": 15,
  "total_contract_value": 293058.00,
  "contract_end_date": "2027-02-28"
}
Documents: Expense thread 100% right
{
  "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": 229.39
    },
    {
      "date": "2025-02-26",
      "category": "lodging",
      "amount_usd": 466.56
    },
    {
      "date": "2025-02-27",
      "category": "ground_transport",
      "amount_usd": 44.82
    }
  ],
  "rejected_item_count": 1,
  "per_diem_days": 3,
  "per_diem_usd": 195.00,
  "total_reimbursable_usd": 2159.25,
  "approver_email": "priya.raman@corvane.com"
}
Documents: Lease amendment 100% right
{
  "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": 111.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 100% right
{
  "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 100% right
{
  "q3_total_usd": 15346000,
  "q2_total_usd": 14464000,
  "q2_central_originally_reported_usd": 3047000,
  "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": 1731000,
  "west_excluding_mountain_q3_usd": 4201000
}

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.