Repository navigation
Two "model failures" in our eval were the provider's response format. How do you tell them apart? #8334
Replies: 3 comments 1 reply
|
I’d run a small protocol check for each provider/model before the full eval, covering a no-tool answer, a tool call, and a call after your refusal message. Check streaming separately if you use it. Keep the request, raw response, SDK-decoded response and scorer decision together. If your harness uses AutoGen’s client, nonempty structured Replaying captured responses through those local stages costs no additional model calls and can locate SDK, adapter or scorer discrepancies. For leaked native-format text, check it against the model’s format and declared tool schema, but keep that diagnostic separate from successful native tool use. Valid-looking text alone does not establish where the response was transformed. Raw generation output and the serving parser’s configuration would help settle that. If those are unavailable, reserve second-provider probes for the ambiguous cases rather than duplicating the whole eval. A fingerprint alone would not settle the cause. |
|
IlyaKhalafi's suggestion to check leaked text against the model's format and the declared tool schema can run offline over the no-call rows you already stored, so the second provider is needed only for what that check leaves open. gpt-oss writes a call as import json, re
LEAK = [("harmony", re.compile(r"to=functions\.(?P<name>[A-Za-z_][\w.-]*)")),
("hermes", re.compile(r"<tool_call>\s*"))]
_dec = json.JSONDecoder()
def first_json(text, start):
i = text.find("{", start)
while i != -1:
try:
return _dec.raw_decode(text, i)[0]
except ValueError:
i = text.find("{", i + 1)
def label_no_call_row(content, tools): # tools: {name: set(required args)} from the request you sent
for fam, rx in LEAK:
m = rx.search(content or "")
if not m:
continue
obj = first_json(content, m.end())
if fam == "harmony":
name, args = m.group("name"), obj
else:
name, args = (obj or {}).get("name"), (obj or {}).get("arguments")
if name in tools and isinstance(args, dict) and tools[name] <= args.keys():
return f"unparsed_native_call:{name}"
return "malformed_native_call"
return "model_answered_in_text"On synthetic rows (written by hand, not provider output), with With that label you can report two rates per arm: what the model meant (parsed calls plus What it cannot tell you: a model that wrote nothing call-like is labelled — LEEVAR |
|
Ilya's three-case protocol (no-tool answer, tool call, call after refusal) maps well onto something cheap you can add on top: keep the probe items permanently in the eval as canaries per endpoint. The no-tool item is the one that bit you here, since a missing tool_calls field and an empty list looked identical to the scorer. If the canary row for a provider shifts after a serving-side update, you see it before the real rows are scored. For the retry-turn problem, I would score the intervention arm only on rows where the first turn parsed cleanly, and report the parse-failure rate per arm as its own number. Otherwise a parser bug that only exists in the arm you are testing reads as the intervention failing, which is exactly what happened with gpt-oss. |
Uh oh!
There was an error while loading. Please reload this page.
We run tool-calling evals across several open-weight models served by third-party providers. Twice in one night, numbers that looked like model behaviour turned out to be the serving layer.
First: two models were "failing" on 90 items each. Every one was an item where the right move is to call no tool, and both models had answered correctly in text. Their provider omits the
tool_callsfield in that case, and our scorer treated a missing field as a failed call. A third model returned an empty list on the same items and scored fine. Under our own void rule, the bug would have thrown out both models' results.Second: we refused a call once and let the model retry. gpt-oss-120b's retries came back as plain text with the tool call written in its own native format, unparsed, and finish_reason
stop. Read by hand, 30 of 31 were the correct call. The same provider parsed the first turn of those conversations fine. It only failed on the retry turn, which only exists in the arm we were testing, so it looked exactly like the intervention failing.What we changed: an absent
tool_callsfield now counts the same as an empty one; we read the raw text behind every row that scored "no call" before trusting a rate; and we record the endpoint and served model on every row.What we still can't do well is tell model behaviour from serving behaviour without running every model on two providers, which doubles the cost. Has anyone found a cheaper discriminator? Response fingerprints, parser test vectors, anything.
(We build agent memory tooling, or at least we are trying to. Today we mostly built tooling to check our tooling.)
All reactions