Lee-W commented on code in PR #72156:
URL: https://github.com/apache/airflow/pull/72156#discussion_r4045854984


##########
providers/common/ai/docs/provider_fallback.rst:
##########
@@ -0,0 +1,208 @@
+ .. 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.
+
+Provider fallback
+=================
+
+A single ``llm_conn_id`` gives a task one provider. When that provider is 
down, the task
+fails and retries into the same outage. ``fallback_conn_ids`` gives the 
connection an
+ordered list of other connections to try, so a provider outage moves to the 
next vendor
+inside the same task attempt.
+
+Configure it on the connection
+------------------------------
+
+Put the chain in the primary connection's extra:
+
+.. code-block:: json
+
+    {
+      "model": "openai:gpt-5",
+      "fallback_conn_ids": ["anthropic_prod", "bedrock_dr"]
+    }
+
+Every entry is an Airflow connection ID, resolved through the hook registered 
for its own
+connection type. A chain can therefore mix vendors whose credentials live in 
different
+connection fields — ``pydanticai`` for OpenAI, ``pydanticai_bedrock`` for a 
Bedrock
+standby — without the Dag knowing anything about either.
+
+That is the point of configuring it here rather than in Dag code: the Dag 
keeps naming one
+connection, and whoever administers the connections owns the failover 
topology. Changing a
+standby provider is a connection edit, not a Dag deployment.
+
+A *bare* model name (e.g. ``"gpt-5"`` rather than ``"openai:gpt-5"``) is 
forwarded down
+the chain as a logical model name: each connection that has no ``model`` of 
its own
+resolves that name against its own platform, so one bare name can reach a 
primary and
+every fallback without repeating it per connection. It does not matter where 
the primary's
+name comes from -- the ``Model`` field on its connection and a ``model_id`` on 
the operator
+or hook are forwarded alike. A fallback with its own ``model`` in
+extra always uses that instead -- this is how a fallback pins a spelling the 
forwarded
+name would not produce, such as Bedrock's region-prefixed ``us.anthropic.`` 
model ids. A
+name that already pins a platform (its segment before the first ``:`` is 
itself a
+recognized provider, e.g. ``"openai:gpt-5"``) is *not* forwarded; a fallback 
with no
+``model`` of its own still raises "no model specified" rather than trying a 
prefixed name
+meant for a different provider. See :doc:`connections/pydantic_ai_azure`,
+:doc:`connections/pydantic_ai_bedrock` and 
:doc:`connections/pydantic_ai_vertex` for how
+each vendor connection resolves a bare name.
+
+Configure it on the operator
+-----------------------------
+
+``fallback_conn_ids`` is also a parameter on
+:class:`~airflow.providers.common.ai.operators.llm.LLMOperator`,
+:class:`~airflow.providers.common.ai.operators.agent.AgentOperator`, their 
subclasses,
+and the matching ``@task.llm`` / ``@task.agent`` decorators -- mirroring 
``model_id``,
+which is settable at the same two layers:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_llm_fallback.py
+    :language: python
+    :dedent: 0
+    :start-after: [START howto_llm_fallback_operator_argument]
+    :end-before: [END howto_llm_fallback_operator_argument]
+
+The operator argument overrides the connection's extra field, and passing 
``[]``
+explicitly disables a chain configured there -- ``None`` (the default) reads 
whatever
+the connection says. Use this when a task should own its own failover order 
instead of
+inheriting it from however the connection is configured.
+
+Configure it in code
+--------------------
+
+:class:`~airflow.providers.common.ai.hooks.pydantic_ai.PydanticAIHook` also 
takes the list
+directly, which is what a task that constructs the hook itself (rather than 
through an
+operator) should use:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_llm_fallback.py
+    :language: python
+    :dedent: 0
+    :start-after: [START howto_llm_fallback_hook_argument]
+    :end-before: [END howto_llm_fallback_hook_argument]
+
+The argument wins over the connection's extra, and passing ``[]`` explicitly 
disables a
+chain configured there. Omitting it entirely (``None``) means "use whatever 
the connection
+says", which is why the two are not interchangeable.
+
+Where this sits among the retry layers
+--------------------------------------
+
+Three mechanisms handle failure at different time scales, and they compose 
rather than
+replace each other:
+
+.. list-table::
+   :header-rows: 1
+   :widths: 25 40 35
+
+   * - Scope
+     - Mechanism
+     - Handles
+   * - Within one model call
+     - ``fallback_conn_ids``
+     - This vendor's API is returning errors; ask the next one (any 
``ModelAPIError``,
+       transient or not)
+   * - Within one task attempt
+     - ``timeout`` in pydantic-ai's ``ModelSettings``
+     - This vendor is slow rather than down
+   * - Across task attempts
+     - :doc:`retry_policies` (including ``LLMRetryPolicy``)
+     - Whether this failure is worth retrying at all
+
+A chain does not remove the need for the outer layers. It covers the case 
where another
+vendor can answer the same prompt now; a bad prompt, an exhausted quota on 
every vendor, or
+a permanent data error still has to be decided by the retry policy.
+
+Adding a chain changes what the retry layer sees. When every connection in the 
chain
+fails, the exception the task raises is 
``pydantic_ai.exceptions.FallbackExceptionGroup``,
+not the last provider's own exception, so retry rules matched against a 
provider-specific
+exception type stop matching. Before adding a chain to a connection that Dags 
already use,
+read :doc:`retry_policies` -- the section "When the connection also carries a 
fallback
+chain" spells out what to check.
+
+Costs to know before configuring a long chain
+---------------------------------------------
+
+**The timeout multiplies.** pydantic-ai applies a ``ModelSettings`` timeout to 
each model
+in the chain, not to the chain as a whole. A 30-second timeout across three 
connections is
+a 90-second worst case for one call.
+
+**There is no circuit breaker.** Every call tries the primary first. During an 
outage each
+task instance pays the primary's timeout again before failing over, so 500 
mapped tasks pay
+it 500 times. Keeping the primary's timeout short bounds both of these.
+
+**Chains are not resolved recursively.** If a connection listed as a fallback 
declares its
+own ``fallback_conn_ids``, resolution fails with an error rather than 
following it. List
+every provider directly on the primary; a flat chain is the one you can read 
off a single
+connection.
+
+**Non-transient errors still walk the whole chain.** Failover triggers on 
pydantic-ai's
+``ModelAPIError`` family, which includes ``ModelHTTPError`` -- raised for any 
4xx as well as
+5xx. A malformed prompt, an expired key, or a misspelled model name is 
therefore retried
+against every connection in the chain before the task sees the failure: N 
requests, N
+timeouts, and N billable calls for a request that was never going to succeed. 
Keep chains
+short, and put deterministic rules for those errors in :doc:`retry_policies`.
+
+**Airflow's task-level** ``retries`` **multiplies on top of the chain.** A 
task with
+``retries=5`` gets up to six attempts -- the initial attempt plus five retries 
-- before
+Airflow marks it failed, and each attempt walks the whole chain again if every 
connection
+is still down. Against the three-connection chain in the example above (the 
primary plus
+two fallbacks), that is up to 18 upstream calls, not 3, before the task is 
finally marked
+failed.
+
+**A bad fallback connection fails the whole chain, including a healthy 
primary.** The
+primary and every fallback are resolved eagerly, before any of them is called, 
so a
+misspelled fallback ``conn_id`` or a fallback connection missing its ``model`` 
raises
+immediately -- the task never reaches the primary, even though the primary 
itself would
+have answered fine. Run ``test_connection`` on the primary to catch this 
before it costs a
+task; see *Verifying a chain* below.
+
+Verifying a chain
+-----------------
+
+Two checks, neither of which requires waiting for a real outage:
+
+*Test the connection.* ``test_connection`` on the primary resolves every 
connection in the
+chain, so a fallback with a missing ``model`` or an unknown connection ID is 
reported by name
+there rather than discovered mid-incident. Credential fields a provider class 
rejects with a
+``TypeError`` are not reported this way -- the hook catches that and retries 
with the
+env-var-based provider constructor, logging a warning either way; that retry 
still raises a
+``pydantic_ai.exceptions.UserError`` if the required env var is also missing, 
so check the
+logs for that failure mode rather than relying on ``test_connection``. It also 
does not call the provider, so a

Review Comment:
   Both now describe the case that really is invisible — a provider class 
rejecting the connection's credentials with a `TypeError` is retried against 
the environment variables, so the test passes while those credentials are 
ignored.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to