Skip to content

feat(ptu): fetch azure_billing flat cost via Cost Management API (LIT-4077 POC) - #34048

Closed
yucheng-berri wants to merge 4 commits into
litellm_lit1697_stage5_rolloutfrom
litellm_lit4077_stage6_azure_billing_poc
Closed

feat(ptu): fetch azure_billing flat cost via Cost Management API (LIT-4077 POC)#34048
yucheng-berri wants to merge 4 commits into
litellm_lit1697_stage5_rolloutfrom
litellm_lit4077_stage6_azure_billing_poc

Conversation

@yucheng-berri

Copy link
Copy Markdown
Contributor

Relevant issues

Part of the LIT-1697 Azure PTU cost attribution stack; stacked on #33439 (stage 5)

Linear ticket

Resolves LIT-4077

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Opened as a draft POC to align on the customer-facing story before a real review pass. Full merge review follows after v1 (stages 1-5) ships and the customer walkthrough lands

Delays in PR merge?

Not applicable; POC, not requesting review yet

Screenshots / Proof of Fix

Live end-to-end against a real Azure subscription. Both flags enabled at runtime, a reservation with cost_source=azure_billing created via API, the backfill CLI acquires a real Entra ID token, calls Cost Management, and writes the response

Backfill log:

2026-07-20 16:29:53 INFO azure.identity._internal.get_token_mixin: ClientSecretCredential.get_token_info succeeded
2026-07-20 16:29:55 INFO LiteLLM Proxy: PTU rollup: azure_billing reservation=fd26ded5-... day=2026-07-20 resource=/subscriptions/<redacted>/.../deployments/gpt-4-ptu returned $0.0000
[2026-07-20] reservations=2 rows_written=1

The $0.00 is a real Azure response for a resource id with no billing history in the dev sub; a customer's own PTU deployment returns its accrued cost on the same code path. Currency is USD, verified against the sub via the Cost Management response

Validator rejection when azure_resource_id is missing:

$ curl -sS -X POST http://localhost:4097/ptu_reservation/new \
    -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
    -d '{"team_id":"...","model":"...","cost_source":"azure_billing","effective_from":"2026-07-01T00:00:00Z"}'
HTTP 400
{"detail":{"error":"azure_resource_id is required when cost_source='azure_billing'"}}

Restart persistence (both flags survive _sync_ui_settings_to_general_settings):

INFO proxy_server: PTU reservation rollup job scheduled at 00:15 UTC daily (no-ops until enable_ptu_cost_attribution is set)
INFO proxy_server: Synced UI settings to general_settings on startup: ['enable_ptu_cost_attribution', 'enable_azure_ptu_billing_pull']

Type

New Feature

Changes

A thin async client at litellm/integrations/azure_cost_management/ calls one Cost Management endpoint the rollup needs. Entra ID auth is inherited from get_azure_ad_token_from_entra_id so no new dependency lands. The client is deliberately narrow (one public method) and follows the litellm/integrations/ pattern (see datadog/, langfuse/, etc.)

A new UI settings flag enable_azure_ptu_billing_pull composes below enable_ptu_cost_attribution and reuses the stage 5 machinery (UISettings model, ALLOWED_UI_SETTINGS_FIELDS, _RUNTIME_GENERAL_SETTINGS_FLAGS). Persistence and startup sync are inherited without touching those code paths

_compute_daily_flat_cost in the rollup branches on cost_source: manual keeps the existing (ptu_count * cost_per_ptu) / days-in-month formula, azure_billing calls the injected fetcher, unknown values return 0 with a warning. The function is now async since it awaits the client, and existing sync tests were converted accordingly

The scheduler wraps the rollup in a small callable that builds a fetcher just-in-time via _build_azure_cost_fetcher_if_enabled so runtime flag flips take effect without a proxy restart. The helper reads general_settings.enable_azure_ptu_billing_pull, the configured subscription_id, and env vars for creds; on any missing input it returns None and the rollup no-ops for azure_billing rows

