When Anthropic released Opus 5.5 I was massively looking forward to it. It's a big step up on the Opus line, and the estate's chat adapters were going to get it the moment I could wire it in.
Then I read the release notes, and there it was, a few paragraphs down: the model now refuses forced tool use. That's a slightly awkward thing to read when forcing a tool is how you've been getting structured answers out of it for months!
The trick
A fair chunk of the estate asks a model a question and hands the reply to code and not to a person, so it has to arrive as JSON in a known shape (I've made the case for that before, and I still stand by it). In go/chat that's Ask: you give it a question and a type, it derives a JSON Schema from the type, and it fills your struct.
The Claude adapter, go/chat-anthropic, did that with a bit of sleight of hand. It offered the model one tool, called submit_response, whose input schema was your schema, and then forced the model to call it. The tool never ran. It was only there so the answer would arrive as a tool call's arguments, which the API validates against the schema. This is the old version, trimmed to the part that matters:
params.Tools = []anthropic.ToolUnionParam{ { OfTool: &anthropic.ToolParam{ Name: toolName, Description: anthropic.String(c.cfg.SchemaDescription), InputSchema: inputSchema, }, }, } params.ToolChoice = anthropic.ToolChoiceUnionParam{ OfTool: &anthropic.ToolChoiceToolParam{ Type: "tool", Name: toolName, }, } If you've ever had a card forced on you by a magician, it's the same move. You think you're picking any card you like, and the magician has made very sure there's only one you can pick. And like any decent trick it needed a bit of stagecraft round the back to keep going: the API insists a tool call is followed by a tool result, so after each answer the adapter wrote a fake one into the conversation, a constant whose content was the single word "recorded" (yes, really).
And it worked a treat on every model I'd pointed it at.
The volunteer stopped playing along
Opus 5.5 and Fable 5.1 turn it down before they've even started. Ask either of them with a forced tool and you get this back, on both:
HTTP 400 {"type":"error","error":{"type":"invalid_request_error", "message":"tool_choice: type \"tool\" and \"any\" are not supported for this model."}} Now, "the model refuses" makes it sound like the model looked at the request and declined, and it never got that far. This is the API turning down the shape of the request for those two models, the same way it would turn down a malformed field. (Opus 5 and Fable 5 still accept it, so it's a change in the newest models and nothing to do with thinking being on by default.)
So Ask with a schema was broken on the newest models, one of which was already in the adapter's catalogue.
Not the fix I went in with
My first instinct came straight from the release notes, which suggested setting the tool mode to auto instead and still getting schema adherence out of it. So I went and read up on it.
The migration guide has three routes, depending on why you were forcing a tool in the first place: steering the model towards a real tool (use auto and name it in the prompt), guaranteeing schema-valid arguments for a real tool (strict under auto), and, the last one, if the forced call existed only to get JSON back, replace it with structured outputs. That last one was my case, to a tee. submit_response had never run once. The old code even called it "a structured-output device rather than a real tool" in its own comments.
By the time I'd written the spec (0024) I'd talked myself out of my auto idea, and I think for the right reason. auto wasn't flaky (it called the tool two times out of two when I tried it), but it would have kept the fake tool, kept the fake "recorded" result, and added a retry loop on top for the day auto decided not to bother. The line I ended up putting at the top of it says it better than I managed in the first draft: the reply should be valid against the schema because the API constrained it, not because the model chose to comply.
No card at all
So the fix (d203fed) doesn't force anything. The schema now travels in output_config.format, the Messages API's structured outputs, and the model is offered no tool at all. The new version is shorter, which I always take as a good sign:
func (c *Claude) applyResponseSchema(params *anthropic.MessageNewParams) error { raw, err := json.Marshal(c.cfg.ResponseSchema) if err != nil { return errors.Wrap(err, "marshalling the response schema") } var schema map[string]any if err := json.Unmarshal(raw, &schema); err != nil { return errors.Wrap(err, "decoding the response schema") } delete(schema, "$schema") delete(schema, "$id") params.OutputConfig.Format = anthropic.JSONOutputFormatParam{Schema: schema} c.logger.Debug("Claude response schema", "schema", string(raw)) return nil } The fake tool result went with it, so a conversation's history now records a question and an answer instead of a pretend tool call. No per-model switch either: they all take the new path, since they all accepted it when I tested.
The spike that waited a week for money
The testing nearly didn't happen on time. The spec was drafted in late September, and the spike that was meant to prove it failed on every call, with the API telling me my credit balance was too low (given the month I'd been having with bills, that felt about right). I topped it up the following week, with a firm note to keep the testing lean, and it ran in thirty-odd small calls.
All fifteen models in the catalogue accepted the format and came back schema-valid, and then it turned up something I wouldn't have guessed. The first draft of the spec folded the schema's description into the request, which seemed harmless, helpful even. With that description in, Opus 5.5 returned perfectly valid JSON with the arrays empty... three times out of three. Without it, the same request came back complete four times out of four! So the adapter now sends the caller's schema as given and adds nothing to it.
The first conformance test for the new behaviour passed against the unchanged adapter, which should have been impossible. The old SetTools, it turns out, threw the schema away whenever you gave the client real tools, so the test was looking for the absence of a tool and finding it for completely the wrong reason. That earned a second test case, and fixed a bug that hadn't been reported along the way.
The trick's over
I don't think forcing a tool was ever wrong, as such. At the time it was the cleverest way I had of getting a guaranteed shape back, and it served the estate well for months. But a trick that needs the volunteer to play along is only ever one release away from falling flat, and this time the volunteer had read the instructions. These days I tell the API the shape I want and it holds the model to it, no card required.
Originally published at phpboyscout.uk on 3 November 2026.

