allow_migrate in the autodetector:" and then simply stopped, burning a full inference round-trip for nothing. The fix isn't a better prompt. It's making narration structurally impossible: tool_choice: "required", which Outlier's OpenAI-compatible API now supports. Measured action rate went from 27% to 89%.A coding agent works in a loop: the model reads the state of a repository, calls a tool (search, read a file, edit a file, run a test), sees the result, and repeats. Every turn that doesn't call a tool is a wasted round-trip — on local hardware, roughly 12 seconds of compute that moves nothing forward.
We instrumented 63 real agent sessions (SWE-bench Verified tasks, blind, greedy decoding) and counted what each of 8,569 model turns actually did:
| What the turn produced | Share |
|---|---|
| A real tool call | 27% |
| Prose narration, no action ("Let me search for…") | ~46% |
| A single token, then silence ("Let", "I", "```") | ~20% |
| A tool call written in shorthand our parser dropped | ~5% |
| Other fragments | ~2% |
The system prompt already ordered the model — in capitals — to emit tool calls only. It narrated anyway. At 27B scale, instruction-following alone does not survive a long agent context.
The strangest slice: 1,741 turns where the model generated exactly one visible token and stopped. Timing gave the cause away — those turns took 1.2 seconds against 18 for real ones, so the model wasn't being cut off; it was choosing to stop instantly. The trigger: when a turn produced prose and no tool call, our loop appended the text and re-prompted without a user message. The model, asked to speak again immediately after ending its own turn, ends the new turn after one token — and the loop repeats, forever, at 1.2s a spin. The fix is to always respond to a no-action turn with an explicit user message demanding a tool call, and to stop honestly after a few strikes.
458 "no-action" turns were actually the model calling a tool — in a shorthand format (<grep_repo>… instead of the canonical <tool_call><function=grep_repo>…) that our parser threw away. Recovery is now automatic, and safe: shorthand is only recognized for names the request actually declared as tools, so real XML in a code answer can never be misread as a tool call.
The rest is the model doing what chat models do: thinking out loud. No prompt fixes this reliably. What fixes it is removing the option.
tool_choice: "required" doesOpenAI's API has a field for exactly this, and Outlier's OpenAI-compatible endpoint now implements it. The mechanism is structural: when a request sets tool_choice: "required", Outlier ends the prompt with the exact opening syntax of a tool call — so the only thing the model can generate is the rest of one.
{
"model": "outlier-code",
"messages": [...],
"tools": [...],
"tool_choice": "required"
}
The forced-function form ({"type": "function", "function": {"name": "grep_repo"}}) is supported too, and giving up stays possible — finishing is itself a tool, so the model can always choose finish explicitly instead of trailing off.
| Before | After | |
|---|---|---|
| Turns producing a real tool call | 27% | 89% |
| Single-token wasted turns | 33% of calls | 0 |
Same model, same weights, same tasks. The difference is that the harness stopped losing actions and the API stopped letting the model narrate.
Action rate is not task success. We promised to publish the resolution measurement with the misses included, and here is the miss: when we made required the default for our own coding agent, the first four benchmark tasks all ended with empty patches — including three that the unforced baseline had patched or solved. Forced to act on every turn, the model stopped deliberating and browsed the repository until time ran out.
The lesson: some of that prose narration was load-bearing thinking — the model's natural pattern is to reason in text, then edit. Deleting the option to think made it act more and accomplish less. So our agent's default is back to auto, with the stutter-loop and dropped-shorthand fixes (which ban waste, not thinking) doing the real work. tool_choice: "required" remains available and correct as an API feature — many agent designs genuinely need it, especially routing and extraction loops where every turn should be a call. Just don't assume a forced action is a useful one.
And the methodological note we keep re-learning: most of what looked like model weakness in this investigation was our own code. Before concluding a local model can't do something, verify the harness that measured it.
Outlier runs frontier-class local AI on your own Mac — private, offline, no subscription. The OpenAI-compatible API (including tool_choice: "required") ships in v1.11.707.