Review · updated OCT 11

Laguna XS 2.1 review: a solid all-rounder

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

The short version
  • Laguna XS 2.1 is a free model from poolside that you can run on your own computer. In our tests it's a solid all-rounder: 82 out of 100, #12 of 56.
  • It solved 22 of 30 coding jobs and scored 91 on reading documents. On our hardest tasks it scored 56.
  • 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. Laguna XS 2.1 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. Laguna XS 2.1 scored 91; the best model scored 100.

TestScorePublic questionsSecret questions
Coding738670
Reading documents919989
Decisions9710096

On the 18 hardest tasks (included in the scores above) it scored 56. 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 Laguna XS 2.1 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 "Laguna XS 2.1 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 Laguna XS 2.1 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]]:
    sections = {}  # Include DEFAULT section and others
    current_section = None
    pending_key = None  # Track last key for continuation

    def interpolate(s: str, current_section_name: str, sections_dict, path=None):
        if path is None:
            path = set()
        res = []
        i = 0

        while i < len(s):
            if s[i] == '$' and i < len(s) - 1 and s[i+1] == '{':
                j = s.find('}', i + 2)
                if j == -1:
                    raise ValueError(f"Unclosed variable {s[i:]}")
                var_str = s[i+2:j]
                i = j + 1
                if ':' in var_str:
                    sec_part, key_part = var_str.split(':', 1)
                else:
                    sec_part, key_part = None, var_str
                sec = sec_part if sec_part is not None else current_section_name
                key_part = key_part.strip()

                if (sec, key_part) in path:
                    raise ValueError("Circular reference detected")

                new_path = set(path)
                new_path.add((sec, key_part))

                if sec not in sections_dict:
                    raise KeyError(f"Section {sec} not found")

                sec_dict = sections_dict[sec]
                if key_part in sec_dict:
                    val = sec_dict[key_part]
                elif sec == current_section_name and 'DEFAULT' in sections_dict and key_part in sections_dict['DEFAULT']:
                    val = sections_dict['DEFAULT'][key_part]
                else:
                    raise KeyError(f"Key {key_part} not found in {sec}")

                new_val = interpolate(val, sec, sections_dict, new_path)
                res.append(new_val)
            else:
                res.append(s[i])
                i += 1
        return ''.join(res)

    for line in text.split('\n'):
        stripped = line.strip()
        if not stripped or stripped[0] in ('#', ';'):
            continue

        section_line = stripped.startswith('[') and stripped.endswith(']')

        if section_line:
            section_name = stripped[1:-1].strip()
            current_section = section_name
            if current_section not in sections:
                sections[current_section] = {}
            pending_key = None
            continue

        if current_section is None:
            raise ValueError("Key-value line before any section header")

        is_continuation = len(line) != len(line.lstrip())

        if is_continuation:
            if pending_key is None:
                raise ValueError("Continuation line has no preceding key")
            cont_text = line.strip()
            sections[current_section][pending_key] += "\n" + cont_text
        else:
            eq_pos = line.find('=')
            colon_pos = line.find(':')

            split_char = '=' if (eq_pos != -1 and (colon_pos == -1 or eq_pos < colon_pos)) else (':' if colon_pos != -1 else None)

            if split_char is None:
                raise ValueError(f"Malformed line: {line}")

            key_part = line.split(split_char)[0].strip().lower()
            value_part = line.split(split_char, 1)[1].strip()

            if value_part.startswith('"'):
                end_quote = value_part.find('"', 1)
                if end_quote == -1:
                    val = value_part[1:]
                else:
                    val = value_part[1:end_quote]
                value_part = val
            else:
                comment_pos = len(value_part)
                for c in (';', '#'):
                    idx = value_part.find(' ' + c)
                    if idx != -1 and idx < comment_pos:
                        comment_pos = idx
                if comment_pos != len(value_part):
                    value_part = value_part[:comment_pos].strip()

            sections[current_section][key_part] = value_part
            pending_key = key_part

    # Perform interpolation
    for sec_name in list(sections.keys()):
        sec_dict = sections[sec_name]
        for key in list(sec_dict.keys()):
            try:
                new_val = interpolate(sec_dict[key], sec_name, sections)
            except KeyError as e:
                raise KeyError(f"Malformed reference in interpolation for {sec_name}.{key}: {e}") from e
            except ValueError as e:
                raise ValueError(f"Invalid interpolation cycle in {sec_name}.{key}: {e}") from e
            sec_dict[key] = new_val

    # Build the result dict, excluding DEFAULT
    result = {}
    for sec_name in sections:
        if sec_name == 'DEFAULT':
            continue
        merged = {}
        if 'DEFAULT' in sections:
            merged.update(sections['DEFAULT'])
        merged.update(sections[sec_name])
        result[sec_name] = merged

    return result
```
Coding: TTL LRU (hard) Worked
```python
from collections import OrderedDict
from typing import Callable, Dict, Any

class TTLCache:
    def __init__(self, capacity: int, ttl: float, clock: Callable[[], float]):
        self._capacity = capacity
        self._ttl = ttl
        self._clock = clock
        self._entries: OrderedDict[Any, tuple] = OrderedDict()
        self._stats: Dict[str, int] = {
            'hits': 0,
            'misses': 0,
            'evictions': 0,
            'expirations': 0
        }

    def put(self, key, value):
        if self._capacity == 0:
            return
        now = self._clock()
        new_expiry = now + self._ttl

        if key in self._entries:
            del self._entries[key]
        else:
            expired = []
            for k in list(self._entries.keys()):
                _, exp = self._entries[k]
                if exp <= now:
                    expired.append(k)
            for k in expired:
                del self._entries[k]
            self._stats['expirations'] += len(expired)
            if len(self._entries) >= self._capacity:
                if self._entries:
                    lru_key = next(iter(self._entries))
                    del self._entries[lru_key]
                    self._stats['evictions'] += 1
        self._entries[key] = (value, new_expiry)

    def get(self, key, default=None):
        if self._capacity == 0:
            self._stats['misses'] += 1
            return default
        now = self._clock()
        if key not in self._entries:
            self._stats['misses'] += 1
            return default
        val, exp = self._entries[key]
        if now >= exp:
            del self._entries[key]
            self._stats['expirations'] += 1
            self._stats['misses'] += 1
            return default
        else:
            self._stats['hits'] += 1
            del self._entries[key]
            self._entries[key] = (val, exp)
            return val

    def __len__(self):
        if self._capacity == 0:
            return 0
        now = self._clock()
        cnt = 0
        for _, exp in self._entries.values():
            if exp > now:
                cnt += 1
        return cnt

    def stats(self):
        return dict(self._stats)
```
Coding: Parse duration Worked
```python
import re

def parse_duration(s: str) -> int:
    s_cleaned = s.strip().replace(" ", "")
    if not s_cleaned:
        raise ValueError("Empty string")

    if not re.fullmatch(r'^(\d+[hmsHMS])+$', s_cleaned):
        raise ValueError("Invalid format")

    parts = []
    for match in re.finditer(r'(\d+)([hmsHMS])', s_cleaned):
        num_str, unit_str = match.group(1), match.group(2).lower()
        parts.append((num_str, unit_str))

    units = [unit for _, unit in parts]
    if len(units) != len(set(units)):
        raise ValueError("Repeated units")

    order = {'h': 0, 'm': 1, 's': 2}
    previous_order = -1
    for unit in units:
        current_order = order[unit]
        if current_order < previous_order:
            raise ValueError("Units are not in order")
        previous_order = current_order

    total_seconds = 0
    for num_str, unit in parts:
        value = int(num_str)
        if unit == 'h':
            total_seconds += value * 3600
        elif unit == 'm':
            total_seconds += value * 60
        else:
            total_seconds += value

    return total_seconds
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""
    unique_sorted = sorted(set(nums))
    parts = []
    if not unique_sorted:
        return ""
    start = unique_sorted[0]
    end = start
    for num in unique_sorted[1:]:
        if num == end + 1:
            end = num
        else:
            length = end - start + 1
            if length >= 3:
                parts.append(f"{start}..{end}")
            else:
                for n in range(start, end + 1):
                    parts.append(str(n))
            start = num
            end = num
    # Process the final run
    length = end - start + 1
    if length >= 3:
        parts.append(f"{start}..{end}")
    else:
        for n in range(start, end + 1):
            parts.append(str(n))
    return ','.join(parts)
