- 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.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 87 | 86 | 87 |
| Reading documents | 99 | 100 | 99 |
| Decisions | 98 | 100 | 98 |
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 computer | Runs it? | Version to download |
|---|---|---|
| Graphics card, 8 GB | No | – |
| Graphics card, 12 GB | No | – |
| Graphics card, 16 GB | No | – |
| Graphics card, 24 GB | No | – |
| Graphics card, 32 GB | No | – |
| Mac, 16 GB | No | – |
| Mac, 24 GB | No | – |
| Mac, 32 GB | No | – |
| Mac, 48 GB | No | – |
| Mac, 64 GB | No | – |
| Mac, 96 GB | Yes | Q4 |
| Mac, 128 GB | Yes | Q6 |
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.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.