independent · not affiliated with Anthropic · nothing leaves your machine

Which Claude model did Claude Code actually serve you?

You pinned a model, but you suspect a different (cheaper) tier is quietly being served. Stop guessing from the UI — your own session logs record, per turn, the exact model ID that was served. Two copy-paste commands turn that into a timestamped timeline and a per-model tally. Everything runs locally against files already on your disk.

This is a recurring worry — e.g. anthropics/claude-code #65889, and the routing/quality issues Anthropic itself acknowledged in April 2026. The point of this page is not to assert a cause. It's to replace "I think it felt different" with "here is the served model ID on each turn, with timestamps, from my logs."

Where the ground truth lives

Claude Code writes one JSON object per line into a session log:

~/.claude/projects/<project>/<session-id>.jsonl

For every assistant turn, the line has type":"assistant", and inside it message.model is the model ID that was actually served for that turn. The same line carries a top-level timestamp (UTC, ISO 8601). So message.model — not the UI, not the status line — is the canonical record of what you got.

Exclude <synthetic>. Some lines have message.model equal to <synthetic>. Those are injected/synthetic turns (e.g. interface-generated messages), not a model that served you. Counting them would pollute the tally, so both commands below drop them.

1 · Per-turn timeline of the served model (with timestamps)

Across every session, list each assistant turn's served model next to its timestamp, sorted in time order. Paste as-is:

for f in ~/.claude/projects/*/*.jsonl; do
  python3 - "$f" <<'PY'
import sys, json
for line in open(sys.argv[1]):
    try: d = json.loads(line)
    except: continue
    if d.get("type") == "assistant":
        m = d.get("message", {}).get("model")
        if m and m != "<synthetic>":
            print(d.get("timestamp",""), m)
PY
done | sort

Each line is one served turn: 2026-06-07T01:23:45.678Z  claude-opus-4-8. If a stretch of turns shows a different (lower) tier than the one you pinned, that's your evidence — with the exact UTC time you can correlate against your activity.

2 · Per-model turn tally (the strong number for a report)

Collapse all of that into a count of turns served by each model — the single figure that makes a report concrete:

cat ~/.claude/projects/*/*.jsonl | python3 -c '
import sys, json, collections
c = collections.Counter()
for line in sys.stdin:
    try: d = json.loads(line)
    except: continue
    if d.get("type")=="assistant":
        m=d.get("message",{}).get("model")
        if m and m!="<synthetic>": c[m]+=1
for m,n in c.most_common(): print(f"{n:7d}  {m}")
'

Output is one row per served model, most-served first — for example a column of turn counts beside claude-opus-4-8, claude-sonnet-4-6, and so on.

3 · Recent-window breakdown (catch a stale "favorite model")

An all-time tally can be the opposite of what you're getting right now — which is also why a stats "favorite model" that ignores recency misleads (cf. anthropics/claude-code #65899). On one real machine the two windows fully inverted:

WindowTop served modelShare
Last 30 daysclaude-opus-4-780%
Last 7 daysclaude-opus-4-873%

So tally only the recent window. Change DAYS to taste:

python3 - <<'PY'
import json, glob, os, collections, datetime
DAYS = 7
cut = (datetime.date.today() - datetime.timedelta(days=DAYS)).isoformat()
c = collections.Counter()
for f in glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")):
    for line in open(f):
        try: d = json.loads(line)
        except: continue
        if d.get("type") == "assistant":
            m = d.get("message", {}).get("model"); t = d.get("timestamp", "")
            if m and m != "<synthetic>" and t[:10] >= cut:
                c[m] += 1
tot = sum(c.values()) or 1
for m, n in c.most_common():
    print(f"{n*100//tot:3d}%  {n:6d}  {m}")
PY

Each row is the recent-window share per model — e.g. 73%  24773  claude-opus-4-8 — so a drifted "favorite" stands out at a glance.

Reading the model IDs

The IDs are explicit, so you can see when a turn crossed into a different tier. Real current examples:

Model ID in message.modelWhat it is
claude-opus-4-8Opus 4.8 (top tier)
claude-opus-4-7Opus 4.7 (top tier)
claude-sonnet-4-6Sonnet 4.6 (mid tier)
claude-haiku-4-5-20251001Haiku 4.5 (fast/low tier)

What's actually worth reporting

Not every difference is equal. Sort what you find by how much it matters:

And turn it into something reproducible rather than an anecdote:

Stay honest about what the logs prove. message.model tells you which model served a turn. It does not, by itself, prove intent or that anyone "downgraded you on purpose." Report the observed fact (served model per turn, with times); leave the cause to the people who can audit the routing.

This page describes a local verification method against your own session logs; it is not account or legal advice and makes no claim about Anthropic's routing intent. For model and Usage Policy questions, confirm against Anthropic's own documentation and support. Issues referenced for context: anthropics/claude-code #65889 (silent routing under a pinned model), #65899 (stats "favorite model" ignores recency).