This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 7993ee19955 Remove `agent_params` from LLM operators so they cannot 
run tools (#74721)
7993ee19955 is described below

commit 7993ee19955bc8f71aa23a4b22b86c5913025b32
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Oct 14 12:39:02 2026 +0100

    Remove `agent_params` from LLM operators so they cannot run tools (#74721)
    
    LLMOperator and the operators built on it pass agent_params straight to
    pydantic-ai's Agent, so tools, toolsets or capabilities turned a single
    model call into an agent loop without AgentOperator's tool approval,
    durable replay or tool logging. Replace agent_params with typed
    model_settings and output_retries arguments; AgentOperator keeps
    agent_params.
---
 providers/common/ai/docs/changelog.rst             | 10 ++++
 providers/common/ai/docs/operators/llm.rst         | 43 ++++++++++----
 providers/common/ai/docs/operators/llm_branch.rst  |  6 +-
 .../common/ai/docs/operators/llm_file_analysis.rst |  6 +-
 .../ai/docs/operators/llm_schema_compare.rst       |  6 +-
 .../example_dags/example_aip_progress_tracker.py   |  6 +-
 .../common/ai/example_dags/example_llm.py          | 11 ++--
 .../airflow/providers/common/ai/operators/llm.py   | 43 ++++++++++----
 .../providers/common/ai/operators/llm_branch.py    |  9 +--
 .../common/ai/operators/llm_file_analysis.py       | 11 ++--
 .../common/ai/operators/llm_schema_compare.py      |  8 ++-
 .../providers/common/ai/operators/llm_sql.py       | 10 ++--
 .../ai/tests/unit/common/ai/operators/test_llm.py  | 66 +++++++++++++++++++---
 .../unit/common/ai/operators/test_llm_branch.py    | 25 ++++++++
 .../common/ai/operators/test_llm_file_analysis.py  | 31 +++++++++-
 .../common/ai/operators/test_llm_schema_compare.py |  9 +--
 .../tests/unit/common/ai/operators/test_llm_sql.py | 13 +++--
 17 files changed, 241 insertions(+), 72 deletions(-)

diff --git a/providers/common/ai/docs/changelog.rst 
b/providers/common/ai/docs/changelog.rst
index 82706b87d39..4095637fe5c 100644
--- a/providers/common/ai/docs/changelog.rst
+++ b/providers/common/ai/docs/changelog.rst
@@ -26,6 +26,16 @@ Changelog
 ---------
 
 .. note::
+  ``LLMOperator``, the operators built on it (``LLMBranchOperator``, 
``LLMSQLQueryOperator``,
+  ``LLMSchemaCompareOperator``, ``LLMFileAnalysisOperator``) and their 
``@task.llm*``
+  decorators no longer accept ``agent_params``. These operators make one model 
call and run
+  no tools in the task; an agent with function tools, toolsets or capabilities 
belongs in
+  ``AgentOperator``, which keeps ``agent_params``. Passing ``agent_params`` to 
an LLM operator now fails when the Dag is
+  parsed. ``agent_params={"model_settings": {...}}`` becomes 
``model_settings={...}``,
+  ``agent_params={"retries": n}`` becomes ``output_retries=n``, and ``tools``, 
``toolsets``,
+  ``capabilities`` or ``spec_file`` move to ``AgentOperator`` or 
``@task.agent``. See
+  :ref:`howto/operator:llm`.
+
   On Airflow >= 3.3, the ``usage_limits`` of the ``LLM*`` operators 
(``LLMOperator``,
   ``LLMBranchOperator``, ``LLMSQLQueryOperator``, ``LLMSchemaCompareOperator``,
   ``LLMFileAnalysisOperator``) counts usage across every attempt of the task 
instance
diff --git a/providers/common/ai/docs/operators/llm.rst 
b/providers/common/ai/docs/operators/llm.rst
index fa0ef52a493..b789fa0472a 100644
--- a/providers/common/ai/docs/operators/llm.rst
+++ b/providers/common/ai/docs/operators/llm.rst
@@ -59,18 +59,35 @@ XCom unchanged, so downstream tasks can type-hint the class 
and use attribute ac
     :start-after: [START howto_operator_llm_structured]
     :end-before: [END howto_operator_llm_structured]
 
-Agent parameters
-----------------
+Model settings and output retries
+---------------------------------
 
-Pass additional keyword arguments to the pydantic-ai ``Agent`` constructor
-via ``agent_params``: for example ``retries``, ``model_settings``, or 
``tools``.
-See the `pydantic-ai Agent docs <https://ai.pydantic.dev/api/agent/>`__ for
-the full list of supported parameters.
+Set ``model_settings`` to pass Pydantic AI
+`ModelSettings <https://ai.pydantic.dev/api/settings/>`__ to the model call, 
such as
+``temperature`` or ``max_tokens``. Set ``output_retries`` to choose how many 
times the
+model is asked again when its output fails validation against ``output_type``.
 
 .. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_llm.py
     :language: python
-    :start-after: [START howto_operator_llm_agent_params]
-    :end-before: [END howto_operator_llm_agent_params]
+    :start-after: [START howto_operator_llm_model_settings]
+    :end-before: [END howto_operator_llm_model_settings]
+
+No tool runs in the task: ``LLMOperator`` accepts no function tools, toolsets 
or
+capabilities, so ``model_settings`` cannot register one. A provider-hosted 
tool that a
+provider's own setting turns on, such as ``openai_native_tools`` for OpenAI's 
web search,
+runs on the provider's side. For a model that calls your tools, use
+:ref:`AgentOperator <howto/operator:agent>`.
+
+Moving from ``agent_params``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Common AI 1.0 removes ``agent_params`` from ``LLMOperator``, the operators 
built on it and
+their ``@task.llm*`` decorators. Passing it fails when the Dag is parsed. Move 
each key:
+
+- ``agent_params={"model_settings": {...}}`` becomes ``model_settings={...}``.
+- ``agent_params={"retries": n}`` becomes ``output_retries=n``.
+- ``tools``, ``toolsets``, ``capabilities`` or ``spec_file`` in 
``agent_params``: use
+  ``AgentOperator`` or ``@task.agent``, which keep ``agent_params``.
 
 .. _howto/operator:llm_usage_limits:
 
@@ -81,8 +98,8 @@ Set ``usage_limits`` to a
 `pydantic-ai UsageLimits 
<https://ai.pydantic.dev/api/usage/#pydantic_ai.usage.UsageLimits>`__
 to fail the task when the run exceeds a configured budget: request count,
 input/output tokens, or tool calls. The check happens inside pydantic-ai's
-run loop, so the limit applies even when ``retries`` triggers multiple model
-calls within a single task.
+run loop, so the limit applies even when ``output_retries`` asks the model 
again
+within a single attempt.
 
 .. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_llm.py
     :language: python
@@ -263,8 +280,10 @@ Parameters
   the operator to return the output without a person, from 0 to 1; no reported
   confidence counts as uncertain. ``on_uncertain`` is ``"review"`` (default) or
   ``"fail"``. Default ``None``: no gate.
-- ``agent_params``: Additional keyword arguments passed to the pydantic-ai 
``Agent``
-  constructor (e.g. ``retries``, ``model_settings``, ``tools``). Supports 
Jinja templating.
+- ``model_settings``: Pydantic AI ``ModelSettings`` for the model call, such as
+  ``temperature`` or ``max_tokens``. Supports Jinja templating.
+- ``output_retries``: How many times the model is asked again when its output 
fails
+  validation. Default ``None`` (Pydantic AI's default).
 - ``usage_limits``: Optional pydantic-ai ``UsageLimits`` enforced on the run, 
or a
   ``dict`` of the same fields (templated via Jinja, then coerced per field 
type).
   Fails the task when token / request / tool-call budgets are exceeded, or 
when a
diff --git a/providers/common/ai/docs/operators/llm_branch.rst 
b/providers/common/ai/docs/operators/llm_branch.rst
index 0e113aed050..04abfe903f2 100644
--- a/providers/common/ai/docs/operators/llm_branch.rst
+++ b/providers/common/ai/docs/operators/llm_branch.rst
@@ -313,8 +313,10 @@ Parameters
   Jinja templating. Default ``None``.
 - ``allow_multiple_branches``: When ``False`` (default) the LLM returns a 
single
   task ID. When ``True`` the LLM may return one or more task IDs.
-- ``agent_params``: Additional keyword arguments passed to the pydantic-ai 
``Agent``
-  constructor (e.g. ``retries``, ``model_settings``). Supports Jinja 
templating.
+- ``model_settings``: Pydantic AI ``ModelSettings`` for the model call, such as
+  ``temperature`` or ``max_tokens``. Supports Jinja templating.
+- ``output_retries``: How many times the model is asked again when its output 
fails
+  validation. Default ``None`` (Pydantic AI's default).
 - ``usage_limits``: Optional pydantic-ai ``UsageLimits`` (or a templated 
``dict`` of
   the same fields) enforced on the run; the task fails when a budget is 
exceeded.
   Default ``None``. See :ref:`Usage Limits <howto/operator:llm_usage_limits>`.
diff --git a/providers/common/ai/docs/operators/llm_file_analysis.rst 
b/providers/common/ai/docs/operators/llm_file_analysis.rst
index 0f9cdbc6279..1a666b16893 100644
--- a/providers/common/ai/docs/operators/llm_file_analysis.rst
+++ b/providers/common/ai/docs/operators/llm_file_analysis.rst
@@ -150,8 +150,10 @@ Parameters
   built-in read-only guidance.
 - ``output_type``: Expected output type (default: ``str``). Set to a Pydantic
   ``BaseModel`` for structured output.
-- ``agent_params``: Additional keyword arguments passed to the pydantic-ai
-  ``Agent`` constructor (e.g. ``retries``, ``model_settings``).
+- ``model_settings``: Pydantic AI ``ModelSettings`` for the model call, such as
+  ``temperature`` or ``max_tokens``. Supports Jinja templating.
+- ``output_retries``: How many times the model is asked again when its output 
fails
+  validation. Default ``None`` (Pydantic AI's default).
 - ``usage_limits``: Optional pydantic-ai ``UsageLimits`` (or a templated 
``dict`` of
   the same fields) enforced on the run; the task fails when a budget is 
exceeded.
   Default ``None``. See :ref:`Usage Limits <howto/operator:llm_usage_limits>`.
diff --git a/providers/common/ai/docs/operators/llm_schema_compare.rst 
b/providers/common/ai/docs/operators/llm_schema_compare.rst
index 39be5fefa9d..aaaad00d60a 100644
--- a/providers/common/ai/docs/operators/llm_schema_compare.rst
+++ b/providers/common/ai/docs/operators/llm_schema_compare.rst
@@ -189,8 +189,10 @@ Parameters
   severity definitions. Passing a value **replaces** the default, so 
concatenate
   with ``DEFAULT_SYSTEM_PROMPT`` to extend it (see
   :ref:`Customizing the System Prompt <howto/operator:llm_schema_compare>` 
above).
-- ``agent_params``: Additional keyword arguments passed to the pydantic-ai
-  ``Agent`` constructor.
+- ``model_settings``: Pydantic AI ``ModelSettings`` for the model call, such as
+  ``temperature`` or ``max_tokens``. Supports Jinja templating.
+- ``output_retries``: How many times the model is asked again when its output 
fails
+  validation. Default ``None`` (Pydantic AI's default).
 - ``usage_limits``: Optional pydantic-ai ``UsageLimits`` (or a templated 
``dict`` of
   the same fields) enforced on the run; the task fails when a budget is 
exceeded.
   Default ``None``. See :ref:`Usage Limits <howto/operator:llm_usage_limits>`.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py
index 191935e42aa..3d322fcd616 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py
@@ -662,7 +662,7 @@ def example_aip_progress_tracker():
         system_prompt=ANALYSIS_SYSTEM_PROMPT,
         output_type=AIPStatus,
         serialize_output=True,
-        agent_params={"model_settings": {"temperature": 0}},
+        model_settings={"temperature": 0},
     ).expand(prompt=prompts)
     # [END aip_tracker_dtm_analysis]
 
@@ -750,7 +750,7 @@ Prioritize AIPs that are close to completion or have shared 
dependencies.
 Use only the data below -- do not add external information.
 
 {{ ti.xcom_pull(task_ids='format_report') }}""",
-        agent_params={"model_settings": {"temperature": 0}},
+        model_settings={"temperature": 0},
         usage_limits=UsageLimits(
             request_limit=5,
             input_tokens_limit=20_000,
@@ -787,7 +787,7 @@ Flag any claims not grounded in the evidence.
             input_tokens_limit=30_000,
             output_tokens_limit=8_000,
         ),
-        agent_params={"model_settings": {"temperature": 0}},
+        model_settings={"temperature": 0},
     )
     # [END aip_tracker_validation]
     synthesize >> validate
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
index b8d2c48b80b..25139db825c 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_llm.py
@@ -75,21 +75,22 @@ def example_llm_operator_structured():
 example_llm_operator_structured()
 
 
-# [START howto_operator_llm_agent_params]
+# [START howto_operator_llm_model_settings]
 @dag(tags=["example"])
-def example_llm_operator_agent_params():
+def example_llm_operator_model_settings():
     LLMOperator(
         task_id="creative_writing",
         prompt="Write a haiku about data pipelines.",
         llm_conn_id="pydanticai_default",
         system_prompt="You are a creative writer.",
-        agent_params={"model_settings": {"temperature": 0.9}, "retries": 3},
+        model_settings={"temperature": 0.9},
+        output_retries=3,
     )
 
 
-# [END howto_operator_llm_agent_params]
+# [END howto_operator_llm_model_settings]
 
-example_llm_operator_agent_params()
+example_llm_operator_model_settings()
 
 
 # [START howto_decorator_llm]
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm.py 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm.py
index 8d06c7764e4..ac894d4903d 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/operators/llm.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/operators/llm.py
@@ -65,6 +65,7 @@ if TYPE_CHECKING:
 
     from pydantic_ai import Agent
     from pydantic_ai.agent import AgentRunResult
+    from pydantic_ai.settings import ModelSettings
     from pydantic_ai.usage import UsageLimits
 
     from airflow.sdk import Context
@@ -91,6 +92,11 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
     The Pydantic class must be defined at module scope: classes nested inside
     a function or ``@dag``-decorated body cannot be deserialized from XCom.
 
+    No tool runs in the task: the operator accepts no function tools, toolsets 
or
+    capabilities, makes one model call (repeated only when the output fails 
validation)
+    and returns its output. For an agent that calls tools, use
+    :class:`~airflow.providers.common.ai.operators.agent.AgentOperator`.
+
     Once ``_clear_usage_budget`` has run, nothing may fail the attempt (raising
     ``DownstreamTasksSkipped`` to signal success is fine): a failure would be 
retried
     with a fresh budget. This applies to subclasses that override ``execute`` 
or
@@ -114,10 +120,12 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         returned to XCom unchanged so downstream tasks can type-hint it
         directly. The class must be defined at module scope -- nested classes
         cannot be deserialized from XCom.
-    :param agent_params: Additional keyword arguments passed to the pydantic-ai
-        ``Agent`` constructor (e.g. ``retries``, ``model_settings``, 
``tools``).
-        See `pydantic-ai Agent docs <https://ai.pydantic.dev/api/agent/>`__
-        for the full list.
+    :param model_settings: Pydantic AI 
:class:`~pydantic_ai.settings.ModelSettings`
+        for the model call, such as ``temperature`` or ``max_tokens``, 
including
+        provider-specific settings. ``None`` (default) uses the model's 
defaults.
+    :param output_retries: How many times the model is asked again when its 
output
+        fails validation against ``output_type``. ``None`` (default) uses 
Pydantic AI's
+        default.
     :param usage_limits: Optional pydantic-ai
         :class:`~pydantic_ai.usage.UsageLimits` enforced on the run, or a dict
         of the same fields (e.g.
@@ -198,7 +206,7 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         "model_id",
         "fallback_conn_ids",
         "system_prompt",
-        "agent_params",
+        "model_settings",
         "usage_limits",
     )
 
@@ -211,7 +219,8 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         fallback_conn_ids: list[str] | None = None,
         system_prompt: str = "",
         output_type: type | UnionType = str,
-        agent_params: dict[str, Any] | None = None,
+        model_settings: ModelSettings | None = None,
+        output_retries: int | None = None,
         usage_limits: UsageLimits | dict[str, Any] | None = None,
         require_approval: bool = False,
         approval_timeout: timedelta | None = None,
@@ -241,7 +250,8 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         # for deserialization (its worker-side DAG walk); otherwise, or when 
the
         # user opts in, dump to a dict so the value is deserializable anywhere.
         self._serialize_model_output = serialize_output or not _CORE_WALKER
-        self.agent_params = agent_params or {}
+        self.model_settings = model_settings
+        self.output_retries = output_retries
         # No validation here -- see coerce_usage_limits() docstring for why.
         self.usage_limits = usage_limits
         if on_approval_timeout not in ("fail", 
*LLMApprovalMixin.TIMEOUT_DEFAULTS):
@@ -305,6 +315,21 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         }
         return PydanticAIHook.get_hook(self.llm_conn_id, 
hook_params=hook_params)
 
+    def _create_agent(self, *, output_type: Any, instructions: str) -> 
Agent[object, Any]:
+        """
+        Build the agent for this operator's model call.
+
+        The only way any LLM operator builds its agent. Only 
``model_settings`` and
+        ``output_retries`` reach it, so no function tool, toolset or 
capability is ever
+        registered; an agent with tools is what ``AgentOperator`` is for.
+        """
+        return self.llm_hook.create_agent(
+            output_type=output_type,
+            instructions=instructions,
+            model_settings=self.model_settings,
+            retries=self.output_retries,
+        )
+
     def execute(self, context: Context) -> Any:
         if self._may_review:
             self.validate_approval_prompt()  # type: ignore[misc]
@@ -312,9 +337,7 @@ class LLMOperator(CancellableAgentRunMixin, BaseOperator, 
LLMApprovalMixin, Usag
         # Coerced first so a bad rendered value fails before the expensive 
setup below.
         usage_limits = coerce_usage_limits(self.usage_limits)
 
-        agent: Agent[object, Any] = self.llm_hook.create_agent(
-            output_type=self.output_type, instructions=self.system_prompt, 
**self.agent_params
-        )
+        agent = self._create_agent(output_type=self.output_type, 
instructions=self.system_prompt)
         result = self._run_llm(context, agent, self.prompt, 
usage_limits=usage_limits)
         output = result.output
 
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_branch.py 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_branch.py
index f5d35cb1543..ec2a4511e96 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_branch.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_branch.py
@@ -122,8 +122,10 @@ class LLMBranchOperator(LLMOperator, BranchMixIn):
         every downstream task rather than only the direct ones, so a task whose
         trigger rule would still run it is skipped too. Only takes effect when 
a
         review is opened. Default ``False``.
-    :param agent_params: Additional keyword arguments passed to the pydantic-ai
-        ``Agent`` constructor (e.g. ``retries``, ``model_settings``, 
``tools``).
+    :param model_settings: Pydantic AI ``ModelSettings`` for the model call, 
such as
+        ``temperature`` or ``max_tokens``. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
+    :param output_retries: How many times the model is asked again when its 
output
+        fails validation. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
 
     ``usage_limits`` is inherited from
     :class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
@@ -229,10 +231,9 @@ class LLMBranchOperator(LLMOperator, BranchMixIn):
         # Coerced first so a bad rendered value fails before the expensive 
setup below.
         usage_limits = coerce_usage_limits(self.usage_limits)
 
-        agent = self.llm_hook.create_agent(
+        agent = self._create_agent(
             output_type=output_type,
             instructions=self.system_prompt,
-            **self.agent_params,
         )
         result = self._run_llm(context, agent, self.prompt, 
usage_limits=usage_limits)
         output = result.output
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
index cf176992eeb..9e61e34a3e8 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_file_analysis.py
@@ -28,8 +28,6 @@ from airflow.providers.common.ai.utils.file_analysis import 
build_file_analysis_
 from airflow.providers.common.ai.utils.usage import coerce_usage_limits
 
 if TYPE_CHECKING:
-    from pydantic_ai import Agent
-
     from airflow.sdk import Context
 
 
@@ -55,8 +53,10 @@ class LLMFileAnalysisOperator(LLMOperator):
         for how blank entries in the list are dropped.
     :param system_prompt: Additional instructions appended to the built-in
         file-analysis system prompt.
-    :param agent_params: Additional keyword arguments passed to the pydantic-ai
-        ``Agent`` constructor (e.g. ``retries``, ``model_settings``, 
``tools``).
+    :param model_settings: Pydantic AI ``ModelSettings`` for the model call, 
such as
+        ``temperature`` or ``max_tokens``. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
+    :param output_retries: How many times the model is asked again when its 
output
+        fails validation. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
     :param file_path: File or prefix to analyze.
     :param file_conn_id: Optional connection ID for the storage backend.
         Overrides a connection embedded in ``file_path``.
@@ -163,10 +163,9 @@ class LLMFileAnalysisOperator(LLMOperator):
             self.sample_rows,
         )
         self.log.debug("Resolved file analysis paths: %s", 
request.resolved_paths)
-        agent: Agent[object, Any] = self.llm_hook.create_agent(
+        agent = self._create_agent(
             output_type=self.output_type,
             instructions=self._build_system_prompt(),
-            **self.agent_params,
         )
         result = self._run_llm(context, agent, request.user_content, 
usage_limits=usage_limits)
         output = result.output
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_schema_compare.py
 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_schema_compare.py
index 91723596627..2e2f8d11502 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_schema_compare.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_schema_compare.py
@@ -106,7 +106,10 @@ class LLMSchemaCompareOperator(LLMOperator):
     :param system_prompt: Instructions included in the LLM system prompt. 
Defaults to
         ``DEFAULT_SYSTEM_PROMPT`` which contains cross-system type 
equivalences and
         severity definitions. Passing a value **replaces** the default system 
prompt
-    :param agent_params: Extra keyword arguments for the pydantic-ai ``Agent``.
+    :param model_settings: Pydantic AI ``ModelSettings`` for the model call, 
such as
+        ``temperature`` or ``max_tokens``. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
+    :param output_retries: How many times the model is asked again when its 
output
+        fails validation. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
     :param data_sources: List of DataSourceConfig objects, one per system.
     :param db_conn_ids: Connection IDs for databases to compare (used with
         ``table_names``).
@@ -337,10 +340,9 @@ class LLMSchemaCompareOperator(LLMOperator):
 
         full_system_prompt = self._build_system_prompt(schema_context)
 
-        agent = self.llm_hook.create_agent(
+        agent = self._create_agent(
             output_type=SchemaCompareResult,
             instructions=full_system_prompt,
-            **self.agent_params,
         )
         self.log.info("Running LLM schema comparison...")
         result = self._run_llm(context, agent, self.prompt, 
usage_limits=usage_limits)
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py 
b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py
index 28247e3eae4..931ad82f0a1 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/operators/llm_sql.py
@@ -73,8 +73,10 @@ class LLMSQLQueryOperator(LLMOperator):
         for how blank entries in the list are dropped.
     :param system_prompt: Additional instructions appended to the built-in SQL
         safety prompt. Use for domain-specific guidance.
-    :param agent_params: Additional keyword arguments passed to the pydantic-ai
-        ``Agent`` constructor (e.g. ``retries``, ``model_settings``).
+    :param model_settings: Pydantic AI ``ModelSettings`` for the model call, 
such as
+        ``temperature`` or ``max_tokens``. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
+    :param output_retries: How many times the model is asked again when its 
output
+        fails validation. See 
:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`.
     :param db_conn_id: Connection ID for database schema introspection.
         The connection must resolve to a ``DbApiHook``.
     :param table_names: Tables to include in the LLM's schema context.
@@ -161,9 +163,7 @@ class LLMSQLQueryOperator(LLMOperator):
 
         full_system_prompt = self._build_system_prompt(schema_info)
 
-        agent = self.llm_hook.create_agent(
-            output_type=str, instructions=full_system_prompt, 
**self.agent_params
-        )
+        agent = self._create_agent(output_type=str, 
instructions=full_system_prompt)
         result = self._run_llm(context, agent, self.prompt, 
usage_limits=usage_limits)
         sql = self._strip_llm_output(result.output, 
dialect=self._resolved_dialect)
 
diff --git a/providers/common/ai/tests/unit/common/ai/operators/test_llm.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm.py
index 4d679cf7e24..6d57211e248 100644
--- a/providers/common/ai/tests/unit/common/ai/operators/test_llm.py
+++ b/providers/common/ai/tests/unit/common/ai/operators/test_llm.py
@@ -25,7 +25,7 @@ from uuid import uuid4
 import pytest
 from pydantic import BaseModel
 from pydantic_ai import Agent
-from pydantic_ai.exceptions import UsageLimitExceeded
+from pydantic_ai.exceptions import UnexpectedModelBehavior, UsageLimitExceeded
 from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart
 from pydantic_ai.models.function import AgentInfo, FunctionModel
 from pydantic_ai.usage import RequestUsage, RunUsage, UsageLimits
@@ -35,10 +35,14 @@ from airflow.providers.common.ai.mixins.approval import (
 )
 from airflow.providers.common.ai.operators import llm as llm_module
 from airflow.providers.common.ai.operators.llm import DecisionPolicy, 
LLMOperator
+from airflow.providers.common.ai.operators.llm_branch import LLMBranchOperator
+from airflow.providers.common.ai.operators.llm_file_analysis import 
LLMFileAnalysisOperator
+from airflow.providers.common.ai.operators.llm_schema_compare import 
LLMSchemaCompareOperator
+from airflow.providers.common.ai.operators.llm_sql import LLMSQLQueryOperator
 from airflow.providers.common.ai.utils.logging import MODEL_NAME_XCOM_KEY
 from airflow.providers.common.ai.utils.usage_budget import USAGE_BUDGET_KEY
 
-from tests_common.test_utils.version_compat import AIRFLOW_V_3_1_PLUS, 
AIRFLOW_V_3_3_PLUS
+from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS, 
AIRFLOW_V_3_1_PLUS, AIRFLOW_V_3_3_PLUS
 
 try:
     from airflow.sdk.serde import SUPPORTS_OPERATOR_DESERIALIZATION_WALKER as 
_CORE_WALKER
@@ -46,7 +50,11 @@ except ImportError:
     _CORE_WALKER = False
 
 from airflow.providers.common.compat.notifier import BaseNotifier
-from airflow.providers.common.compat.sdk import 
AirflowOptionalProviderFeatureException, TaskDeferred
+from airflow.providers.common.compat.sdk import (
+    AirflowException,
+    AirflowOptionalProviderFeatureException,
+    TaskDeferred,
+)
 from airflow.providers.standard.exceptions import HITLRejectException, 
HITLTimeoutError
 
 if AIRFLOW_V_3_3_PLUS:
@@ -99,11 +107,54 @@ class TestLLMOperator:
             "model_id",
             "fallback_conn_ids",
             "system_prompt",
-            "agent_params",
+            "model_settings",
             "usage_limits",
         }
         assert set(LLMOperator.template_fields) == expected
 
+    @pytest.mark.parametrize(
+        ("operator_cls", "operator_kwargs"),
+        [
+            pytest.param(LLMOperator, {}, id="llm"),
+            pytest.param(LLMBranchOperator, {}, id="llm_branch"),
+            pytest.param(LLMSQLQueryOperator, {}, id="llm_sql"),
+            pytest.param(LLMFileAnalysisOperator, {"file_path": 
"/tmp/app.log"}, id="llm_file_analysis"),
+            pytest.param(
+                LLMSchemaCompareOperator,
+                {"db_conn_ids": ["db_a", "db_b"], "table_names": ["orders"]},
+                id="llm_schema_compare",
+            ),
+        ],
+    )
+    def test_llm_operators_reject_agent_params(self, operator_cls, 
operator_kwargs):
+        """No LLM operator accepts agent_params, the one argument that reached 
Agent(tools=...)."""
+        # Airflow 2's BaseOperator reports an unexpected argument as 
AirflowException.
+        with pytest.raises(TypeError if AIRFLOW_V_3_0_PLUS else 
AirflowException, match="agent_params"):
+            operator_cls(
+                task_id="test",
+                prompt="p",
+                llm_conn_id="my_llm",
+                agent_params={"tools": []},
+                **operator_kwargs,
+            )
+
+    @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
+    def test_output_retries_bound_how_often_invalid_output_is_retried(self, 
mock_hook_cls):
+        calls = []
+
+        def respond(messages, info: AgentInfo):
+            calls.append(messages)
+            return ModelResponse(parts=[TextPart(content="not a number")])
+
+        mock_hook_cls.get_hook.return_value.create_agent.side_effect = lambda 
**kw: Agent(
+            FunctionModel(respond), **kw
+        )
+        op = LLMOperator(task_id="test", prompt="p", llm_conn_id="my_llm", 
output_type=int, output_retries=2)
+
+        with pytest.raises(UnexpectedModelBehavior):
+            op.execute(context=MagicMock())
+        assert len(calls) == 3
+
     @pytest.mark.parametrize(
         "bad",
         [pytest.param([], id="list"), pytest.param("0.5", id="str"), 
pytest.param(5, id="int")],
@@ -139,7 +190,7 @@ class TestLLMOperator:
             "What is the capital of France?", usage_limits=None, 
cancellation_token=ANY
         )
         
mock_hook_cls.get_hook.return_value.create_agent.assert_called_once_with(
-            output_type=str, instructions=""
+            output_type=str, instructions="", model_settings=None, retries=None
         )
         mock_hook_cls.get_hook.assert_called_once_with(
             "my_llm", hook_params={"model_id": None, "fallback_conn_ids": None}
@@ -294,7 +345,8 @@ class TestLLMOperator:
             model_id="openai:gpt-5",
             system_prompt="You are an extractor.",
             output_type=Entities,
-            agent_params={"retries": 3, "model_settings": {"temperature": 
0.9}},
+            model_settings={"temperature": 0.9},
+            output_retries=3,
         )
         result = op.execute(context=MagicMock())
 
@@ -306,8 +358,8 @@ class TestLLMOperator:
         
mock_hook_cls.get_hook.return_value.create_agent.assert_called_once_with(
             output_type=Entities,
             instructions="You are an extractor.",
-            retries=3,
             model_settings={"temperature": 0.9},
+            retries=3,
         )
 
     @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
diff --git 
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_branch.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_branch.py
index ac0f46870b4..8960add157e 100644
--- a/providers/common/ai/tests/unit/common/ai/operators/test_llm_branch.py
+++ b/providers/common/ai/tests/unit/common/ai/operators/test_llm_branch.py
@@ -67,6 +67,31 @@ class TestLLMBranchOperator:
                 output_type=int,
             )
 
+    @patch.object(LLMBranchOperator, "do_branch")
+    @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
+    def test_execute_builds_agent_with_model_settings_and_output_retries_only(
+        self, mock_hook_cls, mock_do_branch, make_mock_run_result
+    ):
+        mock_agent = MagicMock(spec=["run_sync"])
+        mock_agent.run_sync.return_value = make_mock_run_result("task_a")
+        mock_hook_cls.get_hook.return_value.create_agent.return_value = 
mock_agent
+        mock_do_branch.return_value = "task_a"
+
+        op = LLMBranchOperator(
+            task_id="test",
+            prompt="Pick a branch",
+            llm_conn_id="my_llm",
+            model_settings={"temperature": 0},
+            output_retries=2,
+        )
+        op.downstream_task_ids = {"task_a", "task_b"}
+        op.execute(MagicMock())
+
+        create_agent_kwargs = 
mock_hook_cls.get_hook.return_value.create_agent.call_args.kwargs
+        assert set(create_agent_kwargs) == {"output_type", "instructions", 
"model_settings", "retries"}
+        assert create_agent_kwargs["model_settings"] == {"temperature": 0}
+        assert create_agent_kwargs["retries"] == 2
+
     @patch.object(LLMBranchOperator, "do_branch")
     @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
     def test_execute_single_branch(self, mock_hook_cls, mock_do_branch, 
make_mock_run_result):
diff --git 
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
index 2a118e08764..74e2f8f2b59 100644
--- 
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
+++ 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_file_analysis.py
@@ -95,13 +95,42 @@ class TestLLMFileAnalysisOperator:
             "model_id",
             "fallback_conn_ids",
             "system_prompt",
-            "agent_params",
+            "model_settings",
             "usage_limits",
             "file_path",
             "file_conn_id",
         }
         assert set(LLMFileAnalysisOperator.template_fields) == expected
 
+    @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
+    @patch(
+        
"airflow.providers.common.ai.operators.llm_file_analysis.build_file_analysis_request",
 autospec=True
+    )
+    def test_execute_builds_agent_with_model_settings_and_output_retries_only(
+        self, mock_build_request, mock_hook_cls
+    ):
+        mock_build_request.return_value = FileAnalysisRequest(
+            user_content="prepared prompt", resolved_paths=["/tmp/app.log"], 
total_size_bytes=10
+        )
+        mock_agent = MagicMock(spec=["run_sync"])
+        mock_agent.run_sync.return_value = _make_mock_run_result("Analysis 
complete")
+        mock_hook_cls.get_hook.return_value.create_agent.return_value = 
mock_agent
+
+        op = LLMFileAnalysisOperator(
+            task_id="test",
+            prompt="Summarize the file",
+            llm_conn_id="my_llm",
+            file_path="/tmp/app.log",
+            model_settings={"temperature": 0},
+            output_retries=2,
+        )
+        op.execute(context={})
+
+        create_agent_kwargs = 
mock_hook_cls.get_hook.return_value.create_agent.call_args.kwargs
+        assert set(create_agent_kwargs) == {"output_type", "instructions", 
"model_settings", "retries"}
+        assert create_agent_kwargs["model_settings"] == {"temperature": 0}
+        assert create_agent_kwargs["retries"] == 2
+
     @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
     @patch(
         
"airflow.providers.common.ai.operators.llm_file_analysis.build_file_analysis_request",
 autospec=True
diff --git 
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_schema_compare.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_schema_compare.py
index 5a0f84e82c3..c8cc658bb93 100644
--- 
a/providers/common/ai/tests/unit/common/ai/operators/test_llm_schema_compare.py
+++ 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_schema_compare.py
@@ -166,11 +166,11 @@ class TestLLMSchemaCompareOperator:
             data_sources=[ds, _make_ds_config(conn_id="ds2")],
             context_strategy="basic",
             system_prompt="additional instructions",
-            agent_params={"temperature": 0.5},
+            model_settings={"temperature": 0.5},
         )
         assert op.context_strategy == "basic"
         assert op.system_prompt == "additional instructions"
-        assert op.agent_params == {"temperature": 0.5}
+        assert op.model_settings == {"temperature": 0.5}
 
     
@mock.patch("airflow.providers.common.ai.operators.llm_schema_compare.BaseHook")
     def test_get_db_hook_success(self, mock_base_hook):
@@ -272,7 +272,7 @@ class TestLLMSchemaCompareOperator:
             llm_conn_id="llm_conn",
             db_conn_ids=["postgres_default", "snowflake_default"],
             table_names=["orders"],
-            agent_params={"param": "value"},
+            model_settings={"temperature": 0},
         )
 
         mock_llm_hook = mock.Mock()
@@ -290,7 +290,8 @@ class TestLLMSchemaCompareOperator:
         mock_llm_hook.create_agent.assert_called_once_with(
             output_type=SchemaCompareResult,
             instructions="system_prompt",
-            param="value",
+            model_settings={"temperature": 0},
+            retries=None,
         )
         mock_agent.run_sync.assert_called_once_with(
             "user_prompt", usage_limits=None, cancellation_token=mock.ANY
diff --git a/providers/common/ai/tests/unit/common/ai/operators/test_llm_sql.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_llm_sql.py
index d70853f6a76..3d5a696e1a5 100644
--- a/providers/common/ai/tests/unit/common/ai/operators/test_llm_sql.py
+++ b/providers/common/ai/tests/unit/common/ai/operators/test_llm_sql.py
@@ -209,7 +209,7 @@ class TestLLMSQLQueryOperator:
             "model_id",
             "fallback_conn_ids",
             "system_prompt",
-            "agent_params",
+            "model_settings",
             "usage_limits",
             "db_conn_id",
             "table_names",
@@ -321,8 +321,8 @@ class TestLLMSQLQueryOperator:
         assert result == "DROP TABLE users"
 
     @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
-    def test_execute_passes_agent_params(self, mock_hook_cls, 
make_mock_run_result):
-        """agent_params inherited from LLMOperator are unpacked into 
create_agent."""
+    def test_execute_passes_model_settings_and_output_retries(self, 
mock_hook_cls, make_mock_run_result):
+        """model_settings and output_retries inherited from LLMOperator reach 
create_agent."""
         mock_hook_cls.get_hook.return_value.create_agent.return_value = 
_make_mock_agent(
             "SELECT 1", make_mock_run_result
         )
@@ -331,13 +331,14 @@ class TestLLMSQLQueryOperator:
             task_id="test",
             prompt="test",
             llm_conn_id="my_llm",
-            agent_params={"retries": 3, "model_settings": {"temperature": 0}},
+            model_settings={"temperature": 0},
+            output_retries=3,
         )
         op.execute(context=MagicMock())
 
         create_agent_call = 
mock_hook_cls.get_hook.return_value.create_agent.call_args
-        assert create_agent_call[1]["retries"] == 3
-        assert create_agent_call[1]["model_settings"] == {"temperature": 0}
+        assert create_agent_call.kwargs["retries"] == 3
+        assert create_agent_call.kwargs["model_settings"] == {"temperature": 0}
 
     @patch("airflow.providers.common.ai.operators.llm.PydanticAIHook", 
autospec=True)
     def test_system_prompt_appended_to_sql_instructions(self, mock_hook_cls, 
make_mock_run_result):

Reply via email to