- 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.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 73 | 86 | 70 |
| Reading documents | 73 | 78 | 71 |
| Decisions | 94 | 96 | 93 |
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 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.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.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.