Review · updated OCT 11

Hunyuan A13B Instruct review: not one we'd recommend right now

It scored 52 out of 100, #29 of 56. It solved 11 of 30 coding jobs and scored 67 on reading documents. Runs on a Mac with 96 GB.

The short version
  • Hunyuan A13B Instruct is a free model from Tencent that you can run on your own computer. In our tests it's not one we'd recommend right now: 52 out of 100, #29 of 56.
  • It solved 11 of 30 coding jobs and scored 67 on reading documents. On our hardest tasks it scored 28.
  • Runs on a Mac with 96 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. Hunyuan A13B Instruct got 11 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. Hunyuan A13B Instruct scored 67; the best model scored 100.

TestScorePublic questionsSecret questions
Coding375730
Reading documents678363
Decisions746676

On the 18 hardest tasks (included in the scores above) it scored 28. 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 Hunyuan A13B Instruct 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 GBYesQ6
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 "Hunyuan A13B Instruct 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 Hunyuan A13B Instruct wrote for our public questions, word for word.

Coding: INI parser (hard) Didn't work
```python
import re
from typing import Dict


def parse_ini(text: str) -> Dict[str, Dict[str, str]]:
    def interpolate(value_part: str, current_section: str, sections: Dict[str, Dict]) -> str:
        visited = set()
        pattern = re.compile(r'\$\{([^}]+)\}')

        def replace(match: re.Match) -> str:
            content = match.group(1)
            parts = content.split(':', 1)

            # Check for cycles
            if len(parts) == 1:
                key = parts[0]
                if key in visited:
                    raise ValueError(f"Interpolation cycle detected: {visited} -> {key}")
                new_visited = visited.copy()
                new_visited.add(key)
            else:
                section_name, key = parts
                if section_name in visited:
                    raise ValueError(f"Interpolation cycle detected: {visited} -> {section_name}:{key}")
                new_visited = visited.copy()
                new_visited.add(section_name)

            # Resolve value
            if len(parts) == 1:
                # Key lookup in current section or DEFAULT
                val = sections[current_section].get(key)
                if val is None:
                    val = sections.get('DEFAULT', {}).get(key)
            else:
                # Section:key lookup
                section = sections.get(section_name)
                if section is None:
                    raise KeyError(f"Section '{section_name}' not found")
                val = section.get(key)
                if val is None:
                    val = sections.get('DEFAULT', {}).get(key)

            # Handle missing key/section
            if val is None:
                if len(parts) == 1:
                    if val is None:
                        raise KeyError(f"Key '{key}' not found in section '{current_section}' or DEFAULT")
                else:
                    if val is None:
                        raise KeyError(f"Key '{key}' not found in section '{section_name}' or DEFAULT")

            # Recursively resolve if string
            if isinstance(val, str):
                return replace(pattern, val)
            return str(val)

        return replace(pattern, value_part)

    sections = {'DEFAULT': {}}
    result = {}
    current_section = None
    prev_was_key = False
    last_key = None

    lines = [line.rstrip('\n') for line in text.splitlines()]

    for line in lines:
        stripped_line = line.strip()
        if not stripped_line or stripped_line.startswith(';') or stripped_line.startswith('#'):
            continue  # Rule 3: ignore empty/comments

        section_match = re.match(r'\s*\[([^\]]+)\s*\]', line)
        if section_match is not None:
            # Rule 1: process section header
            current_section = section_match.group(1)
            prev_was_key = False
            if current_section != 'DEFAULT' and current_section not in sections:
                sections[current_section] = {}
            continue

        if prev_was_key:
            if line.startswith(' '):
                # Rule 6: continuation line
                continuation_text = line.lstrip()
                current_section_dict = sections.get(current_section)
                if current_section_dict is None:
                    raise ValueError("Section not found (internal error)")
                current_section_dict[last_key] += '\n' + continuation_text
            else:
                raise ValueError(f"Malformed line (continuation without key)")
        else:
            # Rule 10: key line or malformed
            eq_pos = line.find('=')
            colon_pos = line.find(':')
            separator_pos = min(eq_pos, colon_pos) if eq_pos != -1 or colon_pos != -1 else -1

            if separator_pos == -1:
                raise ValueError("Malformed line: no separator found")

            key_part = line[:separator_pos].strip()
            value_part = line[separator_pos+1:].lstrip()  # Rule 2: value stripped

            if not key_part:
                raise ValueError("Malformed line: empty key")

            key = key_part.lower()  # Rule 2: key lowercased
            try:
                value = interpolate(value_part, current_section, sections)
            except (KeyError, ValueError) as e:
                raise ValueError(f"Line {text.splitlines().index(line) + 1}: {e}") from e

            if current_section == 'DEFAULT':
                sections['DEFAULT'][key] = value
            else:
                sections.setdefault(current_section, {})[key] = value

            last_key = key
            prev_was_key = True

    # Build result by merging sections with DEFAULT (Rule 8)
    for section_name in sections:
        if section_name == 'DEFAULT':
            continue
        merged = sections['DEFAULT'].copy()
        merged.update(sections[section_name])
        result[section_name] = merged

    return result
```
Coding: TTL LRU (hard) Didn't work
```python
class TTLCache:
    class Node:
        def __init__(self, key, value, expiry):
            self.key = key
            self.value = value
            self.expiry = expiry
            self.prev = None
            self.next = None

    def __init__(self, capacity: int, ttl: float, clock):
        self.capacity = capacity
        self.ttl = ttl
        self.clock = clock
        self.cache = dict()
        self.head = None  # Most Recently Used
        self.tail = None  # Least Recently Used
        self.stats = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "expirations": 0,
        }

    def _remove_node(self, node):
        if node.prev:
            node.prev.next = node.next
        else:
            self.head = node.next
        if node.next:
            node.next.prev = node.prev
        else:
            self.tail = node.prev

    def _move_to_front(self, node):
        if node == self.head:
            return
        self._remove_node(node)
        node.next = self.head
        self.head.prev = node
        self.head = node
        if node.prev is None:
            self.tail = node

    def _add_to_front(self, key, value, expiry):
        new_node = self.Node(key, value, expiry)
        new_node.next = self.head
        if self.head:
            self.head.prev = new_node
        self.head = new_node
        if self.tail is None:
            self.tail = new_node

    def put(self, key, value):
        if self.capacity == 0:
            return
        now = self.clock()
        if key in self.cache:
            node = self.cache[key]
            node.value = value
            node.expiry = now + self.ttl
            self._move_to_front(node)
        else:
            expired = []
            current = self.head
            while current:
                next_node = current.next
                if current.expiry <= now:
                    expired.append(current)
                    self._remove_node(current)
                current = next_node
            self.stats["expirations"] += len(expired)
            current_size = len(self.cache)
            if current_size + 1 > self.capacity:
                if self.tail:
                    evicted_node = self.tail
                    self._remove_node(evicted_node)
                    del self.cache[evicted_node.key]
                    self.stats["evictions"] += 1
            self._add_to_front(key, value, now + self.ttl)

    def get(self, key, default=None):
        if self.capacity == 0:
            return default
        now = self.clock()
        if key not in self.cache:
            self.stats["misses"] += 1
            return default
        node = self.cache[key]
        if node.expiry <= now:
            self._remove_node(node)
            del self.cache[key]
            self.stats["expirations"] += 1
            self.stats["misses"] += 1
            return default
        self._move_to_front(node)
        self.stats["hits"] += 1
        return node.value

    def __len__(self):
        if self.capacity == 0:
            return 0
        now = self.clock()
        count = 0
        current = self.head
        while current:
            if current.expiry > now:
                count += 1
            current = current.next
        return count

    def stats(self):
        return {
            "hits": self.stats["hits"],
            "misses": self.stats["misses"],
            "evictions": self.stats["evictions"],
            "expirations": self.stats["expirations"],
        }
```
Coding: Parse duration Worked
```python
def parse_duration(s: str) -> int:
    if not s.strip():
        raise ValueError("empty string")
    
    i = 0
    parts = []
    n = len(s)
    units_order = {'h', 'm', 's'}
    priorities = {'h': 0, 'm': 1, 's': 2}
    
    while i < n:
        # Skip leading whitespace
        while i < n and s[i].isspace():
            i += 1
        if i >= n:
            break  # Shouldn't occur due to initial check but handle defensively
        
        # Extract number part
        j = i
        while j < n and s[j].isdigit():
            j += 1
        if j == i:
            raise ValueError("number without unit")
        num = int(s[i:j])
        i = j
        
        # Skip whitespace after number
        while i < n and s[i].isspace():
            i += 1
        if i >= n:
            raise ValueError("number without unit")
        
        # Extract unit
        unit_char = s[i]
        unit = unit_char.lower()
        if unit not in units_order:
            raise ValueError("unknown unit")
        i += 1
        
        # Validate unit order and uniqueness
        if parts:
            prev_unit = parts[-1][1]
            prev_prio = priorities[prev_unit]
            curr_prio = priorities[unit]
            if curr_prio < prev_prio:
                raise ValueError("units out of order")
            if curr_prio == prev_prio:
                raise ValueError("repeated unit")
        
        parts.append((num, unit))
    
    # Calculate total seconds
    total = 0
    for num, unit in parts:
        if unit == 'h':
            total += num * 3600
        elif unit == 'm':
            total += num * 60
        else:
            total += num
    
    return total
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums: list[int]) -> str:
    sorted_unique = sorted(set(nums))
    if not sorted_unique:
        return ""
    
    groups = []
    current_group = [sorted_unique[0]]
    for num in sorted_unique[1:]:
        if num == current_group[-1] + 1:
            current_group.append(num)
        else:
            groups.append(current_group)
            current_group = [num]
    groups.append(current_group)
    
    result = []
    for group in groups:
        if len(group) >= 3:
            result.append(f"{group[0]}..{group[-1]}")
        else:
            result.extend(map(str, group))
    
    return ",".join(result)
```
Coding: Top customers Worked
```python
def top_customers(rows: list[dict], n: int) -> list[tuple[str, float]]:
    customer_totals = {}  # Key: lowercase trimmed name, Value: (original trimmed name, total)
    
    for row in rows:
        # Process customer name
        customer = row.get("customer", "").strip()
        if not customer:
            continue  # Skip rows with empty customer after trimming
        
        # Process and parse amount
        amount_str = row.get("amount", "").strip()
        sign = 1
        amount_str_processed = amount_str
        
        # Handle negative amounts
        if amount_str_processed.startswith('-'):
            sign = -1
            amount_str_processed = amount_str_processed[1:]
        
        # Remove dollar sign if present
        if amount_str_processed.startswith('$'):
            amount_str_processed = amount_str_processed[1:]
        
        # Remove commas (thousand separators)
        amount_str_clean = amount_str_processed.replace(',', '')
        
        # Validate and convert to float
        try:
            amount = sign * float(amount_str_clean)
        except ValueError:
            continue  # Skip rows with unparseable amounts
        
        # Update customer totals
        key = customer.lower()
        if key not in customer_totals:
            customer_totals[key] = (customer, amount)
        else:
            existing_name, existing_total = customer_totals[key]
            customer_totals[key] = (existing_name, existing_total + amount)
    
    # Extract and sort customers
    sorted_customers = sorted(
        [(name, total) for (_, (name, total)) in customer_totals.items()],
        key=lambda x: (-x[1], x[0])  # Descending total, ascending name
    )
    
    # Take top n and round totals to 2 decimals
    top_n = sorted_customers[:n]
    return [(name, round(total, 2)) for name, total in top_n]
```
Coding: Slugify Didn't work
```python
import unicodedata


def slugify(title: str, max_len: int = 50) -> str:
    # Step 1 & 2: Transliterate accented characters to ASCII, remove non-ASCII, and lowercase
    normalized = unicodedata.normalize('NFKD', title)
    transliterated = []
    for c in normalized:
        if ord(c) < 128:  # Keep only ASCII characters
            transliterated.append(c.lower())
    transliterated_str = ''.join(transliterated)
    
    # Step 3: Replace non-alphanumeric characters with '-', strip leading/trailing dashes
    slug = re.sub(r'[^a-z0-9]+', '-', transliterated_str).strip('-')
    
    # Handle empty case immediately
    if not slug:
        return slug
    
    # Step 4: Truncate to max_len without splitting words
    words = slug.split('-')
    current_length = 0
    result_words = []
    for word in words:
        # Calculate tentative length if we add this word (including dashes)
        tentative_length = current_length + len(word) + len(result_words)
        if tentative_length > max_len:
            if not result_words:
                # First word is too long, cut to max_len (handle max_len=0)
                return word[:max_len] if max_len > 0 else ''
            else:
                # Can't add this word, return current prefix
                return '-'.join(result_words)
        else:
            result_words.append(word)
            current_length = tentative_length
    
    # Join the remaining valid words
    return '-'.join(result_words)
```
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._last_refill_time = clock()
        self._tokens = capacity  # Start with full bucket

    def _refill(self) -> float:
        """Refill the bucket based on elapsed time and update state."""
        current_time = self.clock()
        elapsed = current_time - self._last_refill_time
        tokens_added = elapsed * self.refill_per_sec
        self._tokens = min(self._tokens + tokens_added, self.capacity)
        self._last_refill_time = current_time
        return self._tokens

    @property
    def tokens(self) -> float:
        """Return current token count after refill."""
        return self._refill()

    def allow(self, cost: float = 1) -> bool:
        """Refill then check/allow consumption of 'cost' tokens."""
        tokens = self._refill()
        if tokens >= cost:
            self._tokens -= cost
            return True
        return False
```
Decisions: Refund window (hard) 100% right
{"outcome": {"full_refund": 0, "partial_refund": 0, "store_credit": 1, "nothing": 0}, "defective": {"true": 0, "false": 1}}
Decisions: Incident matrix (hard) 50% right
{"severity": {"3": 1.0}, "page": {"false": 1.0}}
Decisions: Tool followup (hard) 0% right
```json
{
  "tool": "calendar",
  "confirm": "true"
}
```
Decisions: Legit security alert (hard) 0% right
{"phishing": false, "action_needed": false}
Decisions: Meeting slot (hard) 50% right
{"slot": {"A": 0.0, "B": 1.0, "C": 0.0, "D": 0.0}, "raj_last": true}
Decisions: Review mixed (hard) 100% right
{"hardware": {"true": 1.0}, "support": {"true": 1.0}}
Decisions: Support checkout down 67% right
{
  "department": {
    "billing": 0.9,
    "technical": 0.05,
    "account": 0.03,
    "shipping": 0.01,
    "sales": 0.01
  },
  "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}, "refund": {"true": 1.0}, "tone": {"calm": 1.0}}
Decisions: Moderation doxxing 100% right
{"policy": {"harassment": 1.0}, "personal_info": {"true": 1.0}}
Decisions: Route calendar 0% right
```json
{
  "tool": "calendar",
  "confirm": "true"
}
```
Decisions: Doc invoice missing due 50% right
{
  "doc_type": {"invoice": 0.99, "resume": 0.0, "contract": 0.01, "bank_statement": 0.0, "other": 0.0},
  "missing_due_date": {"true": 0.05, "false": 0.95}
}
Decisions: Phishing paypal 100% right
{"phishing": {"true": 0.95, "false": 0.05}, "risk": {"0": 0.02, "1": 0.02, "2": 0.02, "3": 0.94}}
Decisions: Pii ssn email 100% right
{"data_kind": {"none": 0.05, "contact": 0.3, "financial": 0.1, "government_id": 0.5, "health": 0.05}, "sensitive": {"true": 1.0, "false": 0.0}}
Decisions: Review mixed 100% right
{"sentiment": {"negative": 1.0}, "defect": {"true": 1.0}, "recommend": {"false": 1.0}}
Documents: Saas escalator (hard) 30% right
{"year2_price_per_seat_month": 47.75, "year3_price_per_seat_month": 47.75, "year1_invoice": 4860.0, "year2_invoice": 5157.0, "addon_months_billed": 6, "addon_invoice": 38964.0, "year3_invoice": 11364.5, "year3_discount_percent": 15.0, "total_contract_value": 60345.5, "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.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 83% 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-15T19: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: 80B parameters. First tested OCT 11.

Models that scored about the same

Comments

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