
I recently moved a structured-extraction pipeline from Claude Sonnet 4.6 to Sonnet 5.5 on Amazon Bedrock. On paper it is a model ID change: the same family, a lower price per token, better benchmark scores. In practice, some requests that worked on 4.6 were rejected, one kind of request came back empty with HTTP 200, and some answers changed in ways that schema validation does not catch.
To share numbers rather than anecdotes, I rebuilt the test from scratch on fictional data. Twelve made-up team meeting notes, generated from a fixed seed, so every field has a known answer. Both models extracted the same fields from every note, three times each, through Bedrock in us-west-2. I then ran smaller probes for the API changes, the usual fallback models, long documents and output limits. The whole second round cost about USD 2.50.
The short version:
| Sonnet 4.6 | Sonnet 5.5 | |
|---|---|---|
| Fields with one right answer, schema states formats | 100% | 100% |
| Same, schema with no descriptions | 81.0% | 93.7% |
| Same answer from both models (ignoring wording), explicit schema | 42% of notes | |
| Agreement after one added sentence | 100% | |
| Input tokens per call, short notes | 796 | 979 (1.23x) |
| Input tokens per call, about 15,000 words | 18,502 | 26,609 (1.44x) |
| Cost per call, short notes, list price | USD 0.0056 | USD 0.0049 (13% lower) |
| Median latency, short notes | 1.79 s | 2.51 s |
The surprising row is the third one. Both models were right on every field that has one answer, and still produced the same complete answer for only 42% of notes. Here, "the same answer" means the same dates, attendees, number of decisions, and action items with the same owners and due dates; wording is ignored. The rest of this post is about where that gap comes from, and how to close it.
The test setup
Each note has a title, a date, attendees, decisions and action items. The generator plants the cases that matter in any extraction job:
- Dates in four styles (
March 4, 2025,4 March 2025,2025-03-04,Tuesday, March 4th, 2025), and due dates written as "within 10 days of this meeting". - An "update after the meeting" line at the end that moves one deadline (7 of 12 notes).
- A task with two owners, such as "Mei and Omar to update the FAQ page" (7 notes).
- A tentative task with no owner, such as "It would be good if someone could look into translating the guide" (7 notes).
- Notes with no decisions (3) and no next meeting date (5).
Both models received the same prompt and the same JSON schema through structured output (output_config.format with type: json_schema). The only request differences were the ones the API forces: temperature: 0 on 4.6, and no sampling parameters with thinking switched off on 5.5. I ran three schema variants:
- explicit: descriptions state formats ("ISO 8601", "Null if none were made"), but say nothing about two-owner or no-owner tasks.
- loose: the same schema with every description removed.
- clarified: explicit, plus one sentence on those two cases.
The code and the generated notes are in the reproduction kit linked at the end.
1. Requests that fail loudly
These are the easy ones, because Bedrock returns a ValidationException with a clear message.
| Request | Sonnet 4.6 | Sonnet 5.5 |
|---|---|---|
temperature: 0 | accepted | rejected: "temperature is deprecated for this model" |
top_p or top_k alone | accepted | rejected, same message |
temperature and top_p together | rejected: "cannot both be specified" | rejected |
thinking: {type: disabled} | accepted | rejected: send {"type": "between_tools"} instead |
thinking: {type: enabled, budget_tokens} | accepted | rejected: use adaptive and output_config.effort |
thinking: {type: between_tools} | rejected | accepted |
Forced tool_choice (tool or any) | accepted | rejected: not supported for this model |
| Assistant prefill (last message is a partial assistant reply) | rejected | rejected |
| Empty text block in an assistant turn | rejected: "text content blocks must be non-empty" | rejected |
max_tokens above 128,000 | rejected | rejected |
The thinking setting cannot be shared between the two models: the value that turns thinking off on 5.5 is rejected by 4.6. Thinking is adaptive by default on 5.5. In 24 extraction calls with no thinking field, 5.5 returned no thinking blocks and the same answers as with thinking off, but one short probe without a thinking field did return a thinking block before the text. Read content blocks by type instead of taking content[0].
The forced tool_choice change matters for anyone who gets structured output by defining one tool and forcing the model to call it. That request is rejected on 5.5. Structured output through output_config.format works on both models, so that is the path I used for everything else.
The practical fix is a small request profile per model ID (sampling, thinking, tool choice) in configuration, so the next migration is a config change and not a code change.
2. The refusal that returns HTTP 200
This one costs time, because nothing throws. Sonnet 5.5 returned stop_reason: "refusal", an empty content list and HTTP 200 for a schema with a reasoning field. A pipeline that only catches exceptions sees an empty answer, not an error.
I expected the field name chain_of_thought to be the trigger, so I separated the name from the description, five requests per case:
| Reasoning field | Description | 4.6 refusals | 5.5 refusals |
|---|---|---|---|
| none | 0/5 | 0/5 | |
chain_of_thought | "Your reasoning before answering." | 0/5 | 5/5 |
rationale | "Your reasoning before answering." | 0/5 | 5/5 |
chain_of_thought | "One sentence quoting the line the date comes from." | 0/5 | 0/5 |
rationale, evidence, thinking, reasoning | same neutral description | 0/5 | 0/5 |
In my tests the trigger was the description, not the name. A field that asks the model to write out its reasoning was refused every time. The same field asking for a quote from the source was answered every time, even when it was called chain_of_thought. A quote is also more useful in an extraction result, because it can be checked against the document.
The usual fallback models behave differently. With the reasoning description, Sonnet 5 and Haiku 5.5 returned 0 refusals in 5 requests each. So a fallback on refusal works, as long as your code treats refusal as a failure. Check stop_reason on every response, and when you retry, do not append the empty reply to the conversation.
3. Where the schema is silent, the models choose differently
This is the part of the migration that tests and validation do not catch. Every answer below is valid JSON that matches the schema. The models simply made different choices, and each model made its choice consistently.
Inclusion: something that might or might not belong in a list. The tentative task with no owner was included by 4.6 in 4 of 7 notes, the same 4 notes in every run. Sonnet 5.5 left it out in 21 of 21 answers. Because owner was a required string, 4.6 had to put something there, and it wrote "", "Unassigned" or "Unknown". That placeholder even changed between runs at temperature: 0, which is why 4.6 matched itself on only 89% of notes with the explicit schema. Neither choice is wrong. The schema never said.
Granularity: one entry, or one per part. For "Mei and Omar to update the FAQ page", both models created one entry per owner when the owner field was described as "First name of the person responsible", and one entry with "Mei and Omar" when there were no descriptions. The two models agreed with each other in both cases. The description, not the model, decided the shape.
Format: dates. With "ISO 8601" in the descriptions, all 177 dates from each model were ISO. Without descriptions, 4.6 returned 51 of 156 dates in other formats and usually copied the style of the note (27 of 36 meeting dates). Sonnet 5.5 returned 16 of 156 in other formats, including once "within 10 days of this meeting (11 June 2025)". Every date was still the right day. On a different kind of document I tested earlier, the direction was reversed and 5.5 drifted more. Neither model has a format you can rely on unless you state it.
Empty values: null or []. With "Null if none were made" in the description, both models returned null for all notes without decisions. Without it, 4.6 returned [] in 3 of 9 cases and 5.5 in 2 of 9. A null that becomes [] passes validation and breaks code that checks is None.
Type: a score with no stated format. I added a meeting_date_confidence string field, described only as "How confident you are in the meeting date." Both models returned text for all 12 notes, such as "High - the date is stated explicitly in the notes header", not numbers. With "as a number from 0.0 to 1.0", both returned numbers between 0.95 and 1.0 every time. If code compares a score with a threshold, give the field a number type and a range.
With one added sentence ("If a task has more than one owner, add one entry per owner. Include only tasks with a named owner."), both models scored 100% and produced identical answers for every note in every run.
4. Tokens, latency and cost
Sonnet 5.5 uses a newer tokenizer. The Claude pricing page says it produces about 30% more tokens for the same text, and that the exact increase depends on the content. That matched what I saw: the same short notes were 1.23x the input tokens, and the long version (the same notes padded to about 15,000 words of discussion) was 1.44x. Output was 1.38x larger on the short notes for nearly identical JSON.
The price per token is lower: USD 2 and USD 10 per million input and output tokens for Sonnet 5.5, against USD 3 and USD 15 for Sonnet 4.6. These are Anthropic list prices, before the 10% premium for regional (us.) inference profiles; check Amazon Bedrock pricing for your Region. At those prices, a short-note call cost 13% less on 5.5. A long-note call cost only 4% less, because the token increase was larger.
Two things move with the tokenizer even when the bill goes down. Token budgets and context checks written for 4.6 trip earlier. And a cost model that counts tokens per document needs a new ratio, measured on your own text.
Latency went the other way here: a median of 2.51 s against 1.79 s on short notes, and 3.04 s against 2.61 s on long ones. Both models applied the late deadline update at the end of every long note, 6 of 6 each. These are small documents in one Region on one day, so treat the latency numbers as something to measure on your own workload.
How to make a schema migration-proof
For every field, the schema description should answer five questions. Each one maps to a difference I measured:
- Inclusion. What belongs in this list, and what does not? ("Include only tasks with a named owner.")
- Granularity. One entry per item, or one per part of an item? ("One entry per owner.")
- Format. Exact format for dates, numbers and units. ("ISO 8601, YYYY-MM-DD.")
- Empty. What to return when there is nothing. ("Null if none were made.")
- Type. A real number type and range for anything compared with a threshold.
And for the request itself:
- Keep sampling, thinking and tool-choice settings in per-model configuration.
- Use structured output (
output_config.format) rather than a forced tool call. - Check
stop_reasonon every response, and treatrefusaland empty replies as failures. - Ask for a quote from the source, not for the model's reasoning.
- Never send an empty assistant turn in a retry.
- Compare stored values and shapes between models, not only schema validity.
- Run each model more than once, so a model difference can be told apart from run-to-run variation.
- Recalculate token budgets and cost per document on your own text.
Limits of this test
The notes are short, clean and fictional, so absolute accuracy is higher than real documents would give. The point is the difference between the models under identical conditions. I tested one Region, one day and one family of schemas, through bedrock-runtime InvokeModel only. The refusal finding comes from a small set of descriptions, and other wording may behave differently, which is the reason to check stop_reason rather than rely on a list of safe phrases.
Sonnet 5.5 came out of this test cheaper per call, fully consistent from run to run with an explicit schema, and as accurate as 4.6 on every field with one right answer. Most of the migration work was in the request settings and the schema text. Once those were explicit, the two models gave the same answers.
The scripts and the generated notes are in the reproduction kit: [link to be added].
