Review · updated OCT 12

Ternary Bonsai 2 27B review: a solid all-rounder

It scored 82 out of 100, #15 of 65. It solved 22 of 30 coding jobs and scored 91 on reading documents. Runs on a 12 GB graphics card or a Mac with 16 GB.

The short version
  • Ternary Bonsai 2 27B is a free model from prism-ml that you can run on your own computer. In our tests it's a solid all-rounder: 82 out of 100, #15 of 65.
  • It solved 22 of 30 coding jobs and scored 91 on reading documents. On our hardest tasks it scored 68.
  • Runs on a 12 GB graphics card or a Mac with 16 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. Ternary Bonsai 2 27B 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. Ternary Bonsai 2 27B scored 91; the best model scored 100.

TestScorePublic questionsSecret questions
Coding738670
Reading documents919889
Decisions979698

On the 18 hardest tasks (included in the scores above) it scored 68. 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?

Ternary Bonsai 2 27B comes in one standard size, which needs about 9.9 GB of memory.

Your computerRuns it?Version to download
Graphics card, 8 GBNo–
Graphics card, 12 GBYesstandard
Graphics card, 16 GBYesstandard
Graphics card, 24 GBYesstandard
Graphics card, 32 GBYesstandard
Mac, 16 GBYesstandard
Mac, 24 GBYesstandard
Mac, 32 GBYesstandard
Mac, 48 GBYesstandard
Mac, 64 GBYesstandard
Mac, 96 GBYesstandard
Mac, 128 GBYesstandard

For normal-length chats. Long documents need more memory. Q4 or Q8? · How we worked this out

How to try it

Search for "Ternary Bonsai 2 27B 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 Ternary Bonsai 2 27B wrote for our public questions, word for word.

Coding: INI parser (hard) Didn't work
Coding: TTL LRU (hard) Worked


```python
from collections import OrderedDict


class TTLCache:
    def __init__(self, capacity: int, ttl: float, clock):
        self._capacity = max(0, capacity)
        self._ttl = ttl
        self._clock = clock
        self._store = OrderedDict()
        self._stats = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "expirations": 0,
        }

    def put(self, key, value) -> None:
        now = self._clock()

        if self._capacity == 0:
            return

        if key in self._store:
            self._store[key] = (value, now + self._ttl)
            self._store.move_to_end(key)
            return

        if len(self._store) + 1 > self._capacity:
            self._purge_expired(now)
            if len(self._store) >= self._capacity:
                self._store.popitem(last=False)
                self._stats["evictions"] += 1

        self._store[key] = (value, now + self._ttl)

    def get(self, key, default=None):
        now = self._clock()

        if key not in self._store:
            self._stats["misses"] += 1
            return default

        value, expiry = self._store[key]

        if now >= expiry:
            del self._store[key]
            self._stats["misses"] += 1
            self._stats["expirations"] += 1
            return default

        self._store.move_to_end(key)
        self._stats["hits"] += 1
        return value

    def __len__(self) -> int:
        now = self._clock()
        return sum(1 for _, expiry in self._store.values() if now < expiry)

    def stats(self) -> dict:
        return {
            "hits": self._stats["hits"],
            "misses": self._stats["misses"],
            "evictions": self._stats["evictions"],
            "expirations": self._stats["expirations"],
        }

    def _purge_expired(self, now) -> None:
        expired = [key for key, (_, expiry) in self._store.items() if now >= expiry]
        if expired:
            for key in expired:
                del self._store[key]
            self._stats["expirations"] += len(expired)
```
Coding: Parse duration Worked


```python
import re

_DURATION_RE = re.compile(
    r"(?:\s*([0-9]+)h)?(?:\s*([0-9]+)m)?(?:\s*([0-9]+)s)?\s*",
    re.IGNORECASE | re.ASCII,
)

def parse_duration(s: str) -> int:
    if not s.strip():
        raise ValueError("duration string must not be empty")

    match = _DURATION_RE.fullmatch(s)
    if match is None:
        raise ValueError(f"invalid duration string: {s!r}")

    hours, minutes, seconds = match.groups()

    return (
        (int(hours) if hours is not None else 0) * 3600
        + (int(minutes) if minutes is not None else 0) * 60
        + (int(seconds) if seconds is not None else 0)
    )
```
Coding: Summarize ranges Worked


```python
def summarize_ranges(nums: list[int]) -> str:
    unique = sorted(set(nums))
    if not unique:
        return ""

    parts = []
    i = 0
    n = len(unique)

    while i < n:
        start = unique[i]
        end = start
        j = i + 1

        while j < n and unique[j] == end + 1:
            end = unique[j]
            j += 1

        length = end - start + 1

        if length >= 3:
            parts.append(f"{start}..{end}")
        elif length == 2:
            parts.append(f"{start},{end}")
        else:
            parts.append(f"{start}")

        i = j

    return ",".join(parts)
```
Coding: Top customers Worked


```python
import re

_AMOUNT_RE = re.compile(
    r"^\s*(?P<sign1>[+-]?)\s*(?:\$)?\s*(?P<sign2>[+-]?)\s*"
    r"(?P<number>\d{1,3}(?:,\d{3})*(?:\.\d*)?|\d+(?:\.\d*)?|\.\d+)\s*$"
)


def _parse_amount(value: object) -> float | None:
    if not isinstance(value, str):
        return None

    match = _AMOUNT_RE.fullmatch(value)
    if match is None:
        return None

    sign1 = match.group("sign1")
    sign2 = match.group("sign2")

    if sign1 and sign2:
        return None

    number = match.group("number").replace(",", "")

    try:
        amount = float(number)
    except (ValueError, OverflowError):
        return None

    sign = sign1 or sign2
    if sign == "-":
        return -amount

    return amount


def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    if n <= 0:
        return []

    totals: dict[str, float] = {}
    display_names: dict[str, str] = {}

    for row in rows:
        if not isinstance(row, dict):
            continue

        customer = row.get("customer")
        if not isinstance(customer, str):
            continue

        name = customer.strip()
        if not name:
            continue

        amount = _parse_amount(row.get("amount"))
        if amount is None:
            continue

        key = name.casefold()
        if key not in display_names:
            display_names[key] = name

        totals[key] = totals.get(key, 0.0) + amount

    results = []
    for key, total in totals.items():
        rounded_total = round(total, 2)
        if rounded_total == 0:
            rounded_total = 0.0
        results.append((display_names[key], rounded_total))

    results.sort(key=lambda item: (-item[1], item[0]))
    return results[:n]
```
Coding: Slugify Worked


```python
import re
import unicodedata


def slugify(title: str, max_len: int = 50) -> str:
    normalized = unicodedata.normalize("NFD", title)
    ascii_text = "".join(ch for ch in normalized if ch.isascii())

    slug = re.sub(r"[^a-z0-9]+", "-", ascii_text.lower()).strip("-")

    if not slug or max_len <= 0:
        return ""

    if len(slug) <= max_len:
        return slug

    words = slug.split("-")

    if len(words[0]) > max_len:
        return words[0][:max_len]

    kept = [words[0]]
    current_len = len(words[0])

    for word in words[1:]:
        candidate_len = current_len + 1 + len(word)
        if candidate_len <= max_len:
            kept.append(word)
            current_len = candidate_len
        else:
            break

    return "-".join(kept)
```
Coding: Token bucket Worked


```python
from collections.abc import Callable


class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock: Callable[[], float]) -> None:
        self._capacity = float(capacity)
        self._refill_per_sec = float(refill_per_sec)
        self._clock = clock
        self._tokens = self._capacity
        self._last_time = self._clock()

    def _refill(self) -> float:
        now = self._clock()
        elapsed = now - self._last_time

        if elapsed > 0.0:
            self._tokens = min(self._capacity, self._tokens + elapsed * self._refill_per_sec)
            self._last_time = now

        return self._tokens

    def allow(self, cost: float = 1) -> bool:
        self._refill()

        if cost <= 0.0:
            return True

        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,
    "1": 1,
    "2": 0,
    "3": 0
  },
  "page": {
    "true": 0,
    "false": 1
  }
}
Decisions: Tool followup (hard) 50% right


{
  "tool": {
    "web_search": 0.0,
    "calculator": 0.02,
    "calendar": 0.98,
    "email": 0.0,
    "none": 0.0
  },
  "confirm": {
    "true": 0.05,
    "false": 0.95
  }
}
Decisions: Legit security alert (hard) 100% right


{
  "phishing": {
    "true": 0.01,
    "false": 0.99
  },
  "action_needed": {
    "true": 0.05,
    "false": 0.95
  }
}
Decisions: Meeting slot (hard) 100% right


{
  "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


{
  "hardware": {
    "true": 0.95,
    "false": 0.05
  },
  "support": {
    "true": 0.85,
    "false": 0.15
  }
}
Decisions: Support checkout down 100% right


{
  "department": {
    "billing": 0.04,
    "technical": 0.95,
    "account": 0.005,
    "shipping": 0.005,
    "sales": 0.005
  },
  "urgency": {
    "0": 0,
    "1": 0,
    "2": 0.03,
    "3": 0.97
  },
  "outage": {
    "true": 0.98,
    "false": 0.02
  }
}
Decisions: Refund wrong plan 100% right


{
  "department": {
    "billing": 0.95,
    "technical": 0.01,
    "account": 0.01,
    "shipping": 0.00,
    "sales": 0.03
  },
  "refund": {
    "true": 0.99,
    "false": 0.01
  },
  "tone": {
    "frustrated": 0.05,
    "calm": 0.95
  }
}
Decisions: Moderation doxxing 100% right


{"policy":{"none":0.02,"harassment":0.98,"hate":0.00,"spam":0.00,"self_harm":0.00},"personal_info":{"true":0.97,"false":0.03}}
Decisions: Route calendar 100% right


{
  "tool": {
    "web_search": 0.02,
    "calculator": 0.0,
    "calendar": 0.94,
    "email": 0.03,
    "none": 0.01
  },
  "confirm": {
    "true": 0.88,
    "false": 0.12
  }
}
Decisions: Doc invoice missing due 100% right


{
  "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.001,
    "1": 0.004,
    "2": 0.045,
    "3": 0.95
  }
}
Decisions: Pii ssn email 100% right


{"data_kind":{"none":0.02,"contact":0.1,"financial":0.03,"government_id":0.85,"health":0.0},"sensitive":{"true":0.99,"false":0.01}}
Decisions: Review mixed 100% right


{
  "sentiment": {
    "positive": 0.1,
    "neutral": 0.0,
    "negative": 0.9
  },
  "defect": {
    "true": 0.99,
    "false": 0.01
  },
  "recommend": {
    "true": 0.02,
    "false": 0.98
  }
}
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 96% 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": 65.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 92% right


{
  "ticket_id": "48213",
  "account_id": "ACC-7731",
  "open_issue": "inventory_sync",
  "resolved_issues": [
    "billing_address",
    "invoice_pdf"
  ],
  "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: 28B parameters. First tested OCT 11.

Models that scored about the same

Comments

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