The CRUD gate is relaxed: the earlier blanket reject on cost_source=azure_billing becomes a check that azure_resource_id is present. The pydantic domain validator from stage 1 already enforces the rest (ptu_count/cost_per_ptu must be null, azure_resource_id required)

The backfill CLI grows a matching helper (_build_backfill_azure_fetcher) so operators running scripts/ptu_reservation_backfill.py exercise the same code path as the scheduler when AZURE_SUBSCRIPTION_ID and Entra ID env creds are set

Deferred to follow-up tickets: retry/backoff on 429/503, sliding-window reconciliation for Azure's 24-72h reporting lag, UI toggle for cost_source on the reservation create form, non-USD currency handling, Prometheus counter for fetch failures

Behavior changes

cost_source=azure_billing on POST /ptu_reservation/new is now accepted (previously rejected with "not supported in this release") when azure_resource_id is present

For reservations with cost_source=azure_billing that reach the daily rollup: with enable_azure_ptu_billing_pull on and Entra ID env creds resolvable, flat_cost is fetched from Azure; with the flag off or creds missing, the reservation is skipped with a warning and no sentinel row is written

No change to manual reservations or to the read path (/team/daily/activity, Usage page)

QA runbook

Prereqs: dev proxy on 4097 with STORE_MODEL_IN_DB=True; env vars AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID from a service principal with Cost Management Reader on the target subscription; config.yaml has general_settings.azure_ptu_billing.subscription_id: os.environ/AZURE_SUBSCRIPTION_ID

  1. curl -X PATCH http://localhost:4097/update/ui_settings -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' -d '{"enable_ptu_cost_attribution": true, "enable_azure_ptu_billing_pull": true}'
  2. curl -X POST http://localhost:4097/ptu_reservation/new with a body that sets cost_source=azure_billing and a real Azure azure_resource_id
  3. .venv/bin/python scripts/ptu_reservation_backfill.py --date $(date -u +%F); watch for the log line PTU rollup: azure_billing reservation=... returned $...
  4. curl 'http://localhost:4097/team/daily/activity?team_ids=<team>&start_date=<today>&end_date=<today>'; flat_cost is populated when the Azure resource has billing history
  5. Restart the proxy; startup log shows Synced UI settings to general_settings on startup: ['enable_ptu_cost_attribution', 'enable_azure_ptu_billing_pull']
  6. Toggle either flag off via /update/ui_settings and re-run backfill; azure_billing reservation is skipped with a warning, manual reservations continue to accrue

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.92308% with 33 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/proxy_server.py 16.66% 25 Missing ⚠️
...re_cost_management/azure_cost_management_client.py 90.36% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yucheng-berri
yucheng-berri marked this pull request as ready for review July 21, 2026 00:15
@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please review

