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

Lee-W 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 97fc3f8d814 Add self-hosted model guide for the common.ai provider 
(#69867)
97fc3f8d814 is described below

commit 97fc3f8d81456a4b1ac2376d0bbb2c2507de777b
Author: Wei Lee <[email protected]>
AuthorDate: Thu Jul 16 19:00:41 2026 +0800

    Add self-hosted model guide for the common.ai provider (#69867)
---
 providers/common/ai/docs/index.rst                 |   1 +
 providers/common/ai/docs/retry_policies.rst        |   3 +-
 providers/common/ai/docs/self_hosted_models.rst    | 378 +++++++++++++++++++++
 .../common/ai/example_dags/example_agent.py        |  36 ++
 4 files changed, 417 insertions(+), 1 deletion(-)

diff --git a/providers/common/ai/docs/index.rst 
b/providers/common/ai/docs/index.rst
index a3be5daf4d7..9fce0fd08e0 100644
--- a/providers/common/ai/docs/index.rst
+++ b/providers/common/ai/docs/index.rst
@@ -134,6 +134,7 @@ See the Optional dependencies table below for the exact 
package each extra insta
     Operators <operators/index>
     Examples <examples>
     Retry Policies <retry_policies>
+    Self-hosted models <self_hosted_models>
     HITL Review <hitl_review>
     Observability <observability>
 
diff --git a/providers/common/ai/docs/retry_policies.rst 
b/providers/common/ai/docs/retry_policies.rst
index 8456b1262aa..a24e5df4481 100644
--- a/providers/common/ai/docs/retry_policies.rst
+++ b/providers/common/ai/docs/retry_policies.rst
@@ -161,7 +161,8 @@ Local LLM support
 -----------------
 
 For environments where exception data must not leave the infrastructure, point
-to a local model via Ollama or vLLM:
+to a local model via Ollama or vLLM -- see :ref:`howto/self_hosted_models` for
+general self-hosted connection setup:
 
 .. code-block:: python
 
diff --git a/providers/common/ai/docs/self_hosted_models.rst 
b/providers/common/ai/docs/self_hosted_models.rst
new file mode 100644
index 00000000000..8919051683b
--- /dev/null
+++ b/providers/common/ai/docs/self_hosted_models.rst
@@ -0,0 +1,378 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. _howto/self_hosted_models:
+
+Self-hosted models
+===================
+
+This guide serves an open model on your own infrastructure, points
+:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIHook` at it
+instead of a cloud provider, runs a ``@task.llm`` task and an
+``AgentOperator`` with tool calling against it, and then routes the same
+setup through a bearer-token AI gateway instead of talking to the server
+directly. Running a model locally keeps prompt and response data inside your
+own infrastructure, avoids per-call cost, and works offline.
+
+Before you start
+------------------
+
+This guide assumes a working :doc:`apache-airflow:installation/index`
+(Airflow 3.0+) already exists. Its job stops at wiring Airflow to a server
+that's already running -- it doesn't cover installing or operating the
+model-serving stack itself.
+
+Prerequisites
+^^^^^^^^^^^^^
+
+You need an OpenAI-compatible model server reachable from where Airflow
+runs, and hardware able to run the model you pick. The provider is built on
+pydantic-ai, which reaches these servers by speaking the OpenAI-compatible
+wire protocol -- that's why the server needs to expose an OpenAI-compatible
+API. Any server that does works the same way; see
+pydantic-ai's `OpenAI-compatible models docs
+<https://pydantic.dev/docs/ai/models/overview/#openai-compatible-providers>`__
+for the full list of what qualifies. Ollama, vLLM, and LM Studio are three
+common choices, used as the examples throughout this guide:
+
+- `Ollama <https://ollama.com/>`__ -- see its
+  `hardware support docs <https://docs.ollama.com/gpu>`__ for GPU/CPU
+  requirements.
+- `vLLM <https://docs.vllm.ai/>`__ -- a higher-throughput alternative,
+  commonly GPU-hosted; see its
+  `GPU installation requirements 
<https://docs.vllm.ai/en/latest/getting_started/installation/gpu/>`__.
+- `LM Studio <https://lmstudio.ai/>`__ -- a desktop app with a built-in
+  OpenAI-compatible server; see its
+  `system requirements <https://lmstudio.ai/docs/app/system-requirements>`__.
+
+This page uses these three as running examples rather than trying to cover
+every option; for how to point Airflow at any of the others, see
+:ref:`self_hosted_models_other_servers` below.
+
+1. Start an OpenAI-compatible server
+--------------------------------------
+
+Ollama is the reference server this guide proves end to end. The vLLM and LM
+Studio tabs show equivalent wiring for readers already running one of those
+instead, but this guide hasn't exercised them itself.
+
+.. tab-set::
+
+    .. tab-item:: Ollama
+        :sync: ollama
+
+        `Install it <https://ollama.com/download>`__ and pull a model:
+
+        .. code-block:: bash
+
+            ollama pull gemma4
+
+        Installing Ollama sets up a background service (the macOS/Windows app,
+        or a systemd service on Linux), so the server is already running --
+        ``ollama pull`` only downloads the model. Ollama then serves an
+        OpenAI-compatible API at ``http://localhost:11434``. On a headless host
+        with no service, start it yourself with ``ollama serve``.
+
+    .. tab-item:: vLLM
+        :sync: vllm
+
+        Unlike Ollama and LM Studio, vLLM serves a model by its full
+        HuggingFace identifier (``organization/model-name``) rather than a
+        short tag. Serve one, aliasing the served name to ``gemma4`` to match
+        the connection below:
+
+        .. code-block:: bash
+
+            vllm serve Qwen/Qwen2.5-1.5B-Instruct --served-model-name gemma4
+
+        See `vLLM's installation docs 
<https://docs.vllm.ai/en/latest/getting_started/installation/>`__
+        for install and GPU requirements. vLLM then serves an
+        OpenAI-compatible API at ``http://localhost:8000``.
+
+    .. tab-item:: LM Studio
+        :sync: lmstudio
+
+        Download a model, load it, then start the server:
+
+        .. code-block:: bash
+
+            lms get gemma4
+            lms load gemma4
+            lms server start
+
+        See `LM Studio's CLI docs <https://lmstudio.ai/docs/cli>`__ for the
+        full ``lms`` reference (you can also download and load a model from
+        the LM Studio UI instead). LM Studio then serves an OpenAI-compatible
+        API at ``http://localhost:1234``.
+
+2. Create the connection
+--------------------------
+
+Every LLM call goes through a Pydantic AI connection (``conn_type``
+``pydanticai``, default connection id ``pydanticai_default``).
+
+.. tab-set::
+
+    .. tab-item:: Ollama
+        :sync: ollama
+
+        For a local, keyless server:
+
+        .. code-block:: bash
+
+            export AIRFLOW_CONN_PYDANTICAI_DEFAULT='{"conn_type": 
"pydanticai", "host": "http://localhost:11434/v1";, "extra": {"model": 
"ollama:gemma4"}}'
+
+    .. tab-item:: vLLM
+        :sync: vllm
+
+        .. code-block:: bash
+
+            export AIRFLOW_CONN_PYDANTICAI_DEFAULT='{"conn_type": 
"pydanticai", "host": "http://localhost:8000/v1";, "extra": {"model": 
"openai:gemma4"}}'
+
+    .. tab-item:: LM Studio
+        :sync: lmstudio
+
+        .. code-block:: bash
+
+            export AIRFLOW_CONN_PYDANTICAI_DEFAULT='{"conn_type": 
"pydanticai", "host": "http://localhost:1234/v1";, "extra": {"model": 
"openai:gemma4"}}'
+
+        ``http://localhost:1234`` is the default server port shown in `LM
+        Studio's OpenAI-compatibility docs
+        <https://lmstudio.ai/docs/app/api/endpoints/openai>`__.
+
+- ``host`` -- the server's base URL.
+- ``password`` -- left empty for a keyless local server. That triggers two
+  separate things: the Airflow hook forwards no key at all when ``password``
+  is empty (only ``base_url`` goes to the provider constructor); pydantic-ai's
+  provider class then fills in its own placeholder API key and does send
+  that placeholder in the actual HTTP request. A server with no auth check
+  doesn't care, so the request still succeeds either way.
+- ``extra["model"]`` -- ``ollama:<model>`` picks up Ollama-specific tuning;
+  vLLM and LM Studio have no dedicated pydantic-ai provider class, so they
+  use ``openai:<model>`` instead. See
+  :ref:`Model identifier format <self_hosted_models_model_id>` below for
+  details.
+
+The examples above all use ``gemma4`` to illustrate the format -- the actual
+value must match a model your server is really serving:
+
+- Ollama -- ``ollama list`` shows the tags you've pulled.
+- vLLM -- defaults to its ``--model`` startup argument; the
+  ``--served-model-name`` flag can set an alias explicitly (e.g. to
+  ``gemma4``) if you want the served name to differ from ``--model``.
+- LM Studio -- the model identifier shown for whatever you loaded in its UI.
+- Any of the three (and any other OpenAI-compatible server) --
+  ``curl <host>/v1/models`` lists what the server actually reports as
+  available.
+
+See :ref:`howto/connection:pydanticai` for the full connection field
+reference.
+
+3. Run your first Dag with ``@task.llm``
+-------------------------------------------
+
+The ``@task.llm`` decorator turns a function that returns a prompt string
+into a task that sends that prompt to the connection above and returns the
+response:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_quickstart.py
+    :language: python
+    :start-after: [START howto_quickstart_llm]
+    :end-before: [END howto_quickstart_llm]
+
+Run it like any other Dag (``airflow dags test quickstart_llm``); the
+``summarize`` task completes using the local model, not a cloud provider.
+
+4. Run an agent with tool calling
+------------------------------------
+
+Tool calling needs a model that supports pydantic-ai's ``tools`` capability
+-- not every small local model does.
+
+.. tab-set::
+
+    .. tab-item:: Ollama
+        :sync: ollama
+
+        The connection from the previous steps already points at a model
+        that supports it, so this step runs against the same connection
+        with no changes.
+
+    .. tab-item:: vLLM
+        :sync: vllm
+
+        vLLM needs extra server startup flags to enable tool calling for a
+        given model (``--enable-auto-tool-choice`` plus a matching
+        ``--tool-call-parser``) -- see its
+        `Tool Calling docs 
<https://docs.vllm.ai/en/latest/features/tool_calling/>`__
+        for how to pick the right ones for your model.
+
+    .. tab-item:: LM Studio
+        :sync: lmstudio
+
+        LM Studio's built-in server supports tool calling out of the box
+        through ``/v1/chat/completions`` -- see its
+        `Tool Use docs 
<https://lmstudio.ai/docs/developer/openai-compat/tools>`__.
+        Its docs note that smaller models, or ones not trained for tool use,
+        may return improperly formatted tool calls.
+
+``AgentOperator`` can wrap an existing Airflow hook as a tool via
+``HookToolset``. This example wraps an HTTP connection named ``my_api`` --
+point it at any reachable HTTP endpoint that returns JSON:
+
+.. code-block:: bash
+
+    export AIRFLOW_CONN_MY_API='{"conn_type": "http", "host": "<your reachable 
API base URL>"}'
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_agent.py
+    :language: python
+    :start-after: [START howto_agent_self_hosted]
+    :end-before: [END howto_agent_self_hosted]
+
+The hook above is constructed with ``method="GET"`` because the target
+endpoint -- the OpenAI-compatible model list (``/v1/models``) every one of
+these servers exposes -- is GET-only, while ``HttpHook``
+defaults to ``method="POST"``
+(:class:`~airflow.providers.http.hooks.http.HttpHook`) -- fixed once the
+hook is constructed, not something the model can change per call.
+
+The prompt itself just asks a natural question -- it's the ``system_prompt``
+that tells the agent where to find the answer (``GET /v1/models``), so the
+model doesn't have to guess a path. That matters because ``HttpHook`` raises
+on any response outside the 2xx/3xx range: if the model guessed wrong, the
+task would fail outright rather than the agent recovering gracefully.
+
+Routing through an AI gateway
+--------------------------------
+
+The same wiring routes through a bearer-token AI gateway instead of talking
+to the server directly -- swap three connection fields and nothing else
+changes:
+
+- ``host`` -- the gateway's base URL, with no ``/v1`` suffix; pydantic-ai's
+  ``openai`` provider appends the API path itself.
+- ``password`` -- the bearer token the gateway expects.
+- ``extra["model"]`` -- ``openai:<route>``, where ``<route>`` is the route
+  name the gateway exposes for the model you want.
+
+Only ``api_key`` and ``base_url`` reach the provider class -- there is no
+extension point for custom headers. A gateway that authenticates with a
+bearer token works through this connection shape; one that requires
+additional custom headers does not.
+
+You can validate this pattern locally with any OpenAI-compatible proxy that
+speaks bearer-token auth -- for example, LiteLLM's proxy mode running in
+front of a model server. That's a different role than the ``litellm:`` model
+prefix covered in the reference section below: here LiteLLM stands in for
+the gateway itself, reached through the generic ``openai:<route>`` prefix,
+not addressed via its own model-prefix.
+
+Reference
+----------
+
+The rest of this page is reference material for the connection shapes
+above.
+
+.. _self_hosted_models_model_id:
+
+Model identifier format
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``extra`` JSON's ``model`` value keeps the ``provider:model`` format the
+hook always expects, but which ``provider`` prefix to use depends on the
+endpoint (verified against ``pydantic-ai-slim`` 2.5.0, the version this
+guide was developed against, and 2.10.0, the latest at the time of
+writing):
+
+- **vLLM** has no dedicated provider class in pydantic-ai -- ``openai:<model>``
+  is the only option. pydantic-ai's ``openai`` provider class talks to
+  whatever ``host`` points at, so it works against vLLM's OpenAI-compatible
+  API even though the weights being served aren't OpenAI's.
+- **Ollama** has two valid options: the dedicated ``ollama:<model>`` prefix
+  (recommended) applies Ollama-specific model-profile tuning based on the
+  model name (``llama``, ``gemma``, ``qwen``, ``deepseek``, ``mistral``,
+  ``command``, and ``gpt-oss`` prefixes get tuned defaults); the generic
+  ``openai:<model>`` prefix also works, since Ollama exposes an
+  OpenAI-compatible API too, but skips that tuning.
+
+Both prefixes need the same ``openai`` extra
+(``pip install "apache-airflow-providers-common-ai[openai]"``) -- pydantic-ai's
+Ollama provider is built on the same underlying ``openai`` Python package as
+its OpenAI provider, and there's no separate ``ollama`` extra.
+
+.. note::
+
+   ``LiteLLMProvider`` is an exception: its constructor takes ``api_base``
+   instead of ``base_url``, so passing ``host`` raises a ``TypeError`` that
+   the hook catches and silently falls back to environment-variable auth,
+   ignoring ``host`` entirely. Point a LiteLLM proxy's OpenAI-compatible
+   endpoint via ``openai:<model>`` instead -- this is the same proxy role
+   used in the gateway section above, just addressed through the working
+   prefix rather than the rejected ``litellm:`` one.
+
+.. _self_hosted_models_other_servers:
+
+Other OpenAI-compatible servers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The same ``openai:<model>`` plus ``host`` setup works against any other
+server that exposes an OpenAI-compatible API -- for example
+`LM Studio <https://lmstudio.ai/>`__,
+`llama.cpp <https://github.com/ggml-org/llama.cpp>`__'s ``llama-server``, or
+`LocalAI <https://localai.io/>`__. Point ``host`` at that server's
+OpenAI-compatible endpoint the same way as the vLLM example above.
+
+Cross-hook naming differences
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:class:`~airflow.providers.common.ai.hooks.langchain.LangChainHook` and
+:class:`~airflow.providers.common.ai.hooks.llamaindex.LlamaIndexHook` also
+support self-hosted endpoints through the same ``host`` connection field, but
+the underlying constructor keyword each hook passes it to is not uniform:
+
+.. list-table::
+   :header-rows: 1
+
+   * - Hook
+     - Connection field
+     - Constructor keyword
+   * - ``PydanticAIHook``
+     - ``host``
+     - ``base_url``
+   * - ``LangChainHook``
+     - ``host``
+     - ``base_url``
+   * - ``LlamaIndexHook``
+     - ``host``
+     - ``api_base``
+
+Where to go next
+-------------------
+
+- :ref:`howto/connection:pydanticai` -- the full connection field reference.
+- :doc:`retry_policies` -- the "Local LLM support" section covers pointing
+  ``LLMRetryPolicy`` at a self-hosted endpoint.
+- :doc:`examples` -- more runnable Dags against the ``pydanticai``
+  connection type; point one at any of the connections on this page.
+- pydantic-ai isn't limited to Ollama and vLLM -- any OpenAI-compatible
+  endpoint works the same way. See the
+  `pydantic-ai install docs <https://ai.pydantic.dev/install/#slim-install>`__
+  for the full list of providers it supports.
+- `pydantic-ai's OpenAI-compatible models docs
+  
<https://pydantic.dev/docs/ai/models/overview/#openai-compatible-providers>`__
+  -- background on the ``openai`` provider class this guide's
+  ``openai:<model>`` examples use.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
index 79a0c5fe333..16bca0700eb 100644
--- 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_agent.py
@@ -110,6 +110,42 @@ def example_agent_operator_hook():
 example_agent_operator_hook()
 
 
+# ---------------------------------------------------------------------------
+# 2b. Hook-based tools against a GET-only endpoint (self-hosted models 
tutorial)
+# ---------------------------------------------------------------------------
+
+
+# [START howto_agent_self_hosted]
+@dag(tags=["example"])
+def example_agent_self_hosted():
+    from airflow.providers.http.hooks.http import HttpHook
+
+    # The OpenAI-compatible model list (GET /v1/models) is GET-only; HttpHook 
defaults to POST.
+    http_hook = HttpHook(http_conn_id="my_api", method="GET")
+
+    AgentOperator(
+        task_id="list_models",
+        prompt="Which models are available?",
+        llm_conn_id="pydanticai_default",
+        system_prompt=(
+            "You are an API assistant. Use the tools to answer questions; "
+            "the server's model list is served at GET /v1/models."
+        ),
+        toolsets=[
+            HookToolset(
+                http_hook,
+                allowed_methods=["run"],
+                tool_name_prefix="http_",
+            )
+        ],
+    )
+
+
+# [END howto_agent_self_hosted]
+
+example_agent_self_hosted()
+
+
 # ---------------------------------------------------------------------------
 # 3. @task.agent decorator with dynamic prompt
 # ---------------------------------------------------------------------------

Reply via email to