Review · updated OCT 11

GLM 4.7 Flash review: decent, with trade-offs

It scored 72 out of 100, #21 of 56. It solved 22 of 30 coding jobs and scored 70 on reading documents. Runs on a 24 GB graphics card or a Mac with 48 GB.

The short version
  • 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.

TestScorePublic questionsSecret questions
Coding737174
Reading documents708467
Decisions859382

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 computerRuns it?Version to download
Graphics card, 8 GBNo–
Graphics card, 12 GBNo–
Graphics card, 16 GBNo–
Graphics card, 24 GBYesQ4
Graphics card, 32 GBYesQ6
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBNo–
Mac, 48 GBYesQ6
Mac, 64 GBYesQ8
Mac, 96 GBYesQ8
Mac, 128 GBYesQ8

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.

Models that scored about the same

Comments

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