- GLM 4.7 Flash is a free model from Zhipu AI that you can run on your own computer. In our tests it's decent, with trade-offs: 72 out of 100, #21 of 56.
- It solved 22 of 30 coding jobs and scored 70 on reading documents. On our hardest tasks it scored 43.
- Runs on a 24 GB graphics card or a Mac with 48 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.7 Flash 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.7 Flash scored 70; the best model scored 100.
| Test | Score | Public questions | Secret questions |
|---|---|---|---|
| Coding | 73 | 71 | 74 |
| Reading documents | 70 | 84 | 67 |
| Decisions | 85 | 93 | 82 |
On the 18 hardest tasks (included in the scores above) it scored 43. 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.7 Flash 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 | Yes | Q4 |
| Graphics card, 32 GB | Yes | Q6 |
| Mac, 16 GB | No | – |
| Mac, 24 GB | No | – |
| Mac, 32 GB | No | – |
| Mac, 48 GB | Yes | Q6 |
| Mac, 64 GB | Yes | Q8 |
| Mac, 96 GB | Yes | Q8 |
| Mac, 128 GB | Yes | Q8 |
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.7 Flash 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.7 Flash wrote for our public questions, word for word.
Coding: INI parser (hard) Worked
```python
import re
_SECTION_RE = re.compile(r"^\[([^]]*)\]$")
_INTERP_RE = re.compile(r"\$\{([^}]*)\}")
def _strip_inline_comment(line: str) -> str:
"""Remove an inline comment (' ;' or ' #') unless inside double quotes."""
in_quotes = False
i = 0
n = len(line)
while i < n:
ch = line[i]
if ch == '"':
in_quotes = not in_quotes
elif not in_quotes and ch in ";#" and i > 0 and line[i - 1] in " \t":
return line[:i]
i += 1
return line
def _extract_value(raw_value: str) -> str:
"""Apply inline-comment removal, unquoting, and stripping to a raw value."""
value = _strip_inline_comment(raw_value).strip()
if len(value) >= 2 and value.startswith('"') and value.endswith('"'):
return value[1:-1]
return value
def parse_ini(text: str) -> dict[str, dict[str, str]]:
sections: dict[str, dict[str, str]] = {}
current: str | None = None
last_key: str | None = None
for raw_line in text.splitlines():
stripped = raw_line.strip()
# Rule 3: blank lines and full-line comments are ignored.
if not stripped or stripped[0] in ";#":
continue
# Rule 1: section header (surrounding whitespace allowed).
match = _SECTION_RE.match(stripped)
if match:
current = match.group(1).strip()
sections.setdefault(current, {})
last_key = None
continue
# Rule 6: continuation line (starts with whitespace).
if raw_line[:1] in " \t":
if current is None:
raise ValueError(
f"continuation line before any section: {raw_line!r}"
)
if last_key is None:
raise ValueError(
f"continuation line without a preceding key line: {raw_line!r}"
)
continuation = _strip_inline_comment(raw_line).strip()
sections[current][last_key] += "\n" + continuation
continue
# Key line: must be inside a section (rule 10).
if current is None:
raise ValueError(f"key line before any section header: {raw_line!r}")
# Rule 2: split on the FIRST '=' or ':', whichever comes first.
eq = raw_line.find("=")
colon = raw_line.find(":")
candidates = [p for p in (eq, colon) if p != -1]
if not candidates:
raise ValueError(f"malformed line: {raw_line!r}")
pos = min(candidates)
key = raw_line[:pos].strip().lower()
value = _extract_value(raw_line[pos + 1 :])
if not key:
raise ValueError(f"malformed line (empty key): {raw_line!r}")
# Rule 7: later value wins.
sections[current][key] = value
last_key = key
# Rule 8: DEFAULT's keys are inherited by every other section.
defaults = sections.pop("DEFAULT", {})
effective: dict[str, dict[str, str]] = {
name: dict(defaults) for name in sections
}
for name, pairs in sections.items():
effective[name].update(pairs)
# Rule 9: recursive interpolation.
resolving: set[tuple[str, str]] = set()
def lookup(section: str, key: str) -> str:
if section not in effective:
raise KeyError(section)
if key not in effective[section]:
raise KeyError(key)
return resolve(section, key)
def resolve(section: str, key: str) -> str:
marker = (section, key)
if marker in resolving:
raise ValueError(
f"interpolation cycle detected involving {section}:{key}"
)
resolving.add(marker)
try:
raw = effective[section][key]
def substitute(m: re.Match[str]) -> str:
ref = m.group(1)
if ":" in ref:
sec, k = ref.split(":", 1)
return lookup(sec, k.strip().lower())
return lookup(section, ref.strip().lower())
return _INTERP_RE.sub(substitute, raw)
finally:
resolving.discard(marker)
for name, pairs in effective.items():
for key in pairs:
pairs[key] = resolve(name, key)
return effective
``` Coding: TTL LRU (hard) Didn't work
```python
from typing import Any, Callable, Hashable
class TTLCache:
"""An LRU cache whose entries also expire after a fixed TTL.
All time is obtained from the injected zero-argument ``clock`` callable;
the real system time is never read.
"""
def __init__(self, capacity: int, ttl: float, clock: Callable[[], float]):
if capacity < 0:
raise ValueError("capacity must be non-negative")
if ttl < 0:
raise ValueError("ttl must be non-negative")
self._capacity = capacity
self._ttl = ttl
self._clock = clock
# key -> [value, expiry]; plain dict preserves insertion order, so the
# front of the dict is the least recently used entry.
self._data: "dict[Hashable, list]" = {}
self._hits = 0
self._misses = 0
self._evictions = 0
self._expirations = 0
def _purge_expired(self, now: float) -> None:
"""Remove every expired entry, counting each as an expiration."""
expired_keys = [key for key, (_, expiry) in self._data.items() if now >= expiry]
for key in expired_keys:
del self._data[key]
self._expirations += 1
def put(self, key: Hashable, value: Any) -> None:
now = self._clock()
entry = self._data.get(key)
if entry is not None:
if now >= entry[1]:
# Existing entry has expired; it no longer counts as a replace.
del self._data[key]
self._expirations += 1
else:
# Replace: refresh both expiry and recency.
entry[0] = value
entry[1] = now + self._ttl
self._data.move_to_end(key)
return
# Inserting a NEW key.
if self._capacity == 0:
return
if len(self._data) >= self._capacity:
# First try to make room by dropping expired entries.
self._purge_expired(now)
# If still full, evict the least recently used entry.
while len(self._data) >= self._capacity:
self._data.popitem(last=False)
self._evictions += 1
self._data[key] = [value, now + self._ttl]
def get(self, key: Hashable, default: Any = None) -> Any:
now = self._clock()
entry = self._data.get(key)
if entry is None:
self._misses += 1
return default
if now >= entry[1]:
# Expired: remove it; counts as an expiration AND a miss.
del self._data[key]
self._expirations += 1
self._misses += 1
return default
# Hit: mark most recently used.
self._data.move_to_end(key)
self._hits += 1
return entry[0]
def __len__(self) -> int:
"""Number of non-expired entries at the current time (no stat changes)."""
now = self._clock()
return sum(1 for _, expiry in self._data.values() if now < expiry)
def stats(self) -> dict:
return {
"hits": self._hits,
"misses": self._misses,
"evictions": self._evictions,
"expirations": self._expirations,
}
``` Coding: Parse duration Worked
```python
import re
_DURATION_RE = re.compile(
r"""^\s*
(?:(\d+)\s*h\s*)? # hours
(?:(\d+)\s*m\s*)? # minutes
(?:(\d+)\s*s\s*)? # seconds
$""",
re.VERBOSE | re.IGNORECASE,
)
def parse_duration(s: str) -> int:
"""Convert a duration string like '1h 30m 10s' into total seconds.
Supported units: h, m, s (case-insensitive). Each unit may appear at
most once, and must appear in the order hours, minutes, seconds.
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("duration must be a string")
match = _DURATION_RE.fullmatch(s)
if match is None or all(group is None for group in match.groups()):
raise ValueError(f"invalid duration string: {s!r}")
hours, minutes, seconds = (int(g) if g is not None else 0 for g in match.groups())
return hours * 3600 + minutes * 60 + seconds
``` Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
if not nums:
return ""
s = sorted(set(nums))
parts: list[str] = []
i = 0
n = len(s)
while i < n:
j = i
while j + 1 < n and s[j + 1] == s[j] + 1:
j += 1
run_length = j - i + 1
if run_length >= 3:
parts.append(f"{s[i]}..{s[j]}")
i = j + 1
else:
parts.append(str(s[i]))
i += 1
return ",".join(parts)
``` Coding: Top customers Worked
```python
import re
_AMOUNT_RE = re.compile(r"^\s*([+-]?)\s*\$?\s*([0-9][0-9,]*(\.[0-9]+)?|\.[0-9]+)\s*$")
def _parse_amount(raw) -> float | None:
if raw is None:
return None
s = str(raw).strip()
if not s:
return None
match = _AMOUNT_RE.match(s)
if not match:
return None
sign, digits = match.group(1), match.group(2)
try:
value = float(digits.replace(",", ""))
except ValueError:
return None
return -value if sign == "-" else value
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
if n <= 0:
return []
display_names: dict[str, str] = {}
totals: dict[str, float] = {}
for row in rows:
if not isinstance(row, dict):
continue
raw_name = row.get("customer")
if raw_name is None:
continue
name = str(raw_name).strip()
if not name:
continue
amount = _parse_amount(row.get("amount"))
if amount is None:
continue
key = name.casefold()
if key not in totals:
display_names[key] = name
totals[key] = 0.0
totals[key] += amount
ranked = sorted(
((display_names[key], round(total, 2)) for key, total in totals.items()),
key=lambda item: (-item[1], item[0]),
)
return ranked[: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, drop others
normalized = unicodedata.normalize('NFKD', title)
ascii_part = ''.join(c for c in normalized if ord(c) < 128)
# Step 2: Lowercase
slug = ascii_part.lower()
# Step 3: Replace runs of non-alnum with a single hyphen, strip
slug = re.sub(r'[^a-z0-9]+', '-', slug).strip('-')
# Early exit if already short enough
if len(slug) <= max_len:
return slug
# Step 4: Truncate if needed
words = slug.split('-')
# If the first word itself exceeds max_len, hard cut
if len(words[0]) > max_len:
return slug[:max_len].rstrip('-')
# Otherwise keep whole words as long as they fit
truncated_parts = []
total_len = 0
for i, word in enumerate(words):
if i == 0:
truncated_parts.append(word)
total_len = len(word)
else:
# Adding this word with a preceding hyphen would exceed?
if total_len + 1 + len(word) > max_len:
break
truncated_parts.append('-')
truncated_parts.append(word)
total_len += 1 + len(word)
slug = '-'.join(truncated_parts)
return slug
``` Coding: Token bucket Worked
```python
import threading
class TokenBucket:
"""A token-bucket rate limiter driven by an injected clock.
The bucket starts full. Tokens refill continuously at
``refill_per_sec`` based on elapsed clock time, capped at
``capacity``. All time is obtained from the zero-argument
callable ``clock`` (returning seconds as a float); ``time.time()``
is never called directly.
"""
__slots__ = ("_capacity", "_refill_per_sec", "_clock", "_tokens", "_last_refill", "_lock")
def __init__(self, capacity: float, refill_per_sec: float, clock) -> None:
"""
Args:
capacity: Maximum number of tokens the bucket can hold.
refill_per_sec: Rate at which tokens replenish per second.
clock: Zero-argument callable returning the current time in
seconds (float).
"""
self._capacity = float(capacity)
self._refill_per_sec = float(refill_per_sec)
self._clock = clock
self._tokens = self._capacity
self._last_refill = self._clock()
self._lock = threading.Lock()
@property
def capacity(self) -> float:
"""Maximum token count the bucket can hold."""
return self._capacity
@property
def refill_per_sec(self) -> float:
"""Token replenishment rate per second."""
return self._refill_per_sec
@property
def tokens(self) -> float:
"""Current token count, after applying any pending refill."""
with self._lock:
self._refill_locked()
return self._tokens
def allow(self, cost: float = 1.0) -> bool:
"""Refill the bucket, then try to consume ``cost`` tokens.
Returns True (and subtracts the cost) if enough tokens are
available; otherwise returns False and leaves the balance
unchanged.
"""
cost = float(cost)
with self._lock:
self._refill_locked()
if self._tokens >= cost:
self._tokens -= cost
return True
return False
def _refill_locked(self) -> None:
"""Apply refill based on elapsed time. Caller must hold the lock."""
now = self._clock()
elapsed = now - self._last_refill
if elapsed > 0.0:
self._tokens = min(self._capacity, self._tokens + elapsed * self._refill_per_sec)
self._last_refill = now
elif elapsed < 0.0:
# Clock moved backwards: discard the elapsed delta without
# granting tokens, and resynchronize to the current time.
self._last_refill = now
``` 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
{
"severity": {
"0": 0.0,
"1": 1.0,
"2": 0.0,
"3": 0.0
},
"page": {
"true": 0.0,
"false": 1.0
}
} 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.0,
"false": 1.0
},
"action_needed": {
"true": 0.0,
"false": 1.0
}
}
``` Decisions: Meeting slot (hard) 100% right
```json
{
"slot": {
"A": 0.0,
"B": 1.0,
"C": 0.0,
"D": 0.0
},
"raj_last": {
"true": 1.0,
"false": 0.0
}
}
``` Decisions: Review mixed (hard) 100% right
```json
{
"hardware": {
"true": 1,
"false": 0
},
"support": {
"true": 1,
"false": 0
}
}
``` Decisions: Support checkout down 0% right
```json
{
"1": {
"billing": 0.1,
"technical": 0.9,
"account": 0.0,
"shipping": 0.0,
"sales": 0.0
},
"2": {
"0": 0.0,
"1": 0.0,
"2": 0.0,
"3": 1.0
},
"3": {
"true": 1.0,
"false": 0.0
}
}
``` Decisions: Refund wrong plan 100% right
{
"department": {
"billing": 0.99,
"technical": 0.0,
"account": 0.0,
"shipping": 0.0,
"sales": 0.01
},
"refund": {
"true": 0.99,
"false": 0.01
},
"tone": {
"frustrated": 0.01,
"calm": 0.99
}
} Decisions: Moderation doxxing 100% right
```json
{
"policy": {
"none": 0,
"harassment": 1,
"hate": 0,
"spam": 0,
"self_harm": 0
},
"personal_info": {
"true": 1,
"false": 0
}
}
``` Decisions: Route calendar 100% right
```json
{
"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: Doc invoice missing due 100% right
```json
{
"doc_type": {
"invoice": 0.99,
"resume": 0.01,
"contract": 0.0,
"bank_statement": 0.0,
"other": 0.0
},
"missing_due_date": {
"true": 0.98,
"false": 0.02
}
}
``` Decisions: Phishing paypal 100% right
{
"phishing": {
"true": 0.99,
"false": 0.01
},
"risk": {
"0": 0,
"1": 0,
"2": 0,
"3": 1
}
} Decisions: Pii ssn email 100% right
```json
{
"data_kind": {
"none": 0,
"contact": 0,
"financial": 0.01,
"government_id": 0.99,
"health": 0
},
"sensitive": {
"true": 0.99,
"false": 0.01
}
}
``` Decisions: Review mixed 100% right
{
"sentiment": {
"positive": 0.05,
"neutral": 0.10,
"negative": 0.85
},
"defect": {
"true": 1.0,
"false": 0.0
},
"recommend": {
"true": 0.02,
"false": 0.98
}
} Documents: Saas escalator (hard) 30% right
```json
{
"year2_price_per_seat_month": 46.53,
"year3_price_per_seat_month": 49.37,
"year1_invoice": 58320.00,
"year2_invoice": 60302.88,
"addon_months_billed": 6,
"addon_invoice": 37968.48,
"year3_invoice": 63983.52,
"year3_discount_percent": 10,
"total_contract_value": 220574.88,
"contract_end_date": "2027-02-28"
}
``` Documents: Expense thread 100% 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": 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 91% 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-15T10:30:00Z",
"reissued_invoice": "INV-2025-0812"
} Documents: Sales footnotes 100% right
```json
{
"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: 31B parameters. First tested OCT 10.
Comments
Sign in with GitHub to comment. Spam and abuse are hidden automatically.