Skip to content

fix(reflect): work on models that reject a forced tool_choice; no 500 on an empty observation - #5917

Merged
nicoloboschi merged 2 commits into
mainfrom
fix/reflect-anthropic-tool-choice-5738-5840
Oct 21, 2026
Merged

nicoloboschi merged 2 commits into
mainfrom
fix/reflect-anthropic-tool-choice-5738-5840

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #5738
Fixes #5840

#5738: reflect fails on Claude Opus 5.5 / Sonnet 5.5

Since #4470 the anthropic provider maps reflect's forced steps to tool_choice {"type":"tool"} / {"type":"any"}. These models answer both with 400 tool_choice: type "tool" and "any" are not supported for this model. Reflect forces tools on its first steps and on the closing done call, so every reflect and every mental-model refresh failed.

Now, when the tool loop gets that 400 for a forced choice, it resends the same request with tool_choice: auto. It also remembers this on the provider, so later calls ask with auto straight away. It's detected from the error, not from a list of model names, so a future model that rejects forced choice works too. Reflect already copes with a model that answers in prose where a tool was forced. That's the same path the lmstudio/ollama providers use when they drop required. Any other 400 is raised as before.

Strict structured output (HINDSIGHT_API_LLM_STRICT_SCHEMA on) also forces a tool, so retain and consolidation failed on these models too. Once the model rejects a forced tool, those calls use the existing schema-in-prompt path instead.

Tests: test_anthropic_forced_tool_choice_fallback.py. Named and required are each resent as auto, and the next call asks with auto up front. Both fail without the fix. Strict structured output falls back to schema-in-prompt and stays there. An unrelated 400 is still raised.

#5840: reflect 500s when it cites an observation with empty text

tool_search_observations passes each observation through _prune_nulls, which drops "". So an observation with empty text reaches based_on with no text key, and MemoryFact(**obs_data) failed validation after the answer was already generated. based_on now fills in the required fields the pruning can drop (text="", fact_type="observation").

Test: test_reflect_empty_observation_text.py. Without the fix it fails with text Field required, the error from the issue.

… on an empty observation

- anthropic: Claude Opus 5.5 / Sonnet 5.5 answer a named or "any" tool_choice
  with a 400, so every reflect and mental-model refresh failed on them since
  #4470. The tool loop now resends that request with tool_choice=auto and
  remembers it for the provider, detected from the error rather than a model
  list (#5738).
- reflect: an observation whose text is "" lost its text key in _prune_nulls
  and MemoryFact(**obs_data) failed the whole reflect with a 500 after the
  answer was written. based_on now fills the dropped required fields (#5840).
…pt on models that reject a forced tool (#5738)

Review fixes: the strict-schema one-shot path forced a tool too, so retain and
consolidation failed on the same models with HINDSIGHT_API_LLM_STRICT_SCHEMA on;
it now uses the existing schema-in-prompt path once the model rejects it. Test
added; comments updated.
@nicoloboschi
nicoloboschi merged commit 0ca53ad into main Oct 21, 2026
117 checks passed
@nicoloboschi
nicoloboschi deleted the fix/reflect-anthropic-tool-choice-5738-5840 branch October 21, 2026 08:45
@nicoloboschi nicoloboschi mentioned this pull request Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant