Files
crewAI/lib
João Moura 6e714d6cad feat(flow): accept crew-style LLM config in a conversational declaration (#7062)
* feat(flow): accept crew-style LLM config in a conversational declaration

`conversational.llm` / `intent_llm` / `answer_from_history_llm` and
`router.llm` only accepted a model-id string, so a declaration could not set
`max_tokens`, `temperature` or anything else a declarative crew's agent can.

`_coerce_llm` now delegates to `crewai.utilities.llm_utils.create_llm` -- the
same helper the crew/agent declaration layer resolves through -- so the shapes
match: a model-id string, a config mapping, an `LLMDefinition`, or a live
`LLM`/`BaseLLM` passed straight through.

It keeps one thing `create_llm` does not: `create_llm(1234)` takes the int as a
model name and returns an LLM that only fails later with a provider error. A
declaration is hand-written, so a non-string, non-mapping value raises now
instead. A mapping missing `model` keeps `create_llm`'s own message.

The contract fields stay permissively typed and gain descriptions naming the
accepted shapes. Tightening them to `str | LLMDefinition` would break the DSL
projection: a live custom `BaseLLM` whose config dump lacks a `model` key
degrades to a `{"ref": ...}` mapping, which such a type would reject -- turning
`flow_definition()` on an existing Python flow into a validation error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(flow): resolve declared LLM mappings for intent classification

`intent_llm` and `answer_from_history_llm` were documented as taking the same
shapes as `llm`, but both route through `_collapse_to_outcome`, whose own
coercion accepts only `str | BaseLLM`. A declared config mapping reached it
unchanged and raised "Invalid llm type: <class 'dict'>" mid-turn -- so the
documentation promised something that failed.

Also fixes the field descriptions. The previous commit's `llm` description
landed on `FlowConversationalRouterDefinition.llm` rather than
`FlowConversationalDefinition.llm`, because both fields are literally
`llm: Any = None` and the first match won. All four now describe their own
field and name every accepted shape, including `LLMDefinition` and a live
instance.

Test changes: adds an `LLMDefinition` resolution case, and the declared-mapping
turn test now patches `create_llm` to prove the mapping reaches it instead of
swapping the config out beforehand, which proved nothing.

Found by Cursor and CodeRabbit on #7062.

* docs(flows): name the full router LLM precedence; pin the coercion path

The conversational llm description skipped intent_llm in the router fallback
order (router.llm, then intent_llm, then llm), and the intent_llm test replaced
the declared mapping with a scripted LLM before the turn, so it never exercised
the coercion. Patch create_llm instead: removing the coercion now fails the
test with "Invalid llm type: <class 'dict'>".

Found by CodeRabbit on #7062.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 14:47:37 +05:30
..