Review · updated OCT 11

Ling 3.0 Flash review: a solid all-rounder

It scored 94 out of 100, #4 of 56. It solved 28 of 30 coding jobs and scored 95 on reading documents. Runs on a Mac with 128 GB.

The short version
  • Ling 3.0 Flash is a free model from inclusionai that you can run on your own computer. In our tests it's a solid all-rounder: 94 out of 100, #4 of 56.
  • It solved 28 of 30 coding jobs and scored 95 on reading documents. On our hardest tasks it scored 91.
  • Runs on a Mac with 128 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. Ling 3.0 Flash got 28 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. Ling 3.0 Flash scored 95; the best model scored 100.

TestScorePublic questionsSecret questions
Coding9310091
Reading documents958697
Decisions9910099

On the 18 hardest tasks (included in the scores above) it scored 91. This number separates the top models.

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 Ling 3.0 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 GBNo–
Graphics card, 32 GBNo–
Mac, 16 GBNo–
Mac, 24 GBNo–
Mac, 32 GBNo–
Mac, 48 GBNo–
Mac, 64 GBNo–
Mac, 96 GBNo–
Mac, 128 GBYesQ4

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

How to try it

Search for "Ling 3.0 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 Ling 3.0 Flash wrote for our public questions, word for word.

Coding: INI parser (hard) Worked
```python
def parse_ini(text: str) -> dict[str, dict[str, str]]:
    sections: dict[str, dict[str, str]] = {}
    section_order: list[str] = []
    current_section: str | None = None
    prev_was_key = False
    prev_key: str | None = None

    for line in text.split('\n'):
        stripped = line.strip()

        # Rule 3: ignore empty lines and comment lines
        if stripped == '' or (len(stripped) > 0 and stripped[0] in ';#'):
            prev_was_key = False
            prev_key = None
            continue

        # Rule 1: section headers
        if stripped.startswith('[') and stripped.endswith(']'):
            section_name = stripped[1:-1].strip()
            current_section = section_name
            if section_name not in sections:
                sections[section_name] = {}
                section_order.append(section_name)
            prev_was_key = False
            prev_key = None
            continue

        # Rule 6: continuation lines
        if prev_was_key and len(line) > 0 and (line[0] == ' ' or line[0] == '\t'):
            if current_section is None:
                raise ValueError("Continuation line before any section")
            if prev_key is not None:
                sections[current_section][prev_key] += '\n' + stripped
            continue

        # Rule 2 & 10: key-value lines
        eq_pos = line.find('=')
        colon_pos = line.find(':')

        if eq_pos != -1 and colon_pos != -1:
            pos = min(eq_pos, colon_pos)
        elif eq_pos != -1:
            pos = eq_pos
        elif colon_pos != -1:
            pos = colon_pos
        else:
            raise ValueError(f"Malformed line: {line!r}")

        key = line[:pos].strip().lower()
        raw_value = line[pos + 1:]
        value = _process_value(raw_value)

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

        sections[current_section][key] = value
        prev_was_key = True
        prev_key = key

    # Resolve interpolation and build result
    result: dict[str, dict[str, str]] = {}
    for sec in section_order:
        if sec == 'DEFAULT':
            continue
        result[sec] = {}
        if 'DEFAULT' in sections:
            for dkey in sections['DEFAULT']:
                if dkey not in sections.get(sec, {}):
                    result[sec][dkey] = _resolve(sec, dkey, sections, set())
        if sec in sections:
            for key in sections[sec]:
                result[sec][key] = _resolve(sec, key, sections, set())

    return result


def _process_value(raw_value: str) -> str:
    stripped = raw_value.strip()
    if stripped.startswith('"'):
        end_quote = stripped.find('"', 1)
        if end_quote == -1:
            return stripped[1:]
        return stripped[1:end_quote]
    return _strip_comment(stripped)


def _strip_comment(value: str) -> str:
    in_quotes = False
    i = 0
    while i < len(value):
        if value[i] == '"':
            in_quotes = not in_quotes
        elif not in_quotes and value[i] in ';#':
            if i > 0 and value[i - 1] in ' \t':
                return value[:i - 1].rstrip()
        i += 1
    return value


def _resolve(section: str, key: str, sections: dict, visited: set) -> str:
    if (section, key) in visited:
        raise ValueError(f"Cycle detected: {section}:{key}")
    if section in sections and key in sections[section]:
        raw = sections[section][key]
    elif 'DEFAULT' in sections and key in sections['DEFAULT']:
        raw = sections['DEFAULT'][key]
    else:
        raise KeyError(f"Key {key!r} not found in section {section!r}")
    visited.add((section, key))
    result = _interpolate(raw, section, sections, visited)
    visited.discard((section, key))
    return result


def _interpolate(raw: str, current_section: str, sections: dict, visited: set) -> str:
    result = []
    i = 0
    while i < len(raw):
        if raw[i:i + 2] == '${':
            end = raw.find('}', i)
            if end == -1:
                result.append(raw[i:])
                break
            ref = raw[i + 2:end]
            if ':' in ref:
                sec, key = ref.split(':', 1)
                sec = sec.strip()
                key = key.strip().lower()
                if sec not in sections:
                    raise KeyError(f"Section {sec!r} not found")
            else:
                sec = current_section
                key = ref.strip().lower()
            value = _resolve(sec, key, sections, visited)
            result.append(value)
            i = end + 1
        else:
            result.append(raw[i])
            i += 1
    return ''.join(result)
```
Coding: TTL LRU (hard) Worked
```python
from collections import OrderedDict


class TTLCache:
    def __init__(self, capacity: int, ttl: float, clock):
        self._capacity = capacity
        self._ttl = ttl
        self._clock = clock
        self._data = OrderedDict()  # key -> (value, expiry_time)
        self._hits = 0
        self._misses = 0
        self._evictions = 0
        self._expirations = 0

    def put(self, key, value):
        if self._capacity == 0:
            return

        now = self._clock()
        expiry = now + self._ttl

        if key in self._data:
            # Replace: refresh expiry and recency
            self._data[key] = (value, expiry)
            self._data.move_to_end(key)
            return

        # New key: first remove all expired entries
        self._evict_expired()

        # If still at capacity, evict LRU
        if len(self._data) >= self._capacity:
            self._data.popitem(last=False)
            self._evictions += 1

        self._data[key] = (value, expiry)

    def get(self, key, default=None):
        if key in self._data:
            now = self._clock()
            value, expiry = self._data[key]
            if now < expiry:
                # Not expired: hit
                self._data.move_to_end(key)
                self._hits += 1
                return value
            else:
                # Expired: remove, count as expiration AND miss
                del self._data[key]
                self._expirations += 1
                self._misses += 1
                return default

        self._misses += 1
        return default

    def __len__(self):
        now = self._clock()
        return sum(1 for entry in self._data.values() if now < entry[1])

    def stats(self):
        return {
            "hits": self._hits,
            "misses": self._misses,
            "evictions": self._evictions,
            "expirations": self._expirations,
        }

    def _evict_expired(self):
        now = self._clock()
        expired_keys = [k for k, (_, exp) in self._data.items() if now >= exp]
        for k in expired_keys:
            del self._data[k]
            self._expirations += 1
```
Coding: Parse duration Worked
```python
import re


def parse_duration(s: str) -> int:
    if not s or not s.strip():
        raise ValueError("Empty or blank string")

    pattern = re.compile(
        r"^\s*"
        r"(\d+)\s*([hmsHMS])"
        r"(?:\s*(\d+)\s*([hmsHMS]))?"
        r"(?:\s*(\d+)\s*([hmsHMS]))?"
        r"\s*$"
    )

    match = pattern.match(s)
    if not match:
        raise ValueError("Invalid duration string")

    parts: list[tuple[int, str]] = []
    for i in range(1, 7, 2):
        num = match.group(i)
        unit = match.group(i + 1)
        if num is not None:
            parts.append((int(num), unit.lower()))

    unit_priority = {"h": 0, "m": 1, "s": 2}
    seen: set[str] = set()
    prev_priority = -1
    for _num, unit in parts:
        if unit in seen:
            raise ValueError(f"Repeated unit: {unit}")
        seen.add(unit)
        if unit_priority[unit] < prev_priority:
            raise ValueError(f"Units out of order: {unit}")
        prev_priority = unit_priority[unit]

    total = 0
    for num, unit in parts:
        if unit == "h":
            total += num * 3600
        elif unit == "m":
            total += num * 60
        elif unit == "s":
            total += num

    return total
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    if not nums:
        return ""
    
    sorted_nums = sorted(set(nums))
    
    result = []
    start = sorted_nums[0]
    prev = sorted_nums[0]
    
    for num in sorted_nums[1:]:
        if num == prev + 1:
            prev = num
        else:
            if prev - start >= 2:
                result.append(f"{start}..{prev}")
            else:
                for i in range(start, prev + 1):
                    result.append(str(i))
            start = num
            prev = num
    
    if prev - start >= 2:
        result.append(f"{start}..{prev}")
    else:
        for i in range(start, prev + 1):
            result.append(str(i))
    
    return ",".join(result)
```
Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    totals: dict[str, list] = {}

    for row in rows:
        customer = row.get("customer", "")
        if not isinstance(customer, str):
            continue
        customer = customer.strip()
        if not customer:
            continue

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

        try:
            cleaned = amount_str.replace("$", "").replace(",", "")
            amount = float(cleaned)
        except (ValueError, AttributeError):
            continue

        key = customer.lower()
        if key not in totals:
            totals[key] = [customer, 0.0]
        totals[key][1] += amount

    result = [(name, round(total, 2)) for name, total in totals.values()]
    result.sort(key=lambda x: (-x[1], x[0]))

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


def slugify(title: str, max_len: int = 50) -> str:
    # Step 1: Transliterate — NFKD decomposition separates base characters
    # from combining marks; keeping only ASCII drops the marks and any other
    # non-ASCII characters.
    normalized = unicodedata.normalize("NFKD", title)
    ascii_only = "".join(c for c in normalized if ord(c) < 128)

    # Step 2: Lowercase
    ascii_only = ascii_only.lower()

    # Step 3: Replace runs of non-alphanumeric characters with a single "-"
    slug = re.sub(r"[^a-z0-9]+", "-", ascii_only)
    slug = slug.strip("-")

    # Step 4: Truncate to max_len without cutting words
    if len(slug) > max_len:
        words = slug.split("-")
        result_words: list[str] = []
        current_len = 0

        for i, word in enumerate(words):
            if i == 0:
                if len(word) > max_len:
                    result_words.append(word[:max_len])
                    break
                result_words.append(word)
                current_len = len(word)
            else:
                added = 1 + len(word)  # "-" + word
                if current_len + added <= max_len:
                    result_words.append(word)
                    current_len += added
                else:
                    break

        slug = "-".join(result_words)

    # Step 5: Ensure result never ends with "-"
    return slug.strip("-")
```
Coding: Token bucket Worked
```python
class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock):
        self._capacity = float(capacity)
        self._refill_per_sec = float(refill_per_sec)
        self._clock = clock
        self._tokens = float(capacity)
        self._last_time = clock()

    def _refill(self):
        now = self._clock()
        elapsed = now - self._last_time
        self._last_time = now
        self._tokens = min(self._capacity, self._tokens + elapsed * self._refill_per_sec)

    @property
    def tokens(self) -> float:
        self._refill()
        return self._tokens

    def allow(self, cost: float = 1) -> bool:
        self._refill()
        if self._tokens >= cost:
            self._tokens -= cost
            return True
        return False
```
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.05, "3": 0.0}, "page": {"true": 0.0, "false": 1.0}}
Decisions: Tool followup (hard) 100% right
{"confirm": {"true": 0.9, "false": 0.1}, "tool": {"calendar": 0.95, "none": 0.02, "email": 0.01, "web_search": 0.01, "calculator": 0.01}}
Decisions: Legit security alert (hard) 100% right
{"phishing": {"false": 0.98, "true": 0.02}, "action_needed": {"false": 0.97, "true": 0.03}}
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.9, "false": 0.1}}
Decisions: Support checkout down 100% right
{"department": {"billing": 0.35, "technical": 0.6, "account": 0.0, "shipping": 0.0, "sales": 0.05}, "urgency": {"0": 0.0, "1": 0.0, "2": 0.05, "3": 0.95}, "outage": {"true": 0.98, "false": 0.02}}
Decisions: Refund wrong plan 100% right
{"department": {"billing": 0.95, "technical": 0.01, "account": 0.02, "shipping": 0.01, "sales": 0.01}, "refund": {"true": 0.98, "false": 0.02}, "tone": {"calm": 0.97, "frustrated": 0.03}}
Decisions: Moderation doxxing 100% right
{"policy": {"none": 0.0, "harassment": 0.95, "hate": 0.0, "spam": 0.0, "self_harm": 0.05}, "personal_info": {"true": 0.95, "false": 0.05}}
Decisions: Route calendar 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: 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.0, "1": 0.0, "2": 0.0, "3": 1.0}}
Decisions: Pii ssn email 100% right
{
  "data_kind": {
    "none": 0.0,
    "contact": 0.0,
    "financial": 0.0,
    "government_id": 1.0,
    "health": 0.0
  },
  "sensitive": {
    "true": 1.0,
    "false": 0.0
  }
}
Decisions: Review mixed 100% right
{
  "sentiment": {"positive": 0.05, "neutral": 0.05, "negative": 0.90},
  "defect": {"true": 0.95, "false": 0.05},
  "recommend": {"true": 0.02, "false": 0.98}
}
Documents: Saas escalator (hard) 40% right
{"year2_price_per_seat_month":47.75,"year3_price_per_seat_month":47.75,"year1_invoice":58320.0,"year2_invoice":61884.0,"addon_months_billed":6,"addon_invoice":38964.0,"year3_invoice":136374.0,"year3_discount_percent":15,"total_contract_value":295542.0,"contract_end_date":"2027-02-28"}
Documents: Expense thread 100% 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":195.0,"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.0,"monthly_rent_from_2025_06_01":2236.0,"late_fee_from_2025_06_01":111.8,"security_deposit":2150.0,"total_pet_deposits":800.0,"total_monthly_payment_july_2025":2306.0,"move_in_payment":4700.0}
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: 127B parameters. First tested OCT 11.

Models that scored about the same

Comments

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