Enforce the tool-call contract, asking servers only where it works
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 56s

The executor ended a milestone on any reply without a tool call, and
nothing held a model to the offered tools. Measured on the 4070:
tool_choice "required" is enforced by vLLM (local-fast) and Anthropic,
but qwen3-14b (local-coder) under it cannot stop and writes until the
output ceiling, a known model bug. So enforcement is asked for per
model, and the loop holds the contract itself everywhere.

- The catalog reads supports_tool_choice from LiteLLM's model_info;
  BudgetedClient sends tool_choice only to models declared true, as
  "required" on the normalized route and {"type": "any"} on the
  Anthropic passthrough, and only when tools are offered
- The executor gains an explicit finish tool, the only way to end a
  milestone. Finishing with nothing changed is refused once; a prose
  reply gets one nudge, then escalates; a call to a tool not offered
  counts toward failure_limit, like a failed command; calls after
  finish are recorded as not run
- Clients record the model that actually answered. ModelFacts.answered
  recognises a proxy fallback, which once credited Claude's answers to
  a local model that could not call tools at all
- The context probe reports a fallback instead of trusting it; rerun,
  both local models now refuse over-long prompts, as they should
- The capability matrix checks every answer came from the model asked,
  that models call only offered tools, and that a declared
  supports_tool_choice is true
- Spec A15 and the tuning guide say how enforcement is requested,
  declared and backed up

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-09-24 22:54:49 +00:00
1 parent 389b71a5d1
commit b934c46714
16 files changed
+568 -86

No files matched your search

+50 -2
View File
@@ -150,8 +150,8 @@ class RecordingClient:
self.calls: list[dict[str, Any]] = []
self.last_usage = {"input": reported_input, "output": 5}
def complete(self, events, tools, model, system=None, *, max_tokens=None):
self.calls.append({"events": events, "max_tokens": max_tokens})
def complete(self, events, tools, model, system=None, *, max_tokens=None, **kwargs):
self.calls.append({"events": events, "max_tokens": max_tokens, **kwargs})
return [AssistantText(text="ok", model=model)]
@@ -292,3 +292,51 @@ def test_a_request_that_cannot_fit_stops_the_milestone(tmp_path: Path) -> None:
assert outcome.status == "escalate"
assert "fit" in outcome.note
assert inner.calls == []
# --- tool_choice is only asked of servers that enforce it -------------------
FINISH = [{"type": "function", "function": {"name": "finish", "parameters": {}}}]
def test_required_is_asked_only_of_models_declared_to_enforce_it() -> None:
# Measured: qwen3-14b under "required" cannot stop and rambles to the
# output ceiling, so a model that is not declared is never asked.
inner = RecordingClient()
client = BudgetedClient(
inner, lambda m: (32768, 8192), enforces_tool_choice=lambda m: m == "local-fast"
)
client.complete(transcript(1, 10), FINISH, "local-fast")
client.complete(transcript(1, 10), FINISH, "local-coder")
client.complete(transcript(1, 10), None, "local-fast")
assert [c["require_tool_call"] for c in inner.calls] == [True, False, False]
def test_the_catalog_reads_supports_tool_choice() -> None:
catalog = ModelCatalog.from_payload(
{
"data": [
{"model_name": "a", "model_info": {"supports_tool_choice": True}},
{"model_name": "b", "model_info": {"supports_tool_choice": False}},
{"model_name": "c", "model_info": {}},
]
}
)
assert [catalog.facts(m).supports_tool_choice for m in "abc"] == [True, False, None]
def test_a_fallback_answer_is_recognised() -> None:
facts = ModelCatalog.from_payload(
{
"data": [
{
"model_name": "claude-haiku-4-5",
"litellm_params": {"model": "anthropic/claude-haiku-4-5"},
}
]
}
).facts("claude-haiku-4-5")
assert facts.answered("claude-haiku-4-5") # LiteLLM's alias
assert facts.answered("claude-haiku-4-5-20251001") # the provider's own name
assert facts.answered(None) # nothing reported
assert not facts.answered("claude-sonnet-5") # somebody else answered