devin-ai-integration[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Azure Cost Management API integration for PTU reservation cost attribution (LIT-4077). A thin async client (litellm/integrations/azure_cost_management/) fetches daily billed cost from Azure, wired into the existing rollup scheduler via a just-in-time fetcher builder that reads runtime config so flag changes take effect without a proxy restart.

  • _compute_daily_flat_cost is now async and branches on cost_source: manual reservations keep the existing prorated formula; azure_billing reservations call the injected fetcher with a non-USD guard that skips writes and logs a warning instead of persisting wrong-currency amounts.
  • The CRUD gate is relaxed from a blanket reject on cost_source=azure_billing to requiring azure_resource_id, matching the Pydantic domain validator; a demo stub (_DemoAzureCostFetcher) lets the Usage page show plausible figures during customer demos without a real PTU deployment.
  • The enable_azure_ptu_billing_pull UI flag previously described in the PR was removed; presence of azure_ptu_billing.subscription_id and Entra ID env vars is now the sole opt-in signal.

Confidence Score: 4/5

Safe to merge for public-cloud deployments; sovereign cloud operators will see 401s from Azure until the token scope is fixed.

The new client's Entra ID token scope is hardcoded to https://management.azure.com/.default regardless of management_base_url. The config deliberately supports sovereign cloud overrides (tested in test_config_management_base_url_reads_env_override), but a token issued for the public cloud audience will be rejected by sovereign cloud endpoints, breaking every rollup run for those operators. All other logic — currency guard, error handling, idempotent upsert, demo stub, backfill CLI — looks correct.

Files Needing Attention: litellm/integrations/azure_cost_management/azure_cost_management_client.py — specifically _default_token_provider_factory where the scope needs to be derived from config.management_base_url.

Important Files Changed

Filename Overview
litellm/integrations/azure_cost_management/azure_cost_management_client.py New async client for the Azure Cost Management API. Correctly wraps HTTP errors and network errors, protects client_secret from repr, and handles the zero-rows case — but the Entra ID token scope is hardcoded to the public cloud audience, breaking sovereign cloud deployments when management_base_url is overridden.
litellm/proxy/spend_tracking/ptu_reservation_rollup.py Extends rollup to support azure_billing reservations via injected AzureCostFetcher. Non-USD currency guard is correct; Protocol definition omits last_currency, which could silently suppress the guard for future custom implementations.
litellm/proxy/proxy_server.py Adds _DemoAzureCostFetcher stub and _build_azure_cost_fetcher_if_enabled helper; scheduler now wraps rollup in a thin async callable that builds the fetcher just-in-time. Clean and well-guarded.
litellm/proxy/management_endpoints/ptu_reservation_endpoints.py Relaxes the blanket azure_billing rejection to require azure_resource_id instead, matching the Pydantic domain validator. Change is minimal and correct.
scripts/ptu_reservation_backfill.py Backfill CLI now reads AZURE_SUBSCRIPTION_ID and builds an Azure fetcher matching the scheduler's code path. Logging setup added at entry point. Clean.
tests/test_litellm/integrations/azure_cost_management/test_azure_cost_management_client.py Good unit test coverage for the new client: HTTP errors, network errors, zero-row response, multi-row sum, repr safety, and sovereign cloud URL override. All tests use mocks with no real network calls.
tests/test_litellm/proxy/spend_tracking/test_ptu_reservation_rollup.py Existing tests correctly converted to async; new tests cover azure_billing fetch, non-USD skip, fetcher error, and the end-to-end rollup write path. Coverage is thorough.
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py Description field updated to document the azure_billing auto-pull; enable_azure_ptu_billing_pull was intentionally removed and regression-guarded by a new test.
litellm/types/llms/custom_http.py Adds AzureCostManagement enum value to httpxSpecialProvider so the client gets its own HTTP handler pool. Straightforward addition.

Reviews (5): Last reviewed commit: "feat(ptu): sovereign-cloud base URL + de..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This POC adds an AzureCostManagementClient under litellm/integrations/azure_cost_management/ and wires it into the PTU reservation rollup so reservations with cost_source=azure_billing fetch their daily flat cost from the Azure Cost Management REST API instead of the manual ptu_count × cost_per_ptu formula.

  • New enable_azure_ptu_billing_pull UI flag composes below enable_ptu_cost_attribution; the scheduler builds the fetcher just-in-time so runtime toggles take effect without a restart.
  • The CRUD gate on POST /ptu_reservation/new is relaxed: cost_source=azure_billing is now accepted when azure_resource_id is present.
  • _compute_daily_flat_cost is now async and branches on cost_source; all existing manual tests were correctly converted to async and new azure_billing coverage was added.

Confidence Score: 3/5

The Azure billing fetch path silently writes non-USD costs to ptu_flat_cost without conversion; any customer on a non-USD Azure subscription will see incorrect spend data from the first rollup run.

The non-USD currency issue is a present silent data-corruption path: no error, no rejection, just wrong amounts in the DB for non-USD subscriptions. Combined with the unreachable exception branch and the incomplete error wrapping in the new client, the integration layer needs hardening before merge.

litellm/integrations/azure_cost_management/azure_cost_management_client.py and litellm/proxy/spend_tracking/ptu_reservation_rollup.py — the currency-handling gap spans both files.

Important Files Changed

Filename Overview
litellm/integrations/azure_cost_management/azure_cost_management_client.py New thin Azure Cost Management API client; non-USD currency amounts are returned as-is without conversion or skip, and the exception-handling contract has a gap for non-HTTP network errors.
litellm/proxy/spend_tracking/ptu_reservation_rollup.py Rollup now async-branches on cost_source; azure_billing path fetches from Azure but writes non-USD amounts to ptu_flat_cost without any currency guard or skip.
litellm/proxy/proxy_server.py Adds _build_azure_cost_fetcher_if_enabled helper and wraps PTU rollup in a closure; AzureCostManagementError imported but never used inside the function.
litellm/proxy/management_endpoints/ptu_reservation_endpoints.py CRUD gate relaxed correctly: azure_billing now allowed when azure_resource_id is provided; error message updated to reflect new requirement.
litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py New enable_azure_ptu_billing_pull flag added to UISettings, ALLOWED_UI_SETTINGS_FIELDS, and _RUNTIME_GENERAL_SETTINGS_FLAGS; follows the established pattern correctly.
scripts/ptu_reservation_backfill.py Backfill CLI gains _build_backfill_azure_fetcher and passes it to the rollup; mirrors the proxy path correctly.
tests/test_litellm/integrations/azure_cost_management/test_azure_cost_management_client.py New mock-only tests for the Azure client covering normal, zero-rows, non-USD, HTTP error, and request shape cases; no real network calls.
tests/test_litellm/proxy/spend_tracking/test_ptu_reservation_rollup.py Existing sync tests correctly converted to async, new azure_billing coverage added; test for non-USD rollup behaviour is absent despite the silent write risk.
litellm/proxy/dev_config.yaml Dev config updated with azure_ptu_billing.subscription_id env var reference; follows existing litellm os.environ/ convention.

Reviews (2): Last reviewed commit: "feat(ptu): fetch azure_billing flat cost..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review — all 5 findings from the prior pass are addressed in ac86a11 (currency guard, repr redaction, network error wrap, unused import, unreachable branch). Individual replies posted on each thread with the commit hash. CI also picks up an async-clients check fix and an obsolete stage-1 test update.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re-review please. c6b5c3b collapses the two PTU flags into one: enable_azure_ptu_billing_pull was redundant with the sub-id + Entra ID cred check that _build_azure_cost_fetcher_if_enabled already does. Now a single enable_ptu_cost_attribution toggle in UI Settings, and presence of general_settings.azure_ptu_billing.subscription_id + AZURE_* env vars is the sole signal for the auto-pull. Manual reservations still work when Azure creds are absent (unchanged). New regression test locks in that the second flag isn't reintroduced.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +149 to +150
except httpx.RequestError as exc:
raise AzureCostManagementError(f"Azure Cost Management network error: {exc}") from exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Azure cost lookups that time out are not converted to the client's documented error type

A timed-out Azure cost lookup is not wrapped into the client's own error (the except httpx.RequestError at litellm/integrations/azure_cost_management/azure_cost_management_client.py:149) because the shared HTTP layer already converts timeouts into a different, unrelated error type before this code sees them, so the promised error type never surfaces for timeouts.
Impact: A timeout during an Azure cost fetch raises an unexpected error type instead of the documented one, and the test that claims to cover this scenario passes without exercising real behavior.

How the HTTP handler pre-empts the client's except clauses

AzureCostManagementClient.get_daily_cost calls self._http.post(...) where self._http is an AsyncHTTPHandler (litellm/integrations/azure_cost_management/azure_cost_management_client.py:92). Inside AsyncHTTPHandler.post (litellm/llms/custom_httpx/http_handler.py), httpx.TimeoutException is caught and re-raised as litellm.Timeout, which is NOT a subclass of httpx.RequestError. Therefore the except httpx.RequestError branch at line 149 never runs for timeouts; litellm.Timeout propagates instead of AzureCostManagementError, contradicting the docstring at lines 103-104 ("Raises AzureCostManagementError on non-2xx responses or ... network failure").

The unit test test_get_daily_cost_wraps_network_errors mocks the handler to raise httpx.ReadTimeout directly, so it never sees the real handler's conversion and gives false confidence. In practice the impact is limited because the rollup wraps the call in a broad except Exception (litellm/proxy/spend_tracking/ptu_reservation_rollup.py:68), so a bad fetch still degrades to 0.0.

Note: HTTP status errors ARE handled correctly, since AsyncHTTPHandler raises MaskedHTTPStatusError, a subclass of httpx.HTTPStatusError.

Prompt for agents
In litellm/integrations/azure_cost_management/azure_cost_management_client.py, get_daily_cost posts via AsyncHTTPHandler.post, which (see litellm/llms/custom_httpx/http_handler.py) converts httpx.TimeoutException into litellm.Timeout before it can reach the except httpx.RequestError branch. As a result, timeouts are not wrapped into AzureCostManagementError as the docstring promises, and test_get_daily_cost_wraps_network_errors passes only because it mocks the handler to raise httpx.ReadTimeout directly (bypassing the real conversion). Consider either catching litellm.Timeout (and other litellm exception types the handler may raise) in get_daily_cost and wrapping them in AzureCostManagementError, or updating the test to exercise the real handler behavior so it would catch this gap. Confirm the exact exception types AsyncHTTPHandler.post can raise before deciding what to catch.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Stage 6 POC on top of the LIT-1697 stack. Adds an azure_billing branch to
the daily rollup so a reservation with cost_source=azure_billing pulls
accrued cost from the Azure Cost Management API instead of computing the
manual (ptu_count * cost_per_ptu) / days-in-month formula.

The client is a thin async wrapper around one endpoint the rollup needs;
Entra ID auth is inherited from get_azure_ad_token_from_entra_id (no new
dependency). Deliberately narrow: no retry, no reconciliation for Azure's
24-72h reporting lag, no UI toggle for cost_source. Those are follow-ups.

Gated behind a second UI settings flag enable_azure_ptu_billing_pull that
composes below enable_ptu_cost_attribution. The scheduler builds a fetcher
just-in-time so runtime flag flips take effect without a proxy restart;
manual reservations keep working when either flag is off.

Behavior changes: azure_billing cost_source is now accepted by
/ptu_reservation/new when azure_resource_id is present. The rollup uses
the injected fetcher for those; when no fetcher is available (flag off or
env creds missing) the reservation is skipped with a warning and no
sentinel row is written.
Currency guard (P1): the rollup now inspects azure_fetcher.last_currency
after each call and skips writes when the response reports a non-USD
currency, so a customer on a non-USD Azure subscription cannot land a
raw foreign-currency amount in ptu_flat_cost as if it were USD. The
warning names the reservation, day, and currency so operators can spot
the skip and file a follow-up for currency conversion.

Client hardening (P2):
- client_secret is field(repr=False) so repr(config) no longer exposes it
- httpx.RequestError family (ConnectError, ReadTimeout, RemoteProtocolError)
  is wrapped into AzureCostManagementError so the client honors its
  documented interface for every failure path, not just HTTPStatusError
- Removed the unreachable AttributeError branch in _parse_cost_and_currency
- Removed the unused AzureCostManagementError import in proxy_server.py

CI (ensure_async_clients_test): the client no longer instantiates
AsyncHTTPHandler directly; it goes through get_async_httpx_client with a
new httpxSpecialProvider.AzureCostManagement enum value so it shares the
same cached connection pool contract as the rest of the integrations.

Behavior changes: azure_billing reservations that receive a non-USD Cost
Management response are now skipped with a warning instead of writing
the raw amount; POST /ptu_reservation/new with cost_source=azure_billing
and no azure_resource_id continues to return 400 (stage 1 test updated
to assert the new error message rather than the old blanket rejection).
…_cost_attribution

Two flags for the same feature confused reviewers with no benefit. Presence of
general_settings.azure_ptu_billing.subscription_id together with the Entra ID env
vars is now the sole signal that an operator wants the automated Cost Management
pull; if either is missing, azure_billing reservations no-op with a warning and
manual reservations continue to accrue via the formula.

Removes the enable_azure_ptu_billing_pull field from UISettings, its ALLOWED and
_RUNTIME_GENERAL_SETTINGS_FLAGS entries, and the corresponding gate inside
_build_azure_cost_fetcher_if_enabled. Extends the existing runtime-flag test with
a regression assertion that the removed flag is not reintroduced.

Behavior changes: the UI Settings panel now shows one PTU toggle instead of two.
An operator who previously had the outer flag on but the pull flag off will,
after this change, see azure_billing reservations start pulling from Azure as
soon as their creds and subscription_id are configured; the fallback path when
either is missing is unchanged.
@yucheng-berri
yucheng-berri force-pushed the litellm_lit1697_stage5_rollout branch from 0ffa82f to 1b85426 Compare July 21, 2026 18:53
@yucheng-berri
yucheng-berri force-pushed the litellm_lit4077_stage6_azure_billing_poc branch from ca26fd3 to 74145dd Compare July 21, 2026 18:53
…ughs

Two small additions to make LIT-4077 easier to test and demo without a real PTU.

AzureCostManagementConfig now accepts management_base_url, defaulting to
https://management.azure.com and reading AZURE_MANAGEMENT_BASE_URL from env. That
enables sovereign clouds (Azure Government uses management.usgovcloudapi.net,
Azure China uses management.chinacloudapi.cn) and, incidentally, lets a local
HTTP fake stand in for Azure during end-to-end tests. The client builds URLs
from the config field instead of the previously hardcoded host.

_build_azure_cost_fetcher_if_enabled now returns a small _DemoAzureCostFetcher
when AZURE_PTU_DEMO_USD is set. The stub returns that USD amount for every
reservation/day so a customer walkthrough can show plausible azure_billing
figures without provisioning a PTU or waiting for Azure's 24-72h reporting lag.
A WARNING fires every rollup so the mode is impossible to miss in production
logs; unsetting the env var restores the real client path.

Tests cover the new config field (default, env override, base URL propagation
into request URLs).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +92 to +94
result = await run_ptu_reservation_rollup(
prisma_client, target_date=target, force=True, azure_fetcher=azure_fetcher
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Backfill tool passes an argument the existing backfill test's stand-in does not accept, breaking that test

The backfill run now always forwards an extra Azure-fetcher argument to the rollup (azure_fetcher=azure_fetcher at scripts/ptu_reservation_backfill.py:92-94), but the existing backfill test replaces the rollup with a stand-in that doesn't accept it, so that test errors out.
Impact: The already-checked-in backfill test fails, blocking CI for this change.

Signature mismatch between new call site and existing test double

scripts/ptu_reservation_backfill.py:87 builds a fetcher and scripts/ptu_reservation_backfill.py:92-94 calls run_ptu_reservation_rollup(prisma_client, target_date=target, force=True, azure_fetcher=azure_fetcher). The pre-existing (unmodified) test tests/test_scripts/test_ptu_reservation_backfill.py:91 defines async def _fake_rollup(prisma, *, target_date, force=False) and monkeypatches it in at :95. Since _fake_rollup has no azure_fetcher parameter and no **kwargs, invoking mod._run(...) at :99 raises TypeError: _fake_rollup() got an unexpected keyword argument 'azure_fetcher', failing test_run_calls_rollup_with_force_true. The PR added the new keyword to the caller but did not update this mapped test's fake to match.

Prompt for agents
The backfill script now calls run_ptu_reservation_rollup with a new keyword argument azure_fetcher (scripts/ptu_reservation_backfill.py lines 92-94). The existing test tests/test_scripts/test_ptu_reservation_backfill.py defines a fake rollup _fake_rollup(prisma, *, target_date, force=False) at line 91 that does not accept azure_fetcher, so test_run_calls_rollup_with_force_true will raise TypeError when _run forwards azure_fetcher. Update that test's _fake_rollup signature to accept azure_fetcher (e.g. add azure_fetcher=None) and optionally assert its value, so the existing test passes with the new call site.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +63 to +71
def _default_token_provider_factory(config: AzureCostManagementConfig) -> TokenProvider:
from litellm.llms.azure.common_utils import get_azure_ad_token_from_entra_id

return get_azure_ad_token_from_entra_id(
tenant_id=config.tenant_id,
client_id=config.client_id,
client_secret=config.client_secret,
scope="https://management.azure.com/.default",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Azure cost lookups against sovereign clouds request a token for the wrong cloud and will fail authentication

The access token is always requested for the public Azure cloud (scope="https://management.azure.com/.default" at litellm/integrations/azure_cost_management/azure_cost_management_client.py:70) even when the operator points the client at a different cloud's endpoint, so cost lookups against that cloud are rejected as unauthorized.
Impact: Operators on sovereign/government Azure clouds cannot fetch PTU billing costs; every call is denied even though a different endpoint was configured.

Base URL is configurable but the token audience is hardcoded to public cloud

AzureCostManagementConfig supports overriding management_base_url via AZURE_MANAGEMENT_BASE_URL (azure_cost_management_client.py:56), and tests test_config_management_base_url_reads_env_override / test_get_daily_cost_hits_management_base_url_from_config explicitly frame this as sovereign-cloud support (e.g. https://management.usgovcloudapi.net). However _default_token_provider_factory (azure_cost_management_client.py:63-71) always passes scope="https://management.azure.com/.default". When the client runs in production with no injected token_provider, the default factory acquires a token whose audience is the public cloud, which the sovereign-cloud Cost Management endpoint will reject with a 401. The scope should be derived from config.management_base_url (e.g. f"{config.management_base_url}/.default").

Suggested change
def _default_token_provider_factory(config: AzureCostManagementConfig) -> TokenProvider:
from litellm.llms.azure.common_utils import get_azure_ad_token_from_entra_id
return get_azure_ad_token_from_entra_id(
tenant_id=config.tenant_id,
client_id=config.client_id,
client_secret=config.client_secret,
scope="https://management.azure.com/.default",
)
def _default_token_provider_factory(config: AzureCostManagementConfig) -> TokenProvider:
from litellm.llms.azure.common_utils import get_azure_ad_token_from_entra_id
return get_azure_ad_token_from_entra_id(
tenant_id=config.tenant_id,
client_id=config.client_id,
client_secret=config.client_secret,
scope=f"{config.management_base_url}/.default",
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

@greptileai review latest head

Comment on lines +63 to +71
def _default_token_provider_factory(config: AzureCostManagementConfig) -> TokenProvider:
from litellm.llms.azure.common_utils import get_azure_ad_token_from_entra_id

return get_azure_ad_token_from_entra_id(
tenant_id=config.tenant_id,
client_id=config.client_id,
client_secret=config.client_secret,
scope="https://management.azure.com/.default",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sovereign cloud token scope is hardcoded to public cloud

_default_token_provider_factory always requests a token with scope "https://management.azure.com/.default", ignoring config.management_base_url. When an operator sets AZURE_MANAGEMENT_BASE_URL=https://management.usgovcloudapi.net (a deliberate feature — there is even a test for the URL override), the API requests go to the sovereign cloud endpoint but carry a token issued for the public cloud audience. Azure will reject these with 401 Unauthorized on every rollup run for that operator.

The scope should be derived from config.management_base_url, e.g. f"{config.management_base_url.rstrip('/')}/.default".

@yucheng-berri

Copy link
Copy Markdown
Contributor Author

Closing as superseded. This is stage 6 of the v1 PTU design, which put PTU config on a separate reservation table. The shipped design puts it on the model deployment instead (#35341, #35343, #35391, #35393, #36829), so this branch is built on a table that no longer exists.

The idea here (read real accrued cost from the Azure Cost Management API instead of computing ptu_count x cost_per_ptu_per_hour) never shipped and is still open as a follow-up; it would need rewriting onto the model. Branch litellm_lit4077_stage6_azure_billing_poc is kept.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants