The Berkeley Function-Calling Leaderboard is the standard public scoreboard for tool use, and its Hallucination Measurement block is worth 10 % of every model’s headline score. The check behind that block does not ask whether a model abstained. It asks whether the text it produced could be parsed. For three of the 109 models on the board those are opposite answers, and the block supplies between 79 % and 100 % of what they scored. Everything below is read out of BFCL’s own published code, CSVs and result files.
The short version. Two separate things, both
verifiable in an afternoon. One: the leaderboard page tells the reader its
headline number is an unweighted average. The code computes a weighted one —
40/30/10/10/10 — and the weighted reconstruction matches the published column on all 109 rows
to within 0.007 pp, while the unweighted one misses all 109 rows, by 15 pp on average.
Two: irrelevance is scored as success = not contain_func_call, and
a decode failure sets contain_func_call = False. So a model whose function calls
are malformed is recorded as having declined to call a function.
What this is not. The 40/30/10/10/10 weights are correctly documented — in a different blog post than the one the leaderboard links to. And the decode branch does precisely what its own code comment says it does. Neither finding is hidden behaviour; both are places where the number a reader carries away does not mean what the reader is told it means.
From the leaderboard page, fetched today, verbatim:
Overall Accuracy is the unweighted average of all the sub-categories. For details on score composition, please refer to our blog.
The code says otherwise. From eval_runner_helper.py, the last thing it computes
before writing the CSV:
# TODO: @HuanzhiMao adjust the weights
total_overall_accuracy = calculate_percentage_weighted_accuracy(
[
overall_accuracy_non_live,
overall_accuracy_live,
total_irrelevance,
overall_accuracy_multi_turn,
overall_accuracy_agentic,
],
[10, 10, 10, 30, 40],
display_na_if_category_missing=False,
)
I rebuilt the published Overall Acc column both ways from the five block scores
that sit in the same CSV rows, for all 109 models:
| reconstruction | largest gap vs published | mean gap | rows off by >1 pp | |
|---|---|---|---|---|
| weighted 10/10/10/30/40 | 0.007 pp | — | 0 of 109 | THIS IS THE FORMULA |
| unweighted mean of the five blocks | 24.04 pp | 15.07 pp | 109 of 109 | NOT THE FORMULA |
The worst single row is Amazon-Nova-2-Lite: published 27.10, unweighted 51.14. A reader who takes the page at its word and reconstructs a model’s score from its parts is out by twenty-four points.
Then the sentence sends that reader for “details on score composition” to the V3 blog, which opens the relevant section like this:
Note: This leaderboard composition is now outdated. For the most updated composition, please refer to the BFCL V4 blog.
The correct composition is in the V4 blog, which the leaderboard page does not link: “Overall Score = (Agentic × 40%) + (Multi-Turn × 30%) + (Live × 10%) + (Non-Live × 10%) + (Hallucination × 10%)”. The same post also says “Within-Category Calculation: Unweighted Average” — which is where the word on the leaderboard is very likely a survivor from. It is true inside a category and false across them, and the leaderboard sentence is about across.
Irrelevance is the category that asks a model to not call a function: the user’s
request cannot be served by any tool on offer, and the right behaviour is to decline. Here is how
a run is judged, from eval_runner.py:
except Exception as e:
# Decode failed, which means the model output is not in valid function call format
contain_func_call = False
decode_error = str(e)
# irrelevance test means no function call outputted
if "irrelevance" in test_category:
success = not contain_func_call
else:
success = contain_func_call
The comment is honest about what it assumes: unparseable output is treated as “no function call”. That assumption is doing all the work. Tried to call a tool and produced malformed syntax and correctly declined to call a tool arrive at this branch as the same event, and the branch calls both of them a pass.
Its score file records {"accuracy": 0.954, "correct_count": 229, "total_count": 240}
on non-live irrelevance. I pulled the 229 entries it was credited for and looked at what the
model actually emitted:
| category | scored correct | of those, output contains <tool_call | |
|---|---|---|---|
| irrelevance (non-live) | 229 / 240 | 228 (99.6 %) | CREDITED FOR CALLING |
| live_irrelevance | 700 / 884 | 588 (84.0 %) | CREDITED FOR CALLING |
Two of the 228, as stored in BFCL’s own result file:
id=irrelevance_0 '<tool_call>\n<tool_call name="determine_body_mass_index">\n {\n
"type": "dict",\n "properties": {\n "weight": 70.0,\n
"height": 1.75\n }\n'
id=irrelevance_1 '<tool_call>\n<tool_call name="math.solve" arguments={"a": 1, "b": 2,
"c": 3}/>\n</tool_call>'
That is a hallucinated tool call, named and argumented, scored as an abstention.
The eleven entries Falcon was marked wrong on are the ones where its syntax happened to come out valid. The metric is running backwards: within this category, the better a model formats a call it should not have made, the worse it scores.
Every entry of this model’s result files holds an empty list — 240 of 240 irrelevance, 884 of 884 live irrelevance, 400 of 400 simple_python, 16 of 16 live relevance, with no other value anywhere. Four score files from that same run, unedited:
BFCL_v4_simple_python {"accuracy": 0.0, "correct_count": 0, "total_count": 400}
error: ["Invalid syntax. Failed to decode AST. expected string or bytes-like
object, got 'list'"]
BFCL_v4_live_relevance {"accuracy": 0.0, "correct_count": 0, "total_count": 16}
error: ["Invalid syntax. Failed to decode AST when it should have. expected
string or bytes-like object, got 'list'"]
BFCL_v4_irrelevance {"accuracy": 1.0, "correct_count": 240, "total_count": 240}
BFCL_v4_live_irrelevance {"accuracy": 1.0, "correct_count": 884, "total_count": 884}
One failure mode, one run, three verdicts. The first two files name the failure in an error string. The last two are silent, and they are the ones that reach the score: irrelevance is 100.00 %, and 10 % of 100.00 is the model’s entire published Overall Accuracy of 10.00 %. Every other block is 0.00.
Exactly three models of 109 score 0.00 on relevance detection — never once
recognising a call it should make — while scoring high on irrelevance. That combination
is the signature, and it is the whole population of it:
| rank | model | overall | irrel. | rel. | non-live | live | multi-turn | agentic | share of overall from the irrelevance block |
|---|---|---|---|---|---|---|---|---|---|
| 105 | Ministral-8B-Instruct-2410 (FC) | 11.10 | 100.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.76 | 90.1 % |
| 106 | Falcon3-1B-Instruct (FC) | 11.08 | 87.30 | 0.00 | 9.02 | 2.89 | 0.00 | 2.90 | 78.8 % |
| 108 | Llama-3.1-Nemotron-Ultra-253B-v1 (FC) | 10.00 | 100.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 100.0 % |
| 109 | Gemma-3-1b-it (Prompt) | 7.17 | 33.18 | 37.50 | 20.21 | 11.84 | 0.00 | 1.61 | 46.3 % |
The last row is the one that makes it matter. Gemma-3-1b is a one-billion-parameter model that genuinely does the task some of the time — it calls functions correctly on a fifth of the non-live set and recognises a legitimate call 37.5 % of the time. It is ranked below all three, including below a model that produced an empty result on every single entry of the benchmark. The bottom of this leaderboard is upside down.
Only there — and saying so is the point. A criticism that cannot be bounded is not a finding. The top ten rows of the same board, computed the same way:
| rank | model | overall | irrel. | rel. | share of overall from the irrelevance block | |
|---|---|---|---|---|---|---|
| 1 | Claude-Opus-4-5 (FC) | 77.47 | 84.72 | 62.50 | 10.9 % | BEHAVES |
| 2 | Claude-Sonnet-4-5 (FC) | 73.24 | 86.61 | 68.75 | 11.8 % | BEHAVES |
| 3 | Gemini-3-Pro-Preview (Prompt) | 72.51 | 85.59 | 68.75 | 11.8 % | BEHAVES |
| 4 | GLM-4.6 (FC thinking) | 72.38 | 84.96 | 75.00 | 11.7 % | BEHAVES |
| 5 | Grok-4-1-fast-reasoning (FC) | 69.57 | 79.43 | 81.25 | 11.4 % | BEHAVES |
| 6 | Claude-Haiku-4-5 (FC) | 68.70 | 85.11 | 62.50 | 12.4 % | BEHAVES |
| 7 | Gemini-3-Pro-Preview (FC) | 68.14 | 77.85 | 75.00 | 11.4 % | BEHAVES |
| 8 | o3-2025-04-16 (Prompt) | 63.05 | 83.98 | 93.75 | 13.3 % | BEHAVES |
| 9 | Grok-4-0709 (Prompt) | 62.97 | 84.30 | 81.25 | 13.4 % | BEHAVES |
| 10 | Grok-4-0709 (FC) | 61.38 | 75.40 | 87.50 | 12.3 % | BEHAVES |
Every one of them contributes 10.9–13.4 % of its score from the block — its nominal 10 % weight, give or take — and every one of them recognises real calls 62–94 % of the time. For a model that emits parseable syntax, the check measures what its name says. The defect is not “BFCL is broken”. It is that the check silently changes meaning at exactly the point where a model’s formatting collapses, and there is nothing in the published number that says which regime a row is in.
The failure is a missing distinction, so the fix is to restore it rather than to blacklist today’s three offenders — a blacklist loses to the next model. Three states, not two:
The distinction costs nothing to compute: the exception is already caught and its message is
already stored in decode_error. It is thrown away one line later, when the branch
collapses to a boolean. And the second, cheaper half of the fix has no code in it at all —
publishing a per-row relevance figure beside the irrelevance one would have made all
three of these rows obvious on sight, because irrelevance 100.00 / relevance 0.00
is not a profile any real abstention policy can produce.
No API key and no account. Everything is a public file:
# the five leaderboard CSVs, as served to the page https://github.com/HuanzhiMao/BFCL-Result → 2025-12-16/ # the model outputs and the score files, per model, per category 2025-12-16/result/tiiuae_Falcon3-1B-Instruct-FC/non_live/BFCL_v4_irrelevance_result.json 2025-12-16/score/tiiuae_Falcon3-1B-Instruct-FC/non_live/BFCL_v4_irrelevance_score.json 2025-12-16/score/nvidia_llama-3.1-nemotron-ultra-253b-v1/non_live/BFCL_v4_simple_python_score.json # the scoring code gorilla/berkeley-function-call-leaderboard/bfcl_eval/eval_checker/eval_runner.py gorilla/berkeley-function-call-leaderboard/bfcl_eval/eval_checker/eval_runner_helper.py
Line 1 of a *_score.json is the category summary; every line after it is an entry
the run marked wrong. So the set credited as correct is the entries absent from lines
2 onward — which is how the 228-of-229 count above is built, with no judgement of mine
anywhere in it.
2025-12-16 snapshot, cross-checked against
the live leaderboard CSVs.