```
Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    customer_data = {}

    for row in rows:
        customer = row.get("customer", "").strip()
        if not customer:
            continue
        amount_str = row.get("amount", "")
        processed_str = amount_str.replace("$", "").replace(",", "")
        try:
            amount = float(processed_str)
        except ValueError:
            continue

        key = customer.lower()
        if key not in customer_data:
            customer_data[key] = {
                "name": customer,
                "total": 0.0,
            }
        customer_data[key]["total"] += amount

    customer_list = [
        (entry["name"], round(entry["total"], 2))
        for entry in customer_data.values()
    ]

    customer_list.sort(key=lambda x: (-x[1], x[0]))

    return customer_list[:n]
```
Coding: Slugify Worked
```python
import re
import unicodedata

def slugify(title: str, max_len: int = 50) -> str:
    # Transliterate accented characters to ASCII and drop other non-ASCII
    translit = unicodedata.normalize('NFKD', title)
    translit = translit.encode('ascii', 'ignore').decode('utf-8')
    # Lowercase
    slug = translit.lower()
    # Replace runs of non-alphanumeric with '-' and strip leading/trailing hyphens
    slug = re.sub(r'[^a-z0-9]+', '-', slug).strip('-')
    
    if len(slug) <= max_len:
        return slug
    
    words = slug.split('-')
    selected = []
    current_length = 0
    for word in words:
        if not selected:
            needed = len(word)
        else:
            needed = current_length + 1 + len(word)
        if needed > max_len:
            break
        selected.append(word)
        current_length = needed
    
    if selected:
        new_slug = '-'.join(selected)
    else:
        new_slug = words[0][:max_len]
    
    return new_slug
```
Coding: Token bucket Worked
```python
class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock):
        self.capacity = capacity
        self.refill_per_sec = refill_per_sec
        self.clock = clock
        self._current_tokens = capacity
        self._last_time = clock()

    def _refill(self):
        now = self.clock()
        time_elapsed = now - self._last_time
        add_tokens = self.refill_per_sec * time_elapsed
        self._current_tokens += add_tokens
        if self._current_tokens > self.capacity:
            self._current_tokens = self.capacity
        self._last_time = now

    def allow(self, cost: float = 1) -> bool:
        self._refill()
        if self._current_tokens >= cost:
            self._current_tokens -= cost
            return True
        return False

    @property
    def tokens(self) -> float:
        self._refill()
        return self._current_tokens
```
Decisions: Refund window (hard) 100% right
{"outcome": {"store_credit": 1.0}, "defective": {"false": 1.0}}
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
{"tool": {"calendar": 0.9, "email": 0.05, "web_search": 0.0, "calculator": 0.0, "none": 0.05}, "confirm": {"true": 0.9, "false": 0.1}}
Decisions: Legit security alert (hard) 100% right
{"phishing": {"true": 0.05, "false": 0.95}, "action_needed": {"true": 0.1, "false": 0.9}}
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": 1.0, "false": 0.0}, "support": {"true": 1.0, "false": 0.0}}
Decisions: Support checkout down 100% right
{"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.0, "3": 1.0}, "outage": {"true": 1.0, "false": 0.0}}
Decisions: Refund wrong plan 100% right
{"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
{"policy": {"harassment": 1.0, "hate": 0.0, "none": 0.0, "self_harm": 0.0, "spam": 0.0}, "personal_info": {"true": 1.0, "false": 0.0}}
Decisions: Route calendar 100% right
{
  "tool": {
    "calendar": 0.95,
    "web_search": 0.02,
    "calculator": 0.01,
    "email": 0.01,
    "none": 0.01
  },
  "confirm": {
    "true": 0.9,
    "false": 0.1
  }
}
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": 1.0, "false": 0.0}, "risk": {"0": 0.0, "1": 0.0, "2": 0.0, "3": 1.0}}
Decisions: Pii ssn email 100% right
{"data_kind": {"none": 0, "contact": 0, "financial": 0.1, "government_id": 0.9, "health": 0}, "sensitive": {"true": 1, "false": 0}}
Decisions: Review mixed 100% right
{"sentiment": {"positive": 0.15, "neutral": 0.05, "negative": 0.8}, "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 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.6},{"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,"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: 33B parameters. First tested OCT 11.

Models that scored about the same

Comments

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