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


##########
providers/anthropic/docs/operators/anthropic.rst:
##########
@@ -175,15 +177,34 @@ model requests once its tracked list cost reaches the 
ceiling:
         agent_id="agt_...",
         environment_id="env_...",
         message="Summarise yesterday's incidents.",
-        session_kwargs={
-            "budget": {
-                "type": "limit",
-                # Minor units as an integer string: "2500" is $25.00.
-                "max_list_cost": {"amount": "2500", "currency": "USD"},
-            }
-        },
+        budget=25.00,  # $25.00

Review Comment:
   While $25,00 means USD, I feel something like `USD$ 25.00` might make this 
comment actually helpful. 



##########
providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py:
##########
@@ -123,6 +126,49 @@ def is_terminal(cls, status: str) -> bool:
 #: ``session.status_idle`` stop reason emitted when a session stops against 
its budget.
 BUDGET_REACHED = "budget_reached"
 
+#: What a caller may pass as a session budget: an amount in USD, or the raw 
API payload.

Review Comment:
   Just wnat ot make sure we want to use `#:` syntax (I know there's already 
one above). But this is not that frequently used in airflow yet.



##########
providers/anthropic/tests/unit/anthropic/operators/test_agent.py:
##########
@@ -199,6 +199,67 @@ def test_deferrable_defers_with_trigger(self, 
mock_hook_prop):
         hook.wait_for_session.assert_not_called()
 
 
+class TestBudgetParam:
+    @staticmethod
+    def _op(**kwargs):

Review Comment:
   ```suggestion
       def _make_op(**kwargs):
   ```



##########
providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py:
##########
@@ -123,6 +126,49 @@ def is_terminal(cls, status: str) -> bool:
 #: ``session.status_idle`` stop reason emitted when a session stops against 
its budget.
 BUDGET_REACHED = "budget_reached"
 
+#: What a caller may pass as a session budget: an amount in USD, or the raw 
API payload.
+BudgetSpec = str | int | float | Decimal | Mapping[str, Any]
+
+
+def build_budget(budget: BudgetSpec) -> dict[str, Any]:

Review Comment:
   Should we make it return a TypedDict instead?



##########
providers/anthropic/docs/operators/anthropic.rst:
##########
@@ -147,9 +147,12 @@ Parameters
 * ``poll_interval`` — seconds between session status checks.
 * ``timeout`` — seconds to wait for a terminal status; defaults to 24 hours.
 * ``vault_ids`` — vault IDs providing MCP/credential access to the session.
+* ``budget`` -- spend ceiling for the session, in US dollars (``25.00``) or as 
the raw API
+  payload (a mapping). Templated. See `Session budgets`_ below.

Review Comment:
   **Templated** here is correct in Airflow context, but one might be surprised 
seeing only this word in the middle of other descriptions. might be better to 
extend the description of templated a bit
   
   
   



##########
providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py:
##########
@@ -123,6 +126,49 @@ def is_terminal(cls, status: str) -> bool:
 #: ``session.status_idle`` stop reason emitted when a session stops against 
its budget.
 BUDGET_REACHED = "budget_reached"
 
+#: What a caller may pass as a session budget: an amount in USD, or the raw 
API payload.
+BudgetSpec = str | int | float | Decimal | Mapping[str, Any]
+
+
+def build_budget(budget: BudgetSpec) -> dict[str, Any]:
+    """
+    Normalize a session budget into the API's ``max_list_cost`` payload.
+
+    A scalar is read as **US dollars** (``25``, ``25.0`` and ``"25.00"`` all 
mean $25.00).
+    The API wants minor units as an integer decimal string, so the conversion 
runs through
+    :class:`~decimal.Decimal` (never binary float) and rejects an amount finer 
than a cent
+    rather than silently rounding money. A mapping is deep-copied and 
otherwise returned
+    unchanged, so a raw payload the provider has not caught up with stays 
usable without a
+    provider release.
+
+    .. warning::
+        The ceiling is a stop trigger, not a cap: it is checked between model 
requests, so
+        a request already in flight can carry the session well past it.
+    """
+    if isinstance(budget, Mapping):
+        # Deep, not ``dict()``: a shallow copy leaves the nested 
``max_list_cost`` aliased
+        # to the caller's object, so a later edit of the returned payload 
would reach back
+        # into a templated operator field.
+        return deepcopy(dict(budget))
+    if isinstance(budget, bool):
+        raise ValueError(f"Invalid budget {budget!r}: expected an amount in 
USD or a mapping.")
+    try:
+        dollars = Decimal(str(budget))
+    except (InvalidOperation, ValueError) as e:
+        raise ValueError(f"Invalid budget {budget!r}: not a decimal amount in 
USD.") from e

Review Comment:
   ```suggestion
       try:
           dollars = Decimal(str(budget))
       except (InvalidOperation, ValueError) as e:
           raise ValueError(f"Invalid budget {budget!r}: not a decimal amount 
in USD.") from e
   ```
   
   Should we just go with this? "True" is not a decimal amount either



-- 
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